LOD: quadric edge collapse (Garland-Heckbert) replaces Rule A
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.
This commit is contained in:
+17
-10
@@ -118,22 +118,29 @@ ou **à chaud** (toggle de debug au runtime). Aucun changement de signature exis
|
||||
|
||||
L'utilisateur déclare sa géométrie comme aujourd'hui (`positions`/`indices`/`normals`/`uvs`,
|
||||
y compris générée procéduralement par lui) et la bibliothèque **calcule les niveaux LOD sous le
|
||||
capot**. Mécanisme : décimation gloutonne du maillage indexé —
|
||||
capot**. Mécanisme : **quadric edge collapse** (Garland–Heckbert) sur le maillage indexé —
|
||||
|
||||
```
|
||||
Geometry::decimated(&self, target_triangles: u32) -> Geometry (pure, sans GPU)
|
||||
```
|
||||
- Triangles (non dégénérés) triés par aire croissante ; on retire le plus petit jusqu'à la
|
||||
cible — **sans carte de topologie** (« Règle A ») : tout sous-ensemble de faces d'un mesh valide
|
||||
est un mesh valide (pas de trou à boucher, pas de flip). La cible est donc toujours atteignable
|
||||
(clamp `[1, T]`).
|
||||
- Sortie : re-indexation (weld par position exacte), **normales lisses recalculées** sur les faces
|
||||
- Weld préalable (tolérance relative 1e-6 — grille + 27 voisins : les seams trigonométriques
|
||||
diffèrent d'environ 1e-16, l'égalité exacte ne suffit pas), triangles dégénérés jetés, puis
|
||||
**quadric edge collapse** : file de priorité des arêtes classées par coût (erreur quadrique de
|
||||
l'arête + longueur d'arête) ; on replie la plus bon marché jusqu'à la cible. Un repli **interne**
|
||||
fusionne les 2 triangles incidents (ils dégénèrent — −2 faces) et re-mappe les voisins :
|
||||
**pas de nouvelle face** — caractéristique d'Euler et **clôture préservées** (un mesh fermé
|
||||
reste fermé : pas de trous, pas de « books ») ; un repli de **bordure** retire 1 face.
|
||||
Garde-fous : arête non-manifold (≥ 3 faces) ou face en double (pli) → repli rejeté. La cible
|
||||
est clampée à `[1, T]` et atteinte au mieux (best effort : la granularité −2/−1 peut s'en
|
||||
écarter d'un ou deux triangles — jamais de géométrie corrompue).
|
||||
- Sortie : re-indexation (le weld ci-dessus), **normales lisses recalculées** sur les faces
|
||||
survivantes (normales de faces accumulées par coin soudé, puis normalisées), UVs/couleurs =
|
||||
valeur du premier vertex de chaque groupe soudé (documenté : évite le bleeding entre seams UV ;
|
||||
le LOD sacrifie la précision UV au profit de la silhouette).
|
||||
- Non indexé à l'entrée → weld par position préalable (la sortie est **toujours indexée**).
|
||||
Triangles dégénérés jetés.
|
||||
- **Déterministe** (tri stable, tie-break par index d'origine) → counts reproductibles en tests.
|
||||
- **Déterministe** (file de priorité, tie-break par identifiants de vertex, aucun aléatoire) →
|
||||
counts reproductibles en tests.
|
||||
- Fallback : la cible est clampée à `[1, T]` — un mesh n'est jamais décimé sous un triangle, et
|
||||
le count **réel** va dans le tableau LOD (`validate()` OK, jamais de géométrie corrompue).
|
||||
|
||||
@@ -251,9 +258,9 @@ pub fn lod_level(radius_px: f32, last: u32, max_level: u32, thresholds: &[f32])
|
||||
/// `depth <= eps` (objet dans la caméra) → f32::INFINITY (niveau 0).
|
||||
pub fn projected_radius_px(center_world: Vec3, radius: f32, view: Mat4, proj: Mat4, height_px: f32) -> f32;
|
||||
|
||||
/// Décimation gloutonne (D10, Règle A) : renvoie un `Geometry` indexé avec `target_triangles`
|
||||
/// triangles (clamp `[1, T]`) — les plus petits partent en premier (faible impact visuel),
|
||||
/// normales lisses recalculées. Déterministe.
|
||||
/// Quadric edge collapse (D10, Garland–Heckbert) : renvoie un `Geometry` indexé avec
|
||||
/// ~`target_triangles` triangles (best effort ±1–2, clamp `[1, T]`) — les arêtes au coût
|
||||
/// quadrique le plus faible partent en premier, normales lisses recalculées. Déterministe.
|
||||
pub fn decimated(&self, target_triangles: u32) -> Geometry; // sur Geometry
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user