Motion Blur

Camera-based motion blur orbiting a scene of cubes.

▶ Run in browser

Tags: 3d

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