Dynamic procedural sky

a Preetham analytic day-night sweep driving IBL.

▶ Run in browser

Tags: 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()