From d4c2d93fc5c2493b9be06e3d3420f65395dd22b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Bousqui=C3=A9?= Date: Fri, 25 Sep 2026 20:06:10 +0200 Subject: [PATCH] eng doc --- README_DETAILS.md | 2 +- docs/tech/ARCHI_CPU_GPU.md | 4 +- docs/user/README.md | 91 +++++------ docs/user/bloom.md | 93 ----------- docs/user/cameras/README.md | 16 ++ docs/user/{ => cameras}/camera-input.md | 8 +- docs/user/{ => cameras}/gpu-driven.md | 2 +- docs/user/effects/README.md | 19 +++ docs/user/effects/bloom.md | 99 ++++++++++++ docs/user/effects/dof.md | 64 ++++++++ docs/user/effects/fog.md | 135 ++++++++++++++++ docs/user/effects/hdr.md | 88 +++++++++++ docs/user/effects/msaa.md | 79 ++++++++++ docs/user/emissive-exposure.md | 107 ------------- docs/user/examples.md | 104 ++++++------- docs/user/fog.md | 133 ---------------- docs/user/hdr.md | 77 ---------- docs/user/lights/README.md | 16 ++ docs/user/lights/emissive-exposure.md | 114 ++++++++++++++ docs/user/{ => lights}/lights.md | 12 +- docs/user/{ => lights}/shadows.md | 12 +- docs/user/mesh.md | 108 ------------- docs/user/meshes/README.md | 17 +++ docs/user/{ => meshes}/materials.md | 16 +- docs/user/{ => meshes}/meshes.md | 8 +- docs/user/meshes/sources.md | 113 ++++++++++++++ docs/user/msaa.md | 127 --------------- docs/user/quickstart.md | 195 ++++++++++++------------ lib/examples/cameras/README.md | 4 +- lib/examples/effects/demo.rs | 2 +- lib/examples/meshes/README.md | 4 +- 31 files changed, 988 insertions(+), 881 deletions(-) delete mode 100644 docs/user/bloom.md create mode 100644 docs/user/cameras/README.md rename docs/user/{ => cameras}/camera-input.md (94%) rename docs/user/{ => cameras}/gpu-driven.md (99%) create mode 100644 docs/user/effects/README.md create mode 100644 docs/user/effects/bloom.md create mode 100644 docs/user/effects/dof.md create mode 100644 docs/user/effects/fog.md create mode 100644 docs/user/effects/hdr.md create mode 100644 docs/user/effects/msaa.md delete mode 100644 docs/user/emissive-exposure.md delete mode 100644 docs/user/fog.md delete mode 100644 docs/user/hdr.md create mode 100644 docs/user/lights/README.md create mode 100644 docs/user/lights/emissive-exposure.md rename docs/user/{ => lights}/lights.md (84%) rename docs/user/{ => lights}/shadows.md (87%) delete mode 100644 docs/user/mesh.md create mode 100644 docs/user/meshes/README.md rename docs/user/{ => meshes}/materials.md (84%) rename docs/user/{ => meshes}/meshes.md (92%) create mode 100644 docs/user/meshes/sources.md delete mode 100644 docs/user/msaa.md diff --git a/README_DETAILS.md b/README_DETAILS.md index d5135fb..9360014 100644 --- a/README_DETAILS.md +++ b/README_DETAILS.md @@ -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. -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 diff --git a/docs/tech/ARCHI_CPU_GPU.md b/docs/tech/ARCHI_CPU_GPU.md index 1652a05..577384e 100644 --- a/docs/tech/ARCHI_CPU_GPU.md +++ b/docs/tech/ARCHI_CPU_GPU.md @@ -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é ; > 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. -> 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 > 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é) ; @@ -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 > 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 -> 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 ». 1. Répartition des Rôles : CPU vs GPU (La Source de Vérité) diff --git a/docs/user/README.md b/docs/user/README.md index 6af6b4c..69db648 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -1,64 +1,57 @@ -# User documentation — WSG +# WSG — User documentation -**Usage** documentation for the `wsg-lib` crate: how to build a 3D rendering application -without touching wgpu directly. It targets a developer with basic Rust knowledge; no prior -GPU graphics background is required. - -> **Not to be confused**: these pages explain *how to use* the API. The **technical** -> 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`). +WSG (WGPU Simple Graphics) is a 3D graphics engine built on top of +[wgpu](https://docs.rs/wgpu) and [winit](https://docs.rs/winit). +It deliberately provides **no scene-graph abstraction**: you create resources, +place entities, and write the frame loop yourself. The engine handles the rest +(GPU context, compilation, command encoding, presentation). ## Where to start 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 | -|-------|-------| -| [Meshes](meshes.md) | Geometries: procedural primitives, custom `Geometry`, entities and `Transform` | -| [Materials & textures](materials.md) | Appearance: the `standard` shader, unlit mode, diffuse textures | -| [Lights](lights.md) | Directional, point, spot, ambient, `MAX_LIGHTS` | -| [Shadows](shadows.md) | Shadow mapping: picking the casting light, the packed-index pitfall | -| [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 | +| Folder | Pages | +|--------|-------| +| [`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 | +| [`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) | +| [`cameras/`](cameras/README.md) | [Camera & input](cameras/camera-input.md) — orbital controller, unified input · [GPU-driven rendering](cameras/gpu-driven.md) — culling, LOD | +| [`effects/`](effects/README.md) | [HDR](effects/hdr.md) · [Bloom](effects/bloom.md) · [MSAA](effects/msaa.md) · [Fog](effects/fog.md) · [DoF](effects/dof.md) | -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 | -|---------|--------------|----------------| -| Shadows | `scene.set_shadow_caster(Some(idx))` | No shadow map allocated, no depth pass, no PCF sampling | -| HDR + Tone mapping | `AppBuilder::with_hdr(ToneMapper::Aces)` | No offscreen texture, no TM pass, direct-to-surface render | -| MSAA | `AppBuilder::with_msaa(4)` | Single-sample (1×), zero overhead | -| GPU-driven culling | `AppBuilder::with_gpu_driven(true)` | No compute pipeline, no indirect draw buffers | -| LOD | `scene.create_mesh_with_lod(…, levels)` | Single-level mesh, no decimation, no hysteresis | -| Primitives | Cargo feature `prim-*` (default: all) | Not compiled at all | -| File import | Cargo feature `import-*` | Not compiled at all | +- **Explicit over magic**: no scene graph, no ECS, no hidden state machine. What you write + is what runs. +- **The handler drives the loop**: `AppHandler` is the only required trait (`setup`, + `update`, `render` + optional event hook). +- **String IDs everywhere**: meshes, materials, textures and entities are referenced by + label — no integer handles to manage, errors are readable. +- **Safe core, `unsafe` at the edges**: the public API is fully safe; `unsafe` is confined + to the raw-pointer interop layer. +- **Feature-gated primitives**: every primitive and importer behind a Cargo feature + (`prim-cube`, `import-obj`, …) — default is `all-prims` + `import-obj`. -The distinction matters: -- **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. +## Documentation tree -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 -- 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) · [ROADMAP](../ROADMAP.md) · [DRAFT](../DRAFT.md) -- Full API reference: `cargo doc -p wsg-lib --no-deps` +- [Root README](../../README.md) +- Technical docs: [ARCHI_APP](../tech/ARCHI_APP.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md) · + [ARCHI_CPU_GPU](../tech/ARCHI_CPU_GPU.md) · [ARCHI_RENDU](../tech/ARCHI_RENDU.md) +- [ROADMAP](../ROADMAP.md) diff --git a/docs/user/bloom.md b/docs/user/bloom.md deleted file mode 100644 index 67c1a5b..0000000 --- a/docs/user/bloom.md +++ /dev/null @@ -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). diff --git a/docs/user/cameras/README.md b/docs/user/cameras/README.md new file mode 100644 index 0000000..90699ed --- /dev/null +++ b/docs/user/cameras/README.md @@ -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) diff --git a/docs/user/camera-input.md b/docs/user/cameras/camera-input.md similarity index 94% rename from docs/user/camera-input.md rename to docs/user/cameras/camera-input.md index eaba190..2ceb360 100644 --- a/docs/user/camera-input.md +++ b/docs/user/cameras/camera-input.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) | 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 @@ -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 -> implemented yet — deferred, see [ROADMAP](../ROADMAP.md). +> implemented yet — deferred, see [ROADMAP](../../ROADMAP.md). ## 4. Common recipes @@ -123,5 +123,5 @@ fn update(&mut self, app: &mut wsg_lib::App) { ## Links -- [User README](README.md) · [Lights](lights.md) · [Examples](examples.md) -- [Root README](../../README.md) · [ARCHI_APP](../tech/ARCHI_APP.md) +- [User README](../README.md) · [Lights](../lights/lights.md) · [Examples](../examples.md) +- [Root README](../../../README.md) · [ARCHI_APP](../../tech/ARCHI_APP.md) diff --git a/docs/user/gpu-driven.md b/docs/user/cameras/gpu-driven.md similarity index 99% rename from docs/user/gpu-driven.md rename to docs/user/cameras/gpu-driven.md index 63c4f9c..dddedf0 100644 --- a/docs/user/gpu-driven.md +++ b/docs/user/cameras/gpu-driven.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) diff --git a/docs/user/effects/README.md b/docs/user/effects/README.md new file mode 100644 index 0000000..37e5fdf --- /dev/null +++ b/docs/user/effects/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) diff --git a/docs/user/effects/bloom.md b/docs/user/effects/bloom.md new file mode 100644 index 0000000..52494aa --- /dev/null +++ b/docs/user/effects/bloom.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) diff --git a/docs/user/effects/dof.md b/docs/user/effects/dof.md new file mode 100644 index 0000000..cfcd22b --- /dev/null +++ b/docs/user/effects/dof.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) diff --git a/docs/user/effects/fog.md b/docs/user/effects/fog.md new file mode 100644 index 0000000..3b90124 --- /dev/null +++ b/docs/user/effects/fog.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) diff --git a/docs/user/effects/hdr.md b/docs/user/effects/hdr.md new file mode 100644 index 0000000..904558c --- /dev/null +++ b/docs/user/effects/hdr.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) diff --git a/docs/user/effects/msaa.md b/docs/user/effects/msaa.md new file mode 100644 index 0000000..6fd52b1 --- /dev/null +++ b/docs/user/effects/msaa.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) diff --git a/docs/user/emissive-exposure.md b/docs/user/emissive-exposure.md deleted file mode 100644 index 0e13f7a..0000000 --- a/docs/user/emissive-exposure.md +++ /dev/null @@ -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é. diff --git a/docs/user/examples.md b/docs/user/examples.md index 2459e4e..95e689f 100644 --- a/docs/user/examples.md +++ b/docs/user/examples.md @@ -1,70 +1,72 @@ # Examples -Sixteen examples live in [`lib/examples/`](../../lib/examples/), organized into -**four category folders** — [`meshes/`](../../lib/examples/meshes/README.md), -[`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 ` (names are stable, run from the repo -root). They are **self-contained**: no assets on disk (procedural textures, -hard-coded geometries). +16 examples in **4 folders** (mirroring the topic folders of this documentation), covering the +full range of the engine — from a 2D quad to GPU-driven rendering. -| Folder | Example | What it shows | Corresponding page | -|--------|---------|---------------|--------------------| -| `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) | -| `meshes/` | `cube` | The 3D MVP: a textured (checkerboard) cube, lit (directional + point + spot), spinning | [Meshes](meshes.md), [Materials](materials.md), [Lights](lights.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) | +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), +[`lights/`](../../lib/examples/lights/README.md), [`cameras/`](../../lib/examples/cameras/README.md), +[`effects/`](../../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 -framework, experimentation), you bypass `App` and drive directly: +## The `manual` example: bypassing the helpers -- `Context` (*Manager* layer): GPU lifecycle — `Instance`/`Surface`/`Adapter`/`Device`/ - `Queue`, `configure()` for the swapchain, `get_next_frame()`. -- `Renderer` (*Executor* layer): `render(view, mesh, material)` = one object per submission; - `present(frame)`. -- `PipelineCache`: `register_shader(id, path)` then `Material::new(format, id, &mut cache)`. +[`manual.rs`](../../lib/examples/meshes/manual.rs) renders a rotating cube with **no +high-level helper at all** — no `Scene`, no `Renderer` convenience API, no `AppHandler` +default `render()`. It shows the full pipeline: -The window and GPU are created in winit 0.30's `resumed()` callback (`run_app` + -`ApplicationHandler`), as in `app.rs`. The reference file is -[`manual.rs`](../../lib/examples/meshes/manual.rs); the two-layer architecture is detailed in -[ARCHI_APP](../tech/ARCHI_APP.md) and [FRAME_LOOP](../tech/FRAME_LOOP.md). +1. **`setup`**: manual creation of vertex/index buffers, bind groups, render/compute + pipelines, the swapchain. +2. **`render` (overridden)**: manual command encoding per frame (clear, draw, present) — + 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 -> pixels — it gives more control over command encoding. +This is the reference for what the high-level API does under the hood, and the starting +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 -Repo conventions (see [`lib/examples/README.md`](../../lib/examples/README.md)): +1. Create `lib/examples//.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//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 [[example]] - name = "my_example" - path = "examples//my_example.rs" + name = "" + path = "examples//.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 -- [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) diff --git a/docs/user/fog.md b/docs/user/fog.md deleted file mode 100644 index f5b7e3a..0000000 --- a/docs/user/fog.md +++ /dev/null @@ -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" -``` diff --git a/docs/user/hdr.md b/docs/user/hdr.md deleted file mode 100644 index 8894aa6..0000000 --- a/docs/user/hdr.md +++ /dev/null @@ -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 diff --git a/docs/user/lights/README.md b/docs/user/lights/README.md new file mode 100644 index 0000000..81b0dd8 --- /dev/null +++ b/docs/user/lights/README.md @@ -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) diff --git a/docs/user/lights/emissive-exposure.md b/docs/user/lights/emissive-exposure.md new file mode 100644 index 0000000..9db4d57 --- /dev/null +++ b/docs/user/lights/emissive-exposure.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) diff --git a/docs/user/lights.md b/docs/user/lights/lights.md similarity index 84% rename from docs/user/lights.md rename to docs/user/lights/lights.md index c67a928..d613a0b 100644 --- a/docs/user/lights.md +++ b/docs/user/lights/lights.md @@ -37,9 +37,9 @@ app.scene.add_spot_light( ).unwrap(); ``` -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 -directional, and [`spot_test.rs`](../../lib/examples/lights/spot_test.rs) isolates a single spot +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 +directional, and [`spot_test.rs`](../../../lib/examples/lights/spot_test.rs) isolates a single spot (ambient nearly zero). Global settings: @@ -68,7 +68,7 @@ Two consequences: [Shadows](shadows.md). 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 - [`shadow_test.rs`](../../lib/examples/lights/shadow_test.rs)). + [`shadow_test.rs`](../../../lib/examples/lights/shadow_test.rs)). ## Intensities and tints @@ -80,5 +80,5 @@ Two consequences: ## Links -- [User README](README.md) · [Shadows](shadows.md) · [Materials & textures](materials.md) -- [Root README](../../README.md) · [ARCHI_RENDU](../tech/ARCHI_RENDU.md) +- [User README](../README.md) · [Shadows](shadows.md) · [Materials & textures](../meshes/materials.md) +- [Root README](../../../README.md) · [ARCHI_RENDU](../../tech/ARCHI_RENDU.md) diff --git a/docs/user/shadows.md b/docs/user/lights/shadows.md similarity index 87% rename from docs/user/shadows.md rename to docs/user/lights/shadows.md index 8b8f25f..d0c2994 100644 --- a/docs/user/shadows.md +++ b/docs/user/lights/shadows.md @@ -29,7 +29,7 @@ Two ways to avoid it: 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: @@ -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 ``` - 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) 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 `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. - 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 - (see [Materials & textures](materials.md)) receives none. + (see [Materials & textures](../meshes/materials.md)) receives none. ## Tuning shadow rendering @@ -75,5 +75,5 @@ Tuning tips: ## Links -- [User README](README.md) · [Lights](lights.md) · [Examples](examples.md) -- [Root README](../../README.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md) +- [User README](../README.md) · [Lights](lights.md) · [Examples](../examples.md) +- [Root README](../../../README.md) · [FRAME_LOOP](../../tech/FRAME_LOOP.md) diff --git a/docs/user/mesh.md b/docs/user/mesh.md deleted file mode 100644 index f14b8f2..0000000 --- a/docs/user/mesh.md +++ /dev/null @@ -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) diff --git a/docs/user/meshes/README.md b/docs/user/meshes/README.md new file mode 100644 index 0000000..6473635 --- /dev/null +++ b/docs/user/meshes/README.md @@ -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) diff --git a/docs/user/materials.md b/docs/user/meshes/materials.md similarity index 84% rename from docs/user/materials.md rename to docs/user/meshes/materials.md index 28d60bb..b2ecc7e 100644 --- a/docs/user/materials.md +++ b/docs/user/meshes/materials.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). 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 @@ -22,8 +22,8 @@ app.scene 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 -[ARCHI_RENDU](../tech/ARCHI_RENDU.md) and the -[`shaders/standard_shader.wgsl`](../../lib/src/shaders/standard_shader.wgsl) file). +[ARCHI_RENDU](../../tech/ARCHI_RENDU.md) and the +[`shaders/standard_shader.wgsl`](../../../lib/src/shaders/standard_shader.wgsl) file). ## 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** (`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 @@ -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 -[`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: 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: 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 > switching to unlit. ## Links -- [User README](README.md) · [Meshes](meshes.md) · [Lights](lights.md) · [Examples](examples.md) -- [Root README](../../README.md) · [ARCHI_RENDU](../tech/ARCHI_RENDU.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) diff --git a/docs/user/meshes.md b/docs/user/meshes/meshes.md similarity index 92% rename from docs/user/meshes.md rename to docs/user/meshes/meshes.md index 1f9cf90..5e48572 100644 --- a/docs/user/meshes.md +++ b/docs/user/meshes/meshes.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 -horizon — see [ROADMAP](../ROADMAP.md)). +horizon — see [ROADMAP](../../ROADMAP.md)). ## 4. Moving / animating: the `Transform` 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. -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 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 -- [User README](README.md) · [Quickstart](quickstart.md) · [Materials & textures](materials.md) · [Lights](lights.md) -- [Root README](../../README.md) · [ARCHI_APP](../tech/ARCHI_APP.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) diff --git a/docs/user/meshes/sources.md b/docs/user/meshes/sources.md new file mode 100644 index 0000000..42bf814 --- /dev/null +++ b/docs/user/meshes/sources.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) diff --git a/docs/user/msaa.md b/docs/user/msaa.md deleted file mode 100644 index 1207307..0000000 --- a/docs/user/msaa.md +++ /dev/null @@ -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. diff --git a/docs/user/quickstart.md b/docs/user/quickstart.md index 4adb955..e02a58e 100644 --- a/docs/user/quickstart.md +++ b/docs/user/quickstart.md @@ -1,132 +1,129 @@ # Quickstart -Goal: a window showing an object, with the render loop handled by the library. You will only -write three things: a struct implementing `AppHandler`, your scene declaration in `setup()`, -and your `main()`. +Get a window with a rotating cube on screen in ~30 lines. The full version with comments is +in the [`simple`](../../lib/examples/meshes/simple.rs) example (2D quad, unlit) and +[`cube`](../../lib/examples/meshes/cube.rs) (3D cube, lit). -## Prerequisites - -- 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`: +## 1. Add the dependency ```toml +# Cargo.toml [dependencies] -wsg-lib = { path = "/path/to/wsg/lib" } -pollster = { version = "1", features = ["macro"] } # for #[pollster::main] (AppBuilder is async) +wsg-lib = { path = "../lib" } +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 -verbatim: a flat two-tone quad, rendered automatically every frame. +## 2. Implement `AppHandler` + +Three mandatory methods (`setup`, `update`, `render`) and an optional event hook. ```rust -use wsg_lib::app::AppBuilder; -use wsg_lib::resources::Geometry; -use wsg_lib::utils::WsgError; -use wsg_lib::AppHandler; +use glam::{Quat, Vec3}; +use winit::event::MouseButton; +use winit::keyboard::KeyCode; +use wsg_lib::camera::CameraController; +use wsg_lib::prelude::*; -struct MyQuad; +struct MyHandler { + camera: CameraController, +} -impl AppHandler for MyQuad { - fn setup(&mut self, app: &mut wsg_lib::App) { - // Flat 2D: the `standard` shader in unlit mode returns the vertex color as-is. - app.renderer_mut().set_unlit(true); +impl AppHandler for MyHandler { + fn new() -> Self { + Self { camera: CameraController::default() } + } + + 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 - .register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH) - .unwrap(); + .add_directional_light(Vec3::new(1.0, 1.2, 1.0).normalize(), [1.0, 0.98, 0.92], 1.5)?; + app.scene.set_shadow_caster(Some(0)); + Ok(()) + } - let geometry = Geometry::new(vec![ - [-0.5, 0.5, 0.0], - [ 0.5, 0.5, 0.0], - [ 0.5, -0.5, 0.0], - [-0.5, -0.5, 0.0], - ]) - .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]); + fn update(&mut self, app: &mut App) { + // Spin the cube. + let mut tf = *app.scene.entity_transform("cube").unwrap(); + tf.rotation = Quat::from_rotation_y(self.t) * tf.rotation; + app.scene.set_entity_transform("cube", tf); + self.t += 0.02; - // `None`: the scene injects its default material (`standard`) at render time. - app.scene.create_mesh("quad_mesh", geometry, None).unwrap(); - app.scene.add_entity("quad", "quad_mesh").unwrap(); + // Camera: orbit (left-drag), zoom (wheel), reset (R), presets (1/2/3). + let (dx, dy) = app.input.mouse_delta(); + 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] -async fn main() -> Result<(), WsgError> { - let app = AppBuilder::new().title("WSG Simple").build().await?; - app.run(MyQuad) +## 3. Build the app + +```rust +fn main() -> Result<(), Box> { + 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 -[FRAME_LOOP](../tech/FRAME_LOOP.md)): +## 4. Run it -``` -AppBuilder::build() creates the event loop - │ -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 +```sh +cargo run -p wsg-lib --example cube # the reference "hello world" of the engine ``` -So you implement: - -| 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`. +Controls (in the `cube`/`demo` examples): **left-drag** orbit, **wheel** zoom, **R** reset +camera, **1/2/3** view presets, **H** help overlay, **Esc** quit. ## 5. Where to go next -- Want a 3D object? → [Meshes](meshes.md) -- Want to change the look / add a texture? → [Materials & textures](materials.md) -- Want lights? → [Lights](lights.md) -- Want to see everything at once? → the `demo` example ([Examples](examples.md)) +- [Meshes](meshes/meshes.md) — entities, transforms, custom geometries +- [Materials & textures](meshes/materials.md) — diffuse textures, unlit mode +- [Lights](lights/lights.md) — point/spot lights, ambient +- [Camera & input](cameras/camera-input.md) — the full input API +- [GPU-driven](cameras/gpu-driven.md) — culling, LOD, debugging the GPU path ## Links -- [User README](README.md) · [Meshes](meshes.md) · [Examples](examples.md) -- [Root README](../../README.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md) · [ARCHI_APP](../tech/ARCHI_APP.md) +- [User README](README.md) · [Meshes](meshes/meshes.md) · [Examples](examples.md) +- [Root README](../../README.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md) diff --git a/lib/examples/cameras/README.md b/lib/examples/cameras/README.md index 8361147..48cae26 100644 --- a/lib/examples/cameras/README.md +++ b/lib/examples/cameras/README.md @@ -48,5 +48,5 @@ cargo run -p wsg-lib --example culling > even off-screen). > > The GPU-driven pipeline (compute matrices → culling → indirect draws) is -> 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). +> documented in [`docs/tech/ARCHI_CPU_GPU.md`](../../../docs/tech/ARCHI_CPU_GPU.md) +> and [`docs/user/cameras/gpu-driven.md`](../../../docs/user/cameras/gpu-driven.md). diff --git a/lib/examples/effects/demo.rs b/lib/examples/effects/demo.rs index 5681584..e1b8937 100644 --- a/lib/examples/effects/demo.rs +++ b/lib/examples/effects/demo.rs @@ -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 // 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 - // 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. let frames = match std::env::var("WSG_DEBUG_DUMP") { Ok(v) => v.parse::().ok().filter(|&n| n > 0).unwrap_or(3), diff --git a/lib/examples/meshes/README.md b/lib/examples/meshes/README.md index 8163fd1..1bff906 100644 --- a/lib/examples/meshes/README.md +++ b/lib/examples/meshes/README.md @@ -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 (`run_app` + `ApplicationHandler`). The two-layer architecture is detailed in -[`docs/tech/ARCHI_APP.md`](../../docs/tech/ARCHI_APP.md) and -[`FRAME_LOOP.md`](../../docs/tech/FRAME_LOOP.md). +[`docs/tech/ARCHI_APP.md`](../../../docs/tech/ARCHI_APP.md) and +[`FRAME_LOOP.md`](../../../docs/tech/FRAME_LOOP.md). ```sh cargo run -p wsg-lib --example manual