Skip to main content

mesh_graph/
lib.rs

1//! MeshGraph is a halfedge data structure for representing triangle meshes.
2//!
3//! This is heavily inspired by [SMesh](https://github.com/Bendzae/SMesh) and
4//! [OpenMesh](https://gitlab.vci.rwth-aachen.de:9000/OpenMesh/OpenMesh).
5//!
6//! ## Features
7//!
8//! - Fast spatial queries using parry3d's Bvh
9//! - High performance using slotmap
10//! - Easy integration with Bevy game engine using the `bevy` Cargo feature
11//! - Good debugging using `rerun` Cargo feature to enable the Rerun integration
12//! - Best in class documentation with illustrations
13//!
14//! ## Usage
15//!
16//! ```
17//! use mesh_graph::{MeshGraph, primitives::IcoSphere};
18//!
19//! // Create a new mesh
20//! let mesh_graph = MeshGraph::from(IcoSphere { radius: 10.0, subdivisions: 2 });
21//!
22//! // Get some vertex ID and its vertex node
23//! let (vertex_id, vertex) = mesh_graph.vertices.iter().next().unwrap();
24//!
25//! // Iterate over all outgoing halfedges of the vertex
26//! for halfedge_id in vertex.outgoing_halfedges(&mesh_graph) {
27//!     // do sth
28//! }
29//!
30//! // Get the position of the vertex
31//! let position = mesh_graph.positions[vertex_id];
32//! ```
33//!
34//! Check out the crate [freestyle-sculpt](https://github.com/Synphonyte/freestyle-sculpt) for
35//! a heavy duty example.
36//!
37//! ## Connectivity
38//!
39//! ### Halfedge
40//!
41//! <img src="https://raw.githubusercontent.com/Synphonyte/mesh-graph/refs/heads/main/docs/halfedge/all.svg" alt="Connectivity" style="max-width: 28em" />
42//!
43//! ### Vertex
44//!
45//! <img src="https://raw.githubusercontent.com/Synphonyte/mesh-graph/refs/heads/main/docs/vertex/all.svg" alt="Connectivity" style="max-width: 50em" />
46
47mod access;
48mod elements;
49pub mod integrations;
50mod iter;
51mod ops;
52mod plane_slice;
53pub mod primitives;
54#[cfg(feature = "rerun")]
55mod rerun_impl;
56mod selection;
57#[cfg(feature = "serde")]
58mod serialize;
59pub mod utils;
60
61pub use elements::*;
62pub use iter::*;
63pub use ops::*;
64pub use plane_slice::*;
65pub use selection::*;
66
67use hashbrown::HashMap;
68use parry3d::partitioning::{Bvh, BvhWorkspace};
69
70use glam::Vec3;
71use slotmap::{SecondaryMap, SlotMap};
72use tracing::{error, instrument};
73
74use crate::utils::unwrap_or_return;
75
76#[cfg(feature = "rerun")]
77lazy_static::lazy_static! {
78    pub static ref RR: rerun::RecordingStream = rerun::RecordingStreamBuilder::new("mesh_graph").spawn().unwrap();
79}
80
81/// Halfedge data structure for representing triangle meshes.
82///
83/// Please see the [crate documentation](crate) for more information.
84#[derive(Clone, Default)]
85#[cfg_attr(feature = "bevy", derive(bevy::prelude::Component))]
86#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
87#[cfg_attr(
88    feature = "serde",
89    serde(from = "crate::serialize::MeshGraphIntermediate")
90)]
91pub struct MeshGraph {
92    /// Acceleration structure for fast spatial queries. Uses parry3d's Bvh to implement some of parry3d's spatial queries.
93    #[cfg_attr(feature = "serde", serde(skip))]
94    pub bvh: Bvh,
95    /// Used in conjunction with the BVH to accelerate spatial queries.
96    #[cfg_attr(feature = "serde", serde(skip))]
97    pub bvh_workspace: BvhWorkspace,
98    /// Used to map indices stored in the BVH to face IDs.
99    #[cfg_attr(feature = "serde", serde(skip))]
100    pub index_to_face_id: HashMap<u32, FaceId>,
101    /// Used to compute the next index for a new face
102    #[cfg_attr(feature = "serde", serde(skip))]
103    pub next_index: u32,
104
105    /// Maps vertex IDs to their corresponding graph node
106    pub vertices: SlotMap<VertexId, Vertex>,
107    /// Maps halfedge IDs to their corresponding graph node
108    pub halfedges: SlotMap<HalfedgeId, Halfedge>,
109    /// Maps face IDs to their corresponding graph node
110    pub faces: SlotMap<FaceId, Face>,
111
112    /// Maps vertex IDs to their corresponding positions
113    pub positions: SecondaryMap<VertexId, Vec3>,
114    /// Maps vertex IDs to their corresponding normals
115    pub vertex_normals: Option<SecondaryMap<VertexId, Vec3>>,
116
117    /// Maps vertex IDs to their corresponding outgoing halfedges (not in any particular order)
118    #[cfg_attr(feature = "serde", serde(skip))]
119    pub outgoing_halfedges: SecondaryMap<VertexId, Vec<HalfedgeId>>,
120}
121
122impl MeshGraph {
123    /// Create a new empty mesh graph
124    #[inline]
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    /// Create a triangle mesh graph from vertex positions.
130    /// Every three positions represent a triangle.
131    ///
132    /// Vertices with the same position are merged into a single vertex.
133    pub fn triangles(vertex_positions: &[Vec3]) -> Self {
134        assert!(
135            vertex_positions.len().is_multiple_of(3),
136            "Number of vertex positions should be a multiple of 3"
137        );
138
139        // Create a map to track unique vertices
140        let mut unique_positions: Vec<Vec3> = Vec::with_capacity(vertex_positions.len() / 3);
141        let mut face_indices = Vec::with_capacity(vertex_positions.len());
142
143        for vertex_pos in vertex_positions {
144            // Check if we've seen this position before using a fuzzy float comparison
145            let mut idx = None;
146            for (j, pos) in unique_positions.iter().enumerate() {
147                const EPSILON: f32 = 1e-5;
148
149                if pos.distance_squared(*vertex_pos) < EPSILON {
150                    idx = Some(j);
151                    break;
152                }
153            }
154
155            // Use the existing index or add a new vertex
156            let vertex_idx = if let Some(idx) = idx {
157                idx
158            } else {
159                let new_idx = unique_positions.len();
160                unique_positions.push(*vertex_pos);
161
162                #[cfg(feature = "rerun")]
163                RR.log(
164                    "meshgraph/construct/vertices",
165                    &rerun::Points3D::new(unique_positions.iter().map(crate::utils::vec3_array)),
166                )
167                .unwrap();
168
169                new_idx
170            };
171
172            // Add to face indices
173            face_indices.push(vertex_idx);
174        }
175
176        // Use indexed_triangles to create the mesh
177        Self::indexed_triangles(&unique_positions, &face_indices)
178    }
179
180    /// Create a triangle mesh graph from vertex positions, face indices,
181    /// and a custom vertex attribute.
182    #[instrument]
183    pub fn indexed_triangles_with_custom_attribute<T>(
184        vertex_positions: &[Vec3],
185        face_indices: &[usize],
186        custom_attribute: &[T],
187    ) -> (Self, SecondaryMap<VertexId, T>)
188    where
189        T: Clone + std::fmt::Debug,
190    {
191        let (mesh_graph, vertex_ids) =
192            Self::indexed_triangles_and_vertex_ids(vertex_positions, face_indices);
193
194        let mut custom_attribute_map = SecondaryMap::with_capacity(custom_attribute.len());
195        for (attr, vertex_id) in custom_attribute.iter().zip(vertex_ids) {
196            custom_attribute_map.insert(vertex_id, attr.clone());
197        }
198
199        (mesh_graph, custom_attribute_map)
200    }
201
202    /// Create a triangle mesh graph from vertex positions and face indices.
203    /// Every chunk of three indices represents a triangle.
204    #[inline]
205    pub fn indexed_triangles(vertex_positions: &[Vec3], face_indices: &[usize]) -> Self {
206        Self::indexed_triangles_and_vertex_ids(vertex_positions, face_indices).0
207    }
208
209    /// Create a triangle mesh graph from vertex positions and face indices,
210    /// returning the graph and a list of vertex IDs in the same order as `vertex_positions`.
211    #[instrument]
212    pub fn indexed_triangles_and_vertex_ids(
213        vertex_positions: &[Vec3],
214        face_indices: &[usize],
215    ) -> (Self, Vec<VertexId>) {
216        let mut mesh_graph = Self {
217            bvh: Bvh::new(),
218            bvh_workspace: BvhWorkspace::default(),
219            index_to_face_id: HashMap::with_capacity(face_indices.len() / 3),
220            next_index: 0,
221
222            vertices: SlotMap::with_capacity_and_key(vertex_positions.len()),
223            halfedges: SlotMap::with_capacity_and_key(face_indices.len()),
224            faces: SlotMap::with_capacity_and_key(face_indices.len() / 3),
225
226            positions: SecondaryMap::with_capacity(vertex_positions.len()),
227            vertex_normals: None,
228            outgoing_halfedges: SecondaryMap::with_capacity(vertex_positions.len()),
229        };
230
231        let mut vertex_ids = Vec::with_capacity(vertex_positions.len());
232
233        for pos in vertex_positions {
234            vertex_ids.push(mesh_graph.add_vertex(*pos));
235        }
236
237        for chunk in face_indices.chunks_exact(3) {
238            let a = vertex_ids[chunk[0]];
239            let b = vertex_ids[chunk[1]];
240            let c = vertex_ids[chunk[2]];
241
242            if a == b || b == c || c == a {
243                #[cfg(feature = "rerun")]
244                RR.log(
245                    "meshgraph/construct/zero_face",
246                    &rerun::Points3D::new(
247                        [
248                            mesh_graph.positions[a],
249                            mesh_graph.positions[b],
250                            mesh_graph.positions[c],
251                        ]
252                        .iter()
253                        .map(crate::utils::vec3_array),
254                    ),
255                )
256                .unwrap();
257
258                continue;
259            }
260
261            // Vertices have already been added to the mesh graph, so we can safely use `unwrap()` here
262            let he_a_id = mesh_graph.add_or_get_edge(a, b).unwrap().start_to_end_he_id;
263            let he_b_id = mesh_graph.add_or_get_edge(b, c).unwrap().start_to_end_he_id;
264            let he_c_id = mesh_graph.add_or_get_edge(c, a).unwrap().start_to_end_he_id;
265
266            let _face_id = mesh_graph.add_face(he_a_id, he_b_id, he_c_id);
267        }
268
269        mesh_graph.make_all_outgoing_halfedges_boundary_if_possible();
270        mesh_graph.rebuild_bvh();
271
272        (mesh_graph, vertex_ids)
273    }
274
275    /// Computes the vertex normal from neighboring faces
276    pub fn compute_vertex_normal(&mut self, vertex_id: VertexId) {
277        if self.vertex_normals.is_none() {
278            return;
279        }
280
281        let vertex = unwrap_or_return!(self.vertices.get(vertex_id), "Vertex not found");
282
283        let mut normal = Vec3::ZERO;
284
285        for face_id in vertex.faces(self) {
286            let face = unwrap_or_return!(self.faces.get(face_id), "Face not found");
287            normal += unwrap_or_return!(face.normal(self), "Face normal not found");
288        }
289
290        self.vertex_normals
291            .as_mut()
292            .unwrap()
293            .insert(vertex_id, normal.try_normalize().unwrap_or(Vec3::ZERO));
294    }
295
296    /// Computes the vertex normals by averaging over the computed face normals
297    #[instrument(skip(self))]
298    pub fn compute_vertex_normals(&mut self) {
299        let mut normals = SecondaryMap::with_capacity(self.vertices.len());
300
301        for face in self.faces.values() {
302            let ha_a_id = face.halfedge;
303            let he_a = self.halfedges[ha_a_id];
304
305            let he_b_id = he_a
306                .next
307                .expect("Halfedge has definitely a face and thus a next halfedge");
308            let he_b = self.halfedges[he_b_id];
309
310            let a = match he_a.start_vertex(self) {
311                Some(v) => v,
312                None => {
313                    error!("Start vertex not found");
314                    continue;
315                }
316            };
317            let b = he_a.end_vertex;
318            let c = he_b.end_vertex;
319
320            let (Some(pos_a), Some(pos_b), Some(pos_c)) = (
321                self.positions.get(a),
322                self.positions.get(b),
323                self.positions.get(c),
324            ) else {
325                continue;
326            };
327
328            let diff_a = pos_c - pos_a;
329            let diff_b = pos_c - pos_b;
330
331            // TODO : normalizing necessary here?
332            let face_normal = diff_a.cross(diff_b);
333
334            *normals.entry(a).unwrap().or_default() += face_normal;
335            *normals.entry(b).unwrap().or_default() += face_normal;
336            *normals.entry(c).unwrap().or_default() += face_normal;
337        }
338
339        self.vertex_normals = Some(normals);
340        self.normalize_vertex_normals();
341    }
342
343    /// Ensures that the vertex normals are all normalized
344    pub fn normalize_vertex_normals(&mut self) {
345        if let Some(normals) = &mut self.vertex_normals {
346            for normal in normals.values_mut() {
347                *normal = normal.normalize_or_zero();
348            }
349        }
350    }
351
352    /// Calls the `optimize_incremental` method of the BVH.
353    #[inline]
354    pub fn optimize_bvh_incremental(&mut self) {
355        self.bvh.optimize_incremental(&mut self.bvh_workspace);
356    }
357
358    /// Recomputes the bounding boxes of the BVH. This is necessary when the mesh is modified.
359    #[inline]
360    pub fn refit_bvh(&mut self) {
361        self.bvh.refit(&mut self.bvh_workspace);
362    }
363
364    /// Rebuilds the BVH from scratch
365    #[inline]
366    pub fn rebuild_bvh(&mut self) {
367        self.bvh = Bvh::new();
368        self.bvh_workspace = BvhWorkspace::default();
369
370        for face in self.faces.values() {
371            self.bvh
372                .insert_or_update_partially(face.aabb(self), face.index, 0.0);
373        }
374        self.bvh
375            .rebuild(&mut self.bvh_workspace, Default::default());
376    }
377
378    #[instrument(skip_all)]
379    pub fn rebuild_outgoing_halfedges(&mut self) {
380        self.outgoing_halfedges.clear();
381
382        for halfedge in self.halfedges.values() {
383            let Some(twin_id) = halfedge.twin else {
384                error!("Halfedge has no twin");
385                continue;
386            };
387
388            let Some(entry) = self.outgoing_halfedges.entry(halfedge.end_vertex) else {
389                error!("Vertex key invalid");
390                continue;
391            };
392
393            entry.or_default().push(twin_id);
394        }
395    }
396}