I was thinking about procedurally generating mosaic images to use as a title screen for FreeBuild, and implemented a basic shader to do just that. It essentially works as a wrapper around any other fragment shader, but provides a lowres virtual display, and then overlays a stud pattern on each virtual pixel. You can see it in action here running as a wrapper around Shaun's plasma shader.

Low quality screen grab here:
Made my own plasma routine to make it more closely resemble the classic FreeBuild title screen.



(not sure why Youtube keeps my crap-quality videos from my huge screengrabs...)
Wow, that's looking extremely cool! And what a great idea for a background for the Freebuild main menu.
Yeah, I think if I just slow down the timer a little, it would make a great main menu. And..it seems as the Youtube just hadn't finished processing the higher-res versions of the video, as they're available now.

Anyway, the easily reusable stud routines are here:


const float studRad = 0.3;
const float studHeight = 0.2;
const float studBorder = 0.03;
float yGrid = iResolution.y / 20.;
const vec2 halfXY = vec2(0.5, 0.5);
vec2 gridRes = vec2(iResolution.x/iResolution.y * yGrid,yGrid);
vec2 scaledUv;
vec2 gridC;

// Photoshop/GIMP style multiplicative-ish style filter.
float blend(float a, float b){
    return (a < 0.5) ? 2.*a*b : (1. - 2.*(1.-a)*(1.-b));

vec4 blend(vec4 a, vec4 b){
    return vec4(blend(a.x, b.x),
                blend(a.y, b.y),
                blend(a.z, b.z),

// Chunks up the screen into blocks.
vec2 baseXY(vec2 uv) {
   scaledUv = uv*gridRes;
    gridC = floor(scaledUv);
    return (gridC / gridRes);
// Essentially convolves a virtual pixel with the stud pattern.
vec4 brickify(vec4 baseColor) {
    vec2 subGrid = scaledUv - gridC - halfXY;
    float rad = length(subGrid);
    float lightFactor = smoothstep(-studRad, studRad, subGrid.y);
    float pixelsPerGrid = iResolution.x / gridRes.x;
   vec4 borderColor = vec4(lightFactor, lightFactor, lightFactor, (abs(rad - (studRad - 0.5*studBorder)) <= 0.5*studBorder) ? 0.5*clamp(pixelsPerGrid*(0.5 * studBorder - abs(rad - (studRad - 0.5*studBorder))), 0., 1.) : 0.);
    float rightFactor = 0.3;// mix(0.5, 0, (subGrid.y + 0.5));
    vec4 rightColor = vec4(rightFactor, rightFactor, rightFactor, (0.5 - subGrid.x) <= studBorder ? 0.3 : 0.);
    float bottomFactor = 0.3;
    vec4 bottomColor = vec4(bottomFactor, bottomFactor, bottomFactor, (0.5 + subGrid.y) <= studBorder ? 0.3 : 0.);
    vec4 fragColor = vec4(0.5,0.5,0.5,1);//baseColor;
    fragColor = mix(fragColor, borderColor, borderColor.w);
    if(abs(subGrid.x) <= studRad - 1./pixelsPerGrid && subGrid.y <= 0.){
        float angle = acos(subGrid.x / studRad);
        float yInt = -sin(angle) * studRad;
        float vFac = 0.5*smoothstep(0., studHeight, (yInt - subGrid.y) * 1.5*exp(-pow(subGrid.x,2.)));
        float sFac = vFac;
        vec4 shadowColor = vec4(sFac, sFac, sFac, subGrid.y <= yInt ? 1. : clamp(1. - pixelsPerGrid*abs(rad - studRad), 0., 1.));
        fragColor = mix(fragColor, shadowColor, 0.5*shadowColor.w);
    fragColor = mix(fragColor, rightColor, rightColor.w);
    fragColor = mix(fragColor, bottomColor, bottomColor.w);
    fragColor = blend(baseColor, fragColor);

    return fragColor;

The use is pretty straightforward. There a few parameters at the top governing the appearance of each block. iResolution is a uniform vec2 containing the real pixel size of the screen. Remap your UV coordinates through baseXY to get virtual pixels, which also sets up a few parameters used by brickify, which you call at the end with your output color.

The plasma code basically just makes moving swirly bits:

    float time = iGlobalTime * 5.;
    uv = uv - halfXY;
    uv += vec2(sin(time / 47.), cos(time / 37.));
    float theta = atan2(uv.x, uv.y);
    float r = length(uv);

    vec2 rg = vec2(0.5 + 0.5*cos(theta+time / 37. + sin(7.*r + time / 5.)), 0.5 + 0.5*sin(theta+time / 13. + cos(11.*r + time / 17.)));
    vec2 uvN = sqrt(abs(uv - rg));
    float thetaN = atan2(uvN.y, uvN.x);
    float rN = length(uvN);
    rg = rg*halfXY + halfXY * vec2(rg.x + pow(cos(thetaN + sin(rN + time / 5.) + time / 43.),2.), rg.y + pow(sin(thetaN + cos(rN + time / 11.) + time / 31.),2.));
    rg *= vec2(abs(sin(rg.x * 17. / 5.)), abs(cos(rg.y * 23. / 3.)));
    float thetaR = atan2(rg.x, rg.y);
    float rgM = length(rg);
    rg = halfXY * (rg + (halfXY+halfXY*vec2(sin(thetaR * rgM)*cos(thetaR * rgM),cos(thetaR + time / 47.)*sin(rgM))));
    rg = vec2(mix(sqrt(rg.x),rg.x*rg.x,clamp(rg.y - rg.x,0.,1.)),mix(sqrt(rg.y),rg.y*rg.y,clamp(rg.x - rg.y,0.,1.)));
    vec4 baseColor = vec4(rg * rg, 0.2,1.0);

And relies on this utility routine:

const float pi = 3.1415926535897;
float atan2(in float y, in float x)
    bool s = (abs(x) > abs(y));
    return mix(pi/2.0 - atan(x,y), atan(y,x), s ? 1. : 0.);
Thanks for explaining! Other than the plasma code, which I'd have to spend a lot more time with to understand, that makes sense. Have you considered clamping the colors to the nearest "official" LEGO color? I think that might make it look even more convincing.
I hadn't thought to snap to a palette, but in genera I think that might be hard to do efficiently in GLSL, especially with the limitations imposed by ShaderToy.

The plasma starts with a time-varying polar function, about a time-varying centroid, giving red/green color values. This basic function is a wavy-armed spiral bouncing around. It's then combined with another weird function that mixes the red/green and spatial values, and then a few more sinusoidal patterns are layered on for good measure. All of the time varying oscillations use primes to scale their frequency to prevent them from syncing up at regular intervals and being obviously repetitious (this is an important principle for a lot of texture synthesis applications - make your waves have differing prime frequencies).

Finally, I use sqrt(x) and x^2 as sort of soft floor/ceiling functions, which works well in the 0-1 interval since repeated squaring converges to 0 and repeated square rooting converges to 1. These are blended together, in opposite directions for each color component, based on which color is dominant. This emphasizes greens and reds and squashes the amount of yellow.
Register to Join the Conversation
Have your own thoughts to add to this or any other topic? Want to ask a question, offer a suggestion, share your own programs and projects, upload a file to the file archives, get help with calculator and computer programming, or simply chat with like-minded coders and tech and calculator enthusiasts via the site-wide AJAX SAX widget? Registration for a free Cemetech account only takes a minute.

» Go to Registration page
Page 1 of 1
» All times are UTC - 5 Hours
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum