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 browser

Tags: 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()