refactor examples

This commit is contained in:
Jérôme Bousquié
2026-09-25 10:19:24 +02:00
parent ab3f056dbb
commit 35aeb769a8
37 changed files with 3430 additions and 457 deletions
+121 -195
View File
@@ -1,230 +1,156 @@
# WSG - WGPU Simple Graphics Library
# WSG — WGPU Simple Graphics Library
WSG is a Rust library that wraps [wgpu](https://github.com/gfx-rs/wgpu) and [winit](https://crates.io/crates/winit) for simple GPU drawing. It groups the five core wgpu objects (Instance, Surface, Adapter, Device, Queue) behind a single `Context`, adds small building blocks (`Mesh`, `Material`, `PipelineCache`, `Frame`), and exposes the low-level primitives for advanced users.
**WSG** (WGPU Simple Graphics) est une bibliothèque Rust qui wrap [wgpu](https://github.com/gfx-rs/wgpu) et [winit](https://crates.io/crates/winit) pour dessiner en 3D **sans toucher wgpu directement**.
> **Status: unstable development version.** The **declarative workflow** (`AppBuilder` + `App` + `AppHandler`) is the **recommended** path and is fully working: scene auto-rendering (`App::render_scene`), 3D Phong lighting, textures, shadows, camera and unified input — the `demo` example is the showcase. The **manual workflow** (`Context`/`Renderer`/`PipelineCache`) coexists for fine-grained control. Meshes are declared from a CPU `Geometry` (retained as `Arc<Geometry>` on the Mesh). The **GPU-driven two-pass pipeline** (Compute Pass deriving world matrices + frustum culling → indirect draws) is **implemented** (Step 15, Phase 3): `render_scene` and the shadow pass are 100 % indirect, and frustum culling is opt-in (`AppBuilder::with_culling(true)`, off by default) — see [Status](#status), [docs/user/gpu-driven.md](docs/user/gpu-driven.md) and [Roadmap](#roadmap).
## Ce que vous obtenez
## Status
- **Une fenêtre 3D en ~30 lignes** — pas de wgpu, pas de winit dans votre code
- **Éclairage Phong** (directional, point, spot) + **ombres portées** (shadow mapping)
- **HDR + Tone Mapping** (ACES Filmic / Reinhard) — opt-in, zéro coût si désactivé
- **Pipeline GPU-driven** — world matrices + frustum culling sur le GPU, indirect draws
- **LOD** (Level of Detail) — dégradation automatique de la géométrie selon la distance
- **Primitives procédurales** — cube, sphère, cylindre, cône, tore, plan
- **Import de fichiers** — parser OBJ intégré (glTF en cours)
- **Caméra orbitale** + input unifié (clavier/souris)
- **LOD, culling, HDR, ombres** : tout est **opt-in** — ce que vous n'activez pas ne coûte rien
| Area | State |
|------|-------|
| Manual workflow (`Context` + `Renderer` + `PipelineCache`) | ✅ Working (advanced — fine-grained control) |
| `App` / `AppBuilder` / `AppHandler` event-loop facade | ✅ Working — window, events, frame presentation, and **automatic scene rendering** (the per-frame view is exposed via `Frame::view()`) |
| `Scene` resource/entity registry | ✅ Working — the engine renders every registered entity automatically in one batched render pass (`App::render_scene`) |
| GPU-driven two-pass pipeline (Compute → indirect draw) | ✅ Working (Step 15, Phase 3) — `render_scene` + shadow pass are 100 % indirect; opt-in frustum culling (bug « fenêtre noire » fixed 2026-09-22 — WGSL `select` argument order — and verified by GPU readback, D14). User doc: [gpu-driven.md](docs/user/gpu-driven.md) · spec: [ARCHI_CPU_GPU.md](docs/tech/ARCHI_CPU_GPU.md) |
| 3D infrastructure (uniform bind groups, MVP + camera in the pipeline) | ✅ Working — the `Renderer` uploads per-frame camera matrices (active `Camera`) and per-entity world matrices to shared uniform buffers every frame; the **MVP is reached** (Step 5) : the `cube` example renders a rotating Phong-lit cube via the `standard` shader |
## Forces
Note: `standard_shader.wgsl` (Phong, with an explicit **unlit** mode) is the **single** shader the library ships — flat 2D drawing is its unlit variant (`Renderer::set_unlit(true)` or `app.renderer_mut().set_unlit(true)`). See the `cube` example (3D, lit) and the `simple` example (2D, unlit).
| Force | Détail |
|-------|--------|
| **Zéro wgpu dans votre code** | L'API déclarative (`AppBuilder` + `AppHandler`) encapsule tout |
| **Opt-in = zéro coût** | Un effet non activé n'alloue rien, n'exécute rien |
| **Features Cargo** | Ne compilez que les primitives/import dont vous avez besoin |
| **Un seul shader** | Le `standard` shader (Phong) couvre 90 % des cas ; mode unlit pour la 2D |
| **GPU-driven** | Le CPU envoie des transforms, le GPU fait le reste (matrices, culling, draws) |
## What it does
### Declarative workflow (recommended)
Register your scene once in `setup()`, then let `App` handle the window lifecycle, events,
input and frame presentation — **without importing wgpu or winit**. This is the workflow of
the `simple`, `cube`, `demo`, `shadow_test` and `spot_test` examples (excerpt below is `simple`):
## Quickstart
```rust
use wsg_lib::prelude::*;
use wsg_lib::app::AppBuilder;
use wsg_lib::resources::Geometry;
use wsg_lib::utils::WsgError;
use wsg_lib::AppHandler;
struct MonQuad;
struct MaScene;
impl AppHandler for MonQuad {
impl AppHandler for MaScene {
fn setup(&mut self, app: &mut wsg_lib::App) {
app.renderer_mut().set_unlit(true); // 2D flat (optional)
app.scene
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
.unwrap();
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],
[0.0, 1.0, 0.0, 1.0],
[0.0, 0.0, 1.0, 1.0],
[1.0, 1.0, 0.0, 1.0],
])
.with_indices(vec![0, 1, 2, 0, 2, 3]);
app.scene.create_mesh("quad_mesh", geometry, None).unwrap(); // None = default material
app.scene.add_entity("quad", "quad_mesh").unwrap();
app.scene
.create_material("mat", "standard", None)
.unwrap();
// Un cube lit par Phong, posé au-dessus d'un plan
app.scene
.create_mesh("cube", cube(1.0), Some("mat"))
.unwrap();
app.scene
.add_entity("my_cube", "cube")
.unwrap();
app.scene
.create_mesh("ground", plane(10.0, 10.0, 1, 1), Some("mat"))
.unwrap();
app.scene
.add_entity("floor", "ground")
.unwrap();
}
// `update(&mut self, app)` — your per-frame logic (empty default).
// `render(&mut self, app, frame)` — default: `app.render_scene(frame.view())`,
// the whole scene is drawn automatically in one pass per frame.
}
#[pollster::main]
async fn main() -> Result<(), WsgError> {
let app = AppBuilder::new().title("WSG Simple").build().await?;
app.run(MonQuad)
fn main() -> Result<(), WsgError> {
let mut app = AppBuilder::new()
.title("Ma scène WSG")
.with_hdr(ToneMapper::Aces) // optionnel : HDR + tone mapping
.build()?;
app.run(MaScene);
Ok(())
}
```
> API note: `Scene` methods currently return `Result<_, String>` — typed-error unification is
> on the roadmap. `Scene::create_mesh(id, geometry, material)` takes a CPU `Geometry` (source of
> truth, retained as `Arc<Geometry>` on the Mesh); `material = None` uses the scene's default
> material.
The full user documentation (meshes, materials, lights, shadows, camera & input, all examples)
lives in [docs/user](docs/user/README.md).
### Manual workflow (advanced — fine-grained control)
Bypass the `App` facade and drive `Context`, `Renderer` and `PipelineCache` yourself (same code
as the `manual` example):
```rust
use std::sync::Arc;
use winit::event_loop::EventLoop;
use winit::window::WindowBuilder;
use wsg_lib::core::{Context, Frame, Renderer};
use wsg_lib::pipeline::PipelineCache;
use wsg_lib::resources::{Geometry, Material, Mesh};
use wsg_lib::utils;
fn main() {
// Window + async GPU init
let event_loop = EventLoop::new().unwrap();
let window = Arc::new(WindowBuilder::new().build(&event_loop).unwrap());
let context = pollster::block_on(Context::new(window.clone())).expect("GPU init failed");
let format = context.configure(&context.adapter, 800, 600).expect("surface config failed");
// Renderer + shader cache (falls back to the embedded shader if the file is missing)
// `set_unlit(true)` selects flat 2D rendering (the quad below is drawn in NDC space, unlit).
// Step 9: width/height size the depth buffer allocated inside the Renderer.
let mut renderer = Renderer::new(&context, format, 800, 600);
renderer.set_unlit(true);
let mut cache = PipelineCache::new(Arc::new(context.device.clone()));
cache.register_shader("standard", utils::STANDARD_SHADER_PATH).unwrap();
// Material + mesh (Step 8: the mesh is built from a `Geometry` — positions,
// optional attributes via builder, white defaults via `to_vertices`).
let material = Material::new(renderer.format(), "standard", &mut cache);
let geometry = Geometry::new(vec![
[-0.5, 0.5, 0.0], // top-left
[ 0.5, 0.5, 0.0], // top-right
[ 0.5, -0.5, 0.0], // bottom-right
[-0.5, -0.5, 0.0], // bottom-left
])
.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]);
let mesh = Mesh::from_geometry(renderer.device(), Arc::new(geometry), None);
// Render loop
event_loop.run(|event, elwt| {
match event {
winit::event::Event::AboutToWait => window.request_redraw(),
winit::event::Event::WindowEvent { event: winit::event::WindowEvent::RedrawRequested, .. } => {
if let Some(frame) = Frame::try_new(&context.surface) {
renderer.render(frame.view(), &mesh, &material);
renderer.present(frame);
}
}
winit::event::Event::WindowEvent { event: winit::event::WindowEvent::CloseRequested, .. } => elwt.exit(),
_ => {}
}
}).unwrap();
}
```
## Architecture overview
- **Manager layer (`Context`)** — owns the GPU hardware lifecycle (Instance → Surface → Adapter → Device → Queue). Created once at startup; `configure()` sets up the swapchain, `Frame` wraps each frame's surface texture + view.
- **Executor layer (`Renderer`)** — binds a `Material` pipeline + `Mesh` buffers into a RenderPass and submits the commands. Rendering a whole `Scene` (`render_scene`) batches all entities into **one encoder + one submit per frame**; the low-level `render` still allocates one per object.
- **Supporting pieces** — `PipelineCache` (shader → compiled RenderPipeline, `Arc`-shared), `Material`, `Geometry`/`Mesh`/`Vertex`, `Scene` (string-ID registry), `Camera`/`Transform` (active camera wired to the frame uniforms, Step 4.3). `Geometry` is the CPU source of truth (positions/normals/UVs/colors), `Mesh` uploads it to GPU buffers and retains the `Arc<Geometry>`, `Vertex` is the interleaved upload contract (Step 8).
The **GPU-driven two-pass pipeline** (Step 15, Phase 3) is implemented: 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. Specified in [docs/tech/ARCHI_APP.md](docs/tech/ARCHI_APP.md) and [docs/tech/ARCHI_CPU_GPU.md](docs/tech/ARCHI_CPU_GPU.md); user-facing guide in [docs/user/gpu-driven.md](docs/user/gpu-driven.md).
## Quick reference
| Concept | Type | Responsibility | Status |
|---------|------|---------------|--------|
| App / AppBuilder | Facade | Window lifecycle + winit event loop + frame presentation | ✅ (auto scene rendering via `App::render_scene`) |
| AppHandler | Trait | User-defined `setup()` / `update()` / `render()` callbacks | ✅ (default `render` draws the scene via `App::render_scene`) |
| Scene | Struct | String-ID registry: meshes, materials, entities | ✅ (registry auto-rendered by the facade) |
| Context | Struct | GPU hardware lifecycle (Instance, Surface, Adapter, Device, Queue) | ✅ |
| Renderer | Struct | Binds Material + Mesh into a RenderPass, submits | ✅ (`render_scene` batches one pass/frame) |
| PipelineCache | Struct | Shader → compiled RenderPipeline cache | ✅ |
| Material | Struct | Shader ID → RenderPipeline | ✅ |
| Geometry | Struct | CPU-side scattered vertex data (positions/normals/UVs/colors/indices), source of truth | ✅ (Step 8 — retained `Arc<Geometry>` on Mesh) |
| Mesh / Vertex | Struct | GPU geometry container / CPU-side interleaved upload tuple | ✅ |
| Frame | Struct | Per-frame RAII wrapper (surface texture + view) | ✅ |
| Camera / Transform | Struct | Camera & transform math | ✅ Active camera + transform wired to per-frame uniforms (Step 4.3) |
| Texture | Struct | GPU diffuse image (device + view + sampler, `Rgba8UnormSrgb`) | ✅ (Step 10 — `from_rgba8`/`from_bytes`/`from_file`/`white_placeholder`) |
| Lights / Light | Struct | Scene-wide light list (directional + point + spot, `MAX_LIGHTS = 8`) + ambient | ✅ (Steps 12-13) |
| CameraController | Struct | Orbital camera (yaw/pitch/distance/target; `orbit`/`zoom`/`reset`/`apply_to`) | ✅ (Step 15.C) |
| InputState | Struct | Unified keyboard/mouse state (pressed/held/released, mouse delta, scroll) | ✅ (Step 15.B — `app.input`) |
| math::primitives | Module | Procedural `Geometry` generators (cube, plane, uv_sphere, icosphere, cylinder, cone, torus) | ✅ (Step 15.A) |
## Getting started
WSG is **not published on crates.io** — depend on it by path:
```toml
[dependencies]
wsg-lib = { path = "/path/to/wsg/lib" }
pollster = { version = "1", features = ["macro"] } # for #[pollster::main] (async AppBuilder)
winit = "0.30" # only if your code mentions winit types (KeyCode, MouseButton)
wsg-lib = { path = "../lib" }
pollster = { version = "1", features = ["macro"] }
```
| Action | Command |
|--------|---------|
| Build everything | `cargo build --workspace` |
| Run the showcase (primitives, lights, shadows, orbital camera) | `cargo run -p wsg-lib --example demo` |
| Run the 3D MVP example | `cargo run -p wsg-lib --example cube` |
| Run the minimal example | `cargo run -p wsg-lib --example simple` |
| Run the shadow / spot light showcases | `cargo run -p wsg-lib --example shadow_test` / `cargo run -p wsg-lib --example spot_test` |
| Run the advanced (manual) example | `cargo run -p wsg-lib --example manual` |
| Check everything (incl. examples) | `cargo check --all-targets` |
```sh
cargo run --example demo # le showcase complet (6 primitives, 3 lumières, ombres, HDR)
```
The `demo` example is the showcase: one of each primitive, procedural textures, three lights, a shadow-casting light and a live orbital camera. `simple` is the minimal declarative app (a colored quad, unlit); `cube` is the 3D MVP (a rotating Phong-lit, textured cube); `shadow_test` and `spot_test` isolate the shadow and spot-light systems; `manual` is the reference for the low-level workflow. All of them except `manual` use the declarative path and draw a scene **without importing wgpu**.
## Fonctionnalités
| Catégorie | Ce qui est disponible |
|-----------|----------------------|
| **Géométrie** | 6 primitives procédurales + import OBJ + `Geometry` custom |
| **Rendu** | Phong (lit), unlit (2D flat), HDR + tone mapping (ACES/Reinhard) |
| **Lumières** | Directional, point, spot (8 max) + ambient |
| **Ombres** | Shadow mapping (directional/spot), slope-scaled bias, PCF |
| **LOD** | Décimation quadric auto, hystérésis, 1 buffer multi-niveaux |
| **GPU-driven** | Compute pass (matrices + culling) → indirect draws |
| **Caméra** | Orbitale (drag/zoom/reset) + presets (front/side/top) |
| **Input** | Clavier (pressed/held/released), souris (delta, scroll, boutons) |
| **Textures** | RGBA8 (de bytes, de fichier, placeholder blanc) |
## Documentation
Three layers (user docs and API reference in **English**; technical docs in **French**):
| Où | Quoi |
|----|------|
| [docs/user/](docs/user/README.md) | **Guide utilisateur** (EN) — comment utiliser l'API, pas à pas |
| [docs/tech/](docs/tech/ARCHI_APP.md) | **Architecture interne** (FR) — décisions, specs, cibles |
| [docs/ROADMAP.md](docs/ROADMAP.md) | Feuille de route (phases 1-5 ✅, phase 6 en cours) |
| [docs/PLAN.md](docs/PLAN.md) | Livre de recette (historique des étapes) |
| `cargo doc -p wsg-lib --no-deps` | **Référence API** (rustdoc, 100 % couvert) |
**User documentation — [docs/user](docs/user/README.md)** (how to use the API, no wgpu knowledge needed):
- [Quickstart](docs/user/quickstart.md) — first window, first object, in ~30 lines
- [Meshes](docs/user/meshes.md) · [Materials & textures](docs/user/materials.md) · [Lights](docs/user/lights.md)
- [Shadows](docs/user/shadows.md) · [Camera & input](docs/user/camera-input.md) · [Examples](docs/user/examples.md)
## Exemples
**Technical documentation — `docs/tech/`** (internal architecture; each document states whether it describes the **current** or the **target** architecture):
- [ARCHI_APP](docs/tech/ARCHI_APP.md) — engine architecture. ✅ **Current** — facade (`App`/`AppHandler`) and GPU-driven two-pass pipeline (implemented in Phase 3, 2026-09-22, with the documented deviations); only the future double-buffering notes remain target.
- [ARCHI_CPU_GPU](docs/tech/ARCHI_CPU_GPU.md) — CPU/GPU workload split specification. ✅ **Current** — implemented in ROADMAP Phase 3 (2026-09-22, Étape 17, decisions D1–D14); deviations from the original spec are noted in the document.
- [ARCHI_RENDU](docs/tech/ARCHI_RENDU.md) — update/render mutability model. ✅ Current dichotomy (auto scene render) / 🎯 **Target** — material batching.
- [ARCHI_ARENES](docs/tech/ARCHI_ARENES.md) — 🎯 **Target/deferred** — slotmap generational handles; String IDs are used today.
- [FRAME_LOOP](docs/tech/FRAME_LOOP.md) — frame lifetime and resource persistence. ✅ **Current** — implemented.
| Exemple | Ce qu'il montre |
|---------|----------------|
| `demo` | Le showcase : 6 primitives, 3 lumières, ombres, HDR, LOD, caméra orbitale |
| `cube` | MVP 3D : un cube lit par Phong, texture checkerboard |
| `simple` | Minimal : un quad coloré en mode unlit (2D) |
| `shadow_test` | Ombres portées isolées |
| `spot_test` | Spotlight isolé |
| `import` | Import de fichier OBJ (feature `import-obj`) |
| `manual` | Workflow low-level (Context/Renderer/PipelineCache, sans App) |
**API reference** — full rustdoc: `cargo doc -p wsg-lib --no-deps` (every public type is documented).
## Features Cargo
## Roadmap
```toml
# Default : toutes les primitives
wsg-lib = { path = "../lib" }
1. ✅ **Scene auto-rendering** — `App::render_scene` iterates registered entities and draws them in one encoder/submit per frame; the frame view is exposed to `AppHandler::render` for custom draws. (Done 2026-09-16.)
2. ✅ **GPU-driven two-pass pipeline** — Compute Pass (world matrices + frustum culling) filling an indirect draw buffer, then indirect draws (see ARCHI_CPU_GPU). *(Done 2026-09-22 — see item 17. Deviation from the original spec: one indirect draw **per slot** rather than a single fused draw, D1 — see ARCHI_CPU_GPU.)*
3. **CPU→GPU transform sync** — persistent transform buffers with ring (triple) buffering.
4. ✅ **Real 3D pipeline (MVP reached)** — MVP uniforms + camera support in the vertex shader. *(Engine plumbing done 2026-09-16; Step 5, 2026-09-17: `standard` wired into the `cube` example — a unit cube lit (Phong) and spinning, rendered automatically by `App::render_scene`. Removal of `basic`: flat 2D = unlit variant of `standard` via `Renderer::set_unlit`.)*
5. **Typed resource handles** — keep String IDs for the MVP (current design, source of truth in `Scene`); slotmap-based generational handles (`ARCHI_ARENES.md`) are deferred to a later performance pass.
6. **Error unification** — replace `Result<_, String>` in `Scene`/`PipelineCache` with typed errors.
7. ✅ **CPU geometry storage (Step 8)** — `Mesh` retains a shared `Arc<Geometry>` (CPU source of truth with colors) alongside its GPU buffers; meshes are declared from a `Geometry` via `Mesh::from_geometry`/`Scene::create_mesh(id, geometry, material)` instead of raw `&[Vertex]` arrays. (Done 2026-09-18; `transform` stays on `Entity` — deviation D3.)
8. ✅ **Diffuse textures (Step 10, Phase 4.1)** — `resources::Texture` (GPU image: device+view+sampler, `Rgba8UnormSrgb`, loaders `from_rgba8`/`from_bytes`/`from_file`) attached to a `Material` as diffuse texture. The `standard` shader samples it via bind group **@2** (shared layout: sampler+texture); UVs are forwarded as vertex attribute location 2. Without a texture the material uses a shared 1×1 white placeholder so lit and unlit rendering are unchanged (no regression). The `cube` example now uses a procedural checkerboard texture. (Done 2026-09-18.)
9. ✅ **Window resize (Step 11, Phase 4.4)** — `App::resize` reconfigures the surface (`Context::configure`) and recreates the depth texture (`Renderer::resize_depth`) together on each `WindowEvent::Resized`, so color and depth attachments always match. Guards against 0×0 (minimize). The surface format is re-synced to the Renderer and Scene if it ever changes. (Done 2026-09-18; verified at runtime on the `cube` example.)
10. ✅ **Multi-lighting (Step 12, Phase 4.2)** — the scene now carries a global light list (directional + point) with a white ambient, uploaded into the per-frame `FrameUniforms` array each frame. `Scene::add_directional_light` / `add_point_light` / `set_ambient` / `clear_lights` configure it; `FrameUniforms::default()` (one white directional along +Z + white ambient) reproduces the pre-multi-light look exactly. The `standard` fragment accumulates ambient + all lights; the `cube` example adds a warm point light on top of the default directional. (Done 2026-09-18.)
11. ✅ **Spot lights (Step 13, Phase 4.2)** — spot lights (oriented cone + half-angle) added on top of the multi-lighting system. `Scene::add_spot_light(pos, dir, color, intensity, radius, half_angle)` registers a spot light; the `standard` fragment accumulates a spot term with a smoothed penumbra (half-angle ± 0.1 rad) and linear attenuation. `Light` grew from 48 to 64 bytes (added `dir_angle`); `FrameUniforms` from 576 to 704 bytes (added `num_spot`). Non-regression: default scene unchanged. The `cube` example adds a green spot light aimed at the cube. (Done 2026-09-18.)
12. ✅ **Shadows — shadow mapping (Step 14, Phase 4.2, optional)** — classic two-pass shadow mapping on a **single** light (directional or spot), selected by `Scene::set_shadow_caster(index)`. A depth-only pass (`shadow_shader.wgsl` + dedicated `shadow_pipeline`) renders the scene into a 1024² `Depth32Float` shadow map (`Renderer`-owned, slope-scaled depth bias); the `standard` fragment re-projects each fragment into light space and applies a **PCF 3×3** comparison-sampler test (bind group **@3**, shared). `FrameUniforms` grew from 704 to 784 bytes (`shadow_light_index`, `light_view_proj`, `shadow_params`). Shadows are **off by default** (`shadow_caster = None`) so `simple`/`cube`/`manual`/`spot_test` are unchanged. The `shadow_test` example casts a soft shadow from a cube onto a ground slab. (Done 2026-09-19.)
13. ✅ **Procedural primitive meshes (Step 15.A)** — `math::primitives` provides drop-in `Geometry` generators (`cube`, `plane`, `uv_sphere`, `icosphere`, `cylinder`, `cone`, `torus`) with positions + per-face/smooth normals + UVs + indices. Re-exported at `math::*`. The `cube` and `spot_test` examples now reuse `math::cube(1.0)` (the `cube_geometry` helper was factored away; `shadow_test` keeps its generic `box_geometry`). (Done 2026-09-20; 6 unit tests.)
14. ✅ **Unified input (Step 15.B)** — `core::input::InputState` gives cross-frame **pressed/held/released** semantics for keyboard (physical `KeyCode`) and mouse (buttons, position, per-frame delta, wheel scroll), rotated by `begin_frame`/`end_frame` around `AppHandler::update`. `App` exposes it as a public `input` field, fed from winit `WindowEvent`s and reset each frame. Gamepad is reserved/deferred (DRAFT D7). (Done 2026-09-20; 5 unit tests; winit event handling is host-driven on the CPU, not WGSL.)
15. ✅ **Orbital camera + final demo (Step 15.C)** — `resources::CameraController` (yaw/pitch/distance/target, `apply_to` writes into a `Camera`, drag-orbit + wheel-zoom + clamps) drives the new `demo` example: one of each primitive, procedural textures, standard Phong material, a shadow-casting directional light + point + spot, and live mouse-orbit / wheel-zoom / `R` reset / `1`/`2`/`3` view presets. Run with `cargo run -p wsg-lib --example demo`. (Done 2026-09-20; runtime-verified headless.)
16. ✅ **User documentation (Step 16, Phase 5)** — `docs/user/` (quickstart, meshes, materials, lights, shadows, camera & input, examples) written in English and cross-linked to each other, to the tech docs and to rustdoc; tech docs interlinked with their stale status banners refreshed; this README re-anchored (declarative workflow = recommended, manual = advanced, `demo` = showcase, pollster 1.x). (Done 2026-07-19.)
17. ✅ **GPU-driven rendering (Step 15, Phase 3.1/3.2/3.3)** — world matrices and indirect draw args move from CPU to GPU. `shaders/gpu_driven.wgsl` (two compute entry points, `compute_matrices` + `cull`, one module, explicit 3-group layout) runs before the render passes over a fixed 256-slot table (the world-matrix buffer is bound to the `uniform` object slot; WebGPU caps a `uniform` binding at 64 KB and a `uniform` offset at 256 B, so each matrix slot is padded to 256 B and 256 × 256 B = 64 KB is the max); `render_scene` and the shadow pass become **100 % indirect** (one indirect draw per active slot, culled/inactive slots are no-ops), and the per-entity CPU draw loop is gone. New `math::Frustum` (Gribb–Hartmann, WebGPU `[0,1]` z) + `BBox` on `Geometry`; `TransformSlot`/`MatSlot`/`BBoxSlot`/`DrawSlot`/`CullUniforms` Pod mirrors of the WGSL structs. Frustum **culling is off by default** (non-regression) and opt-in via `AppBuilder::with_culling(true)` / `Renderer::set_culling(bool)`; the `demo` enables it. The object bind-group layout is now dynamic so every entity shares one GPU matrix buffer via per-slot offsets. (Done 2026-09-22; WGSL + frustum + scene-slot tests, 57 lib / 3 WGSL / 3 doctests all green. **Culling fix 2026-09-22**: the WGSL `select` arguments had been written HLSL-style, silently zeroing the draw count of every *visible* entity — a black window; fixed and verified by GPU readback, see D14 in ARCHI_CPU_GPU.md.)
# Minimal : juste le cube
wsg-lib = { path = "../lib", default-features = false, features = ["prim-cube"] }
# Avec import OBJ
wsg-lib = { path = "../lib", features = ["import-obj"] }
```
| Feature | Active |
|---------|--------|
| `prim-cube`, `prim-plane`, `prim-sphere`, `prim-cylinder`, `prim-cone`, `prim-torus` | Primitives |
| `all-prims` (default) | Les 6 primitives |
| `import-obj` | Parser Wavefront OBJ |
| `import-gltf` | glTF (stub) |
## Build
```sh
cargo build --workspace # tout
cargo test --workspace # 116 tests
cargo check --all-targets # vérification rapide
cargo run -p wsg-lib --example demo # lancer le showcase
```
## Projet
- **Langage** : Rust 2024
- **Dépendances** : wgpu 30, winit 0.30, glam (math)
- **Pas publié sur crates.io** (dépendance par path)
- **Status** : MVP complet (phases 1-5 ✅), post-MVP en cours (phase 6)
---
*Documentation détaillée (architecture, status, API reference, workflow manuel) : [README_DETAILS.md](README_DETAILS.md)*