Fog¶
Distance-based fog via WorldEnvironment.
▶ Run in browserTags: 3d
Demonstrates:
Distance fog with adjustable density/start/end
Fog colour control
Height fog toggle
Fog mode switching (linear / exponential / exponential_squared)
Bloom + tonemap combined with fog
Controls: A / D - Orbit camera left / right W / S - Zoom in / out Q / E - Raise / lower camera 1 - Toggle fog 2 - Toggle bloom 3 - Cycle fog mode Up / Down - Adjust fog density Left / Right - Adjust tonemap exposure Escape - Quit
Run: uv run python examples/features/3d/fog.py
Source¶
1"""Fog: Distance-based fog via WorldEnvironment.
2
3# /// simvx
4# web = { width = 1280, height = 720, reason = "Fog renders differently than desktop (no tonemap exposure on web)." }
5# ///
6
7Demonstrates:
8 - Distance fog with adjustable density/start/end
9 - Fog colour control
10 - Height fog toggle
11 - Fog mode switching (linear / exponential / exponential_squared)
12 - Bloom + tonemap combined with fog
13
14Controls:
15 A / D - Orbit camera left / right
16 W / S - Zoom in / out
17 Q / E - Raise / lower camera
18 1 - Toggle fog
19 2 - Toggle bloom
20 3 - Cycle fog mode
21 Up / Down - Adjust fog density
22 Left / Right - Adjust tonemap exposure
23 Escape - Quit
24
25Run: uv run python examples/features/3d/fog.py
26"""
27
28
29import math
30
31import numpy as np
32
33from simvx.core import (
34 Camera3D,
35 DirectionalLight3D,
36 Input,
37 InputMap,
38 Key,
39 Material,
40 Mesh,
41 MeshInstance3D,
42 Node,
43 Text2D,
44 WorldEnvironment,
45)
46from simvx.graphics import App
47
48FOG_MODES = ["linear", "exponential", "exponential_squared"]
49
50
51class FogDemo(Node):
52 def on_ready(self):
53 InputMap.add_action("orbit_left", [Key.A])
54 InputMap.add_action("orbit_right", [Key.D])
55 InputMap.add_action("pitch_up", [Key.W])
56 InputMap.add_action("pitch_down", [Key.S])
57 InputMap.add_action("zoom_in", [Key.Q])
58 InputMap.add_action("zoom_out", [Key.E])
59 InputMap.add_action("toggle_fog", [Key.KEY_1])
60 InputMap.add_action("toggle_bloom", [Key.KEY_2])
61 InputMap.add_action("cycle_fog_mode", [Key.KEY_3])
62 InputMap.add_action("density_up", [Key.UP])
63 InputMap.add_action("density_down", [Key.DOWN])
64 InputMap.add_action("exposure_up", [Key.LEFT])
65 InputMap.add_action("exposure_down", [Key.RIGHT])
66 InputMap.add_action("quit", [Key.ESCAPE])
67
68 self._yaw = 30.0
69 self._pitch = 25.0
70 self._distance = 25.0
71 self._target = (0.0, 2.0, 0.0)
72 self._fog_mode_idx = 1 # exponential
73
74 self._cam = Camera3D(name="Camera", fov=60, near=0.1, far=200.0)
75 self.add_child(self._cam)
76
77 # WorldEnvironment: fog + bloom + tonemap. Warm orange fog contrasts the
78 # blue gradient sky so distance fog is obvious when toggled, and bloom
79 # threshold is low enough that the strongly-emissive balls clearly halo.
80 self._env = self.add_child(WorldEnvironment())
81 self._env.fog_enabled = True
82 self._env.fog_colour = (0.95, 0.55, 0.25, 1.0)
83 self._env.fog_density = 0.12
84 self._env.fog_start = 2.0
85 self._env.fog_end = 50.0
86 self._env.fog_mode = "exponential"
87 self._env.bloom_enabled = True
88 self._env.bloom_threshold = 0.8
89 self._env.bloom_intensity = 1.2
90 self._env.bloom_soft_knee = 0.7
91 self._env.tonemap_exposure = 0.9
92
93 # Lighting
94 key = DirectionalLight3D(name="KeyLight", intensity=1.5)
95 key.look_at((-1.0, -2.0, -1.0))
96 self.add_child(key)
97
98 fill = DirectionalLight3D(name="FillLight", intensity=0.3, colour=(0.6, 0.7, 1.0))
99 fill.look_at((1.0, -1.0, 2.0))
100 self.add_child(fill)
101
102 # Ground plane
103 ground = MeshInstance3D(name="Ground", mesh=Mesh.cube())
104 ground.material = Material(colour=(0.3, 0.35, 0.3), roughness=0.9, metallic=0.0)
105 ground.scale = (50.0, 0.1, 50.0)
106 ground.position = (0.0, -0.05, 0.0)
107 self.add_child(ground)
108
109 # Strongly-emissive metallic orbs to show bloom. ``emissive_colour`` is
110 # (r, g, b, intensity): the intensity multiplier pushes the fragment
111 # HDR value well above the bloom threshold so the halo is unmistakable
112 # when bloom is on and disappears entirely when toggled off.
113 emissive_specs = [
114 ((1.0, 0.15, 0.05), 6.0), # fiery red
115 ((0.05, 1.0, 0.25), 5.0), # emerald
116 ((0.2, 0.3, 1.0), 5.0), # electric blue
117 ((1.0, 0.8, 0.1), 6.0), # amber
118 ((1.0, 0.1, 0.9), 5.0), # magenta
119 ((0.1, 0.9, 1.0), 5.0), # cyan
120 ]
121 for i, (rgb, intensity) in enumerate(emissive_specs):
122 angle = i * math.pi * 2 / len(emissive_specs)
123 mat = Material(
124 colour=(rgb[0] * 0.2, rgb[1] * 0.2, rgb[2] * 0.2),
125 roughness=0.25, metallic=0.9,
126 emissive_colour=(*rgb, intensity),
127 )
128 obj = MeshInstance3D(name=f"Emissive{i}", mesh=Mesh.sphere(radius=0.6), material=mat)
129 obj.position = (math.cos(angle) * 6.0, 1.0, math.sin(angle) * 6.0)
130 self.add_child(obj)
131
132 # Scattered objects at various distances: fog fades distant ones
133 colours = [
134 (0.9, 0.2, 0.2), (0.2, 0.9, 0.2), (0.2, 0.2, 0.9), (0.9, 0.9, 0.2),
135 (0.9, 0.2, 0.9), (0.2, 0.9, 0.9), (1.0, 0.5, 0.0), (0.5, 0.0, 1.0),
136 ]
137 rng = np.random.default_rng(42)
138 for i in range(30):
139 colour = colours[i % len(colours)]
140 mat = Material(colour=colour, roughness=0.4, metallic=0.3)
141 if i % 3 == 0:
142 mesh = Mesh.sphere(radius=0.8)
143 elif i % 3 == 1:
144 mesh = Mesh.cube()
145 else:
146 mesh = Mesh.cylinder(radius=0.5, height=2.0)
147 obj = MeshInstance3D(name=f"Obj{i}", mesh=mesh, material=mat)
148 obj.position = (rng.uniform(-20, 20), 0.8 if i % 3 != 2 else 1.0, rng.uniform(-20, 20))
149 self.add_child(obj)
150
151 # Tall pillars (visible at distance, good for fog depth testing)
152 pillar_mat = Material(colour=(0.6, 0.6, 0.65), roughness=0.5, metallic=0.1)
153 for i in range(8):
154 angle = i * math.pi * 2 / 8
155 pillar = MeshInstance3D(name=f"Pillar{i}", mesh=Mesh.cube(), material=pillar_mat)
156 pillar.scale = (0.8, 6.0, 0.8)
157 pillar.position = (math.cos(angle) * 15.0, 3.0, math.sin(angle) * 15.0)
158 self.add_child(pillar)
159
160 self._hud = self.add_child(Text2D(name="HUD", text="", font_scale=1.2, x=10.0, y=10.0))
161 self._update_camera()
162
163 def on_process(self, dt):
164 if Input.is_action_pressed("orbit_left"):
165 self._yaw += 60.0 * dt
166 if Input.is_action_pressed("orbit_right"):
167 self._yaw -= 60.0 * dt
168 if Input.is_action_pressed("zoom_in"):
169 self._distance = max(5.0, self._distance - 10.0 * dt)
170 if Input.is_action_pressed("zoom_out"):
171 self._distance = min(60.0, self._distance + 10.0 * dt)
172 if Input.is_action_pressed("pitch_up"):
173 self._pitch = min(80.0, self._pitch + 30.0 * dt)
174 if Input.is_action_pressed("pitch_down"):
175 self._pitch = max(-10.0, self._pitch - 30.0 * dt)
176
177 if Input.is_action_just_pressed("quit"):
178 self.app.quit()
179 return
180
181 env = self._env
182
183 if Input.is_action_just_pressed("toggle_fog"):
184 env.fog_enabled = not env.fog_enabled
185 if Input.is_action_just_pressed("toggle_bloom"):
186 env.bloom_enabled = not env.bloom_enabled
187 if Input.is_action_just_pressed("cycle_fog_mode"):
188 self._fog_mode_idx = (self._fog_mode_idx + 1) % len(FOG_MODES)
189 env.fog_mode = FOG_MODES[self._fog_mode_idx]
190
191 if Input.is_action_pressed("density_up"):
192 env.fog_density = min(0.2, env.fog_density + 0.02 * dt)
193 if Input.is_action_pressed("density_down"):
194 env.fog_density = max(0.001, env.fog_density - 0.02 * dt)
195
196 if Input.is_action_pressed("exposure_up"):
197 env.tonemap_exposure = min(5.0, env.tonemap_exposure + 1.0 * dt)
198 if Input.is_action_pressed("exposure_down"):
199 env.tonemap_exposure = max(0.1, env.tonemap_exposure - 1.0 * dt)
200
201 self._update_camera()
202 self._update_hud()
203
204 def _update_camera(self):
205 yaw_rad = math.radians(self._yaw)
206 pitch_rad = math.radians(self._pitch)
207 cp = math.cos(pitch_rad)
208 x = self._target[0] + self._distance * cp * math.sin(yaw_rad)
209 y = self._target[1] + self._distance * math.sin(pitch_rad)
210 z = self._target[2] + self._distance * cp * math.cos(yaw_rad)
211 self._cam.position = (x, y, z)
212 self._cam.look_at(self._target)
213
214 def _update_hud(self):
215 env = self._env
216 lines = [
217 "Fog Demo (WorldEnvironment)",
218 f"[1] Fog: {'ON' if env.fog_enabled else 'OFF'} Density: {env.fog_density:.3f} (Up/Down)",
219 f"[2] Bloom: {'ON' if env.bloom_enabled else 'OFF'}",
220 f"[3] Mode: {env.fog_mode}",
221 f" Exposure: {env.tonemap_exposure:.2f} (Left/Right)",
222 "A/D orbit W/S pitch Q/E zoom Esc quit",
223 ]
224 self._hud.text = "\n".join(lines)
225
226
227if __name__ == "__main__":
228 app = App(title="Fog Demo", width=1280, height=720)
229 app.run(FogDemo())