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