IBL

Image-based lighting on metallic spheres.

▶ Run in browser

Tags: 3d

Demonstrates:

  • Procedural skybox cubemap providing ambient IBL

  • IBL pass generating irradiance, prefiltered specular, and BRDF LUT

  • Row of spheres with varying roughness (0.05 to 0.95)

  • Row of spheres with varying metallic (0.0 to 1.0)

  • Directional light for direct illumination

Controls: Escape - Quit

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

Source

  1"""IBL: Image-based lighting on metallic spheres.
  2
  3# /// simvx
  4# web = { width = 1280, height = 720, reason = "Skybox and image-based lighting not yet supported on web." }
  5# ///
  6
  7Demonstrates:
  8  - Procedural skybox cubemap providing ambient IBL
  9  - IBL pass generating irradiance, prefiltered specular, and BRDF LUT
 10  - Row of spheres with varying roughness (0.05 to 0.95)
 11  - Row of spheres with varying metallic (0.0 to 1.0)
 12  - Directional light for direct illumination
 13
 14Controls:
 15    Escape  - Quit
 16
 17Run: uv run python examples/features/3d/ibl.py
 18"""
 19
 20import math
 21
 22from simvx.core import (
 23    Camera3D,
 24    DirectionalLight3D,
 25    Input,
 26    InputMap,
 27    Key,
 28    Material,
 29    Mesh,
 30    MeshInstance3D,
 31    Node,
 32    Text2D,
 33    Vec3,
 34    WorldEnvironment,
 35)
 36from simvx.graphics import App
 37
 38SPHERE_COUNT = 7
 39SPACING = 2.5
 40
 41
 42class IBLScene(Node):
 43    def on_ready(self):
 44        InputMap.add_action("quit", [Key.ESCAPE])
 45
 46        # Procedural deep-blue skybox + IBL, declared on the central
 47        # WorldEnvironment node. EnvironmentSync calls load_cubemap and the
 48        # renderer's set_skybox auto-runs the IBL precompute (irradiance +
 49        # prefiltered specular + BRDF LUT) on first install.
 50        self.add_child(WorldEnvironment(
 51            environment_map={"colour": (0.15, 0.22, 0.35)},
 52        ))
 53
 54        # Camera looking at the sphere rows. Scene is Z-up (ground at Z=-2.5,
 55        # sphere rows at Z=2.0 and Z=-1.5), so pass an explicit ``up=(0,0,1)``
 56        # to ``look_at`` or the default +Y up rolls the camera as it orbits.
 57        cam = self.add_child(Camera3D(
 58            position=(0, -12, 4), fov=55, near=0.1, far=100.0,
 59            look_at=Vec3(0, 0, 1.0), up=Vec3(0, 0, 1),
 60        ))
 61
 62        # Directional light (sun) -- moderate intensity so IBL contribution is visible
 63        sun = DirectionalLight3D(position=(5, -8, 10))
 64        sun.colour = (1.0, 0.98, 0.92)
 65        sun.intensity = 1.0
 66        sun.look_at(Vec3(0, 0, 0))
 67        self.add_child(sun)
 68
 69        # Higher tessellation keeps the sharp end (low roughness) from aliasing
 70        # its specular highlight across individual faces, which reads as a flash.
 71        sphere_mesh = Mesh.sphere(0.9, rings=48, segments=64)
 72
 73        # Top row: varying roughness (metallic = 1.0). Minimum 0.15 so the
 74        # leftmost sphere still shows a tight highlight but the specular lobe
 75        # is wide enough to sample multiple prefiltered-specular mip taps per
 76        # pixel rather than strobing a single mip tap.
 77        for i in range(SPHERE_COUNT):
 78            t = i / max(SPHERE_COUNT - 1, 1)
 79            roughness = 0.15 + t * 0.8
 80            mat = Material(colour=(0.9, 0.6, 0.2, 1.0), metallic=1.0, roughness=roughness)
 81            x = (i - SPHERE_COUNT // 2) * SPACING
 82            self.add_child(MeshInstance3D(mesh=sphere_mesh, material=mat, position=(x, 0, 2.0)))
 83
 84        # Bottom row: varying metallic (roughness = 0.25)
 85        for i in range(SPHERE_COUNT):
 86            t = i / max(SPHERE_COUNT - 1, 1)
 87            mat = Material(colour=(0.8, 0.1, 0.1, 1.0), metallic=t, roughness=0.25)
 88            x = (i - SPHERE_COUNT // 2) * SPACING
 89            self.add_child(MeshInstance3D(mesh=sphere_mesh, material=mat, position=(x, 0, -1.5)))
 90
 91        # Ground plane
 92        ground_mat = Material(colour=(0.15, 0.15, 0.18, 1.0), metallic=0.0, roughness=0.9)
 93        self.add_child(
 94            MeshInstance3D(
 95                mesh=Mesh.cube(1.0),
 96                material=ground_mat,
 97                position=(0, 0, -2.5),
 98                scale=Vec3(25, 25, 0.2),
 99            )
100        )
101
102        # Labels
103        self.add_child(Text2D(text="IBL Demo: Image-Based Lighting", x=10, y=10, font_scale=1.8))
104        self.add_child(Text2D(text="Top: Gold (metallic=1.0), roughness 0.15 -> 0.95", x=10, y=45, font_scale=1.2))
105        self.add_child(Text2D(text="Bottom: Red (roughness=0.25), metallic 0.0 -> 1.0", x=10, y=70, font_scale=1.2))
106        self.add_child(Text2D(text="Skybox provides ambient environment lighting via IBL", x=10, y=695, font_scale=1.0))
107
108        self._time = 0.0
109        self._cam = cam
110
111    def on_process(self, dt):
112        if Input.is_action_just_pressed("quit"):
113            self.app.quit()
114            return
115        # Gentle horizontal camera orbit in the XY plane (Z stays fixed so the
116        # ground stays at the bottom of the view). Explicit up=(0,0,1)
117        # prevents the default +Y up from rolling the view.
118        self._time += dt * 0.15
119        dist = 14.0
120        self._cam.position = Vec3(
121            math.sin(self._time) * dist,
122            -math.cos(self._time) * dist,
123            4.0,
124        )
125        self._cam.look_at(Vec3(0, 0, 0.5), up=Vec3(0, 0, 1))
126
127
128if __name__ == "__main__":
129    App(title="IBL Demo", width=1280, height=720).run(IBLScene())