Skip to content

feat(engine): adaptive per-frame sample count for motion blur (floor 16, ceiling 64) #4029

Description

@miguel-heygen

Summary

data-motion-blur-samples / CaptureOptions.motionBlur.samplesPerFrame (packages/engine/src/services/motionBlur.ts:23,53,55,70-77) is a fixed per-render sample count, default 16, ceiling 64 (MAX_SAMPLES_PER_FRAME). Measuring the shipped feature (v0.8.45) against the accuracy bar set in #4010 (72.4 dB mean PSNR vs. the 960fps shutter integral) shows the default does not universally clear it: it depends on the content's edge hardness and displacement.

Measurements

Harness: an independent 128-sample reference per frame, built by hand (raw sub-frame window.__hf.seek(t, {subframe, suppressEvents}) calls + screenshots, averaged with a standalone accumulator — not the production accumulation code). Validated before trusting it: the production K=16 output and a hand-rolled K=16 reference built the same way are byte-identical, confirming the harness measures the real pipeline rather than a stand-in.

Composition: a single 20px-wide opaque bar on a 1536x200 transparent canvas, 30fps, default shutter (180°, phase -90°).

  1. PSNR, moderate translation (66.7 px/frame — the same displacement basis used for the original 72.4 dB number in fix(registry): make motion-blur match After Effects shutter-based blur #4008): default K=16 → 30.1 dB mean PSNR, well under the bar. K=64 → 80.0 dB, clears it comfortably.
  2. High-velocity (1020 px/frame, matching the registry motion-blur example's fastest row): K=16 → 15 true zero-coverage gaps across the swept region (the smear visibly breaks into separate copies). K=64 → 0 gaps (fully continuous, resolved into a smear).
  3. At-rest: blur on vs. off is byte-identical — stays pixel-sharp.

Cases 2 and 3 match the acceptance criteria from #4010 as written. Case 1 does not.

Caveat: this is one hard-edge synthetic frame, not the original multi-frame text composition that produced 72.4 dB. A sharp, narrow, fully-opaque edge is a harder case for discretization at low K than typical content (antialiased text, softer boundaries) — this is not "the feature is broken," it's evidence that a fixed default sample count is content-dependent and can miss the bar on legitimately common content (thin high-contrast elements: text strokes, iconography, UI chrome).

Proposal

Adaptive per-frame sample count, derived from the motion magnitude measured across the shutter window (e.g. displacement of the animated properties between the window's sub-frame endpoints), rather than a fixed samplesPerFrame. Floor at the current default (16), ceiling at the current max (64, MAX_SAMPLES_PER_FRAME), so:

  • Static and slow-moving content pays what it already pays today, at the floor — case 3 above must stay free. Cost never drops below today's fixed 16; it only rises on the frames that need it.
  • Fast or hard-edged content scales up toward 64 automatically, closing the gap in case 1 without every render paying the 64-sample cost.

Open question for whoever picks this up: what "motion across the shutter window" should be measured from — screen-space displacement of the blurred element's bounding box between the window's endpoint samples is the cheapest signal and is already implicitly available since the endpoints are captured either way; whether that alone is a good enough proxy for rotation/scale-driven blur (as opposed to pure translation) needs checking against a case like case 1 but with rotation instead of translation.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions