Point and spot shadows¶
PointLight3D and SpotLight3D with shadows=True.
▶ Run in browserTags: 3d
Shadows are opt-in per light (light.shadows = True): a bobbing central
PointLight3D casts a cubemap shadow and a corner SpotLight3D casts a projected
shadow map across a cluster of primitives, while a dim directional fill keeps the
unlit sides readable.
How many positional lights may cast at once is
WorldEnvironment.shadow_caster_count (default 1 point + 1 spot); this demo
runs at that default, and shadow_casters.py raises it. Lights past the
budget still light the scene, cast nothing, and are reported once.
Controls: A / D - Orbit camera left / right W / S - Zoom in / out Q / E - Raise / lower camera
Run: uv run python examples/features/3d/point_shadows.py
Source¶
1#!/usr/bin/env python3
2"""Point and spot shadows: PointLight3D and SpotLight3D with shadows=True.
3
4Shadows are opt-in per light (``light.shadows = True``): a bobbing central
5PointLight3D casts a cubemap shadow and a corner SpotLight3D casts a projected
6shadow map across a cluster of primitives, while a dim directional fill keeps the
7unlit sides readable.
8
9How many positional lights may cast at once is
10``WorldEnvironment.shadow_caster_count`` (default 1 point + 1 spot); this demo
11runs at that default, and ``shadow_casters.py`` raises it. Lights past the
12budget still light the scene, cast nothing, and are reported once.
13
14Controls:
15 A / D - Orbit camera left / right
16 W / S - Zoom in / out
17 Q / E - Raise / lower camera
18
19Run: uv run python examples/features/3d/point_shadows.py
20"""
21
22import math
23
24from simvx.core import (
25 Camera3D,
26 DirectionalLight3D,
27 Input,
28 InputMap,
29 Key,
30 Material,
31 Mesh,
32 MeshInstance3D,
33 Node3D,
34 PointLight3D,
35 Quat,
36 SpotLight3D,
37 Text2D,
38 Vec3,
39)
40from simvx.graphics import App
41
42WIDTH, HEIGHT = 1280, 720
43
44
45class PointShadowsScene(Node3D):
46 def __init__(self):
47 super().__init__(name="PointShadowsDemo")
48
49 # Camera
50 self._cam_angle = 30.0
51 self._cam_height = 12.0
52 self._cam_dist = 22.0
53 self.camera = self.add_child(
54 Camera3D(
55 name="Camera",
56 fov=55,
57 near=0.1,
58 far=200.0,
59 )
60 )
61 self._update_camera()
62
63 # Dim directional light (ambient-ish): no harsh directional shadows
64 sun = self.add_child(DirectionalLight3D(name="Sun"))
65 sun.colour = (0.15, 0.15, 0.2)
66 sun.intensity = 0.3
67 sun.rotation = Quat.from_euler(math.radians(-60), math.radians(-30), 0)
68
69 # ---- Point light in the center (main shadow caster) ----
70 self._point_light = self.add_child(
71 PointLight3D(
72 name="PointLight",
73 position=Vec3(0.0, 5.0, 0.0),
74 )
75 )
76 self._point_light.colour = (1.0, 0.9, 0.7)
77 self._point_light.intensity = 3.0
78 self._point_light.range = 30.0
79 self._point_light.shadows = True # point shadows are opt-in
80
81 # Visual marker for the point light: parented to the light, so it
82 # inherits the light's transform and follows the bob for free.
83 self._point_light.add_child(
84 MeshInstance3D(
85 name="PointLightBulb",
86 mesh=Mesh.sphere(0.15, rings=8, segments=12),
87 material=Material(
88 colour=(1.0, 0.9, 0.7),
89 emissive_colour=(1.0, 0.9, 0.5, 5.0),
90 ),
91 )
92 )
93
94 # ---- Spot light in a corner ----
95 self._spot_light = self.add_child(
96 SpotLight3D(
97 name="SpotLight",
98 position=Vec3(8.0, 8.0, 8.0),
99 )
100 )
101 self._spot_light.colour = (0.3, 0.5, 1.0)
102 self._spot_light.intensity = 4.0
103 self._spot_light.range = 25.0
104 self._spot_light.inner_cone = 20.0
105 self._spot_light.outer_cone = 35.0
106 self._spot_light.shadows = True # spot shadows are opt-in
107 self._spot_light.rotation = Quat.from_euler(math.radians(-50), math.radians(-45), 0)
108
109 # Visual marker for the spot light, likewise parented to its light.
110 self._spot_light.add_child(
111 MeshInstance3D(
112 name="SpotLightBulb",
113 mesh=Mesh.sphere(0.12, rings=8, segments=12),
114 material=Material(
115 colour=(0.3, 0.5, 1.0),
116 emissive_colour=(0.3, 0.5, 1.0, 4.0),
117 ),
118 )
119 )
120
121 # ---- Ground plane ----
122 self.add_child(
123 MeshInstance3D(
124 name="Ground",
125 mesh=Mesh.cube(1.0),
126 material=Material(colour=(0.2, 0.2, 0.22), metallic=0.0, roughness=0.9),
127 position=Vec3(0, -0.05, 0),
128 scale=Vec3(25, 0.1, 25),
129 )
130 )
131
132 # ---- Back wall ----
133 self.add_child(
134 MeshInstance3D(
135 name="BackWall",
136 mesh=Mesh.cube(1.0),
137 material=Material(colour=(0.25, 0.22, 0.2), metallic=0.0, roughness=0.85),
138 position=Vec3(0, 5, -10),
139 scale=Vec3(20, 10, 0.2),
140 )
141 )
142
143 # ---- Side wall ----
144 self.add_child(
145 MeshInstance3D(
146 name="SideWall",
147 mesh=Mesh.cube(1.0),
148 material=Material(colour=(0.22, 0.25, 0.2), metallic=0.0, roughness=0.85),
149 position=Vec3(-10, 5, 0),
150 scale=Vec3(0.2, 10, 20),
151 )
152 )
153
154 # ---- Shadow-casting objects around the point light ----
155 objects = [
156 # Central pillar
157 (
158 "Pillar",
159 Mesh.cylinder(0.6, 3.0, segments=16),
160 Material(colour=(0.7, 0.3, 0.1), metallic=0.2, roughness=0.6),
161 Vec3(0, 1.5, 0),
162 ),
163 # Cubes
164 ("CubeA", Mesh.cube(1.2), Material(colour=(0.15, 0.5, 0.8), metallic=0.8, roughness=0.1), Vec3(4, 0.6, -3)),
165 ("CubeB", Mesh.cube(0.8), Material(colour=(0.9, 0.2, 0.2), metallic=0.0, roughness=0.7), Vec3(-3, 0.4, 4)),
166 (
167 "CubeC",
168 Mesh.cube(1.5),
169 Material(colour=(0.85, 0.85, 0.9), metallic=0.95, roughness=0.05),
170 Vec3(-5, 0.75, -5),
171 ),
172 # Spheres
173 (
174 "SphereA",
175 Mesh.sphere(0.8, rings=16, segments=24),
176 Material(colour=(1.0, 0.8, 0.0), metallic=1.0, roughness=0.15),
177 Vec3(3, 0.8, 4),
178 ),
179 (
180 "SphereB",
181 Mesh.sphere(0.5, rings=12, segments=16),
182 Material(colour=(0.1, 0.9, 0.3), metallic=0.0, roughness=0.8),
183 Vec3(-4, 0.5, 0),
184 ),
185 # Cones
186 (
187 "ConeA",
188 Mesh.cone(0.6, 1.8, segments=16),
189 Material(colour=(0.8, 0.4, 0.9), metallic=0.3, roughness=0.4),
190 Vec3(5, 0.9, 0),
191 ),
192 (
193 "ConeB",
194 Mesh.cone(0.5, 1.2, segments=12),
195 Material(colour=(0.95, 0.6, 0.1), metallic=0.0, roughness=0.5),
196 Vec3(0, 0.6, 6),
197 ),
198 ]
199
200 for name, mesh, material, pos in objects:
201 self.add_child(
202 MeshInstance3D(
203 name=name,
204 mesh=mesh,
205 material=material,
206 position=pos,
207 )
208 )
209
210 # ---- Objects in the spot light's cone for spot shadow demo ----
211 spot_objects = [
212 (
213 "SpotCubeA",
214 Mesh.cube(1.0),
215 Material(colour=(0.5, 0.5, 0.6), metallic=0.5, roughness=0.3),
216 Vec3(5, 0.5, 5),
217 ),
218 (
219 "SpotSphere",
220 Mesh.sphere(0.6, rings=12, segments=16),
221 Material(colour=(0.9, 0.5, 0.1), metallic=0.0, roughness=0.6),
222 Vec3(6, 0.6, 6),
223 ),
224 (
225 "SpotCylinder",
226 Mesh.cylinder(0.4, 2.0, segments=12),
227 Material(colour=(0.3, 0.7, 0.5), metallic=0.1, roughness=0.7),
228 Vec3(4, 1.0, 7),
229 ),
230 ]
231 for name, mesh, material, pos in spot_objects:
232 self.add_child(
233 MeshInstance3D(
234 name=name,
235 mesh=mesh,
236 material=material,
237 position=pos,
238 )
239 )
240
241 # ---- HUD ----
242 self.add_child(Text2D(text="POINT & SPOT SHADOW DEMO", position=(10, 8), font_scale=1.6))
243 self._hint = self.add_child(Text2D(text="WASD/QE: Camera orbit", font_scale=1.2))
244 self._time = 0.0
245
246 def on_ready(self):
247 InputMap.add_action("cam_left", [Key.A, Key.LEFT])
248 InputMap.add_action("cam_right", [Key.D, Key.RIGHT])
249 InputMap.add_action("cam_fwd", [Key.W, Key.UP])
250 InputMap.add_action("cam_back", [Key.S, Key.DOWN])
251 InputMap.add_action("cam_up", [Key.Q])
252 InputMap.add_action("cam_down", [Key.E])
253
254 def _update_camera(self):
255 rad = math.radians(self._cam_angle)
256 x = math.cos(rad) * self._cam_dist
257 z = math.sin(rad) * self._cam_dist
258 self.camera.position = Vec3(x, self._cam_height, z)
259 self.camera.look_at(Vec3(0, 2.0, 0))
260
261 def on_update(self, dt: float):
262 self._time += dt
263 # Keep the controls hint pinned to the live window's bottom edge.
264 self._hint.position = (10, self.tree.screen_size[1] - 30)
265
266 # Camera controls
267 speed = 45.0
268 if Input.is_action_pressed("cam_left"):
269 self._cam_angle += speed * dt
270 if Input.is_action_pressed("cam_right"):
271 self._cam_angle -= speed * dt
272 if Input.is_action_pressed("cam_fwd"):
273 self._cam_dist = max(8, self._cam_dist - 12 * dt)
274 if Input.is_action_pressed("cam_back"):
275 self._cam_dist = min(40, self._cam_dist + 12 * dt)
276 if Input.is_action_pressed("cam_up"):
277 self._cam_height = min(25, self._cam_height + 8 * dt)
278 if Input.is_action_pressed("cam_down"):
279 self._cam_height = max(2, self._cam_height - 8 * dt)
280 self._update_camera()
281
282 # Gentle point light bob; the bulb marker rides along as its child.
283 self._point_light.position = Vec3(0.0, 5.0 + math.sin(self._time * 0.8) * 0.5, 0.0)
284
285
286def main():
287 app = App(title="SimVX Point Shadows Demo", width=WIDTH, height=HEIGHT)
288 app.run(PointShadowsScene())
289
290
291if __name__ == "__main__":
292 main()