Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commite8beb5604c

README.md

taa.renderFeature

One compute pass after lighting, and a per-frame offset to where the projection samples.

What it does

A pixel is one sample of a continuous image. Which point inside the pixel that sample is taken at is normally fixed forever, and that is what makes an edge resolve to the same staircase in every frame — the frames agree because they all ask the same question.

This pass moves the sample. Each render the projection is offset by a sub-pixel amount (renderer.setProjectionJitter), so consecutive renders are different estimates of the same image, and the pass keeps a running result across them. After a handful of frames that result holds detail no single frame contains: a silhouette becomes a gradient, a specular highlight stops flashing on and off as it crosses the sample point, fine texture stops crawling.

Where the samples go matters. The offsets come from a Halton sequence in bases 2 and 3 — successive points of a low-discrepancy sequence avoid each other, so a short run of frames covers the pixel evenly, where independent random points clump and leave gaps and a regular grid repeats as a visible texture. The two bases are coprime, so the axes do not fall into step. Eight offsets is enough to resolve an edge and short enough that the result refreshes before anything drifts.

Carrying the result forward

The accumulated result is only usable at a pixel where it describes the same surface. Three tests decide that, in order.

Where to look. @scene.motion gives the screen-space velocity of the surface under each pixel — where it was in the previous frame. The velocity is read from the closest-depth pixel of the 3x3 neighbourhood rather than from the pixel itself: along a moving silhouette the pixel's own velocity belongs to whichever of the two surfaces won the depth test, and the nearer surface is the one whose edge is sweeping across the pixel.

Whether to look at all. A reprojection landing outside the frame has nothing recorded there. That pixel starts its own accumulation from the current frame rather than inventing one.

Whether the answer is still about this surface. Something newly uncovered reprojects to wherever the thing that was covering it went, and reads as a perfectly valid sample of the wrong surface. That is the smear temporal antialiasing is known for. The neighbourhood's own colour statistics bound it: the 3x3 block around the pixel describes what colours belong there now, so the carried sample is clipped into the range the mean and standard deviation of that block describe before it is trusted.

Mean and spread rather than min and max, because a single bright texel stretches the extremes far enough to admit a sample that is nothing like the pixel, while the standard deviation stays honest about how much the block actually varies.

The clip runs in YCoCg and along the line from the neighbourhood mean. Luminance is one axis there, so the box tracks a neighbourhood that varies mostly in brightness far more tightly than an RGB box, whose corners admit colours the neighbourhood never contained. Moving the sample along a line rather than clamping three channels independently keeps its hue, so the result is a colour between two the frame actually holds.

Velocity and the offset

The offset is folded into the projection, so it would otherwise show up in velocity as a sub-pixel wobble on a scene that is not moving. The engine publishes the offset alongside the matrix and the vertex stage subtracts it back out, so @scene.motion describes the surfaces and a still scene reports no motion however the samples are placed.

Every view keeps its own result

A frame draws several views: the viewport, every render-to-texture camera, every capture. Each runs this pass against its own picture, at its own size, so each keeps its own running result. The result lives in a history target (renderer.texture.create { history = true }), which holds one pair of images per view: for the length of a view's render the pass resolves into the target's guid and reads previous, the image that view's previous render resolved. A minimap or a capture drawn beside the viewport neither reads the viewport's result nor disturbs it.

Each view also walks the sample sequence on its own count of renders. Its first render, and its first after a cut, samples the pixel centre and starts the result from that render alone, which is all a one-shot capture ever draws.

The images are rgba16f, so an accumulated highlight brighter than white keeps its value instead of clipping on the way through.

Parameters

taa_params carries the tuning, written by @builtin::systems.antiAliasing.temporalAntiAliasing. taa_state is the history target's state, written by the engine before each render of a view: whether previous holds that view's previous result, how many renders in a row have carried it, and the render's sub-pixel offset. A render with no result to carry begins from its own sample.