Motion Blur¶
Camera-based motion blur orbiting a scene of cubes.
▶ Run in browserTags: 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)