- Use glam's directx (WebGPU) projection module for both the camera
perspective and the shadow orthographic: NDC clip depth is [0,1] as
wgpu expects, instead of OpenGL's [-1,1] which clipped half the frustum
and broke depth-space consistency with the shadow map.
- Extend the shadow orthographic far plane to 2*r so the whole scene box
(and the shadow cast behind it, toward the camera) is covered.
- Switch the shadow comparison sampler to GreaterEqual so open sky is lit
and surfaces behind a blocker are shadowed (previous LessEqual inverted
the shadow, blackening the entire ground and making the cube float).
- Use the surface->light direction (+position_dir) for the directional
N*L term; the old negation darkened the cube top and lit the camera
faces, producing the inverted-pyramid appearance.
- Drop the now-redundant [0,1] depth remap in the main-pass shader.
For directional lights dir_angle is Vec4::ZERO, so the old code built the
shadow light_view_proj from dir=(0,0,0), making eye==target and look_at_mat4
degenerate -> NaN light_view_proj -> compute_shadow -> fully black frame.
Directional direction lives in position_dir.xyz (surface->light); the shadow
camera looks along the light's travel direction (light->scene), i.e. the
negation, matching the DRAFT spec.
With the shadow map cleared to 1.0 (farthest depth from light) and the
shadow pass writing smaller depths for surfaces closer to the light,
the fragment-to-light distance must be <= the stored surface depth for
lit pixels. GreaterEqual was inverted — everything appeared lit with no
shadows rendered.
Implement shadow mapping for directional lights:
- Scene::set_shadow_caster(Option<usize>) selects the shadow-casting light
by packed frame-array index (None disables; point lights rejected at render).
- Lights::get(index) resolves a packed index across the directional/point/spot lists.
- Renderer allocates a shadow depth map, comparison sampler, group-3 bind groups,
shadow uniform buffer and shadow pipeline; render_scene does a depth-only
shadow pass before the main pass; compute_shadow_light_view_proj builds an
orthographic light-space frustum from the scene radius.
- standard_shader: shadow_light_index/light_view_proj/shadow_params uniforms,
@group(3) depth map + comparison sampler, 3x3 PCF compute_shadow().
- shadow_shader: path/vertex shader with attribute layout matching the shared
vertex buffer (only position consumed).
- shadow_test example: directional shadow caster casts a PCF-softened shadow
onto a ground slab; documented in examples README.
spot_test : le cube tourne désormais sur les axes X et Y (quaternions
composés Y*X, vitesses légèrement différentes), pour que tous les sommets
passent devant le cône et que l'effet du faisceau soit visible sur les 6
faces.
Ajout de lib/examples/README.md : table des exemples avec la commande de
lancement de chacun + conventions. À maintenir à jour à chaque nouvel
exemple.
Le test du cône comparait l (surface->lumière) à dir_angle.xyz (lumière->scène),
deux directions opposées, donc cone ≈ -1 et facteur spot = 0 : les spots
n'éclairaient rien (cube noir). On teste désormais -l (lumière->point), aligné
avec l'axe du cône. Ajout d'un test Rust qui verrouille l'invariant (alignement
+1 sur l'axe). Build/test/WGSL/fmt OK.
Pour vérifier visuellement la lumière spot (Étape 13) : seule la spot est allumée,
le cube est noir hors du faisceau. Le cône orienté + bord lissé + éclairage fixe dans
l'espace monde (le cube tourne) sont nets. cargo run -p wsg-lib --example spot_test
La documentation reflète désormais uniquement ce qui *est* et ce qu'on
envisage de faire, pas ce qui aurait pu être (changements d'avis retirés) :
ROADMAP.md
- 1.2 : remplacé le récit de décision 'transform sur Mesh' (DRAFT Étape 8,
déviation) par l'énoncé de l'état actuel : Mesh porte son matériau + sa
Geometry CPU partagée, pas de transform (porté par Entity).
- Phase 2 réécrite : suppression des 'Arènes complètes' (SlotMap) et de la
struct Entity { mesh_id, material_id } contredite par l'Étape 7 (material
déplacé sur Mesh) et le choix String IDs ; migration 'handles typés'
conservée comme unique pointeur (1.4 / Notes de Décision).
- 4.2 : 'Lumières hémisphériques' cochée (déjà dans standard_shader).
- Notes de Décision : retiré 'UVs en Phase 4' (caduque, UVs implémentés dès
Geometry).
PLAN.md
- Statut réel condensé et mis à jour jusqu'à l'Étape 10 (textures).
- Phase 4 : Textures cochée (faite), Lumières laissée en plan (ROADMAP 4.2).
- Check-list pollster dé-obsolétisée ; suppression de la 'Note pour mémoire'
décrivant le module-pivot exec.rs qu'on a décidé de ne pas construire.
DRAFT.md
- Vidé (fin de l'Étape 10) en préparation de l'étape suivante.
- ROADMAP : Phase 4.1 (Textures) cochée — struct Texture, uvs, bind group
shader, Material avec texture diffuse.
- README : jalon 8 (diffuse textures) ajouté à la Roadmap ; ligne resource
'texture' + material enrichi dans le tableau des resources du module.
- DRAFT Étape 10 : cases 10.1-10.6 cochées (terminé et vérifié le
2026-09-18), point d'étape clôturé, bilan de fin d'étape rédigé (y compris
la divergence D2 : multiplication texel * couleur du vertex au lieu de
remplacement, et la structure réelle du PipelineCache).
Implémente le plan Étape 10 (Phase 4.1 Textures), décisions D1-D4 actées :
- 10.1 : nouveau type resources::Texture (device+view+sampler), format
Rgba8UnormSrgb, sampler linear/repeat, dep 'image' (png/jpeg). Constructeurs
from_rgba8 / from_bytes / from_file / white_placeholder.
- 10.2 : create_texture_bind_group_layout (groupe 2 : sampler+texture, fragment).
build_pipeline pose désormais 3 layouts [frame, object, texture] — « un seul
layout pour tous » (D1). PipelineCache détient le layout + le placeholder blanc.
- 10.3 : shader standard — UV transmis au fragment (location 2), groupe @2
texture_sampler + diffuse_texture, échantillonnage inconditionnel
base = texel * couleur(vertex) (D2) : sans texture (placeholder blanc) pas
de régression en lit comme en unlit.
- 10.4 : Material gagne texture: Option<Arc<Texture>> + texture_bind_group,
construit dans le constructeur via le cache (layout partagé + placeholder).
- 10.5 : draw_entity bind @group(2) ; Scene : add_texture / get_texture /
add_material_texture ; init_gpu accepte la Queue pour bâtir le placeholder.
- exemple cube : géométrie avec UV [0,1]² par face + texture damier procédurale.
Validation : fmt, check 0 warning, tests verts (3 + doc), doc sans missing_docs,
cube/simple/manual lancés sans erreur backend.
D1 placeholder 1×1 + groupe 2 sur toutes les pipelines · D2 échantillonnage
inconditionnel · D3 image + Rgba8UnormSrgb · D4 Material propriétaire du bind
group. Suite : implémentation 10.1-10.5 (refactor) puis docs (10.6).
- DRAFT.md vidé Après l'Étape 9 (bilan conservé dans git) et réinitialisé
pour l'Étape 10 : plan Textures avec décisions D1-D4 à valider.
- ROADMAP : coche 'uvs dans Geometry' (déjà implémenté dans le code),
prérequis de l'Étape 10.
- DRAFT.md : cases 9.1-9.4 cochées (terminées et vérifiées 2026-09-18),
point d'étape validé, bilan de fin d'étape rédigé.
- README.md : Renderer::new prend désormais width/height (signature Étape 9).
- ROADMAP.md : décisions D1-D4 actées + resize avec recréation de la depth
texture planifié en Phase 4.4 (acté D3, 2026-09-18).
- 9.1: allocate shared Depth32Float depth texture + view in Renderer,
sized to the initial surface (create_depth_texture helper, reusable
for the Phase 4.4 resize); Renderer::new now takes width/height.
- 9.2: attach depth_stencil_attachment (clear 1.0 / store) to both the
low-level render and high-level render_scene passes.
- 9.3: every pipeline declares a matching DepthStencilState (write true,
compare Less) via the shared DEPTH_FORMAT constant (D1).
Décisions D1-D4 actées 2026-09-18 (DRAFT Étape 9).
- D2 : Geometry reste en math (structure de donnees pure), re-export racine
- D4 : voie unique Mesh::from_geometry(Arc<Geometry>) ; suppression de l API &[Vertex]
- D5 : retention CPU (Arc<Geometry>) + buffers GPU pre-uploades
- D6 : convertisseur nomme Geometry::to_vertices() avec regles de remplissage
Toutes les decisions D1-D6 et la proposition 8.1 sont desormais validees.
Étape 7 (DRAFT): the PipelineCache moves into the Scene (SceneGpu holds
device + format + cache, wired via Scene::init_gpu in AppRunner::resumed),
so App no longer owns a cache field. Mesh now holds Option<Arc<Material>>
(with material()/set_material()/with_material()); Entity drops material_id
({mesh_id, transform}); iter_entities yields (label, &Mesh, &Transform);
Renderer::render_scene resolves the material from the mesh or the Scene's
lazy default_material(). New declarative Scene helpers: register_shader,
add_material_shader, create_mesh. cube/simple examples migrated; manual
remains low-level and unchanged.
- Camera enrichie: fov/near/far stockés, Default (pos (0,0,3), 45°, near 0.1,
far 100), with_perspective(), projection_matrix(aspect) depuis les params
stockés (au lieu de les passer en argument).
- Scene porte une caméra active: set_camera()/camera() (défaut Camera::default).
- Renderer::render_scene(view, scene, aspect) écrit chaque frame view/proj/
cam_pos réels dans le buffer frame (write_frame_uniforms) avant de dessiner;
le Renderer garde le handle du frame_buffer. Le chemin bas-niveau render()
conserve les valeurs par défaut (identité).
- App::render_scene calcule l'aspect depuis window.inner_size() (le Renderer
reste indépendant de la fenêtre).
Docs synchronisées: DRAFT (4.3 coche), README (statut 3D-infra + quick ref),
PLAN (caméras), ROADMAP (1.1/1.3/1.5/2.3).
Validation: check workspace+examples 0 warning, test (Pod + wgsl) OK, doc 0
warning, fmt propre. Le rendu 3D visible attend Étape 5 (brancher standard).
Étape 3 (infrastructure uniforms) + le câblage minimal d'Étape 4 pour garder
les exemples exécutables (wgpu requiert que tous les bind groups du layout
pipeline soient posés au draw) :
- resources/uniform.rs : types bytemuck Pod FrameUniforms (192 B) et
ObjectUniform (64 B), alignés 16 octets sans padding; offsets vérifiés par un
test unitaire contre le contrat du shader. glam feature bytemuck activé.
- pipeline_cache: create_uniform_bind_group_layouts() expose les 2 layouts
(frame @0 Vertex|Fragment + object @1 Vertex); build_pipeline les attache à
TOUT pipeline (un seul layout pour tous, décision actée).
- Renderer: alloue le buffer frame partagé + BindGroup(0) (défaut identité,
mode lit) et un object identité partagé pour le chemin bas-niveau; cache
RefCell<HashMap<label,(buffer,bindgroup)>> par entité, model réécrit chaque
frame depuis transform.to_matrix(); draw_entity pose groupes 0+1.
4.3 (caméra active + aspect) non implémenté: simple/manual restent exécutables
car basic ignore ces uniforms. Documentation DRAFT mise à jour.
Validation: check workspace+examples 0 warning, doc 0 warning, test (Pod+wgsl)
OK, fmt propre.
Étape 2 du plan 3D+Phong : nouveau shader unifié, non encore branché à un
pipeline (Étape 3 : infra uniforms).
- shaders/standard_shader.wgsl : contrat vertex complet (position/normal/uv/color,
56 octets, corrige la défaut latente du basic) ; 2 bind groups formative frame
(view/proj/cam_pos/light_dir/light_color/options) + object (model) ; éclairage
hemisphérique ambient + diffuse directionnel; mode unlit (options.x) pour que la
2D plate soit un cas partticulier de la 3D.
- utils/conf.rs : constantes STANDARD_SHADER_PATH + STANDARD_SHADER (include_str!).
- tests/wgsl_validate.rs : validation hors-ligne naga (via wgpu::naga, aucune
nouvelle dépendance) tant que le shader n'est pas compilé par un pipeline.
- shaders/README.md : documente le contrat standard et le statut du basic.
- Erreur WGSL corrigée en validation: cast mat4x4->mat3x3 non supporte, remplacé
par construction explicite de la sous-matrice 3x3.
- Validation: cargo test (naga OK), check workspace+examples 0 warning, doc OK, fmt OK.
Étape 1 du plan 3D+Phong : fondations de données sans toucher au rendu.
- resources: exporte Camera (fichier auparavant orphelin), migre les fonctions
glam dépréciées look_at_rh/perspective_rh_gl vers glam::camera::rh::* (induit
par la compilation de camera.rs, sinon 2 warnings).
- scene: nouveau type Entity { mesh_id, material_id, transform } (scene/entity.rs);
Scene::entities passe de HashMap<String,(String,String)> à HashMap<String,Entity>.
- API: add_entity conserve sa signature (transform identité par défaut), ajout de
add_entity_with_transform, entity_transform, set_entity_transform; iter_entities
rend désormais aussi le &Transform. renderer et examples inchangés a posteriori.
- Validation: cargo check --workspace et examples 0 warning, cargo doc 0 warning, fmt OK.
La lib n'a qu'un seul point de couplage au runtime (block_on dans app.rs) ; on ne crée volontairement
pas d'abstraction à ce stade. Consigné dans PLAN.md et ARCHI_APP.md : isoler derrière un module-pivot
unique si la lib acquiert d'autres appels async. Corrige aussi l'item check-list pollster devenu faux
dev-dependencies -> dependencies depuis la migration winit 0.30.
winit 0.30.13 removed WindowBuilder and deprecated EventLoop::run. Move
window/GPU creation into ApplicationHandler::resumed, expose AppHandler::setup
hook, switch App::run to run_app, and migrate both examples. pollster becomes a
regular dependency (used by app.rs).