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.15 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 = "Larger browser canvas; skybox + IBL run cross-backend." }
  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.15 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    Key,
 27    Material,
 28    Mesh,
 29    MeshInstance3D,
 30    Node,
 31    Text2D,
 32    Vec3,
 33    WorldEnvironment,
 34)
 35from simvx.graphics import App
 36
 37SPHERE_COUNT = 7
 38SPACING = 2.5
 39
 40
 41class IBLScene(Node):
 42    input_actions = {"quit": [Key.ESCAPE]}
 43
 44    def on_ready(self):
 45        # Procedural deep-blue skybox + IBL, declared on the central
 46        # WorldEnvironment node. EnvironmentSync calls load_cubemap and the
 47        # renderer's set_skybox auto-runs the IBL precompute (irradiance +
 48        # prefiltered specular + BRDF LUT) on first install.
 49        self.add_child(
 50            WorldEnvironment(
 51                environment_map={"colour": (0.15, 0.22, 0.35)},
 52            )
 53        )
 54
 55        # Camera looking at the sphere rows. Scene is Z-up (ground at Z=-2.5,
 56        # sphere rows at Z=2.0 and Z=-1.5), so pass an explicit ``up=(0,0,1)``
 57        # to ``look_at`` or the default +Y up rolls the camera as it orbits.
 58        cam = self.add_child(
 59            Camera3D(
 60                position=(0, -12, 4),
 61                fov=55,
 62                near=0.1,
 63                far=100.0,
 64                look_at=Vec3(0, 0, 1.0),
 65                up=Vec3(0, 0, 1),
 66            )
 67        )
 68
 69        # Directional light (sun) -- moderate intensity so IBL contribution is visible
 70        sun = DirectionalLight3D(position=(5, -8, 10))
 71        sun.colour = (1.0, 0.98, 0.92)
 72        sun.intensity = 1.0
 73        sun.look_at(Vec3(0, 0, 0))
 74        self.add_child(sun)
 75
 76        # Higher tessellation keeps the sharp end (low roughness) from aliasing
 77        # its specular highlight across individual faces, which reads as a flash.
 78        sphere_mesh = Mesh.sphere(0.9, rings=48, segments=64)
 79
 80        # Top row: varying roughness (metallic = 1.0). Minimum 0.15 so the
 81        # leftmost sphere still shows a tight highlight but the specular lobe
 82        # is wide enough to sample multiple prefiltered-specular mip taps per
 83        # pixel rather than strobing a single mip tap.
 84        for i in range(SPHERE_COUNT):
 85            t = i / max(SPHERE_COUNT - 1, 1)
 86            roughness = 0.15 + t * 0.8
 87            mat = Material(colour=(0.9, 0.6, 0.2, 1.0), metallic=1.0, roughness=roughness)
 88            x = (i - SPHERE_COUNT // 2) * SPACING
 89            self.add_child(MeshInstance3D(mesh=sphere_mesh, material=mat, position=(x, 0, 2.0)))
 90
 91        # Bottom row: varying metallic (roughness = 0.25)
 92        for i in range(SPHERE_COUNT):
 93            t = i / max(SPHERE_COUNT - 1, 1)
 94            mat = Material(colour=(0.8, 0.1, 0.1, 1.0), metallic=t, roughness=0.25)
 95            x = (i - SPHERE_COUNT // 2) * SPACING
 96            self.add_child(MeshInstance3D(mesh=sphere_mesh, material=mat, position=(x, 0, -1.5)))
 97
 98        # Ground plane
 99        ground_mat = Material(colour=(0.15, 0.15, 0.18, 1.0), metallic=0.0, roughness=0.9)
100        self.add_child(
101            MeshInstance3D(
102                mesh=Mesh.cube(1.0),
103                material=ground_mat,
104                position=(0, 0, -2.5),
105                scale=Vec3(25, 25, 0.2),
106            )
107        )
108
109        # Labels
110        self.add_child(Text2D(text="IBL Demo: Image-Based Lighting", position=(10, 10), font_scale=1.8))
111        self.add_child(
112            Text2D(text="Top: Gold (metallic=1.0), roughness 0.15 -> 0.95", position=(10, 45), font_scale=1.2)
113        )
114        self.add_child(
115            Text2D(text="Bottom: Red (roughness=0.25), metallic 0.0 -> 1.0", position=(10, 70), font_scale=1.2)
116        )
117        self._hint = self.add_child(
118            Text2D(
119                text="Skybox provides ambient environment lighting via IBL  |  Esc: quit",
120                position=(10, 695),
121                font_scale=1.0,
122            )
123        )
124
125        self._time = 0.0
126        self._cam = cam
127
128    def on_update(self, dt):
129        if Input.is_action_just_pressed("quit"):
130            self.app.quit()
131            return
132        # Pin the caption to the bottom edge of the live window (survives resizes).
133        self._hint.position = (10, self.app.height - 30)
134        # Gentle horizontal camera orbit in the XY plane (Z stays fixed so the
135        # ground stays at the bottom of the view). Explicit up=(0,0,1)
136        # prevents the default +Y up from rolling the view.
137        self._time += dt * 0.15
138        dist = 14.0
139        self._cam.position = Vec3(
140            math.sin(self._time) * dist,
141            -math.cos(self._time) * dist,
142            4.0,
143        )
144        self._cam.look_at(Vec3(0, 0, 0.5), up=Vec3(0, 0, 1))
145
146
147if __name__ == "__main__":
148    App(title="IBL Demo", width=1280, height=720).run(IBLScene())