Files
wsg/docs/user/effects/fog.md
T
Jérôme Bousquié d4c2d93fc5 eng doc
2026-09-25 20:06:10 +02:00

3.7 KiB
Raw Blame History

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"