doc
This commit is contained in:
+10
-43
@@ -1,45 +1,12 @@
|
||||
# DRAFT — Étape suivante
|
||||
# Prochaine étape
|
||||
|
||||
> 📅 **Étape 15 — TERMINÉE (2026-09-20).** Document vidé conformément à la convention
|
||||
> (« ce document est vidé à la complétion de chaque étape »). Le plan détaillé (objectifs,
|
||||
> décisions D1–D9, détail d'implémentation, périmètre) est archivé dans l'historique git du
|
||||
> présent fichier — `git log -- docs/DRAFT.md`.
|
||||
> Étape 16 (Phase 5 — Documentation & Polish) **terminée** le 2026-07-19.
|
||||
>
|
||||
> Traduction anglaise de toute la documentation (hors `docs/tech/`, DRAFT/PLAN/ROADMAP) **terminée** le 2026-07-19 :
|
||||
> `docs/user/*`, `README.md`, READMEs de modules, doc/rustdoc de tous les `.rs` (src + examples + tests),
|
||||
> `Étape`→`Step` global. Vérifications : 50 tests OK, `cargo fmt` clean, aucun lien cassé, 0 accent restant hors zone franche.
|
||||
|
||||
## Bilan de l'Étape 15 (archive)
|
||||
|
||||
**Objectif atteint** : un démonstrateur `demo` interactif cumulant les étapes 8-14 — un sol texturé
|
||||
damier + 6 primitives (cube, sphère UV, icosphère, cylindre, cône, tore) texturées, éclairées
|
||||
(multi-lumières + spot, Étapes 12-13), projetant des **ombres** (Étape 14), et pilotées par une
|
||||
**caméra orbitale** au clavier/souris (glisser = orbite, molette = zoom, `R` = reset, `1`/`2`/`3` =
|
||||
présettes) via un nouveau **module d'input unifié** — le tout dans le workflow déclaratif
|
||||
`AppBuilder` + `AppHandler`, sans importer wgpu.
|
||||
|
||||
**Trois volets livrés :**
|
||||
- **15.A — `math::primitives`** : générateurs de `Geometry` prêts à l'emploi (`cube`, `plane`,
|
||||
`uv_sphere`, `icosphere`, `cylinder`, `cone`, `torus`) + 6 tests. Le `cube_geometry` des exemples
|
||||
(`cube.rs`, `spot_test.rs`) a été factorisé vers `math::cube(1.0)`.
|
||||
- **15.B — `core::input`** : `InputState` unifié (clavier `KeyCode` / souris position+delta+molette,
|
||||
sémantique pressed/held/released) branché dans `App` (`app.input` public, rotation
|
||||
`begin_frame`/`end_frame` autour de `update`) + 5 tests. Gamepad **reporté** (D7) — champ réservé.
|
||||
- **15.C — `demo` + caméra orbitale** : `resources::CameraController` (yaw/pitch/distance/target,
|
||||
`apply_to`) + exemple `demo` (sol + 6 primitives + lumières + ombres + caméra orbitale).
|
||||
|
||||
**Livraison** : ROADMAP 2.2 (primitifs) et 2.3 (input) cochées (gamepad `[~]` partiel) ; README items
|
||||
13-15 ajoutés ; `cargo fmt --all -- --check` propre ; build workspace + exemples ; 28 tests unitaires
|
||||
+ doctests au vert ; `demo` vérifié au runtime (headless).
|
||||
|
||||
**Corrections de bugs découvertes en cours de route :**
|
||||
- *Ombres du `demo`* : `set_shadow_caster(Some(0))` désignait la lumière directionnelle **+Z par
|
||||
défaut** préchargée par `Lights::new()` (index 0 packé) au lieu de la lumière chaude du demo
|
||||
(index 1) → la caméra d'ombre regardait −Z et des objets non-alignés s'occluaient mutuellement
|
||||
(tore/cône noircis). Corrigé en `Some(1)`.
|
||||
- *Tore noir* : le winding du tore (`primitives::torus`) était inversé (`[a, c, b]`), la face externe
|
||||
était cullée et seul l'intérieur (normales vers l'extérieur → N·L ≤ 0) restait visible. Corrigé en
|
||||
`[a, b, c]` + `[b, d, c]` (CCW vu de l'extérieur).
|
||||
- *`App::run`* : ajout d'un `device.poll()` par frame — sans lui, les callbacks asynchrones wgpu
|
||||
(`on_submitted_work_done`, `map_async`) ne firent jamais dans la boucle de production.
|
||||
- *Docs* : `STANDARD_SHADER_PATH` pointait vers un fichier absent (le vrai shader est embarqué via
|
||||
`include_str!`) ; le fallback est attendu et inoffensif — doc corrigée.
|
||||
|
||||
**Hors périmètre (reporté)** : `set_active_camera` multi-caméras (2.1), `InputUniforms` WGSL
|
||||
(debug/gizmo), GPU-driven (Phase 3), batching matériau (4.3), LOD, HDR/tone mapping, gamepad complet.
|
||||
## Prochaines options
|
||||
- **Phase 3 — GPU-driven rendering** (ROADMAP 3.1/3.2/3.3) : indirect draw, buffers de paramètres GPU, culling GPU. C'est le gros morceau performance qui reste.
|
||||
- **Phase 4.4 — Performance** : LOD, instancing/multi-instancing, occlusion culling, batching par matériau (4.3).
|
||||
- Multi-caméras (`scene.set_active_camera`) — restant de la Phase 2.1.
|
||||
|
||||
+5
-5
@@ -102,7 +102,7 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
|
||||
### 2.1 Camera dans la Scene
|
||||
- [x] Intégrer `Camera` comme ressource de la Scene (Étape 4.3 : `Scene::set_camera` / `camera()`, caméra active unique)
|
||||
- [ ] Permettre plusieurs caméras (actuelle/inactive) et une sélection par identifiant (`scene.set_active_camera(camera_id)`)
|
||||
- [ ] Exposer une caméra orbitale contrôlable (exemple final, Phase 5)
|
||||
- [x] Exposer une caméra orbitale contrôlable (exemple final, Phase 5) — *(Étape 15.C, 2026-09-20 : `CameraController` orbitale pilotée par l'input unifié, branchée sur l'exemple `demo`)*
|
||||
|
||||
### 2.2 Meshes primitifs (bibliothèque procédurale, WSGL)
|
||||
- [x] Module `math::primitives` générant des `Geometry` prêts à l'emploi (positions + normales + UVs + indices) : `cube`, `plane`, `uv_sphere`, `icosphere`, `cylinder`, `cone` (et `torus` en bonus) — *(Étape 15.A, 2026-09-20 : implémenté, commit `4da89c7`)*
|
||||
@@ -181,10 +181,10 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
|
||||
|
||||
## Phase 5️⃣ — Documentation & Polish
|
||||
|
||||
- [ ] Exemple complet : mesh texturé, éclairé, avec caméra orbitale
|
||||
- [ ] Documentation API (`docs/ARCHI_SCENE.md`)
|
||||
- [ ] Tests unitaires : `Geometry`, `Scene`, `Transform`
|
||||
- [ ] README mis à jour avec les nouvelles fonctionnalités
|
||||
- [x] Exemple complet : mesh texturé, éclairé, avec caméra orbitale — *(Étape 15, 2026-09-20 : exemple `demo` — les 7 primitives, textures procédurales, 3 lumières, ombre portée, caméra orbitale live ; vérifié headless)*
|
||||
- [x] Documentation API — *(Étape 16, 2026-07-19 : guide utilisateur `docs/user/` en français (8 pages interconnectées) + rustdoc complet sur toute l'API publique ; le `ARCHI_SCENE.md` séparé prévu est remplacé par les pages `docs/user/` + rustdoc — DRAFT Étape 16, décision D3)*
|
||||
- [x] Tests unitaires : `Geometry`, `Scene`, `Transform` — *(Étape 16 : modules de tests ajoutés à `math/geometry.rs`, `math/transform.rs`, `scene/scene.rs` ; 28 tests unitaires + doctests au total, `cargo test --workspace` vert)*
|
||||
- [x] README mis à jour avec les nouvelles fonctionnalités — *(Étape 16 : README racine re-ancré — workflow déclaratif = recommandé, manuel = avancé, `demo` = showcase, pollster 1.x ; README de modules `lib/src/**` à jour ; liens tech docs interconnectés)*
|
||||
|
||||
---
|
||||
|
||||
|
||||
+15
-6
@@ -15,14 +15,17 @@ stale_after: 2027-01-31
|
||||
|
||||
wsg_lib est un moteur de rendu modulaire basé sur wgpu. Il adopte une architecture à deux niveaux : une façade de haut niveau pour la productivité et un accès bas niveau pour un contrôle total.
|
||||
|
||||
> **État du document : CIBLE (architecture visée, en grande partie non implémentée).**
|
||||
> **État du document : ACTUEL pour la façade (`App`/`AppHandler`, §3, §4A) ; CIBLE pour la partie
|
||||
> GPU-driven (§1, §4B, §5, §6).** La façade `AppBuilder`/`App`/`AppHandler` est livrée et est le
|
||||
> **workflow recommandé** : `setup` (déclaration de la scène) → par frame `update` (mutation) →
|
||||
> `render` (défaut : `App::render_scene` = itération des entités + **rendu groupé en une passe**,
|
||||
> un `CommandEncoder`/soumission par frame ; passe d'ombre en tête si un caster est actif).
|
||||
> Exemples : `simple` (2D unlit), `cube` (3D éclairé), `demo` (vitrine : primitives, lumières,
|
||||
> ombres, caméra orbitale). Le workflow **manuel** (exemple `manual`) coexiste pour le contrôle fin.
|
||||
> Les sections §1, §4B, §5 et §6 décrivent la **cible** : pipeline GPU-driven à deux passes
|
||||
> (Compute Pass → `draw_indexed_indirect`), buffers persistants en VRAM (Transform/Matrix/BBox/Indirect)
|
||||
> et synchronisation single/double buffer. **Rien de tout cela n'existe encore dans le code** — c'est
|
||||
> la trajectoire de ROADMAP.md (et README étape 2-3). L'état **réel actuel** est dans README.md :
|
||||
> workflow manuel uniquement, `Renderer` dessine un objet par soumission, shader en NDC sans MVP.
|
||||
> La §3 (`App`/`AppHandler`) correspond à l'état actuel, à une nuance près : `render()` ne peut pas
|
||||
> encore dessiner la scène (l'acquisition/présentation de frame fonctionne, pas le rendu de la scène).
|
||||
> la trajectoire ROADMAP Phase 3.
|
||||
|
||||
## 1. Philosophie et Principes
|
||||
|
||||
@@ -85,7 +88,7 @@ pub trait AppHandler {
|
||||
|
||||
- **Shaders** : Chargés avant la renderloop.
|
||||
- **PipelineCache** : Enregistre les shaders.
|
||||
- **Matériaux** : Créés avec un shader associé. Un mesh sans matériau explicite utilise `basic_shader` par défaut.
|
||||
- **Matériaux** : Créés avec un shader associé. Un mesh sans matériau explicite utilise le matériau par défaut de la scène (`standard`).
|
||||
- **Scene** : Assemblage des objets. L'utilisateur peuple la scène via `app.scene`.
|
||||
|
||||
### B. Boucle de Rendu — Pipeline GPU-Driven
|
||||
@@ -155,3 +158,9 @@ Single buffer (phase initiale) : Update écrit, Compute lit au frame suivant —
|
||||
- **Synchronisation** : Toujours appeler `begin_compute_pass` avant `begin_render_pass` sur le même `CommandEncoder`. Les barrières entre passes sont automatiques — ne jamais insérer de barrière manuelle sauf besoin critique.
|
||||
- **Synchronisation single buffer (phase initiale)** : La séquence `queue.submit()` après chaque compute pass garantit que les données Transform sont valides avant le render pass suivant. Aucun conflit de lecture/écriture n'est possible tant que `desired_maximum_frame_latency` ≥ 3.
|
||||
- **Double Buffering (future migration)** : Sera implémenté sur les buffers Transform et Matrix seulement, pas sur BoundingBox ni Indirect Draw. Le switch se résume à : dupliquer ces deux buffers, ajouter une méthode `swap()` appelée dans `AboutToWait`, modifier les bind groups pour pointer vers l'index courant. Pas besoin de refonte architecturale.
|
||||
|
||||
## Liens
|
||||
|
||||
- [ARCHI_RENDU](ARCHI_RENDU.md) · [FRAME_LOOP](FRAME_LOOP.md) · [ARCHI_CPU_GPU](ARCHI_CPU_GPU.md) · [ARCHI_ARENES](ARCHI_ARENES.md)
|
||||
- Documentation utilisateur : [docs/user](../user/README.md) · [README racine](../../README.md) · [ROADMAP](../ROADMAP.md)
|
||||
- Référence API : `cargo doc -p wsg-lib --no-deps`
|
||||
|
||||
@@ -220,7 +220,7 @@ impl ResourceManager {
|
||||
6. Suppression Dynamique : Bien que possible, la suppression de ressources pendant la boucle de rendu doit être faite avec prudence. Assurez-vous que les entités ou objets qui référençaient cette ressource soient informés ou nettoyés pour éviter d'utiliser des Handles invalides. La suppression est souvent mieux gérée en fin de frame ou via un système de "marquage pour suppression" suivi d'un nettoyage différé.
|
||||
7. Futur : SecondaryMaps : slotmap permet d'utiliser des SecondaryMap pour associer dynamiquement des données à des ressources existantes sans modifier leur structure principale. Par exemple, `SecondaryMap<MeshId, Transform>` pourrait stocker les transformations actuelles de chaque maillage. Cela peut être utile pour le rendu ou pour des systèmes de physique/transformation indépendants.
|
||||
|
||||
> **Note sur les Transforms côté GPU** : `SecondaryMap<MeshId, Transform>` est une suggestion d'approche générale. Si le modèle le plus performant pour votre cas d'usage est plutôt un vecteur/plat de Transforms (`Vec<Transform>`) alimentant un Storage Buffer CPU → GPU (comme décrit dans [ARCHI_CPU_GPU](ARCHI_CPU_GPU)), alors c'est cette approche qu'il faut adopter. Comme toutes les ressources sont créées avant le début de la boucle de rendu, vous pouvez décider à ce moment-là du meilleur modèle de stockage — en fonction du volume de meshes et de la fréquence de mise à jour des transforms.
|
||||
> **Note sur les Transforms côté GPU** : `SecondaryMap<MeshId, Transform>` est une suggestion d'approche générale. Si le modèle le plus performant pour votre cas d'usage est plutôt un vecteur/plat de Transforms (`Vec<Transform>`) alimentant un Storage Buffer CPU → GPU (comme décrit dans [ARCHI_CPU_GPU](ARCHI_CPU_GPU.md)), alors c'est cette approche qu'il faut adopter. Comme toutes les ressources sont créées avant le début de la boucle de rendu, vous pouvez décider à ce moment-là du meilleur modèle de stockage — en fonction du volume de meshes et de la fréquence de mise à jour des transforms.
|
||||
|
||||
# Avantages de cette Approche
|
||||
|
||||
@@ -230,3 +230,9 @@ impl ResourceManager {
|
||||
* Conformité avec Rust : Respecte les principes de propriété et de sécurité mémoire de Rust sans recourir à Rc<RefCell<T>> ou d'autres constructions potentiellement coûteuses ou moins sûres pour la gestion partagée des ressources.
|
||||
* Typage Fort : Les types MeshId, MaterialId, etc., empêchent les erreurs de compilation liées au mélange de Handles de types différents.
|
||||
* Extensibilité : L'écosystème slotmap (SecondaryMap) offre des perspectives pour des architectures plus complexes à l'avenir.
|
||||
|
||||
## Liens
|
||||
|
||||
- [ARCHI_APP](ARCHI_APP.md) · [ARCHI_RENDU](ARCHI_RENDU.md) · [ARCHI_CPU_GPU](ARCHI_CPU_GPU.md) · [FRAME_LOOP](FRAME_LOOP.md)
|
||||
- Documentation utilisateur : [docs/user](../user/README.md) · [README racine](../../README.md) · [ROADMAP](../ROADMAP.md)
|
||||
- Référence API : `cargo doc -p wsg-lib --no-deps`
|
||||
|
||||
@@ -72,3 +72,9 @@ Transform Buffer,Stocke les positions/rotations/échelles brutes.,Storage Buffer
|
||||
Matrix Buffer,Stocke les World Matrices finales calculées.,Storage Buffer,GPU (Calculé) → GPU (Lu par le Render)
|
||||
Bounding Box Buffer,Stocke les AABB de chaque mesh pour le culling.,Storage Buffer,CPU → GPU (Statique)
|
||||
Indirect Draw Buffer,Contient la liste dynamique des objets à dessiner.,Indirect Buffer + Storage,GPU (Rempli par Compute) → GPU (Lu par Render)
|
||||
|
||||
## Liens
|
||||
|
||||
- [ARCHI_APP](ARCHI_APP.md) · [ARCHI_RENDU](ARCHI_RENDU.md) · [ARCHI_ARENES](ARCHI_ARENES.md) · [FRAME_LOOP](FRAME_LOOP.md)
|
||||
- Documentation utilisateur : [docs/user](../user/README.md) · [README racine](../../README.md) · [ROADMAP](../ROADMAP.md)
|
||||
- Référence API : `cargo doc -p wsg-lib --no-deps`
|
||||
|
||||
@@ -15,13 +15,15 @@ stale_after: 2027-01-31
|
||||
|
||||
Ce document définit la stratégie de gestion de la mutabilité et des données du moteur wsg_lib, conçue pour maximiser la performance et garantir la sécurité mémoire via Rust.
|
||||
|
||||
> **État du document : CIBLE (modèle de mutabilité pour le futur rendu automatisé).**
|
||||
> Le cycle update/render strict, l'itération **automatique** des entités et `renderer.render_scene()`
|
||||
> décrits ici ne sont **pas implémentés** : c'est l'**étape 1 du Roadmap README** (scene auto-rendering).
|
||||
> Aujourd'hui `App::run` acquiert/présente la frame mais `render()` ne peut pas encore dessiner la scène,
|
||||
> et le `Renderer` ne dessine qu'un objet par soumission, à la main (exemple `manual`). La terminologie
|
||||
> `MeshId`/`MaterialId` (handles typés) est celle de la **cible** ; l'état actuel utilise des **String IDs**
|
||||
> dans `Scene`. La dichotomie update/render reste toutefois le modèle de référence retenu pour la suite.
|
||||
> **État du document : ACTUEL pour la dichotomie update/render (implémentée) ; CIBLE pour le batching.**
|
||||
> Le cycle strict est en place : `AppHandler::update` (mutation libre de la scène) tourne avant
|
||||
> `AppHandler::render`, dont l'implémentation par défaut appelle `app.render_scene(frame.view())` —
|
||||
> le moteur itère automatiquement les entités et les dessine en **une passe groupée** par frame
|
||||
> (rendu automatisé livré le 2026-09-16 ; la passe d'ombre est ajoutée en tête quand un caster est
|
||||
> actif). Le workflow **manuel** (`Renderer::render` objet par objet, exemple `manual`) coexiste
|
||||
> pour le contrôle fin. Reste en **cible** : le **tri/batching par matériau** (ROADMAP 4.3) et les
|
||||
> **handles typés** `MeshId`/`MaterialId` (voir [ARCHI_ARENES](ARCHI_ARENES.md)) — l'état actuel
|
||||
> utilise des **String IDs** dans `Scene`.
|
||||
|
||||
## 1. La Dichotomie Update / Render
|
||||
|
||||
@@ -65,4 +67,14 @@ Bien que cette architecture facilite la gestion de la mémoire, des règles stri
|
||||
|
||||
> "Si vous devez changer la position d'un objet ou son matériau, faites-le dans `update()`. Si vous avez besoin d'afficher un élément de debug ou un rendu spécial, faites-le dans `render()`, mais traitez les objets de la scène comme des données en lecture seule."
|
||||
|
||||
Cette structure permet au projet d'être extrêmement scalable. L'ajout futur de fonctionnalités (Lumières, Textures, Caméras) ne nécessitera que d'ajouter de nouveaux conteneurs dans la Scene et de mettre à jour le système de tri dans `Renderer::render_scene()` (méthode à créer — cible de l'étape 1 du Roadmap README).
|
||||
Cette structure permet au projet d'être extrêmement scalable. L'ajout des fonctionnalités Lumières,
|
||||
Textures et Caméras (livrées — voir [ROADMAP](../ROADMAP.md)) a effectivement consisté à ajouter des
|
||||
conteneurs dans la Scene (`lights`, `textures`, `camera`) et à les consommer dans
|
||||
`Renderer::render_scene()` (existant — il écrit les uniformes de frame chaque frame). Il restera à
|
||||
y ajouter le **système de tri par matériau** (batching, ROADMAP 4.3) quand il sera justifié.
|
||||
|
||||
## Liens
|
||||
|
||||
- [ARCHI_APP](ARCHI_APP.md) · [FRAME_LOOP](FRAME_LOOP.md) · [ARCHI_CPU_GPU](ARCHI_CPU_GPU.md) · [ARCHI_ARENES](ARCHI_ARENES.md)
|
||||
- Documentation utilisateur : [docs/user](../user/README.md) · [README racine](../../README.md) · [ROADMAP](../ROADMAP.md)
|
||||
- Référence API : `cargo doc -p wsg-lib --no-deps`
|
||||
|
||||
+23
-4
@@ -14,12 +14,25 @@ stale_after: 2027-01-31
|
||||
# La Boucle de Rendu (Frame Loop)
|
||||
|
||||
> **État du document : ACTUEL (implémenté).** Ce document décrit la frame lifetime telle qu'elle est
|
||||
> réellement implémentée. Il concerne le rendu **CPU-piloté actuel** (objet par objet, exemple `manual`).
|
||||
> Le pipeline GPU-driven de l'état **visé** est décrit dans ARCHI_APP.md / ARCHI_CPU_GPU.md (cible).
|
||||
> réellement implémentée. **Deux flux coexistent** : le flux **facade `App`** (rendu automatique de la
|
||||
> scène, `App::render_scene` — le workflow recommandé, exemples `simple`/`cube`/`demo`) et le flux
|
||||
> **manuel** (`Context`/`Renderer`/`Frame` pilotés à la main — exemple `manual`, un objet par soumission).
|
||||
> Le pipeline **GPU-driven** (compute pass + draw indirect) de l'état **visé** est décrit dans
|
||||
> [ARCHI_APP](ARCHI_APP.md) / [ARCHI_CPU_GPU](ARCHI_CPU_GPU.md) (cible).
|
||||
|
||||
Pour afficher quelque chose, nous suivons un cycle immuable appelé la Frame Lifetime. Deux flux coexistent, tous deux basés sur `Frame` :
|
||||
Pour afficher quelque chose, nous suivons un cycle immuable appelé la Frame Lifetime, basé sur `Frame` :
|
||||
|
||||
**Flux `Frame` (utilisé par `App::run` et l'exemple `manual`) :**
|
||||
**Flux facade `App` (recommandé — `App::run` + `AppHandler`) :**
|
||||
- **`Context::get_next_frame()`** : acquiert la surface texture et crée sa `TextureView` (dans `Frame`).
|
||||
- **`AppHandler::render` (défaut) → `App::render_scene(view)`** : le moteur itère les entités de la
|
||||
scène et les dessine en **une passe groupée** (un `CommandEncoder` + une soumission par frame ;
|
||||
passe d'ombre en tête si un caster est actif).
|
||||
- **`Renderer::present(frame)`** : présente l'image à l'écran.
|
||||
- Chaque frame, avant `update`, le moteur appelle `device.poll()` (les callbacks asynchrones wgpu —
|
||||
`on_submitted_work_done`, `map_async` — ne se déclenchent que lors d'un poll), et la fenêtre
|
||||
redimensionnée est gérée par `App::resize` (surface + depth texture recréées ensemble).
|
||||
|
||||
**Flux `manual` (exemple `manual` — un objet par soumission) :**
|
||||
- **`Context::get_next_frame()`** (ou `Frame::try_new(&context.surface)`) : acquiert la surface texture et crée sa `TextureView` (dans `Frame`).
|
||||
- **`Renderer::render(&view, &mesh, &material)`** : crée un `CommandEncoder`, écrit les ordres de dessin dans la `TextureView`, puis soumet à la file (`queue`).
|
||||
- **`Renderer::present(frame)`** : présente l'image à l'écran.
|
||||
@@ -28,6 +41,12 @@ Pour afficher quelque chose, nous suivons un cycle immuable appelé la Frame Lif
|
||||
- **`Context::begin_frame()`** : acquiert la surface et renvoie la `wgpu::SurfaceTexture` (sans vue).
|
||||
- **`Context::end_frame(surface_texture)`** : soumet et présente cette texture.
|
||||
|
||||
## Liens
|
||||
|
||||
- [ARCHI_APP](ARCHI_APP.md) · [ARCHI_RENDU](ARCHI_RENDU.md) · [ARCHI_CPU_GPU](ARCHI_CPU_GPU.md) · [ARCHI_ARENES](ARCHI_ARENES.md)
|
||||
- Documentation utilisateur : [docs/user](../user/README.md) · [README racine](../../README.md) · [ROADMAP](../ROADMAP.md)
|
||||
- Référence API : `cargo doc -p wsg-lib --no-deps`
|
||||
|
||||
---
|
||||
|
||||
## Pourquoi cette séparation est vitale
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# User documentation — WSG
|
||||
|
||||
**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`).
|
||||
|
||||
## 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:
|
||||
|
||||
| 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 |
|
||||
| [Camera & input](camera-input.md) | Active camera, orbital controller, unified keyboard/mouse state |
|
||||
| [Examples](examples.md) | The 7 repo examples, the advanced `manual` workflow, adding your own example |
|
||||
|
||||
The pages are cross-linked: each page ends with a link to the next one.
|
||||
|
||||
## 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`
|
||||
@@ -0,0 +1,110 @@
|
||||
# Camera & input
|
||||
|
||||
Two bricks drive the viewpoint: the scene's **active `Camera`** (view/projection matrices
|
||||
built every frame) and the unified **`InputState`** (keyboard/mouse, cross-frame
|
||||
semantics). The orbital **`CameraController`** bridges the two.
|
||||
|
||||
## 1. The active camera
|
||||
|
||||
The scene holds a single camera, read by the engine every frame to write the view/projection
|
||||
matrices into the frame buffer (aspect recomputed from the window size).
|
||||
|
||||
```rust
|
||||
use wsg_lib::resources::Camera;
|
||||
use glam::Vec3;
|
||||
|
||||
app.scene.set_camera(Camera::new(
|
||||
Vec3::new(3.0, 2.0, 3.0), // eye position
|
||||
Vec3::ZERO, // target point
|
||||
Vec3::Y, // "up" vector
|
||||
));
|
||||
```
|
||||
|
||||
- **Default**: position `(0, 0, 3)`, looking at the origin, 45° vertical fov, near 0.1,
|
||||
far 100 — frames a unit cube with no tuning.
|
||||
- `Camera::with_perspective(fov, near, far)` adjusts the projection (fov in radians).
|
||||
- Read: `app.scene.camera()`; direct mutation: `app.scene.camera_mut()`.
|
||||
- The `up` field matters: the orbital camera forces it to `+Y` (level horizon).
|
||||
|
||||
> The matrices use the **WebGPU** convention (NDC depth `[0,1]`) — do not replace
|
||||
> `projection_matrix` with an OpenGL `[-1,1]` projection, the near part of the frustum would
|
||||
> be clipped.
|
||||
|
||||
## 2. The orbital controller
|
||||
|
||||
`CameraController` represents the viewpoint in spherical coordinates around a target:
|
||||
`yaw` (azimuth around +Y), `pitch` (elevation, bounded to ±~83°), `distance` (radius,
|
||||
bounded to `[0.1, 100]`), `target` (target point).
|
||||
|
||||
```rust
|
||||
use wsg_lib::resources::CameraController;
|
||||
|
||||
let mut ctrl = CameraController::default(); // target at origin, distance 3, front view
|
||||
ctrl.orbit(dx, dy); // mouse drag: yaw/pitch (bounded pitch, no poles)
|
||||
ctrl.zoom(scroll_y); // wheel: zoom (positive scroll = move closer)
|
||||
ctrl.reset(); // back to the default framing
|
||||
ctrl.apply_to(app.scene.camera_mut()); // write the framing into the active camera (do this EVERY frame)
|
||||
```
|
||||
|
||||
`CameraController::from_camera(&cam)` rebuilds a controller from an existing camera
|
||||
(useful to start the orbit from a manual framing).
|
||||
|
||||
The exact wiring snippet (orbit + zoom + reset + `1`/`2`/`3` presets, driven from
|
||||
`app.input`) is in [`demo.rs`](../../lib/examples/demo.rs), `update()` section.
|
||||
|
||||
## 3. The unified input state
|
||||
|
||||
`app.input` (public field of `App`) is fed by winit events and **rotated** automatically
|
||||
every frame (`begin_frame`/`end_frame` around your `update`). Three semantics per control:
|
||||
|
||||
| Semantics | Methods | Meaning |
|
||||
|------------|----------|---------|
|
||||
| **pressed** | `key_pressed(code)`, `mouse_button_pressed(btn)` | true **only** on the frame the key/button was just pressed |
|
||||
| **held** | `key_held(code)`, `mouse_button_held(btn)` | true while the key/button stays down |
|
||||
| **released** | `key_released(code)`, `mouse_button_released(btn)` | true **only** on the release frame |
|
||||
|
||||
Plus: `mouse_position() -> (f32, f32)`, `mouse_delta() -> (f32, f32)` (accumulated over the
|
||||
frame, reset between frames), `scroll_delta() -> (f32, f32)` (wheel).
|
||||
|
||||
`KeyCode` values are winit's physical codes (`winit::keyboard::KeyCode`); mouse buttons are
|
||||
`winit::event::MouseButton`. The library does not re-export them: if your code mentions
|
||||
them, add `winit = "0.30"` to your own dependencies (as the examples do). Input-less
|
||||
applications (like `simple`/`cube`) don't need winit: `app.input` remains usable, only
|
||||
`KeyCode` comparisons require the import.
|
||||
|
||||
```rust
|
||||
use winit::keyboard::KeyCode;
|
||||
|
||||
fn update(&mut self, app: &mut wsg_lib::App) {
|
||||
// Orbit + zoom driven by the mouse (excerpts from demo):
|
||||
let (dx, dy) = app.input.mouse_delta();
|
||||
self.camera.orbit(dx, dy);
|
||||
let (_, sy) = app.input.scroll_delta();
|
||||
self.camera.zoom(sy);
|
||||
|
||||
// R: reset — key_pressed fires once, not on key-repeat.
|
||||
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());
|
||||
}
|
||||
```
|
||||
|
||||
> **Gamepad**: the API is reserved (`InputState` will pass through `DeviceEvent`s) but not
|
||||
> implemented yet — deferred, see [ROADMAP](../ROADMAP.md).
|
||||
|
||||
## 4. Common recipes
|
||||
|
||||
| Need | Recipe |
|
||||
|--------|--------|
|
||||
| Standard orbital camera | `CameraController` + `mouse_delta`/`scroll_delta` (snippet above) |
|
||||
| FPS camera (WASD) | `key_held(KeyCode::KeyW)` in `update` → move `camera.position`/`target`; override `render()` if needed |
|
||||
| Changing the orbit target | `ctrl.target = subject_position;` (following an object) |
|
||||
| View presets | `key_pressed(Digit1/2/3)` → write yaw/pitch/distance (from the `demo`) |
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](README.md) · [Lights](lights.md) · [Examples](examples.md)
|
||||
- [Root README](../../README.md) · [ARCHI_APP](../tech/ARCHI_APP.md)
|
||||
@@ -0,0 +1,47 @@
|
||||
# Examples
|
||||
|
||||
Seven examples live in [`lib/examples/`](../../lib/examples/) and all launch with
|
||||
`cargo run -p wsg-lib --example <name>`. They are **self-contained**: no assets on disk
|
||||
(procedural textures, hard-coded geometries).
|
||||
|
||||
| Example | Command | What it shows | Corresponding page |
|
||||
|---------|----------|---------------|--------------------|
|
||||
| `simple` | `cargo run -p wsg-lib --example 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) |
|
||||
| `cube` | `cargo run -p wsg-lib --example cube` | The 3D MVP: a textured (checkerboard) cube, lit (directional + point + spot), spinning | [Meshes](meshes.md), [Materials](materials.md), [Lights](lights.md) |
|
||||
| `demo` | `cargo run -p wsg-lib --example demo` | The full showcase: ground + 6 primitives, textures, 3 lights, **shadows**, **orbital camera** on keyboard/mouse (drag = orbit, wheel = zoom, `R` = reset, `1`/`2`/`3` = presets) | [All pages](README.md) |
|
||||
| `shadow_test` | `cargo run -p wsg-lib --example 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) |
|
||||
| `spot_test` | `cargo run -p wsg-lib --example spot_test` | Isolated spot (ambient nearly zero): the directed beam, the penumbra, the attenuation | [Lights](lights.md) |
|
||||
| `manual` | `cargo run -p wsg-lib --example manual` | The **advanced** workflow: `Context`/`Renderer`/`PipelineCache` driven by hand, without the `App` facade (winit 0.30 `ApplicationHandler`) | below |
|
||||
|
||||
## The `manual` workflow (advanced)
|
||||
|
||||
When the `App` facade doesn't fit (fine-grained loop control, integration into an existing
|
||||
framework, experimentation), you bypass `App` and drive directly:
|
||||
|
||||
- `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)`.
|
||||
|
||||
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/manual.rs); the two-layer architecture is detailed in
|
||||
[ARCHI_APP](../tech/ARCHI_APP.md) and [FRAME_LOOP](../tech/FRAME_LOOP.md).
|
||||
|
||||
> **Tip**: start with the declarative workflow. The manual workflow doesn't render more
|
||||
> pixels — it gives more control over command encoding.
|
||||
|
||||
## Adding your own example
|
||||
|
||||
Repo conventions (see `lib/examples/README.md`):
|
||||
|
||||
1. Create `lib/examples/my_example.rs` (Cargo discovers it automatically).
|
||||
2. Keep it **self-contained**: procedural textures, hard-coded geometries, no external assets.
|
||||
3. Use the declarative workflow (`AppBuilder` + `Scene`) when possible.
|
||||
4. Document the example in `lib/examples/README.md` (and here, `docs/user/examples.md`).
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](README.md) · [Quickstart](quickstart.md) · [Camera & input](camera-input.md)
|
||||
- [Root README](../../README.md) · [ROADMAP](../ROADMAP.md)
|
||||
@@ -0,0 +1,84 @@
|
||||
# Lights
|
||||
|
||||
Lights are **scene-global**: a single list is packed into the frame uniforms every frame, and
|
||||
**all** entities receive their lighting (per-material lights are out of the current scope).
|
||||
|
||||
## Model
|
||||
|
||||
- Bounded capacity: **`MAX_LIGHTS = 8`** lights in total (directional + point + spot
|
||||
combined). Adding beyond that returns an error.
|
||||
- **Default**: one white directional light along **+Z** (from the surface point toward the
|
||||
light) + white ambient. This default exactly reproduces the historical single-light
|
||||
rendering — your scene "just works" with no configuration.
|
||||
- Ambient (`set_ambient`) is a global hemispherical term, independent of the lights.
|
||||
|
||||
## Adding lights
|
||||
|
||||
```rust
|
||||
use glam::Vec3;
|
||||
|
||||
// Directional: `dir` points FROM the surface point TOWARD the light.
|
||||
app.scene
|
||||
.add_directional_light(Vec3::new(1.0, 1.2, 1.0).normalize(), [1.0, 0.98, 0.92], 1.5)
|
||||
.unwrap();
|
||||
|
||||
// Point: world position, tint, intensity, attenuation radius (linear down to 0).
|
||||
app.scene
|
||||
.add_point_light(Vec3::new(0.5, 1.6, 1.8), [1.0, 0.7, 0.3], 1.2, 6.0)
|
||||
.unwrap();
|
||||
|
||||
// Spot: position, cone axis (FROM the light TOWARD the scene), tint, intensity, radius,
|
||||
// half-angle in radians (penumbra smoothed at the edge).
|
||||
app.scene.add_spot_light(
|
||||
Vec3::new(-2.5, 2.2, 1.0), // position
|
||||
Vec3::new(2.5, -2.2, -1.0).normalize(), // axis, toward the scene
|
||||
[0.3, 1.0, 0.5], // green tint
|
||||
1.4, 8.0, 0.45, // intensity, radius, half-angle (~26°)
|
||||
).unwrap();
|
||||
```
|
||||
|
||||
These three calls are the ones in the [`demo`](../../lib/examples/demo.rs) example;
|
||||
[`cube.rs`](../../lib/examples/cube.rs) shows a point + a spot on top of the default
|
||||
directional, and [`spot_test.rs`](../../lib/examples/spot_test.rs) isolates a single spot
|
||||
(ambient nearly zero).
|
||||
|
||||
Global settings:
|
||||
|
||||
| Method | Effect |
|
||||
|---------|--------|
|
||||
| `set_ambient([r, g, b])` | hemispherical ambient color (default white) |
|
||||
| `clear_lights()` | empties the list — only ambient will light the scene (useful for a flat look without switching to unlit) |
|
||||
| `set_lights(Lights)` | replaces the whole list (batch reset) |
|
||||
| `lights()` | reads the current list |
|
||||
|
||||
## ⚠️ Packed indices (important for shadows)
|
||||
|
||||
Lights are stacked in the GPU array **by type, in order**:
|
||||
|
||||
```
|
||||
index 0 .. n_dir-1 : directional
|
||||
index n_dir .. +n_point-1 : point
|
||||
index … .. +n_spot-1 : spot
|
||||
```
|
||||
|
||||
Two consequences:
|
||||
|
||||
1. **Index 0 is the default +Z directional** (the one `Lights::new()` pre-loads),
|
||||
not your first added light. This is a classic pitfall — see
|
||||
[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/shadow_test.rs)).
|
||||
|
||||
## Intensities and tints
|
||||
|
||||
- `color` is an RGB in `[0..1]`; `intensity` is an unbounded multiplier.
|
||||
- Local lights (point/spot) attenuate **linearly** — intensity drops to zero at `radius`.
|
||||
Beyond the radius, the light contributes nothing.
|
||||
- The `standard` shader accumulates ambient + all lights (no mutual occlusion between
|
||||
lights; the spot cone culling happens at the fragment).
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](README.md) · [Shadows](shadows.md) · [Materials & textures](materials.md)
|
||||
- [Root README](../../README.md) · [ARCHI_RENDU](../tech/ARCHI_RENDU.md)
|
||||
@@ -0,0 +1,100 @@
|
||||
# Materials & textures
|
||||
|
||||
A **`Material`** describes a mesh's appearance: it references a shader (by id) and
|
||||
optionally a **diffuse texture**. Several materials pointing at the same shader share the
|
||||
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.
|
||||
|
||||
## 1. Registering the shader
|
||||
|
||||
```rust
|
||||
app.scene
|
||||
.register_shader("standard", wsg_lib::utils::STANDARD_SHADER_PATH)
|
||||
.unwrap();
|
||||
```
|
||||
|
||||
> **Note**: `STANDARD_SHADER_PATH` points to an optional file on disk; if it is missing
|
||||
> (the normal case for the embedded library), loading falls back to the shader **embedded at
|
||||
> compile time** (`include_str!`, byte-identical). The fallback message you may see is
|
||||
> therefore **expected and harmless**.
|
||||
|
||||
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).
|
||||
|
||||
## 2. Creating materials
|
||||
|
||||
```rust
|
||||
// Textureless material: the color comes from per-vertex colors (or white by default).
|
||||
app.scene.add_material_shader("mat", "standard").unwrap();
|
||||
|
||||
// Textured material: the texture must first be registered in the scene (below).
|
||||
app.scene.add_material_texture("mat_textured", "standard", "my_texture").unwrap();
|
||||
```
|
||||
|
||||
Binding a material to a mesh happens at mesh creation (see [Meshes](meshes.md)):
|
||||
|
||||
```rust
|
||||
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/simple.rs) example.
|
||||
|
||||
## 3. Diffuse textures
|
||||
|
||||
`Texture` is a GPU image in `Rgba8UnormSrgb` (linear sampler, repeat addressing).
|
||||
Four constructors:
|
||||
|
||||
| Constructor | Usage |
|
||||
|--------------|-------|
|
||||
| `Texture::from_rgba8(device, queue, w, h, rgba, label)` | raw RGBA8 bytes (procedural) |
|
||||
| `Texture::from_bytes(device, queue, label, bytes)` | encoded data (PNG/JPEG… via the `image` crate) |
|
||||
| `Texture::from_file(device, queue, label, path)` | image file on disk |
|
||||
| `Texture::white_placeholder(device, queue)` | 1×1 white — used internally when a material has no texture |
|
||||
|
||||
You get `device`/`queue` in `setup()` via `app.context()`:
|
||||
|
||||
```rust
|
||||
let (device, queue) = {
|
||||
let ctx = app.context();
|
||||
(ctx.device.clone(), ctx.queue.clone())
|
||||
};
|
||||
let texture = Texture::from_rgba8(&device, &queue, 8, 8, &my_rgba, "checker").unwrap();
|
||||
app.scene.add_texture("checker_texture", texture).unwrap();
|
||||
app.scene.add_material_texture("ground_mat", "standard", "checker_texture").unwrap();
|
||||
```
|
||||
|
||||
The exact snippet (8×8 checkerboard + stripes generation) is in
|
||||
[`demo.rs`](../../lib/examples/demo.rs) and [`cube.rs`](../../lib/examples/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
|
||||
is bound — no visual effect, no regression);
|
||||
2. the `Geometry` carries **UVs** (`.with_uvs(…)`). Without UVs, sampling is constant.
|
||||
The procedural primitives (`uv_sphere`, `cube`, …) already provide them.
|
||||
|
||||
## 4. Unlit mode (flat / 2D rendering)
|
||||
|
||||
"Flat" rendering (vertex colors as-is, no lighting) is a **renderer switch**, not a material:
|
||||
|
||||
```rust
|
||||
app.renderer_mut().set_unlit(true); // in setup()
|
||||
```
|
||||
|
||||
This is the mode of the `simple` example (2D quad). In this mode the scene's lights are
|
||||
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
|
||||
> 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)
|
||||
@@ -0,0 +1,125 @@
|
||||
# Meshes: geometries, entities and transforms
|
||||
|
||||
A displayed object in WSG goes through three levels:
|
||||
|
||||
```
|
||||
Geometry (CPU, source of truth) ──► Mesh (GPU: vertex/index buffers) ──► Entity (placement in the scene)
|
||||
```
|
||||
|
||||
- **`Geometry`**: raw CPU-side data — positions + optional normals/UVs/colors/indices.
|
||||
- **`Mesh`**: GPU container (buffers uploaded once). It **retains** its `Arc<Geometry>` on the
|
||||
CPU side, along with its material.
|
||||
- **`Entity`**: a `mesh + Transform` association. This is the unit the engine draws. The same
|
||||
`Mesh` can be shared by several entities (each with its own `Transform`).
|
||||
|
||||
## 1. Procedural primitives (the shortest path)
|
||||
|
||||
The `math::primitives` module provides ready-to-use `Geometry` generators
|
||||
(positions + normals + UVs + indices):
|
||||
|
||||
| Function | Parameters | Result |
|
||||
|----------|-----------|--------|
|
||||
| `cube(size)` | side length | origin-centered cube, per-face normals |
|
||||
| `plane(width, depth, seg_x, seg_z)` | dimensions + subdivisions | horizontal plane (Y-up), UVs |
|
||||
| `uv_sphere(radius, sectors, stacks)` | radius + resolution | UV sphere (seam visible) |
|
||||
| `icosphere(radius, subdivisions)` | radius + subdivisions | smooth sphere (normalized, seam-free) |
|
||||
| `cylinder(radius, height, sectors)` | radius, height, resolution | centered cylinder |
|
||||
| `cone(radius, height, sectors)` | radius, height, resolution | cone (base at the bottom when translated in Y) |
|
||||
| `torus(major, minor, major_segments, minor_segments)` | radii + resolution | torus |
|
||||
|
||||
```rust
|
||||
use wsg_lib::math::{cube, icosphere, torus};
|
||||
|
||||
app.scene.create_mesh("cube_mesh", cube(0.8), Some("solid_mat")).unwrap();
|
||||
app.scene.create_mesh("sphere_mesh", icosphere(0.5, 2), Some("solid_mat")).unwrap();
|
||||
```
|
||||
|
||||
## 2. Custom `Geometry` (your own mesh)
|
||||
|
||||
`Geometry` is a builder: positions are mandatory, everything else is optional
|
||||
(sensible defaults are applied at upload — e.g. normal `[0,0,1]`, white color).
|
||||
|
||||
```rust
|
||||
use wsg_lib::resources::Geometry;
|
||||
|
||||
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]) // required for lighting (Phong)
|
||||
.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]); // triangulation (without indices: triangle list)
|
||||
```
|
||||
|
||||
Other attributes: `.with_uvs(vec![[u, v], …])` (required for textures — see
|
||||
[Materials & textures](materials.md)). `geometry.validate()` checks the arrays for
|
||||
consistency (aligned lengths, indices in range) before upload.
|
||||
|
||||
> **Indices**: `Vec<u16>` — a custom mesh must therefore stay under 65,536 vertices. The
|
||||
> engine's primitives respect this limit.
|
||||
|
||||
## 3. Registering in the scene
|
||||
|
||||
```rust
|
||||
// The mesh is built (GPU buffers) and bound to its material in one call.
|
||||
// `material = None`: the scene will use its default material (`standard`) at render time.
|
||||
app.scene.create_mesh("cube_mesh", geometry, Some("cube_material"))?;
|
||||
|
||||
// The entity references the mesh by its id (String IDs).
|
||||
app.scene.add_entity("cube", "cube_mesh")?;
|
||||
// …or with an explicit placement:
|
||||
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)).
|
||||
|
||||
## 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/cube.rs) example:
|
||||
|
||||
```rust
|
||||
fn update(&mut self, app: &mut wsg_lib::App) {
|
||||
self.angle += 0.02;
|
||||
let mut tf = *app.scene.entity_transform("cube").expect("entity present");
|
||||
tf.rotation = Quat::from_rotation_y(self.angle) * Quat::from_rotation_x(self.angle * 0.3);
|
||||
app.scene.set_entity_transform("cube", tf);
|
||||
}
|
||||
```
|
||||
|
||||
Other entity operations: `entity_transform(label)` (read), `remove_entity(label)` (hides
|
||||
without freeing resources), `entity_count()`.
|
||||
|
||||
> **Rotation order**: `Quat` does not commute — `rot_y * rot_x` is not `rot_x * rot_y`.
|
||||
> The order above (Y then X) gives a readable "top spinning" motion.
|
||||
|
||||
## 5. Mesh sharing
|
||||
|
||||
Create **one** mesh per geometry and as many entities as occurrences:
|
||||
|
||||
```rust
|
||||
app.scene.create_mesh("rock_mesh", icosphere(0.3, 1), Some("rock_mat")).unwrap();
|
||||
for i in 0..10 {
|
||||
let label = format!("rock_{i}");
|
||||
let mut tf = Transform::identity();
|
||||
tf.translation = Vec3::new(i as f32 * 0.8, 0.15, 0.0);
|
||||
app.scene.add_entity_with_transform(&label, "rock_mesh", tf).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,131 @@
|
||||
# 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()`.
|
||||
|
||||
## 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`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
wsg-lib = { path = "/path/to/wsg/lib" }
|
||||
pollster = { version = "1", features = ["macro"] } # for #[pollster::main] (AppBuilder is async)
|
||||
```
|
||||
|
||||
## 2. The minimal application
|
||||
|
||||
This snippet is the [`simple`](../../lib/examples/simple.rs) example from the repo, almost
|
||||
verbatim: a flat two-tone quad, rendered automatically every frame.
|
||||
|
||||
```rust
|
||||
use wsg_lib::app::AppBuilder;
|
||||
use wsg_lib::resources::Geometry;
|
||||
use wsg_lib::utils::WsgError;
|
||||
use wsg_lib::AppHandler;
|
||||
|
||||
struct MyQuad;
|
||||
|
||||
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);
|
||||
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], // red
|
||||
[0.0, 1.0, 0.0, 1.0], // green
|
||||
[0.0, 0.0, 1.0, 1.0], // blue
|
||||
[1.0, 1.0, 0.0, 1.0], // yellow
|
||||
])
|
||||
.with_indices(vec![0, 1, 2, 0, 2, 3]);
|
||||
|
||||
// `None`: the scene injects its default material (`standard`) at render time.
|
||||
app.scene.create_mesh("quad_mesh", geometry, None).unwrap();
|
||||
app.scene.add_entity("quad", "quad_mesh").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[pollster::main]
|
||||
async fn main() -> Result<(), WsgError> {
|
||||
let app = AppBuilder::new().title("WSG Simple").build().await?;
|
||||
app.run(MyQuad)
|
||||
}
|
||||
```
|
||||
|
||||
Note: **no `wgpu` or `winit` imports** — the `App` facade encapsulates them entirely.
|
||||
|
||||
## 3. What the library does for you
|
||||
|
||||
The full lifecycle, as driven by `App::run` (technical details in
|
||||
[FRAME_LOOP](../tech/FRAME_LOOP.md)):
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
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/`):
|
||||
|
||||
| Command | What you see |
|
||||
|----------|--------------|
|
||||
| `cargo run -p wsg-lib --example simple` | the quad above (flat 2D, unlit) |
|
||||
| `cargo run -p wsg-lib --example cube` | a textured, lit, spinning cube (3D) |
|
||||
| `cargo run -p wsg-lib --example demo` | the full showcase: 6 primitives + lights + shadows + orbital camera |
|
||||
|
||||
For your own application: create a crate, add the §1 dependency, paste the §2 code into
|
||||
`src/main.rs`, and `cargo run`.
|
||||
|
||||
## 5. Where to go next
|
||||
|
||||
- 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))
|
||||
|
||||
## 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)
|
||||
@@ -0,0 +1,79 @@
|
||||
# Shadows (shadow mapping)
|
||||
|
||||
Shadows are **off by default** and are enabled by designating **a single** casting light:
|
||||
|
||||
```rust
|
||||
app.scene.set_shadow_caster(Some(index)); // packed index — see the pitfall below
|
||||
app.scene.set_shadow_caster(None); // shadows off (default)
|
||||
```
|
||||
|
||||
Only a **directional or spot** light can cast shadows. A **point** light index disables the
|
||||
shadow pass (cubemap shadows are out of scope).
|
||||
|
||||
## ⚠️ The packed-index pitfall
|
||||
|
||||
`set_shadow_caster` takes the light's index **in the packed array** (directionals first,
|
||||
then point, then spot — recalled in [Lights](lights.md)).
|
||||
|
||||
**Index 0 is the default +Z directional** pre-loaded by `Lights::new()`, not necessarily
|
||||
your light. Symptom of a wrong index: the shadow camera looks in an unexpected direction and
|
||||
misaligned objects occlude each other (blackened objects, ghost shadows).
|
||||
|
||||
Two ways to avoid it:
|
||||
|
||||
1. **Clear the list before adding yours** — your light becomes index 0:
|
||||
|
||||
```rust
|
||||
app.scene.clear_lights(); // removes the default +Z
|
||||
app.scene.add_directional_light(dir, [1.0, 0.98, 0.92], 1.6).unwrap();
|
||||
app.scene.set_shadow_caster(Some(0)); // now it really is YOUR light
|
||||
```
|
||||
|
||||
This is the technique in [`shadow_test.rs`](../../lib/examples/shadow_test.rs).
|
||||
|
||||
2. **Count the indices** — if you keep the default light and add yours, it lands at index 1:
|
||||
|
||||
```rust
|
||||
app.scene.add_directional_light(toward_light, [1.0, 0.98, 0.92], 1.5).unwrap(); // → index 1
|
||||
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/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)):
|
||||
|
||||
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
|
||||
via `SHADOW_MAP_SIZE`), with a depth bias (slope-scaled + constant) to avoid shadow acne.
|
||||
2. **Color pass**: the `standard` fragment shader re-projects each fragment into light space
|
||||
and compares its depth against the map via a **3×3 PCF** (softened shadow edges).
|
||||
|
||||
Things to know:
|
||||
|
||||
- **Directional light**: the shadow frustum is orthographic, centered on the scene center
|
||||
(`SHADOW_SCENE_CENTER`, radius `SHADOW_SCENE_RADIUS = 5.0` by default). Objects **far from
|
||||
the origin** may fall outside the frustum and stop casting.
|
||||
- **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.
|
||||
|
||||
## Tuning shadow rendering
|
||||
|
||||
The constants `SHADOW_MAP_SIZE`, `SHADOW_DEPTH_BIAS`, `SHADOW_SCENE_RADIUS`,
|
||||
`SHADOW_SCENE_CENTER` are exposed in `wsg_lib::utils` (defaults: 1024, 0.006, 5.0, origin).
|
||||
|
||||
Tuning tips:
|
||||
|
||||
- **Speckled shadow edges (acne)**: raise the bias.
|
||||
- **Peter-panning** (shadow detached from the object): lower the bias.
|
||||
- **Shadow clipped at the scene edge**: raise the frustum radius (directional).
|
||||
- **Shadows too blurry, want them crisper**: raise the map size.
|
||||
|
||||
## Links
|
||||
|
||||
- [User README](README.md) · [Lights](lights.md) · [Examples](examples.md)
|
||||
- [Root README](../../README.md) · [FRAME_LOOP](../tech/FRAME_LOOP.md)
|
||||
Reference in New Issue
Block a user