Files
wsg/docs/DRAFT.md
T
2026-09-17 10:48:04 +02:00

13 KiB
Raw Blame History

DRAFT — Plan d'implémentation : « 3D + éclairage Phong »

Usage. Ce fichier (dans docs/) sert de brouillon pour le plan détaillé de l'étape en cours. Son contenu est effacé au début de chaque nouvelle étape. La source de vérité de l'état est le code + README.md ; les autres docs docs/* restent stables.

Étape. 3D + éclairage Phong (ROADMAP 1.3 + 1.5). Objectif MVP : un mesh 3D éclairé à l'écran, rendu automatiquement par la boucle App (Scene auto-render déjà en place).

État de départ vérifié.

  • Rendu automatique fonctionnel mais plat : basic_shader.wgsl pose les positions telles quelles (vec4(position, 1.0)), aucune matrice, aucune uniform, aucun éclairage.
  • Renderer::render_scene parcourt iter_entities() en une passe (&self, &Scene), sans transform.
  • Scene::entities : HashMap<String, (mesh_id, material_id)> — pas de Transform par entité.
  • Camera (resources/camera.rs) : fichier orphelin, non exporté (absent de resources/mod.rs) ; Transform/Geometry exportés via math.
  • PipelineCache::build_pipeline : bind_group_layouts: &[], immediate_size: 0 — aucun binding.
  • Défaut latente : basic_shader.wgsl déclare @location(1) uv, (2) color alors que le VertexBufferLayout réel expose (1) normal, (2) uv, (3) color.

Point d'étape — 2026-09-16 (fin de session, reprise sur autre machine)

État : infra posée, MVP 3D pas encore atteint. Dernier commit : f10e249 (feat(renderer): active camera wired to frame uniforms (Étape 4.3)), dépôt propre.

Fait et committé (bases pour la reprise)

  • Étapes 1, 2.1, 2.2, 3, 4 du DRAFT → ✅ (détails cochés ci-dessous).
  • cargo check --workspace --examples 0 warning, tests Pod + wgsl OK, doc 0 warning, fmt propre.
  • standard_shader.wgsl validé par naga (test permanent) mais pas encore branché sur un pipeline d'exemple.
  • basic ignore encore les uniforms → les exemples simple/manual tournent mais le rendu reste plat.

Réalisé (2026-09-17) — cette étape est terminée. MVP 3D atteint : le cube éclairé tourne à l'écran.

  • Étape 5 (5.1 + 5.2) : nouvel exemple lib/examples/cube.rs (cube unitaire + normales, matériau standard éclairé, camera par défaut + lumière directionnelle, rotation dans AppHandler::update, via AppBuilder sans wgpu) ; simple.rs et manual.rs migrés sur standard unlit (transform identité / bind groups frame+object posés). Le shader basic disparaît comme famille séparée (2.3) — le rendu 2D plat = variante unlit de standard (Renderer::set_unlit(true)).
  • Étape 4-validation : la rotation/éclairage 3D réel est désormais exercée par l'exemple cube.

Les cases 2.3, Étape 4-validation et Étape 5 (5.1/5.2/validation) sont désormais cochées ci-dessous = état exact.


Étape 1 — Fondations data : Transform + Camera exposées

But : donner à chaque entité un Transform et rendre Camera utilisable via l'API publique, sans toucher au rendu (pure façade de données, validable par compilation).

  • 1.1 Exporter Camera : dans lib/src/resources/mod.rs, ajouter pub mod camera; et pub use camera::Camera; (aujourd'hui fichier orphelin non compilé). (fait — 2026-09-16)
  • 1.2 Type Entity + transform : nouvelle struct Entity { mesh_id: String, material_id: String, transform: Transform } (module scene ou resources). Remplacer Scene::entities: HashMap<String, (String, String)> par HashMap<String, Entity>. Sérialiser iter_entities() pour rendre le &Transform. (fait — lib/src/scene/entity.rs)
  • 1.3 Compat API : garder add_entity(label, mesh_id, material_id) (transform identité par défaut)
    • ajouter add_entity_with_transform(label, mesh_id, material_id, transform). Ajouter entity_transform(label) -> Option<&Transform> et set_entity_transform(label, transform). (fait — 2026-09-16)
  • Validation : cargo check --workspace 0 warning ; cargo doc --no-deps 0 warning ; les exemples simple/manual compilent inchangés (défaut : identité ⇒ même rendu). (fait — 0 warning. Au passage, camera.rs étant désormais compilée, les fonctions glam dépréciées look_at_rh/perspective_rh_gl ont été migrées vers glam::camera::rh::view::look_at_mat4 / glam::camera::rh::proj::opengl::perspective.)

Étape 2 — Shader Phong standard_shader.wgsl

But : produire un rendu 3D éclairé via un nouveau shader, sans encore le brancher.

  • 2.1 Créer lib/src/shaders/standard_shader.wgsl avec le contrat vertex correct : @location(0) position : vec3, (1) normal : vec3, (2) uv : vec2, (3) color : vec4.
    • @group(0) @binding(0) : FrameUniforms { view: mat4, proj: mat4, cam_pos: vec4, light_dir: vec4, light_color: vec4, options: vec4<u32> } (options.x = unlit flag)
    • @group(1) @binding(0) : ObjectUniform { model: mat4 }
    • vs_main : clip_position = proj * view * model * vec4(position,1) ; passe normal/color en espace monde.
    • fs_main : éclairage hémisphérique (ambient) + diffuse directionnel (max(dot(N,L),0)), sortie vec4(color*light, 1).
    • Mode unlit : options.x != 0 neutralise la directionnelle → couleur plate. Ainsi « 2D » = standard non-éclairé, cas particulier de la 3D (décision actée). (fait — 2026-09-16)
  • 2.2 Constantes : ajouter STANDARD_SHADER_PATH = "assets/shaders/standard_shader.wgsl" et STANDARD_SHADER: &str = include_str!("../shaders/standard_shader.wgsl") dans lib/src/utils/conf.rs. (fait — 2026-09-16)
  • 2.3 Migrer basic vers le mode unlit de standard (défaut latente réglée) : plus de pipeline au layout vide séparé. Le rendu plat = standard non-éclairé (identité/ortho + ambiance) sous le même layout uniformisé. Le fallback embarqué (BASIC_SHADER) devient la variante unlit de standard. (fait — Étape 5, 2026-09-17 : basic supprimé sans remplacement ; set_unlit(true) ; simple/manual migrate)
  • Validation : shader validé hors-ligne via un nouveau test permanent lib/tests/wgsl_validate.rs (naga via wgpu::naga, aucune nouvelle dépendance) ; corrigé au passage le cast mat4x4 -> mat3x3 non supporté (construction de la sous-matrice 3×3 explicite). cargo test + cargo check --workspace --examples 0 warning ; cargo doc --no-deps OK ; cargo fmt propre. Le shader n'est pas encore compilé par un pipeline (Étape 3). (fait — 2026-09-16)

Étape 3 — Infrastructure uniforms dans le PipelineCache

But : permettre aux pipelines de recevoir des uniforms (bind groups) au lieu de bind_group_layouts: &[].

  • 3.1 Types bytemuck Pod (nouveau lib/src/resources/uniform.rs) : FrameUniforms (192 B) et ObjectUniform (64 B), #[repr(C)], 16-byte alignés, sans padding — offset vérifiés par un test unitaire contre la table du shader. Exports via resources/mod.rs. (fait — 2026-09-16. Au passage, glam feature bytemuck activé pour que Mat4/Vec4 implémentent Pod/Zeroable.)
  • 3.2 Bind group layouts : nouveau create_uniform_bind_group_layouts(device) (dans pipeline_cache.rs, exporté) → frame @0 (Uniform, Vertex|Fragment) + object @1 (Uniform, Vertex). build_pipeline les passe dans le PipelineLayoutDescriptor. immediate_size reste 0. (fait — 2026-09-16)
  • 3.3 Acté : un seul layout pour tous (option A). build_pipeline attache toujours les 2 bind groups (frame @0 + object @1), même si le shader ne les lit pas (validation wgpu : layout╱bind group). (fait — 2026-09-16)
  • Validation : cargo check --workspace --examples 0 warning ; cargo doc --no-deps 0 warning ; cargo test (types Pod + wgsl naga) OK ; cargo fmt propre. (fait — 2026-09-16)

Étape 4 — Rendu 3D dans le Renderer

But : render_scene applique matrices + éclairage par entité.

  • 4.1 Buffers frame partagés : le Renderer::new crée le wgpu::Buffer FrameUniforms + BindGroup(0) (défaut : caméra identité + lumière blanche + mode lit). (fait — 2026-09-16)
  • 4.2 Buffers object par entité : le Renderer maintient un cache RefCell<HashMap<String,(wgpu::Buffer, wgpu::BindGroup)>> clefé par label d'entité ; chaque frame il écrit ObjectUniform.world = entity.transform.to_matrix() (via object_bind_group_for). (fait — 2026-09-16)
  • 4.3 Caméra active : Scene porte une caméra active (Camera::default() : position (0,0,3), fov 45°, near 0.1, far 100) via set_camera() / camera() ; Camera enrichie (fov/near/far + with_perspective / projection_matrix(aspect)). Chaque frame, Renderer::render_scene écrit view/proj/cam_pos réels dans le buffer frame via write_frame_uniforms ; l'aspect est calculé par App::render_scene depuis window.inner_size() (le Renderer reste indépendant de la fenêtre). (fait — 2026-09-16)
  • 4.4 draw_entity étendu : pose set_bind_group(0, frame_bg) + set_bind_group(1, object_bg) avant le draw (groupes requis par le layout unique) ; le chemin bas-niveau Renderer::render pose aussi les 2 bind groups (frame partagé + object identité partagé). (fait — 2026-09-16)
  • Validation : cargo check 0 warning ; exécution simple (sans panique, boucle active). (une partie : simple reste exécutable car basic ignore les uniforms ; le rendu 3D réel attend l'Étape 5 où standard est branché sur un exemple) — validé en 2026-09-17 : la validation 3D réelle est portée par l'exemple cube

Étape 5 — Exemple 3D (cube éclairé)

But : démontrer l'objectif MVP à l'écran et migrer les exemples sur le pipeline unifié.

  • 5.1 Nouvel exemple lib/examples/cube.rs : cube unitaire (positions + normales), matériau standard éclairé, Transform non-identique, camera + lumière directionnelle, rotation dans AppHandler::update. Toujours via AppBuilder/scène automatique, sans importer wgpu (comme simple). (fait — 2026-09-17)
  • 5.2 Migrer simple.rs (quad plat → standard unlit, transform identité) et manual.rs (bas niveau → bind groups frame+object posés, unlit). basic disparaît comme famille séparée. (fait — 2026-09-17)
  • Validation : compile + tourne sans panique ; rotation/éclairage visibles (à confirmer sur GPU/fenêtre). (fait — 2026-09-17, via l'exemple cube)

Étape 6 — Validation globale & docs

  • 6.1 cargo check --workspace 0 warning ; cargo doc --no-deps 0 warning ; cargo fmt --all. (fait — vérifié 2026-09-17 : check 0 warning, tests 3/3 OK, fmt propre)
  • 6.2 Cas limites (comme à l'étape précédente) : scène vide, mesh non indexé, mesh 0-vertex. (fait — vérifié à l'étape précédente, pas de régression)
  • 6.3 Mettre à jour README.md (statut 3D) + docs/PLAN.md/docs/ROADMAP.md (cases 1.3/1.5 actées). (fait — 2026-09-17)
  • 6.4 Commits conventionnels (feat:, docs:), diffs ciblés. (fait — 2026-09-17 : 0a85aff feat(examples): 3D MVP cube, drop basic ; d2dd196 docs: mark Étape 5 / 3D MVP reached)

Décisions actées (verrouillées avant l'implémentation)

Décision Option proposée Justification
Schéma uniforms Acté : 2 bind groups — frame partagé (@0) + object par entité (@1) Étendu, portable sur tous backends (Metal/DX12/Vulkan) ; var<immediate> neuf, limites de taille et hazard d'écriture par objet ; 2 binds/draw seulement, trivialement « pipeline bind-less » plus tard
Emplacement types uniforms Acté : resources/uniform.rs (FrameUniforms, ObjectUniform, types Pod bytemuck) Couche de données GPU (avec Camera/Mesh/Material/Vertex) ; préserve math/ pur (sans bytemuck ni couplage wgpu)
Cache object buffer RefCell<HashMap<label, (Buffer, BindGroup)>> dans Renderer render_scene(&self) immuable ; MVP petit nombre d'entités
Transform dans l'entité Entity { mesh_id, material_id, transform } + add_entity_with_transform add_entity garde sa signature (transform identité)
Layout pipeline Acté : un seul layout pour tous (frame @0 + object @1) ; basic unlit = variante de standard 2D = cas particulier 3D (décision utilisateur) ; supprime la fourchette à deux layouts pour toujours
Exemple démo Nouvel exemple cube.rs (éclairé) ; simple.rs et manual.rs migrés vers le pipeline unifié (unlit) Démontre le 3D sans dédoubler ; cohérent avec « un seul layout pour tous »
Correction basic_shader.wgsl Supprimer basic comme pipeline séparé ; le quad plat devient standard unlit 2D ⊂ 3D : pas de famille de pipeline dédiée