Screen-space global illumination

coloured light bleeding between surfaces.

▶ Run in browser

Tags: 3d ssgi global-illumination colour-bleed pbr

Screen-space GI casts a half-resolution hemisphere of rays from every lit surface, marches them through the depth buffer + thin G-buffer, and gathers the on-screen scene colour they hit: one bounce of indirect diffuse. The result feeds the pluggable indirect-diffuse ambient hook, so a brightly lit red wall throws a red tint across the white floor beside it and the green wall tints the other side, all on top of the flat ambient (a Cornell-box classic). The gather is Hi-Z traced (sharing the SSR trace utilities) and temporally accumulated to denoise, so the bounce converges over a few frames; fast camera motion trails a little (an accepted tradeoff). A direction that leaves the screen or finds no near surface gathers nothing, so open floor keeps the flat ambient and nothing is crushed.

Usage: uv run python examples/features/3d/ssgi.py

Controls: Space / click / tap - Toggle SSGI on/off Escape - Quit

Source

  1"""Screen-space global illumination: coloured light bleeding between surfaces.
  2
  3Screen-space GI casts a half-resolution hemisphere of rays from every
  4lit surface, marches them through the depth buffer + thin G-buffer, and gathers
  5the on-screen scene colour they hit: one bounce of indirect diffuse. The result
  6feeds the pluggable indirect-diffuse ambient hook, so a brightly lit
  7red wall throws a red tint across the white floor beside it and the green wall
  8tints the other side, all on top of the flat ambient (a Cornell-box classic).
  9The gather is Hi-Z traced (sharing the SSR trace utilities) and temporally
 10accumulated to denoise, so the bounce converges over a few frames; fast camera
 11motion trails a little (an accepted tradeoff). A direction that leaves the screen
 12or finds no near surface gathers nothing, so open floor keeps the flat ambient
 13and nothing is crushed.
 14
 15# /// simvx
 16# tags = ["3d", "ssgi", "global-illumination", "colour-bleed", "pbr"]
 17# screenshot_frame = 45
 18# ///
 19
 20Usage:
 21    uv run python examples/features/3d/ssgi.py
 22
 23Controls:
 24    Space / click / tap - Toggle SSGI on/off
 25    Escape              - Quit
 26"""
 27
 28import math
 29
 30from simvx.core import (
 31    Camera3D,
 32    Input,
 33    InputMap,
 34    Key,
 35    Material,
 36    Mesh,
 37    MeshInstance3D,
 38    MouseButton,
 39    Node3D,
 40    PointLight3D,
 41    Quat,
 42    Text2D,
 43    Vec3,
 44    WorldEnvironment,
 45)
 46from simvx.graphics import App
 47
 48WIDTH, HEIGHT = 1280, 720
 49
 50
 51class SSGIScene(Node3D):
 52    def __init__(self, **kwargs):
 53        super().__init__(name="SSGIDemo", **kwargs)
 54
 55        # Camera looking down the axis into an open-fronted box, so the coloured
 56        # side walls and the white floor 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, 3.0, 9.5)
 59        self.camera.look_at(Vec3(0.0, 2.6, -2.0))
 60
 61        # A single bright point light near the ceiling: it lights the coloured
 62        # walls directly, and SSGI carries their colour onto the neutral surfaces.
 63        lamp = self.add_child(PointLight3D(name="Lamp", position=Vec3(0.0, 5.4, 1.0)))
 64        lamp.colour = (1.0, 0.97, 0.92)
 65        lamp.intensity = 20.0
 66        lamp.range = 24.0
 67        lamp.shadows = True  # sole key light: cast shadows (point shadows are opt-in)
 68
 69        # Cornell-style box: white floor / ceiling / back wall, a saturated red
 70        # left wall and green right wall (matte, so they bounce diffusely). Rough,
 71        # non-metallic surfaces so the effect is pure indirect diffuse.
 72        white = Material(colour=(0.80, 0.80, 0.80), metallic=0.0, roughness=0.9)
 73        red = Material(colour=(0.85, 0.08, 0.08), metallic=0.0, roughness=0.9)
 74        green = Material(colour=(0.08, 0.75, 0.12), metallic=0.0, roughness=0.9)
 75
 76        span = 6.0  # interior half-width / height
 77        thick = 0.3
 78
 79        def wall(name, mat, position, scale):
 80            self.add_child(
 81                MeshInstance3D(
 82                    name=name,
 83                    mesh=Mesh.cube(1.0),
 84                    material=mat,
 85                    position=position,
 86                    scale=scale,
 87                )
 88            )
 89
 90        wall("Floor", white, Vec3(0, 0, -1), Vec3(span * 2, thick, span * 2))
 91        wall("Ceiling", white, Vec3(0, span, -1), Vec3(span * 2, thick, span * 2))
 92        wall("BackWall", white, Vec3(0, span * 0.5, -1 - span), Vec3(span * 2, span * 2, thick))
 93        wall("LeftWall", red, Vec3(-span, span * 0.5, -1), Vec3(thick, span * 2, span * 2))
 94        wall("RightWall", green, Vec3(span, span * 0.5, -1), Vec3(thick, span * 2, span * 2))
 95
 96        # Two white blocks standing on the floor: they catch the coloured bleed on
 97        # their inward faces and cast contact darkening where they meet the floor.
 98        self.add_child(
 99            MeshInstance3D(
100                name="TallBlock",
101                mesh=Mesh.cube(1.0),
102                material=white,
103                position=Vec3(-2.2, 1.8, -2.6),
104                scale=Vec3(2.0, 3.6, 2.0),
105                rotation=Quat.from_euler(0, math.radians(18), 0),
106            )
107        )
108        self.add_child(
109            MeshInstance3D(
110                name="ShortBlock",
111                mesh=Mesh.cube(1.0),
112                material=white,
113                position=Vec3(2.0, 1.0, -0.4),
114                scale=Vec3(2.0, 2.0, 2.0),
115                rotation=Quat.from_euler(0, math.radians(-20), 0),
116            )
117        )
118
119        self._env = self.add_child(WorldEnvironment(name="Env"))
120        # Flat, dim ambient so the colour bleed reads clearly against it (no IBL:
121        # SSGI is purely additive here, so it can never crush the image).
122        self._env.ambient_light_colour = (0.10, 0.10, 0.12)
123        self._env.ambient_light_energy = 0.5
124        self._env.ssgi_enabled = True
125        self._env.ssgi_intensity = 0.85
126        self._env.ssgi_max_distance = 12.0
127
128        self._title = self.add_child(Text2D(text="SCREEN-SPACE GLOBAL ILLUMINATION", position=(10, 8), font_scale=1.5))
129        self._status = self.add_child(Text2D(text="SSGI: ON", position=(10, 40), font_scale=1.3))
130        self._controls = self.add_child(
131            Text2D(text="SPACE/TAP:Toggle SSGI   ESC:Quit", position=(10, HEIGHT - 30), font_scale=1.1)
132        )
133
134    def on_ready(self):
135        InputMap.add_action("toggle_ssgi", [Key.SPACE])
136        InputMap.add_action("quit", [Key.ESCAPE])
137
138    def on_update(self, dt: float):
139        # Space or a click/tap flips the Property directly (touch arrives as
140        # MouseButton.LEFT on web, so the demo stays usable on phones).
141        if Input.is_action_just_pressed("toggle_ssgi") or Input.is_mouse_button_just_pressed(MouseButton.LEFT):
142            self._env.ssgi_enabled = not self._env.ssgi_enabled
143            self._status.text = f"SSGI: {'ON' if self._env.ssgi_enabled else 'OFF'}"
144        if Input.is_action_just_pressed("quit"):
145            self.app.quit()
146        # Pin the controls hint to the live viewport bottom (resize-aware).
147        self._controls.position = (10, self.app.height - 30)
148
149
150def main():
151    scene = SSGIScene()
152    app = App(title="SimVX SSGI Demo", width=WIDTH, height=HEIGHT)
153    app.run(scene)
154
155
156if __name__ == "__main__":
157    main()