Irradiance Probe Volume

baked diffuse light that bleeds wall colour onto a moving object.

▶ Run in browser

Tags: 3d gi irradiance-volume probe colour-bleed pbr

An IrradianceVolume3D is a box-shaped grid of light probes. Each probe records the diffuse light arriving at its position from every direction, as spherical-harmonic coefficients. A surface inside the box blends the eight probes around it and reads the light coming from the way it faces, so an object moving through the room picks up soft bounce light from the walls:

IrradianceVolume3D(extents=(4.4, 3.2, 4.4), spacing=1.8, position=(0, 3, -1))

extents are half-extents of the box, centred on the node, and spacing is the distance between probes. intensity scales the indirect light, and update_budget is how many probes are baked per frame, so a large grid spreads its bake over the first few frames. bake_mode chooses when it bakes: "once" (the default, for static lighting), "always" (re-bakes continuously for lighting that changes) or "disabled" (the volume does nothing). Call request_update() after moving the volume or its lights.

Here a plain grey sphere glides between a red wall and a green wall. It has no colour of its own to show, so the red tint it takes on near the red wall and the green tint near the green wall come from the baked probes. The ambient light is kept very low so the bounce is the main fill. Unlike screen-space GI, the volume keeps working when the walls are off screen, and unlike a reflection probe it gives a soft diffuse term that suits matte surfaces.

Controls: Space / Volume on/off - Toggle the volume

Source

  1# /// simvx
  2# tags = ["3d", "gi", "irradiance-volume", "probe", "colour-bleed", "pbr"]
  3# screenshot_frame = 45
  4# ///
  5"""Irradiance Probe Volume: baked diffuse light that bleeds wall colour onto a moving object.
  6
  7An ``IrradianceVolume3D`` is a box-shaped grid of light probes. Each probe
  8records the diffuse light arriving at its position from every direction, as
  9spherical-harmonic coefficients. A surface inside the box blends the eight
 10probes around it and reads the light coming from the way it faces, so an
 11object moving through the room picks up soft bounce light from the walls:
 12
 13    IrradianceVolume3D(extents=(4.4, 3.2, 4.4), spacing=1.8, position=(0, 3, -1))
 14
 15``extents`` are half-extents of the box, centred on the node, and ``spacing``
 16is the distance between probes. ``intensity`` scales the indirect light, and
 17``update_budget`` is how many probes are baked per frame, so a large grid
 18spreads its bake over the first few frames. ``bake_mode`` chooses when it
 19bakes: ``"once"`` (the default, for static lighting), ``"always"`` (re-bakes
 20continuously for lighting that changes) or ``"disabled"`` (the volume does
 21nothing). Call ``request_update()`` after moving the volume or its lights.
 22
 23Here a plain grey sphere glides between a red wall and a green wall. It has
 24no colour of its own to show, so the red tint it takes on near the red wall
 25and the green tint near the green wall come from the baked probes. The
 26ambient light is kept very low so the bounce is the main fill. Unlike
 27screen-space GI, the volume keeps working when the walls are off screen, and
 28unlike a reflection probe it gives a soft diffuse term that suits matte
 29surfaces.
 30
 31Controls:
 32    Space / Volume on/off   - Toggle the volume
 33"""
 34
 35import math
 36
 37from simvx.core import (
 38    Anchor,
 39    Button,
 40    Camera3D,
 41    CanvasLayer,
 42    DirectionalLight3D,
 43    IrradianceVolume3D,
 44    Key,
 45    Label,
 46    Material,
 47    Mesh,
 48    MeshInstance3D,
 49    Node3D,
 50    WorldEnvironment,
 51    on_input,
 52)
 53from simvx.graphics import App
 54
 55
 56class ProbeVolumeDemo(Node3D):
 57    input_context = {"toggle_volume": [Key.SPACE]}
 58
 59    def on_ready(self):
 60        camera = self.add_child(Camera3D(fov=55, near=0.1, far=60.0, position=(0, 2.8, 9)))
 61        camera.look_at((0, 2.2, -2))
 62        self.add_child(DirectionalLight3D(colour=(1.0, 0.97, 0.92), intensity=1.7, direction=(-0.1, -0.94, -0.32)))
 63        self.add_child(WorldEnvironment(ambient_light_colour=(0.04, 0.04, 0.05), ambient_light_energy=0.5))
 64
 65        # An open-fronted box: white floor, ceiling and back, a red left wall and a green right wall.
 66        white = Material(colour=(0.8, 0.8, 0.8), roughness=0.9)
 67        red = Material(colour=(0.9, 0.06, 0.06), roughness=1.0)
 68        green = Material(colour=(0.06, 0.85, 0.1), roughness=1.0)
 69        for size, position, material in [
 70            ((10, 0.3, 10), (0, 0, -1), white),
 71            ((10, 0.3, 10), (0, 5, -1), white),
 72            ((10, 10, 0.3), (0, 2.5, -6), white),
 73            ((0.3, 10, 10), (-5, 2.5, -1), red),
 74            ((0.3, 10, 10), (5, 2.5, -1), green),
 75        ]:
 76            self.add_child(MeshInstance3D(mesh=Mesh.box(size), material=material, position=position))
 77
 78        grey = Material(colour=(0.85, 0.85, 0.85), roughness=0.85)
 79        self.sphere = self.add_child(MeshInstance3D(mesh=Mesh.sphere(1.3, segments=48), material=grey))
 80
 81        self.volume = self.add_child(
 82            IrradianceVolume3D(
 83                extents=(4.4, 3.2, 4.4), spacing=1.8, position=(0, 3, -1), intensity=2.4, update_budget=24
 84            )
 85        )
 86
 87        hud = self.add_child(CanvasLayer())
 88        self.status = hud.add_child(Label(anchor=Anchor.TOP_LEFT, inset=10, font_size=20))
 89
 90        # Example help overlay: not part of the technique shown.
 91        hud.add_child(Button("Volume on/off", action="toggle_volume", anchor=Anchor.BOTTOM_RIGHT, inset=10))
 92        hud.add_child(Label("Space: toggle the volume", anchor=Anchor.BOTTOM_LEFT, inset=10, font_size=14))
 93
 94    @on_input("toggle_volume")
 95    def toggle_volume(self, _event):
 96        self.volume.bake_mode = "disabled" if self.volume.bake_mode == "once" else "once"
 97
 98    def on_update(self, dt):
 99        # Sweep from the red wall through the middle to the green wall and back.
100        self.sphere.position = (-2.8 * math.sin(self.tree.now * 0.9), 2.2, -1.0)
101        on = self.volume.bake_mode != "disabled"
102        self.status.text = f"Irradiance volume: {'ON' if on else 'OFF'}"
103
104
105if __name__ == "__main__":
106    App(title="Irradiance Probe Volume", width=1280, height=720).run(ProbeVolumeDemo())