This commit is contained in:
Jérôme Bousquié
2026-09-25 11:20:20 +02:00
parent 35aeb769a8
commit 9614156848
15 changed files with 822 additions and 333 deletions
+2
View File
@@ -22,6 +22,7 @@ GPU graphics background is required.
| [Shadows](shadows.md) | Shadow mapping: picking the casting light, the packed-index pitfall |
| [Mesh & primitives](mesh.md) | Procedural generators + file import (OBJ), feature-gated |
| [HDR & tone mapping](hdr.md) | Offscreen float render + ACES/Reinhard, opt-in via `with_hdr` |
| [MSAA (anti-aliasing)](msaa.md) | Multi-sample edge smoothing, opt-in via `with_msaa(4)` |
| [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 |
@@ -36,6 +37,7 @@ WSG follows a strict rule: **a feature you don't enable costs nothing at runtime
|---------|--------------|----------------|
| Shadows | `scene.set_shadow_caster(Some(idx))` | No shadow map allocated, no depth pass, no PCF sampling |
| HDR + Tone mapping | `AppBuilder::with_hdr(ToneMapper::Aces)` | No offscreen texture, no TM pass, direct-to-surface render |
| MSAA | `AppBuilder::with_msaa(4)` | Single-sample (1×), zero overhead |
| GPU-driven culling | `AppBuilder::with_gpu_driven(true)` | No compute pipeline, no indirect draw buffers |
| LOD | `scene.create_mesh_with_lod(…, levels)` | Single-level mesh, no decimation, no hysteresis |
| Primitives | Cargo feature `prim-*` (default: all) | Not compiled at all |
+127
View File
@@ -0,0 +1,127 @@
# MSAA (Anti-aliasing)
Multi-Sample Anti-Aliasing (MSAA) smooths jagged edges by rendering the scene
at a higher sample count (e.g. 4 samples per pixel), then averaging the samples
into the final image.
## Activation
MSAA is opt-in via the builder. When disabled (default), the renderer uses
single-sample rendering with zero overhead:
```rust
let app = AppBuilder::new()
.title("My App")
.with_msaa(4) // 4× MSAA (also: 2 or 8)
.build()
.await?;
```
## How it works
MSAA is a **rasterizer feature** — no new shader is needed. The pipeline:
```text
┌─────────────────────────────────────────────────────────────────────┐
│ Without MSAA (default) │
│ │
│ Main pass ──→ HDR texture (1 sample) ──→ [Bloom] ──→ TM ──→ Surface │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ With MSAA 4× (and HDR) │
│ │
│ Main pass ──→ MSAA texture (4 samples, Rgba16Float) │
│ ↓ resolve (hardware average) │
│ HDR texture (1 sample) │
│ ↓ │
│ [Bloom] ──→ TM ──→ Surface │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ With MSAA 4× (no HDR) │
│ │
│ Main pass ──→ MSAA texture (4 samples, surface format) │
│ ↓ resolve │
│ Swapchain (surface) │
└─────────────────────────────────────────────────────────────────────┘
```
Key points:
- The **main scene pass** renders into the MSAA texture (N samples/pixel).
- The **resolve** (hardware average) produces the single-sample output.
- **Post-processes** (bloom, tone mapping) operate on the **resolved**
single-sample texture — they are completely unaffected by MSAA.
- The **shadow map** is always single-sample (depth-only, not visible directly).
## Sample count
| Count | Quality | Cost (approx.) | Use case |
|-------|---------|----------------|----------|
| 2 | Basic | ~1.1× | Low-end / battery |
| **4** | **Good** | **~1.3–1.5×** | **Default, most games** |
| 8 | Excellent | ~1.6–2.0× | High-end / static scenes |
The cost is in the rasterizer/fill-rate (each pixel is shaded N times at
triangles' edges). Interior pixels (covered by a single triangle) are
shaded only once — MSAA only multiplies the **edge** cost.
## Runtime query
```rust
// In AppHandler::setup or update:
let active = app.renderer().msaa_enabled(); // true if sample_count > 1
let count = app.renderer().msaa_sample_count(); // 1, 2, 4, or 8
```
MSAA is a **build-time** setting: the multi-sample textures are allocated
at startup. Changing the sample count requires recreating the textures
(window resize does this automatically).
## Configuration struct
```rust
use wsg_lib::MsaaConfig;
let config = MsaaConfig { sample_count: 4 };
// Or use the builder shortcut (validated):
let app = AppBuilder::new().with_msaa(4).build().await?;
```
## Compatibility
| Feature | Compatible? | Notes |
|---------|-------------|-------|
| HDR + Tone Mapping | ✅ | MSAA texture is `Rgba16Float`, resolves into HDR |
| Bloom | ✅ | Bloom reads the resolved (single-sample) HDR texture |
| Shadows | ✅ | Shadow map is always single-sample |
| Frustum Culling | ✅ | Independent (compute pass) |
| LOD | ✅ | Independent (draw args) |
| Emissive | ✅ | Per-entity, in the main pass |
## Limitations
- **Does not smooth UV-dependent aliasing** (texture shimmer). For that,
use mipmaps + anisotropic filtering (future: texture module).
- **Cost scales with overdraw**: fully transparent or heavily overlapping
geometry pays the full N× cost.
- **GPU support**: most modern GPUs support 4× for all formats. 8× may be
limited for float formats (check `Device::limits().max_color_attachment_samples`).
## Example
```rust
use wsg_lib::app::AppBuilder;
use wsg_lib::core::ToneMapper;
let app = AppBuilder::new()
.title("MSAA Demo")
.size(1280, 720)
.with_msaa(4)
.with_hdr(ToneMapper::Aces)
.build()
.await?;
```
See `examples/msaa.rs` for a full interactive demo with cube, sphere,
and ground plane where aliasing is clearly visible without MSAA.