Raymarching
Draw a 3D scene with no triangles. Shoot a ray per pixel and step along it by the distance to the nearest surface.
How it works
The usual pipeline starts from triangles: the vertex shader places them, the rasterizer finds the pixels they cover,
and the fragment shaderThe GPU program that runs once for every pixel a triangle covers and decides its color. Fragment shader The GPU program that runs once for every pixel a triangle covers and decides its color.
The scene isn’t a mesh. It is a function.
Scenes as distance functions
A signed distance function (SDF) takes a point in space and returns how far that point is from the nearest surface:
positive outside, negative inside, zero exactly on it. A sphere of radius r at the origin is one line:
float sdSphere(vec3 p, float r) {
return length(p) - r;
}
Boxes, tori and capsules have short formulas too, and they combine: the min of two distances is both shapes at once,
the max is where they overlap. A whole scene becomes one function, usually called map(p).
Sphere tracing
The distance is what makes the search fast. If map(p) returns 2.0, nothing in the scene is closer than 2.0 to p,
in any direction. So the ray can jump 2.0 forward without passing through anything. At the new point, ask again and
jump again. Far from everything the steps are long; as the ray nears a surface they shrink, and when the distance
falls below a small threshold, the ray has hit.
#version 300 es
precision highp float;
uniform vec2 uResolution;
out vec4 fragColor;
float map(vec3 p) {
return length(p) - 1.0; // one unit sphere at the origin
}
void main() {
// a camera at z = 3 looking toward -z, one ray per pixel
vec2 p = (2.0 * gl_FragCoord.xy - uResolution) / uResolution.y;
vec3 origin = vec3(0.0, 0.0, 3.0);
vec3 dir = normalize(vec3(p, -1.5)); // -1.5 sets the field of view
float t = 0.0; // distance travelled along the ray
bool hit = false;
for (int n = 0; n < 96; n++) {
float d = map(origin + dir * t);
if (d < 0.001) { hit = true; break; }
t += d;
if (t > 20.0) break; // left the scene
}
vec3 color = vec3(0.05);
if (hit) color = vec3(1.0);
fragColor = vec4(color, 1.0);
}
This draws a white disc on a dark background: the sphere, found, but not yet shaded.
Normals from the gradient
Lighting needs the surface normalA unit vector perpendicular to a surface, pointing the way it faces. Lighting and Fresnel effects compare it with other directions. Normal A unit vector perpendicular to a surface, pointing the way it faces. Lighting and Fresnel effects compare it with other directions.map increases,
its gradient, is the normal. Sampling map a tiny step either side on each axis estimates it:
vec3 normalAt(vec3 p) {
const vec2 e = vec2(0.001, 0.0);
return normalize(vec3(
map(p + e.xyy) - map(p - e.xyy),
map(p + e.yxy) - map(p - e.yxy),
map(p + e.yyx) - map(p - e.yyx)
));
}
That costs six more calls to Dot product Multiplies two vectors component by component and adds the results. For unit vectors it is the cosine of the angle between them, from 1 (same direction) to -1 (opposite).map. A cheaper version samples the four corners of a tetrahedron instead. With a normal
and a light direction, the dot productMultiplies two vectors component by component and adds the results. For unit vectors it is the cosine of the angle between them, from 1 (same direction) to -1 (opposite).dot(a, b) = |a| |b| cos θ
vec3 n = normalAt(origin + dir * t);
float diffuse = max(dot(n, normalize(vec3(0.6, 0.8, 0.4))), 0.0);
What it costs
Every pixel runs the loop, and every step evaluates the whole scene. Cost is pixels × steps × the price of map. Rays
that skim past a surface without touching it are the expensive ones: the distance stays small, so the steps stay small.
The usual levers are a step limit, a hit threshold that grows with distance, a maximum ray length, and a lower
rendering resolution.
The method also depends on map telling the truth. A function that overestimates the distance (common after twisting
or bending space) makes the ray jump past thin surfaces, which then show holes. Scaling every step by 0.5 to 0.9 trades
speed for safety.
What it buys is just as real. Soft shadows and ambient occlusion come from a few more marches toward the light or along
the normal. Infinite repetition is a mod on the input point. Shapes melt into each other with a smooth min. None of
that is easy with triangles.
Meeting a mesh scene
On its own, a raymarched shape doesn’t know about the meshes around it. To let them hide each other, the shader writes
the hit’s depth (gl_FragDepth in GLSL, DEPTH in Godot) so the depth test can compare it with the rest of the scene.
Without that, raymarching suits full-screen demos, backgrounds, and effects inside a box or quad.
In Godot
Any shader can loop, so the same code works in a canvas_item shader on a ColorRect (2D, full screen) or a
spatial shader on a cube, where the ray starts at the camera and passes through each covered pixel. In the spatial
case, writing DEPTH lets the result sit inside a 3D scene.
On this site
The Sphere–box smooth blend card is a full-screen raymarcher. Its inputs are UV and time; it also reads
i.resolution for the aspect ratio, and builds the camera ray the same way, starting from i.uv instead of
gl_FragCoord.