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
77 # Visual marker for the point light
78 self.add_child(
79 MeshInstance3D(
80 name="PointLightBulb",
81 mesh=Mesh.sphere(0.15, rings=8, segments=12),
82 material=Material(
83 colour=(1.0, 0.9, 0.7),
84 emissive_colour=(1.0, 0.9, 0.5, 5.0),
85 ),
86 position=Vec3(0.0, 5.0, 0.0),
87 )
88 )
89
90 # ---- Spot light in a corner ----
91 self._spot_light = self.add_child(
92 SpotLight3D(
93 name="SpotLight",
94 position=Vec3(8.0, 8.0, 8.0),
95 )
96 )
97 self._spot_light.colour = (0.3, 0.5, 1.0)
98 self._spot_light.intensity = 4.0
99 self._spot_light.range = 25.0
100 self._spot_light.inner_cone = 20.0
101 self._spot_light.outer_cone = 35.0
102 self._spot_light.rotation = Quat.from_euler(math.radians(-50), math.radians(-45), 0)
103
104 # Visual marker for the spot light
105 self.add_child(
106 MeshInstance3D(
107 name="SpotLightBulb",
108 mesh=Mesh.sphere(0.12, rings=8, segments=12),
109 material=Material(
110 colour=(0.3, 0.5, 1.0),
111 emissive_colour=(0.3, 0.5, 1.0, 4.0),
112 ),
113 position=Vec3(8.0, 8.0, 8.0),
114 )
115 )
116
117 # ---- Ground plane ----
118 self.add_child(
119 MeshInstance3D(
120 name="Ground",
121 mesh=Mesh.cube(1.0),
122 material=Material(colour=(0.2, 0.2, 0.22), metallic=0.0, roughness=0.9),
123 position=Vec3(0, -0.05, 0),
124 scale=Vec3(25, 0.1, 25),
125 )
126 )
127
128 # ---- Back wall ----
129 self.add_child(
130 MeshInstance3D(
131 name="BackWall",
132 mesh=Mesh.cube(1.0),
133 material=Material(colour=(0.25, 0.22, 0.2), metallic=0.0, roughness=0.85),
134 position=Vec3(0, 5, -10),
135 scale=Vec3(20, 10, 0.2),
136 )
137 )
138
139 # ---- Side wall ----
140 self.add_child(
141 MeshInstance3D(
142 name="SideWall",
143 mesh=Mesh.cube(1.0),
144 material=Material(colour=(0.22, 0.25, 0.2), metallic=0.0, roughness=0.85),
145 position=Vec3(-10, 5, 0),
146 scale=Vec3(0.2, 10, 20),
147 )
148 )
149
150 # ---- Shadow-casting objects around the point light ----
151 objects = [
152 # Central pillar
153 (
154 "Pillar",
155 Mesh.cylinder(0.6, 3.0, segments=16),
156 Material(colour=(0.7, 0.3, 0.1), metallic=0.2, roughness=0.6),
157 Vec3(0, 1.5, 0),
158 ),
159 # Cubes
160 ("CubeA", Mesh.cube(1.2), Material(colour=(0.15, 0.5, 0.8), metallic=0.8, roughness=0.1), Vec3(4, 0.6, -3)),
161 ("CubeB", Mesh.cube(0.8), Material(colour=(0.9, 0.2, 0.2), metallic=0.0, roughness=0.7), Vec3(-3, 0.4, 4)),
162 (
163 "CubeC",
164 Mesh.cube(1.5),
165 Material(colour=(0.85, 0.85, 0.9), metallic=0.95, roughness=0.05),
166 Vec3(-5, 0.75, -5),
167 ),
168 # Spheres
169 (
170 "SphereA",
171 Mesh.sphere(0.8, rings=16, segments=24),
172 Material(colour=(1.0, 0.8, 0.0), metallic=1.0, roughness=0.15),
173 Vec3(3, 0.8, 4),
174 ),
175 (
176 "SphereB",
177 Mesh.sphere(0.5, rings=12, segments=16),
178 Material(colour=(0.1, 0.9, 0.3), metallic=0.0, roughness=0.8),
179 Vec3(-4, 0.5, 0),
180 ),
181 # Cones
182 (
183 "ConeA",
184 Mesh.cone(0.6, 1.8, segments=16),
185 Material(colour=(0.8, 0.4, 0.9), metallic=0.3, roughness=0.4),
186 Vec3(5, 0.9, 0),
187 ),
188 (
189 "ConeB",
190 Mesh.cone(0.5, 1.2, segments=12),
191 Material(colour=(0.95, 0.6, 0.1), metallic=0.0, roughness=0.5),
192 Vec3(0, 0.6, 6),
193 ),
194 ]
195
196 for name, mesh, material, pos in objects:
197 self.add_child(
198 MeshInstance3D(
199 name=name,
200 mesh=mesh,
201 material=material,
202 position=pos,
203 )
204 )
205
206 # ---- Objects in the spot light's cone for spot shadow demo ----
207 spot_objects = [
208 (
209 "SpotCubeA",
210 Mesh.cube(1.0),
211 Material(colour=(0.5, 0.5, 0.6), metallic=0.5, roughness=0.3),
212 Vec3(5, 0.5, 5),
213 ),
214 (
215 "SpotSphere",
216 Mesh.sphere(0.6, rings=12, segments=16),
217 Material(colour=(0.9, 0.5, 0.1), metallic=0.0, roughness=0.6),
218 Vec3(6, 0.6, 6),
219 ),
220 (
221 "SpotCylinder",
222 Mesh.cylinder(0.4, 2.0, segments=12),
223 Material(colour=(0.3, 0.7, 0.5), metallic=0.1, roughness=0.7),
224 Vec3(4, 1.0, 7),
225 ),
226 ]
227 for name, mesh, material, pos in spot_objects:
228 self.add_child(
229 MeshInstance3D(
230 name=name,
231 mesh=mesh,
232 material=material,
233 position=pos,
234 )
235 )
236
237 # ---- HUD ----
238 self.add_child(Text2D(text="POINT & SPOT SHADOW DEMO", x=10, y=8, font_scale=1.6))
239 self.add_child(Text2D(text="WASD/QE: Camera orbit", x=10, y=690, font_scale=1.2))
240 self._time = 0.0
241
242 def on_ready(self):
243 InputMap.add_action("cam_left", [Key.A, Key.LEFT])
244 InputMap.add_action("cam_right", [Key.D, Key.RIGHT])
245 InputMap.add_action("cam_fwd", [Key.W, Key.UP])
246 InputMap.add_action("cam_back", [Key.S, Key.DOWN])
247 InputMap.add_action("cam_up", [Key.Q])
248 InputMap.add_action("cam_down", [Key.E])
249
250 def _update_camera(self):
251 rad = math.radians(self._cam_angle)
252 x = math.cos(rad) * self._cam_dist
253 z = math.sin(rad) * self._cam_dist
254 self.camera.position = Vec3(x, self._cam_height, z)
255 self.camera.look_at(Vec3(0, 2.0, 0))
256
257 def on_process(self, dt: float):
258 self._time += dt
259
260 # Camera controls
261 speed = 45.0
262 if Input.is_action_pressed("cam_left"):
263 self._cam_angle += speed * dt
264 if Input.is_action_pressed("cam_right"):
265 self._cam_angle -= speed * dt
266 if Input.is_action_pressed("cam_fwd"):
267 self._cam_dist = max(8, self._cam_dist - 12 * dt)
268 if Input.is_action_pressed("cam_back"):
269 self._cam_dist = min(40, self._cam_dist + 12 * dt)
270 if Input.is_action_pressed("cam_up"):
271 self._cam_height = min(25, self._cam_height + 8 * dt)
272 if Input.is_action_pressed("cam_down"):
273 self._cam_height = max(2, self._cam_height - 8 * dt)
274 self._update_camera()
275
276 # Gentle point light bob
277 y = 5.0 + math.sin(self._time * 0.8) * 0.5
278 self._point_light.position = Vec3(0.0, y, 0.0)
279 # Update bulb marker too
280 try:
281 bulb = self.children["PointLightBulb"]
282 bulb.position = Vec3(0.0, y, 0.0)
283 except KeyError:
284 pass
285
286
287def main():
288 app = App(title="SimVX Point Shadows Demo", width=WIDTH, height=HEIGHT)
289 app.run(PointShadowsScene())
290
291
292if __name__ == "__main__":
293 main()