3.7 KiB
Distance fog
Principle
Distance fog blends objects toward a predefined color based on their distance to the camera. It is the standard tool for:
- Hiding the rendered edge of the world — the illusion of an infinite world (Skyrim, GTA, Minecraft)
- Adding depth — a natural atmospheric effect
- Masking transitions — tile loading, LOD pops
Activation
use wsg_lib::prelude::*;
let app = AppBuilder::new()
.with_fog(FogConfig::exponential2([0.7, 0.75, 0.85], 0.06))
.build()
.await?;
Without .with_fog(), fog is disabled — zero GPU cost (the shader branch is never taken).
Modes
| Mode | Formula | Use |
|---|---|---|
Linear |
saturate((far - d) / (far - near)) |
Sharp cutoff between two distances |
Exponential |
exp(-density × d) |
Natural fog (forest, lake) |
Exponential2 |
exp(-density² × d²) |
Gradual start, sharp cutoff — ideal for masking |
Constructors
// Linear: fade between near and far
FogConfig::linear([0.7, 0.8, 0.9], 5.0, 50.0)
// Exponential: natural fade
FogConfig::exponential([0.6, 0.7, 0.8], 0.03)
// Exponential²: world-edge masking
FogConfig::exponential2([0.7, 0.75, 0.85], 0.08)
Parameters
| Field | Type | Description |
|---|---|---|
mode |
FogMode |
Linear / Exponential / Exponential2 |
color |
[f32; 3] |
Fog color (RGB, linear space) |
near |
f32 |
Start distance (linear mode only) |
far |
f32 |
End distance, full fog (linear mode) |
density |
f32 |
Density (exp / exp² modes). Typical: 0.01–0.3 |
Choosing the color
The fog color must match the sky/clear color for a seamless "infinite world" effect. With HDR + ACES, use linear values consistent with the tone mapping.
Choosing the density (exp²)
To mask the edge of the world at a distance D:
density ≈ 2.0 / D
Examples:
- World visible up to 25 units →
density = 0.08 - World visible up to 50 units →
density = 0.04 - World visible up to 100 units →
density = 0.02
Runtime change
// In update():
if key_pressed(KeyCode::Digit1) {
app.renderer_mut().set_fog(Some(FogConfig::linear([0.7, 0.8, 0.9], 5.0, 30.0)));
}
if key_pressed(KeyCode::Digit4) {
app.renderer_mut().set_fog(None); // disable
}
The change takes effect on the next frame.
Pipeline
Main pass (fragment shader)
↓
Lighting → final_rgb
↓
FOG: mix(final_rgb, fog_color, 1 - fog_factor) ← here
↓
→ HDR texture / swapchain
↓
(Bloom) → Tone Mapping → surface
Fog is applied before tone mapping: HDR values stay unclamped, and the TM applies the ACES/Reinhard curve to the already-fogged result. Result: the fog is perceptually coherent.
Compatibility
| With | OK? | Note |
|---|---|---|
| HDR + TM | ✅ | Fog before TM (recommended) |
| Bloom | ✅ | Bloom extracts the bright areas of the post-fog result |
| MSAA | ✅ | Independent (rasterizer vs fragment shader) |
| GPU culling | ✅ | Independent (culling decides what to draw, fog decides the color) |
| Shadows | ✅ | The shadow is computed before the fog |
Limitations (v1)
- Scene-level only: a single fog for the whole scene. Per-material fog would require an extra parameter in the per-object bind group.
- Euclidean distance: no volumetric or directional fog.
- Fixed color: no color gradient with distance.
Example
See lib/examples/effects/fog.rs: 15 cubes in a row + 5 spheres on an 80×80 plane, with
runtime switching between the 3 modes.
cargo run -p wsg-lib --example fog --features "all-prims"