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 - Toggle cascade debug colour overlay 1 / 2 / 3 - Set 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 - Toggle cascade debug colour overlay
10 1 / 2 / 3 - Set 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 Node3D,
29 Text2D,
30 WorldEnvironment,
31)
32from simvx.graphics import App
33
34
35class ShadowQualityScene(Node3D):
36 def on_ready(self):
37 InputMap.add_action("toggle_debug", [Key.D])
38 InputMap.add_action("cascades_1", [Key.KEY_1])
39 InputMap.add_action("cascades_2", [Key.KEY_2])
40 InputMap.add_action("cascades_3", [Key.KEY_3])
41 InputMap.add_action("quit", [Key.ESCAPE])
42
43 # WorldEnvironment: cascade debug toggle + runtime count both flow
44 # through EnvironmentSync into the shared shadow SSBO
45 # (``debug_cascades_flag`` and ``active_cascade_count``).
46 self._env = self.add_child(WorldEnvironment(name="Env"))
47 self._env.shadow_debug_cascades = False
48
49 # Camera
50 self._cam = self.add_child(Camera3D(
51 position=(0, -25, 12), fov=60, look_at=(0, 0, 0), up=(0, 0, 1),
52 ))
53
54 # Sun (directional light producing shadows)
55 sun = DirectionalLight3D(position=(-8, -12, 10))
56 sun.colour = (1.0, 0.95, 0.85)
57 sun.intensity = 1.2
58 sun.shadows = True # directional shadows are opt-in
59 sun.look_at((0, 0, 0))
60 self.add_child(sun)
61
62 # Ground plane
63 ground_mat = Material(colour=(0.35, 0.45, 0.35, 1), roughness=0.9, metallic=0.0)
64 ground = MeshInstance3D(mesh=Mesh.cube(), material=ground_mat, position=(0, 0, -0.5))
65 ground.scale = (30, 30, 0.2)
66 self.add_child(ground)
67
68 # Scatter objects at various distances to show cascade splits
69 cube_mesh = Mesh.cube()
70 sphere_mesh = Mesh.sphere()
71 colours = [
72 (0.9, 0.3, 0.2, 1),
73 (0.2, 0.7, 0.9, 1),
74 (0.9, 0.8, 0.2, 1),
75 (0.6, 0.3, 0.8, 1),
76 (0.3, 0.9, 0.4, 1),
77 (0.9, 0.5, 0.1, 1),
78 ]
79 # Near objects
80 for i in range(4):
81 mat = Material(colour=colours[i % len(colours)], roughness=0.4, metallic=0.2)
82 self.add_child(MeshInstance3D(mesh=cube_mesh, material=mat, position=(i * 2 - 3, -2, 1)))
83
84 # Mid-range objects
85 for i in range(5):
86 mat = Material(colour=colours[(i + 2) % len(colours)], roughness=0.3, metallic=0.5)
87 self.add_child(MeshInstance3D(mesh=sphere_mesh, material=mat, position=(i * 3 - 6, 5, 1.2)))
88
89 # Far objects
90 for i in range(3):
91 mat = Material(colour=colours[(i + 4) % len(colours)], roughness=0.5, metallic=0.1)
92 obj = MeshInstance3D(mesh=cube_mesh, material=mat, position=(i * 4 - 4, 15, 1.5))
93 obj.scale = (1.5, 1.5, 3.0)
94 self.add_child(obj)
95
96 # Tall pillars casting long shadows
97 pillar_mat = Material(colour=(0.7, 0.7, 0.75, 1), roughness=0.6, metallic=0.3)
98 for x in (-8, 0, 8):
99 pillar = MeshInstance3D(mesh=cube_mesh, material=pillar_mat, position=(x, 0, 3))
100 pillar.scale = (0.5, 0.5, 6.0)
101 self.add_child(pillar)
102
103 # HUD
104 self._hud = self.add_child(
105 Text2D(text="", x=10, y=10, font_scale=1.5)
106 )
107
108 self._time = 0.0
109
110 def on_process(self, dt):
111 self._time += dt
112
113 if Input.is_action_just_pressed("quit"):
114 self.app.quit()
115 return
116
117 if Input.is_action_just_pressed("toggle_debug"):
118 self._env.shadow_debug_cascades = not self._env.shadow_debug_cascades
119 if Input.is_action_just_pressed("cascades_1"):
120 self._env.shadow_cascade_count = 1
121 if Input.is_action_just_pressed("cascades_2"):
122 self._env.shadow_cascade_count = 2
123 if Input.is_action_just_pressed("cascades_3"):
124 self._env.shadow_cascade_count = 3
125
126 debug = "ON" if self._env.shadow_debug_cascades else "OFF"
127 self._hud.text = (
128 f"[D] Debug cascades: {debug} "
129 f"[1/2/3] Active cascades: {self._env.shadow_cascade_count}"
130 )
131
132 # Slowly orbit camera so shadows move and cascade transitions are visible
133 r = 25.0
134 angle = self._time * 0.15
135 self._cam.position = (r * math.sin(angle), -r * math.cos(angle), 12)
136 self._cam.look_at((0, 0, 0), up=(0, 0, 1))
137
138
139if __name__ == "__main__":
140 app = App(title="Shadow Quality Demo", width=1280, height=720)
141 app.run(ShadowQualityScene())