Three.js From Zero · Article s4-04

Screen-Space Reflections (SSR)

← threejs-from-zeroS4 · Article 04 Season 4
Article S4-04 · Three.js From Zero

Screen-Space Reflections (SSR)

Environment maps (S4-02) give you infinity-far reflections — correct for skies, wrong for the nearby objects. A wet floor should reflect the chair beside it, not just the ceiling light. SSR fills that gap: ray-march in screen space to find actual in-view geometry a reflection should hit.

loading…

The algorithm (high-level)

  1. Render the scene into a color buffer + depth buffer (already done)
  2. For each pixel of a reflective surface:
    • Compute the reflection direction from view + normal
    • Project the ray origin + direction into screen space
    • March along that ray in screen space, pixel by pixel
    • Each step: compare the ray's depth to the stored depth buffer's depth
    • When the ray goes BEHIND the depth buffer → we've hit something. Sample the color at that pixel.
  3. Mix the reflected color with the surface's direct lighting + envmap

The ray march in GLSL

bool traceScreenRay(vec3 rayOrigin, vec3 rayDir, out vec2 hitUV) {
  const int STEPS = 32;
  const float STEP_SIZE = 0.03;

  vec3 p = rayOrigin;
  for (int i = 0; i < STEPS; i++) {
    p += rayDir * STEP_SIZE;

    // Project into screen space
    vec4 clip = uProjection * vec4(p, 1.0);
    vec2 uv = clip.xy / clip.w * 0.5 + 0.5;

    // Compare ray depth to scene depth at this UV
    float sceneDepth = texture2D(uDepth, uv).r;
    float rayDepth = clip.z / clip.w;

    if (rayDepth > sceneDepth + 0.001 && rayDepth < sceneDepth + 0.05) {
      hitUV = uv;
      return true;
    }
  }
  return false;
}

32 steps works for small reflections. For long reflections, use hierarchical Z (HiZ) or adaptive step sizes. Three.js's SSRPass does HiZ.

Pros + cons of SSR

ProCon
Cheap — no scene re-renderOnly reflects things CURRENTLY ON SCREEN
Arbitrary moving geometryMisses off-screen geometry (sky, objects behind camera)
Per-pixel (respects normals)Noisy at high roughness without denoising
Works with any PBR materialCan't reflect things occluded by foreground objects

Hybrid SSR + envmap

Best practice: try SSR first. If the ray misses (hits screen edge, runs out of steps), fall back to the envmap reflection. You get crisp near-field reflections + clean sky reflections where screen space fails.

vec3 ssrColor = vec3(0);
vec2 hit;
bool valid = traceScreenRay(pos, reflectDir, hit);
if (valid) {
  ssrColor = texture2D(uColor, hit).rgb;
} else {
  ssrColor = textureCube(uEnvMap, reflectDir).rgb;
}

// Fade SSR at screen edges to hide the hard cutoff
float edgeFade = 1.0 - max(abs(hit.x - 0.5), abs(hit.y - 0.5)) * 2.0;
float ssrWeight = valid ? clamp(edgeFade * 2.0, 0.0, 1.0) : 0.0;

vec3 finalReflection = mix(envColor, ssrColor, ssrWeight);

Rough SSR

For rough materials, you don't want a mirror reflection — you want a blurred one. Three paths:

  1. Multiple random rays per pixel (Monte Carlo). Expensive. Noisy without accumulation.
  2. Pre-blur the color buffer at different levels, sample based on roughness. Cheap. Leaky.
  3. SSGI hybrid: 1 ray, temporally accumulated across frames. Industry standard.

Three.js's SSRPass

import { SSRPass } from 'three/addons/postprocessing/SSRPass.js';

const ssrPass = new SSRPass({
  renderer, scene, camera,
  width, height,
  groundReflector: floorMesh,   // the receiver
  selects: [boxes, spheres],     // the meshes that should appear in reflections
});
ssrPass.thickness = 0.018;
ssrPass.distanceAttenuation = true;
ssrPass.fresnel = true;
ssrPass.infiniteThick = false;
composer.addPass(ssrPass);

This is what the demo uses. Under the hood: a floor reflector that ray-marches against a depth texture populated from the listed "selects".

Common first-time pitfalls

  • Reflection disappears at screen edge. That's SSR's fundamental limit. Fade at boundary + fall back to envmap.
  • Reflected colors look washed out. Running SSR BEFORE tone mapping means you're reflecting linear-space colors. Do it right — read the post-tonemapping color buffer.
  • Noisy reflections. Not enough steps, or no temporal accumulation. Raise steps or add TAA (S5-03).
  • Artifacts on glancing surfaces. Reflection direction nearly parallel to screen plane → ray leaves screen fast. Clamp reflection normal toward camera.

Exercises

  1. Hierarchical-Z SSR: instead of fixed step size, traverse a depth min/max pyramid. Accelerates deep reflections.
  2. SSGI variant: same ray march but accumulate over frames (requires motion vectors + reprojection).
  3. Compare cost: profile SSR vs planar reflection camera for a simple floor. Planar wins up to ~2 reflectors; SSR wins at N.

What's next

S4-05 — Volumetric fog. Raymarched atmospheric depth, god rays, height fog.