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