- weld: Δ UV = 0.5 exact n'est plus soudé (ambigu: fente à sa plus large
vs saut légitime — la colonne u=1 du cône vs le chart disque du cap
tombait exactement dessus et mélangeait les charts)
- collapse: les UV se blendent linéairement. Le fold par coordonnée
(Δ entier → 0) figeait l'UV du sommet sur les vertices de base
(bande de rayures, signalée par l'utilisateur) — il était inutile:
les jumeaux de fente sont gelés, aucun repli ne traverse la fente
- welded renvoie un struct Welded (clippy)
- test cône: dual-chart (fan latéral bilinéaire + disque du cap),
surface latérale r = λ; test seam/span réécrit selon la sémantique finale
- doc: gpu-driven.md, ARCHI_CPU_GPU.md (box LOD), DRAFT.md (D10) alignés
sur la sémantique finale (gel des jumeaux, blend linéaire, seuil strict)
- AGENTS.md: gotcha 'ne jamais folder un saut entier de tuile'
102 tests passent, demo lance et rend sans erreur.
Root cause of the user-reported artifacts (stripes disappearing on the
far LOD): normals were RECOMPUTED from the surviving faces after the
collapse. uv_sphere is wound inward, so the recomputed normals pointed
inward — the far LOD was back-face-lit (measured deviation 2.0 vs 0.0
on L0).
Fix — normals are INHERITED, never recomputed:
- welded() now also welds normals (first-encountered per cluster)
- Collapse owns the normal table; collapse_edge λ-blends + renormalizes
at the same λ as the position (no seam guard: the attribute-aware
weld kept hard-edge vertices separate, so no edge crosses a shading
break)
- compaction reads the post-collapse table instead of recomputing
- the outward reorientation added earlier is removed: the source
winding + normals are preserved as-is, so every LOD level is
shading-compatible with L0 whatever the source orientation
Rim protection (attribute-aware weld leaves UV-seam slits / pole fans
as boundary rims): a face touching such a rim is never removed while
interior collapses remain — strict PQ mode (edges whose incident faces
are fully interior, re-validated at pop) with a best-effort fallback
when the interior alone cannot reach the target. Seam-free meshes
(icosahedron) stay topologically closed; sewn meshes stay geometrically
complete (no hole at the slit) — tests now assert seam-column survival.
Docs: gpu-driven.md §LOD, ARCHI_CPU_GPU LOD note, ROADMAP 4.3 updated
with the attribute-aware weld + rim protection + inherited normals.
Gate: fmt ✓, check 0 warnings ✓, 104 tests ✓, demo runs ✓.
Measured (uv_sphere 32×20): normal max deviation 0.0000 on L0–L2
(was 2.0000), UV max error 0.0084 vs analytical (was 0.5000).
Le vertex-cible d'un repli se déplace au point optimal de l'arête mais
conserveait l'UV du weld — désaccord position/UV croissant en cascade :
la texture 'fuit' et les motifs (rayures) disparaissent aux niveaux
lointains, avec un changement radical entre deux LOD.
- Collapse porte désormais les tables uvs/colors (clonées au weld).
- collapse_edge interpole les UVs de la cible : uv_t ← (1−λ)·uv_s + λ·uv_t,
avec le même λ que le déplacement (cost_and_point renvoie désormais λ).
- Garde-fou seam : si |Δu| > 0.5 ou |Δv| > 0.5 (saut de texture), la cible
garde son UV — l'interpolation ne traverse jamais une seam.
- Couleurs : toujours interpolées (espace colorimétrique continu).
- Compaction : la sortie lit les tables mises à jour (c.uvs/c.colors), pas
les tables d'origine du weld.
- Docs : DRAFT.md (Sortie), gpu-driven.md (décimination), ARCHI_CPU_GPU (LOD).
Rule A (area-sorted triangle removal) left holes and open boundaries
on closed meshes (visible artifacts when zoomed far out). The decimator
is now a proper quadric edge collapse:
- Collapse: welded u32 topology, per-vertex quadrics (accumulated
incident face planes), edge cost = quadric error at the optimal
point (clamped to the segment) + edge length, BinaryHeap with a
custom Ord (f32 is not Ord; inverted compare, NaN-safe).
- Standard GH semantics WITHOUT the new face: both incident pair faces
degenerate and are removed; neighbouring faces remap and sweep over
the region. Preserves the Euler characteristic and closedness (no
holes, no books, no duplicate faces); interior collapse = -2 faces,
boundary = -1. Guards: non-manifold edge (>2 faces) or a fold
(duplicate sorted triple) rejects the collapse.
- welded(): fuzzy welding (1e-6 relative tolerance, grid + 27-
neighbour broad phase, exact verify) - trig-generated seams differ
by ~1e-16, exact-bit welding missed them.
- Root causes fixed along the way: dead face slots are never reused
(stale edge/vface entries), vfaces updated on remap, degenerate
faces dropped, best-effort target (granularity -2/-1 can land 1-2
off; soft cap, deterministic).
- Docs: DRAFT D10, ROADMAP 4.3, ARCHI_CPU_GPU, gpu-driven, lib
README, mesh/scene/demo comments - 'greedy decimation / Rule A'
replaced by 'quadric edge collapse'.
- lod.rs test: deprecated glam perspective alias -> explicit
glam::camera::rh::proj::opengl::perspective.
cargo test --workspace: 100 passed (94 lib + 3 wgsl + 3 integration),
0 failed; demo runs clean with LOD on.
Bug fixes found while validating stage 15:
- demo: set_shadow_caster(Some(0)) selected the default +Z light (packed
index 0 from Lights::new()); the demo's warm directional light is packed
at index 1. The shadow camera then looked down -Z, so misaligned objects
occluded each other (cone/torus rendered black). Use index 1.
- primitives::torus: index winding was flipped ([a,c,b]); the outer surface
(outward normals, CCW from outside) was culled and only the dark interior
stayed visible. Reversed to [a,b,c]/[b,d,c] so it is CCW from outside.
- app: call device.poll() each frame in about_to_wait; without it wgpu
async callbacks (on_submitted_work_done, map_async) never fire in the
windowed loop.
Docs:
- conf: clarify the embedded-shader fallback is expected/harmless and that
SHADOW_SHADER_PATH is kept for API compatibility only.
- README item 15: runtime-verified headless.
- DRAFT.md: emptied to a completion summary per convention (full stage-15
plan preserved in git history).
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).
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).
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).
- 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.
- 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.