Irradiance probe volume¶
baked diffuse GI colour-bleed onto a moving object.
▶ Run in browserTags: 3d gi irradiance-volume probe colour-bleed pbr
An IrradianceVolume3D (design D10) bakes a grid of diffuse-lighting probes from the scene, each storing the incoming radiance as spherical-harmonic L1 coefficients. A neutral sphere gliding through the volume trilinearly blends the eight probes around it and evaluates their SH for each surface normal, so it picks up soft indirect light baked from the surrounding walls: reddish as it nears the red wall, greenish near the green wall, with a smooth wash in between. Unlike screen-space GI this needs no on-screen source and survives off-screen geometry, and unlike a reflection probe it is a low-frequency DIFFUSE term that reads correctly on rough, matte surfaces.
The grid bakes once over the first few frames (update_budget probes/frame),
then lights every subsequent frame for free.
Usage: uv run python examples/features/3d/probe_volume.py
Controls: Space - Toggle the volume on/off Escape - Quit
Source¶
1"""Irradiance probe volume: baked diffuse GI colour-bleed onto a moving object.
2
3An IrradianceVolume3D (design D10) bakes a grid of diffuse-lighting probes from
4the scene, each storing the incoming radiance as spherical-harmonic L1
5coefficients. A neutral sphere gliding through the volume trilinearly blends the
6eight probes around it and evaluates their SH for each surface normal, so it
7picks up soft indirect light baked from the surrounding walls: reddish as it
8nears the red wall, greenish near the green wall, with a smooth wash in between.
9Unlike screen-space GI this needs no on-screen source and survives off-screen
10geometry, and unlike a reflection probe it is a low-frequency DIFFUSE term that
11reads correctly on rough, matte surfaces.
12
13The grid bakes once over the first few frames (``update_budget`` probes/frame),
14then lights every subsequent frame for free.
15
16# /// simvx
17# tags = ["3d", "gi", "irradiance-volume", "probe", "colour-bleed", "pbr"]
18# screenshot_frame = 45
19# ///
20
21Usage:
22 uv run python examples/features/3d/probe_volume.py
23
24Controls:
25 Space - Toggle the volume on/off
26 Escape - Quit
27"""
28
29import math
30
31from simvx.core import (
32 Camera3D,
33 DirectionalLight3D,
34 Input,
35 InputMap,
36 IrradianceVolume3D,
37 Key,
38 Material,
39 Mesh,
40 MeshInstance3D,
41 Node3D,
42 Text2D,
43 Vec3,
44 WorldEnvironment,
45)
46from simvx.graphics import App
47
48WIDTH, HEIGHT = 1280, 720
49
50
51class ProbeVolumeScene(Node3D):
52 def __init__(self, **kwargs):
53 super().__init__(name="ProbeVolumeDemo", **kwargs)
54
55 # Camera looking into an open-fronted box so both coloured side walls and
56 # the moving sphere between them fill the frame.
57 self.camera = self.add_child(Camera3D(name="Camera", fov=55, near=0.1, far=60.0))
58 self.camera.position = Vec3(0.0, 2.8, 9.0)
59 self.camera.look_at(Vec3(0.0, 2.2, -2.0))
60
61 # A dim directional key so the walls are lit (the baked radiance the
62 # volume captures) without washing the scene; flat ambient stays low so
63 # the baked indirect reads clearly against it.
64 sun = self.add_child(DirectionalLight3D(name="Sun"))
65 sun.direction = Vec3(-0.1, -0.94, -0.32)
66 sun.colour = (1.0, 0.97, 0.92)
67 sun.intensity = 1.7
68
69 white = Material(colour=(0.80, 0.80, 0.80), metallic=0.0, roughness=0.9)
70 red = Material(colour=(0.90, 0.06, 0.06), metallic=0.0, roughness=1.0)
71 green = Material(colour=(0.06, 0.85, 0.10), metallic=0.0, roughness=1.0)
72
73 span = 5.0
74 thick = 0.3
75
76 def wall(name, mat, position, scale):
77 self.add_child(MeshInstance3D(name=name, mesh=Mesh.cube(1.0), material=mat, position=position, scale=scale))
78
79 wall("Floor", white, Vec3(0, 0, -1), Vec3(span * 2, thick, span * 2))
80 wall("Ceiling", white, Vec3(0, span, -1), Vec3(span * 2, thick, span * 2))
81 wall("BackWall", white, Vec3(0, span * 0.5, -1 - span), Vec3(span * 2, span * 2, thick))
82 wall("LeftWall", red, Vec3(-span, span * 0.5, -1), Vec3(thick, span * 2, span * 2))
83 wall("RightWall", green, Vec3(span, span * 0.5, -1), Vec3(thick, span * 2, span * 2))
84
85 # The moving witness: a neutral matte sphere with NO texture, so its only
86 # colour comes from lighting. It glides across the box between the walls.
87 self._sphere = self.add_child(
88 MeshInstance3D(
89 name="Sphere",
90 mesh=Mesh.sphere(1.3, segments=48),
91 material=Material(colour=(0.85, 0.85, 0.85), metallic=0.0, roughness=0.85),
92 position=Vec3(0.0, 2.2, -1.0),
93 )
94 )
95
96 # The probe volume spanning the interior. "once" bakes the whole grid over
97 # the first few frames, then lights every frame.
98 self._vol = self.add_child(IrradianceVolume3D(name="Volume", extents=(4.4, 3.2, 4.4), spacing=1.8))
99 self._vol.position = Vec3(0.0, 3.0, -1.0)
100 self._vol.intensity = 2.4
101 self._vol.update_budget = 24
102
103 self._env = self.add_child(WorldEnvironment(name="Env"))
104 # Very low flat ambient: the baked volume is the dominant fill light, so
105 # the coloured bleed on the sphere is unmistakable.
106 self._env.ambient_light_colour = (0.04, 0.04, 0.05)
107 self._env.ambient_light_energy = 0.5
108
109 self._on = True
110 self._cooldown = 0.0
111 self._t = 0.0
112 self._title = self.add_child(Text2D(text="IRRADIANCE PROBE VOLUME", position=(10, 8), font_scale=1.5))
113 self._status = self.add_child(Text2D(text="VOLUME: ON", position=(10, 40), font_scale=1.3))
114 self.add_child(Text2D(text="SPACE:Toggle Volume ESC:Quit", position=(10, 690), font_scale=1.1))
115
116 def on_ready(self):
117 InputMap.add_action("toggle_volume", [Key.SPACE])
118 InputMap.add_action("quit", [Key.ESCAPE])
119
120 def on_update(self, dt: float):
121 self._t += dt
122 # Glide the sphere between the red (left) and green (right) walls so the
123 # baked indirect it picks up sweeps from red through neutral to green.
124 x = -2.8 * math.sin(self._t * 0.9)
125 self._sphere.position = Vec3(x, 2.2, -1.0)
126
127 self._cooldown = max(0.0, self._cooldown - dt)
128 if Input.is_action_just_pressed("toggle_volume") and self._cooldown <= 0.0:
129 self._on = not self._on
130 self._cooldown = 0.3
131 self._vol.bake_mode = "once" if self._on else "disabled"
132 self._status.text = f"VOLUME: {'ON' if self._on else 'OFF'}"
133 if Input.is_action_just_pressed("quit"):
134 self.app.quit()
135
136
137def main():
138 scene = ProbeVolumeScene()
139 app = App(title="SimVX Irradiance Probe Volume", width=WIDTH, height=HEIGHT, physics_fps=60)
140 app.run(scene)
141
142
143if __name__ == "__main__":
144 main()