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