Fog¶
Distance-based fog via WorldEnvironment.
▶ Run in browserTags: 3d
Distance fog blends geometry toward a fog colour the further it is from the
camera, which sells depth and hides the far edge of a scene. It is a setting on
the scene’s WorldEnvironment node, alongside bloom and tonemapping:
env = WorldEnvironment(fog_enabled=True, fog_colour=(0.95, 0.55, 0.25, 1.0), fog_density=0.05)
fog_mode picks the falloff curve, a FogMode. LINEAR ramps from fog_start to
fog_end; EXPONENTIAL and EXPONENTIAL_SQUARED thicken with
fog_density and ignore the start and end. Fog tints geometry only: the sky
is left alone, which is why a warm orange fog stands out against the blue sky.
The scene rings a field of objects with emissive orbs that bloom and tall
pillars that fade into the fog, and an OrbitCamera3D lets you walk the view
in and out of it.
Controls: Drag / right stick - Orbit the camera Scroll / triggers - Zoom 1 - Toggle fog 2 - Toggle bloom 3 - Cycle fog mode Up / Down - Fog density (exponential modes) Right / Left - Tonemap exposure
Source¶
1# /// simvx
2# web = { width = 1280, height = 720, reason = "Fog renders differently than desktop (no tonemap exposure on web)." }
3# ///
4"""Fog: Distance-based fog via WorldEnvironment.
5
6Distance fog blends geometry toward a fog colour the further it is from the
7camera, which sells depth and hides the far edge of a scene. It is a setting on
8the scene's ``WorldEnvironment`` node, alongside bloom and tonemapping:
9
10 env = WorldEnvironment(fog_enabled=True, fog_colour=(0.95, 0.55, 0.25, 1.0), fog_density=0.05)
11
12``fog_mode`` picks the falloff curve, a ``FogMode``. ``LINEAR`` ramps from ``fog_start`` to
13``fog_end``; ``EXPONENTIAL`` and ``EXPONENTIAL_SQUARED`` thicken with
14``fog_density`` and ignore the start and end. Fog tints geometry only: the sky
15is left alone, which is why a warm orange fog stands out against the blue sky.
16
17The scene rings a field of objects with emissive orbs that bloom and tall
18pillars that fade into the fog, and an ``OrbitCamera3D`` lets you walk the view
19in and out of it.
20
21Controls:
22 Drag / right stick - Orbit the camera
23 Scroll / triggers - Zoom
24 1 - Toggle fog
25 2 - Toggle bloom
26 3 - Cycle fog mode
27 Up / Down - Fog density (exponential modes)
28 Right / Left - Tonemap exposure
29"""
30
31import math
32
33import numpy as np
34
35from simvx.core import (
36 Anchor,
37 Axis1D,
38 CanvasLayer,
39 DirectionalLight3D,
40 FogMode,
41 Input,
42 Key,
43 Label,
44 Material,
45 Mesh,
46 MeshInstance3D,
47 Node,
48 OrbitCamera3D,
49 Vec3,
50 WorldEnvironment,
51 on_input,
52)
53from simvx.graphics import App
54
55FOG_MODES = list(FogMode)
56ORB_COLOURS = [(1.0, 0.15, 0.05), (0.05, 1.0, 0.25), (0.2, 0.3, 1.0), (1.0, 0.8, 0.1), (1.0, 0.1, 0.9), (0.1, 0.9, 1.0)]
57PROP_COLOURS = [(0.9, 0.2, 0.2), (0.2, 0.9, 0.2), (0.2, 0.2, 0.9), (0.9, 0.9, 0.2), (1.0, 0.5, 0.0), (0.5, 0.0, 1.0)]
58
59
60class FogDemo(Node):
61 input_context = {
62 "toggle_fog": [Key.KEY_1],
63 "toggle_bloom": [Key.KEY_2],
64 "cycle_fog_mode": [Key.KEY_3],
65 "density": [Axis1D(low=Key.DOWN, high=Key.UP)],
66 "exposure": [Axis1D(low=Key.LEFT, high=Key.RIGHT)],
67 }
68
69 def on_ready(self):
70 self.add_child(
71 OrbitCamera3D(
72 pivot=Vec3(0, 2, 0),
73 distance=15.0,
74 yaw=math.radians(30),
75 pitch=math.radians(-25),
76 min_distance=5.0,
77 max_distance=60.0,
78 min_pitch=math.radians(-80),
79 max_pitch=math.radians(10),
80 fov=60,
81 far=200.0,
82 )
83 )
84 # A low bloom threshold so the strongly emissive orbs clearly halo.
85 self.env = self.add_child(
86 WorldEnvironment(
87 fog_enabled=True,
88 fog_colour=(0.95, 0.55, 0.25, 1.0),
89 fog_density=0.05,
90 fog_start=2.0,
91 fog_end=50.0,
92 fog_mode=FogMode.EXPONENTIAL,
93 bloom_enabled=True,
94 bloom_threshold=0.8,
95 bloom_intensity=1.2,
96 bloom_soft_knee=0.7,
97 tonemap_exposure=0.9,
98 )
99 )
100 self.add_child(DirectionalLight3D(intensity=1.5, direction=(-1, -2, -1)))
101 self.add_child(DirectionalLight3D(intensity=0.3, colour=(0.6, 0.7, 1.0), direction=(1, -1, 2)))
102 self.add_child(MeshInstance3D(mesh=Mesh.plane(50), material=Material(colour=(0.3, 0.35, 0.3), roughness=0.9)))
103
104 # emissive_colour is (r, g, b, intensity): the multiplier lifts the orbs far above the bloom threshold.
105 for i, rgb in enumerate(ORB_COLOURS):
106 angle = i * math.tau / len(ORB_COLOURS)
107 material = Material(
108 colour=tuple(c * 0.2 for c in rgb), roughness=0.25, metallic=0.9, emissive_colour=(*rgb, 5.0)
109 )
110 position = (math.cos(angle) * 6.0, 1.0, math.sin(angle) * 6.0)
111 self.add_child(MeshInstance3D(mesh=Mesh.sphere(radius=0.6), material=material, position=position))
112
113 # Props scattered near and far, so the fog visibly swallows the distant ones.
114 shapes = [(Mesh.sphere(radius=0.8), 0.8), (Mesh.box(), 0.5), (Mesh.cylinder(radius=0.5, height=2.0), 1.0)]
115 rng = np.random.default_rng(42)
116 for i in range(30):
117 mesh, y = shapes[i % len(shapes)]
118 material = Material(colour=PROP_COLOURS[i % len(PROP_COLOURS)], roughness=0.4, metallic=0.3)
119 position = (rng.uniform(-20, 20), y, rng.uniform(-20, 20))
120 self.add_child(MeshInstance3D(mesh=mesh, material=material, position=position))
121
122 pillar_material = Material(colour=(0.6, 0.6, 0.65), roughness=0.5, metallic=0.1)
123 for i in range(8):
124 angle = i * math.tau / 8
125 position = (math.cos(angle) * 15.0, 3.0, math.sin(angle) * 15.0)
126 self.add_child(MeshInstance3D(mesh=Mesh.box((0.8, 6.0, 0.8)), material=pillar_material, position=position))
127
128 self.hud = self.add_child(CanvasLayer()).add_child(Label(anchor=Anchor.TOP_LEFT, inset=10, font_size=16))
129
130 @on_input("toggle_fog")
131 def _toggle_fog(self, _event):
132 self.env.fog_enabled = not self.env.fog_enabled
133
134 @on_input("toggle_bloom")
135 def _toggle_bloom(self, _event):
136 self.env.bloom_enabled = not self.env.bloom_enabled
137
138 @on_input("cycle_fog_mode")
139 def _cycle_fog_mode(self, _event):
140 self.env.fog_mode = FOG_MODES[(FOG_MODES.index(self.env.fog_mode) + 1) % len(FOG_MODES)]
141
142 def on_update(self, dt):
143 env = self.env
144 env.fog_density = min(0.2, max(0.001, env.fog_density + Input.value("density") * 0.02 * dt))
145 env.tonemap_exposure = min(5.0, max(0.1, env.tonemap_exposure + Input.value("exposure") * dt))
146 self.hud.text = "\n".join(
147 [
148 f"[1] Fog: {'ON' if env.fog_enabled else 'OFF'} Density: {env.fog_density:.3f} (Up/Down)",
149 f"[2] Bloom: {'ON' if env.bloom_enabled else 'OFF'}",
150 f"[3] Mode: {env.fog_mode}",
151 f"Exposure: {env.tonemap_exposure:.2f} (Right/Left)",
152 "Drag to orbit, scroll to zoom",
153 ]
154 )
155
156
157if __name__ == "__main__":
158 App(title="Fog", width=1280, height=720).run(FogDemo())