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" (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()