Motion Blur

Camera motion blur on a WorldEnvironment, over a scene of orbiting cubes.

▶ Run in browser

Tags: 3d

Motion blur smears each pixel along the direction it moved on screen since the previous frame, the way a real camera’s shutter does while it pans. It is a post effect on the scene’s WorldEnvironment, alongside bloom and fog:

env = WorldEnvironment(motion_blur_enabled=True, motion_blur_intensity=1.0, motion_blur_samples=12)

motion_blur_intensity scales the length of the smear and motion_blur_samples is how many taps are taken along it: more samples give a smoother streak at a higher cost. All three are ordinary properties, so changing them at runtime takes effect on the next frame.

The blur comes from camera motion, so an OrbitCamera3D circles a grid of cubes on its own through camera.orbit(). Speed the orbit up and the streaks lengthen; slow it down and the scene sharpens. The camera takes its own input as well, so dragging it adds motion of your own.

Controls: M / Blur on/off - Toggle motion blur Up / Down - Intensity Right / Left - Sample count D / A - Orbit faster / slower Drag / scroll - Orbit and zoom the camera

Source

  1"""Motion Blur: Camera motion blur on a WorldEnvironment, over a scene of orbiting cubes.
  2
  3Motion blur smears each pixel along the direction it moved on screen since the
  4previous frame, the way a real camera's shutter does while it pans. It is a
  5post effect on the scene's ``WorldEnvironment``, alongside bloom and fog:
  6
  7    env = WorldEnvironment(motion_blur_enabled=True, motion_blur_intensity=1.0, motion_blur_samples=12)
  8
  9``motion_blur_intensity`` scales the length of the smear and
 10``motion_blur_samples`` is how many taps are taken along it: more samples give
 11a smoother streak at a higher cost. All three are ordinary properties, so
 12changing them at runtime takes effect on the next frame.
 13
 14The blur comes from camera motion, so an ``OrbitCamera3D`` circles a grid of
 15cubes on its own through ``camera.orbit()``. Speed the orbit up and the streaks
 16lengthen; slow it down and the scene sharpens. The camera takes its own input
 17as well, so dragging it adds motion of your own.
 18
 19Controls:
 20    M / Blur on/off   - Toggle motion blur
 21    Up / Down         - Intensity
 22    Right / Left      - Sample count
 23    D / A             - Orbit faster / slower
 24    Drag / scroll     - Orbit and zoom the camera
 25"""
 26
 27import math
 28
 29from simvx.core import (
 30    Anchor,
 31    Axis1D,
 32    Button,
 33    CanvasLayer,
 34    DirectionalLight3D,
 35    Input,
 36    Key,
 37    Label,
 38    Material,
 39    Mesh,
 40    MeshInstance3D,
 41    Node,
 42    OrbitCamera3D,
 43    Property,
 44    WorldEnvironment,
 45    on_input,
 46)
 47from simvx.graphics import App
 48
 49COLOURS = [
 50    (0.8, 0.2, 0.2),
 51    (0.2, 0.7, 0.2),
 52    (0.2, 0.3, 0.8),
 53    (0.8, 0.7, 0.1),
 54    (0.7, 0.2, 0.7),
 55    (0.2, 0.7, 0.7),
 56    (0.9, 0.5, 0.1),
 57    (0.6, 0.6, 0.6),
 58]
 59
 60
 61class MotionBlurDemo(Node):
 62    orbit_speed = Property(2.0, range=(0.1, 5.0))  # radians per second
 63
 64    input_context = {
 65        "toggle_blur": [Key.M],
 66        "intensity": [Axis1D(low=Key.DOWN, high=Key.UP)],
 67        "samples": [Axis1D(low=Key.LEFT, high=Key.RIGHT)],
 68        "orbit_speed": [Axis1D(low=Key.A, high=Key.D)],
 69    }
 70
 71    def on_ready(self):
 72        self.env = self.add_child(
 73            WorldEnvironment(motion_blur_enabled=True, motion_blur_intensity=1.0, motion_blur_samples=12)
 74        )
 75        self.camera = self.add_child(
 76            OrbitCamera3D(pivot=(0, 0.5, 0), distance=8.5, pitch=math.radians(-17), max_pitch=math.radians(-3))
 77        )
 78        self.add_child(DirectionalLight3D(colour=(1.0, 0.95, 0.9), intensity=1.5, direction=(-0.5, -1.0, -0.3)))
 79        self.add_child(MeshInstance3D(mesh=Mesh.plane(20), material=Material(colour=(0.3, 0.35, 0.3), roughness=0.9)))
 80
 81        # A 4 x 4 grid of cubes in three heights, so the silhouettes streak at different lengths.
 82        cube = Mesh.box()
 83        index = 0
 84        for x in (-3, -1, 1, 3):
 85            for z in (-3, -1, 1, 3):
 86                height = 0.5 + index % 3 * 0.5
 87                material = Material(colour=COLOURS[index % len(COLOURS)], roughness=0.4, metallic=0.1)
 88                self.add_child(
 89                    MeshInstance3D(mesh=cube, material=material, position=(x, height / 2, z), scale=(1, height, 1))
 90                )
 91                index += 1
 92
 93        pillar = Material(colour=(0.9, 0.85, 0.7), roughness=0.3)
 94        self.add_child(MeshInstance3D(mesh=Mesh.box((0.6, 4.0, 0.6)), material=pillar, position=(0, 2, 0)))
 95        chrome = Material(colour=(0.1, 0.5, 0.9), roughness=0.2, metallic=0.8)
 96        self.add_child(MeshInstance3D(mesh=Mesh.sphere(), material=chrome, position=(4, 1, 0)))
 97
 98        hud = self.add_child(CanvasLayer())
 99        self.status = hud.add_child(Label(anchor=Anchor.TOP_LEFT, inset=10, font_size=18))
100
101        # Example help overlay: not part of the technique shown.
102        hud.add_child(Button("Blur on/off", action="toggle_blur", anchor=Anchor.BOTTOM_RIGHT, inset=10))
103        help_text = "M: blur   Up/Down: intensity   Left/Right: samples   A/D: orbit speed   Drag: orbit"
104        hud.add_child(Label(help_text, anchor=Anchor.BOTTOM_LEFT, inset=10, font_size=14))
105
106    @on_input("toggle_blur")
107    def toggle_blur(self, _event):
108        self.env.motion_blur_enabled = not self.env.motion_blur_enabled
109
110    @on_input("samples")
111    def step_samples(self, event):
112        # The desktop renderer clamps the sample count to 4-32, a range the property does not declare.
113        step = 2 if event.value > 0 else -2
114        self.env.motion_blur_samples = max(4, min(32, self.env.motion_blur_samples + step))
115
116    def on_update(self, dt):
117        env = self.env
118        # The desktop renderer caps the intensity at 2.0, below the property's declared range of 0-5.
119        env.motion_blur_intensity = min(2.0, env.motion_blur_intensity + Input.value("intensity") * dt)
120        self.orbit_speed += Input.value("orbit_speed") * 1.5 * dt
121        self.camera.orbit(self.orbit_speed * dt, 0.0)
122
123        self.status.text = (
124            f"Motion blur: {'ON' if env.motion_blur_enabled else 'OFF'}\n"
125            f"Intensity: {env.motion_blur_intensity:.1f}\n"
126            f"Samples: {env.motion_blur_samples}\n"
127            f"Orbit speed: {self.orbit_speed:.1f}"
128        )
129
130
131if __name__ == "__main__":
132    App(title="Motion Blur", width=1280, height=720).run(MotionBlurDemo())