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

89 lines
3.9 KiB
Markdown

# 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
```rust
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](../lights/emissive-exposure.md).
## 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
- [Shadows](../lights/shadows.md)
- [GPU-driven rendering](../cameras/gpu-driven.md)
- [Examples](../examples.md)