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);
}