Motion Blur - MotionBlurPost
When the camera or the subject moves while a real camera's shutter is open, the image streaks along the direction of motion. MotionBlurPost computes the screen-space path each pixel travelled since the previous frame and averages several samples along it, producing realistic motion blur.
The path of a pixel comes from one of two sources:
| Mode | How to enable | Effect |
|---|---|---|
| Camera motion blur | Default | Reconstructed from depth and the previous view-projection matrix; only blurs the motion caused by panning, orbiting and dollying the camera. With a still camera, objects that move on their own stay sharp |
| Object motion blur | setting.render.objectMotionVectors = true | Every object also draws its own screen-space velocity, so moving objects streak even with a still camera |
// Initialize the engine; default parameters can be configured here
let engine = await Engine3D.init({
setting: {
render: {
postProcessing: {
motionBlur: {
intensity: 0.5,
samples: 12,
maxBlurPixels: 64
}
}
}
}
});
// Add the post-processing component
let postProcessing = scene.addComponent(PostProcessingComponent);
// Add MotionBlurPost
let motionBlur = postProcessing.addPost(MotionBlurPost);
// The motionBlur object can be configured too, equivalent to the engine settings above
motionBlur.intensity = 0.5; // 180° shutter
motionBlur.samples = 16;engine.setting.render.postProcessing.motionBlur parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
enable | boolean | false | Whether the effect is on; set automatically when MotionBlurPost is added / removed |
intensity | number | 0.5 | Shutter scale. 1 blurs each pixel along the full distance it moved in one frame (a 360° shutter at 60 Hz), 0.5 is the classic 180° film shutter, 0 disables the blur |
samples | number | 12 | Samples along the direction of motion, 2 ~ 32; more is smoother and slower |
maxBlurPixels | number | 64 | Upper bound of the blur length in pixels, so a camera cut or teleport does not smear the whole frame |
frameRateIndependent | boolean | true | Normalize the blur length to a 60 Hz frame so it looks the same at any frame rate; turn off to blur by the raw per-frame displacement |
Camera Cuts
When the camera jumps to a new position, the displacement between the previous and the current frame is huge and produces one frame of unnatural streaks (although limited by maxBlurPixels). Call resetHistory() after the cut and the next frame is not blurred:
camera.object3D.localPosition = newPosition;
motionBlur.resetHistory();With object motion blur on, resetHistory() also resets MotionVectorPass, and per-object velocities are not drawn on the frame after the reset, so objects that jump together with the camera do not streak either.
Example: Camera Motion Blur
In the example below the camera orbits the scene on its own; the faster it turns, the longer the streaks. Dragging the mouse to look around blurs the image too; set orbitSpeed to 0 and the image snaps back to sharp.
import { AtmosphericComponent, BoxGeometry, CameraUtil, Color, ComponentBase, DirectLight, Engine3D, HoverCameraController, KelvinUtil, LitMaterial, MeshRenderer, MotionBlurPost, Object3D, PostProcessingComponent, Scene3D, Time, View3D } from '@orillusion/core';
import * as dat from 'dat.gui';
/**
* MotionBlurPost: the camera orbits a field of pillars on its own; the faster it turns,
* the longer the streaks. Drag to look around (the blur follows your camera motion too),
* or set orbitSpeed to 0 and the image snaps back to sharp.
*/
class Sample_MotionBlur {
private scene: Scene3D;
private post: PostProcessingComponent;
private blur: MotionBlurPost;
async run() {
const engine = await Engine3D.init({ setting: { shadow: { enable: true, shadowBound: 140 } } });
this.scene = new Scene3D();
this.scene.addComponent(AtmosphericComponent).sunY = 0.65;
const camera = CameraUtil.createCamera3DObject(this.scene);
camera.perspective(60, engine.aspect, 0.1, 2000);
const ctrl = camera.object3D.addComponent(HoverCameraController);
ctrl.setCamera(30, -14, 75);
const orbit = camera.object3D.addComponent(AutoOrbit);
orbit.ctrl = ctrl;
const lightObj = new Object3D();
lightObj.rotationX = 50;
lightObj.rotationY = 130;
const light = lightObj.addComponent(DirectLight);
light.lightColor = KelvinUtil.color_temperature_to_rgb(5600);
light.intensity = 3;
light.castShadow = true;
light.enableCSM = true;
this.scene.addChild(lightObj);
this.build();
const view = new View3D();
view.scene = this.scene;
view.camera = camera;
engine.startRenderView(view);
this.post = this.scene.addComponent(PostProcessingComponent);
this.blur = this.post.addPost(MotionBlurPost);
this.blur.intensity = 1;
this.initGUI(orbit);
}
private build() {
const palette = [new Color(0.95, 0.95, 0.95), new Color(0.05, 0.05, 0.05), new Color(0.9, 0.25, 0.15), new Color(0.15, 0.4, 0.9), new Color(0.95, 0.8, 0.2)];
const mats = palette.map((c) => {
const m = new LitMaterial();
m.baseColor = c;
m.roughness = 0.7;
m.metallic = 0;
return m;
});
const pillar = new BoxGeometry(3, 1, 3);
for (let x = -8; x <= 8; x++) {
for (let z = -8; z <= 8; z++) {
if (Math.abs(x) < 2 && Math.abs(z) < 2) continue;
const h = 5 + ((x * 7 + z * 13) & 7) * 2.5;
const obj = new Object3D();
const mr = obj.addComponent(MeshRenderer);
mr.geometry = pillar;
mr.material = mats[Math.abs(x * 3 + z * 5) % mats.length];
mr.castShadow = true;
obj.x = x * 8;
obj.z = z * 8;
obj.y = h / 2;
obj.scaleY = h;
this.scene.addChild(obj);
}
}
const floor = new Object3D();
const fm = floor.addComponent(MeshRenderer);
fm.geometry = new BoxGeometry(400, 1, 400);
const mat = new LitMaterial();
mat.baseColor = new Color(0.5, 0.5, 0.5);
mat.roughness = 0.9;
mat.metallic = 0;
fm.material = mat;
fm.castShadow = false;
floor.y = -0.5;
this.scene.addChild(floor);
}
private initGUI(orbit: AutoOrbit) {
const gui = new dat.GUI();
const f = gui.addFolder('MotionBlurPost');
// Attaching / detaching the post turns the effect on / off
f.add({ enable: true }, 'enable').onChange((v: boolean) => {
if (v) this.blur = this.post.addPost(MotionBlurPost);
else this.post.removePost(MotionBlurPost);
});
f.add(orbit, 'speed', 0, 240, 1).name('orbitSpeed');
const b = this.blur;
f.add(b, 'intensity', 0, 2, 0.01);
f.add(b, 'samples', 2, 32, 1);
f.add(b, 'maxBlurPixels', 0, 200, 1);
f.add(b, 'frameRateIndependent');
f.open();
// Start collapsed so the panel does not cover the scene
gui.close();
}
}
/** Turns the hover camera around its target at `speed` degrees per second. */
class AutoOrbit extends ComponentBase {
public ctrl: HoverCameraController;
public speed = 60;
public onUpdate(): void {
if (this.ctrl) this.ctrl.roll += this.speed * Time.delta * 0.001;
}
}
new Sample_MotionBlur().run();Object Motion Blur
In the default mode the blur only follows the camera's motion: objects moving on their own (a spinning propeller, a driving car) do not streak while the camera stands still. Turn on objectMotionVectors when initializing the engine and moving objects blur according to their own speed:
let engine = await Engine3D.init({
setting: {
render: {
// Must be set at initialization
objectMotionVectors: true
}
}
});
// Used the same way as camera motion blur
let motionBlur = scene.addComponent(PostProcessingComponent).addPost(MotionBlurPost);
motionBlur.intensity = 1;With it on, the engine keeps every object's model matrix from the previous frame and adds a velocity sub-pass to every opaque material. MotionVectorPass writes each pixel's screen-space velocity (camera motion + the object's own motion) into a velocity texture, and MotionBlurPost blurs along it. motionBlur.usesVelocityTexture tells whether per-object velocities are in use.
Note
objectMotionVectorsmust be set inEngine3D.init: the velocity sub-pass is created when materials are set up, so changing it at runtime has no effect;- only opaque, depth-writing materials produce velocities; transparent objects are not blurred;
- skinned and morph-target meshes only carry their rigid motion (the previous model matrix with the current pose); the motion of bones and morph targets themselves is not blurred;
- every opaque object is drawn one extra time per frame, so watch the cost in scenes with many objects.
Example: Object Motion Blur
In the example below the camera stands still while a propeller spins, a pendulum swings, a row of spheres shuttles back and forth and a cube tumbles. Each streaks according to its own speed, while the static ground and pillars stay sharp.
import { AtmosphericComponent, BoxGeometry, CameraUtil, Color, ComponentBase, CylinderGeometry, DirectLight, Engine3D, GeometryBase, HoverCameraController, KelvinUtil, LitMaterial, MeshRenderer, MotionBlurPost, Object3D, PostProcessingComponent, Scene3D, SphereGeometry, Time, View3D } from '@orillusion/core';
import * as dat from 'dat.gui';
/**
* Per-object motion blur (setting.render.objectMotionVectors).
* The camera stands still while the scene moves: a propeller spins, a pendulum swings, a row of spheres
* shuttles back and forth and a cube tumbles. Each streaks according to its own speed, while the static
* ground and pillars stay sharp.
*/
class Sample_ObjectMotionBlur {
private scene: Scene3D;
private blur: MotionBlurPost;
async run() {
const engine = await Engine3D.init({
setting: {
shadow: { enable: true, shadowBound: 80 },
// Must be set at init: every opaque material gets its velocity pass when it is set up
render: { objectMotionVectors: true }
}
});
this.scene = new Scene3D();
this.scene.addComponent(AtmosphericComponent).sunY = 0.65;
const camera = CameraUtil.createCamera3DObject(this.scene);
camera.perspective(50, engine.aspect, 0.1, 500);
camera.object3D.addComponent(HoverCameraController).setCamera(20, -12, 40);
const lightObj = new Object3D();
lightObj.rotationX = 45;
lightObj.rotationY = 140;
const light = lightObj.addComponent(DirectLight);
light.lightColor = KelvinUtil.color_temperature_to_rgb(5600);
light.intensity = 3;
light.castShadow = true;
light.enableCSM = true;
this.scene.addChild(lightObj);
const anim = this.scene.addComponent(SceneAnimation);
this.build(anim);
const view = new View3D();
view.scene = this.scene;
view.camera = camera;
engine.startRenderView(view);
const post = this.scene.addComponent(PostProcessingComponent);
this.blur = post.addPost(MotionBlurPost);
this.blur.intensity = 1;
this.blur.samples = 16;
this.blur.maxBlurPixels = 96;
this.initGUI(anim);
}
private lit(r: number, g: number, b: number, roughness = 0.6, metallic = 0): LitMaterial {
const m = new LitMaterial();
m.baseColor = new Color(r, g, b);
m.roughness = roughness;
m.metallic = metallic;
return m;
}
private mesh(parent: Object3D, geo: GeometryBase, mat: LitMaterial, x = 0, y = 0, z = 0): Object3D {
const obj = new Object3D();
const mr = obj.addComponent(MeshRenderer);
mr.geometry = geo;
mr.material = mat;
mr.castShadow = true;
obj.x = x;
obj.y = y;
obj.z = z;
parent.addChild(obj);
return obj;
}
private build(anim: SceneAnimation) {
this.mesh(this.scene, new BoxGeometry(80, 0.4, 80), this.lit(0.5, 0.5, 0.52, 0.9), 0, -0.2, 0);
// Static pillars: a sharp reference next to the moving things
const pillar = new BoxGeometry(1, 8, 1);
for (let i = -3; i <= 3; i++) this.mesh(this.scene, pillar, this.lit(0.85, 0.85, 0.85, 0.7), i * 6, 4, -12);
// Propeller: hub + three blades, rotating around z
const propeller = new Object3D();
propeller.x = -10;
propeller.y = 8;
this.scene.addChild(propeller);
this.mesh(propeller, new CylinderGeometry(0.8, 0.8, 1.2, 24), this.lit(0.2, 0.2, 0.22, 0.4, 1)).rotationX = 90;
const blade = new BoxGeometry(0.4, 7, 1.2);
const bladeMats = [this.lit(0.9, 0.2, 0.15), this.lit(0.95, 0.8, 0.2), this.lit(0.2, 0.5, 0.95)];
for (let i = 0; i < 3; i++) {
const arm = new Object3D();
arm.rotationZ = i * 120;
propeller.addChild(arm);
this.mesh(arm, blade, bladeMats[i], 0, 3.8, 0);
}
this.mesh(this.scene, new BoxGeometry(0.6, 8, 0.6), this.lit(0.3, 0.3, 0.3), -10, 4, -1);
// Pendulum: pivot at the top, bob at the end of a rod
const pendulum = new Object3D();
pendulum.y = 12;
this.scene.addChild(pendulum);
this.mesh(pendulum, new CylinderGeometry(0.12, 0.12, 9, 12), this.lit(0.6, 0.6, 0.6, 0.4, 1), 0, -4.5, 0);
this.mesh(pendulum, new SphereGeometry(1.4, 32, 32), this.lit(0.95, 0.55, 0.15, 0.3, 1), 0, -9.5, 0);
this.mesh(this.scene, new BoxGeometry(6, 0.4, 0.4), this.lit(0.3, 0.3, 0.3), 0, 12.2, 0);
// Shuttle: five spheres sliding along x, each with a different phase
const ball = new SphereGeometry(0.9, 32, 32);
const shuttle: Object3D[] = [];
for (let i = 0; i < 5; i++) shuttle.push(this.mesh(this.scene, ball, this.lit(0.2 + i * 0.15, 0.8, 0.3), 0, 1.2, 4 + i * 2.2));
const tumbler = this.mesh(this.scene, new BoxGeometry(3, 3, 3), this.lit(0.85, 0.3, 0.6, 0.5), 11, 4, 0);
anim.propeller = propeller;
anim.pendulum = pendulum;
anim.shuttle = shuttle;
anim.tumbler = tumbler;
}
private initGUI(anim: SceneAnimation) {
const gui = new dat.GUI();
const f = gui.addFolder('Object motion blur');
f.add({ blur: true }, 'blur').onChange((v: boolean) => (this.blur.intensity = v ? 1 : 0));
f.add(this.blur, 'intensity', 0, 3, 0.05);
f.add(this.blur, 'samples', 2, 32, 1);
f.add(this.blur, 'maxBlurPixels', 4, 256, 1);
f.add(anim, 'propellerRpm', 0, 300, 1);
f.add(anim, 'swing');
f.add(anim, 'shuttleSpeed', 0, 3, 0.05);
f.add(anim, 'tumble');
f.open();
// Start collapsed so the panel does not cover the scene
gui.close();
}
}
/** Moves the objects every frame. */
class SceneAnimation extends ComponentBase {
public propellerRpm = 90;
public swing = true;
public shuttleSpeed = 1;
public tumble = true;
public propeller: Object3D;
public pendulum: Object3D;
public shuttle: Object3D[] = [];
public tumbler: Object3D;
private t = 0;
public onUpdate(): void {
if (!this.propeller) return;
const dt = Math.min(Time.delta, 50) / 1000;
this.t += dt;
// rpm to degrees per second
this.propeller.rotationZ += this.propellerRpm * 6 * dt;
if (this.swing) this.pendulum.rotationZ = Math.sin(this.t * 2.2) * 50;
for (let i = 0; i < this.shuttle.length; i++) {
this.shuttle[i].x = Math.sin((this.t * this.shuttleSpeed + i * 0.6) * 1.8) * 12;
}
if (this.tumble) {
this.tumbler.rotationX += 140 * dt;
this.tumbler.rotationY += 90 * dt;
}
}
}
new Sample_ObjectMotionBlur().run();
