Shadow Caster Budget

several point lights casting at once.

▶ Run in browser

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