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.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 = "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.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", position=(10, 10), font_scale=1.8))
104 self.add_child(
105 Text2D(text="Top: Gold (metallic=1.0), roughness 0.15 -> 0.95", position=(10, 45), font_scale=1.2))
106 self.add_child(
107 Text2D(text="Bottom: Red (roughness=0.25), metallic 0.0 -> 1.0", position=(10, 70), font_scale=1.2))
108 self._hint = self.add_child(
109 Text2D(text="Skybox provides ambient environment lighting via IBL | Esc: quit",
110 position=(10, 695), font_scale=1.0))
111
112 self._time = 0.0
113 self._cam = cam
114
115 def on_update(self, dt):
116 if Input.is_action_just_pressed("quit"):
117 self.app.quit()
118 return
119 # Pin the caption to the bottom edge of the live window (survives resizes).
120 self._hint.position = (10, self.app.height - 30)
121 # Gentle horizontal camera orbit in the XY plane (Z stays fixed so the
122 # ground stays at the bottom of the view). Explicit up=(0,0,1)
123 # prevents the default +Y up from rolling the view.
124 self._time += dt * 0.15
125 dist = 14.0
126 self._cam.position = Vec3(
127 math.sin(self._time) * dist,
128 -math.cos(self._time) * dist,
129 4.0,
130 )
131 self._cam.look_at(Vec3(0, 0, 0.5), up=Vec3(0, 0, 1))
132
133
134if __name__ == "__main__":
135 App(title="IBL Demo", width=1280, height=720).run(IBLScene())