Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit65083fe8f2

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:

orderpassoutput
60ssr_traceone 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
61ssr_resolveeach 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.