eng doc
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
# Effects — user documentation
|
||||
|
||||
The **post-process side**: everything that happens between the main pass and the screen.
|
||||
All effects are opt-in — a feature you don't enable costs nothing (no textures, no passes).
|
||||
|
||||
| Page | Topic |
|
||||
|------|-------|
|
||||
| [HDR & tone mapping](hdr.md) | Offscreen float render + ACES/Reinhard, opt-in via `with_hdr` |
|
||||
| [Bloom](bloom.md) | Post-process glow: threshold → blur → composite |
|
||||
| [MSAA (anti-aliasing)](msaa.md) | Multi-sample edge smoothing, opt-in via `with_msaa(4)` |
|
||||
| [Fog (distance)](fog.md) | Distance fog (3 modes), masks world edges, opt-in via `with_fog()` |
|
||||
| [DoF (depth of field)](dof.md) | Cinematic bokeh blur, focus distance, opt-in via `with_dof()` |
|
||||
|
||||
Example folder: [`lib/examples/effects/`](../../../lib/examples/effects/README.md)
|
||||
(`demo`, `bloom`, `hdr`, `msaa`, `fog`, `dof`).
|
||||
|
||||
## Links
|
||||
|
||||
- [User documentation index](../README.md) · [Quickstart](../quickstart.md) · [Examples](../examples.md)
|
||||
@@ -0,0 +1,99 @@
|
||||
# Bloom (Step 23)
|
||||
|
||||
**Bloom** is a post-process that creates a "glow" effect around the bright areas of the image.
|
||||
Pixels whose luminance exceeds a threshold are extracted, blurred, then added back to the
|
||||
original image.
|
||||
|
||||
> **Prerequisite**: bloom requires HDR (`AppBuilder::with_hdr`). Without HDR, values are already
|
||||
> clamped to [0,1] and there is nothing "bright" to extract.
|
||||
|
||||
## Activation
|
||||
|
||||
```rust
|
||||
use wsg_lib::prelude::*;
|
||||
|
||||
let app = AppBuilder::new()
|
||||
.with_hdr(ToneMapper::Aces) // required
|
||||
.with_bloom(BloomConfig {
|
||||
threshold: 1.0, // HDR luminance threshold
|
||||
knee: 0.5, // soft-knee width
|
||||
intensity: 0.8, // glow intensity
|
||||
radius: 4.0, // blur radius (pixels, half-res)
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.await?;
|
||||
```
|
||||
|
||||
## `BloomConfig`
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `threshold` | `f32` | `1.0` | Luminance threshold (linear HDR units). Only pixels above the threshold contribute to the bloom. |
|
||||
| `knee` | `f32` | `0.5` | Soft-knee width. Larger = smoother transition. |
|
||||
| `intensity` | `f32` | `0.8` | Multiplier applied to the blurred result before adding it to the HDR. |
|
||||
| `radius` | `f32` | `4.0` | Blur radius in pixels (at half resolution). Larger = wider glow. |
|
||||
|
||||
## Runtime update
|
||||
|
||||
```rust
|
||||
// In the handler (fn update):
|
||||
if app.bloom_enabled() {
|
||||
app.set_bloom_config(BloomConfig {
|
||||
intensity: new_intensity,
|
||||
..app.bloom_config()
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Changes take effect on the next frame (the uniforms are re-written every frame).
|
||||
|
||||
## Pipeline (4 GPU passes)
|
||||
|
||||
```
|
||||
Scene ──→ HDR (full res, Rgba16Float)
|
||||
│
|
||||
├──→ [1] Threshold (full → half res)
|
||||
│ Soft-knee: smoothstep(knee, knee+1, lum)
|
||||
│
|
||||
├──→ [2] Blur H (half res)
|
||||
│ 9-tap separable Gaussian, direction = (1/w, 0)
|
||||
│
|
||||
├──→ [3] Blur V (half res)
|
||||
│ 9-tap separable Gaussian, direction = (0, 1/h)
|
||||
│ (ping-pong: writes into the bright texture)
|
||||
│
|
||||
└──→ [4] Composite (full res)
|
||||
output = HDR + bloom × intensity
|
||||
(writes into a 3rd full-res texture)
|
||||
│
|
||||
▼
|
||||
Tone Mapping (reads the composite)
|
||||
│
|
||||
▼
|
||||
Surface (sRGB)
|
||||
```
|
||||
|
||||
## Cost
|
||||
|
||||
- **Without bloom** (default): zero overhead. The TM reads the HDR texture directly.
|
||||
- **With bloom**: 4 extra passes (1 full-res + 3 half-res) + 3 intermediate textures. The cost
|
||||
is moderate because the blur runs at half resolution.
|
||||
|
||||
## Non-regression
|
||||
|
||||
- `with_bloom()` without `with_hdr()` → warning + no-op (the bloom is ignored).
|
||||
- Without `with_bloom()` → the TM reads the HDR texture directly (the Step 20 behavior is
|
||||
unchanged).
|
||||
|
||||
## Limitations (MVP)
|
||||
|
||||
- A single mip level (no multi-mip "soft" bloom à la Unreal).
|
||||
- No directional bloom.
|
||||
- The blur is a 9-tap Gaussian (good enough for a "soft" glow).
|
||||
- No per-layer bloom (no per-material "bloom mask").
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](../README.md) · [HDR & tone mapping](hdr.md) · [Examples](../examples.md)
|
||||
- [Root README](../../../README.md)
|
||||
@@ -0,0 +1,64 @@
|
||||
# DoF (depth of field)
|
||||
|
||||
Depth of field simulates camera-lens behavior: objects at the **focus distance** are sharp,
|
||||
everything else is progressively blurred (the cinematic "bokeh" look). DoF is a post-process
|
||||
that operates on the HDR texture + depth buffer, before tone mapping.
|
||||
|
||||
> **Prerequisite**: like bloom, DoF reads the HDR texture (and the depth buffer for the
|
||||
> focus/blur computation). Enable HDR together with it.
|
||||
|
||||
## Activation
|
||||
|
||||
DoF is opt-in through the builder. Without it, no DoF textures are allocated and the pipeline
|
||||
cost is **zero**:
|
||||
|
||||
```rust
|
||||
use wsg_lib::prelude::*;
|
||||
|
||||
let app = AppBuilder::new()
|
||||
.with_hdr(ToneMapper::Aces)
|
||||
.with_dof(DoFConfig::cinematic(4.0)) // sharp at 4.0 world units
|
||||
.build()
|
||||
.await?;
|
||||
```
|
||||
|
||||
## `DoFConfig`
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|-------|------|---------|
|
||||
| `focus_distance` | `f32` | World distance where the image is perfectly sharp |
|
||||
| `aperture` | `f32` | Blur intensity (0.0–1.0, clamped). Scales the circle of confusion |
|
||||
| `max_blur` | `f32` | Maximum blur radius in pixels (clamps the CoC) |
|
||||
|
||||
Presets:
|
||||
|
||||
```rust
|
||||
DoFConfig::new(focus_distance, aperture, max_blur) // custom
|
||||
DoFConfig::cinematic(focus_distance) // aperture 0.3, max blur 12 px (cutscenes)
|
||||
DoFConfig::subtle(focus_distance) // aperture 0.1, max blur 8 px (gameplay)
|
||||
```
|
||||
|
||||
## Runtime change
|
||||
|
||||
The `dof` example switches focus presets with the keys `1`–`4` (near / mid / far / infinity)
|
||||
and follows the zoom:
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example dof
|
||||
```
|
||||
|
||||
## Cost
|
||||
|
||||
- **Without DoF** (default): zero overhead — no textures, no pass.
|
||||
- **With DoF**: 1 extra fullscreen pass + 2 intermediate textures (the bokeh buffer), before
|
||||
tone mapping.
|
||||
|
||||
## Limitations (MVP)
|
||||
|
||||
- A single focus distance per frame (no per-pixel focus / rack-focus over time).
|
||||
- The blur is a fixed-radius Gaussian scaled by the circle of confusion.
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](../README.md) · [Bloom](bloom.md) · [HDR & tone mapping](hdr.md) · [Examples](../examples.md)
|
||||
- [Root README](../../../README.md)
|
||||
@@ -0,0 +1,135 @@
|
||||
# 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
|
||||
|
||||
```rust
|
||||
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
|
||||
|
||||
```rust
|
||||
// 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
|
||||
|
||||
```rust
|
||||
// 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
|
||||
|
||||
```text
|
||||
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.
|
||||
|
||||
```sh
|
||||
cargo run -p wsg-lib --example fog --features "all-prims"
|
||||
```
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](../README.md) · [HDR & tone mapping](hdr.md) · [Examples](../examples.md)
|
||||
- [Root README](../../../README.md)
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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)
|
||||
@@ -0,0 +1,79 @@
|
||||
# MSAA (anti-aliasing)
|
||||
|
||||
MSAA (Multisample Anti-Aliasing) smooths the edges of meshes by sampling each pixel multiple
|
||||
times **at rasterization time** (before the fragment shader).
|
||||
|
||||
## Enabling
|
||||
|
||||
```rust
|
||||
use wsg_lib::prelude::*;
|
||||
|
||||
let app = AppBuilder::new()
|
||||
.with_msaa(4) // 4x MSAA
|
||||
.build()
|
||||
.await?;
|
||||
```
|
||||
|
||||
- **Without** `with_msaa`: the swapchain is used as-is (1x, no cost).
|
||||
- **With** `with_msaa(4)`: the swapchain is created with `sample_count = 4`, and a
|
||||
**resolve** pass (multisample → swapchain) runs at the end of each frame.
|
||||
|
||||
## Cost
|
||||
|
||||
- The **main pass** becomes more expensive (fragment shader run `sample_count` times per pixel
|
||||
on aliased edges — in practice much less, since fully-covered pixels are only processed once).
|
||||
- One extra **fullscreen resolve** per frame (GPU-native, very cheap).
|
||||
- VRAM: the swapchain buffer is multiplied by `sample_count` (4x for 4x MSAA).
|
||||
|
||||
In practice: 4x MSAA is **negligible** on a discrete GPU and perfectly acceptable on an
|
||||
integrated one for scenes of this complexity.
|
||||
|
||||
## MSAA + HDR
|
||||
|
||||
The two compose naturally:
|
||||
|
||||
```
|
||||
Scene ──→ HDR multisample texture (sample_count = N)
|
||||
│
|
||||
▼ resolve
|
||||
HDR texture (single sample)
|
||||
│
|
||||
▼ (bloom?)
|
||||
▼ tone mapping
|
||||
Swapchain (sRGB)
|
||||
```
|
||||
|
||||
- `with_msaa(4)` + `with_hdr(…)`: the offscreen HDR texture becomes multisample and is
|
||||
**resolved** before tone mapping (and before bloom, which operates on the single-sample
|
||||
buffer).
|
||||
- `with_msaa(4)` without HDR: the swapchain itself is multisample, resolved at the end of the
|
||||
frame.
|
||||
|
||||
## MSAA + fog
|
||||
|
||||
Fog is applied **inside** the fragment shader (per sample), so it is inherently MSAA-compatible:
|
||||
each sample computes its own fog factor based on its own depth. No aliasing on the fog
|
||||
boundaries.
|
||||
|
||||
## What MSAA does NOT fix
|
||||
|
||||
- **Transparency aliasing** (there is no transparency in the engine — all opaque).
|
||||
- **Texture shimmering** at distance: this is the domain of **anisotropic filtering** (already
|
||||
enabled: `SampleFilter::AnisotropicClamped` + `anisotropy = 4`).
|
||||
- **Temporal flicker** of fine details: this would be the domain of TAA (out of scope).
|
||||
|
||||
## Limitations
|
||||
|
||||
- `sample_count` is fixed **at swapchain creation** (not changeable at runtime without
|
||||
recreating the window/surface).
|
||||
- `2` and `4` are the useful values. `8` exists but the cost/quality ratio is bad.
|
||||
- Not all adapters support MSAA on the swapchain — if unsupported, the builder falls back to
|
||||
1x with a warning.
|
||||
|
||||
See `lib/examples/effects/msaa.rs` for a full interactive demo (torus + Icosphere, with
|
||||
zoom/orbit, MSAA 4x by default).
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](../README.md) · [HDR & tone mapping](hdr.md) · [Examples](../examples.md)
|
||||
- [Root README](../../../README.md)
|
||||
Reference in New Issue
Block a user