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

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

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

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

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

Interactive: Material Properties

Ground: Rough, matte
Red: Smooth plastic
Gold: Metallic, shiny
Blue: Glossy, reflective

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

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

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

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

Color

Box

Color

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

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 mod creates infinite geometry from a single shape
  • Space folding with abs creates symmetric and fractal-like structures
  • Bounding volumes and early termination optimize performance
  • These techniques combine freely to create rich, expressive scenes