Shadow quality demo¶
cascade debug visualization and texel snapping.
▶ Run in browserTags: 3d
Demonstrates:
Cascaded shadow maps with texel snapping (prevents edge swimming)
Debug cascade colouring: red=near, green=mid, blue=far
Runtime cascade count (1, 2, or 3) via WorldEnvironment
Controls: D or click/tap left half - Toggle cascade debug colour overlay 1 / 2 / 3 or click/tap right half - Set/cycle active cascade count Escape - Quit
Run: uv run python examples/features/3d/shadows.py
Source¶
1"""Shadow quality demo -- cascade debug visualization and texel snapping.
2
3Demonstrates:
4 - Cascaded shadow maps with texel snapping (prevents edge swimming)
5 - Debug cascade colouring: red=near, green=mid, blue=far
6 - Runtime cascade count (1, 2, or 3) via WorldEnvironment
7
8Controls:
9 D or click/tap left half - Toggle cascade debug colour overlay
10 1 / 2 / 3 or click/tap right half - Set/cycle active cascade count
11 Escape - Quit
12
13Run: uv run python examples/features/3d/shadows.py
14"""
15
16
17import math
18
19from simvx.core import (
20 Camera3D,
21 DirectionalLight3D,
22 Input,
23 InputMap,
24 Key,
25 Material,
26 Mesh,
27 MeshInstance3D,
28 MouseButton,
29 Node3D,
30 Text2D,
31 WorldEnvironment,
32)
33from simvx.graphics import App
34
35
36class ShadowQualityScene(Node3D):
37 def on_ready(self):
38 InputMap.add_action("toggle_debug", [Key.D])
39 InputMap.add_action("cascades_1", [Key.KEY_1])
40 InputMap.add_action("cascades_2", [Key.KEY_2])
41 InputMap.add_action("cascades_3", [Key.KEY_3])
42 InputMap.add_action("quit", [Key.ESCAPE])
43
44 # WorldEnvironment: cascade debug toggle + runtime count both flow
45 # through EnvironmentSync into the shared shadow SSBO
46 # (``debug_cascades_flag`` and ``active_cascade_count``).
47 self._env = self.add_child(WorldEnvironment(name="Env"))
48 self._env.shadow_debug_cascades = False
49
50 # Camera
51 self._cam = self.add_child(Camera3D(
52 position=(0, -25, 12), fov=60, look_at=(0, 0, 0), up=(0, 0, 1),
53 ))
54
55 # Sun (directional light producing shadows)
56 sun = DirectionalLight3D(position=(-8, -12, 10))
57 sun.colour = (1.0, 0.95, 0.85)
58 sun.intensity = 1.2
59 sun.shadows = True # directional shadows are opt-in
60 sun.look_at((0, 0, 0))
61 self.add_child(sun)
62
63 # Ground plane
64 ground_mat = Material(colour=(0.35, 0.45, 0.35, 1), roughness=0.9, metallic=0.0)
65 ground = MeshInstance3D(mesh=Mesh.cube(), material=ground_mat, position=(0, 0, -0.5))
66 ground.scale = (30, 30, 0.2)
67 self.add_child(ground)
68
69 # Scatter objects at various distances to show cascade splits
70 cube_mesh = Mesh.cube()
71 sphere_mesh = Mesh.sphere()
72 colours = [
73 (0.9, 0.3, 0.2, 1),
74 (0.2, 0.7, 0.9, 1),
75 (0.9, 0.8, 0.2, 1),
76 (0.6, 0.3, 0.8, 1),
77 (0.3, 0.9, 0.4, 1),
78 (0.9, 0.5, 0.1, 1),
79 ]
80 # Near objects
81 for i in range(4):
82 mat = Material(colour=colours[i % len(colours)], roughness=0.4, metallic=0.2)
83 self.add_child(MeshInstance3D(mesh=cube_mesh, material=mat, position=(i * 2 - 3, -2, 1)))
84
85 # Mid-range objects
86 for i in range(5):
87 mat = Material(colour=colours[(i + 2) % len(colours)], roughness=0.3, metallic=0.5)
88 self.add_child(MeshInstance3D(mesh=sphere_mesh, material=mat, position=(i * 3 - 6, 5, 1.2)))
89
90 # Far objects
91 for i in range(3):
92 mat = Material(colour=colours[(i + 4) % len(colours)], roughness=0.5, metallic=0.1)
93 obj = MeshInstance3D(mesh=cube_mesh, material=mat, position=(i * 4 - 4, 15, 1.5))
94 obj.scale = (1.5, 1.5, 3.0)
95 self.add_child(obj)
96
97 # Tall pillars casting long shadows
98 pillar_mat = Material(colour=(0.7, 0.7, 0.75, 1), roughness=0.6, metallic=0.3)
99 for x in (-8, 0, 8):
100 pillar = MeshInstance3D(mesh=cube_mesh, material=pillar_mat, position=(x, 0, 3))
101 pillar.scale = (0.5, 0.5, 6.0)
102 self.add_child(pillar)
103
104 # HUD
105 self._hud = self.add_child(
106 Text2D(text="", position=(10, 10), font_scale=1.5)
107 )
108
109 self._time = 0.0
110
111 def on_update(self, dt):
112 self._time += dt
113
114 if Input.is_action_just_pressed("quit"):
115 self.app.quit()
116 return
117
118 # Mouse/touch: left half toggles the debug overlay, right half cycles
119 # the active cascade count (touch arrives as MouseButton.LEFT on web).
120 if Input.is_mouse_button_just_pressed(MouseButton.LEFT):
121 if float(Input.mouse_position.x) < self.app.width * 0.5:
122 self._env.shadow_debug_cascades = not self._env.shadow_debug_cascades
123 else:
124 self._env.shadow_cascade_count = self._env.shadow_cascade_count % 3 + 1
125
126 if Input.is_action_just_pressed("toggle_debug"):
127 self._env.shadow_debug_cascades = not self._env.shadow_debug_cascades
128 if Input.is_action_just_pressed("cascades_1"):
129 self._env.shadow_cascade_count = 1
130 if Input.is_action_just_pressed("cascades_2"):
131 self._env.shadow_cascade_count = 2
132 if Input.is_action_just_pressed("cascades_3"):
133 self._env.shadow_cascade_count = 3
134
135 debug = "ON" if self._env.shadow_debug_cascades else "OFF"
136 self._hud.text = (
137 f"[D / tap left] Debug cascades: {debug} "
138 f"[1/2/3 / tap right] Active cascades: {self._env.shadow_cascade_count}"
139 )
140
141 # Slowly orbit camera so shadows move and cascade transitions are visible
142 r = 25.0
143 angle = self._time * 0.15
144 self._cam.position = (r * math.sin(angle), -r * math.cos(angle), 12)
145 self._cam.look_at((0, 0, 0), up=(0, 0, 1))
146
147
148if __name__ == "__main__":
149 app = App(title="Shadow Quality Demo", width=1280, height=720)
150 app.run(ShadowQualityScene())