Building From Bedrock 1K shader code

This is the exhaustively documented shader code of the Building From Bedrock 1 kilobyte intro. Remember, don’t try this at work!

Feedback welcome at the Pouet page !

//
//  Building From Bedrock 1K / Fulcrum
//
// This is the shader part of our 1K intro that won 1st place at the
// Assembly Summer 2024 1K intro competition. To understand this code,
// you'll need basic GLSL shader knowledge, and how ray-marching
// SDFs (signed distance fields) works. A good starting point is
// "Rendering worlds with 2 triangles:
// https://iquilezles.org/articles/nvscene2008/rwwtt.pdf
//
// Apart from whitespace, this is the original code in the intro, including
// all minifications and variable renaming. There are nuances that get lost
// in un-minified code. The code only runs on Nvidia, since AMDs new shader 
// compiler is very pedantic and considers a lot of things errors that used
// to be warnings...

// We pass the time (more specifically, the current frame number) in gl_Color.x,
// and rely on truncation to skip the .x . The v variable is the total distance
// traveled on the current ray.
float c=gl_Color*170,v=0;

// This pseudo-FBM (Fractal Brownian Motion) function is used for everything:
// terrain, material choices, textures, clouds, rain,... It returns a positive
//  value for any 3D point, varying smoothly over location.
float e(vec3 r)
{
  // f is the number of octaves, like "layers" of noise that we add together.
  // The more layers, the more rough the output becomes, but also the slower the
  // intro runs, as this is calculated hundreds of times per pixel.
  // g is the total return value.
  float f=0,g=0;
  for(;f<3;f++) // 3 octaves of noise is enough
  {
    // Here we scale our 3D space. 
    // The smoothstep gradually increases the noise from 0 at the start of the
    // intro, until the time the lava solidifies.
    // The pow() almost doubles the frequency of each noise layer, while the other
    // pow() a few lines below halves the height of the noise. It is actually a
    // really bad idea to use pow() for this, as it is very slow. The correct way
    // is to use 2 variables for frequency and height, and multiply them by about 2
    // or about 0.5 respectively. For 3 octaves, evaluating 6 pow()s instead of 6
    // multiplications make the intro run at least 30% slower! But using pow() is
    // smaller as we don't need variable definitions, and for a 1K, size is more
    // important than speed. Just don't use this in production code!
    vec3 y=r*smoothstep(0,3,c)*pow(1.9,f);

    // Add some 3D sine noise to our 3D space
    y.zxy+=sin(y*1.13)*1.63;

    // Evaluate the 3D space at 2 positions (with and without .yzx swizzle),
    // add them up, scale by the layer height (using pow() again), and add to
    // the total. I feel this could be simplified probably, but at the time
    // I was finishing the intro, I couldn't change this without changing
    // every other constant in the intro as well, which I didn't have time for.
    g+=.44*length(sin(y.yzx)+cos(y))*pow(.42,f);

    // Between every noise layer, rotate 3D space so the layers don't correlate
    // too much.
    r=r.yzx;
  }
  return g;
}

// This is the pseudo-SDF for the terrain. It's not a real safe SDF, 
// you might get clipping at some points, but it looks good enough for a 1K.
float n(vec3 y)
{
  // This is the "Blockify"-function which turns a 3D surface into a blocky
  // approximation. Combining the -1 and +.07 compresses worse than keeping
  // them separate and in this order.
  y=floor(y)*.07+max(-1+fract(y)+.07,0);

  // The y.y is a horizontal plane. Modify it with the noise function, and
  // adjust for acceptable height.
  return e(y)+y.y-2;
}

void main()
{
  // Store the 2D pixel coordinate for the rain.
  vec3 s=gl_FragCoord, 
       // our current location on the ray.
       y, 
       // Convert our 2D pixel into a 3D view direction. The constants
       // work only for 1280*720 screen resolution. You typically normalize
       // at this point, but we'll do that later.
       r=vec3(s.xy*.0015-vec2(1,.55),1),
       // The camera sways from left to right, and goes forward at an
       // exponential rate. Again, the order of multiplications is needed
       // for best compression.
       z=vec3(-50*cos(c*2),10,c*3*c);
  
  // This rotates the view direction left and right, using half of the
  // classic rotation matrix. For small rotations, the cos() part can be,
  // ignored, but at higher angles you get visual distortion
  r.xz-=vec2(r.z,-r.x)*.6*sin(c);
  // pseudo-rotate the camera a little bit down.
  r.yz-=.2;

  // To make the camera follow the terrain smoothly, we evaluate the noise
  // function directly, with some scaling factors matching that of the SDF,
  // and raise or lower the camera. If we would use the SDF function n()
  // directly, the camera would follow the blocks of the terrain and it would
  // look like you're falling down stairs nonstop, which is very annoying to watch.
  // The -7 at the end is a safety margin, because our SDF is not a real safe
  // one and we clip into terrain without it.
  z.y+=e(-z*.07)/.07-7;

  // For the rain, stretch the 2D screen vertically and scroll it down quickly
  s.y=s.y*.3+c*3000;

  // f is the counter of raymarch steps, g is the distance to the nearest surface.
  // This line is an exact copy of the first line of the noise function, which
  // compresses better.
  float f=0,g=0;

  // The raymarching loop. I had to manually change the variables so both
  // for() loops use the same name, this is something shader minifiers don't
  // always get right. Check their output!
  // Instead of marching until the distance almost 0, we march till it's slightly
  // negative, due to the not-really-safe SDF.
  for(;f<600&&-.07<g;f++)
      // We're doing a lot of things here:
      // - normalize the ray direction (slower to do this 600 times, but smaller)
      // - find the current point y on the ray
      // - find the current distance g to the terrain SDF
      // - and update v, the total distance traveled on the ray. 
    v+=g=n(y=v*normalize(r)+z);

  // Get one texel of the texture by using the FBM function on a quantized current position
  g=e(floor(y*13));

  // Now we are done marching, let's color this pixel
  // This is done by having a base color (stone, ground, sand, grass, sky) and modify it
  // with several effects (rain, lava, water)
  gl_FragColor.xyz=
      // Add the rain. Instead of mixing colors properly, we lower red & green and add blue
      vec3(-.2,-.2,.1)*
      // Use the FBM function to get a wavy 2D pattern
      e(-s*.1)
      // This starts and stops the rain at the right times
      *smoothstep(5,7,c)*smoothstep(9,8,c)
      
      // Then, figure out what we hit, and color accordingly
      +( n(y)<1? // Did we hit something, or are we looking at sky?

          // We hit ground, which is all lava at the start. No proper mixing again, just
          //  add a lot of red, remove blue, and use the texture for green. When the lava
          // cools, this factor becomes 0.
         vec3(.8,g*.2,-.3)*smoothstep(4,3,c)

           // Did we hit the rising water?
          + (y.y<8.8*smoothstep(6,7,c)?
              // Yes: same add blue, remove red&green trick, but more intense if we are
              //  deeper. This looks more realistic than a single color for all depths.
              vec3(-.2,-.2,.1)*(6-y.y*.6) 
              :0) // No: keep color of whatever we will hit.
          
          // Start without textures, and make them appear at the start of the intro
          +g*.2*smoothstep(0,1,c)

          // Due to our limited amount of steps, there are big artifacts causing
          // holes in the terrain. To fix this, we have an intermediate stage between
          // "Ground hit" and "Sky hit" that we fill with a grayish color, that we do
          // not calculate lighting for. It makes the raymarch artifacts far less
          // noticable.
          +(n(y)<.01? 
              // Here we are using a tiny lighthack calculation, instead of the usual
              // approach with 3 or 6 evaluations for a normal etc. We evaluate the
              // distance to the current position, but with a tiny shift, and use that
              // as light intensity. The vector for the light direction is similar to
              // one of the colors, for better compression.
              .2+20*n(vec3(1,.9,-.6)*.01+y)
              :vec3(.1)) // dark gray for artifact hiding

            // Have we hit stone, or something else? The floor ensures the answer is the
            // same per block. The smoothstep makes it appear gradually, although I'm not
            // sure how much this is still visible after I added the lava
            *(e(floor(y)*.2)<3-2.2*smoothstep(1,5,c)?
                vec3(.4) // Grey stone

                // If not stone, is it sand? Sand appears gradually, rising a tiny bit
                // above the final water level. 
                :y.y<9.01*smoothstep(7,8,c)?
                  vec3(1,.9,.6) // yellow sand

                  // If not sand, it's ground, but ground has grass on top of it, if it
                  // is the top layer (not 100% accurate though). Evaluate the SDF field
                  // n() at a slightly higher point, and if it is empty, we show grass. The
                  // smoothstep grows the grass, the texture intensity g is abused to cause
                  // a ragged edge, and the call to e() is to grow the grass in blocky 
                  // clusters, not everywhere at once but not per fractional block either.

                  :.005<n(vec3(0,-.1+g*.3*smoothstep(12,13,c+2*e(floor(y)*.2)),0)+y)?
                    vec3(.2,.7,0) // Green grass
                    :vec3(.6,.3,0) // Brown ground

        // We're not even close to having hit something, so show the sky, possibly with clouds.
        // In fact, the sky starts as full of clouds, and they disappear during the rain sequence.
        // Instead of using the current position y, we simply work with the ray direction r, which
        // we project on a flat plane of clouds by the r.y division. The FBM function n() creates
        // cloudy shapes, the floor() makes them blocky
        :(n(floor(r/r.y*2.1)*10)>smoothstep(5,11,c)?
            vec3(.6) // Gray clouds look better then white ones.
            // blue sky, but with a vertical gradient. 
            // Multiply with the time to go from night to noon
            :vec3(.5,.6,.9)-r.y)*.1*c);
}