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

3.9 KiB

HDR & tone mapping

Step 20 — Opt-in HDR rendering with tone mapping.

Principle

By default, WSG renders directly to the swapchain in 8-bit sRGB. This is fine for simple scenes, but the moment you want bloom or physically plausible intensities, you hit the ceiling: 8-bit clamps everything to [0,1] before any post-process can run.

When HDR is enabled, the scene is first rendered into an offscreen float texture (Rgba16Float, full window resolution), where values are unbounded (no clamping). The tone mapping pass then compresses the HDR signal into [0,1] sRGB for the swapchain.

   Without HDR (default)              With HDR (.with_hdr(ToneMapper::Aces))
   ─────────────────────              ──────────────────────────────────────
   Scene ──────────→ Swapchain        Scene ──→ HDR texture (Rgba16Float)
     (8-bit sRGB, clamped)              │        (float, unbounded)
                                        ▼
                                   Tone Mapping (ACES / Reinhard)
                                        │
                                        ▼
                                   Swapchain (sRGB)

Enabling

use wsg_lib::prelude::*;

let app = AppBuilder::new()
    .with_hdr(ToneMapper::Aces)   // or ToneMapper::Reinhard
    .build()
    .await?;
  • Without with_hdr: the pipeline is untouched, zero cost (no extra texture, no extra pass).
  • With with_hdr: one extra full-res texture + one fullscreen pass per frame. Negligible cost on any discrete GPU; moderate on an integrated one (full-res read+write).

Tone mappers

Curve Characteristics
Aces Default. Filmic look, good highlight roll-off, slightly desaturated in the shadows. The standard for games and engines.
Reinhard Simple c / (1 + c). Neutral and fast, but highlights "washed out" (the curve saturates quickly).

The demo example starts in LDR (no HDR): the sphere's emissive intensity of 3.0 is clamped to 1.0 — it looks "burnt" but no glow. Pressing a key enables HDR+ACES and the highlight rolls off gracefully.

Exposure

Runtime-adjustable since Step 22: initialize with AppBuilder::with_exposure(…), adjust with app.set_exposure(…) (multiplicative, clamped to [0.01, 10.0] — only active when HDR is on). See Emissive + Exposure.

Interaction with other features

Feature Behavior under HDR
Shadows Unchanged — the shadow pass still writes depth; the color pass just targets the HDR texture instead of the swapchain.
Fog Applied before tone mapping (inside the main pass). The fog color must be chosen in linear space, consistent with the tone curve.
MSAA Compatible — the HDR texture becomes the multisample render target and is resolved before tone mapping.
Bloom Requires HDR. Without it, bloom is ignored (warning).

Cost and non-regression

  • No HDR (default): nothing is allocated, nothing is run. The swapchain is targeted directly.
  • HDR enabled: one extra Rgba16Float texture + one fullscreen pass (tone mapping). If bloom is also enabled, three more half-res textures and a few more passes (see the bloom page).
  • The demo example shows both paths side by side: the LDR start (emissive clamped) and the HDR+ACES mode (highlight roll-off).

Limitations

  • No auto-exposure (histogram-based). Exposure is fixed at build time (the demo hardcodes 1.0).
  • No DITHERING on the output: banding may appear in smooth gradients near black (sRGB 8-bit ceiling).

See also