Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit61a33237e0

readme.md

motionBlur system

A camera shutter, and the reconstruction that makes it read as one.

A frame is not an instant. A real shutter stays open for part of the frame's duration and everything that moved in that time is recorded as the path it took — which is why a fast pan reads as motion rather than as a sequence of sharp, stuttering stills.

This gathers that path back out of the frame the renderer just drew, using the velocity buffer the renderer already writes.

Using it

local id = entity.spawn("motion_blur").id
entity(id).component.add("MotionBlur", {
    shutterAngle = 180,   -- degrees of a 360-degree rotation
    samples = 16,         -- taps per pixel
})

Every field is live. Setting shutterAngle to 0 closes the shutter without losing the rest. For runtime control (a quality slider, a photo mode) drive the module:

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

motionBlur.set({ shutterAngle = 180, samples = 16 })
motionBlur.get()
motionBlur.active()
motionBlur.clear()

To see the blur a player sees, read the frame the screen showed: capture { source = "presented" } (frames = n for a run of consecutive frames), or renderer.presentedFrames() for the pixels and what each frame was drawn with. A camera's own motion lives there alone: an offscreen capture (source = "screen", "main", "camera", ...) draws a fresh frame with no previous camera pose, so a pan smears nothing in it, while objects moving in front of a still camera smear in both.

What the knobs do

fieldeffect
shutterAngleHow long the shutter is open, in degrees of a full rotation. 180 — half the frame — is the film convention. 360 never closes. 0 closes it and releases the passes.
samplesTaps per pixel. More resolves a long smear more smoothly, at linear cost.
maxRadiusLongest blur allowed, in pixels, whatever the shutter says. Bounded at 32 by the filter's tile size.
depthScaleHow far apart, in world units, two surfaces must be before the filter treats them as foreground and background rather than as one surface.

Shutter angle is the physical control: motion vectors span one frame, so a shutter open for half a frame smears half the per-frame travel. Doubling the frame rate at a fixed shutter angle halves the blur on its own, exactly as it does on a camera.

How it renders

shutterBlur.renderFeature runs three compute passes after lighting.

The first two reduce the velocity buffer to tiles: the dominant velocity in each 32x32 tile, then the dominant velocity across each tile's 3x3 neighbourhood. This is what lets a moving object blur past its own silhouette. A pixel of static background beside a fast object has no velocity of its own, so a filter reading only that pixel leaves it perfectly sharp and the blur stops dead at the object's edge. Reading the neighbourhood tells that pixel which direction something nearby was travelling, so it can go looking for what swept over it. Diagonal neighbours are taken only when they are actually travelling toward the tile, which is what keeps a fast object from acquiring a square halo.

The third gathers. Each tap is weighted by whether it plausibly covered this pixel during the exposure, in three separable cases: the tap is in front and its own blur reaches here; the tap is behind and this pixel's blur moved off it, revealing it; or both sit at a similar depth and are blurred together. Weighting those separately is what keeps a motion boundary readable — a single blended weight averages a sharp background into a moving foreground and the edge dissolves. Taps alternate between walking the neighbourhood's dominant velocity and this pixel's own, and their positions are jittered per pixel so the taps land as noise rather than as banded copies of the moving object.

Depth is linearised through the drawn camera's own inverse view-projection, so the pass is correct in offscreen captures and RTT cameras at their own resolutions.

The blur reads the scene's velocity buffer, which carries both object and camera motion. A surface the velocity buffer covers smears on its own account; one it does not still has moving objects smear over it, because the tile passes carry a neighbourhood's velocity to pixels that have none of their own.

Files

motionBlur.package/
  MotionBlur.component/     -- the camera's shutter, authored and persisted
  motionBlur.module/        -- .set / .get / .clear / .active
  shutterBlur.renderFeature/ -- the three compute passes