3D SDF Primitive Gallery
by GameDev.net · GLSL ES 3.00 (WebGL2) · 25 Aug 2026
Run the shader to adjust these controls.
What it demonstrates
Once the marching loop works, adding shapes is adding distance functions. This shader gathers the four primitives most scenes are built from, a sphere, box, torus and cylinder, and puts them on a ground plane under one light. It is also the first package to use a Common pass: the camera, the march loop and the normal estimate now live there, so the Image pass contains only the scene.
Shader inputs
void mainImage(out vec4 fragColor, in vec2 fragCoord)
Called once per pixel. Write the colour to fragColor.
-
iResolutionvec3 - Viewport size in pixels (z is the pixel aspect ratio).
-
iTimefloat - Seconds since the shader started.
-
iTimeDeltafloat - Seconds since the previous frame.
-
iFrameRatefloat - Frames per second, smoothed.
-
iFrameint - Frames rendered since the start.
-
iMousevec4 - Mouse position: xy while held, zw of the last click.
-
iDatevec4 - Year, month, day, and seconds within the day.
-
iChannel0sampler2D - Texture bound to channel 0.
-
iChannel1sampler2D - Texture bound to channel 1.
-
iChannel2sampler2D - Texture bound to channel 2.
-
iChannel3sampler2D - Texture bound to channel 3.
-
iChannelResolutionvec3[4] - Pixel size of each bound channel texture.
-
iChannelTimefloat[4] - Playback time of each channel, in seconds.
-
iSampleRatefloat - Audio sample rate, always 44100.
Common4
// The Common pass is prepended to the Image pass before compilation, so this
// is shared source rather than a second shader. The scene itself is declared
// here and defined in the Image pass, which is where the lesson lives.
float mapScene(vec3 position);
vec3 cameraRay(vec2 fragCoord, vec3 origin, vec3 target, float lens) {
vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;
vec3 forward = normalize(target - origin);
vec3 right = normalize(cross(vec3(0.0, 1.0, 0.0), forward));
vec3 up = cross(forward, right);
return normalize(forward * lens + right * uv.x + up * uv.y);
}
float marchScene(vec3 origin, vec3 direction, float maxDistance) {
float travelled = 0.0;
for (int i = 0; i < 96; i++) {
vec3 position = origin + direction * travelled;
float dist = mapScene(position);
if (dist < 0.001) return travelled;
travelled += dist;
if (travelled > maxDistance) break;
}
return -1.0;
}
vec3 sceneNormal(vec3 position, float epsilon) {
vec2 offset = vec2(epsilon, 0.0);
return normalize(vec3(
mapScene(position + offset.xyy) - mapScene(position - offset.xyy),
mapScene(position + offset.yxy) - mapScene(position - offset.yxy),
mapScene(position + offset.yyx) - mapScene(position - offset.yyx)));
}
vec3 skyColor(vec3 direction) {
return mix(vec3(0.05, 0.07, 0.12), vec3(0.16, 0.22, 0.34), direction.y * 0.5 + 0.5);
}
Main Image4
uniform int uShape; // @param 0..3 = 2 "Shape"
uniform float uSpin; // @param 0.0..6.28 = 0.9 "Spin offset"
uniform float uCameraHeight; // @param -0.4..2.5 = 0.9 "Camera height"
float sdSphere(vec3 p, float radius) {
return length(p) - radius;
}
float sdBox(vec3 p, vec3 halfSize) {
vec3 q = abs(p) - halfSize;
return length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0);
}
float sdTorus(vec3 p, float major, float minor) {
vec2 ring = vec2(length(p.xz) - major, p.y);
return length(ring) - minor;
}
float sdCylinder(vec3 p, float height, float radius) {
vec2 d = abs(vec2(length(p.xz), p.y)) - vec2(radius, height);
return min(max(d.x, d.y), 0.0) + length(max(d, 0.0));
}
float mapScene(vec3 position) {
float angle = iTime * 0.35 + uSpin;
float c = cos(angle);
float s = sin(angle);
vec3 p = vec3(c * position.x - s * position.z, position.y, s * position.x + c * position.z);
float shape;
if (uShape == 0) {
shape = sdSphere(p - vec3(0.0, -0.20, 0.0), 0.85);
} else if (uShape == 1) {
shape = sdBox(p - vec3(0.0, -0.43, 0.0), vec3(0.62));
} else if (uShape == 2) {
shape = sdTorus(p - vec3(0.0, -0.77, 0.0), 0.75, 0.28);
} else {
shape = sdCylinder(p - vec3(0.0, -0.30, 0.0), 0.75, 0.55);
}
float ground = position.y + 1.05;
return min(shape, ground);
}
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec3 origin = vec3(0.0, uCameraHeight, -3.4);
vec3 direction = cameraRay(fragCoord, origin, vec3(0.0, -0.25, 0.0), 1.6);
float hit = marchScene(origin, direction, 20.0);
if (hit < 0.0) {
fragColor = vec4(skyColor(direction), 1.0);
return;
}
vec3 position = origin + direction * hit;
vec3 normal = sceneNormal(position, 0.0015);
vec3 toLight = normalize(vec3(0.7, 0.9, -0.6));
float diffuse = max(dot(normal, toLight), 0.0);
float ground = smoothstep(0.05, -0.05, position.y + 1.04);
vec3 albedo = mix(vec3(0.85, 0.55, 0.28), vec3(0.30, 0.32, 0.36), ground);
vec3 color = albedo * (0.14 + 0.86 * diffuse);
color = mix(skyColor(direction), color, exp(-0.012 * hit * hit));
fragColor = vec4(color, 1.0);
}
Learn from this shader
How it works
The Common pass is prepended to the Image pass before compilation, so the two files are one shader, not two stages. That is why it can declare the scene as a prototype and let the Image pass define it: shared machinery calls into per project content without knowing anything about it. Each primitive follows the pattern of its 2D counterpart, with the box clamping per axis distances and the torus collapsing the horizontal plane to a radius so it becomes a circle problem. The scene rotates by rotating the sample point, and the ground joins with a union. Distance fog hides the hard horizon.
Try changing
Step Shape and watch the same lighting and shadow terminator behave differently on flat faces than on curves. Move Camera height down to graze the plane, where marching costs the most steps. In a fork, replace the union with a smooth minimum so the shape melts into the floor, or return the shape index alongside the distance to give each primitive its own material.
Using it in a game
These four functions plus rounding and blending cover most props a raymarched scene needs. In a mesh renderer the same primitives are useful as analytic colliders, trigger volumes, and decal projectors, where a distance query is cheaper and more stable than intersecting geometry.
Explore the techniques
Continue with curated explanations and progressively related examples.
Discussion