Shadow Caster Budget¶
several point lights casting at once.
▶ Run in browserTags: 3d
A light asks for a shadow with shadows=True, and
WorldEnvironment.shadow_caster_count decides how many point lights, and how
many spot lights, get one in the same frame:
WorldEnvironment(shadow_caster_count=4)
PointLight3D(colour=(1.0, 0.75, 0.4), intensity=6.0, range=14.0, shadows=True)
At the default quality it is one of each, and 0 switches positional shadows off. A point light’s shadow is a cubemap, so each caster draws every shadow-casting mesh six times, and again whenever its light, the geometry or a material they are drawn with changes; a still scene draws its maps once and keeps them. Treat the budget as a quality dial to raise deliberately, not a limit to max out. Changing it reallocates the shadow atlases, so the frame it changes hitches: set it once for a scene, not per frame.
Four coloured lamps ring the floor here, each beside its own block. Each lamp’s range barely reaches its neighbours, so every shadow belongs to one lamp. Press 1 to 4, or the buttons, to set the budget: the lamps that fit it cast, the rest still light the scene and cast nothing, and the renderer logs a warning naming how many were left out.
Which lamps fit is decided by PointLight3D.shadow_priority first, then by
how much each lamp is worth to the camera (its brightness and reach against its
distance), so orbiting hands the shadow to the lamp you approach. A lamp already
casting keeps its map until another is worth a clear margin more, so the
handover lands just past the halfway point between two lamps rather than on it.
Controls: 1 / 2 / 3 / 4 / buttons - Shadow caster budget Drag / right stick / A D - Orbit the camera Scroll / pinch / W S - Zoom
Source¶
1"""Shadow Caster Budget: several point lights casting at once.
2
3A light asks for a shadow with ``shadows=True``, and
4``WorldEnvironment.shadow_caster_count`` decides how many point lights, and how
5many spot lights, get one in the same frame:
6
7 WorldEnvironment(shadow_caster_count=4)
8 PointLight3D(colour=(1.0, 0.75, 0.4), intensity=6.0, range=14.0, shadows=True)
9
10At the default quality it is one of each, and 0 switches positional shadows
11off. A point light's shadow is a cubemap, so each caster draws every
12shadow-casting mesh six times, and again whenever its light, the geometry or a
13material they are drawn with changes; a still scene draws its maps once and
14keeps them. Treat the budget as a quality dial to raise deliberately, not a
15limit to max out. Changing it reallocates the shadow atlases, so the frame it
16changes hitches: set it once for a scene, not per frame.
17
18Four coloured lamps ring the floor here, each beside its own block. Each lamp's
19range barely reaches its neighbours, so every shadow belongs to one lamp. Press
201 to 4, or the buttons, to set the budget: the lamps that fit it cast, the rest
21still light the scene and cast nothing, and the renderer logs a warning naming
22how many were left out.
23
24Which lamps fit is decided by ``PointLight3D.shadow_priority`` first, then by
25how much each lamp is worth to the camera (its brightness and reach against its
26distance), so orbiting hands the shadow to the lamp you approach. A lamp already
27casting keeps its map until another is worth a clear margin more, so the
28handover lands just past the halfway point between two lamps rather than on it.
29
30Controls:
31 1 / 2 / 3 / 4 / buttons - Shadow caster budget
32 Drag / right stick / A D - Orbit the camera
33 Scroll / pinch / W S - Zoom
34"""
35
36import math
37
38from simvx.core import (
39 Anchor,
40 Axis1D,
41 Button,
42 ButtonGroup,
43 CanvasLayer,
44 HBoxContainer,
45 Input,
46 Key,
47 Label,
48 Material,
49 Mesh,
50 MeshInstance3D,
51 Node3D,
52 OrbitCamera3D,
53 PointLight3D,
54 WorldEnvironment,
55 on_input,
56)
57from simvx.graphics import App
58
59LAMP_COLOURS = [(1.0, 0.75, 0.4), (0.4, 0.8, 1.0), (0.5, 1.0, 0.5), (1.0, 0.5, 0.8)]
60BUDGETS = {"budget_1": 1, "budget_2": 2, "budget_3": 3, "budget_4": 4}
61KEY_ZOOM_SPEED = 20.0 # world units per second
62
63
64class ShadowCasters(Node3D):
65 input_context = {
66 "budget_1": [Key.KEY_1],
67 "budget_2": [Key.KEY_2],
68 "budget_3": [Key.KEY_3],
69 "budget_4": [Key.KEY_4],
70 "orbit": [Axis1D(low=Key.A, high=Key.D), Axis1D(low=Key.LEFT, high=Key.RIGHT)],
71 "zoom": [Axis1D(low=Key.S, high=Key.W), Axis1D(low=Key.DOWN, high=Key.UP)],
72 }
73
74 def on_ready(self):
75 self.env = self.add_child(
76 WorldEnvironment(
77 shadow_caster_count=4,
78 sky_colour_top=(0.03, 0.03, 0.05, 1.0),
79 sky_colour_bottom=(0.05, 0.05, 0.07, 1.0),
80 ambient_light_colour=(0.04, 0.04, 0.06, 1.0),
81 )
82 )
83 self.camera = self.add_child(
84 OrbitCamera3D(
85 pivot=(0, 1.5, 0),
86 distance=61,
87 yaw=math.radians(55),
88 pitch=-0.56,
89 min_distance=40,
90 max_distance=90,
91 min_pitch=-1.3,
92 max_pitch=-0.2,
93 fov=55,
94 far=200,
95 )
96 )
97 floor = Material(colour=(0.75, 0.75, 0.78), roughness=0.9)
98 self.add_child(MeshInstance3D(mesh=Mesh.plane(80.0), material=floor))
99
100 block, stone = Mesh.box(2.0), Material(colour=(0.55, 0.5, 0.5), roughness=0.7)
101 for i, colour in enumerate(LAMP_COLOURS):
102 angle = math.tau * i / len(LAMP_COLOURS)
103 x, z = math.cos(angle), math.sin(angle)
104 self.add_child(MeshInstance3D(mesh=block, material=stone, position=(x * 22, 1, z * 22)))
105 # Every lamp asks for a shadow; the budget decides which get one.
106 lamp = self.add_child(
107 PointLight3D(colour=colour, intensity=6.0, range=14.0, shadows=True, position=(x * 25, 5, z * 25))
108 )
109 glow = Material(colour=colour, emissive_colour=colour, emissive_strength=4.0)
110 lamp.add_child(MeshInstance3D(mesh=Mesh.sphere(0.2, rings=8, segments=12), material=glow))
111
112 hud = self.add_child(CanvasLayer())
113 self.readout = hud.add_child(Label(anchor=Anchor.TOP_LEFT, inset=12, font_size=18))
114 bar = hud.add_child(HBoxContainer(anchor=Anchor.BOTTOM_LEFT, inset=12))
115 choice = ButtonGroup()
116 self.buttons = {name: bar.add_child(Button(action=name, group=choice)) for name in BUDGETS}
117 self.show_budget()
118
119 # Example help overlay: not part of the technique shown.
120 help_text = "Drag, right stick or A/D: orbit Scroll, pinch or W/S: zoom"
121 hud.add_child(Label(help_text, anchor=Anchor.TOP_RIGHT, inset=12, font_size=14))
122
123 @on_input(tuple(BUDGETS))
124 def set_budget(self, event):
125 self.env.shadow_caster_count = BUDGETS[event.action]
126 self.show_budget()
127
128 def show_budget(self):
129 budget = self.env.shadow_caster_count
130 casting = min(budget, len(LAMP_COLOURS))
131 self.readout.text = f"shadow_caster_count = {budget} ({casting} of {len(LAMP_COLOURS)} lamps casting)"
132 for name, button in self.buttons.items():
133 button.checked = BUDGETS[name] == budget
134
135 def on_update(self, dt):
136 self.camera.orbit(-Input.value("orbit") * self.camera.stick_speed * dt, 0.0)
137 self.camera.zoom(Input.value("zoom") * KEY_ZOOM_SPEED * dt)
138
139
140if __name__ == "__main__":
141 App(title="Shadow Caster Budget", width=1280, height=720).run(ShadowCasters())