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" 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"`` 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 Key,
29 Material,
30 Mesh,
31 MeshInstance3D,
32 Node,
33 Text2D,
34 WorldEnvironment,
35)
36from simvx.graphics import App
37
38WIDTH, HEIGHT = 1280, 720
39
40
41class DynamicSkyScene(Node):
42 input_actions = {
43 "quit": [Key.ESCAPE],
44 "pause": [Key.SPACE],
45 }
46
47 def on_ready(self):
48 self._t = 0.0
49 self._paused = False
50
51 # Procedural Preetham sky: no texture asset, just the sun + haze. The
52 # sky cube it bakes also feeds the IBL ambient the spheres reflect.
53 env = self.add_child(WorldEnvironment())
54 env.sky_mode = "procedural"
55 env.sky_turbidity = 2.6
56 env.sky_ground_albedo = (0.16, 0.15, 0.13, 1.0)
57 env.bloom_enabled = True
58 env.bloom_threshold = 1.1
59 env.bloom_intensity = 0.5
60
61 self.add_child(Camera3D(position=(0, 2.4, 8.5), look_at=(0, 0.7, 0), up=(0, 1, 0)))
62
63 # The sun: a DirectionalLight3D whose direction the sweep animates. Its
64 # position TO the sun is what the Preetham sky is built from.
65 self.sun = self.add_child(DirectionalLight3D(intensity=3.2))
66 self.sun.direction = (0.55, -0.9, 0.4)
67
68 # A dull floor so the sky/ground horizon reads and the spheres cast onto
69 # something; a row of spheres from mirror-metal to rough shows how the
70 # IBL specular vs diffuse picks up the sky colour and the sun glint.
71 self.add_child(
72 MeshInstance3D(
73 mesh=Mesh.cube(1.0),
74 material=Material(colour=(0.38, 0.4, 0.42, 1.0), roughness=0.85),
75 position=(0, -0.8, 0),
76 scale=(24.0, 0.4, 24.0),
77 )
78 )
79 for i, rough in enumerate((0.05, 0.2, 0.45, 0.75)):
80 x = (i - 1.5) * 2.4
81 self.add_child(
82 MeshInstance3D(
83 mesh=Mesh.sphere(1.0),
84 material=Material(colour=(0.95, 0.95, 0.96, 1.0), metallic=1.0, roughness=rough),
85 position=(x, 0.2, 0),
86 )
87 )
88
89 # HUD: a status line so pausing is visibly acknowledged (the sun sweep is
90 # slow, so a paused frame looks almost identical without this).
91 self.add_child(Text2D(text="DYNAMIC SKY", position=(10, 8), font_scale=1.5))
92 self._status = self.add_child(Text2D(text="SUN SWEEP: RUNNING", position=(10, 40), font_scale=1.3))
93 self._hint = self.add_child(Text2D(text="SPACE:Pause sweep ESC:Quit", position=(10, 690), font_scale=1.1))
94
95 def on_update(self, dt):
96 # Pin the controls hint to the live viewport bottom (resize-aware).
97 self._hint.position = (10, self.app.height - 30)
98 if Input.is_action_pressed("quit"):
99 self.app.quit()
100 if Input.is_action_just_pressed("pause"):
101 self._paused = not self._paused
102 self._status.text = "SUN SWEEP: PAUSED" if self._paused else "SUN SWEEP: RUNNING"
103 if not self._paused:
104 self._t += dt
105 # Sweep the sun in a vertical arc in front of the camera: it climbs from
106 # the horizon, crosses near the zenith and sets behind, so the sky goes
107 # dawn -> day -> dusk -> night and the sun disc tracks across the view.
108 phase = 0.35 + self._t * 0.3
109 sun_to = (0.12, math.sin(phase), -math.cos(phase))
110 # DirectionalLight3D.direction is the travel direction (away from sun).
111 self.sun.direction = (-sun_to[0], -sun_to[1], -sun_to[2])
112
113
114def main():
115 App(width=WIDTH, height=HEIGHT, title="SimVX - Dynamic Sky").run(DynamicSkyScene())
116
117
118if __name__ == "__main__":
119 main()