Volumetric Fog¶
Ray-marched light scattering and local FogVolume3D regions.
▶ Run in browserTags: 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())