eng doc
This commit is contained in:
+1
-1
@@ -35,7 +35,7 @@ Note: `standard_shader.wgsl` (Phong + PBR, with an explicit **unlit** mode) is t
|
|||||||
|
|
||||||
A Compute Pass derives each entity's world matrix and fills per-entity indirect draw arguments (with opt-in frustum culling), then the main and shadow render passes issue one indirect draw per active slot.
|
A Compute Pass derives each entity's world matrix and fills per-entity indirect draw arguments (with opt-in frustum culling), then the main and shadow render passes issue one indirect draw per active slot.
|
||||||
|
|
||||||
Spec: [docs/tech/ARCHI_CPU_GPU.md](docs/tech/ARCHI_CPU_GPU.md) · User guide: [docs/user/gpu-driven.md](docs/user/gpu-driven.md)
|
Spec: [docs/tech/ARCHI_CPU_GPU.md](docs/tech/ARCHI_CPU_GPU.md) · User guide: [docs/user/cameras/gpu-driven.md](docs/user/cameras/gpu-driven.md)
|
||||||
|
|
||||||
### Module layout
|
### Module layout
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ Ce document sert de spécification technique et de trame d'implémentation pour
|
|||||||
> groupés par `Material` (1 `set_pipeline` + 1 bind group @2 par matériau distinct, pas par entité ;
|
> groupés par `Material` (1 `set_pipeline` + 1 bind group @2 par matériau distinct, pas par entité ;
|
||||||
> le pass d'ombre — un seul pipeline — est inchangé). Réordonnancement sûr car tous les pipelines
|
> le pass d'ombre — un seul pipeline — est inchangé). Réordonnancement sûr car tous les pipelines
|
||||||
> sont opaques (`BlendState::REPLACE`) ; les no-ops cullés restent émis dans leur groupe.
|
> sont opaques (`BlendState::REPLACE`) ; les no-ops cullés restent émis dans leur groupe.
|
||||||
> Détail : `docs/user/gpu-driven.md` § « Batching by material ».
|
> Détail : `docs/user/cameras/gpu-driven.md` § « Batching by material ».
|
||||||
> **LOD (Étape 19, 2026-09-23)** : le pass `cull` remplit désormais les arguments indirects à partir
|
> **LOD (Étape 19, 2026-09-23)** : le pass `cull` remplit désormais les arguments indirects à partir
|
||||||
> du **niveau de détail** du slot, et non d'un seul jeu de comptes. Le choix du niveau est fait **côté CPU**
|
> du **niveau de détail** du slot, et non d'un seul jeu de comptes. Le choix du niveau est fait **côté CPU**
|
||||||
> (rayon de la sphère bounding projeté en pixels + hystérésis asymétrique — `math/lod.rs`, pur et unit-testé) ;
|
> (rayon de la sphère bounding projeté en pixels + hystérésis asymétrique — `math/lod.rs`, pur et unit-testé) ;
|
||||||
@@ -55,7 +55,7 @@ Ce document sert de spécification technique et de trame d'implémentation pour
|
|||||||
> buffers vertex/index du mesh** (offsets en
|
> buffers vertex/index du mesh** (offsets en
|
||||||
> unités d'élément, pas d'octet — c'est ce qu'exigent les arguments `drawIndirect*` de WebGPU ; plafond u16 :
|
> unités d'élément, pas d'octet — c'est ce qu'exigent les arguments `drawIndirect*` de WebGPU ; plafond u16 :
|
||||||
> 65 535 sommets/mesh, 4 niveaux max). LOD activé par défaut ; `set_lod_enabled(false)` restaure un rendu
|
> 65 535 sommets/mesh, 4 niveaux max). LOD activé par défaut ; `set_lod_enabled(false)` restaure un rendu
|
||||||
> bit-à-bit identique au pré-LOD (niveau 0 partout = comptes complets). Détail : `docs/user/gpu-driven.md`
|
> bit-à-bit identique au pré-LOD (niveau 0 partout = comptes complets). Détail : `docs/user/cameras/gpu-driven.md`
|
||||||
> § « Level of Detail ».
|
> § « Level of Detail ».
|
||||||
|
|
||||||
1. Répartition des Rôles : CPU vs GPU (La Source de Vérité)
|
1. Répartition des Rôles : CPU vs GPU (La Source de Vérité)
|
||||||
|
|||||||
+42
-49
@@ -1,64 +1,57 @@
|
|||||||
# User documentation — WSG
|
# WSG — User documentation
|
||||||
|
|
||||||
**Usage** documentation for the `wsg-lib` crate: how to build a 3D rendering application
|
WSG (WGPU Simple Graphics) is a 3D graphics engine built on top of
|
||||||
without touching wgpu directly. It targets a developer with basic Rust knowledge; no prior
|
[wgpu](https://docs.rs/wgpu) and [winit](https://docs.rs/winit).
|
||||||
GPU graphics background is required.
|
It deliberately provides **no scene-graph abstraction**: you create resources,
|
||||||
|
place entities, and write the frame loop yourself. The engine handles the rest
|
||||||
> **Not to be confused**: these pages explain *how to use* the API. The **technical**
|
(GPU context, compilation, command encoding, presentation).
|
||||||
> documentation (internal architecture, design decisions, future targets) lives in
|
|
||||||
> [../tech/](../tech/ARCHI_APP.md), and the exhaustive API reference is generated by rustdoc
|
|
||||||
> (`cargo doc -p wsg-lib --no-deps`).
|
|
||||||
|
|
||||||
## Where to start
|
## Where to start
|
||||||
|
|
||||||
1. [Quickstart](quickstart.md) — your first window and your first object, in ~30 lines.
|
1. [Quickstart](quickstart.md) — your first window and your first object, in ~30 lines.
|
||||||
2. Then, at your own pace, depending on what you need:
|
2. Then, at your pace, pick a **topic folder** (which mirrors the example folders in
|
||||||
|
[`lib/examples/`](../../lib/examples/README.md) — each page is paired with its examples):
|
||||||
|
|
||||||
| Page | Topic |
|
| Folder | Pages |
|
||||||
|-------|-------|
|
|--------|-------|
|
||||||
| [Meshes](meshes.md) | Geometries: procedural primitives, custom `Geometry`, entities and `Transform` |
|
| [`meshes/`](meshes/README.md) | [Meshes](meshes/meshes.md) — geometries, entities and `Transform` · [Geometry sources](meshes/sources.md) — procedural generators + file import · [Materials & textures](meshes/materials.md) — the `standard` shader, unlit mode, textures |
|
||||||
| [Materials & textures](materials.md) | Appearance: the `standard` shader, unlit mode, diffuse textures |
|
| [`lights/`](lights/README.md) | [Lights](lights/lights.md) — directional/point/spot/ambient, `MAX_LIGHTS` · [Shadows](lights/shadows.md) — shadow mapping · [Emissive + Exposure](lights/emissive-exposure.md) |
|
||||||
| [Lights](lights.md) | Directional, point, spot, ambient, `MAX_LIGHTS` |
|
| [`cameras/`](cameras/README.md) | [Camera & input](cameras/camera-input.md) — orbital controller, unified input · [GPU-driven rendering](cameras/gpu-driven.md) — culling, LOD |
|
||||||
| [Shadows](shadows.md) | Shadow mapping: picking the casting light, the packed-index pitfall |
|
| [`effects/`](effects/README.md) | [HDR](effects/hdr.md) · [Bloom](effects/bloom.md) · [MSAA](effects/msaa.md) · [Fog](effects/fog.md) · [DoF](effects/dof.md) |
|
||||||
| [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)` |
|
|
||||||
| [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 plane, opt-in via `with_dof()` |
|
|
||||||
| [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 16 repo examples in 4 folders (`meshes/`, `lights/`, `cameras/`, `effects/`), the advanced `manual` workflow, adding your own example |
|
|
||||||
|
|
||||||
The pages are cross-linked: each page ends with a link to the next one.
|
Plus [Examples](examples.md) — the 16 examples of the repo in 4 folders, the advanced
|
||||||
|
`manual` workflow, and how to add your own example.
|
||||||
|
|
||||||
## Design principle: opt-in = zero cost
|
The pages are cross-linked: each page ends with links to its related pages.
|
||||||
|
|
||||||
WSG follows a strict rule: **a feature you don't enable costs nothing at runtime**.
|
## Design principles
|
||||||
|
|
||||||
| Feature | How to enable | If NOT enabled |
|
- **Explicit over magic**: no scene graph, no ECS, no hidden state machine. What you write
|
||||||
|---------|--------------|----------------|
|
is what runs.
|
||||||
| Shadows | `scene.set_shadow_caster(Some(idx))` | No shadow map allocated, no depth pass, no PCF sampling |
|
- **The handler drives the loop**: `AppHandler` is the only required trait (`setup`,
|
||||||
| HDR + Tone mapping | `AppBuilder::with_hdr(ToneMapper::Aces)` | No offscreen texture, no TM pass, direct-to-surface render |
|
`update`, `render` + optional event hook).
|
||||||
| MSAA | `AppBuilder::with_msaa(4)` | Single-sample (1×), zero overhead |
|
- **String IDs everywhere**: meshes, materials, textures and entities are referenced by
|
||||||
| GPU-driven culling | `AppBuilder::with_gpu_driven(true)` | No compute pipeline, no indirect draw buffers |
|
label — no integer handles to manage, errors are readable.
|
||||||
| LOD | `scene.create_mesh_with_lod(…, levels)` | Single-level mesh, no decimation, no hysteresis |
|
- **Safe core, `unsafe` at the edges**: the public API is fully safe; `unsafe` is confined
|
||||||
| Primitives | Cargo feature `prim-*` (default: all) | Not compiled at all |
|
to the raw-pointer interop layer.
|
||||||
| File import | Cargo feature `import-*` | Not compiled at all |
|
- **Feature-gated primitives**: every primitive and importer behind a Cargo feature
|
||||||
|
(`prim-cube`, `import-obj`, …) — default is `all-prims` + `import-obj`.
|
||||||
|
|
||||||
The distinction matters:
|
## Documentation tree
|
||||||
- **Runtime opt-in** (shadows, HDR, culling, LOD): the code is compiled into your binary
|
|
||||||
but is **completely inert** if you never call the activation method. No GPU resources are
|
|
||||||
allocated, no passes execute, no per-frame overhead. The cost of the code being in the
|
|
||||||
binary is a few KB — negligible.
|
|
||||||
- **Compile-time opt-in** (primitives, import): the code is **not compiled at all** unless
|
|
||||||
you opt in via Cargo features. This matters when you want to minimize compile time or
|
|
||||||
binary size for a minimal build.
|
|
||||||
|
|
||||||
You can mix both: build with `--no-default-features --features "prim-cube"` for a minimal
|
```
|
||||||
binary, then enable shadows/HDR at runtime only for the scenes that need them.
|
README.md this index (the one you are reading)
|
||||||
|
quickstart.md the 30-line path to a window + a cube
|
||||||
|
examples.md the 16 repo examples, the manual workflow, adding your own
|
||||||
|
meshes/ meshes, geometry sources, materials & textures
|
||||||
|
lights/ lights, shadows, emissive + exposure
|
||||||
|
cameras/ camera & input, GPU-driven rendering (culling, LOD)
|
||||||
|
effects/ HDR, bloom, MSAA, fog, DoF
|
||||||
|
```
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- Technical documentation (architecture): [ARCHI_APP](../tech/ARCHI_APP.md) · [ARCHI_RENDU](../tech/ARCHI_RENDU.md) · [ARCHI_CPU_GPU](../tech/ARCHI_CPU_GPU.md) · [ARCHI_ARENES](../tech/ARCHI_ARENES.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md)
|
- [Root README](../../README.md)
|
||||||
- [Root README](../../README.md) · [ROADMAP](../ROADMAP.md) · [DRAFT](../DRAFT.md)
|
- Technical docs: [ARCHI_APP](../tech/ARCHI_APP.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md) ·
|
||||||
- Full API reference: `cargo doc -p wsg-lib --no-deps`
|
[ARCHI_CPU_GPU](../tech/ARCHI_CPU_GPU.md) · [ARCHI_RENDU](../tech/ARCHI_RENDU.md)
|
||||||
|
- [ROADMAP](../ROADMAP.md)
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
# Bloom (Étape 23)
|
|
||||||
|
|
||||||
Le **bloom** est un post-process qui crée un effet de "glow" autour des zones brillantes de
|
|
||||||
l'image. Les pixels dont la luminance dépasse un seuil sont extraits, floutés, puis ajoutés
|
|
||||||
à l'image originale.
|
|
||||||
|
|
||||||
> **Prérequis** : le bloom nécessite l'HDR (`AppBuilder::with_hdr`). Sans HDR, les valeurs
|
|
||||||
> sont déjà clampées à [0,1] et il n'y a rien de "brillant" à extraire.
|
|
||||||
|
|
||||||
## Activation
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use wsg_lib::prelude::*;
|
|
||||||
|
|
||||||
let app = AppBuilder::new()
|
|
||||||
.with_hdr(ToneMapper::Aces) // requis
|
|
||||||
.with_bloom(BloomConfig {
|
|
||||||
threshold: 1.0, // seuil de luminance HDR
|
|
||||||
knee: 0.5, // largeur du soft-knee
|
|
||||||
intensity: 0.8, // intensité du glow
|
|
||||||
radius: 4.0, // rayon du blur (pixels, demi-rés)
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.build()
|
|
||||||
.await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## `BloomConfig`
|
|
||||||
|
|
||||||
| Champ | Type | Défaut | Description |
|
|
||||||
|-------|------|--------|-------------|
|
|
||||||
| `threshold` | `f32` | `1.0` | Seuil de luminance (unités HDR linéaires). Seuls les pixels > seuil contribuent au bloom. |
|
|
||||||
| `knee` | `f32` | `0.5` | Largeur du soft-knee. Plus grand = transition plus douce. |
|
|
||||||
| `intensity` | `f32` | `0.8` | Multiplicateur appliqué au résultat flouté avant addition à l'HDR. |
|
|
||||||
| `radius` | `f32` | `4.0` | Rayon du blur en pixels (à la demi-résolution). Plus grand = glow plus étendu. |
|
|
||||||
|
|
||||||
## Mise à jour runtime
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Dans le handler (fn update):
|
|
||||||
if app.bloom_enabled() {
|
|
||||||
app.set_bloom_config(BloomConfig {
|
|
||||||
intensity: new_intensity,
|
|
||||||
..app.bloom_config()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Les changements prennent effet au frame suivant (les uniforms sont ré-écrits chaque frame).
|
|
||||||
|
|
||||||
## Pipeline (4 passes GPU)
|
|
||||||
|
|
||||||
```
|
|
||||||
Scene ──→ HDR (full res, Rgba16Float)
|
|
||||||
│
|
|
||||||
├──→ [1] Threshold (full → half res)
|
|
||||||
│ Soft-knee: smoothstep(knee, knee+1, lum)
|
|
||||||
│
|
|
||||||
├──→ [2] Blur H (half res)
|
|
||||||
│ 9-tap Gaussian séparable, direction = (1/w, 0)
|
|
||||||
│
|
|
||||||
├──→ [3] Blur V (half res)
|
|
||||||
│ 9-tap Gaussian séparable, direction = (0, 1/h)
|
|
||||||
│ (ping-pong: écrit dans la texture bright)
|
|
||||||
│
|
|
||||||
└──→ [4] Composite (full res)
|
|
||||||
output = HDR + bloom × intensity
|
|
||||||
(écrit dans une 3e texture full-res)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Tone Mapping (lit le composite)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Surface (sRGB)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Coût
|
|
||||||
|
|
||||||
- **Sans bloom** (défaut) : zéro overhead. Le TM lit directement la texture HDR.
|
|
||||||
- **Avec bloom** : 4 passes supplémentaires (1 full-res + 3 half-res) + 3 textures
|
|
||||||
intermédiaires. Le coût est modéré car le blur est en demi-résolution.
|
|
||||||
|
|
||||||
## Non-régression
|
|
||||||
|
|
||||||
- `with_bloom()` sans `with_hdr()` → warning + no-op (le bloom est ignoré).
|
|
||||||
- Sans `with_bloom()` → le TM lit la texture HDR directement (comportement Étape 20 inchangé).
|
|
||||||
|
|
||||||
## Limitations (MVP)
|
|
||||||
|
|
||||||
- Un seul niveau de mip (pas de multi-mip "soft" bloom à la Unreal).
|
|
||||||
- Pas de directional bloom.
|
|
||||||
- Le blur est un Gaussian 9-taps (qualité suffisante pour un glow "soft").
|
|
||||||
- Pas de bloom séparé par couche (pas de "bloom mask" par matériau).
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Cameras — user documentation
|
||||||
|
|
||||||
|
The **viewpoint side**: the active camera, the orbital controller, unified input, and the
|
||||||
|
GPU-driven pipeline (frustum culling, LOD) that the camera drives.
|
||||||
|
|
||||||
|
| Page | Topic |
|
||||||
|
|------|-------|
|
||||||
|
| [Camera & input](camera-input.md) | Active camera, `CameraController` (orbit/zoom/reset), unified keyboard/mouse state, recipes |
|
||||||
|
| [GPU-driven rendering](gpu-driven.md) | GPU world matrices + indirect draws, opt-in frustum culling, LOD, debugging |
|
||||||
|
|
||||||
|
Example folder: [`lib/examples/cameras/`](../../../lib/examples/cameras/README.md)
|
||||||
|
(`culling`).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [User documentation index](../README.md) · [Quickstart](../quickstart.md) · [Examples](../examples.md)
|
||||||
@@ -57,7 +57,7 @@ Two public fields tune the feel of the camera (defaults in parentheses):
|
|||||||
| `zoom_factor` | multiplicative distance change per wheel notch (`distance *= factor^scroll`) | `0.9` (10% per notch) |
|
| `zoom_factor` | multiplicative distance change per wheel notch (`distance *= factor^scroll`) | `0.9` (10% per notch) |
|
||||||
|
|
||||||
The exact wiring snippet (orbit + zoom + reset + `1`/`2`/`3` presets, driven from
|
The exact wiring snippet (orbit + zoom + reset + `1`/`2`/`3` presets, driven from
|
||||||
`app.input`) is in [`demo.rs`](../../lib/examples/effects/demo.rs), `update()` section.
|
`app.input`) is in [`demo.rs`](../../../lib/examples/effects/demo.rs), `update()` section.
|
||||||
|
|
||||||
## 3. The unified input state
|
## 3. The unified input state
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ fn update(&mut self, app: &mut wsg_lib::App) {
|
|||||||
```
|
```
|
||||||
|
|
||||||
> **Gamepad**: the API is reserved (`InputState` will pass through `DeviceEvent`s) but not
|
> **Gamepad**: the API is reserved (`InputState` will pass through `DeviceEvent`s) but not
|
||||||
> implemented yet — deferred, see [ROADMAP](../ROADMAP.md).
|
> implemented yet — deferred, see [ROADMAP](../../ROADMAP.md).
|
||||||
|
|
||||||
## 4. Common recipes
|
## 4. Common recipes
|
||||||
|
|
||||||
@@ -123,5 +123,5 @@ fn update(&mut self, app: &mut wsg_lib::App) {
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [User README](README.md) · [Lights](lights.md) · [Examples](examples.md)
|
- [User README](../README.md) · [Lights](../lights/lights.md) · [Examples](../examples.md)
|
||||||
- [Root README](../../README.md) · [ARCHI_APP](../tech/ARCHI_APP.md)
|
- [Root README](../../../README.md) · [ARCHI_APP](../../tech/ARCHI_APP.md)
|
||||||
@@ -242,4 +242,4 @@ the indirect-draw machinery still active.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Next: [Examples](examples.md) · Back to [User documentation index](README.md)
|
Next: [Examples](../examples.md) · Back to [User documentation index](../README.md)
|
||||||
@@ -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)
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
# Émissive + Exposure
|
|
||||||
|
|
||||||
## Principe
|
|
||||||
|
|
||||||
Deux features complémentaires (Étape 22) :
|
|
||||||
|
|
||||||
| Feature | Effet | Coût |
|
|
||||||
|---------|-------|------|
|
|
||||||
| **Exposure** (6.1) | Multiplie la luminance avant la courbe de tone mapping | Zéro si HDR inactif |
|
|
||||||
| **Emissive** (6.2) | Ajoute une couleur émise (indépendante des lumières) | Zéro si `emissive = [0,0,0,0]` |
|
|
||||||
|
|
||||||
## Exposure
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Initialisation (optionnel, default = 1.0)
|
|
||||||
let app = AppBuilder::new()
|
|
||||||
.with_hdr(ToneMapper::Aces)
|
|
||||||
.with_exposure(1.5) // démarre plus clair
|
|
||||||
.build().await?;
|
|
||||||
|
|
||||||
// Runtime (dans update())
|
|
||||||
app.set_exposure(app.exposure() * 1.1); // +1 "stop"
|
|
||||||
app.set_exposure(1.0); // reset
|
|
||||||
```
|
|
||||||
|
|
||||||
### Comportement
|
|
||||||
|
|
||||||
- L'exposure est un **multiplicateur** appliqué à la texture HDR avant la courbe de tone mapping.
|
|
||||||
- `exposure = 2.0` → l'image est 2× plus claire (comme ouvrir le diaphragme d'un photo).
|
|
||||||
- `exposure = 0.5` → l'image est 2× plus sombre.
|
|
||||||
- Clampé à `[0.01, 10.0]` pour éviter les valeurs dégénérées.
|
|
||||||
- **N'a d'effet que si HDR est actif** (`with_hdr(...)`). En LDR, la valeur est ignorée.
|
|
||||||
|
|
||||||
### Clavier (demo)
|
|
||||||
|
|
||||||
| Touche | Effet |
|
|
||||||
|--------|-------|
|
|
||||||
| `+` | ×1.1 (plus clair) |
|
|
||||||
| `-` | ÷1.1 (plus sombre) |
|
|
||||||
| `0` | Reset à 1.0 |
|
|
||||||
|
|
||||||
## Emissive
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use wsg_lib::resources::Material;
|
|
||||||
|
|
||||||
// Créer un matériau avec émissivité
|
|
||||||
let mut mat = /* ... */;
|
|
||||||
mat.emissive = [1.0, 0.3, 0.1, 1.5]; // orange, intensité 1.5 (> 1.0 = glow HDR)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Format
|
|
||||||
|
|
||||||
`emissive = [r, g, b, intensity]` :
|
|
||||||
|
|
||||||
- **rgb** : la couleur de l'émission (même espace que la couleur base du vertex)
|
|
||||||
- **a (intensity)** : le multiplicateur. `1.0` = couleur normale, `> 1.0` = surbrillance (ne se voit qu'en HDR)
|
|
||||||
|
|
||||||
### Formule shader
|
|
||||||
|
|
||||||
```
|
|
||||||
final_color = lit + base_color * emissive.rgb * emissive.a
|
|
||||||
```
|
|
||||||
|
|
||||||
- L'émission est **additive** : visible même dans le noir total (pas de lumière nécessaire).
|
|
||||||
- Elle est **indépendante des ombres** : un objet émissif ne projette pas d'ombre et n'est pas ombragé.
|
|
||||||
- `emissive = [0,0,0,0]` (default) → aucun changement (non-régression garantie).
|
|
||||||
|
|
||||||
### Cas d'usage
|
|
||||||
|
|
||||||
| Usage | Valeur |
|
|
||||||
|-------|--------|
|
|
||||||
| LED / indicateur | `[0, 1, 0, 1.0]` (vert, intensité normale) |
|
|
||||||
| Flamme / soleil | `[1, 0.8, 0.2, 3.0]` (orange, glow HDR) |
|
|
||||||
| Neon | `[0, 0.5, 1, 2.5]` (cyan, glow) |
|
|
||||||
| Inactif | `[0, 0, 0, 0]` (default) |
|
|
||||||
|
|
||||||
### Clavier (demo)
|
|
||||||
|
|
||||||
| Touche | Effet |
|
|
||||||
|--------|-------|
|
|
||||||
| `E` | Toggle glow orange sur la sphère/cylindre |
|
|
||||||
|
|
||||||
## Interactions
|
|
||||||
|
|
||||||
| Combination | Résultat |
|
|
||||||
|-------------|----------|
|
|
||||||
| Emissive + HDR + ACES | Glow doux, highlights roll off (le plus joli) |
|
|
||||||
| Emissive + LDR | Clamped à 1.0 (pas de glow, mais couleur visible dans le noir) |
|
|
||||||
| Emissive + shadows | L'objet émissif n'est PAS ombragé (l'émission bypass le shadow term) |
|
|
||||||
| Exposure + Emissive | L'exposure amplifie aussi l'émission (cohérent : tout est dans la texture HDR) |
|
|
||||||
|
|
||||||
## Non-régression
|
|
||||||
|
|
||||||
- **Emissive** : `[0,0,0,0]` par défaut → le shader additionne `base * 0 * 0 = 0` → aucun changement.
|
|
||||||
- **Exposure** : `1.0` par défaut → `pow(color, 1/1) = color` → aucun changement.
|
|
||||||
- Les deux sont **opt-in** : sans `with_hdr(...)` ni `emissive != 0`, le pipeline est identique à l'état précédent.
|
|
||||||
|
|
||||||
## Limitations (MVP)
|
|
||||||
|
|
||||||
- L'emissive est **par matériau**, pas par vertex (pas de gradient d'émission dans un mesh).
|
|
||||||
- L'emissive est **statique** à la création du matériau (changer `mat.emissive` requiert de re-registrer le matériau via `add_material`).
|
|
||||||
- Pas de **bloom** (Étape 23) : le glow HDR est visible mais pas "flou" / diffusé.
|
|
||||||
+53
-51
@@ -1,70 +1,72 @@
|
|||||||
# Examples
|
# Examples
|
||||||
|
|
||||||
Sixteen examples live in [`lib/examples/`](../../lib/examples/), organized into
|
16 examples in **4 folders** (mirroring the topic folders of this documentation), covering the
|
||||||
**four category folders** — [`meshes/`](../../lib/examples/meshes/README.md),
|
full range of the engine — from a 2D quad to GPU-driven rendering.
|
||||||
[`lights/`](../../lib/examples/lights/README.md),
|
|
||||||
[`cameras/`](../../lib/examples/cameras/README.md),
|
|
||||||
[`effects/`](../../lib/examples/effects/README.md) — each folder with its own
|
|
||||||
`README.md` (per-example details: keys, what to observe). All launch with
|
|
||||||
`cargo run -p wsg-lib --example <name>` (names are stable, run from the repo
|
|
||||||
root). They are **self-contained**: no assets on disk (procedural textures,
|
|
||||||
hard-coded geometries).
|
|
||||||
|
|
||||||
| Folder | Example | What it shows | Corresponding page |
|
All examples are in [`lib/examples/`](../../lib/examples/README.md); each folder has its own
|
||||||
|--------|---------|---------------|--------------------|
|
README (description + how to run): [`meshes/`](../../lib/examples/meshes/README.md),
|
||||||
| `meshes/` | `simple` | The minimal declarative workflow: a two-tone 2D quad, **unlit**, rendered automatically. The "15 lines, no wgpu" model | [Quickstart](quickstart.md), [Materials](materials.md) (§ unlit) |
|
[`lights/`](../../lib/examples/lights/README.md), [`cameras/`](../../lib/examples/cameras/README.md),
|
||||||
| `meshes/` | `cube` | The 3D MVP: a textured (checkerboard) cube, lit (directional + point + spot), spinning | [Meshes](meshes.md), [Materials](materials.md), [Lights](lights.md) |
|
[`effects/`](../../lib/examples/effects/README.md).
|
||||||
| `meshes/` | `pbr` | PBR metallic/roughness + normal mapping (6 materials) | [Materials](materials.md) |
|
|
||||||
| `meshes/` | `import` | OBJ file import (non-graphical, prints stats to stdout) | [Meshes](mesh.md) |
|
|
||||||
| `meshes/` | `manual` | The **advanced** workflow: `Context`/`Renderer`/`PipelineCache` driven by hand, without the `App` facade | below |
|
|
||||||
| `lights/` | `shadow` | Shadow mapping in isolation (4 objects on a floor) | [Shadows](shadows.md) |
|
|
||||||
| `lights/` | `shadow_test` | Isolated shadow mapping: a cube casts a PCF-softened shadow on the ground (`clear_lights` technique → caster at index 0) | [Shadows](shadows.md) |
|
|
||||||
| `lights/` | `spot_test` | Isolated spot (ambient nearly zero): the directed beam, the penumbra, the attenuation | [Lights](lights.md) |
|
|
||||||
| `lights/` | `emissive` | Emissive materials (intensities 0 → 4.0) + runtime exposure | [Emissive & exposure](emissive-exposure.md) |
|
|
||||||
| `cameras/` | `culling` | GPU-driven frustum culling: 15×15 grid, off-frustum cubes skipped (zero CPU cost) | [GPU-driven](gpu-driven.md) |
|
|
||||||
| `effects/` | `demo` | The full showcase: ground + 6 LOD primitives, textures, 3 lights, **shadows**, **orbital camera** on keyboard/mouse, HDR/ACES, bloom | [All pages](README.md) |
|
|
||||||
| `effects/` | `bloom` | Post-process bloom (threshold → blur → composite) | [Bloom](bloom.md) |
|
|
||||||
| `effects/` | `hdr` | HDR + tone mapping (ACES) + runtime exposure control | [HDR](hdr.md) |
|
|
||||||
| `effects/` | `msaa` | MSAA 4× (smooth edges vs stair-stepped) | [MSAA](msaa.md) |
|
|
||||||
| `effects/` | `fog` | Distance fog (3 modes: linear, exp, exp²) | [Fog](fog.md) |
|
|
||||||
| `effects/` | `dof` | Depth of field (cinematic bokeh, focus presets) | [effects README](../../lib/examples/effects/README.md) |
|
|
||||||
|
|
||||||
## The `manual` workflow (advanced)
|
| Example | Folder | What it shows | How to run | Corresponding page |
|
||||||
|
|---------|--------|---------------|------------|--------------------|
|
||||||
|
| `simple` | meshes | A 2D quad with vertex colors, unlit mode (~30 lines) | `cargo run -p wsg-lib --example simple` | [Quickstart](quickstart.md), [Materials](meshes/materials.md) |
|
||||||
|
| `cube` | meshes | A rotating cube: point + spot light, checkerboard texture, procedural normal map, orbit/zoom | `cargo run -p wsg-lib --example cube` | [Meshes](meshes/meshes.md), [Materials](meshes/materials.md), [Lights](lights/lights.md) |
|
||||||
|
| `pbr` | meshes | A procedural PBR material (metal/roughness) + a checker diffuse | `cargo run -p wsg-lib --example pbr` | [Materials](meshes/materials.md) |
|
||||||
|
| `import` | meshes | Wavefront **OBJ** import (CLI: file path as argument, procedural cube as fallback) | `cargo run -p wsg-lib --example import --features import-obj -- model.obj` | [Geometry sources](meshes/sources.md) |
|
||||||
|
| `manual` | meshes | **Advanced**: the full manual workflow — buffers, pipelines, command encoding, no helpers | `cargo run -p wsg-lib --example manual` | [ARCHI_APP](../tech/ARCHI_APP.md), [FRAME_LOOP](../tech/FRAME_LOOP.md) |
|
||||||
|
| `shadow` | lights | Shadow mapping: the classic pitfall — the packed-index shadow caster | `cargo run -p wsg-lib --example shadow` | [Shadows](lights/shadows.md) |
|
||||||
|
| `shadow_test` | lights | Shadow mapping in isolation (cleared list → your light is index 0) | `cargo run -p wsg-lib --example shadow_test` | [Shadows](lights/shadows.md) |
|
||||||
|
| `spot_test` | lights | A single spotlight (cone + penumbra), ambient nearly zero | `cargo run -p wsg-lib --example spot_test` | [Lights](lights/lights.md) |
|
||||||
|
| `emissive` | lights | Emissive materials + HDR glow, runtime exposure (+/-/0 keys) | `cargo run -p wsg-lib --example emissive` | [Emissive & exposure](lights/emissive-exposure.md) |
|
||||||
|
| `culling` | cameras | **GPU-driven**: world matrices + indirect draws on the GPU, opt-in frustum culling, LOD | `cargo run -p wsg-lib --example culling` | [GPU-driven](cameras/gpu-driven.md) |
|
||||||
|
| `demo` | effects | The full showcase: all features combined (shadows, HDR, bloom, MSAA, fog, lights, orbital camera) | `cargo run -p wsg-lib --example demo` | [All pages](README.md) |
|
||||||
|
| `bloom` | effects | HDR + bloom: threshold → blur → composite | `cargo run -p wsg-lib --example bloom` | [Bloom](effects/bloom.md) |
|
||||||
|
| `hdr` | effects | HDR + tone mapping (ACES / Reinhard), emissive showcase | `cargo run -p wsg-lib --example hdr` | [HDR](effects/hdr.md) |
|
||||||
|
| `msaa` | effects | 4x MSAA anti-aliasing on the swapchain | `cargo run -p wsg-lib --example msaa` | [MSAA](effects/msaa.md) |
|
||||||
|
| `fog` | effects | Distance fog, 3 modes switchable at runtime (linear / exponential / exp²) | `cargo run -p wsg-lib --example fog` | [Fog](effects/fog.md) |
|
||||||
|
| `dof` | effects | Depth of field: Gaussian blur scaled by defocus distance, cinematic bokeh; focus presets 1-4 + continuous zoom | `cargo run -p wsg-lib --example dof` | [DoF](effects/dof.md) |
|
||||||
|
|
||||||
When the `App` facade doesn't fit (fine-grained loop control, integration into an existing
|
## The `manual` example: bypassing the helpers
|
||||||
framework, experimentation), you bypass `App` and drive directly:
|
|
||||||
|
|
||||||
- `Context` (*Manager* layer): GPU lifecycle — `Instance`/`Surface`/`Adapter`/`Device`/
|
[`manual.rs`](../../lib/examples/meshes/manual.rs) renders a rotating cube with **no
|
||||||
`Queue`, `configure()` for the swapchain, `get_next_frame()`.
|
high-level helper at all** — no `Scene`, no `Renderer` convenience API, no `AppHandler`
|
||||||
- `Renderer` (*Executor* layer): `render(view, mesh, material)` = one object per submission;
|
default `render()`. It shows the full pipeline:
|
||||||
`present(frame)`.
|
|
||||||
- `PipelineCache`: `register_shader(id, path)` then `Material::new(format, id, &mut cache)`.
|
|
||||||
|
|
||||||
The window and GPU are created in winit 0.30's `resumed()` callback (`run_app` +
|
1. **`setup`**: manual creation of vertex/index buffers, bind groups, render/compute
|
||||||
`ApplicationHandler`), as in `app.rs`. The reference file is
|
pipelines, the swapchain.
|
||||||
[`manual.rs`](../../lib/examples/meshes/manual.rs); the two-layer architecture is detailed in
|
2. **`render` (overridden)**: manual command encoding per frame (clear, draw, present) —
|
||||||
[ARCHI_APP](../tech/ARCHI_APP.md) and [FRAME_LOOP](../tech/FRAME_LOOP.md).
|
the handler controls **every** `CommandEncoder` operation.
|
||||||
|
3. **Uniforms written by hand** with `queue.write_buffer` (projection, view, model matrices).
|
||||||
|
|
||||||
> **Tip**: start with the declarative workflow. The manual workflow doesn't render more
|
This is the reference for what the high-level API does under the hood, and the starting
|
||||||
> pixels — it gives more control over command encoding.
|
point for features that don't exist yet in the engine (custom pipelines, post-processes,
|
||||||
|
custom compute). The technical details are in [ARCHI_APP](../tech/ARCHI_APP.md) and
|
||||||
|
[FRAME_LOOP](../tech/FRAME_LOOP.md).
|
||||||
|
|
||||||
|
Rule of thumb: **use `AppHandler` + `Scene` for everything the engine already does, and drop
|
||||||
|
to `manual` style only when you need what it doesn't** — the two styles can be mixed in the
|
||||||
|
same app (e.g. `Scene` for the scene, a manual post-process pass in `render()`).
|
||||||
|
|
||||||
## Adding your own example
|
## Adding your own example
|
||||||
|
|
||||||
Repo conventions (see [`lib/examples/README.md`](../../lib/examples/README.md)):
|
1. Create `lib/examples/<folder>/<name>.rs` — pick the folder it belongs to
|
||||||
|
(`meshes` / `lights` / `cameras` / `effects`).
|
||||||
|
2. Declare the `[[example]]` entry in `lib/Cargo.toml` (the folder structure means Cargo
|
||||||
|
no longer auto-discovers examples):
|
||||||
|
|
||||||
1. Create `lib/examples/<folder>/my_example.rs` (pick the matching category —
|
|
||||||
`meshes/`, `lights/`, `cameras/`, `effects/` — or add a new folder + README).
|
|
||||||
2. Declare it in `lib/Cargo.toml` — examples live in subfolders, so Cargo does
|
|
||||||
**not** discover them automatically:
|
|
||||||
```toml
|
```toml
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "my_example"
|
name = "<name>"
|
||||||
path = "examples/<folder>/my_example.rs"
|
path = "examples/<folder>/<name>.rs"
|
||||||
```
|
```
|
||||||
3. Keep it **self-contained**: procedural textures, hard-coded geometries, no external assets.
|
|
||||||
4. Document the example in the folder's `README.md` (and here, `docs/user/examples.md`).
|
3. Required features: the base crate has no primitives by default in examples — declare
|
||||||
|
`required-features` if your example uses them (e.g. `required-features = ["prim-cube"]`).
|
||||||
|
4. Register it in the folder's README and in the table above.
|
||||||
|
5. Verify: `cargo build --workspace --examples` + run it.
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [User README](README.md) · [Quickstart](quickstart.md) · [Camera & input](camera-input.md)
|
- [User README](README.md) · [Quickstart](quickstart.md)
|
||||||
- [Root README](../../README.md) · [ROADMAP](../ROADMAP.md)
|
- [Root README](../../README.md) · [ROADMAP](../ROADMAP.md)
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
# Brouillard de distance (Fog)
|
|
||||||
|
|
||||||
## Principe
|
|
||||||
|
|
||||||
Le brouillard de distance fond les objets vers une couleur prédéfinie en fonction
|
|
||||||
de leur distance à la caméra. C'est l'outil standard pour :
|
|
||||||
|
|
||||||
- **Masquer le bord du monde rendu** — illusion d'un monde infini (Skyrim, GTA, Minecraft)
|
|
||||||
- **Donner de la profondeur** — effet atmosphérique naturel
|
|
||||||
- **Camoufler les transitions** — chargement de tuiles, 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?;
|
|
||||||
```
|
|
||||||
|
|
||||||
Sans `.with_fog()`, le brouillard est désactivé — **zéro coût GPU** (la branche
|
|
||||||
shader est jamais prise).
|
|
||||||
|
|
||||||
## Modes
|
|
||||||
|
|
||||||
| Mode | Formule | Usage |
|
|
||||||
|------|---------|-------|
|
|
||||||
| `Linear` | `saturate((far - d) / (far - near))` | Cutoff net entre deux distances |
|
|
||||||
| `Exponential` | `exp(-density × d)` | Brouillard naturel (forêt, lac) |
|
|
||||||
| `Exponential2` | `exp(-density² × d²)` | Départ progressif, cutoff net — **idéal pour masquer** |
|
|
||||||
|
|
||||||
### Constructeurs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Linéaire : fondu entre near et far
|
|
||||||
FogConfig::linear([0.7, 0.8, 0.9], 5.0, 50.0)
|
|
||||||
|
|
||||||
// Exponentiel : fondu naturel
|
|
||||||
FogConfig::exponential([0.6, 0.7, 0.8], 0.03)
|
|
||||||
|
|
||||||
// Exponentiel² : masquage de bord de monde
|
|
||||||
FogConfig::exponential2([0.7, 0.75, 0.85], 0.08)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Paramètres
|
|
||||||
|
|
||||||
| Champ | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| `mode` | `FogMode` | Linéaire / Exponentiel / Exponential2 |
|
|
||||||
| `color` | `[f32; 3]` | Couleur du brouillard (RGB, espace linéaire) |
|
|
||||||
| `near` | `f32` | Distance début (mode linéaire uniquement) |
|
|
||||||
| `far` | `f32` | Distance fin, brouillard complet (mode linéaire) |
|
|
||||||
| `density` | `f32` | Densité (modes exp / exp²). Typique : 0.01–0.3 |
|
|
||||||
|
|
||||||
### Choisir la couleur
|
|
||||||
|
|
||||||
La couleur du brouillard **doit correspondre à la couleur du ciel/clear color**
|
|
||||||
pour un effet "monde infini" seamless. Avec HDR + ACES, utiliser des valeurs
|
|
||||||
linéaires cohérentes avec le tone mapping.
|
|
||||||
|
|
||||||
### Choisir la densité (exp²)
|
|
||||||
|
|
||||||
Pour masquer le bord du monde à une distance `D` :
|
|
||||||
|
|
||||||
```
|
|
||||||
density ≈ 2.0 / D
|
|
||||||
```
|
|
||||||
|
|
||||||
Exemples :
|
|
||||||
- Monde visible jusqu'à 25 unités → `density = 0.08`
|
|
||||||
- Monde visible jusqu'à 50 unités → `density = 0.04`
|
|
||||||
- Monde visible jusqu'à 100 unités → `density = 0.02`
|
|
||||||
|
|
||||||
## Changement à l'exécution
|
|
||||||
|
|
||||||
```rust
|
|
||||||
// Dans 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); // désactiver
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Le changement prend effet au frame suivant.
|
|
||||||
|
|
||||||
## Pipeline
|
|
||||||
|
|
||||||
```text
|
|
||||||
Main pass (shader fragment)
|
|
||||||
↓
|
|
||||||
Lighting → final_rgb
|
|
||||||
↓
|
|
||||||
FOG: mix(final_rgb, fog_color, 1 - fog_factor) ← ici
|
|
||||||
↓
|
|
||||||
→ HDR texture / swapchain
|
|
||||||
↓
|
|
||||||
(Bloom) → Tone Mapping → surface
|
|
||||||
```
|
|
||||||
|
|
||||||
Le brouillard s'applique **avant** le tone mapping : les valeurs HDR restent
|
|
||||||
non clampées, et le TM applique la courbe ACES/Reinhard au résultat déjà
|
|
||||||
brouillé. Résultat : le brouillard est perceptuellement cohérent.
|
|
||||||
|
|
||||||
## Compatibilité
|
|
||||||
|
|
||||||
| Avec | OK ? | Note |
|
|
||||||
|------|------|------|
|
|
||||||
| HDR + TM | ✅ | Fog avant TM (recommandé) |
|
|
||||||
| Bloom | ✅ | Le bloom extrait les zones brillantes du résultat post-fog |
|
|
||||||
| MSAA | ✅ | Indépendant (rasterizer vs fragment shader) |
|
|
||||||
| Culling GPU | ✅ | Indépendant (culling décide quoi dessiner, fog décide la couleur) |
|
|
||||||
| Shadows | ✅ | L'ombre est calculée avant le fog |
|
|
||||||
|
|
||||||
## Limitations (v1)
|
|
||||||
|
|
||||||
- **Scene-level uniquement** : un seul brouillard pour toute la scène.
|
|
||||||
Un brouillard par matériau nécessiterait un paramètre additionnel dans le
|
|
||||||
bind group par objet.
|
|
||||||
- **Distance euclidienne** : pas de brouillard volumétrique ni directionnel.
|
|
||||||
- **Couleur fixe** : pas de gradient de couleur avec la distance.
|
|
||||||
|
|
||||||
## Exemple
|
|
||||||
|
|
||||||
Voir `lib/examples/effects/fog.rs` : 15 cubes en rangée + 5 sphères sur un plan 80×80,
|
|
||||||
avec commutation runtime entre les 3 modes.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run -p wsg-lib --example fog --features "all-prims"
|
|
||||||
```
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
# 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
|
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Lights — user documentation
|
||||||
|
|
||||||
|
The **lighting side** of the scene: the light model, shadows, and emissive materials.
|
||||||
|
|
||||||
|
| Page | Topic |
|
||||||
|
|------|-------|
|
||||||
|
| [Lights](lights.md) | Scene-global lights (directional/point/spot/ambient), `MAX_LIGHTS`, packed indices |
|
||||||
|
| [Shadows](shadows.md) | Shadow mapping: picking the casting light, the packed-index pitfall, tuning |
|
||||||
|
| [Emissive + Exposure](emissive-exposure.md) | Emissive materials (HDR glow) and runtime exposure |
|
||||||
|
|
||||||
|
Example folder: [`lib/examples/lights/`](../../../lib/examples/lights/README.md)
|
||||||
|
(`shadow`, `shadow_test`, `spot_test`, `emissive`).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [User documentation index](../README.md) · [Quickstart](../quickstart.md) · [Examples](../examples.md)
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
# Emissive + Exposure
|
||||||
|
|
||||||
|
## Principle
|
||||||
|
|
||||||
|
Two complementary features (Step 22):
|
||||||
|
|
||||||
|
| Feature | Effect | Cost |
|
||||||
|
|---------|--------|------|
|
||||||
|
| **Exposure** (6.1) | Multiplies luminance before the tone-mapping curve | Zero when HDR is inactive |
|
||||||
|
| **Emissive** (6.2) | Adds an emitted color (independent of the lights) | Zero when `emissive = [0,0,0,0]` |
|
||||||
|
|
||||||
|
## Exposure
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Initialization (optional, default = 1.0)
|
||||||
|
let app = AppBuilder::new()
|
||||||
|
.with_hdr(ToneMapper::Aces)
|
||||||
|
.with_exposure(1.5) // start brighter
|
||||||
|
.build().await?;
|
||||||
|
|
||||||
|
// Runtime (in update())
|
||||||
|
app.set_exposure(app.exposure() * 1.1); // +1 "stop"
|
||||||
|
app.set_exposure(1.0); // reset
|
||||||
|
```
|
||||||
|
|
||||||
|
### Behavior
|
||||||
|
|
||||||
|
- Exposure is a **multiplier** applied to the HDR texture before the tone-mapping curve.
|
||||||
|
- `exposure = 2.0` → the image is 2× brighter (like opening a camera's aperture).
|
||||||
|
- `exposure = 0.5` → the image is 2× darker.
|
||||||
|
- Clamped to `[0.01, 10.0]` to avoid degenerate values.
|
||||||
|
- **Only has an effect when HDR is active** (`with_hdr(...)`). In LDR the value is ignored.
|
||||||
|
|
||||||
|
### Keyboard (demo)
|
||||||
|
|
||||||
|
| Key | Effect |
|
||||||
|
|-----|--------|
|
||||||
|
| `+` | ×1.1 (brighter) |
|
||||||
|
| `-` | ÷1.1 (darker) |
|
||||||
|
| `0` | Reset to 1.0 |
|
||||||
|
|
||||||
|
## Emissive
|
||||||
|
|
||||||
|
### API
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wsg_lib::resources::Material;
|
||||||
|
|
||||||
|
// Create a material with emissivity
|
||||||
|
let mut mat = /* ... */;
|
||||||
|
mat.emissive = [1.0, 0.3, 0.1, 1.5]; // orange, intensity 1.5 (> 1.0 = HDR glow)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format
|
||||||
|
|
||||||
|
`emissive = [r, g, b, intensity]`:
|
||||||
|
|
||||||
|
- **rgb**: the emission color (same space as the vertex base color)
|
||||||
|
- **a (intensity)**: the multiplier. `1.0` = normal color, `> 1.0` = highlight (only visible in HDR)
|
||||||
|
|
||||||
|
### Shader formula
|
||||||
|
|
||||||
|
```
|
||||||
|
final_color = lit + base_color * emissive.rgb * emissive.a
|
||||||
|
```
|
||||||
|
|
||||||
|
- The emission is **additive**: visible even in total darkness (no light needed).
|
||||||
|
- It is **independent of shadows**: an emissive object casts no shadow and is not shadowed.
|
||||||
|
- `emissive = [0,0,0,0]` (default) → no change (non-regression guaranteed).
|
||||||
|
|
||||||
|
### Use cases
|
||||||
|
|
||||||
|
| Use | Value |
|
||||||
|
|-----|-------|
|
||||||
|
| LED / indicator | `[0, 1, 0, 1.0]` (green, normal intensity) |
|
||||||
|
| Flame / sun | `[1, 0.8, 0.2, 3.0]` (orange, HDR glow) |
|
||||||
|
| Neon | `[0, 0.5, 1, 2.5]` (cyan, glow) |
|
||||||
|
| Inactive | `[0, 0, 0, 0]` (default) |
|
||||||
|
|
||||||
|
### Keyboard (demo)
|
||||||
|
|
||||||
|
| Key | Effect |
|
||||||
|
|-----|--------|
|
||||||
|
| `E` | Toggle orange glow on the sphere/cylinder |
|
||||||
|
|
||||||
|
## Interactions
|
||||||
|
|
||||||
|
| Combination | Result |
|
||||||
|
|-------------|--------|
|
||||||
|
| Emissive + HDR + ACES | Soft glow, highlights roll off (the nicest) |
|
||||||
|
| Emissive + LDR | Clamped to 1.0 (no glow, but the color is visible in the dark) |
|
||||||
|
| Emissive + shadows | The emissive object is NOT shadowed (emission bypasses the shadow term) |
|
||||||
|
| Exposure + Emissive | Exposure also amplifies the emission (consistent: everything is in the HDR texture) |
|
||||||
|
|
||||||
|
## Non-regression
|
||||||
|
|
||||||
|
- **Emissive**: `[0,0,0,0]` by default → the shader adds `base * 0 * 0 = 0` → no change.
|
||||||
|
- **Exposure**: `1.0` by default → `pow(color, 1/1) = color` → no change.
|
||||||
|
- Both are **opt-in**: without `with_hdr(...)` and `emissive != 0`, the pipeline is identical
|
||||||
|
to the previous state.
|
||||||
|
|
||||||
|
## Limitations (MVP)
|
||||||
|
|
||||||
|
- Emissive is **per material**, not per vertex (no emission gradient within a mesh).
|
||||||
|
- Emissive is **static** at material creation (changing `mat.emissive` requires re-registering
|
||||||
|
the material via `add_material`).
|
||||||
|
- No **bloom** (Step 23): the HDR glow is visible but not "blurred" / spread.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [User README](../README.md) · [HDR & tone mapping](../effects/hdr.md) · [Examples](../examples.md)
|
||||||
|
- [Root README](../../../README.md)
|
||||||
@@ -37,9 +37,9 @@ app.scene.add_spot_light(
|
|||||||
).unwrap();
|
).unwrap();
|
||||||
```
|
```
|
||||||
|
|
||||||
These three calls are the ones in the [`demo`](../../lib/examples/effects/demo.rs) example;
|
These three calls are the ones in the [`demo`](../../../lib/examples/effects/demo.rs) example;
|
||||||
[`cube.rs`](../../lib/examples/meshes/cube.rs) shows a point + a spot on top of the default
|
[`cube.rs`](../../../lib/examples/meshes/cube.rs) shows a point + a spot on top of the default
|
||||||
directional, and [`spot_test.rs`](../../lib/examples/lights/spot_test.rs) isolates a single spot
|
directional, and [`spot_test.rs`](../../../lib/examples/lights/spot_test.rs) isolates a single spot
|
||||||
(ambient nearly zero).
|
(ambient nearly zero).
|
||||||
|
|
||||||
Global settings:
|
Global settings:
|
||||||
@@ -68,7 +68,7 @@ Two consequences:
|
|||||||
[Shadows](shadows.md).
|
[Shadows](shadows.md).
|
||||||
2. If you want **your** light to be the only one (and thus at index 0), clear the list
|
2. If you want **your** light to be the only one (and thus at index 0), clear the list
|
||||||
first: `app.scene.clear_lights();` then `add_*_light(…)` (this is the technique in
|
first: `app.scene.clear_lights();` then `add_*_light(…)` (this is the technique in
|
||||||
[`shadow_test.rs`](../../lib/examples/lights/shadow_test.rs)).
|
[`shadow_test.rs`](../../../lib/examples/lights/shadow_test.rs)).
|
||||||
|
|
||||||
## Intensities and tints
|
## Intensities and tints
|
||||||
|
|
||||||
@@ -80,5 +80,5 @@ Two consequences:
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [User README](README.md) · [Shadows](shadows.md) · [Materials & textures](materials.md)
|
- [User README](../README.md) · [Shadows](shadows.md) · [Materials & textures](../meshes/materials.md)
|
||||||
- [Root README](../../README.md) · [ARCHI_RENDU](../tech/ARCHI_RENDU.md)
|
- [Root README](../../../README.md) · [ARCHI_RENDU](../../tech/ARCHI_RENDU.md)
|
||||||
@@ -29,7 +29,7 @@ Two ways to avoid it:
|
|||||||
app.scene.set_shadow_caster(Some(0)); // now it really is YOUR light
|
app.scene.set_shadow_caster(Some(0)); // now it really is YOUR light
|
||||||
```
|
```
|
||||||
|
|
||||||
This is the technique in [`shadow_test.rs`](../../lib/examples/lights/shadow_test.rs).
|
This is the technique in [`shadow_test.rs`](../../../lib/examples/lights/shadow_test.rs).
|
||||||
|
|
||||||
2. **Count the indices** — if you keep the default light and add yours, it lands at index 1:
|
2. **Count the indices** — if you keep the default light and add yours, it lands at index 1:
|
||||||
|
|
||||||
@@ -38,12 +38,12 @@ Two ways to avoid it:
|
|||||||
app.scene.set_shadow_caster(Some(1)); // this is the demo's warm light that casts
|
app.scene.set_shadow_caster(Some(1)); // this is the demo's warm light that casts
|
||||||
```
|
```
|
||||||
|
|
||||||
This is the technique in [`demo.rs`](../../lib/examples/effects/demo.rs).
|
This is the technique in [`demo.rs`](../../../lib/examples/effects/demo.rs).
|
||||||
|
|
||||||
## How it works (to understand the limits)
|
## How it works (to understand the limits)
|
||||||
|
|
||||||
Each frame, if a caster is active, the engine runs **two passes** (technical details in
|
Each frame, if a caster is active, the engine runs **two passes** (technical details in
|
||||||
[FRAME_LOOP](../tech/FRAME_LOOP.md)):
|
[FRAME_LOOP](../../tech/FRAME_LOOP.md)):
|
||||||
|
|
||||||
1. **Shadow pass**: the scene is rendered as seen *from the light* (depth-only
|
1. **Shadow pass**: the scene is rendered as seen *from the light* (depth-only
|
||||||
`shadow_shader.wgsl` shader) into a 1024² `Depth32Float` shadow map (size configurable
|
`shadow_shader.wgsl` shader) into a 1024² `Depth32Float` shadow map (size configurable
|
||||||
@@ -59,7 +59,7 @@ Things to know:
|
|||||||
- **Spot light**: the light's cone naturally bounds the shadow.
|
- **Spot light**: the light's cone naturally bounds the shadow.
|
||||||
- Only one light casts at a time (no multi-light shadows).
|
- Only one light casts at a time (no multi-light shadows).
|
||||||
- Shadows only affect meshes rendered by `standard` in lit mode — a renderer in unlit mode
|
- Shadows only affect meshes rendered by `standard` in lit mode — a renderer in unlit mode
|
||||||
(see [Materials & textures](materials.md)) receives none.
|
(see [Materials & textures](../meshes/materials.md)) receives none.
|
||||||
|
|
||||||
## Tuning shadow rendering
|
## Tuning shadow rendering
|
||||||
|
|
||||||
@@ -75,5 +75,5 @@ Tuning tips:
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [User README](README.md) · [Lights](lights.md) · [Examples](examples.md)
|
- [User README](../README.md) · [Lights](lights.md) · [Examples](../examples.md)
|
||||||
- [Root README](../../README.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md)
|
- [Root README](../../../README.md) · [FRAME_LOOP](../../tech/FRAME_LOOP.md)
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
# Module `mesh` — Sources de géométrie
|
|
||||||
|
|
||||||
Le module `wsg::mesh` est le point d'entrée unique pour **d'où vient la géométrie** :
|
|
||||||
générateurs procéduraux ou import de fichiers.
|
|
||||||
|
|
||||||
## Primitives procédurales
|
|
||||||
|
|
||||||
Chaque famille de primitives est derrière une **feature** — vous ne compilez que ce dont vous avez besoin.
|
|
||||||
|
|
||||||
| Feature | Fonction | Description |
|
|
||||||
|---------|----------|-------------|
|
|
||||||
| `prim-cube` | `cube(size)` | Cube centré, 24 sommets, normales par face |
|
|
||||||
| `prim-plane` | `plane(w, d, seg_x, seg_z)` | Plan horizontal XZ (normale +Y), subdivisé |
|
|
||||||
| `prim-sphere` | `uv_sphere(r, sectors, stacks)` | Sphère lat/long, normales lisses |
|
|
||||||
| `prim-sphere` | `icosphere(r, subdivisions)` | Icosphère (subdiv icosahedron) |
|
|
||||||
| `prim-cylinder` | `cylinder(r, h, sectors)` | Cylindre (côté + caps), normales analytiques |
|
|
||||||
| `prim-cone` | `cone(r, h, sectors)` | Cône (apex + base fermée) |
|
|
||||||
| `prim-torus` | `torus(major, minor, seg_maj, seg_min)` | Tore, normales lisses |
|
|
||||||
|
|
||||||
### Features par défaut
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# Cargo.toml de votre projet
|
|
||||||
[dependencies]
|
|
||||||
wsg-lib = { path = "../lib" }
|
|
||||||
# Default: toutes les primitives activées (all-prims)
|
|
||||||
```
|
|
||||||
|
|
||||||
```toml
|
|
||||||
# Ne compiler que le cube et la sphère :
|
|
||||||
wsg-lib = { path = "../lib", default-features = false, features = ["prim-cube", "prim-sphere"] }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use wsg_lib::prelude::*;
|
|
||||||
|
|
||||||
let cube = cube(2.0);
|
|
||||||
let sphere = uv_sphere(1.0, 32, 16);
|
|
||||||
let ico = icosphere(1.0, 2);
|
|
||||||
|
|
||||||
// Tous retournent un Geometry (positions + normals + UVs + indices)
|
|
||||||
assert_eq!(cube.positions.len(), 24);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Import de fichiers
|
|
||||||
|
|
||||||
| Feature | Fonction | Format |
|
|
||||||
|---------|----------|--------|
|
|
||||||
| `import-obj` | `load_obj(path)` / `parse_obj(str)` | Wavefront OBJ |
|
|
||||||
| `import-gltf` | `load_gltf(path)` | glTF 2.0 / GLB (stub) |
|
|
||||||
|
|
||||||
### Parser OBJ
|
|
||||||
|
|
||||||
Supporte : `v`, `vn`, `vt`, `f` (3-4 sommets, triangulation en éventail).
|
|
||||||
Si le fichier n'a pas de normales, elles sont **calculées** (pondération par aire).
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use wsg_lib::mesh::{load_obj, parse_obj};
|
|
||||||
|
|
||||||
// Depuis un fichier
|
|
||||||
let geom = load_obj("model.obj")?;
|
|
||||||
|
|
||||||
// Depuis une string
|
|
||||||
let geom = parse_obj("v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n")?;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Erreurs
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use wsg_lib::mesh::import::MeshImportError;
|
|
||||||
|
|
||||||
match load_obj("missing.obj") {
|
|
||||||
Ok(geom) => { /* … */ }
|
|
||||||
Err(MeshImportError::Io(e)) => eprintln!("fichier inaccessible: {e}"),
|
|
||||||
Err(MeshImportError::Parse(e)) => eprintln!("syntaxe invalide: {e}"),
|
|
||||||
Err(MeshImportError::Unsupported(e)) => eprintln!("feature non supportée: {e}"),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## De `Geometry` à la scène
|
|
||||||
|
|
||||||
Le module `mesh` produit des `Geometry` (données CPU). Pour les rendre,
|
|
||||||
passez par `Scene::create_mesh` qui les transfère en GPU :
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use wsg_lib::prelude::*;
|
|
||||||
use wsg_lib::mesh::cube;
|
|
||||||
|
|
||||||
// Dans AppHandler::setup :
|
|
||||||
let geom = cube(1.0);
|
|
||||||
app.scene.create_mesh("my_mesh", geom, Some("my_mat"))?;
|
|
||||||
app.scene.add_entity("my_entity", "my_mesh")?;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo run -p wsg-lib --example import --features import-obj -- model.obj
|
|
||||||
```
|
|
||||||
|
|
||||||
## Convention
|
|
||||||
|
|
||||||
- **Y-up**, origine centrée (sauf `plane` : plan XZ à y=0)
|
|
||||||
- Normales **sortantes**
|
|
||||||
- UVs dans [0,1]²
|
|
||||||
- Winding **CCW** (face avant)
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Meshes — user documentation
|
||||||
|
|
||||||
|
The **geometry side** of the scene: where the geometry comes from, how meshes and entities are
|
||||||
|
organized, and how objects look.
|
||||||
|
|
||||||
|
| Page | Topic |
|
||||||
|
|------|-------|
|
||||||
|
| [Meshes](meshes.md) | The three levels `Geometry` → `Mesh` → `Entity`; procedural primitives, custom geometry, `Transform`, mesh sharing |
|
||||||
|
| [Geometry sources](sources.md) | The `wsg::mesh` module: feature-gated procedural generators + file import (OBJ, glTF stub) |
|
||||||
|
| [Materials & textures](materials.md) | The `standard` shader, unlit mode, diffuse textures |
|
||||||
|
|
||||||
|
Example folder: [`lib/examples/meshes/`](../../../lib/examples/meshes/README.md)
|
||||||
|
(`simple`, `cube`, `pbr`, `import`, `manual`).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [User documentation index](../README.md) · [Quickstart](../quickstart.md) · [Examples](../examples.md)
|
||||||
@@ -5,7 +5,7 @@ optionally a **diffuse texture**. Several materials pointing at the same shader
|
|||||||
same compiled GPU pipeline (the `PipelineCache` held by the scene).
|
same compiled GPU pipeline (the `PipelineCache` held by the scene).
|
||||||
|
|
||||||
The engine ships a single shader: **`standard`** — multi-light Phong lighting (see
|
The engine ships a single shader: **`standard`** — multi-light Phong lighting (see
|
||||||
[Lights](lights.md)), with an **unlit** mode for flat rendering.
|
[Lights](../lights/lights.md)), with an **unlit** mode for flat rendering.
|
||||||
|
|
||||||
## 1. Registering the shader
|
## 1. Registering the shader
|
||||||
|
|
||||||
@@ -22,8 +22,8 @@ app.scene
|
|||||||
|
|
||||||
For a custom shader: register your `.wgsl` file path under an id of your choice (it must
|
For a custom shader: register your `.wgsl` file path under an id of your choice (it must
|
||||||
expose the same bind groups as `standard` — frame @0, object @1, texture @2, shadow @3 — see
|
expose the same bind groups as `standard` — frame @0, object @1, texture @2, shadow @3 — see
|
||||||
[ARCHI_RENDU](../tech/ARCHI_RENDU.md) and the
|
[ARCHI_RENDU](../../tech/ARCHI_RENDU.md) and the
|
||||||
[`shaders/standard_shader.wgsl`](../../lib/src/shaders/standard_shader.wgsl) file).
|
[`shaders/standard_shader.wgsl`](../../../lib/src/shaders/standard_shader.wgsl) file).
|
||||||
|
|
||||||
## 2. Creating materials
|
## 2. Creating materials
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ app.scene.create_mesh("cube_mesh", cube(1.0), Some("mat_textured")).unwrap();
|
|||||||
|
|
||||||
A mesh created with `material = None` is rendered with the scene's **default material**
|
A mesh created with `material = None` is rendered with the scene's **default material**
|
||||||
(`standard`, built once then cached) — that is the behavior of the
|
(`standard`, built once then cached) — that is the behavior of the
|
||||||
[`simple`](../../lib/examples/meshes/simple.rs) example.
|
[`simple`](../../../lib/examples/meshes/simple.rs) example.
|
||||||
|
|
||||||
## 3. Diffuse textures
|
## 3. Diffuse textures
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ app.scene.add_material_texture("ground_mat", "standard", "checker_texture").unwr
|
|||||||
```
|
```
|
||||||
|
|
||||||
The exact snippet (8×8 checkerboard + stripes generation) is in
|
The exact snippet (8×8 checkerboard + stripes generation) is in
|
||||||
[`demo.rs`](../../lib/examples/effects/demo.rs) and [`cube.rs`](../../lib/examples/meshes/cube.rs).
|
[`demo.rs`](../../../lib/examples/effects/demo.rs) and [`cube.rs`](../../../lib/examples/meshes/cube.rs).
|
||||||
|
|
||||||
Two conditions for a texture to show up:
|
Two conditions for a texture to show up:
|
||||||
1. the material is created via `add_material_texture` (otherwise the 1×1 white placeholder
|
1. the material is created via `add_material_texture` (otherwise the 1×1 white placeholder
|
||||||
@@ -90,11 +90,11 @@ This is the mode of the `simple` example (2D quad). In this mode the scene's lig
|
|||||||
ignored; per-vertex colors (or white) are rendered directly. 2D is a special case of 3D:
|
ignored; per-vertex colors (or white) are rendered directly. 2D is a special case of 3D:
|
||||||
the single `standard` pipeline serves both.
|
the single `standard` pipeline serves both.
|
||||||
|
|
||||||
> `clear_lights()` (see [Lights](lights.md)) gives a similar result but keeps the lit
|
> `clear_lights()` (see [Lights](../lights/lights.md)) gives a similar result but keeps the lit
|
||||||
> pipeline: only ambient stays active. Use it when you want to "turn off the lights" without
|
> pipeline: only ambient stays active. Use it when you want to "turn off the lights" without
|
||||||
> switching to unlit.
|
> switching to unlit.
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [User README](README.md) · [Meshes](meshes.md) · [Lights](lights.md) · [Examples](examples.md)
|
- [User README](../README.md) · [Meshes](meshes.md) · [Lights](../lights/lights.md) · [Examples](../examples.md)
|
||||||
- [Root README](../../README.md) · [ARCHI_RENDU](../tech/ARCHI_RENDU.md)
|
- [Root README](../../../README.md) · [ARCHI_RENDU](../../tech/ARCHI_RENDU.md)
|
||||||
@@ -79,14 +79,14 @@ app.scene.add_entity_with_transform("cube", "cube_mesh", transform)?;
|
|||||||
```
|
```
|
||||||
|
|
||||||
All these methods return `Result<_, String>` (unifying the typed errors is on the
|
All these methods return `Result<_, String>` (unifying the typed errors is on the
|
||||||
horizon — see [ROADMAP](../ROADMAP.md)).
|
horizon — see [ROADMAP](../../ROADMAP.md)).
|
||||||
|
|
||||||
## 4. Moving / animating: the `Transform`
|
## 4. Moving / animating: the `Transform`
|
||||||
|
|
||||||
Placement lives on the **entity** (not on the mesh): `Transform { translation: Vec3,
|
Placement lives on the **entity** (not on the mesh): `Transform { translation: Vec3,
|
||||||
rotation: Quat, scale: Vec3 }`, converted to a world matrix by the engine every frame.
|
rotation: Quat, scale: Vec3 }`, converted to a world matrix by the engine every frame.
|
||||||
|
|
||||||
The snippet below is the animation from the [`cube`](../../lib/examples/meshes/cube.rs) example:
|
The snippet below is the animation from the [`cube`](../../../lib/examples/meshes/cube.rs) example:
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
fn update(&mut self, app: &mut wsg_lib::App) {
|
fn update(&mut self, app: &mut wsg_lib::App) {
|
||||||
@@ -121,5 +121,5 @@ The GPU buffers are uploaded only once; only the world matrices differ.
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [User README](README.md) · [Quickstart](quickstart.md) · [Materials & textures](materials.md) · [Lights](lights.md)
|
- [User README](../README.md) · [Quickstart](../quickstart.md) · [Materials & textures](materials.md) · [Lights](../lights/lights.md)
|
||||||
- [Root README](../../README.md) · [ARCHI_APP](../tech/ARCHI_APP.md)
|
- [Root README](../../../README.md) · [ARCHI_APP](../../tech/ARCHI_APP.md)
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Geometry sources: procedural generators and file import
|
||||||
|
|
||||||
|
The `wsg::mesh` module is the single entry point for **where the geometry comes from**:
|
||||||
|
procedural generators or file import.
|
||||||
|
|
||||||
|
## Procedural primitives
|
||||||
|
|
||||||
|
Each primitive family is behind a **feature** — you only compile what you need.
|
||||||
|
|
||||||
|
| Feature | Function | Description |
|
||||||
|
|---------|----------|-------------|
|
||||||
|
| `prim-cube` | `cube(size)` | Centered cube, 24 vertices, per-face normals |
|
||||||
|
| `prim-plane` | `plane(w, d, seg_x, seg_z)` | Horizontal XZ plane (normal +Y), subdivided |
|
||||||
|
| `prim-sphere` | `uv_sphere(r, sectors, stacks)` | Lat/long sphere, smooth normals |
|
||||||
|
| `prim-sphere` | `icosphere(r, subdivisions)` | Icosphere (subdivided icosahedron) |
|
||||||
|
| `prim-cylinder` | `cylinder(r, h, sectors)` | Cylinder (side + caps), analytic normals |
|
||||||
|
| `prim-cone` | `cone(r, h, sectors)` | Cone (apex + closed base) |
|
||||||
|
| `prim-torus` | `torus(major, minor, seg_maj, seg_min)` | Torus, smooth normals |
|
||||||
|
|
||||||
|
### Default features
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Your project's Cargo.toml
|
||||||
|
[dependencies]
|
||||||
|
wsg-lib = { path = "../lib" }
|
||||||
|
# Default: all primitives enabled (all-prims)
|
||||||
|
```
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Only compile the cube and the sphere:
|
||||||
|
wsg-lib = { path = "../lib", default-features = false, features = ["prim-cube", "prim-sphere"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wsg_lib::prelude::*;
|
||||||
|
|
||||||
|
let cube = cube(2.0);
|
||||||
|
let sphere = uv_sphere(1.0, 32, 16);
|
||||||
|
let ico = icosphere(1.0, 2);
|
||||||
|
|
||||||
|
// All return a Geometry (positions + normals + UVs + indices)
|
||||||
|
assert_eq!(cube.positions.len(), 24);
|
||||||
|
```
|
||||||
|
|
||||||
|
## File import
|
||||||
|
|
||||||
|
| Feature | Function | Format |
|
||||||
|
|---------|----------|--------|
|
||||||
|
| `import-obj` | `load_obj(path)` / `parse_obj(str)` | Wavefront OBJ |
|
||||||
|
| `import-gltf` | `load_gltf(path)` | glTF 2.0 / GLB (stub) |
|
||||||
|
|
||||||
|
### OBJ parser
|
||||||
|
|
||||||
|
Supports: `v`, `vn`, `vt`, `f` (3–4 vertices, fan triangulation).
|
||||||
|
If the file has no normals, they are **computed** (area-weighted).
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wsg_lib::mesh::{load_obj, parse_obj};
|
||||||
|
|
||||||
|
// From a file
|
||||||
|
let geom = load_obj("model.obj")?;
|
||||||
|
|
||||||
|
// From a string
|
||||||
|
let geom = parse_obj("v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 3\n")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Errors
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wsg_lib::mesh::import::MeshImportError;
|
||||||
|
|
||||||
|
match load_obj("missing.obj") {
|
||||||
|
Ok(geom) => { /* … */ }
|
||||||
|
Err(MeshImportError::Io(e)) => eprintln!("file not accessible: {e}"),
|
||||||
|
Err(MeshImportError::Parse(e)) => eprintln!("invalid syntax: {e}"),
|
||||||
|
Err(MeshImportError::Unsupported(e)) => eprintln!("unsupported feature: {e}"),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## From `Geometry` to the scene
|
||||||
|
|
||||||
|
The `mesh` module produces `Geometry` (CPU data). To render it, go through
|
||||||
|
`Scene::create_mesh`, which uploads it to the GPU:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use wsg_lib::prelude::*;
|
||||||
|
use wsg_lib::mesh::cube;
|
||||||
|
|
||||||
|
// In AppHandler::setup:
|
||||||
|
let geom = cube(1.0);
|
||||||
|
app.scene.create_mesh("my_mesh", geom, Some("my_mat"))?;
|
||||||
|
app.scene.add_entity("my_entity", "my_mesh")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p wsg-lib --example import --features import-obj -- model.obj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Y-up**, centered on the origin (except `plane`: XZ plane at y=0)
|
||||||
|
- **Outward** normals
|
||||||
|
- UVs in [0,1]²
|
||||||
|
- **CCW** winding (front face)
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [User README](../README.md) · [Meshes](meshes.md) · [Materials & textures](materials.md) · [Examples](../examples.md)
|
||||||
|
- [Root README](../../../README.md)
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
# 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 `lib/examples/effects/msaa.rs` for a full interactive demo with cube, sphere,
|
|
||||||
and ground plane where aliasing is clearly visible without MSAA.
|
|
||||||
+96
-99
@@ -1,132 +1,129 @@
|
|||||||
# Quickstart
|
# Quickstart
|
||||||
|
|
||||||
Goal: a window showing an object, with the render loop handled by the library. You will only
|
Get a window with a rotating cube on screen in ~30 lines. The full version with comments is
|
||||||
write three things: a struct implementing `AppHandler`, your scene declaration in `setup()`,
|
in the [`simple`](../../lib/examples/meshes/simple.rs) example (2D quad, unlit) and
|
||||||
and your `main()`.
|
[`cube`](../../lib/examples/meshes/cube.rs) (3D cube, lit).
|
||||||
|
|
||||||
## Prerequisites
|
## 1. Add the dependency
|
||||||
|
|
||||||
- A recent Rust toolchain (the library is **edition 2024** — run `rustup update` if needed).
|
|
||||||
- A windowing environment (X11/Wayland on Linux, or native macOS/Windows).
|
|
||||||
- WSG is **not published on crates.io**: it is consumed by file path.
|
|
||||||
|
|
||||||
## 1. Dependencies
|
|
||||||
|
|
||||||
In your application's `Cargo.toml`:
|
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
|
# Cargo.toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
wsg-lib = { path = "/path/to/wsg/lib" }
|
wsg-lib = { path = "../lib" }
|
||||||
pollster = { version = "1", features = ["macro"] } # for #[pollster::main] (AppBuilder is async)
|
glam = "0.29" # Vec3/Quat — re-exported but you need it in your own code
|
||||||
|
winit = "0.30" # KeyCode/MouseButton for the input (only if you use app.input)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2. The minimal application
|
> The workspace pins `glam 0.29` and `winit 0.30`; match these versions to avoid
|
||||||
|
> type mismatches.
|
||||||
|
|
||||||
This snippet is the [`simple`](../../lib/examples/meshes/simple.rs) example from the repo, almost
|
## 2. Implement `AppHandler`
|
||||||
verbatim: a flat two-tone quad, rendered automatically every frame.
|
|
||||||
|
Three mandatory methods (`setup`, `update`, `render`) and an optional event hook.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use wsg_lib::app::AppBuilder;
|
use glam::{Quat, Vec3};
|
||||||
use wsg_lib::resources::Geometry;
|
use winit::event::MouseButton;
|
||||||
use wsg_lib::utils::WsgError;
|
use winit::keyboard::KeyCode;
|
||||||
use wsg_lib::AppHandler;
|
use wsg_lib::camera::CameraController;
|
||||||
|
use wsg_lib::prelude::*;
|
||||||
|
|
||||||
struct MyQuad;
|
struct MyHandler {
|
||||||
|
camera: CameraController,
|
||||||
|
}
|
||||||
|
|
||||||
impl AppHandler for MyQuad {
|
impl AppHandler for MyHandler {
|
||||||
fn setup(&mut self, app: &mut wsg_lib::App) {
|
fn new() -> Self {
|
||||||
// Flat 2D: the `standard` shader in unlit mode returns the vertex color as-is.
|
Self { camera: CameraController::default() }
|
||||||
app.renderer_mut().set_unlit(true);
|
}
|
||||||
|
|
||||||
|
fn setup(&mut self, app: &mut App) -> Result<(), String> {
|
||||||
|
// A cube (primitive) + the standard material.
|
||||||
|
app.scene.create_mesh("cube_mesh", cube(1.0), Some("cube_mat"))?;
|
||||||
|
app.scene.add_entity("cube", "cube_mesh")?;
|
||||||
|
|
||||||
|
// A warm directional light + shadows on it.
|
||||||
app.scene
|
app.scene
|
||||||
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
|
.add_directional_light(Vec3::new(1.0, 1.2, 1.0).normalize(), [1.0, 0.98, 0.92], 1.5)?;
|
||||||
.unwrap();
|
app.scene.set_shadow_caster(Some(0));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
let geometry = Geometry::new(vec![
|
fn update(&mut self, app: &mut App) {
|
||||||
[-0.5, 0.5, 0.0],
|
// Spin the cube.
|
||||||
[ 0.5, 0.5, 0.0],
|
let mut tf = *app.scene.entity_transform("cube").unwrap();
|
||||||
[ 0.5, -0.5, 0.0],
|
tf.rotation = Quat::from_rotation_y(self.t) * tf.rotation;
|
||||||
[-0.5, -0.5, 0.0],
|
app.scene.set_entity_transform("cube", tf);
|
||||||
])
|
self.t += 0.02;
|
||||||
.with_normals(vec![[0.0, 0.0, 1.0]; 4])
|
|
||||||
.with_colors(vec![
|
|
||||||
[1.0, 0.0, 0.0, 1.0], // red
|
|
||||||
[0.0, 1.0, 0.0, 1.0], // green
|
|
||||||
[0.0, 0.0, 1.0, 1.0], // blue
|
|
||||||
[1.0, 1.0, 0.0, 1.0], // yellow
|
|
||||||
])
|
|
||||||
.with_indices(vec![0, 1, 2, 0, 2, 3]);
|
|
||||||
|
|
||||||
// `None`: the scene injects its default material (`standard`) at render time.
|
// Camera: orbit (left-drag), zoom (wheel), reset (R), presets (1/2/3).
|
||||||
app.scene.create_mesh("quad_mesh", geometry, None).unwrap();
|
let (dx, dy) = app.input.mouse_delta();
|
||||||
app.scene.add_entity("quad", "quad_mesh").unwrap();
|
if app.input.mouse_button_held(MouseButton::Left) {
|
||||||
|
self.camera.orbit(dx, dy);
|
||||||
|
}
|
||||||
|
let (_, sy) = app.input.scroll_delta();
|
||||||
|
self.camera.zoom(sy);
|
||||||
|
if app.input.key_pressed(KeyCode::KeyR) {
|
||||||
|
self.camera.yaw = 0.6;
|
||||||
|
self.camera.pitch = 0.35;
|
||||||
|
self.camera.distance = 6.5;
|
||||||
|
}
|
||||||
|
self.camera.apply_to(app.scene.camera_mut());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&mut self, app: &mut App) -> Result<(), String> {
|
||||||
|
// Default implementation: renders the whole scene. Override only for custom passes.
|
||||||
|
app.renderer().render_scene(app.context())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#[pollster::main]
|
## 3. Build the app
|
||||||
async fn main() -> Result<(), WsgError> {
|
|
||||||
let app = AppBuilder::new().title("WSG Simple").build().await?;
|
```rust
|
||||||
app.run(MyQuad)
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let app = AppBuilder::new()
|
||||||
|
.title("My WSG app")
|
||||||
|
.size(1024, 768)
|
||||||
|
.with_culling(true) // optional: skip off-screen entities
|
||||||
|
.with_shadows() // optional: enable shadow mapping
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut handler = MyHandler::new();
|
||||||
|
app.run(&mut handler).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: **no `wgpu` or `winit` imports** — the `App` facade encapsulates them entirely.
|
`AppBuilder` methods you will use early:
|
||||||
|
|
||||||
## 3. What the library does for you
|
| Method | Purpose |
|
||||||
|
|--------|---------|
|
||||||
|
| `.title(…)` / `.size(w, h)` | Window |
|
||||||
|
| `.with_vsync(false)` / `.with_frame_limit(n)` | Frame pacing (vsync off + 144 fps cap in the `demo`) |
|
||||||
|
| `.with_culling(true)` | Opt-in frustum culling (see [GPU-driven](cameras/gpu-driven.md)) |
|
||||||
|
| `.with_shadows()` | Opt-in shadow mapping (see [Shadows](lights/shadows.md)) |
|
||||||
|
| `.with_hdr(ToneMapper::Aces)` | Opt-in HDR + tone mapping (see [HDR](effects/hdr.md)) |
|
||||||
|
|
||||||
The full lifecycle, as driven by `App::run` (technical details in
|
## 4. Run it
|
||||||
[FRAME_LOOP](../tech/FRAME_LOOP.md)):
|
|
||||||
|
|
||||||
```
|
```sh
|
||||||
AppBuilder::build() creates the event loop
|
cargo run -p wsg-lib --example cube # the reference "hello world" of the engine
|
||||||
│
|
|
||||||
App::run(handler) starts the loop
|
|
||||||
│
|
|
||||||
resumed (winit) window + GPU (Instance/Surface/Adapter/Device/Queue) + Renderer
|
|
||||||
│
|
|
||||||
handler.setup(&mut app) ← you declare the scene here (once, GPU ready)
|
|
||||||
│
|
|
||||||
▼ per frame, in a loop:
|
|
||||||
input.begin_frame() current frame's keyboard/mouse state
|
|
||||||
handler.update(&mut app) ← your logic (motion, input, …)
|
|
||||||
input.end_frame()
|
|
||||||
handler.render(app, frame) ← default: app.render_scene(frame.view())
|
|
||||||
│ (the whole scene is drawn automatically, one pass per frame)
|
|
||||||
└─ present → next frame
|
|
||||||
```
|
```
|
||||||
|
|
||||||
So you implement:
|
Controls (in the `cube`/`demo` examples): **left-drag** orbit, **wheel** zoom, **R** reset
|
||||||
|
camera, **1/2/3** view presets, **H** help overlay, **Esc** quit.
|
||||||
| Hook | When | Role | Default |
|
|
||||||
|------|-------|------|---------|
|
|
||||||
| `setup(&mut self, app)` | once, GPU ready | declare shaders, materials, textures, meshes, entities, lights, camera | empty |
|
|
||||||
| `update(&mut self, app)` | every frame, before render | animate: transforms, input, lights… | empty |
|
|
||||||
| `render(&mut self, app, frame)` | every frame, after update | **default**: draws the whole scene; override for custom rendering | `app.render_scene(frame.view())` |
|
|
||||||
|
|
||||||
Golden rule: **mutate the scene in `update()`** (and `setup()`), only read it in `render()`
|
|
||||||
(model detailed in [ARCHI_RENDU](../tech/ARCHI_RENDU.md)).
|
|
||||||
|
|
||||||
## 4. Running it
|
|
||||||
|
|
||||||
From the WSG repo root (the examples live in `lib/examples/`, one folder per
|
|
||||||
category: `meshes/`, `lights/`, `cameras/`, `effects/`):
|
|
||||||
|
|
||||||
| Command | What you see |
|
|
||||||
|----------|--------------|
|
|
||||||
| `cargo run -p wsg-lib --example simple` | the quad above (flat 2D, unlit) |
|
|
||||||
| `cargo run -p wsg-lib --example cube` | a textured, lit, spinning cube (3D) |
|
|
||||||
| `cargo run -p wsg-lib --example demo` | the full showcase: 6 primitives + lights + shadows + orbital camera |
|
|
||||||
|
|
||||||
For your own application: create a crate, add the §1 dependency, paste the §2 code into
|
|
||||||
`src/main.rs`, and `cargo run`.
|
|
||||||
|
|
||||||
## 5. Where to go next
|
## 5. Where to go next
|
||||||
|
|
||||||
- Want a 3D object? → [Meshes](meshes.md)
|
- [Meshes](meshes/meshes.md) — entities, transforms, custom geometries
|
||||||
- Want to change the look / add a texture? → [Materials & textures](materials.md)
|
- [Materials & textures](meshes/materials.md) — diffuse textures, unlit mode
|
||||||
- Want lights? → [Lights](lights.md)
|
- [Lights](lights/lights.md) — point/spot lights, ambient
|
||||||
- Want to see everything at once? → the `demo` example ([Examples](examples.md))
|
- [Camera & input](cameras/camera-input.md) — the full input API
|
||||||
|
- [GPU-driven](cameras/gpu-driven.md) — culling, LOD, debugging the GPU path
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [User README](README.md) · [Meshes](meshes.md) · [Examples](examples.md)
|
- [User README](README.md) · [Meshes](meshes/meshes.md) · [Examples](examples.md)
|
||||||
- [Root README](../../README.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md) · [ARCHI_APP](../tech/ARCHI_APP.md)
|
- [Root README](../../README.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md)
|
||||||
|
|||||||
@@ -48,5 +48,5 @@ cargo run -p wsg-lib --example culling
|
|||||||
> even off-screen).
|
> even off-screen).
|
||||||
>
|
>
|
||||||
> The GPU-driven pipeline (compute matrices → culling → indirect draws) is
|
> The GPU-driven pipeline (compute matrices → culling → indirect draws) is
|
||||||
> documented in [`docs/tech/ARCHI_CPU_GPU.md`](../../docs/tech/ARCHI_CPU_GPU.md)
|
> documented in [`docs/tech/ARCHI_CPU_GPU.md`](../../../docs/tech/ARCHI_CPU_GPU.md)
|
||||||
> and [`docs/user/gpu-driven.md`](../../docs/user/gpu-driven.md).
|
> and [`docs/user/cameras/gpu-driven.md`](../../../docs/user/cameras/gpu-driven.md).
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ impl AppHandler for Demo {
|
|||||||
// entity's angular offset from the view axis is bounded by atan(1.7/6.1) ≈ 15.5°, under
|
// entity's angular offset from the view axis is bounded by atan(1.7/6.1) ≈ 15.5°, under
|
||||||
// the ~22° vertical half-FOV (verified 2026-09-22: 600 frames swept, GPU==CPU on all
|
// the ~22° vertical half-FOV (verified 2026-09-22: 600 frames swept, GPU==CPU on all
|
||||||
// 6000 cull verdicts, zero flips on the ring). Counts only flip to 0 for entities far
|
// 6000 cull verdicts, zero flips on the ring). Counts only flip to 0 for entities far
|
||||||
// off-axis (e.g. behind the near plane) — see docs/user/gpu-driven.md.
|
// off-axis (e.g. behind the near plane) — see docs/user/cameras/gpu-driven.md.
|
||||||
// Unset → 0 (the showcase stays silent); set but non-numeric (e.g. `WSG_DEBUG_DUMP=on`) → 3.
|
// Unset → 0 (the showcase stays silent); set but non-numeric (e.g. `WSG_DEBUG_DUMP=on`) → 3.
|
||||||
let frames = match std::env::var("WSG_DEBUG_DUMP") {
|
let frames = match std::env::var("WSG_DEBUG_DUMP") {
|
||||||
Ok(v) => v.parse::<u32>().ok().filter(|&n| n > 0).unwrap_or(3),
|
Ok(v) => v.parse::<u32>().ok().filter(|&n| n > 0).unwrap_or(3),
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ Useful for understanding what the `App` facade encapsulates:
|
|||||||
|
|
||||||
The window and GPU are created in winit 0.30's `resumed()` callback
|
The window and GPU are created in winit 0.30's `resumed()` callback
|
||||||
(`run_app` + `ApplicationHandler`). The two-layer architecture is detailed in
|
(`run_app` + `ApplicationHandler`). The two-layer architecture is detailed in
|
||||||
[`docs/tech/ARCHI_APP.md`](../../docs/tech/ARCHI_APP.md) and
|
[`docs/tech/ARCHI_APP.md`](../../../docs/tech/ARCHI_APP.md) and
|
||||||
[`FRAME_LOOP.md`](../../docs/tech/FRAME_LOOP.md).
|
[`FRAME_LOOP.md`](../../../docs/tech/FRAME_LOOP.md).
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo run -p wsg-lib --example manual
|
cargo run -p wsg-lib --example manual
|
||||||
|
|||||||
Reference in New Issue
Block a user