How to Implement Motion Blur and Temporal Effects in LuisaRender

LuisaRender implements motion blur by sampling the camera's shutter interval over multiple time steps, updating the scene pipeline at each sample, and accumulating weighted radiance contributions.

Motion blur and other temporal effects in LuisaRender rely on a time-sampling architecture that evaluates animated scene elements at specific shutter instances. The renderer supports keyframed transforms, animated lights, and deformable geometry by querying the scene state at arbitrary times within the camera's exposure window. This article explains the camera shutter configuration, the pipeline update mechanism, and the integrator loop patterns required to render motion blur, referencing the actual implementation in the luisagroup/luisarender repository.

Camera Shutter Configuration

The camera node defines the temporal boundaries of the exposure via the shutter_span parameter and controls temporal sampling density through shutter_samples. These map directly to the Camera::_shutter_span member and the shutter_samples() method in src/base/camera.h (lines 95–106).

To enable motion blur, configure your camera JSON as follows:

{
  "type": "Camera",
  "name": "MotionBlurCam",
  "film": { "width": 1920, "height": 1080 },
  "shutter_span": [0.0, 0.033],
  "shutter_samples": 8,
  "exposure": 1.0
}
  • shutter_span: A two-element array [start_time, end_time] in seconds defining the exposure interval.
  • shutter_samples: The number of discrete time steps sampled within the interval. Higher values reduce noise but increase render time.

Animating Scene Elements

LuisaRender evaluates time-varying transforms and light parameters through the AnimatedTransform and animated value nodes. These are resolved when pipeline().update(command_buffer, time) is invoked, triggering evaluation logic in src/util/transform.h and the scene graph.

Animated Transforms

Attach an AnimatedTransform to any mesh or instance to enable motion blur for geometry:

{
  "type": "Mesh",
  "name": "FlyingSaucer",
  "file": "saucer.obj",
  "transform": {
    "type": "AnimatedTransform",
    "keyframes": [
      { "time": 0.0,   "translation": [-5, 0, 0] },
      { "time": 0.033, "translation": [5, 0, 0] }
    ]
  }
}

The renderer interpolates between keyframes based on the current shutter sample time.

Animated Lights

Temporal effects also apply to light intensity and color:

{
  "type": "PointLight",
  "name": "Strobe",
  "intensity": {
    "type": "AnimatedFloat",
    "keyframes": [
      { "time": 0.0,   "value": 0.0 },
      { "time": 0.008, "value": 100.0 },
      { "time": 0.016, "value": 0.0 },
      { "time": 0.024, "value": 100.0 },
      { "time": 0.033, "value": 0.0 }
    ]
  }
}

During each shutter sample, the pipeline evaluates the light's intensity at the specific time, creating stroboscopic or fading effects automatically.

Implementing the Temporal Render Loop

Integrators implement motion blur by iterating over ShutterSample structures provided by the camera. Each sample contains a time and a weight (shutter weight). The integrator must call pipeline().update(command_buffer, time) to synchronize the scene state before rendering that sample's contribution.

The canonical pattern appears in src/integrators/aov.cpp (lines 60–62 and 71–73):

// Retrieve shutter samples from camera configuration
auto shutter_samples = camera->node()->shutter_samples();

for (auto s : shutter_samples) {
    // 1. Update animated scene elements to the current sample time
    pipeline().update(command_buffer, s.point.time);
    
    // 2. Dispatch the rendering kernel for this temporal slice
    command_buffer << render_kernel(sample_id++, s.point.time, s.point.weight)
                   .dispatch(resolution);
}

The s.point.weight factor represents the shutter weight (line 12–15 in the same file). The integrator multiplies the radiance by this weight before accumulation, ensuring correct exposure across the shutter interval.

Path-tracing integrators such as PathTracing follow this same loop structure but distribute path samples across both space and time, allowing motion blur without the sample-per-pixel restrictions found in AOV integrators.

Motion Blur Compatibility and Limitations

Not all integrators support motion blur equally. The AOVIntegrator enforces a strict compatibility check: it requires that the auxiliary buffer's samples-per-pixel (spp) match the camera's shutter samples exactly. If they differ, the integrator aborts with the error:


"AOVIntegrator is not compatible with motion blur if rendered with different spp from the camera."

This restriction exists because AOVs accumulate auxiliary data (depth, normals, etc.) that must correlate temporally with the color buffer. Path-tracing integrators do not impose this limitation because they treat time as an additional dimension sampled stochastically alongside lens and light samples.

To verify compatibility, check your integrator's source file (e.g., src/integrators/aov.cpp lines 60–62) for assertions regarding shutter_samples and spp.

Summary

  • Camera Configuration: Define shutter_span and shutter_samples in the camera node to control exposure duration and temporal sampling density.
  • Scene Animation: Use AnimatedTransform for geometry motion and animated value nodes for light parameters; the pipeline evaluates these at sample times.
  • Pipeline Update: Call pipeline().update(command_buffer, time) inside the integrator's shutter sample loop to synchronize animated elements.
  • Shutter Weighting: Multiply radiance by shutter_weight (provided in ShutterSample) to achieve correct exposure across the interval.
  • Integrator Selection: Use path-tracing integrators for full motion blur flexibility; avoid AOVIntegrator unless sample counts match exactly.

Frequently Asked Questions

How does LuisaRender handle sub-frame motion for fast-moving objects?

LuisaRender handles sub-frame motion by sampling multiple time points within the camera's shutter_span interval. For each sample, the pipeline().update(command_buffer, time) call evaluates animated transforms at that specific timestamp, interpolating between keyframes defined in AnimatedTransform nodes. This allows the renderer to capture motion blur even for objects moving rapidly between frames.

Can I use motion blur with the AOV integrator for rendering depth or normal passes?

The AOVIntegrator supports motion blur only when the auxiliary buffer's samples-per-pixel exactly matches the camera's shutter_samples count. If the counts differ, the integrator aborts with a compatibility error. For flexible motion blur with arbitrary sample counts, use the PathTracing integrator or other path-based integrators that do not enforce this restriction.

What is the performance cost of increasing shutter samples?

Each additional shutter sample requires a full pipeline update (pipeline().update) and a separate render kernel dispatch. Therefore, render time scales linearly with shutter_samples. For production scenes, balance quality against performance by using 4–8 samples for moderate motion blur, or 16+ samples for high-velocity objects or long exposures.

How do I animate light intensity for effects like flickering or strobes?

Animate light intensity using the AnimatedFloat type in the light's JSON definition, specifying keyframes with time and value pairs. During rendering, the pipeline automatically evaluates the intensity at each shutter sample time when pipeline().update is called. This creates temporal lighting effects such as strobes or fading without requiring manual frame-by-frame rendering.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →