Point and spot shadows

PointLight3D and SpotLight3D with shadows=True.

▶ Run in browser

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