Three.js From Zero · Article s4-04
Screen-Space Reflections (SSR)
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.
The algorithm (high-level)
- Render the scene into a color buffer + depth buffer (already done)
- 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.
- 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
| Pro | Con |
|---|---|
| Cheap — no scene re-render | Only reflects things CURRENTLY ON SCREEN |
| Arbitrary moving geometry | Misses off-screen geometry (sky, objects behind camera) |
| Per-pixel (respects normals) | Noisy at high roughness without denoising |
| Works with any PBR material | Can'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:
- Multiple random rays per pixel (Monte Carlo). Expensive. Noisy without accumulation.
- Pre-blur the color buffer at different levels, sample based on roughness. Cheap. Leaky.
- 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
- Hierarchical-Z SSR: instead of fixed step size, traverse a depth min/max pyramid. Accelerates deep reflections.
- SSGI variant: same ray march but accumulate over frames (requires motion vectors + reprojection).
- 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.