Skip to main content
GameDev.net gamedev.net

Screen-Space Reflection

by GameDev.net · GLSL ES 3.00 (WebGL2) · 30 Aug 2026

Use in your engine

Run the shader to adjust these controls.

What it demonstrates

Screen-space reflection traces reflected view-space rays through information already rendered on screen. Buffer A produces view-space normals and linear depth for a procedural floor and spheres. Buffer B independently traces and shades the same scene without sampling Buffer A. The image pass receives geometry on channel 0 and scene colour on channel 1, keeping the demo isolated from external textures.

Source Revision 1

Author notes are linked to specific lines.

Shader inputs

void mainImage(out vec4 fragColor, in vec2 fragCoord)

Called once per pixel. Write the colour to fragColor.

iResolution vec3
Viewport size in pixels (z is the pixel aspect ratio).
iTime float
Seconds since the shader started.
iTimeDelta float
Seconds since the previous frame.
iFrameRate float
Frames per second, smoothed.
iFrame int
Frames rendered since the start.
iMouse vec4
Mouse position: xy while held, zw of the last click.
iDate vec4
Year, month, day, and seconds within the day.
iChannel0 sampler2D
Texture bound to channel 0.
iChannel1 sampler2D
Texture bound to channel 1.
iChannel2 sampler2D
Texture bound to channel 2.
iChannel3 sampler2D
Texture bound to channel 3.
iChannelResolution vec3[4]
Pixel size of each bound channel texture.
iChannelTime float[4]
Playback time of each channel, in seconds.
iSampleRate float
Audio sample rate, always 44100.
Common
const float SCENE_FAR = 14.0;
const float CAMERA_FOCAL = 1.7;

float sphereHit(vec3 rayOrigin, vec3 rayDirection, vec3 center, float radius) {
    vec3 offset = rayOrigin - center;
    float b = dot(offset, rayDirection);
    float c = dot(offset, offset) - radius * radius;
    float h = b * b - c;
    if (h < 0.0) return SCENE_FAR;
    float root = sqrt(h);
    float nearHit = -b - root;
    float farHit = -b + root;
    return nearHit > 0.0 ? nearHit : (farHit > 0.0 ? farHit : SCENE_FAR);
}

void traceScene(vec3 rayDirection, out float depth, out vec3 normal) {
    float nearest = SCENE_FAR;
    normal = vec3(0.0, 0.0, 1.0);
    if (rayDirection.y < -0.001) {
        float floorHit = -1.05 / rayDirection.y;
        if (floorHit > 0.0 && floorHit < nearest) {
            nearest = floorHit;
            normal = vec3(0.0, 1.0, 0.0);
        }
    }
    vec3 centers[3] = vec3[3](
        vec3(-1.15, -0.25, -4.1),
        vec3(0.72, -0.47, -3.25),
        vec3(0.35, 0.25, -5.6)
    );
    float radii[3] = float[3](0.80, 0.58, 0.72);
    for (int i = 0; i < 3; ++i) {
        float hit = sphereHit(vec3(0.0), rayDirection, centers[i], radii[i]);
        if (hit < nearest) {
            nearest = hit;
            normal = normalize(rayDirection * hit - centers[i]);
        }
    }
    depth = nearest >= SCENE_FAR ? SCENE_FAR : -(rayDirection * nearest).z;
}

vec3 cameraRay(vec2 fragCoord) {
    vec2 screen = (2.0 * fragCoord - iResolution.xy) / iResolution.y;
    return normalize(vec3(screen, -CAMERA_FOCAL));
}

vec3 reconstructViewPosition(vec2 uv, float depth) {
    vec2 screen = (2.0 * uv - 1.0) * vec2(iResolution.x / iResolution.y, 1.0);
    return vec3(screen * depth / CAMERA_FOCAL, -depth);
}

vec2 projectViewPosition(vec3 position) {
    vec2 screen = position.xy * CAMERA_FOCAL / max(-position.z, 0.001);
    return 0.5 + 0.5 * screen / vec2(iResolution.x / iResolution.y, 1.0);
}

vec3 skyColor(vec3 direction) {
    float horizon = smoothstep(-0.25, 0.65, direction.y);
    return mix(vec3(0.07, 0.09, 0.14), vec3(0.36, 0.58, 0.82), horizon);
}
Buffer A1
1 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
2 float depth;
3 vec3 normal;
4 traceScene(cameraRay(fragCoord), depth, normal);
5 fragColor = vec4(normal * 0.5 + 0.5, depth / SCENE_FAR);
6 }
7
Buffer B
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec3 rayDirection = cameraRay(fragCoord);
    float depth;
    vec3 normal;
    traceScene(rayDirection, depth, normal);
    if (depth >= SCENE_FAR - 0.01) {
        fragColor = vec4(skyColor(rayDirection), 1.0);
        return;
    }
    vec3 position = rayDirection * depth / -rayDirection.z;
    vec3 lightDirection = normalize(vec3(-0.5, 0.8, 0.35));
    float diffuse = max(dot(normal, lightDirection), 0.0);
    float checker = mod(floor(position.x * 1.4) + floor(position.z * 1.4), 2.0);
    vec3 floorColor = mix(vec3(0.08, 0.11, 0.15), vec3(0.24, 0.29, 0.34), checker);
    vec3 objectColor = mix(vec3(0.18, 0.42, 0.68), vec3(0.82, 0.31, 0.16), normal.y * 0.5 + 0.5);
    float floorMask = 1.0 - step(0.02, abs(position.y + 1.05));
    vec3 base = mix(objectColor, floorColor, floorMask);
    vec3 color = base * (0.18 + 0.82 * diffuse) + skyColor(normal) * 0.12;
    fragColor = vec4(pow(color, vec3(0.4545)), 1.0);
}
Main Image5
1 uniform int uSteps; // @param 8..40 = 28 "Ray steps"
2 uniform float uMaxDistance; // @param 0.5..8.0 = 5.0 "Max distance"
3 uniform float uThickness; // @param 0.01..0.60 = 0.18 "Hit thickness"
4 uniform float uBias; // @param 0.0..0.20 = 0.04 "Depth bias"
5 uniform float uIntensity; // @param 0.0..1.0 = 0.78 "Reflection intensity"
6
7 void mainImage(out vec4 fragColor, in vec2 fragCoord) {
8 vec2 uv = fragCoord / iResolution.xy;
9 vec4 packed = texture(iChannel0, uv);
10 vec3 scene = texture(iChannel1, uv).rgb;
11 float depth = packed.a * SCENE_FAR;
12 if (depth >= SCENE_FAR - 0.01) {
13 fragColor = vec4(scene, 1.0);
14 return;
15 }
16 vec3 normal = normalize(packed.rgb * 2.0 - 1.0);
17 vec3 position = reconstructViewPosition(uv, depth);
18 vec3 reflectionDirection = reflect(normalize(position), normal);
19 vec3 reflectedColor = skyColor(reflectionDirection);
20 float hitMask = 0.0;
21 vec2 lastUv = uv;
22 for (int i = 0; i < 40; ++i) {
23 if (i >= uSteps) break;
24 float distanceAlongRay = (float(i) + 1.0) * uMaxDistance / float(uSteps);
25 vec3 rayPosition = position + normal * 0.06 + reflectionDirection * distanceAlongRay;
26 if (rayPosition.z > -0.05) break;
27 vec2 projectedUv = projectViewPosition(rayPosition);
28 if (any(lessThan(projectedUv, vec2(0.0))) || any(greaterThan(projectedUv, vec2(1.0)))) break;
29 float sceneDepth = texture(iChannel0, projectedUv).a * SCENE_FAR;
30 float depthDelta = -rayPosition.z - sceneDepth;
31 lastUv = projectedUv;
32 if (depthDelta > uBias && depthDelta < uThickness) {
33 reflectedColor = texture(iChannel1, projectedUv).rgb;
34 hitMask = 1.0;
35 break;
36 }
37 }
38 float edgeDistance = min(min(lastUv.x, lastUv.y), min(1.0 - lastUv.x, 1.0 - lastUv.y));
39 float edgeFade = smoothstep(0.0, 0.12, edgeDistance);
40 float fresnel = pow(1.0 - max(dot(-normalize(position), normal), 0.0), 3.0);
41 float floorMask = 1.0 - step(0.02, abs(position.y + 1.05));
42 float reflectiveSurface = mix(0.35, 1.0, floorMask);
43 float weight = uIntensity * reflectiveSurface * mix(0.35, 1.0, fresnel) * mix(0.55, edgeFade, hitMask);
44 fragColor = vec4(mix(scene, reflectedColor, weight), 1.0);
45 }
46

Inputs for this pass

  • iChannel0 Buffer A
  • iChannel1 Buffer B
  • iChannel2 Empty
  • iChannel3 Empty

Learn from this shader

How it works

The image pass reconstructs a view-space surface position, reflects the camera ray around its normal, and advances that ray in bounded steps. Every candidate position is projected to UV, then its ray depth is compared with Buffer A. A small crossing interval identifies a likely hit and retrieves colour from Buffer B. Proximity to the known floor plane selects its material and stronger reflectivity, rather than treating every upward-facing sphere point as floor. Boundary and Fresnel fades hide unstable cases without pretending the missing data exists.

Try changing

Increase Steps to reduce gaps while watching the cost rise. Thickness can recover missed thin objects, but excessive values attach reflections to unrelated surfaces. Bias prevents the ray from immediately hitting its source. Max distance controls how far the search travels before falling back to the environment colour.

Using it in a game

This is an opaque full-screen pass. Replace channel 0 with view-space normals and linear camera depth, and channel 1 with scene colour from the same frame and projection. Off-screen, occluded, or back-facing objects can never appear, so production SSR commonly adds hierarchical depth, temporal reuse, roughness-aware filtering, and an environment-map fallback. Unity, Unreal, Godot, and raw WebGL use different depth conventions and projection matrices; adapt reconstruction and projection rather than copying this source unchanged.

Explore the techniques

Continue with curated explanations and progressively related examples.

LicenseMIT
Views0
Forks0

Discussion

Loading comments...