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