Add InputState (DRAFT Etapes 15, sous-volt 15.B): pressed/held/released
semantics via HashSet rotation, mouse position/delta/scroll accumulators.
Wire it into App (pub field), forward winit WindowEvents in the event
handler, and begin_frame/end_frame around AppHandler::update.
Handleable key/mouse/scroll logic extracted into private helpers so all
five unit tests avoid constructing winit KeyEvent (private fields).
The shadow pass was correct but the demo light was much too steep (52°
elevation), so the blocker's shadow fell in a ~0.5-unit sliver tight against
the cube's base and was invisible against the bright ground (offscreen pixel
probe found a single dark pixel). Verified with an offscreen probe using the
real Renderer::render_scene + shadow path:
- steep front light (0.6,1.1,0.6) -> 1 dark pixel (no visible shadow)
- shallow side light (1.0,0.3,0.0) -> 17 107 pixels (shadow pipeline OK)
- tuned front-right (1.0,0.5,0.0) -> 16 979 pixels (clear visible shadow)
The azimuth matters most: from the elevated front-right camera, a shadow cast
toward -z falls behind the cube and is occluded; one cast toward -x runs across
the ground to the left of the cube and reads clearly. Tuned light therefore sits
front-right and low (toward_light (1.0,0.5,0.0)), keeping the front faces lit
while casting a clearly visible PCF-softened shadow.
Also reapply the LessEqual comparison sampler fix (commit 39167ee had set it,
but was later reverted to GreaterEqual by 9a51ff7 while debugging; the probe
confirms LessEqual is the correct, non-inverted test). Correct 'rotating cube'
to 'cube' in README/ROADMAP (shadow_test scene is static).
- 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
- 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.
- 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).
É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.
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).
- AppHandler::render now receives the current &Frame; its default
implementation renders the whole scene automatically via
app.render_scene(frame.view()) (Option A). Users can simply not
implement render for full auto-rendering.
- Add Renderer::render_scene: batch-renders every scene entity in a
single render pass. Factored per-mesh draw logic into a private
draw_entity helper shared with Renderer::render.
- Add App::render_scene(view) delegating to the Renderer.
- Fill simple.rs with a real quad (mesh/material/entity) without
importing wgpu; the scene now auto-renders via the trait default.
- Wrap in backticks every bare type in rustdoc comments (vertex.rs,
frame.rs, pipeline_cache.rs, material.rs, mesh.rs, scene.rs, error.rs)
so rustdoc no longer misreads them as intra-doc links or HTML tags.
- Add a missing doc comment on the public Frame struct.
- Add #![warn(missing_docs)] to the crate root so unevidenced public
items are surfaced going forward.
cargo doc --no-deps now generates with zero warnings.
- Replace the broken App::new() (which does not exist) with AppBuilder.
- Strip it down to the PLAN's ~15-line target: no explicit winit/wgpu,
AppHandler render() left empty (scene rendering still on the roadmap).
- Verified: builds, opens a black window and runs the update->render->present
loop until closed.
- Update README/PLAN wording now that the simple example compiles (it still
does not draw a scene).
- README stays the source of truth for current state; drive/item-5 now
reflects the settled decision: String IDs for the MVP, slotmap deferred.
- ROADMAP gains a 'point de depart' (present-state) block, marks glam done,
removes the slotmap Phase-1 mandates, and documents the String-ID decision
plus the deferred 'typed handles' step.
- PLAN corrects stale [X] checkmarks (App render() cannot draw, Scene
rendering not automated, simple example does not compile) and notes its
verification checklist against current reality.
- Remove the unused slotmap direct dependency from wsg-lib (package remains
in Cargo.lock only as a transitive dep of glow).