Builtin Library
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.