Builtin Library
README.md
ssr
The render feature behind ScreenSpaceReflections and
@builtin::systems.reflections.ssr. Two compute passes run at the
afterDepthPrepass seam, once the visibility raster has laid the frame's depth
and before anything shades:
| order | pass | output |
|---|---|---|
| 60 | ssr_trace | one ray per pixel, drawn from the pixel's GGX lobe: where it hit and how confident the hit is, or the direction it travelled when it found nothing, into the feature's ssr_rays target |
| 61 | ssr_resolve | each pixel's lobe gathered over its own ray and its neighbours' rays, accumulated over frames in the feature's history target, into @scene.reflection |
That channel is what a material's environment term reads the same frame, at its own pixel: the traced light, mixed over the probes and the sky by the share of the surface's lobe the screen answered, with the material's own Fresnel, roughness and tint weighing it into the pixel the way they weigh any environment. Nothing here touches the lit picture.
The trace marches against this frame's depth, which the visibility raster
has just written, so the reflection stands where the camera is now. What it reads
about the surfaces (the reflecting surface's normal and roughness, the hit's
normal) comes from the previous frame: the G-buffer as that frame left it
(readsPrevious), reprojected through the previous view-projection
@frame.camera carries in its rows [5..9). The resolve reads the light at each
hit from @scene.colorHistory, the light the previous frame ended with, at
the pixel the hit stood at then. A point the previous frame did not show
reflects its environment alone.
The rays
Every pixel draws one ray from the lobe its roughness describes. A mirror (below
perceptual roughness 0.1) takes the reflection vector. A rougher pixel samples a
half vector from the distribution of normals visible from the camera (Heitz
2018) at alpha = roughness², and reflects the view ray about it, from noise
that depends on the pixel and the frame. Each ray carries the density it was
drawn with, as log2(pdf), so the resolve can weigh it against another pixel's
lobe.
The ray texel is rgba16f: for a hit, xy is the hit pixel minus this pixel (in
whole pixels), z is log2(pdf) and w the confidence in (0, 1]; for a ray
that found nothing, xy is the octahedral encoding of the direction, z the
same log2(pdf) and w is -1; a pixel that traced no ray holds zero. A
mirror ray carries 15 in z in place of a density.
The march
ssr_trace walks the ray through the depth buffer.
Steps are uniform in screen space, not world space. A ray heading toward
the horizon covers enormous world distance across a few pixels, so stepping in
world units there means either missing every thin occluder near the camera or
spending the whole budget on distance nothing can be seen at. The world
parameter is recovered from the screen parameter through 1/w, which is what
interpolates linearly under perspective.
At each step the ray's world position is compared against the world position
reconstructed from the depth sample under it. Comparing in world space is what
lets thickness mean a distance in world units rather than a depth-buffer
epsilon whose meaning changes with range.
The march looks only for the crossing, where the ray first passes behind the depth buffer. Thickness is tested after bisecting that crossing, not during the march: one march step covers roughly as much depth as a surface is thick, so a ray tested during the march can jump from in front of a surface to well behind it without ever landing inside it.
Bisection runs for whichever is larger, the quality tier's budget or however
many halvings bring one march step under thickness. A coarse march that
stopped bisecting early would leave the crossing bracketed more loosely than a
surface is thick, and every hit would then fail the thickness test, so
long-range reflections would vanish outright instead of thinning.
A rough pixel's ray marches fewer steps and refinements (down to 0.35 of the tier's, reached 0.65 of roughness above a mirror), because the resolve blurs what the rays find.
A hit on a surface that faces along the ray (what is on screen there is that surface's far side) is trusted at half the confidence.
The resolve
For each pixel the resolve gathers its own ray and up to taps neighbours (4 at
low and medium, 8 at high, 12 at ultra) on a disk of up to 6 px that
turns with the pixel and the frame and grows with roughness. A neighbour counts
only where its normal is within 0.9 of the pixel's and its depth within 5
percent. Each ray is weighed by the pixel's own lobe: D · Vis · cos / pdf.
A pixel's result is the light of the rays that hit and the share of the lobe they cover. A ray that found nothing counts toward the lobe and carries no light. The history accumulates that result, premultiplied, at up to 0.9 of the previous frame for a rough surface and none for a mirror, with the history colour held to the mean plus or minus 1.25 standard deviations of the rays gathered.
What the channel holds
@scene.reflection holds linear radiance in .rgb and in .a the share of
the lobe the screen answered, multiplied by intensity and by the hand-over
above roughnessCutoff. A surface at or below the cutoff keeps the full share,
one 0.1 above it holds zero, and between the two the share fades. A miss is a
zero the material reads as "the environment alone". The channel is cleared at
the top of every scene render this feature does not run for, so such a frame
reflects the environment alone too. The passes read the light the previous
frame ended with, so they run on the lit channel alone (debugChannel = 0); a
camera drawing a diagnostic pass is drawn lit first, unpresented, so the passes
run for where that camera stands, and the diagnostic reads the channel gathered
for it.
Memory
Each view that draws the feature holds three screen-sized rgba16f images: the
rays and the history pair. At 1080p that is about 16.6 MB each, about 50 MB for
the viewport.