Motion Blur

Camera-based motion blur orbiting a scene of cubes.

▶ Run in browser

Tags: 3d

Motion blur is a WorldEnvironment post effect. The scene adds one WorldEnvironment and drives motion_blur_enabled, motion_blur_intensity and motion_blur_samples on it at runtime; the renderer picks the values up each frame. A camera orbiting a grid of cubes supplies the screen-space motion the effect smears, so raising the orbit speed makes the blur more obvious.

Controls: M : Toggle motion blur on/off Up/Down : Adjust motion blur intensity Left/Right: Adjust sample count A/D : Speed up / slow down orbit ESC : Quit

Usage: uv run python examples/features/3d/motion_blur.py

Source

  1#!/usr/bin/env python3
  2"""Motion Blur: Camera-based motion blur orbiting a scene of cubes.
  3
  4# /// simvx
  5# web = { root = "MotionBlurDemo" }
  6# ///
  7
  8Motion blur is a WorldEnvironment post effect. The scene adds one
  9WorldEnvironment and drives ``motion_blur_enabled``, ``motion_blur_intensity``
 10and ``motion_blur_samples`` on it at runtime; the renderer picks the values up
 11each frame. A camera orbiting a grid of cubes supplies the screen-space motion
 12the effect smears, so raising the orbit speed makes the blur more obvious.
 13
 14Controls:
 15    M         : Toggle motion blur on/off
 16    Up/Down   : Adjust motion blur intensity
 17    Left/Right: Adjust sample count
 18    A/D       : Speed up / slow down orbit
 19    ESC       : Quit
 20
 21Usage:
 22    uv run python examples/features/3d/motion_blur.py
 23"""
 24
 25import math
 26
 27from simvx.core import (
 28    Camera3D,
 29    DirectionalLight3D,
 30    Input,
 31    InputMap,
 32    Key,
 33    Material,
 34    Mesh,
 35    MeshInstance3D,
 36    Node,
 37    Property,
 38    Text2D,
 39    Vec3,
 40    WorldEnvironment,
 41)
 42from simvx.graphics import App
 43
 44WIDTH, HEIGHT = 1280, 720
 45
 46
 47class MotionBlurDemo(Node):
 48    """Scene with an orbiting camera to demonstrate motion blur."""
 49
 50    orbit_speed = Property(2.0, range=(0.1, 5.0))
 51    orbit_radius = Property(8.0, range=(3.0, 20.0))
 52
 53    def __init__(self, **kwargs):
 54        super().__init__(**kwargs)
 55        self._time = 0.0
 56
 57    def on_ready(self):
 58        super().on_ready()
 59
 60        InputMap.add_action("toggle_blur", [Key.M])
 61        InputMap.add_action("intensity_up", [Key.UP])
 62        InputMap.add_action("intensity_down", [Key.DOWN])
 63        InputMap.add_action("samples_up", [Key.RIGHT])
 64        InputMap.add_action("samples_down", [Key.LEFT])
 65        InputMap.add_action("orbit_faster", [Key.D])
 66        InputMap.add_action("orbit_slower", [Key.A])
 67        InputMap.add_action("quit", [Key.ESCAPE])
 68
 69        # Camera
 70        self.camera = self.add_child(Camera3D(position=Vec3(0.0, 4.0, 8.0), look_at=Vec3(0.0, 0.0, 0.0)))
 71
 72        # Directional light
 73        light = DirectionalLight3D()
 74        light.direction = Vec3(-0.5, -1.0, -0.3)
 75        light.colour = (1.0, 0.95, 0.9)
 76        light.intensity = 1.5
 77        self.add_child(light)
 78
 79        # Ground plane
 80        ground = MeshInstance3D(mesh=Mesh.cube(), material=Material(colour=(0.3, 0.35, 0.3), roughness=0.9))
 81        ground.position = Vec3(0.0, -0.6, 0.0)
 82        ground.scale = Vec3(20.0, 0.2, 20.0)
 83        self.add_child(ground)
 84
 85        # Coloured cubes in a grid
 86        colours = [
 87            (0.8, 0.2, 0.2),
 88            (0.2, 0.7, 0.2),
 89            (0.2, 0.3, 0.8),
 90            (0.8, 0.7, 0.1),
 91            (0.7, 0.2, 0.7),
 92            (0.2, 0.7, 0.7),
 93            (0.9, 0.5, 0.1),
 94            (0.6, 0.6, 0.6),
 95        ]
 96        cube_mesh = Mesh.cube()
 97        idx = 0
 98        for x in range(-2, 2):
 99            for z in range(-2, 2):
100                scale_y = 0.5 + (idx % 3) * 0.5
101                cube = MeshInstance3D(
102                    mesh=cube_mesh,
103                    material=Material(colour=colours[idx % len(colours)], roughness=0.4, metallic=0.1),
104                )
105                cube.position = Vec3(x * 2.0 + 1.0, scale_y * 0.5, z * 2.0 + 1.0)
106                cube.scale = Vec3(1.0, scale_y, 1.0)
107                self.add_child(cube)
108                idx += 1
109
110        # Tall pillar
111        pillar = MeshInstance3D(mesh=cube_mesh, material=Material(colour=(0.9, 0.85, 0.7), roughness=0.3))
112        pillar.position = Vec3(0.0, 2.0, 0.0)
113        pillar.scale = Vec3(0.6, 4.0, 0.6)
114        self.add_child(pillar)
115
116        # Metallic sphere
117        sphere = MeshInstance3D(
118            mesh=Mesh.sphere(),
119            material=Material(colour=(0.1, 0.5, 0.9), roughness=0.2, metallic=0.8),
120        )
121        sphere.position = Vec3(4.0, 1.0, 0.0)
122        self.add_child(sphere)
123
124        # The WorldEnvironment owns the effect: these are ordinary read-write
125        # properties, so the controls below mutate them in place.
126        self._env = self.add_child(WorldEnvironment(name="Env"))
127        self._env.motion_blur_enabled = True
128        self._env.motion_blur_intensity = 1.0
129        self._env.motion_blur_samples = 12
130
131        # HUD text
132        self._hud_blur = self.add_child(Text2D(text="Motion Blur: ON (M)", position=(10, 8), font_scale=1.4))
133        self._hud_intensity = self.add_child(Text2D(text="Intensity: 1.0 (Up/Down)", position=(10, 35), font_scale=1.1))
134        self._hud_samples = self.add_child(Text2D(text="Samples: 12 (Left/Right)", position=(10, 58), font_scale=1.1))
135        self._hud_speed = self.add_child(Text2D(text="Orbit: 2.0 (A/D)", position=(10, 81), font_scale=1.1))
136
137    def on_update(self, dt: float):
138        if Input.is_action_just_pressed("quit"):
139            self.app.quit()
140            return
141
142        self._time += dt
143
144        # Orbit camera
145        angle = self._time * self.orbit_speed
146        x = math.cos(angle) * self.orbit_radius
147        z = math.sin(angle) * self.orbit_radius
148        y = 3.0 + math.sin(self._time * 0.5) * 1.5
149        self.camera.position = Vec3(x, y, z)
150        self.camera.look_at(Vec3(0.0, 0.5, 0.0))
151
152        # Controls: the environment properties are the state, no local mirror.
153        env = self._env
154        if Input.is_action_just_pressed("toggle_blur"):
155            env.motion_blur_enabled = not env.motion_blur_enabled
156        if Input.is_action_just_pressed("intensity_up"):
157            env.motion_blur_intensity = min(2.0, env.motion_blur_intensity + 0.1)
158        if Input.is_action_just_pressed("intensity_down"):
159            env.motion_blur_intensity = max(0.0, env.motion_blur_intensity - 0.1)
160        if Input.is_action_just_pressed("samples_up"):
161            env.motion_blur_samples = min(32, env.motion_blur_samples + 2)
162        if Input.is_action_just_pressed("samples_down"):
163            env.motion_blur_samples = max(4, env.motion_blur_samples - 2)
164        if Input.is_action_pressed("orbit_faster"):
165            self.orbit_speed = min(5.0, self.orbit_speed + 1.5 * dt)
166        if Input.is_action_pressed("orbit_slower"):
167            self.orbit_speed = max(0.1, self.orbit_speed - 1.5 * dt)
168
169        # Update HUD
170        self._hud_blur.text = f"Motion Blur: {'ON' if env.motion_blur_enabled else 'OFF'} (M)"
171        self._hud_intensity.text = f"Intensity: {env.motion_blur_intensity:.1f} (Up/Down)"
172        self._hud_samples.text = f"Samples: {env.motion_blur_samples} (Left/Right)"
173        self._hud_speed.text = f"Orbit: {self.orbit_speed:.1f} (A/D)"
174
175
176if __name__ == "__main__":
177    scene = MotionBlurDemo(name="MotionBlurDemo")
178    app = App(title="Motion Blur Demo", width=WIDTH, height=HEIGHT)
179    app.run(scene)