This commit is contained in:
Jérôme Bousquié
2026-09-24 11:21:35 +02:00
parent 004761252b
commit 805babe53d
18 changed files with 733 additions and 400 deletions
+1
View File
@@ -20,6 +20,7 @@ GPU graphics background is required.
| [Materials & textures](materials.md) | Appearance: the `standard` shader, unlit mode, diffuse textures |
| [Lights](lights.md) | Directional, point, spot, ambient, `MAX_LIGHTS` |
| [Shadows](shadows.md) | Shadow mapping: picking the casting light, the packed-index pitfall |
| [HDR & tone mapping](hdr.md) | Offscreen float render + ACES/Reinhard, opt-in via `with_hdr` |
| [GPU-driven rendering](gpu-driven.md) | GPU world matrices + indirect draws, opt-in frustum culling |
| [Camera & input](camera-input.md) | Active camera, orbital controller, unified keyboard/mouse state |
| [Examples](examples.md) | The 7 repo examples, the advanced `manual` workflow, adding your own example |
+77
View File
@@ -0,0 +1,77 @@
# HDR & Tone Mapping
> **Étape 20** — Opt-in HDR rendering with tone mapping.
## What it does
By default, the WSG renderer draws directly to the window's sRGB surface. Color values
above 1.0 are **clipped** (saturated to white) — you lose all information in bright areas.
When HDR is enabled, the pipeline becomes:
```
Main pass → offscreen Rgba16Float texture (unbounded float)
TM pass → fullscreen triangle samples HDR texture, applies curve, writes to sRGB surface
```
The tone mapping **compresses** the [0, ∞) range to [0, 1] with a perceptual curve,
so bright areas are smoothly rolled off instead of clipping.
## Enabling HDR
```rust
use wsg_lib::core::ToneMapper;
use wsg_lib::app::AppBuilder;
let app = AppBuilder::new()
.title("My HDR App")
.with_hdr(ToneMapper::Aces) // ← enables HDR
.build()
.await?;
```
Without `.with_hdr(...)`, the renderer operates in LDR mode (direct to surface, zero overhead).
## Tone mapping curves
| Variant | Curve | Use case |
|---------|-------|----------|
| `ToneMapper::Aces` | ACES Filmic (Narkowicz 2015) | Cinematic look, soft highlight rolloff, good contrast |
| `ToneMapper::Reinhard` | `x / (1 + x)` | Simple, flat; less contrast but computationally trivial |
The curve is **compiled into the pipeline** at construction time (one WGSL entry point
per variant) — there is no runtime branching cost.
## Cost
| HDR state | Extra per-frame cost |
|-----------|---------------------|
| Disabled (default) | **Zero** — no texture, no pass, no pipeline |
| Enabled | +1 fullscreen render pass (triangle, 3 verts) + 1 offscreen texture (same size as window) |
The extra pass is negligible on any GPU (a few hundred microseconds). The offscreen
texture costs ~12 bytes/pixel of VRAM (RGBA16F = 8 bytes/px + the surface's own buffer).
## How it works (technical)
- **Offscreen texture**: `Rgba16Float`, same size as the window. Created in `Renderer::new`,
recreated on resize.
- **Main pass**: the color attachment targets the HDR texture instead of the surface.
The `standard_shader.wgsl` fragment output (linear float, unbounded) is stored as-is.
- **TM pass**: a fullscreen triangle (3 vertices, no vertex buffer) samples the HDR texture,
multiplies by exposure (currently fixed at 1.0), applies the tone curve, and writes to
the sRGB surface. The hardware performs the linear→sRGB gamma conversion automatically
(the surface format is `Rgba8UnormSrgb`).
- **No double gamma**: the shader outputs linear [0,1]; the sRGB surface encoding is
handled by the rasterizer.
## Exposure
Currently fixed at 1.0 (no user control yet). A future step will expose an
`exposure` field in a `HdrConfig` struct for live adjustment.
## See also
- [Shadows](shadows.md) — the other opt-in visual feature
- [GPU-driven rendering](gpu-driven.md) — the compute pipeline that feeds the main pass
- [Examples](examples.md) — the `demo` example enables HDR by default