LOD: fix inverted normals on decimated meshes + rim protection
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).
This commit is contained in:
+1
-1
@@ -162,7 +162,7 @@ generated: { by: human:jerome, at: 2026-07-31T00:00:00Z }
|
||||
|
||||
### 4.3 Optimisations
|
||||
- [x] Batching par Material (réduction des state changes GPU) — 2026-09-22 (Étape 18 : draws groupés par `Arc<Material>` dans la passe principale, 1 `set_pipeline` par matériau distinct — le démo passe de 7 à 3 ; pass d'ombre inchangé)
|
||||
- [x] Level of Detail (LOD) — 2026-09-23 (Étape 19 : ≤ 4 niveaux/mesh — L0 exacte, L1–L3 par **quadric edge collapse** (Garland–Heckbert) au setup (`Geometry::decimated`/`generate_lod_levels` : arêtes classées par coût quadrique, repli interne −2 faces / bordure −1, un mesh fermé reste fermé, weld tolérance 1e-6, rebase u16), packés dans les buffers vertex/index du mesh (offsets en unités d'élément, plafond 65 535 sommets) ; décision par frame **côté CPU** (sphère bounding projetée en pixels + hystérésis asymétrique ×0.8 — `math/lod.rs` pure, unit-testée), exécution **côté GPU** (le pass `cull` mappe niveau → ligne de la table LOD → args indirects) ; **activé par défaut**, `set_lod_enabled(false)` → rendu bit-à-bit identique au pré-LOD. Vérifié par readback GPU : zoom 4,6× → tous les meshes multi-niveaux passent au niveau 1 avec exactement leurs lignes L1 (ex. sphère 3840 → 1824 indices), stable frame à frame)
|
||||
- [x] Level of Detail (LOD) — 2026-09-23 (Étape 19 : ≤ 4 niveaux/mesh — L0 exacte, L1–L3 par **quadric edge collapse** (Garland–Heckbert) au setup (`Geometry::decimated`/`generate_lod_levels` : arêtes classées par coût quadrique, repli interne −2 faces / bordure −1, weld tolérance 1e-6 **conscient des attributs** (UV ≤ ½ tuile + normales dot > 0.9 — jamais à travers une seam/côte dure), mesh sans couture reste fermé, lèvres de couture protégées sinon (surface géométriquement complète), UVs/couleurs/normales interpolés au repli (normales héritées, jamais recalculées), rebase u16), packés dans les buffers vertex/index du mesh (offsets en unités d'élément, plafond 65 535 sommets) ; décision par frame **côté CPU** (sphère bounding projetée en pixels + hystérésis asymétrique ×0.8 — `math/lod.rs` pure, unit-testée), exécution **côté GPU** (le pass `cull` mappe niveau → ligne de la table LOD → args indirects) ; **activé par défaut**, `set_lod_enabled(false)` → rendu bit-à-bit identique au pré-LOD. Vérifié par readback GPU : zoom 4,6× → tous les meshes multi-niveaux passent au niveau 1 avec exactement leurs lignes L1 (ex. sphère 3840 → 1824 indices), stable frame à frame)
|
||||
- [ ] HDR + Tone Mapping (optionnel)
|
||||
|
||||
### 4.4 Gestion du Resize (cycle de vie Surface + Depth)
|
||||
|
||||
@@ -42,9 +42,12 @@ Ce document sert de spécification technique et de trame d'implémentation pour
|
||||
> (rayon de la sphère bounding projeté en pixels + hystérésis asymétrique — `math/lod.rs`, pur et unit-testé) ;
|
||||
> le GPU n'effectue que le mappage niveau → ligne de la table LOD du mesh. Les niveaux d'un mesh sont
|
||||
> générés par **quadric edge collapse** (Garland–Heckbert) au setup (`Geometry::decimated` : les
|
||||
> arêtes au coût quadrique minimal sont repliées en premier, un mesh fermé reste fermé — pas de
|
||||
> trous, pas de « books » ; soudure tolérance 1e-6, UVs/couleurs interpolés au repli — jamais
|
||||
> à travers une seam UV —, rebase u16) et **empilés dans les buffers vertex/index du mesh** (offsets en
|
||||
> arêtes au coût quadrique minimal sont repliées en premier ; soudure **consciente des attributs**
|
||||
> — un doublon ne fusionne que si UV proches (≤ ½ tuile) ET normales proches (dot > 0.9) — ; un mesh
|
||||
> sans couture reste fermé, sur un mesh couturé les lèvres de couture sont protégées (pas de trous,
|
||||
> pas de « books ») ; UVs/couleurs/normales interpolés au repli, normales **héritées** de la source
|
||||
> (jamais recalculées — l'éclairage reste identique au niveau 0 quelle que soit l'orientation source),
|
||||
> jamais à travers une seam UV ; rebase u16) et **empilés dans les buffers vertex/index du mesh** (offsets en
|
||||
> unités d'élément, pas d'octet — c'est ce qu'exigent les arguments `drawIndirect*` de WebGPU ; plafond u16 :
|
||||
> 65 535 sommets/mesh, 4 niveaux max). LOD activé par défaut ; `set_lod_enabled(false)` restaure un rendu
|
||||
> bit-à-bit identique au pré-LOD (niveau 0 partout = comptes complets). Détail : `docs/user/gpu-driven.md`
|
||||
|
||||
+13
-7
@@ -96,11 +96,17 @@ CPU):
|
||||
automatically from level 0 by **quadric edge collapse** (Garland–Heckbert,
|
||||
`Geometry::generate_lod_levels`): edges are ranked by quadric error and collapsed
|
||||
cheapest-first; an interior collapse merges both incident triangles (−2 faces) and remaps the
|
||||
neighbours — no new face, so a **closed mesh stays closed** (no holes, no non-manifold
|
||||
"books"), a boundary collapse removes one face; duplicate corners are welded (relative
|
||||
tolerance 1e-6); a survivor **moved** by a collapse gets its UV/color **interpolated**
|
||||
between the collapsed endpoints (same λ as its new position — the texture stays attached
|
||||
to the surface and coarsens smoothly across levels, never across a UV seam), and the
|
||||
neighbours — no new face, so a **seam-free mesh stays closed** (no holes, no non-manifold
|
||||
"books") and a boundary collapse removes one face; duplicate corners are welded **aware of
|
||||
their attributes** (relative position tolerance 1e-6, merged only when the UVs are within half
|
||||
a tile and the normals within ~25° — a seam or a hard edge stays a separate corner, so a
|
||||
collapse can never cross a texture chart or a shading break; on a mesh with a UV seam the
|
||||
seam's lips are then *protected* — their faces never collapse while interior ones remain — so
|
||||
the surface stays geometrically complete); a survivor **moved** by a collapse gets its
|
||||
UV/color/normal **interpolated** between the collapsed endpoints (same λ as its new position —
|
||||
texture and shading stay attached to the surface and coarsen smoothly across levels, never
|
||||
across a UV seam; normals are **inherited from the source, never recomputed**, so the lighting
|
||||
is identical to level 0 whatever the source's winding), and the
|
||||
levels are **packed into the mesh's single
|
||||
vertex/index buffers** (see the constraint below). Level 0 is always your exact geometry.
|
||||
2. **Per frame (CPU).** For each entity, the bounding sphere used by culling is projected to screen
|
||||
@@ -219,8 +225,8 @@ and verified fixed, see the D14 note in `docs/tech/ARCHI_CPU_GPU.md`.)
|
||||
- **Mesh bounding boxes are recomputed when meshes are added**; a scene whose mesh set changes
|
||||
at runtime simply re-uploads the small bbox table (a few bytes per mesh).
|
||||
- **LOD levels are packed into the mesh's own buffers**: u16 indices cap the *total* across all
|
||||
levels at 65 535 vertices, and there are at most 4 levels. The decimation (smallest-triangle
|
||||
removal + welding) is a setup-time cost only (a few ms for thousands of triangles); the
|
||||
levels at 65 535 vertices, and there are at most 4 levels. The decimation (quadric edge collapse
|
||||
+ attribute-aware welding) is a setup-time cost only (a few ms for thousands of triangles); the
|
||||
per-frame cost is one sphere projection per entity on the CPU.
|
||||
- **LOD detail loss is visible by design** — the hysteresis dead band makes the pop rare and
|
||||
one-directional (immediate when gaining detail, delayed when losing it), but a coarse level is
|
||||
|
||||
Reference in New Issue
Block a user