Screen-space global illumination

coloured light bleeding between surfaces.

▶ Run in browser

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

Screen-space GI (design D8) 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 (design D8), 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 - Toggle SSGI on/off Escape - Quit

Source

  1"""Screen-space global illumination: coloured light bleeding between surfaces.
  2
  3Screen-space GI (design D8) 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 (design D8), 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   - 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    Node3D,
 39    PointLight3D,
 40    Quat,
 41    Text2D,
 42    Vec3,
 43    WorldEnvironment,
 44)
 45from simvx.graphics import App
 46
 47WIDTH, HEIGHT = 1280, 720
 48
 49
 50class SSGIScene(Node3D):
 51    def __init__(self, **kwargs):
 52        super().__init__(name="SSGIDemo", **kwargs)
 53
 54        # Camera looking down the axis into an open-fronted box, so the coloured
 55        # side walls and the white floor between them fill the frame.
 56        self.camera = self.add_child(Camera3D(name="Camera", fov=55, near=0.1, far=60.0))
 57        self.camera.position = Vec3(0.0, 3.0, 9.5)
 58        self.camera.look_at(Vec3(0.0, 2.6, -2.0))
 59
 60        # A single bright point light near the ceiling: it lights the coloured
 61        # walls directly, and SSGI carries their colour onto the neutral surfaces.
 62        lamp = self.add_child(PointLight3D(name="Lamp", position=Vec3(0.0, 5.4, 1.0)))
 63        lamp.colour = (1.0, 0.97, 0.92)
 64        lamp.intensity = 20.0
 65        lamp.range = 24.0
 66        lamp.shadows = True  # sole key light: cast shadows (point shadows are opt-in)
 67
 68        # Cornell-style box: white floor / ceiling / back wall, a saturated red
 69        # left wall and green right wall (matte, so they bounce diffusely). Rough,
 70        # non-metallic surfaces so the effect is pure indirect diffuse.
 71        white = Material(colour=(0.80, 0.80, 0.80), metallic=0.0, roughness=0.9)
 72        red = Material(colour=(0.85, 0.08, 0.08), metallic=0.0, roughness=0.9)
 73        green = Material(colour=(0.08, 0.75, 0.12), metallic=0.0, roughness=0.9)
 74
 75        span = 6.0  # interior half-width / height
 76        thick = 0.3
 77
 78        def wall(name, mat, position, scale):
 79            self.add_child(
 80                MeshInstance3D(
 81                    name=name,
 82                    mesh=Mesh.cube(1.0),
 83                    material=mat,
 84                    position=position,
 85                    scale=scale,
 86                )
 87            )
 88
 89        wall("Floor", white, Vec3(0, 0, -1), Vec3(span * 2, thick, span * 2))
 90        wall("Ceiling", white, Vec3(0, span, -1), Vec3(span * 2, thick, span * 2))
 91        wall("BackWall", white, Vec3(0, span * 0.5, -1 - span), Vec3(span * 2, span * 2, thick))
 92        wall("LeftWall", red, Vec3(-span, span * 0.5, -1), Vec3(thick, span * 2, span * 2))
 93        wall("RightWall", green, Vec3(span, span * 0.5, -1), Vec3(thick, span * 2, span * 2))
 94
 95        # Two white blocks standing on the floor: they catch the coloured bleed on
 96        # their inward faces and cast contact darkening where they meet the floor.
 97        self.add_child(
 98            MeshInstance3D(
 99                name="TallBlock",
100                mesh=Mesh.cube(1.0),
101                material=white,
102                position=Vec3(-2.2, 1.8, -2.6),
103                scale=Vec3(2.0, 3.6, 2.0),
104                rotation=Quat.from_euler(0, math.radians(18), 0),
105            )
106        )
107        self.add_child(
108            MeshInstance3D(
109                name="ShortBlock",
110                mesh=Mesh.cube(1.0),
111                material=white,
112                position=Vec3(2.0, 1.0, -0.4),
113                scale=Vec3(2.0, 2.0, 2.0),
114                rotation=Quat.from_euler(0, math.radians(-20), 0),
115            )
116        )
117
118        self._ssgi_on = True
119        self._cooldown = 0.0
120        self._env = self.add_child(WorldEnvironment(name="Env"))
121        # Flat, dim ambient so the colour bleed reads clearly against it (no IBL:
122        # SSGI is purely additive here, so it can never crush the image).
123        self._env.ambient_light_colour = (0.10, 0.10, 0.12)
124        self._env.ambient_light_energy = 0.5
125        self._env.ssgi_enabled = True
126        self._env.ssgi_intensity = 0.85
127        self._env.ssgi_max_distance = 12.0
128
129        self._title = self.add_child(Text2D(text="SCREEN-SPACE GLOBAL ILLUMINATION", position=(10, 8), font_scale=1.5))
130        self._status = self.add_child(Text2D(text="SSGI: ON", position=(10, 40), font_scale=1.3))
131        self.add_child(Text2D(text="SPACE:Toggle SSGI   ESC:Quit", position=(10, 690), font_scale=1.1))
132
133    def on_ready(self):
134        InputMap.add_action("toggle_ssgi", [Key.SPACE])
135        InputMap.add_action("quit", [Key.ESCAPE])
136
137    def on_update(self, dt: float):
138        self._cooldown = max(0.0, self._cooldown - dt)
139        if Input.is_action_just_pressed("toggle_ssgi") and self._cooldown <= 0.0:
140            self._ssgi_on = not self._ssgi_on
141            self._cooldown = 0.3
142            self._env.ssgi_enabled = self._ssgi_on
143            self._status.text = f"SSGI: {'ON' if self._ssgi_on else 'OFF'}"
144        if Input.is_action_just_pressed("quit"):
145            self.app.quit()
146
147
148def main():
149    scene = SSGIScene()
150    app = App(title="SimVX SSGI Demo", width=WIDTH, height=HEIGHT, physics_fps=60)
151    app.run(scene)
152
153
154if __name__ == "__main__":
155    main()