Shadow Quality¶
cascaded shadow maps and the cascade debug view.
▶ Run in browserTags: 3d
Demonstrates:
Directional shadows across a scene scattered from near to far
Debug cascade colouring: red=near, green=mid, blue=far
Runtime cascade count (1, 2, or 3) via WorldEnvironment
The camera orbits slowly so shadows sweep and the cascade splits move through the scene. With one cascade the whole view shares a single shadow map (coarse far shadows); with three, the near band keeps its detail.
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: cascaded shadow maps and the cascade debug view.
2
3Demonstrates:
4 - Directional shadows across a scene scattered from near to far
5 - Debug cascade colouring: red=near, green=mid, blue=far
6 - Runtime cascade count (1, 2, or 3) via WorldEnvironment
7
8The camera orbits slowly so shadows sweep and the cascade splits move through
9the scene. With one cascade the whole view shares a single shadow map (coarse
10far shadows); with three, the near band keeps its detail.
11
12Controls:
13 D or click/tap left half - Toggle cascade debug colour overlay
14 1 / 2 / 3 or click/tap right half - Set/cycle active cascade count
15 Escape - Quit
16
17Run: uv run python examples/features/3d/shadows.py
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 MouseButton,
32 Node3D,
33 Text2D,
34 WorldEnvironment,
35)
36from simvx.graphics import App
37
38
39class ShadowQualityScene(Node3D):
40 def on_ready(self):
41 InputMap.add_action("toggle_debug", [Key.D])
42 InputMap.add_action("cascades_1", [Key.KEY_1])
43 InputMap.add_action("cascades_2", [Key.KEY_2])
44 InputMap.add_action("cascades_3", [Key.KEY_3])
45 InputMap.add_action("quit", [Key.ESCAPE])
46
47 # Two WorldEnvironment properties drive this page: shadow_debug_cascades
48 # tints every shaded fragment by the cascade that lit it, and
49 # shadow_cascade_count sets how many splits the directional light
50 # renders. Both take effect on the next frame.
51 self._env = self.add_child(WorldEnvironment(name="Env"))
52 self._env.shadow_debug_cascades = False
53
54 # Camera
55 self._cam = self.add_child(Camera3D(position=(0, 12, 25), fov=60, look_at=(0, 0, 0)))
56
57 # Sun (directional light producing shadows)
58 sun = DirectionalLight3D(position=(-8, 10, 12))
59 sun.colour = (1.0, 0.95, 0.85)
60 sun.intensity = 1.2
61 sun.shadows = True # directional shadows are opt-in
62 sun.look_at((0, 0, 0))
63 self.add_child(sun)
64
65 # Ground plane
66 ground_mat = Material(colour=(0.35, 0.45, 0.35, 1), roughness=0.9, metallic=0.0)
67 ground = MeshInstance3D(mesh=Mesh.cube(), material=ground_mat, position=(0, -0.5, 0))
68 ground.scale = (30, 0.2, 30)
69 self.add_child(ground)
70
71 # Scatter objects at various distances to show cascade splits
72 cube_mesh = Mesh.cube()
73 sphere_mesh = Mesh.sphere()
74 colours = [
75 (0.9, 0.3, 0.2, 1),
76 (0.2, 0.7, 0.9, 1),
77 (0.9, 0.8, 0.2, 1),
78 (0.6, 0.3, 0.8, 1),
79 (0.3, 0.9, 0.4, 1),
80 (0.9, 0.5, 0.1, 1),
81 ]
82 # Near objects
83 for i in range(4):
84 mat = Material(colour=colours[i % len(colours)], roughness=0.4, metallic=0.2)
85 self.add_child(MeshInstance3D(mesh=cube_mesh, material=mat, position=(i * 2 - 3, 1, 2)))
86
87 # Mid-range objects
88 for i in range(5):
89 mat = Material(colour=colours[(i + 2) % len(colours)], roughness=0.3, metallic=0.5)
90 self.add_child(MeshInstance3D(mesh=sphere_mesh, material=mat, position=(i * 3 - 6, 1.2, -5)))
91
92 # Far objects
93 for i in range(3):
94 mat = Material(colour=colours[(i + 4) % len(colours)], roughness=0.5, metallic=0.1)
95 obj = MeshInstance3D(mesh=cube_mesh, material=mat, position=(i * 4 - 4, 1.5, -15))
96 obj.scale = (1.5, 3.0, 1.5)
97 self.add_child(obj)
98
99 # Tall pillars casting long shadows
100 pillar_mat = Material(colour=(0.7, 0.7, 0.75, 1), roughness=0.6, metallic=0.3)
101 for x in (-8, 0, 8):
102 pillar = MeshInstance3D(mesh=cube_mesh, material=pillar_mat, position=(x, 3, 0))
103 pillar.scale = (0.5, 6.0, 0.5)
104 self.add_child(pillar)
105
106 # HUD
107 self._hud = self.add_child(Text2D(text="", position=(10, 10), font_scale=1.5))
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), 12, r * math.cos(angle))
145 self._cam.look_at((0, 0, 0))
146
147
148if __name__ == "__main__":
149 app = App(title="Shadow Quality Demo", width=1280, height=720)
150 app.run(ShadowQualityScene())