Shadow quality demo

cascade debug visualization and texel snapping.

▶ Run in browser

Tags: 3d

Demonstrates:

  • Cascaded shadow maps with texel snapping (prevents edge swimming)

  • Debug cascade colouring: red=near, green=mid, blue=far

  • Runtime cascade count (1, 2, or 3) via WorldEnvironment

Controls: D - Toggle cascade debug colour overlay 1 / 2 / 3 - Set active cascade count Escape - Quit

Run: uv run python examples/features/3d/shadows.py

Source

  1"""Shadow quality demo -- cascade debug visualization and texel snapping.
  2
  3Demonstrates:
  4  - Cascaded shadow maps with texel snapping (prevents edge swimming)
  5  - Debug cascade colouring: red=near, green=mid, blue=far
  6  - Runtime cascade count (1, 2, or 3) via WorldEnvironment
  7
  8Controls:
  9    D       - Toggle cascade debug colour overlay
 10    1 / 2 / 3 - Set active cascade count
 11    Escape  - Quit
 12
 13Run: uv run python examples/features/3d/shadows.py
 14"""
 15
 16
 17import math
 18
 19from simvx.core import (
 20    Camera3D,
 21    DirectionalLight3D,
 22    Input,
 23    InputMap,
 24    Key,
 25    Material,
 26    Mesh,
 27    MeshInstance3D,
 28    Node3D,
 29    Text2D,
 30    WorldEnvironment,
 31)
 32from simvx.graphics import App
 33
 34
 35class ShadowQualityScene(Node3D):
 36    def on_ready(self):
 37        InputMap.add_action("toggle_debug", [Key.D])
 38        InputMap.add_action("cascades_1", [Key.KEY_1])
 39        InputMap.add_action("cascades_2", [Key.KEY_2])
 40        InputMap.add_action("cascades_3", [Key.KEY_3])
 41        InputMap.add_action("quit", [Key.ESCAPE])
 42
 43        # WorldEnvironment: cascade debug toggle + runtime count both flow
 44        # through EnvironmentSync into the shared shadow SSBO
 45        # (``debug_cascades_flag`` and ``active_cascade_count``).
 46        self._env = self.add_child(WorldEnvironment(name="Env"))
 47        self._env.shadow_debug_cascades = False
 48
 49        # Camera
 50        self._cam = self.add_child(Camera3D(
 51            position=(0, -25, 12), fov=60, look_at=(0, 0, 0), up=(0, 0, 1),
 52        ))
 53
 54        # Sun (directional light producing shadows)
 55        sun = DirectionalLight3D(position=(-8, -12, 10))
 56        sun.colour = (1.0, 0.95, 0.85)
 57        sun.intensity = 1.2
 58        sun.shadows = True  # directional shadows are opt-in
 59        sun.look_at((0, 0, 0))
 60        self.add_child(sun)
 61
 62        # Ground plane
 63        ground_mat = Material(colour=(0.35, 0.45, 0.35, 1), roughness=0.9, metallic=0.0)
 64        ground = MeshInstance3D(mesh=Mesh.cube(), material=ground_mat, position=(0, 0, -0.5))
 65        ground.scale = (30, 30, 0.2)
 66        self.add_child(ground)
 67
 68        # Scatter objects at various distances to show cascade splits
 69        cube_mesh = Mesh.cube()
 70        sphere_mesh = Mesh.sphere()
 71        colours = [
 72            (0.9, 0.3, 0.2, 1),
 73            (0.2, 0.7, 0.9, 1),
 74            (0.9, 0.8, 0.2, 1),
 75            (0.6, 0.3, 0.8, 1),
 76            (0.3, 0.9, 0.4, 1),
 77            (0.9, 0.5, 0.1, 1),
 78        ]
 79        # Near objects
 80        for i in range(4):
 81            mat = Material(colour=colours[i % len(colours)], roughness=0.4, metallic=0.2)
 82            self.add_child(MeshInstance3D(mesh=cube_mesh, material=mat, position=(i * 2 - 3, -2, 1)))
 83
 84        # Mid-range objects
 85        for i in range(5):
 86            mat = Material(colour=colours[(i + 2) % len(colours)], roughness=0.3, metallic=0.5)
 87            self.add_child(MeshInstance3D(mesh=sphere_mesh, material=mat, position=(i * 3 - 6, 5, 1.2)))
 88
 89        # Far objects
 90        for i in range(3):
 91            mat = Material(colour=colours[(i + 4) % len(colours)], roughness=0.5, metallic=0.1)
 92            obj = MeshInstance3D(mesh=cube_mesh, material=mat, position=(i * 4 - 4, 15, 1.5))
 93            obj.scale = (1.5, 1.5, 3.0)
 94            self.add_child(obj)
 95
 96        # Tall pillars casting long shadows
 97        pillar_mat = Material(colour=(0.7, 0.7, 0.75, 1), roughness=0.6, metallic=0.3)
 98        for x in (-8, 0, 8):
 99            pillar = MeshInstance3D(mesh=cube_mesh, material=pillar_mat, position=(x, 0, 3))
100            pillar.scale = (0.5, 0.5, 6.0)
101            self.add_child(pillar)
102
103        # HUD
104        self._hud = self.add_child(
105            Text2D(text="", x=10, y=10, font_scale=1.5)
106        )
107
108        self._time = 0.0
109
110    def on_process(self, dt):
111        self._time += dt
112
113        if Input.is_action_just_pressed("quit"):
114            self.app.quit()
115            return
116
117        if Input.is_action_just_pressed("toggle_debug"):
118            self._env.shadow_debug_cascades = not self._env.shadow_debug_cascades
119        if Input.is_action_just_pressed("cascades_1"):
120            self._env.shadow_cascade_count = 1
121        if Input.is_action_just_pressed("cascades_2"):
122            self._env.shadow_cascade_count = 2
123        if Input.is_action_just_pressed("cascades_3"):
124            self._env.shadow_cascade_count = 3
125
126        debug = "ON" if self._env.shadow_debug_cascades else "OFF"
127        self._hud.text = (
128            f"[D] Debug cascades: {debug}    "
129            f"[1/2/3] Active cascades: {self._env.shadow_cascade_count}"
130        )
131
132        # Slowly orbit camera so shadows move and cascade transitions are visible
133        r = 25.0
134        angle = self._time * 0.15
135        self._cam.position = (r * math.sin(angle), -r * math.cos(angle), 12)
136        self._cam.look_at((0, 0, 0), up=(0, 0, 1))
137
138
139if __name__ == "__main__":
140    app = App(title="Shadow Quality Demo", width=1280, height=720)
141    app.run(ShadowQualityScene())