Skip to main content

threecrate_simplification/
progressive.rs

1//! Progressive mesh implementation based on Hoppe (1996)
2//!
3//! Provides a progressive mesh representation that encodes a mesh as a coarse
4//! base mesh plus a sequence of vertex split operations. This allows
5//! reconstructing the mesh at any level of detail between the base and the
6//! original, enabling LOD rendering and streaming.
7
8use crate::edge_collapse::{EdgeCost, HalfEdgeMesh, INVALID};
9use priority_queue::PriorityQueue;
10use serde::{Deserialize, Serialize};
11use std::collections::HashSet;
12use threecrate_core::{Error, Point3f, Result, TriangleMesh, Vector3f};
13
14/// A single vertex split operation (inverse of an edge collapse).
15///
16/// Records how to split vertex `vertex_s` into `vertex_s` (at updated position)
17/// and `vertex_t` (a restored vertex), along with the face connectivity changes
18/// needed to reverse the collapse.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct VertexSplit {
21    /// Vertex being split (the survivor of the original collapse)
22    pub vertex_s: usize,
23    /// Restored vertex index (the vertex that was removed in the collapse)
24    pub vertex_t: usize,
25    /// Position of vertex_s after the split
26    pub position_s: Point3f,
27    /// Position of vertex_t (restored)
28    pub position_t: Point3f,
29    /// Normal of vertex_s after split
30    pub normal_s: Option<Vector3f>,
31    /// Normal of vertex_t (restored)
32    pub normal_t: Option<Vector3f>,
33    /// Color of vertex_s after split
34    pub color_s: Option<[u8; 3]>,
35    /// Color of vertex_t (restored)
36    pub color_t: Option<[u8; 3]>,
37    /// Restored face on left side of the edge (if any)
38    pub face_left: Option<[usize; 3]>,
39    /// Restored face on right side of the edge (if any)
40    pub face_right: Option<[usize; 3]>,
41    /// Faces whose connectivity changes: (face_index, new vertex indices)
42    pub modified_faces: Vec<(usize, [usize; 3])>,
43}
44
45/// Progressive mesh: a base mesh plus a sequence of vertex splits.
46///
47/// The base mesh is the coarsest representation. Applying vertex splits
48/// in order progressively refines the mesh back toward the original.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct ProgressiveMesh {
51    /// The coarsest level of detail
52    pub base_mesh: TriangleMesh,
53    /// Ordered sequence of vertex split operations (apply in order to refine)
54    pub vertex_splits: Vec<VertexSplit>,
55    /// Total vertex count of the original (fully refined) mesh
56    pub full_vertex_count: usize,
57    /// Total face count of the original (fully refined) mesh
58    pub full_face_count: usize,
59}
60
61/// Record of a single edge collapse for later inversion into a vertex split.
62struct CollapseRecord {
63    /// The vertex that survived (v1)
64    vertex_s: usize,
65    /// The vertex that was removed (v2)
66    vertex_t: usize,
67    /// Position of v1 before collapse (so we can restore it on split)
68    position_s_before: Point3f,
69    /// Position of v2 before collapse
70    position_t_before: Point3f,
71    /// Normal of v1 before collapse
72    normal_s_before: Option<Vector3f>,
73    /// Normal of v2 before collapse
74    normal_t_before: Option<Vector3f>,
75    /// Color of v1 before collapse
76    color_s_before: Option<[u8; 3]>,
77    /// Color of v2 before collapse
78    color_t_before: Option<[u8; 3]>,
79    /// Faces that were removed (degenerate after collapse)
80    removed_faces: Vec<(usize, [usize; 3])>,
81    /// Faces that had v2 replaced with v1 (face_idx, old verts before rewrite)
82    rewritten_faces: Vec<(usize, [usize; 3])>,
83}
84
85impl ProgressiveMesh {
86    /// Generate a progressive mesh by simplifying the input mesh down to a
87    /// base level, recording each edge collapse as a reversible vertex split.
88    ///
89    /// `base_face_ratio` controls how much to simplify: 0.1 means the base
90    /// mesh will have ~10% of the original faces.
91    pub fn from_mesh(mesh: &TriangleMesh, base_face_ratio: f32) -> Result<Self> {
92        if mesh.is_empty() {
93            return Err(Error::InvalidData("Mesh is empty".to_string()));
94        }
95        let base_face_ratio = base_face_ratio.clamp(0.01, 1.0);
96        let target_faces = (base_face_ratio * mesh.faces.len() as f32).max(1.0) as usize;
97
98        let full_vertex_count = mesh.vertex_count();
99        let full_face_count = mesh.face_count();
100
101        let mut hem = HalfEdgeMesh::from_triangle_mesh(mesh);
102        let mut collapse_records: Vec<CollapseRecord> = Vec::new();
103
104        let mut queue = build_queue(&hem);
105
106        while hem.active_face_count > target_faces && !queue.is_empty() {
107            let (_, edge_cost) = match queue.pop() {
108                Some(item) => item,
109                None => break,
110            };
111
112            let v1 = edge_cost.v1;
113            let v2 = edge_cost.v2;
114
115            if hem.vertex_removed[v1]
116                || hem.vertex_removed[v2]
117                || hem.vertex_edge[v1] == INVALID
118                || hem.vertex_edge[v2] == INVALID
119            {
120                continue;
121            }
122
123            if hem.find_half_edge(v1, v2).is_none() {
124                continue;
125            }
126
127            if !hem.check_link_condition(v1, v2) {
128                continue;
129            }
130
131            // Snapshot state before collapse
132            let position_s_before = hem.positions[v1];
133            let position_t_before = hem.positions[v2];
134            let normal_s_before = hem.normals.as_ref().map(|n| n[v1]);
135            let normal_t_before = hem.normals.as_ref().map(|n| n[v2]);
136            let color_s_before = hem.colors.as_ref().map(|c| c[v1]);
137            let color_t_before = hem.colors.as_ref().map(|c| c[v2]);
138
139            // Snapshot face state: find faces adjacent to edge (v1,v2) that
140            // will be removed, and faces incident on v2 that will be rewritten.
141            let faces_before = snapshot_faces_around_edge(&hem, v1, v2);
142
143            let (pos, _cost) = hem.compute_collapse_cost(v1, v2);
144
145            if !hem.collapse_edge(v1, v2, pos) {
146                continue;
147            }
148
149            // Determine which faces were removed vs rewritten
150            let (removed_faces, rewritten_faces) =
151                classify_face_changes(&hem, &faces_before, v1, v2);
152
153            collapse_records.push(CollapseRecord {
154                vertex_s: v1,
155                vertex_t: v2,
156                position_s_before,
157                position_t_before,
158                normal_s_before,
159                normal_t_before,
160                color_s_before,
161                color_t_before,
162                removed_faces,
163                rewritten_faces,
164            });
165
166            // Periodically rebuild queue
167            if collapse_records.len() % 100 == 0 {
168                queue = rebuild_queue(&hem, collapse_records.len() * 1000);
169            }
170        }
171
172        let base_mesh = hem.to_triangle_mesh();
173
174        // Convert collapse records to vertex splits (reverse order)
175        let vertex_splits: Vec<VertexSplit> = collapse_records
176            .into_iter()
177            .rev()
178            .map(|rec| {
179                let face_left = rec.removed_faces.first().map(|(_, f)| *f);
180                let face_right = rec.removed_faces.get(1).map(|(_, f)| *f);
181
182                let modified_faces: Vec<(usize, [usize; 3])> = rec
183                    .rewritten_faces
184                    .iter()
185                    .map(|(fi, old_verts)| (*fi, *old_verts))
186                    .collect();
187
188                VertexSplit {
189                    vertex_s: rec.vertex_s,
190                    vertex_t: rec.vertex_t,
191                    position_s: rec.position_s_before,
192                    position_t: rec.position_t_before,
193                    normal_s: rec.normal_s_before,
194                    normal_t: rec.normal_t_before,
195                    color_s: rec.color_s_before,
196                    color_t: rec.color_t_before,
197                    face_left,
198                    face_right,
199                    modified_faces,
200                }
201            })
202            .collect();
203
204        Ok(ProgressiveMesh {
205            base_mesh,
206            vertex_splits,
207            full_vertex_count,
208            full_face_count,
209        })
210    }
211
212    /// Reconstruct the mesh at a specific refinement level.
213    ///
214    /// `level` is clamped to `[0, num_levels()]`. Level 0 is the base mesh,
215    /// `num_levels()` is full detail.
216    pub fn reconstruct_at_level(&self, level: usize) -> TriangleMesh {
217        let level = level.min(self.vertex_splits.len());
218        if level == 0 {
219            return self.base_mesh.clone();
220        }
221
222        // Start from original mesh data and replay only the needed splits.
223        // We rebuild by applying vertex splits to the base mesh.
224        let mut vertices = self.base_mesh.vertices.clone();
225        let mut faces = self.base_mesh.faces.clone();
226        let mut normals = self.base_mesh.normals.clone();
227        let mut colors = self.base_mesh.colors.clone();
228
229        // Build index mapping: base mesh uses compacted indices, but vertex
230        // splits reference original (pre-compaction) indices. We need to map.
231        // Strategy: grow vertex/face arrays as splits are applied.
232
233        for split in self.vertex_splits.iter().take(level) {
234            // Ensure vertex arrays are large enough
235            while vertices.len() <= split.vertex_t.max(split.vertex_s) {
236                vertices.push(Point3f::origin());
237                if let Some(ref mut n) = normals {
238                    n.push(Vector3f::zeros());
239                }
240                if let Some(ref mut c) = colors {
241                    c.push([0, 0, 0]);
242                }
243            }
244
245            // Restore positions
246            vertices[split.vertex_s] = split.position_s;
247            vertices[split.vertex_t] = split.position_t;
248
249            // Restore normals
250            if let Some(ref mut n) = normals {
251                if let Some(ns) = split.normal_s {
252                    n[split.vertex_s] = ns;
253                }
254                if let Some(nt) = split.normal_t {
255                    n[split.vertex_t] = nt;
256                }
257            }
258
259            // Restore colors
260            if let Some(ref mut c) = colors {
261                if let Some(cs) = split.color_s {
262                    c[split.vertex_s] = cs;
263                }
264                if let Some(ct) = split.color_t {
265                    c[split.vertex_t] = ct;
266                }
267            }
268
269            // Restore modified faces (change v_s back to v_t where needed)
270            for &(fi, old_verts) in &split.modified_faces {
271                while faces.len() <= fi {
272                    faces.push([0, 0, 0]);
273                }
274                faces[fi] = old_verts;
275            }
276
277            // Restore removed faces
278            if let Some(face) = split.face_left {
279                faces.push(face);
280            }
281            if let Some(face) = split.face_right {
282                faces.push(face);
283            }
284        }
285
286        // Clean up: remove any degenerate faces and compact
287        let faces: Vec<[usize; 3]> = faces
288            .into_iter()
289            .filter(|f| {
290                f[0] != f[1]
291                    && f[1] != f[2]
292                    && f[2] != f[0]
293                    && f[0] < vertices.len()
294                    && f[1] < vertices.len()
295                    && f[2] < vertices.len()
296            })
297            .collect();
298
299        let mut mesh = TriangleMesh::from_vertices_and_faces(vertices, faces);
300        if let Some(n) = normals {
301            mesh.set_normals(n);
302        }
303        if let Some(c) = colors {
304            mesh.set_colors(c);
305        }
306        mesh
307    }
308
309    /// Reconstruct the mesh at a given detail ratio.
310    ///
311    /// `detail_ratio` ranges from 0.0 (base/coarsest) to 1.0 (full detail).
312    pub fn reconstruct_at_ratio(&self, detail_ratio: f32) -> TriangleMesh {
313        let detail_ratio = detail_ratio.clamp(0.0, 1.0);
314        let level = (detail_ratio * self.vertex_splits.len() as f32).round() as usize;
315        self.reconstruct_at_level(level)
316    }
317
318    /// Get a reference to the base (coarsest) mesh.
319    pub fn base(&self) -> &TriangleMesh {
320        &self.base_mesh
321    }
322
323    /// Number of refinement levels (vertex splits) available.
324    pub fn num_levels(&self) -> usize {
325        self.vertex_splits.len()
326    }
327
328    /// Serialize the progressive mesh to bytes using bincode.
329    pub fn serialize_to_bytes(&self) -> Result<Vec<u8>> {
330        bincode::serialize(self)
331            .map_err(|e| Error::InvalidData(format!("Serialization failed: {}", e)))
332    }
333
334    /// Deserialize a progressive mesh from bytes.
335    pub fn deserialize_from_bytes(data: &[u8]) -> Result<Self> {
336        bincode::deserialize(data)
337            .map_err(|e| Error::InvalidData(format!("Deserialization failed: {}", e)))
338    }
339}
340
341// ============================================================
342// Helper functions
343// ============================================================
344
345/// Snapshot face data around edge (v1, v2) before collapse.
346/// Returns Vec of (face_index, [v0, v1, v2]) for all faces incident on v1 or v2.
347fn snapshot_faces_around_edge(
348    hem: &HalfEdgeMesh,
349    v1: usize,
350    v2: usize,
351) -> Vec<(usize, [usize; 3])> {
352    let mut result = Vec::new();
353    let mut seen = HashSet::new();
354
355    for &v in &[v1, v2] {
356        for &he in &hem.outgoing_half_edges(v) {
357            let face = hem.half_edges[he].face;
358            if face == INVALID || !seen.insert(face) {
359                continue;
360            }
361            let he0 = hem.face_edge[face];
362            if he0 == INVALID {
363                continue;
364            }
365            let he1 = hem.half_edges[he0].next;
366            let fv0 = hem.source(he0);
367            let fv1 = hem.half_edges[he0].target;
368            let fv2 = hem.half_edges[he1].target;
369            result.push((face, [fv0, fv1, fv2]));
370        }
371    }
372
373    result
374}
375
376/// After collapse of v2 into v1, classify which faces were removed vs rewritten.
377fn classify_face_changes(
378    hem: &HalfEdgeMesh,
379    faces_before: &[(usize, [usize; 3])],
380    _v1: usize,
381    v2: usize,
382) -> (Vec<(usize, [usize; 3])>, Vec<(usize, [usize; 3])>) {
383    let mut removed = Vec::new();
384    let mut rewritten = Vec::new();
385
386    for &(fi, verts) in faces_before {
387        if hem.face_edge[fi] == INVALID {
388            // Face was removed during collapse
389            removed.push((fi, verts));
390        } else {
391            // Check if face was rewritten (had v2 replaced with v1)
392            let has_v2 = verts[0] == v2 || verts[1] == v2 || verts[2] == v2;
393            if has_v2 {
394                rewritten.push((fi, verts));
395            }
396        }
397    }
398
399    (removed, rewritten)
400}
401
402/// Build priority queue of edge collapse candidates (no boundary preservation).
403fn build_queue(hem: &HalfEdgeMesh) -> PriorityQueue<usize, EdgeCost> {
404    let mut queue = PriorityQueue::new();
405    let mut seen_edges: HashSet<(usize, usize)> = HashSet::new();
406    let mut edge_id = 0usize;
407
408    for vi in 0..hem.positions.len() {
409        if hem.vertex_removed[vi] {
410            continue;
411        }
412        for &he in &hem.outgoing_half_edges(vi) {
413            let target = hem.half_edges[he].target;
414            let key = (vi.min(target), vi.max(target));
415            if !seen_edges.insert(key) {
416                continue;
417            }
418
419            let (pos, cost) = hem.compute_collapse_cost(vi, target);
420
421            queue.push(
422                edge_id,
423                EdgeCost {
424                    v1: vi,
425                    v2: target,
426                    position: pos,
427                    cost,
428                },
429            );
430            edge_id += 1;
431        }
432    }
433
434    queue
435}
436
437/// Rebuild priority queue after many collapses.
438fn rebuild_queue(hem: &HalfEdgeMesh, id_offset: usize) -> PriorityQueue<usize, EdgeCost> {
439    let mut queue = PriorityQueue::new();
440    let mut seen_edges: HashSet<(usize, usize)> = HashSet::new();
441    let mut edge_id = id_offset;
442
443    for vi in 0..hem.positions.len() {
444        if hem.vertex_removed[vi] || hem.vertex_edge[vi] == INVALID {
445            continue;
446        }
447        for &he in &hem.outgoing_half_edges(vi) {
448            if hem.half_edges[he].face == INVALID {
449                continue;
450            }
451            let target = hem.half_edges[he].target;
452            let key = (vi.min(target), vi.max(target));
453            if !seen_edges.insert(key) {
454                continue;
455            }
456
457            let (pos, cost) = hem.compute_collapse_cost(vi, target);
458
459            queue.push(
460                edge_id,
461                EdgeCost {
462                    v1: vi,
463                    v2: target,
464                    position: pos,
465                    cost,
466                },
467            );
468            edge_id += 1;
469        }
470    }
471
472    queue
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use nalgebra::Point3;
479
480    fn make_tetrahedron() -> TriangleMesh {
481        TriangleMesh::from_vertices_and_faces(
482            vec![
483                Point3::new(0.0, 0.0, 0.0),
484                Point3::new(1.0, 0.0, 0.0),
485                Point3::new(0.5, 1.0, 0.0),
486                Point3::new(0.5, 0.5, 1.0),
487            ],
488            vec![[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]],
489        )
490    }
491
492    fn make_plane_grid(size: usize) -> TriangleMesh {
493        let mut vertices = Vec::new();
494        for y in 0..size {
495            for x in 0..size {
496                vertices.push(Point3::new(x as f32, y as f32, 0.0));
497            }
498        }
499        let mut faces = Vec::new();
500        for y in 0..(size - 1) {
501            for x in 0..(size - 1) {
502                let tl = y * size + x;
503                let tr = tl + 1;
504                let bl = (y + 1) * size + x;
505                let br = bl + 1;
506                faces.push([tl, bl, tr]);
507                faces.push([tr, bl, br]);
508            }
509        }
510        TriangleMesh::from_vertices_and_faces(vertices, faces)
511    }
512
513    #[test]
514    fn test_progressive_from_empty_mesh() {
515        let mesh = TriangleMesh::new();
516        assert!(ProgressiveMesh::from_mesh(&mesh, 0.5).is_err());
517    }
518
519    #[test]
520    fn test_progressive_from_tetrahedron() {
521        let mesh = make_tetrahedron();
522        let pm = ProgressiveMesh::from_mesh(&mesh, 0.5).unwrap();
523        assert_eq!(pm.full_vertex_count, 4);
524        assert_eq!(pm.full_face_count, 4);
525        assert!(pm.base_mesh.face_count() <= mesh.face_count());
526    }
527
528    #[test]
529    fn test_progressive_from_grid() {
530        let mesh = make_plane_grid(6);
531        let pm = ProgressiveMesh::from_mesh(&mesh, 0.3).unwrap();
532
533        // Base mesh should have fewer faces than original
534        assert!(
535            pm.base_mesh.face_count() < mesh.face_count(),
536            "base should have fewer faces: {} vs {}",
537            pm.base_mesh.face_count(),
538            mesh.face_count()
539        );
540
541        // Should have at least one vertex split
542        assert!(
543            !pm.vertex_splits.is_empty(),
544            "should have vertex splits recorded"
545        );
546    }
547
548    #[test]
549    fn test_progressive_base_access() {
550        let mesh = make_plane_grid(6);
551        let pm = ProgressiveMesh::from_mesh(&mesh, 0.3).unwrap();
552
553        let base = pm.base();
554        assert_eq!(base.face_count(), pm.base_mesh.face_count());
555    }
556
557    #[test]
558    fn test_progressive_num_levels() {
559        let mesh = make_plane_grid(6);
560        let pm = ProgressiveMesh::from_mesh(&mesh, 0.3).unwrap();
561
562        assert!(pm.num_levels() > 0);
563    }
564
565    #[test]
566    fn test_progressive_reconstruct_level_zero_is_base() {
567        let mesh = make_plane_grid(6);
568        let pm = ProgressiveMesh::from_mesh(&mesh, 0.3).unwrap();
569
570        let level0 = pm.reconstruct_at_level(0);
571        assert_eq!(level0.vertex_count(), pm.base_mesh.vertex_count());
572        assert_eq!(level0.face_count(), pm.base_mesh.face_count());
573    }
574
575    #[test]
576    fn test_progressive_reconstruct_ratio_zero_is_base() {
577        let mesh = make_plane_grid(6);
578        let pm = ProgressiveMesh::from_mesh(&mesh, 0.3).unwrap();
579
580        let r0 = pm.reconstruct_at_ratio(0.0);
581        assert_eq!(r0.vertex_count(), pm.base_mesh.vertex_count());
582        assert_eq!(r0.face_count(), pm.base_mesh.face_count());
583    }
584
585    #[test]
586    fn test_progressive_monotonic_detail() {
587        let mesh = make_plane_grid(8);
588        let pm = ProgressiveMesh::from_mesh(&mesh, 0.2).unwrap();
589
590        if pm.num_levels() < 2 {
591            return; // Not enough levels to test monotonicity
592        }
593
594        let mut prev_faces = pm.base_mesh.face_count();
595        let step = (pm.num_levels() / 4).max(1);
596
597        for level in (step..=pm.num_levels()).step_by(step) {
598            let reconstructed = pm.reconstruct_at_level(level);
599            assert!(
600                reconstructed.face_count() >= prev_faces,
601                "face count should monotonically increase: level {} has {} faces, prev had {}",
602                level,
603                reconstructed.face_count(),
604                prev_faces
605            );
606            prev_faces = reconstructed.face_count();
607        }
608    }
609
610    #[test]
611    fn test_progressive_with_normals() {
612        let mut mesh = make_plane_grid(5);
613        let normals: Vec<Vector3f> = (0..mesh.vertex_count())
614            .map(|_| Vector3f::new(0.0, 0.0, 1.0))
615            .collect();
616        mesh.set_normals(normals);
617
618        let pm = ProgressiveMesh::from_mesh(&mesh, 0.3).unwrap();
619        assert!(pm.base_mesh.normals.is_some());
620    }
621
622    #[test]
623    fn test_progressive_with_colors() {
624        let mut mesh = make_plane_grid(5);
625        let colors: Vec<[u8; 3]> = (0..mesh.vertex_count()).map(|_| [128, 64, 200]).collect();
626        mesh.set_colors(colors);
627
628        let pm = ProgressiveMesh::from_mesh(&mesh, 0.3).unwrap();
629        assert!(pm.base_mesh.colors.is_some());
630    }
631
632    #[test]
633    fn test_progressive_serialization_roundtrip() {
634        let mesh = make_tetrahedron();
635        let pm = ProgressiveMesh::from_mesh(&mesh, 0.5).unwrap();
636
637        let bytes = pm.serialize_to_bytes().unwrap();
638        assert!(!bytes.is_empty());
639
640        let pm2 = ProgressiveMesh::deserialize_from_bytes(&bytes).unwrap();
641        assert_eq!(pm2.full_vertex_count, pm.full_vertex_count);
642        assert_eq!(pm2.full_face_count, pm.full_face_count);
643        assert_eq!(pm2.vertex_splits.len(), pm.vertex_splits.len());
644        assert_eq!(pm2.base_mesh.face_count(), pm.base_mesh.face_count());
645    }
646
647    #[test]
648    fn test_progressive_clamp_ratio() {
649        let mesh = make_plane_grid(6);
650        let pm = ProgressiveMesh::from_mesh(&mesh, 0.3).unwrap();
651
652        // Should not panic for out-of-range ratios
653        let _ = pm.reconstruct_at_ratio(-1.0);
654        let _ = pm.reconstruct_at_ratio(2.0);
655    }
656
657    #[test]
658    fn test_progressive_clamp_level() {
659        let mesh = make_plane_grid(6);
660        let pm = ProgressiveMesh::from_mesh(&mesh, 0.3).unwrap();
661
662        // Should not panic for out-of-range levels
663        let _ = pm.reconstruct_at_level(999999);
664    }
665}