Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commitdf78e4f826

readme.md

velocityDilation system

How far a moving surface's velocity reaches past its own silhouette.

The renderer writes one velocity buffer per frame: each texel is the screen-space distance the surface drawn on it travelled between the previous frame and this one. Motion blur, temporal antialiasing, temporal upsampling and a denoiser's history term all read that one buffer, one texel per pixel.

A texel of it describes the surface that was drawn on it, and nothing else. At a moving object's edge that is a frame behind the truth: the background pixels the object is about to cover still report the background's velocity. A reprojection reads history from where nothing moved, and a blur stops dead at the silhouette instead of sweeping past it.

Dilation changes what a texel answers. Each one takes the velocity of the closest surface within a radius of it, so a nearer moving object wins over the background it is passing in front of and its velocity extends that far out past its own silhouette. Where depth cannot separate two surfaces — a Gaussian splat cloud composites by coverage and leaves the depth of what stands behind it — the faster velocity is the one that reaches out. Every reader picks that up with no change at its own call site: the buffer it already binds now answers differently near an edge.

Using it

local id = entity.spawn("velocity_dilation").id
entity(id).component.add("VelocityDilation", { radius = 2 })

For runtime control — a quality slider, a photo mode — drive the module:

local velocityDilation = require("@builtin::systems.velocityDilation.velocityDilation")

velocityDilation.set({ radius = 2 })
velocityDilation.get()      -- { radius = 2 }
velocityDilation.active()   -- true while the passes run
velocityDilation.clear()    -- back to what the geometry passes wrote

radius is clamped to [0, velocityDilation.maxRadius()]. The search costs (2r+1)² texture loads per pixel, which is what the ceiling bounds. A radius of 0 releases the passes and the screen-sized intermediate target with them.

What it costs and what it changes

While armed it runs two passes at afterLighting and holds one screen-sized rgba16f target, and each pixel costs (2r+1)² texture loads.

Dilation is per-pixel. motionBlur carries its own reduction on top of it, at 32-pixel tile granularity, which is coarser and reaches further — the two compose, and dilation is what a reader with no neighbourhood search of its own gets.

Velocity advances only while the frame is being simulated: in a paused or edit frame the previous transforms never step, so the buffer this dilates is zero everywhere and there is nothing for a wider neighbourhood to find.