Point and Spot Shadows¶
shadow maps from positional lights.
▶ Run in browserTags: 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())