Volumetric Fog

Ray-marched light scattering and local FogVolume3D regions.

▶ Run in browser

Tags: 3d

Volumetric fog marches a ray from the camera through the scene and adds up the light the fog scatters toward the viewer at each step, so it glows where the sun shines through it and haloes the sun’s direction. It is a setting on the scene’s WorldEnvironment:

env = WorldEnvironment(volumetric_fog_enabled=True, volumetric_fog_density=0.018, volumetric_fog_anisotropy=0.7)

volumetric_fog_anisotropy is the scattering direction, from -1 (light bounces back toward the sun) through 0 (every direction alike) to 1 (light carries on forward, which makes the bright halo when you look toward the sun). volumetric_fog_albedo tints the scattered light, volumetric_fog_length is how far the march reaches, and volumetric_fog_gi_inject adds ambient in-scatter so the fog reads as a luminous veil rather than only darkening the scene. fog_height and fog_height_density thin the fog above a height, which keeps it pooled near the ground.

A FogVolume3D adds denser fog inside a shape (FogVolumeShape.BOX, SPHERE or CYLINDER, sized by size) and colours the light it scatters with its own albedo; falloff softens its edges. Where volumes overlap, their densities add and the one with the highest priority sets the colour. Here a warm orange box sits to the left and a cool teal sphere to the right, around a ring of pillars and a glowing orb, under a sun and a dim fill.

Controls: Drag / right stick / A D W S - Orbit the camera Scroll / triggers / Q E - Zoom 1 - Toggle volumetric fog 2 - Toggle the FogVolume3D box and sphere Up / Down - Fog density Right / Left - Anisotropy

Source

  1# /// simvx
  2# web = { width = 1280, height = 720 }
  3# ///
  4"""Volumetric Fog: Ray-marched light scattering and local FogVolume3D regions.
  5
  6Volumetric fog marches a ray from the camera through the scene and adds up the
  7light the fog scatters toward the viewer at each step, so it glows where the
  8sun shines through it and haloes the sun's direction. It is a setting on the
  9scene's ``WorldEnvironment``:
 10
 11    env = WorldEnvironment(volumetric_fog_enabled=True, volumetric_fog_density=0.018, volumetric_fog_anisotropy=0.7)
 12
 13``volumetric_fog_anisotropy`` is the scattering direction, from -1 (light
 14bounces back toward the sun) through 0 (every direction alike) to 1 (light
 15carries on forward, which makes the bright halo when you look toward the sun).
 16``volumetric_fog_albedo`` tints the scattered light, ``volumetric_fog_length``
 17is how far the march reaches, and ``volumetric_fog_gi_inject`` adds ambient
 18in-scatter so the fog reads as a luminous veil rather than only darkening the
 19scene. ``fog_height`` and ``fog_height_density`` thin the fog above a height,
 20which keeps it pooled near the ground.
 21
 22A ``FogVolume3D`` adds denser fog inside a shape (``FogVolumeShape.BOX``,
 23``SPHERE`` or ``CYLINDER``, sized by ``size``) and colours the light it
 24scatters with its own ``albedo``; ``falloff`` softens its edges. Where volumes
 25overlap, their densities add and the one with the highest ``priority`` sets the
 26colour. Here a warm orange box sits to the left and a cool teal sphere to the
 27right, around a ring of pillars and a glowing orb, under a sun and a dim fill.
 28
 29Controls:
 30    Drag / right stick / A D W S   - Orbit the camera
 31    Scroll / triggers / Q E        - Zoom
 32    1                              - Toggle volumetric fog
 33    2                              - Toggle the FogVolume3D box and sphere
 34    Up / Down                      - Fog density
 35    Right / Left                   - Anisotropy
 36"""
 37
 38import math
 39
 40from simvx.core import (
 41    Anchor,
 42    Axis1D,
 43    Axis2D,
 44    CanvasLayer,
 45    DirectionalLight3D,
 46    FogVolume3D,
 47    FogVolumeShape,
 48    Input,
 49    Key,
 50    Label,
 51    Material,
 52    Mesh,
 53    MeshInstance3D,
 54    Node,
 55    OrbitCamera3D,
 56    WorldEnvironment,
 57    on_input,
 58)
 59from simvx.graphics import App
 60
 61ORBIT_SPEED = math.radians(60)  # radians per second from the keys
 62ZOOM_SPEED = 12.0  # world units per second from the keys
 63
 64
 65class VolumetricFogDemo(Node):
 66    input_context = {
 67        "toggle_fog": [Key.KEY_1],
 68        "toggle_volumes": [Key.KEY_2],
 69        "density": [Axis1D(low=Key.DOWN, high=Key.UP)],
 70        "anisotropy": [Axis1D(low=Key.LEFT, high=Key.RIGHT)],
 71        "orbit": [Axis2D.WASD],
 72        "zoom": [Axis1D(low=Key.E, high=Key.Q)],
 73    }
 74
 75    def on_ready(self):
 76        self.camera = self.add_child(
 77            OrbitCamera3D(
 78                pivot=(0, 2, 0),
 79                distance=26.0,
 80                yaw=math.radians(35),
 81                pitch=math.radians(-18),
 82                min_distance=6.0,
 83                max_distance=70.0,
 84                min_pitch=math.radians(-80),
 85                max_pitch=math.radians(10),
 86                far=200.0,
 87            )
 88        )
 89        self.env = self.add_child(
 90            WorldEnvironment(
 91                sky_colour_top=(0.05, 0.07, 0.12, 1.0),
 92                sky_colour_bottom=(0.10, 0.12, 0.16, 1.0),
 93                volumetric_fog_enabled=True,
 94                volumetric_fog_density=0.018,
 95                volumetric_fog_length=80.0,
 96                volumetric_fog_anisotropy=0.7,
 97                volumetric_fog_albedo=(0.85, 0.9, 1.0, 1.0),
 98                volumetric_fog_gi_inject=0.45,
 99                fog_height=0.0,
100                fog_height_density=0.02,
101                bloom_enabled=True,
102                bloom_threshold=0.9,
103            )
104        )
105        # The sun the fog scatters, and a dim blue fill from the opposite side.
106        self.add_child(DirectionalLight3D(intensity=3.0, colour=(1.0, 0.95, 0.85), direction=(-0.6, -0.5, -0.7)))
107        self.add_child(DirectionalLight3D(intensity=0.25, colour=(0.5, 0.6, 0.9), direction=(1.0, -0.5, 1.0)))
108
109        # A floor, a ring of pillars and a glowing orb give the fog something to catch light around.
110        floor = Material(colour=(0.18, 0.2, 0.22), roughness=0.9)
111        self.add_child(MeshInstance3D(mesh=Mesh.box((60, 0.1, 60)), material=floor, position=(0, -0.05, 0)))
112        pillar, stone = Mesh.box((0.8, 7.0, 0.8)), Material(colour=(0.55, 0.55, 0.6), roughness=0.5)
113        for i in range(8):
114            angle = i * math.tau / 8
115            position = (math.cos(angle) * 12.0, 3.5, math.sin(angle) * 12.0)
116            self.add_child(MeshInstance3D(mesh=pillar, material=stone, position=position))
117        glow = Material(colour=(0.2, 0.2, 0.05), roughness=0.3, emissive_colour=(1.0, 0.85, 0.3, 6.0))
118        self.add_child(MeshInstance3D(mesh=Mesh.sphere(radius=0.8), material=glow, position=(0, 2, 0)))
119
120        # Warm and cool volumes, both far denser than the thin global haze, so each stands out.
121        self.volumes = [
122            self.add_child(
123                FogVolume3D(
124                    shape=FogVolumeShape.BOX,
125                    size=(10.0, 7.0, 10.0),
126                    density=1.0,
127                    albedo=(1.0, 0.5, 0.15, 1.0),
128                    falloff=1.5,
129                    position=(-6, 3, 0),
130                )
131            ),
132            self.add_child(
133                FogVolume3D(
134                    shape=FogVolumeShape.SPHERE,
135                    size=(9.0, 9.0, 9.0),
136                    density=0.9,
137                    albedo=(0.15, 0.7, 0.85, 1.0),
138                    position=(6, 3, 0),
139                )
140            ),
141        ]
142
143        hud = self.add_child(CanvasLayer())
144        self.status = hud.add_child(Label(anchor=Anchor.TOP_LEFT, inset=12, font_size=18))
145
146        # Example help overlay: not part of the technique shown.
147        help_text = "Drag / A D W S: orbit   Scroll / Q E: zoom   Up/Down: density   Left/Right: anisotropy"
148        hud.add_child(Label(help_text, anchor=Anchor.BOTTOM_LEFT, inset=12, font_size=14))
149
150    @on_input("toggle_fog")
151    def toggle_fog(self, _event):
152        self.env.volumetric_fog_enabled = not self.env.volumetric_fog_enabled
153
154    @on_input("toggle_volumes")
155    def toggle_volumes(self, _event):
156        for volume in self.volumes:
157            volume.visible = not volume.visible
158
159    def on_update(self, dt):
160        orbit = Input.value("orbit")
161        self.camera.orbit(-orbit.x * ORBIT_SPEED * dt, orbit.y * ORBIT_SPEED * dt)
162        self.camera.zoom(Input.value("zoom") * ZOOM_SPEED * dt)
163
164        # Both knobs are range-clamped Properties: density to [0, 1], anisotropy to [-1, 1].
165        env = self.env
166        env.volumetric_fog_density += Input.value("density") * 0.05 * dt
167        env.volumetric_fog_anisotropy += Input.value("anisotropy") * 0.5 * dt
168        self.status.text = "\n".join(
169            [
170                f"[1] Volumetric fog: {'ON' if env.volumetric_fog_enabled else 'OFF'}",
171                f"[2] FogVolume3D box and sphere: {'ON' if self.volumes[0].visible else 'OFF'}",
172                f"Density: {env.volumetric_fog_density:.3f}   Anisotropy: {env.volumetric_fog_anisotropy:+.2f}",
173            ]
174        )
175
176
177if __name__ == "__main__":
178    App(title="Volumetric Fog", width=1280, height=720).run(VolumetricFogDemo())