IBL¶
Image-based lighting on metallic spheres.
▶ Run in browserTags: 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())