Cogs and Levers A blog full of technical stuff

Raymarching Reflections

Introduction

Ray tracing is known for producing stunning reflections, we can achieve the same effect using ray marching. In this post, we’ll walk through a classic two-sphere reflective scene, but instead of traditional ray tracing, we’ll ray march our way to stunning reflections.

This post builds on previous articles:

Let’s get started!

Setup

The first step is defining a scene with two spheres and a ground plane. In ray marching, objects are defined using signed distance functions (SDFs). Our scene SDF is just a combination of smaller SDFs.

SDFs

The SDF for a sphere gives us the distance from any point to the surface of the sphere:

float sdfSphere(vec3 p, vec3 center, float radius) {
    return length(p - center) - radius;
}

The SDF for a ground plane:

float sdfGround(vec3 p) {
    return p.y + 1.5;  // Flat ground at y = -1.5
}

Finally, we combine the objects into a scene SDF:

float sceneSDF(vec3 p) {
    float sphere1 = sdfSphere(p, vec3(-1.0, 0.0, 3.0), 1.0);
    float sphere2 = sdfSphere(p, vec3(1.0, 0.0, 3.0), 1.0);
    float ground = sdfGround(p);
    return min(ground, min(sphere1, sphere2));
}

Raymarching

Now we trace a ray through our scene using ray marching.

vec3 rayMarch(vec3 rayOrigin, vec3 rayDir, int maxSteps, float maxDist) {
    float totalDistance = 0.0;
    vec3 hitPoint;

    for (int i = 0; i < maxSteps; i++) {
        hitPoint = rayOrigin + rayDir * totalDistance;
        float dist = sceneSDF(hitPoint);
        if (dist < 0.001) break;  // Close enough to surface
        if (totalDistance > maxDist) return vec3(0.5, 0.7, 1.0); // Sky color
        totalDistance += dist;
    }

    return hitPoint; // Return the hit location
}

Surface Normals

For lighting and reflections, we need surface normals. These are estimated using small offsets in each direction:

vec3 getNormal(vec3 p) {
    vec2 e = vec2(0.001, 0.0);
    return normalize(vec3(
        sceneSDF(p + e.xyy) - sceneSDF(p - e.xyy),
        sceneSDF(p + e.yxy) - sceneSDF(p - e.yxy),
        sceneSDF(p + e.yyx) - sceneSDF(p - e.yyx)
    ));
}

Reflections

Reflections are computed using the reflect function:

\[R = I - 2 (N \cdot I) N\]

where:

  • \(I\) is the incoming ray direction,
  • \(N\) is the surface normal,
  • \(R\) is the reflected ray.

In GLSL, this is done using:

vec3 reflectedDir = reflect(rayDir, getNormal(hitPoint));

Now, we ray march again along the reflected direction:

vec3 computeReflection(vec3 hitPoint, vec3 rayDir) {
    vec3 normal = getNormal(hitPoint);
    vec3 reflectedDir = reflect(rayDir, normal);
    
    vec3 reflectionHit = rayMarch(hitPoint + normal * 0.01, reflectedDir, 50, 10.0);
    return phongLighting(reflectionHit, -reflectedDir);
}

Full Shader

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = (fragCoord - 0.5 * iResolution.xy) / iResolution.y;

    // Camera Setup
    vec3 rayOrigin = vec3(0, 0, -5);
    vec3 rayDir = normalize(vec3(uv, 1.0));

    // Perform Ray Marching
    vec3 hitPoint = rayMarch(rayOrigin, rayDir, 100, 10.0);

    // If we hit an object, apply shading
    vec3 color;
    if (hitPoint != vec3(0.5, 0.7, 1.0)) {
        vec3 viewDir = normalize(rayOrigin - hitPoint);
        vec3 baseLight = phongLighting(hitPoint, viewDir);
        vec3 reflection = computeReflection(hitPoint, rayDir);
        color = mix(baseLight, reflection, 0.5); // Blend reflections
    } else {
        color = vec3(0.5, 0.7, 1.0); // Sky color
    }

    fragColor = vec4(color, 1.0);
}

Running this shader, you should see two very reflective spheres reflecting each other.

Conclusion

With just a few functions, we’ve recreated a classic ray tracing scene using ray marching. This technique allows us to:

  • Render reflective surfaces without traditional ray tracing
  • Generate soft shadows using SDF normals
  • Extend the method for refraction and more complex materials