Point and Spot Shadows

shadow maps from positional lights.

▶ Run in browser

Tags: 3d

Shadows are opt-in per light: shadows=True makes a light render a shadow map every frame it or what it sees changes. A PointLight3D shines in every direction, so it renders a cubemap and throws shadows all around it; a SpotLight3D renders one projected map, as wide as its cone.

PointLight3D(colour=(1.0, 0.9, 0.7), intensity=3.0, range=30.0, shadows=True)
SpotLight3D(colour=(0.3, 0.5, 1.0), intensity=4.0, inner_cone=20.0, outer_cone=35.0, shadows=True)

Here a warm point light bobs above a cluster of primitives, casting shadows across the floor and up the two walls, and a blue spot light in the corner shines down on a second group. A spot light aims along its forward, so look_at() points it at the group; the cone angles are half-angles in degrees, and between inner_cone and outer_cone the beam fades out. A dim DirectionalLight3D with shadows off keeps the unlit sides readable.

Each light carries a small emissive sphere as its child, so the bulb marks the light and moves with it.

Shadow maps cost a pass each, so WorldEnvironment.shadow_caster_count caps how many point lights and how many spot lights cast at once. It defaults to one of each, which is what this scene uses; shadow_casters.py raises it. A light past the budget still lights the scene and casts nothing.

The OrbitCamera3D takes its own drag, stick and zoom input; the keys are a typed Axis2D orbit action and an Axis1D zoom action fed to camera.orbit() and camera.zoom().

Controls: Drag / right stick / WASD / arrow keys - Orbit the camera Scroll / pinch / triggers / Q and E - Zoom

Source

  1"""Point and Spot Shadows: shadow maps from positional lights.
  2
  3Shadows are opt-in per light: ``shadows=True`` makes a light render a shadow
  4map every frame it or what it sees changes. A ``PointLight3D`` shines in every
  5direction, so it renders a cubemap and throws shadows all around it; a
  6``SpotLight3D`` renders one projected map, as wide as its cone.
  7
  8    PointLight3D(colour=(1.0, 0.9, 0.7), intensity=3.0, range=30.0, shadows=True)
  9    SpotLight3D(colour=(0.3, 0.5, 1.0), intensity=4.0, inner_cone=20.0, outer_cone=35.0, shadows=True)
 10
 11Here a warm point light bobs above a cluster of primitives, casting shadows
 12across the floor and up the two walls, and a blue spot light in the corner
 13shines down on a second group. A spot light aims along its ``forward``, so
 14``look_at()`` points it at the group; the cone angles are half-angles in
 15degrees, and between ``inner_cone`` and ``outer_cone`` the beam fades out. A dim
 16``DirectionalLight3D`` with shadows off keeps the unlit sides readable.
 17
 18Each light carries a small emissive sphere as its child, so the bulb marks the
 19light and moves with it.
 20
 21Shadow maps cost a pass each, so ``WorldEnvironment.shadow_caster_count`` caps
 22how many point lights and how many spot lights cast at once. It defaults to
 23one of each, which is what this scene uses; ``shadow_casters.py`` raises it. A
 24light past the budget still lights the scene and casts nothing.
 25
 26The ``OrbitCamera3D`` takes its own drag, stick and zoom input; the keys are a
 27typed ``Axis2D`` orbit action and an ``Axis1D`` zoom action fed to
 28``camera.orbit()`` and ``camera.zoom()``.
 29
 30Controls:
 31    Drag / right stick / WASD / arrow keys - Orbit the camera
 32    Scroll / pinch / triggers / Q and E    - Zoom
 33"""
 34
 35import math
 36
 37from simvx.core import (
 38    Anchor,
 39    Axis1D,
 40    Axis2D,
 41    CanvasLayer,
 42    DirectionalLight3D,
 43    Input,
 44    Key,
 45    Label,
 46    Material,
 47    Mesh,
 48    MeshInstance3D,
 49    Node3D,
 50    OrbitCamera3D,
 51    PointLight3D,
 52    SpotLight3D,
 53)
 54from simvx.graphics import App
 55
 56KEY_ZOOM_SPEED = 12.0  # world units per second
 57
 58# (mesh, colour, metallic, roughness, position)
 59PROPS = [
 60    (Mesh.cylinder(0.6, 3.0), (0.7, 0.3, 0.1), 0.2, 0.6, (0, 1.5, 0)),
 61    (Mesh.box(1.2), (0.15, 0.5, 0.8), 0.8, 0.1, (4, 0.6, -3)),
 62    (Mesh.box(0.8), (0.9, 0.2, 0.2), 0.0, 0.7, (-3, 0.4, 4)),
 63    (Mesh.box(1.5), (0.85, 0.85, 0.9), 0.95, 0.05, (-5, 0.75, -5)),
 64    (Mesh.sphere(0.8, rings=16, segments=24), (1.0, 0.8, 0.0), 1.0, 0.15, (3, 0.8, 4)),
 65    (Mesh.sphere(0.5), (0.1, 0.9, 0.3), 0.0, 0.8, (-4, 0.5, 0)),
 66    (Mesh.cone(0.6, 1.8), (0.8, 0.4, 0.9), 0.3, 0.4, (5, 0.9, 0)),
 67    (Mesh.cone(0.5, 1.2), (0.95, 0.6, 0.1), 0.0, 0.5, (0, 0.6, 6)),
 68    # The group under the spot light.
 69    (Mesh.box(1.0), (0.5, 0.5, 0.6), 0.5, 0.3, (5, 0.5, 5)),
 70    (Mesh.sphere(0.6), (0.9, 0.5, 0.1), 0.0, 0.6, (6, 0.6, 6)),
 71    (Mesh.cylinder(0.4, 2.0), (0.3, 0.7, 0.5), 0.1, 0.7, (4, 1.0, 7)),
 72]
 73
 74
 75def bulb(radius, colour, strength):
 76    glow = Material(colour=colour, emissive_colour=colour, emissive_strength=strength)
 77    return MeshInstance3D(mesh=Mesh.sphere(radius, rings=8, segments=12), material=glow)
 78
 79
 80class PointShadowsScene(Node3D):
 81    input_context = {"orbit": [Axis2D.WASD, Axis2D.ARROWS], "zoom": [Axis1D(low=Key.Q, high=Key.E)]}
 82
 83    def on_ready(self):
 84        self.camera = self.add_child(
 85            OrbitCamera3D(
 86                pivot=(0, 2, 0),
 87                distance=24,
 88                yaw=1.05,
 89                pitch=-0.43,
 90                min_distance=8,
 91                max_distance=40,
 92                min_pitch=-1.3,
 93                max_pitch=-0.05,
 94            )
 95        )
 96
 97        self.add_child(DirectionalLight3D(colour=(0.15, 0.15, 0.2), intensity=0.3, direction=(0.25, -0.87, -0.43)))
 98
 99        self.point_light = self.add_child(
100            PointLight3D(colour=(1.0, 0.9, 0.7), intensity=3.0, range=30.0, shadows=True, position=(0, 5, 0))
101        )
102        self.point_light.add_child(bulb(0.15, (1.0, 0.9, 0.7), 5.0))
103
104        spot = self.add_child(
105            SpotLight3D(
106                colour=(0.3, 0.5, 1.0),
107                intensity=4.0,
108                range=25.0,
109                inner_cone=20.0,
110                outer_cone=35.0,
111                shadows=True,
112                position=(8, 8, 8),
113            )
114        )
115        spot.look_at((5, 0, 6))
116        spot.add_child(bulb(0.12, (0.3, 0.5, 1.0), 4.0))
117
118        self.add_child(MeshInstance3D(mesh=Mesh.plane(25.0), material=Material(colour=(0.2, 0.2, 0.22), roughness=0.9)))
119        for size, colour, position in [
120            ((20, 10, 0.2), (0.25, 0.22, 0.2), (0, 5, -10)),
121            ((0.2, 10, 20), (0.22, 0.25, 0.2), (-10, 5, 0)),
122        ]:
123            self.add_child(
124                MeshInstance3D(mesh=Mesh.box(size), material=Material(colour=colour, roughness=0.85), position=position)
125            )
126        for mesh, colour, metallic, roughness, position in PROPS:
127            material = Material(colour=colour, metallic=metallic, roughness=roughness)
128            self.add_child(MeshInstance3D(mesh=mesh, material=material, position=position))
129
130        # Example help overlay: not part of the technique shown.
131        hint = "Drag, right stick, WASD or arrows: orbit   Scroll, pinch or Q/E: zoom"
132        self.add_child(CanvasLayer()).add_child(Label(hint, anchor=Anchor.BOTTOM_LEFT, inset=10, font_size=16))
133
134    def on_update(self, dt):
135        # A gentle bob, so the point light's shadows visibly move; the bulb rides along as its child.
136        self.point_light.position = (0, 5 + 0.5 * math.sin(self.tree.now * 0.8), 0)
137
138        keys = Input.value("orbit")
139        speed = self.camera.stick_speed * dt
140        self.camera.orbit(-keys.x * speed, -keys.y * speed)
141        self.camera.zoom(Input.value("zoom") * KEY_ZOOM_SPEED * dt)
142
143
144if __name__ == "__main__":
145    App(title="Point and Spot Shadows", width=1280, height=720).run(PointShadowsScene())