e8ff364d0d
- Light étendu à 4×Vec4 (64 o) : dir_angle (axe du cône + cos demi-angle) - FrameUniforms 576→704 o : compteur num_spot, _pad[1] - Lights + spot: Vec<Light> ; into_frame_array renvoie (arr, n_dir, n_point, n_spot) - Scene::add_spot_light(pos, dir, color, intensity, radius, half_angle) ; clear_lights inclut spot - standard_shader.wgsl : 3e boucle d'accumulation (pénombre lissée ±0.1 rad + atténuation linéaire) - exemple cube : lumière spot verte pointée vers le cube - Docs : shaders/resources README (704 B), README roadmap (item 11), ROADMAP (item coché) - Build/test/fmt OK (5 tests + validation WGSL + doctests) ; non-régression par défaut
191 lines
15 KiB
Markdown
191 lines
15 KiB
Markdown
# 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.
|
||
|
||
> **Status: unstable development version.** The manual workflow below is fully working, the high-level "declarative" workflow (automatic `App` scene rendering) works for flat/NDC drawing, and the **3D MVP is reached** : the `cube` example (Étape 5) renders a rotating, Phong-lit cube through `App::render_scene`. Since Étape 8, meshes are declared from a CPU `Geometry` (retained as `Arc<Geometry>` on the Mesh) instead of raw vertex arrays. The GPU-driven two-pass pipeline described in the architecture docs is **not implemented yet** — see [Status](#status) and [Roadmap](#roadmap).
|
||
|
||
## Status
|
||
|
||
| Area | State |
|
||
|------|-------|
|
||
| Manual workflow (`Context` + `Renderer` + `PipelineCache`) | ✅ Working |
|
||
| `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) | 📋 Roadmap — spec in [docs/tech/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** (Étape 5) : the `cube` example renders a rotating Phong-lit cube via the `standard` shader |
|
||
|
||
Note: the `standard_shader.wgsl` (Phong, with an explicit **unlit** mode) is now the **single** shader the library ships. The old `basic_shader.wgsl` was removed as a separate pipeline family (Étape 5) : flat 2D drawing is the unlit variant of `standard` (`Renderer::set_unlit(true)` or `app.renderer_mut().set_unlit(true)`, DRAFT « 2D ⊂ 3D »). See the `cube` example (3D, lit) and the `simple` example (2D, unlit).
|
||
|
||
## What it does
|
||
|
||
### Manual workflow (working — recommended today)
|
||
|
||
Bypass the `App` facade and drive `Context`, `Renderer` and `PipelineCache` yourself. This is the only workflow that renders pixels today (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).
|
||
// Étape 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 (Étape 8 : le mesh est construit depuis une `Geometry` — positions,
|
||
// attributs optionnels en builder, défauts blancs via `to_vertices`).
|
||
let material = Material::new(renderer.format(), "standard", &mut cache);
|
||
let geometry = Geometry::new(vec![
|
||
[-0.5, 0.5, 0.0], // Haut-Gauche
|
||
[ 0.5, 0.5, 0.0], // Haut-Droite
|
||
[ 0.5, -0.5, 0.0], // Bas-Droite
|
||
[-0.5, -0.5, 0.0], // Bas-Gauche
|
||
])
|
||
.with_colors(vec![
|
||
[1.0, 0.0, 0.0, 1.0], // Rouge
|
||
[0.0, 1.0, 0.0, 1.0], // Vert
|
||
[0.0, 0.0, 1.0, 1.0], // Bleu
|
||
[1.0, 1.0, 0.0, 1.0], // Jaune
|
||
])
|
||
.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();
|
||
}
|
||
```
|
||
|
||
### Declarative workflow (work in progress)
|
||
|
||
The intended API: register your scene's resources and entities once, then let `App` handle the window lifecycle, event processing and frame presentation. Users implement the `AppHandler` trait to inject per-frame logic:
|
||
|
||
```rust
|
||
use wsg_lib::app::AppBuilder;
|
||
use wsg_lib::{App, AppHandler};
|
||
|
||
struct MyGame;
|
||
|
||
impl AppHandler for MyGame {
|
||
// update() has an empty default — implement it to mutate scene state each frame.
|
||
// render(app, frame) has a default that draws the whole scene automatically via
|
||
// app.render_scene(frame.view()). You don't need to implement it for the common case.
|
||
}
|
||
|
||
#[pollster::main]
|
||
async fn main() -> Result<(), wsg_lib::utils::WsgError> {
|
||
let app = AppBuilder::new().build().await?;
|
||
|
||
// Register your scene once (string IDs), then App renders it automatically each frame.
|
||
// Since Étape 7 the Scene owns the pipeline cache: build materials/meshes through it and
|
||
// link the material to the mesh (no material_id on the entity anymore).
|
||
// Since Étape 8 meshes are declared from a `Geometry` (positions + optional attributes).
|
||
// app.renderer_mut().set_unlit(true); // select flat 2D rendering (optional)
|
||
// app.scene.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)?;
|
||
// app.scene.add_material_shader("mat", "standard")?; // build via the Scene's cache
|
||
// let geometry = wsg_lib::resources::Geometry::new(vec![[-0.5,0.5,0.0],[0.5,0.5,0.0]])
|
||
// .with_colors(vec![[1.0,0.0,0.0,1.0],[0.0,1.0,0.0,1.0]]);
|
||
// app.scene.create_mesh("quad", geometry, Some("mat"))?; // mesh links its Material
|
||
// app.scene.add_entity("my_quad", "quad")?;
|
||
|
||
app.run(MyGame)
|
||
}
|
||
```
|
||
|
||
> API note: `Scene::register_shader` / `add_material_shader` / `create_mesh` / `add_entity` and
|
||
> `PipelineCache::register_shader` currently return `Result<_, String>` — typed error unification
|
||
> is on the roadmap. Since Étape 8, `Scene::create_mesh(id, geometry, material)` takes a CPU
|
||
> `Geometry` (source of truth, retained on the Mesh) instead of raw `&[Vertex]`.
|
||
|
||
## 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, Étape 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 (Étape 8).
|
||
|
||
The planned target architecture — a GPU-driven two-pass pipeline (Compute Pass: world matrices + frustum culling → Indirect Draw Buffer, then a single `draw_indexed_indirect` per frame) — is 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) but is **not implemented yet**.
|
||
|
||
## 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 `update()` / `render()` callbacks | ✅ (`Frame::view()` exposed; default `render` draws the 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 | ✅ (Étape 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 (Étape 4.3) |
|
||
|
||
## Getting started
|
||
|
||
WSG is **not published on crates.io** — depend on it by path:
|
||
|
||
```toml
|
||
[dependencies]
|
||
wsg-lib = { path = "/path/to/wsg/lib" }
|
||
pollster = "0.4" # only if you use the async AppBuilder
|
||
```
|
||
|
||
| Action | Command |
|
||
|--------|---------|
|
||
| Build everything | `cargo build --workspace` |
|
||
| Run the 3D MVP example | `cargo run -p wsg-lib --example cube` |
|
||
| Run the working example | `cargo run -p wsg-lib --example manual` |
|
||
| Check everything (incl. examples) | `cargo check --all-targets` |
|
||
|
||
The `manual` example is the reference for the low-level workflow. The `simple` example (App facade) registers a colored quad and renders it automatically through the declarative path — it draws a scene without importing wgpu. The `cube` example (Étape 5) demonstrates the 3D MVP: a rotating Phong-lit cube, also through the declarative path and without importing wgpu.
|
||
|
||
## Documentation
|
||
|
||
The architecture docs live in `docs/tech/` and are written in **French**. Each document states whether it describes the **current** (implemented) state or the **target** (planned, not yet implemented) architecture:
|
||
|
||
- [ARCHI_APP](docs/tech/ARCHI_APP.md) — engine architecture. 🎯 **Target** — the GPU-driven two-pass pipeline parts are not implemented yet.
|
||
- [ARCHI_CPU_GPU](docs/tech/ARCHI_CPU_GPU.md) — CPU/GPU workload split specification. 🎯 **Target** — GPU-driven pipeline, ROADMAP Phase 3.
|
||
- [ARCHI_RENDU](docs/tech/ARCHI_RENDU.md) — update/render mutability model. 🎯 **Target** — model for the future scene auto-render.
|
||
- [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.
|
||
|
||
## Roadmap
|
||
|
||
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, single `draw_indexed_indirect` (see ARCHI_CPU_GPU).
|
||
3. **CPU→GPU transform sync** — persistent transform buffers with ring (triple) buffering.
|
||
4. ✅ **Real 3D pipeline (MVP atteint)** — MVP uniforms + camera support in the vertex shader. *(Engine plumbing done 2026-09-16 ; Étape 5, 2026-09-17 : `standard` branché sur l'exemple `cube` — un cube unitaire éclairé (Phong) qui tourne, rendu automatiquement par `App::render_scene`. Retrait de `basic` : le 2D plat = variante unlit de `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 (Étape 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 (Étape 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 (Étape 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 (Étape 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 (Étape 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.)
|