Dynamic procedural sky¶
a Preetham analytic day-night sweep driving IBL.
▶ Run in browserTags: 3d sky procedural preetham ibl lighting
Setting WorldEnvironment.sky_mode = "procedural" (design D12) makes the
renderer synthesize a Preetham analytic sky cubemap from the scene’s first
DirectionalLight3D (the sun), its sky_turbidity (atmospheric haze) and its
sky_ground_albedo (ground bounce). The cube drives BOTH the skybox
background (horizon gradient + a warm sun disc, fading to night as the sun sets)
AND the image-based lighting: the metal and rough spheres pick up the sky’s
colour and the sun’s glint through the split-sum IBL path, re-baked incrementally
as the sun sweeps. No cubemap texture asset is loaded: the whole sky is closed
form. Press SPACE to pause the sweep.
Usage: uv run python examples/features/3d/dynamic_sky.py
Source¶
1"""Dynamic procedural sky: a Preetham analytic day-night sweep driving IBL.
2
3Setting ``WorldEnvironment.sky_mode = "procedural"`` (design D12) makes the
4renderer synthesize a Preetham analytic sky cubemap from the scene's first
5DirectionalLight3D (the sun), its ``sky_turbidity`` (atmospheric haze) and its
6``sky_ground_albedo`` (ground bounce). The cube drives BOTH the skybox
7background (horizon gradient + a warm sun disc, fading to night as the sun sets)
8AND the image-based lighting: the metal and rough spheres pick up the sky's
9colour and the sun's glint through the split-sum IBL path, re-baked incrementally
10as the sun sweeps. No cubemap texture asset is loaded: the whole sky is closed
11form. Press SPACE to pause the sweep.
12
13# /// simvx
14# tags = ["3d", "sky", "procedural", "preetham", "ibl", "lighting"]
15# screenshot_frame = 44
16# ///
17
18Usage:
19 uv run python examples/features/3d/dynamic_sky.py
20"""
21
22import math
23
24from simvx.core import (
25 Camera3D,
26 DirectionalLight3D,
27 Input,
28 InputMap,
29 Key,
30 Material,
31 Mesh,
32 MeshInstance3D,
33 Node,
34 Text2D,
35 WorldEnvironment,
36)
37from simvx.graphics import App
38
39WIDTH, HEIGHT = 1280, 720
40
41
42class DynamicSkyScene(Node):
43 def on_ready(self):
44 InputMap.add_action("quit", [Key.ESCAPE])
45 InputMap.add_action("pause", [Key.SPACE])
46 self._t = 0.0
47 self._paused = False
48
49 # Procedural Preetham sky: no texture asset, just the sun + haze. The
50 # sky cube it bakes also feeds the IBL ambient the spheres reflect.
51 env = self.add_child(WorldEnvironment())
52 env.sky_mode = "procedural"
53 env.sky_turbidity = 2.6
54 env.sky_ground_albedo = (0.16, 0.15, 0.13, 1.0)
55 env.bloom_enabled = True
56 env.bloom_threshold = 1.1
57 env.bloom_intensity = 0.5
58
59 self.add_child(Camera3D(position=(0, 2.4, 8.5), look_at=(0, 0.7, 0), up=(0, 1, 0)))
60
61 # The sun: a DirectionalLight3D whose direction the sweep animates. Its
62 # position TO the sun is what the Preetham sky is built from.
63 self.sun = self.add_child(DirectionalLight3D(intensity=3.2))
64 self.sun.direction = (0.55, -0.9, 0.4)
65
66 # A dull floor so the sky/ground horizon reads and the spheres cast onto
67 # something; a row of spheres from mirror-metal to rough shows how the
68 # IBL specular vs diffuse picks up the sky colour and the sun glint.
69 self.add_child(
70 MeshInstance3D(
71 mesh=Mesh.cube(1.0),
72 material=Material(colour=(0.38, 0.4, 0.42, 1.0), roughness=0.85),
73 position=(0, -0.8, 0),
74 scale=(24.0, 0.4, 24.0),
75 )
76 )
77 for i, rough in enumerate((0.05, 0.2, 0.45, 0.75)):
78 x = (i - 1.5) * 2.4
79 self.add_child(
80 MeshInstance3D(
81 mesh=Mesh.sphere(1.0),
82 material=Material(colour=(0.95, 0.95, 0.96, 1.0), metallic=1.0, roughness=rough),
83 position=(x, 0.2, 0),
84 )
85 )
86
87 # HUD: a status line so pausing is visibly acknowledged (the sun sweep is
88 # slow, so a paused frame looks almost identical without this).
89 self.add_child(Text2D(text="DYNAMIC SKY", position=(10, 8), font_scale=1.5))
90 self._status = self.add_child(Text2D(text="SUN SWEEP: RUNNING", position=(10, 40), font_scale=1.3))
91 self._hint = self.add_child(Text2D(text="SPACE:Pause sweep ESC:Quit", position=(10, 690), font_scale=1.1))
92
93 def on_update(self, dt):
94 # Pin the controls hint to the live viewport bottom (resize-aware).
95 self._hint.position = (10, self.app.height - 30)
96 if Input.is_action_pressed("quit"):
97 self.app.quit()
98 if Input.is_action_just_pressed("pause"):
99 self._paused = not self._paused
100 self._status.text = "SUN SWEEP: PAUSED" if self._paused else "SUN SWEEP: RUNNING"
101 if not self._paused:
102 self._t += dt
103 # Sweep the sun in a vertical arc in front of the camera: it climbs from
104 # the horizon, crosses near the zenith and sets behind, so the sky goes
105 # dawn -> day -> dusk -> night and the sun disc tracks across the view.
106 phase = 0.35 + self._t * 0.3
107 sun_to = (0.12, math.sin(phase), -math.cos(phase))
108 # DirectionalLight3D.direction is the travel direction (away from sun).
109 self.sun.direction = (-sun_to[0], -sun_to[1], -sun_to[2])
110
111
112def main():
113 App(width=WIDTH, height=HEIGHT, title="SimVX - Dynamic Sky").run(DynamicSkyScene())
114
115
116if __name__ == "__main__":
117 main()