Advanced Raymarching
Reflections, materials, and scene composition
You have learned to march rays through distance fields, to compute normals, to light surfaces with diffuse and specular terms, to cast soft shadows, and to simulate atmospheric effects. These techniques alone can produce compelling images.
But raymarching has more to offer. In this final chapter, we explore the techniques that elevate simple scenes into rich, expressive worlds.
Reflections
When light hits a smooth surface, it bounces. The angle of reflection equals the angle of incidence, mirrored around the surface normal. In GLSL, the reflect function handles this:
vec3 reflectedDir = reflect(rayDirection, surfaceNormal);To render reflections, we simply march a new ray from the hit point in the reflected direction:
vec3 reflectDir = reflect(rd, normal);
float reflT = rayMarch(hitPoint + normal * 0.01, reflectDir);
if (reflT > 0.0) {
vec3 reflP = hitPoint + reflectDir * reflT;
vec3 reflN = getNormal(reflP);
reflColor = shade(reflP, reflN, reflectDir);
}The offset along the normal (hitPoint + normal * 0.01) prevents the ray from immediately hitting the surface it started on.
Interactive: Reflections
Watch how the floor reflects the colored spheres, and how additional bounces create reflections of reflections
Notice how the floor picks up colors from the spheres above it. Reflections create visual connections between objects, making scenes feel cohesive rather than like isolated props on a stage.
Multiple Bounces
One level of reflection is nice. Two levels reveal reflections of reflections. But there is a catch: each bounce requires another full raymarch, and performance degrades quickly.
In practice, two or three bounces suffice for most scenes. Beyond that, the contribution to the final image becomes imperceptible while the computational cost keeps growing.
The implementation is straightforward iteration:
vec3 color = shade(p, n, rd);
vec3 currentDir = rd;
vec3 currentPos = p;
vec3 currentNormal = n;
for (int bounce = 0; bounce < maxBounces; bounce++) {
vec3 reflDir = reflect(currentDir, currentNormal);
float t = rayMarch(currentPos + currentNormal * 0.01, reflDir);
if (t < 0.0) {
color = mix(color, skyColor, reflectivity);
break;
}
currentPos = currentPos + reflDir * t;
currentNormal = getNormal(currentPos);
vec3 bounceColor = shade(currentPos, currentNormal, reflDir);
color = mix(color, bounceColor, reflectivity);
currentDir = reflDir;
}The reflectivity factor controls how much each bounce contributes. Metals have high reflectivity. Plastic and wet surfaces have moderate reflectivity. Matte surfaces have almost none.
Material IDs
So far, we have colored objects based on their position. But real scenes have distinct materials: a red plastic ball, a chrome sphere, a wooden floor. We need a way to identify which object each ray hit.
The solution is to return both distance and a material identifier from the SDF:
struct Hit {
float dist;
int matId;
};
Hit sceneSDF(vec3 p) {
Hit result;
result.dist = 1e10;
result.matId = 0;
float sphere = sphereSDF(p, center, radius);
if (sphere < result.dist) {
result.dist = sphere;
result.matId = 1;
}
float box = boxSDF(p, size);
if (box < result.dist) {
result.dist = box;
result.matId = 2;
}
return result;
}After marching to a surface, the material ID tells us which shading parameters to use:
Material getMaterial(int id) {
if (id == 1) return Material(vec3(0.9, 0.2, 0.2), 0.3, 0.0);
if (id == 2) return Material(vec3(1.0, 0.76, 0.33), 0.2, 1.0);
return Material(vec3(0.5), 0.5, 0.0);
}Interactive: Material Properties
Each shape carries a material ID that determines its color and surface properties
Each object now has its own color, roughness, and metallic properties. The gold box reflects its surroundings differently than the matte red sphere.
Domain Repetition
One of raymarching's most powerful tricks is domain repetition. By wrapping coordinates with mod, we create infinite copies of our geometry from a single shape definition:
vec3 repeatXZ(vec3 p, float spacing) {
p.xz = mod(p.xz + 0.5 * spacing, spacing) - 0.5 * spacing;
return p;
}
float sceneSDF(vec3 p) {
vec3 repeated = repeatXZ(p, 2.0);
return sphereSDF(repeated, vec3(0, 0.5, 0), 0.5);
}The mod operation folds space so that every point maps to a canonical cell. A single sphere definition becomes an infinite grid of spheres.
Interactive: Domain Repetition
Domain repetition creates infinite patterns from a single shape definition
This technique costs nothing extra in terms of geometry. The SDF still evaluates a single sphere, but that sphere appears everywhere. Combined with fog, you can create endless landscapes that fade into the distance.
Folding Space
Related to repetition is space folding. Instead of wrapping coordinates, we reflect them:
p.x = abs(p.x);This simple operation mirrors everything across the YZ plane. Apply it before evaluating the SDF, and a shape on one side appears on both sides.
Repeated folding creates kaleidoscopic effects:
for (int i = 0; i < iterations; i++) {
p = abs(p);
p -= offset;
p *= scale;
}Each iteration doubles the symmetry. A few iterations transform simple shapes into intricate fractal-like structures.
Interactive: Space Folding
Folding space with abs() creates kaleidoscopic patterns from simple shapes
Folding is the foundation of many fractal SDFs, including the famous Mandelbulb and various IFS (Iterated Function System) fractals. The recursive nature of folding creates complexity from simplicity.
Putting It All Together
Here is a scene editor that combines everything we have covered: positioning objects, assigning materials, controlling reflections and shadows. Play with it. Move things around. Change colors. See how the techniques interact.
Interactive: Scene Editor
Sphere
Box
This is the essence of raymarching: a small set of mathematical operations, combined thoughtfully, producing rich visual results. No mesh data, no texture maps, just pure computation.
Optimization Techniques
Raymarching can be expensive. Each pixel might require dozens of SDF evaluations. Complex scenes with multiple bounces multiply this cost further. Several techniques help keep performance manageable.
Bounding volumes skip the detailed SDF for rays that clearly miss. Before evaluating your complex scene, check against a simple sphere or box that encloses it:
float boundingT = intersectBoundingSphere(ro, rd, sceneCenter, sceneRadius);
if (boundingT < 0.0) return skyColor;
t = max(0.0, boundingT - margin);Early termination stops marching when the ray has clearly escaped the scene. If the SDF returns a large value and we are already far from the origin, there is nothing left to hit.
Adaptive step size takes larger steps when the SDF indicates we are far from any surface, and smaller steps as we approach. The standard march already does this naturally, but you can tune the clamping for your specific scene.
LOD (Level of Detail) simplifies the SDF for distant objects. A complex shape far away can be approximated by a sphere without visible difference.
Interactive: Raymarching Step Visualization
Toggle step visualization to see where the raymarch spends its effort
The step visualization reveals where computational effort goes. Areas near surfaces require many small steps. Open space can be crossed quickly. Optimization is about spending effort where it matters.
Where to Go From Here
You have learned the fundamentals of raymarching: the algorithm, 3D SDFs, boolean operations, shading, shadows, reflections, materials, domain manipulation, and optimization. These building blocks combine into infinite possibilities.
Here are some directions to explore:
Fractals. The Mandelbulb, Sierpinski shapes, and IFS fractals are all raymarched with SDFs. The math is more involved, but the rendering techniques are identical.
Volumetric effects. Instead of marching to surfaces, accumulate density along the ray. This produces clouds, smoke, and god rays.
Animation. Everything we have done is a function of position. Add time as a parameter, and shapes morph, rotate, and dance.
Post-processing. Apply bloom, color grading, vignette, and other effects to the rendered image. Small touches add polish.
Larger scenes. Combine domain repetition with material variation. Add procedural detail. Build worlds.
The techniques in this course appear in countless creative projects, from demoscene productions to music visualizers to video game effects. They run efficiently on consumer GPUs and produce results that rival complex 3D engines.
Most importantly, raymarching rewards experimentation. Change a constant. Swap a min for a max. Add a sine wave. The feedback loop between code and visual result is immediate and addictive.
Go make something beautiful.
Key Takeaways
- Reflections use
reflect(rd, normal)and another raymarch from the hit point - Multiple bounces require iteration, but two or three suffice for most scenes
- Material IDs distinguish objects for different shading properties
- Domain repetition with
modcreates infinite geometry from a single shape - Space folding with
abscreates symmetric and fractal-like structures - Bounding volumes and early termination optimize performance
- These techniques combine freely to create rich, expressive scenes