avian3d/collision/collider/
trimesh_builder.rs

1//! Types used for creating triangle meshes from [`Collider`]s.
2
3use core::num::NonZeroU32;
4
5use bevy::prelude::*;
6use parry::shape::{SharedShape, TypedShape};
7use thiserror::Error;
8
9use crate::prelude::*;
10
11/// An ergonomic builder for triangle meshes from [`Collider`]s.
12///
13/// The builder can configure different subdivision levels for different shapes.
14/// If a shape was not explicitly configured, the builder will use [`Self::fallback_subdivisions`].
15///
16/// Shapes with rounded corners such as [`Collider::round_cuboid`] will be subdivided as if they were not rounded.
17///
18/// # Example
19///
20/// ```
21/// # use avian3d::{prelude::*, math::Vector};
22///
23/// let collider = Collider::sphere(1.0);
24///
25/// // Using default settings
26/// let trimesh = collider.trimesh_builder().build().unwrap();
27///
28/// // Using extra subdivisions
29/// let trimesh = collider
30///     .trimesh_builder()
31///     .sphere_subdivisions(20, 20)
32///     .build()
33///     .unwrap();
34///
35/// // Setting different subdivisions for different shapes
36/// let trimesh = collider
37///     .trimesh_builder()
38///     .sphere_subdivisions(20, 20)
39///     .capsule_subdivisions(10, 5)
40///     .fallback_subdivisions(15)
41///     .build()
42///     .unwrap();
43///
44/// // Generating the trimesh with a transformation
45/// let trimesh = collider
46///     .trimesh_builder()
47///     .translated(Vector::new(1.0, 0.0, 0.0))
48///     .build()
49///     .unwrap();
50/// ```
51#[derive(Debug, Clone)]
52pub struct TrimeshBuilder {
53    /// The shape to be converted into a triangle mesh.
54    pub shape: SharedShape,
55    /// The position of the shape. The default is [0, 0, 0].
56    pub position: Position,
57    /// The rotation of the shape. The default is the identity rotation.
58    pub rotation: Rotation,
59    /// Whether a failure to trimesh a subshape in a compound shape should fail the entire build process.
60    /// Default is true.
61    pub fail_on_compound_error: bool,
62    /// The number of subdivisions to use for shapes that do not have a specific subdivision count.
63    /// Default is 16.
64    pub fallback_subdivisions: NonZeroU32,
65    /// The number of subdivisions for shapes that derive from a sphere.
66    /// Default is None.
67    pub sphere_subdivisions: Option<(NonZeroU32, NonZeroU32)>,
68    /// The number of subdivisions for shapes that derive from a capsule.
69    /// Default is None.
70    pub capsule_subdivision: Option<(NonZeroU32, NonZeroU32)>,
71    /// The number of subdivisions for shapes that derive from a cylinder.
72    /// Default is None.
73    pub cylinder_subdivisions: Option<NonZeroU32>,
74    /// The number of subdivisions for shapes that derive from a cone.
75    /// Default is None.
76    pub cone_subdivisions: Option<NonZeroU32>,
77}
78
79/// A generic triangle mesh representation.
80#[derive(Debug, Clone, PartialEq, Reflect, Default)]
81pub struct Trimesh {
82    /// The vertices in
83    pub vertices: Vec<Vector>,
84    /// The indices in counter-clockwise winding
85    pub indices: Vec<[u32; 3]>,
86}
87
88impl Trimesh {
89    /// Extends the trimesh with the vertices and indices of another trimesh.
90    /// The indices of `other` will be offset by the number of vertices in `self`.
91    pub fn extend(&mut self, other: Trimesh) {
92        let next_vertex_index = self.vertices.len() as u32;
93        self.vertices.extend(other.vertices);
94        self.indices.extend(
95            other
96                .indices
97                .iter()
98                .map(|is| is.map(|i| i + next_vertex_index)),
99        );
100    }
101}
102
103/// An error that can occur when building a triangle mesh with [`TrimeshBuilder::build`].
104#[derive(Debug, Error)]
105pub enum TrimeshBuilderError {
106    /// The shape is not supported by the builder.
107    #[error("Unsupported shape type: {0}")]
108    UnsupportedShape(String),
109}
110
111impl TrimeshBuilder {
112    /// Creates a new [`TrimeshBuilder`] for the given shape. Usually you'll want to call [`Collider::trimesh_builder`] instead.
113    pub fn new(shape: SharedShape) -> Self {
114        TrimeshBuilder {
115            shape,
116            position: default(),
117            rotation: default(),
118            fail_on_compound_error: true,
119            // arbitrary number
120            fallback_subdivisions: 16_u32.try_into().unwrap(),
121            sphere_subdivisions: None,
122            capsule_subdivision: None,
123            cylinder_subdivisions: None,
124            cone_subdivisions: None,
125        }
126    }
127
128    /// Translates the mesh. Subsequent calls to this method will add to the previous translation.
129    pub fn translated(&mut self, position: impl Into<Position>) -> &mut Self {
130        self.position.0 += position.into().0;
131        self
132    }
133
134    /// Rotates the mesh. Subsequent calls to this method will add to the previous rotation.
135    pub fn rotated(&mut self, rotation: impl Into<Rotation>) -> &mut Self {
136        self.rotation = rotation.into() * self.rotation;
137        self
138    }
139
140    /// Sets the fallback subdivision count for shapes that don't have a specific subdivision count.
141    /// Default is 16.
142    pub fn fallback_subdivisions(&mut self, subdivisions: impl TryInto<NonZeroU32>) -> &mut Self {
143        self.fallback_subdivisions = subdivisions
144            .try_into()
145            .unwrap_or_else(|_| panic!("Fallback subdivision count must be non-zero"));
146        self
147    }
148
149    /// Sets the subdivision count for sphere shapes. `theta` is the number of subdivisions along the
150    /// latitude, and `phi` is the number of subdivisions along the longitude.
151    pub fn sphere_subdivisions(
152        &mut self,
153        theta: impl TryInto<NonZeroU32>,
154        phi: impl TryInto<NonZeroU32>,
155    ) -> &mut Self {
156        self.sphere_subdivisions = Some((
157            theta
158                .try_into()
159                .unwrap_or_else(|_| panic!("Sphere theta subdivisions must be non-zero")),
160            phi.try_into()
161                .inspect(|phi| {
162                    assert!(phi.get() >= 2, "Sphere phi subdivisions must be at least 2")
163                })
164                .unwrap_or_else(|_| panic!("Sphere phi subdivisions must be non-zero")),
165        ));
166        self
167    }
168
169    /// Sets the subdivision count for capsule shapes. `theta` is the number of subdivisions along the
170    /// latitude, and `phi` is the number of subdivisions along the longitude.
171    pub fn capsule_subdivisions(
172        &mut self,
173        theta: impl TryInto<NonZeroU32>,
174        phi: impl TryInto<NonZeroU32>,
175    ) -> &mut Self {
176        self.capsule_subdivision = Some((
177            theta
178                .try_into()
179                .unwrap_or_else(|_| panic!("Capsule theta subdivisions must be non-zero")),
180            phi.try_into()
181                .inspect(|phi| {
182                    assert!(
183                        phi.get() >= 2,
184                        "Capsule phi subdivisions must be at least 2"
185                    )
186                })
187                .unwrap_or_else(|_| panic!("Capsule phi subdivisions must be non-zero")),
188        ));
189        self
190    }
191
192    /// Sets the subdivision count for cylinder shapes.
193    pub fn cylinder_subdivisions(&mut self, subdivisions: impl TryInto<NonZeroU32>) -> &mut Self {
194        self.cylinder_subdivisions = Some(
195            subdivisions
196                .try_into()
197                .unwrap_or_else(|_| panic!("Cylinder subdivisions must be non-zero")),
198        );
199        self
200    }
201
202    /// Sets the subdivision count for cone shapes.
203    pub fn cone_subdivisions(&mut self, subdivisions: impl TryInto<NonZeroU32>) -> &mut Self {
204        self.cone_subdivisions = Some(
205            subdivisions
206                .try_into()
207                .unwrap_or_else(|_| panic!("Cone subdivisions must be non-zero")),
208        );
209        self
210    }
211
212    /// Whether a failure to trimesh a subshape in a compound shape should fail the entire build process.
213    /// Default is true.
214    pub fn fail_on_compound_error(&mut self, fail_on_compound_error: bool) -> &mut Self {
215        self.fail_on_compound_error = fail_on_compound_error;
216        self
217    }
218
219    fn subdivisions(&self, get: impl Fn(&Self) -> Option<NonZeroU32>) -> u32 {
220        get(self).unwrap_or(self.fallback_subdivisions).into()
221    }
222
223    /// Builds the trimesh from the configured settings.
224    ///
225    /// Returns an error if the shape is not supported.
226    /// If the shape is a compound, errors in a subshape will either be ignored or fail the entire build process
227    /// depending on the value of [`Self::fail_on_compound_error`].
228    pub fn build(&self) -> Result<Trimesh, TrimeshBuilderError> {
229        let (vertices, indices) = match self.shape.as_typed_shape() {
230            // Simple cases
231            TypedShape::Cuboid(cuboid) => cuboid.to_trimesh(),
232            TypedShape::Voxels(voxels) => voxels.to_trimesh(),
233            TypedShape::ConvexPolyhedron(convex_polyhedron) => convex_polyhedron.to_trimesh(),
234            TypedShape::HeightField(height_field) => height_field.to_trimesh(),
235            // Triangles
236            TypedShape::Triangle(triangle) => {
237                (vec![triangle.a, triangle.b, triangle.c], vec![[0, 1, 2]])
238            }
239            TypedShape::TriMesh(tri_mesh) => {
240                (tri_mesh.vertices().to_vec(), tri_mesh.indices().to_vec())
241            }
242            // Need subdivisions
243            TypedShape::Ball(ball) => ball.to_trimesh(
244                self.subdivisions(|t| t.sphere_subdivisions?.0.into()),
245                self.subdivisions(|t| t.sphere_subdivisions?.1.into()),
246            ),
247            TypedShape::Capsule(capsule) => capsule.to_trimesh(
248                self.subdivisions(|t| t.capsule_subdivision?.0.into()),
249                self.subdivisions(|t| t.capsule_subdivision?.1.into()),
250            ),
251            TypedShape::Cylinder(cylinder) => {
252                cylinder.to_trimesh(self.subdivisions(|t| t.cylinder_subdivisions))
253            }
254            TypedShape::Cone(cone) => cone.to_trimesh(self.subdivisions(|t| t.cone_subdivisions)),
255            // Compounds need to be unpacked
256            TypedShape::Compound(compound) => {
257                let mut sub_builder = self.clone();
258                return compound.shapes().iter().try_fold(
259                    Trimesh::default(),
260                    move |mut compound_trimesh, (sub_pos, shape)| {
261                        sub_builder.shape = shape.clone();
262                        sub_builder.position = Position(
263                            self.position.0 + self.rotation * Vector::from(sub_pos.translation),
264                        );
265                        sub_builder.rotation = self
266                            .rotation
267                            .mul_quat(sub_pos.rotation.into())
268                            .normalize()
269                            .into();
270                        let trimesh = match sub_builder.build() {
271                            Ok(trimesh) => trimesh,
272                            Err(error) => {
273                                return if self.fail_on_compound_error {
274                                    Err(error)
275                                } else {
276                                    Ok(compound_trimesh)
277                                };
278                            }
279                        };
280
281                        compound_trimesh.extend(trimesh);
282
283                        // No need to track recursive compounds because parry panics on nested compounds anyways lol
284                        Ok(compound_trimesh)
285                    },
286                );
287            }
288            // Rounded shapes ignore the rounding and use the inner shape
289            TypedShape::RoundCuboid(round_shape) => round_shape.inner_shape.to_trimesh(),
290            TypedShape::RoundTriangle(round_shape) => (
291                vec![
292                    round_shape.inner_shape.a,
293                    round_shape.inner_shape.b,
294                    round_shape.inner_shape.c,
295                ],
296                vec![[0, 1, 2]],
297            ),
298            TypedShape::RoundConvexPolyhedron(round_shape) => round_shape.inner_shape.to_trimesh(),
299            TypedShape::RoundCylinder(round_shape) => round_shape
300                .inner_shape
301                .to_trimesh(self.subdivisions(|t| t.cylinder_subdivisions)),
302            TypedShape::RoundCone(round_shape) => round_shape
303                .inner_shape
304                .to_trimesh(self.subdivisions(|t| t.cone_subdivisions)),
305            // Not supported
306            TypedShape::Segment(segment) => {
307                return Err(TrimeshBuilderError::UnsupportedShape(format!(
308                    "{segment:?}",
309                )));
310            }
311            TypedShape::Polyline(polyline) => {
312                return Err(TrimeshBuilderError::UnsupportedShape(format!(
313                    "{polyline:?}",
314                )));
315            }
316            TypedShape::HalfSpace(half_space) => {
317                return Err(TrimeshBuilderError::UnsupportedShape(format!(
318                    "{half_space:?}",
319                )));
320            }
321            TypedShape::Custom(_shape) => {
322                return Err(TrimeshBuilderError::UnsupportedShape("Custom".to_string()));
323            }
324        };
325        let pos = self.position;
326        Ok(Trimesh {
327            vertices: vertices
328                .into_iter()
329                .map(|v| pos.0 + Vector::from(self.rotation * Vector::from(v)))
330                .collect(),
331            indices,
332        })
333    }
334}
335
336impl Collider {
337    /// Create a [`TrimeshBuilder`] for building a [`Trimesh`].
338    pub fn trimesh_builder(&self) -> TrimeshBuilder {
339        TrimeshBuilder::new(self.shape_scaled().clone())
340    }
341}
342
343#[cfg(feature = "collider-from-mesh")]
344impl From<Trimesh> for Mesh {
345    fn from(trimesh: Trimesh) -> Self {
346        use bevy::asset::RenderAssetUsages;
347        use bevy::mesh::{Indices, PrimitiveTopology, VertexAttributeValues, prelude::*};
348
349        let mut mesh = Mesh::new(
350            PrimitiveTopology::TriangleList,
351            RenderAssetUsages::default(),
352        );
353        mesh.insert_attribute(
354            Mesh::ATTRIBUTE_POSITION,
355            VertexAttributeValues::Float32x3(
356                trimesh
357                    .vertices
358                    .into_iter()
359                    .map(|v| v.f32().to_array())
360                    .collect(),
361            ),
362        );
363        mesh.insert_indices(Indices::U32(
364            trimesh.indices.into_iter().flatten().collect(),
365        ));
366        mesh.compute_normals();
367        if let Err(err) = mesh.generate_tangents() {
368            warn!("Failed to generate tangents for mesh: {err}");
369        }
370
371        mesh
372    }
373}
374
375#[cfg(test)]
376mod tests {
377    use bevy_math::DVec3;
378
379    use super::*;
380
381    #[test]
382    fn rasterizes_cuboid() {
383        let collider = Collider::cuboid(1.0, 2.0, 3.0);
384        let trimesh = collider.trimesh_builder().build().unwrap();
385        assert_eq!(trimesh.vertices.len(), 8);
386        assert_eq!(trimesh.indices.len(), 12);
387    }
388
389    #[test]
390    fn rasterizes_compound() {
391        let a = Collider::cuboid(1.0, 2.0, 3.0);
392        let b = Collider::sphere(0.4);
393        let collider = Collider::compound(vec![
394            (Vector::new(1.0, 2.0, 3.0), Quat::from_rotation_z(0.2), a),
395            (
396                Vector::new(-12.0, 4.0, -0.01),
397                Quat::from_rotation_x(0.1),
398                b,
399            ),
400        ]);
401        let trimesh = collider
402            .trimesh_builder()
403            .fallback_subdivisions(2)
404            .translated(Vector::new(3.0, -2.0, 0.0))
405            .rotated(Quat::from_rotation_y(-3.0))
406            .build()
407            .unwrap();
408        assert_eq!(
409            trimesh.vertices,
410            vec![
411                DVec3::new(1.6634156046802073, -1.0794012104819941, -4.354963570435039)
412                    .adjust_precision(),
413                DVec3::new(2.086775603322501, -1.079401210481994, -1.3849860769934481)
414                    .adjust_precision(),
415                DVec3::new(1.1165170453015136, -0.8807318727109551, -1.2466790821715974)
416                    .adjust_precision(),
417                DVec3::new(0.6931570466592198, -0.8807318727109549, -4.2166565756131895)
418                    .adjust_precision(),
419                DVec3::new(2.0567779125581613, 0.8807319423722887, -4.411036004147714)
420                    .adjust_precision(),
421                DVec3::new(2.480137911200455, 0.8807319423722885, -1.4410585107061231)
422                    .adjust_precision(),
423                DVec3::new(1.5098793531794676, 1.0794012801433277, -1.3027515158842724)
424                    .adjust_precision(),
425                DVec3::new(1.0865193545371739, 1.0794012801433275, -4.2727290093258645)
426                    .adjust_precision(),
427                DVec3::new(14.886956777274035, 1.60199840348629, -1.6440063661356903)
428                    .adjust_precision(),
429                DVec3::new(14.485324381553463, 2.0000000696613336, -1.627092099091476)
430                    .adjust_precision(),
431                DVec3::new(15.277318379804553, 2.0000000696613336, -1.739988098729421)
432                    .adjust_precision(),
433                DVec3::new(14.875685984083981, 2.398001735836377, -1.7230738316852063)
434                    .adjust_precision(),
435            ]
436        );
437        assert_eq!(
438            trimesh.indices,
439            vec![
440                [4, 5, 0],
441                [5, 1, 0],
442                [5, 6, 1],
443                [6, 2, 1],
444                [6, 7, 3],
445                [2, 6, 3],
446                [7, 4, 0],
447                [3, 7, 0],
448                [0, 1, 2],
449                [3, 0, 2],
450                [7, 6, 5],
451                [4, 7, 5],
452                [8, 9, 10],
453                [8, 10, 9],
454                [9, 11, 10],
455                [10, 11, 9],
456            ]
457        );
458    }
459}