Skip to main content

oxijolt/shape/
mesh.rs

1//! Triangle meshes.
2
3use std::ptr::null;
4
5use oxijolt_sys::*;
6
7use super::geometry::{cross, length, length_sq, sub, v3, V3};
8use super::{initialize, Shape, ShapeSettings};
9use crate::{limits, MeshError, PhysicsMaterial, ShapeError, Vec3};
10
11/// Most materials Jolt accepts for one mesh (`MeshShape::FLAGS_MATERIAL_BITS` = 5).
12const MAX_MESH_MATERIALS: usize = 32;
13/// Most triangles Jolt stores per leaf of a mesh's tree (`MeshShape::MaxTrianglesPerLeaf`).
14const MAX_TRIANGLES_PER_LEAF: u32 = 8;
15/// Smallest `|(v1 - v0) x (v2 - v0)|` (twice the area, m²) of a triangle given to Jolt. Jolt's
16/// collision detection asserts on a triangle whose `f32` cross product has a squared length of
17/// at most 1e-12 (`EPAPenetrationDepth::GetPenetrationDepthStepGJK`, `IsNearZero`); the floor
18/// is 1e-6 raised by a relative 1e-6, about eight times the rounding of that squared length.
19const MIN_TRIANGLE_CROSS: f64 = 1.000_001e-6;
20/// How many times the largest change Jolt's rounding can make to a triangle's cross product the
21/// cross product must keep above [`MIN_TRIANGLE_CROSS`]; see [`is_collidable`].
22const CROSS_ERROR_MARGIN: f64 = 2.0;
23/// Steps of Jolt's 21-bit vertex quantization across a mesh's bounds on each axis
24/// (`TriangleCodecIndexed8BitPackSOA4Flags::COMPONENT_MASK`).
25const QUANTIZATION_STEPS: f64 = ((1u32 << 21) - 1) as f64;
26
27/// How Jolt builds a mesh's bounding volume tree (Jolt `MeshShapeSettings::EBuildQuality`).
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
29pub enum MeshBuildQuality {
30    /// A tree that is slower to build and faster to query. Jolt's default.
31    #[default]
32    FavorRuntimePerformance,
33    /// A tree that is faster to build and slower to query.
34    FavorBuildSpeed,
35}
36
37impl MeshBuildQuality {
38    fn to_jph(self) -> JPH_Mesh_Shape_BuildQuality {
39        match self {
40            Self::FavorRuntimePerformance => JPH_Mesh_Shape_BuildQuality_FavorRuntimePerformance,
41            Self::FavorBuildSpeed => JPH_Mesh_Shape_BuildQuality_FavorBuildSpeed,
42        }
43    }
44}
45
46/// Settings of [`Shape::new_mesh_with_settings`] other than the geometry. The defaults are
47/// Jolt's (`MeshShapeSettings`): no materials, an active-edge threshold of 5 degrees, 8
48/// triangles per leaf, [`MeshBuildQuality::FavorRuntimePerformance`]; and convex shapes up to
49/// [`MeshSettings::DEFAULT_MAX_CONVEX_EXTENT`].
50#[derive(Clone, Debug)]
51pub struct MeshSettings<'a> {
52    materials: Option<(&'a [&'a PhysicsMaterial], &'a [u8])>,
53    active_edge_cos_threshold_angle: f32,
54    max_triangles_per_leaf: u32,
55    build_quality: MeshBuildQuality,
56    max_convex_extent: f32,
57}
58
59impl Default for MeshSettings<'_> {
60    fn default() -> Self {
61        Self {
62            materials: None,
63            active_edge_cos_threshold_angle: 0.996195,
64            max_triangles_per_leaf: 8,
65            build_quality: MeshBuildQuality::default(),
66            max_convex_extent: Self::DEFAULT_MAX_CONVEX_EXTENT,
67        }
68    }
69}
70
71impl<'a> MeshSettings<'a> {
72    /// The default of [`max_convex_extent`](Self::max_convex_extent), metres: the largest round
73    /// extent at which every prop of the real models tested in CI loses under 0.1 % of its area
74    /// to the triangle rule ([docs/real-meshes.md]). A mesh of small detailed objects that only
75    /// meets small bodies keeps more of its triangles with a smaller extent. Convex shapes larger than the extent a mesh
76    /// was built for can trip Jolt's assertions on its thinnest triangles in an asserts build;
77    /// a mesh that must meet them is built with a larger extent. See
78    /// [docs/limits.md#convex-shapes-against-meshes].
79    ///
80    /// [docs/real-meshes.md]: https://github.com/pockerhead/oxijolt/blob/main/docs/real-meshes.md
81    /// [docs/limits.md#convex-shapes-against-meshes]: https://github.com/pockerhead/oxijolt/blob/main/docs/limits.md#convex-shapes-against-meshes
82    pub const DEFAULT_MAX_CONVEX_EXTENT: f32 = 300.0;
83
84    /// Gives triangle `i` the material `list[indices[i]]`. `list` holds 1 to 32 materials,
85    /// `indices` one entry per triangle, each naming a material of the list. Without materials
86    /// every triangle uses Jolt's default material. The shape holds its own reference to each
87    /// material.
88    #[must_use]
89    pub fn materials(mut self, list: &'a [&'a PhysicsMaterial], indices: &'a [u8]) -> Self {
90        self.materials = Some((list, indices));
91        self
92    }
93
94    /// Cosine of the angle between two triangles above which their shared edge counts as
95    /// active, in `[-1, 1]`; a negative value makes every edge active. Concave edges are never
96    /// active. Smaller values give more ghost collisions with edges, larger ones slower
97    /// depenetration (Jolt's wording). Default `0.996195`, the cosine of 5 degrees.
98    #[must_use]
99    pub fn active_edge_cos_threshold_angle(mut self, value: f32) -> Self {
100        self.active_edge_cos_threshold_angle = value;
101        self
102    }
103
104    /// Most triangles per leaf of the tree, in `1..=8`. Default 8.
105    #[must_use]
106    pub fn max_triangles_per_leaf(mut self, value: u32) -> Self {
107        self.max_triangles_per_leaf = value;
108        self
109    }
110
111    /// How the tree is built. Default [`MeshBuildQuality::FavorRuntimePerformance`].
112    #[must_use]
113    pub fn build_quality(mut self, value: MeshBuildQuality) -> Self {
114        self.build_quality = value;
115        self
116    }
117
118    /// The size of the largest convex shape the mesh must collide with reliably, metres: the
119    /// largest absolute coordinate of the convex shape's local bounds (relative to its centre of
120    /// mass, after scaling) plus the separation distance of the collide query. Jolt collides a
121    /// triangle in the convex shape's space, so its rounding grows with this extent, and the
122    /// mesh drops the triangles too thin for it. In `0..=2 * limits::MAX_SHAPE_EXTENT`; default
123    /// [`Self::DEFAULT_MAX_CONVEX_EXTENT`]. A larger convex shape against a thin triangle can
124    /// trip Jolt's assertions in an asserts build and gets a distorted contact otherwise; see
125    /// [docs/limits.md#convex-shapes-against-meshes].
126    ///
127    /// [docs/limits.md#convex-shapes-against-meshes]: https://github.com/pockerhead/oxijolt/blob/main/docs/limits.md#convex-shapes-against-meshes
128    #[must_use]
129    pub fn max_convex_extent(mut self, metres: f32) -> Self {
130        // -0.0 is in range; keep it as 0.0 so that errors name it as 0 m.
131        self.max_convex_extent = if metres == 0.0 { 0.0 } else { metres };
132        self
133    }
134
135    /// Checks everything but the geometry against `triangle_count`.
136    fn validate(&self, triangle_count: usize) -> Result<(), ShapeError> {
137        let invalid = |what| Err(ShapeError::InvalidValue(what));
138        let threshold = self.active_edge_cos_threshold_angle;
139        if !(threshold.is_finite() && (-1.0..=1.0).contains(&threshold)) {
140            return invalid("active_edge_cos_threshold_angle must be between -1 and 1");
141        }
142        if !(1..=MAX_TRIANGLES_PER_LEAF).contains(&self.max_triangles_per_leaf) {
143            return invalid("max_triangles_per_leaf must be between 1 and 8");
144        }
145        if !(0.0..=2.0 * limits::MAX_SHAPE_EXTENT).contains(&self.max_convex_extent) {
146            return invalid("max_convex_extent must be between 0 and 2 * limits::MAX_SHAPE_EXTENT");
147        }
148        if let Some((list, indices)) = self.materials {
149            if !(1..=MAX_MESH_MATERIALS).contains(&list.len()) {
150                return invalid("mesh material list must hold 1 to 32 materials");
151            }
152            if indices.len() != triangle_count {
153                return invalid("mesh material indices must hold one value per triangle");
154            }
155            if indices
156                .iter()
157                .any(|&index| usize::from(index) >= list.len())
158            {
159                return invalid("material indices must name a material of the list");
160            }
161        }
162        Ok(())
163    }
164}
165
166/// Whether Jolt can index `vertices` vertices and `triangles` triangles: its mesh code stores
167/// both as `int`.
168fn mesh_counts_fit(vertices: usize, triangles: usize) -> bool {
169    let max = i32::MAX as usize;
170    vertices <= max && triangles <= max
171}
172
173/// The geometry checks of [`Shape::new_mesh_with_settings`].
174fn validate_geometry(vertices: &[Vec3], triangles: &[[u32; 3]]) -> Result<(), ShapeError> {
175    if vertices.is_empty() || triangles.is_empty() {
176        return Err(ShapeError::InvalidValue(
177            "a mesh needs vertices and triangles",
178        ));
179    }
180    if !mesh_counts_fit(vertices.len(), triangles.len()) {
181        return Err(ShapeError::InvalidValue(
182            "a mesh has at most i32::MAX vertices and triangles",
183        ));
184    }
185    if !vertices
186        .iter()
187        .all(|&vertex| limits::is_local_offset(vertex))
188    {
189        return Err(ShapeError::InvalidValue(
190            "mesh vertices must be finite and within limits::MAX_SHAPE_EXTENT",
191        ));
192    }
193    // Jolt reads `vertices[index]` without a range check when it sanitizes the triangles.
194    if triangles
195        .iter()
196        .flatten()
197        .any(|&index| index as usize >= vertices.len())
198    {
199        return Err(ShapeError::InvalidValue(
200            "triangle index beyond the vertex list",
201        ));
202    }
203    Ok(())
204}
205
206/// The triangles [`Shape::new_mesh`] left out because they are too small or too thin for Jolt to
207/// collide with reliably (degenerate triangles among them).
208#[derive(Clone, Debug, Default, PartialEq)]
209pub struct DroppedTriangles {
210    indices: Vec<usize>,
211    area: f64,
212}
213
214impl DroppedTriangles {
215    /// How many triangles were dropped.
216    pub fn count(&self) -> usize {
217        self.indices.len()
218    }
219
220    /// Whether every triangle was kept.
221    pub fn is_empty(&self) -> bool {
222        self.indices.is_empty()
223    }
224
225    /// The positions of the dropped triangles in the `triangles` given to the constructor, in
226    /// ascending order.
227    pub fn indices(&self) -> &[usize] {
228        &self.indices
229    }
230
231    /// The total area of the dropped triangles, m².
232    pub fn area(&self) -> f32 {
233        self.area as f32
234    }
235
236    fn push(&mut self, index: usize, [a, b, c]: [V3; 3]) {
237        self.indices.push(index);
238        self.area += 0.5 * length(cross(sub(b, a), sub(c, a)));
239    }
240}
241
242impl Shape {
243    /// A triangle mesh of `vertices` (shape space, metres) and `triangles` (three indices into
244    /// `vertices` each) with [`MeshSettings::default`], and the triangles it dropped; see
245    /// [`new_mesh_with_settings`](Self::new_mesh_with_settings).
246    pub fn new_mesh(
247        vertices: &[Vec3],
248        triangles: &[[u32; 3]],
249    ) -> Result<(Self, DroppedTriangles), ShapeError> {
250        Self::new_mesh_with_settings(vertices, triangles, &MeshSettings::default())
251    }
252
253    /// A triangle mesh of `vertices` (shape space, metres) and `triangles` (three indices into
254    /// `vertices` each), and the triangles it dropped.
255    ///
256    /// A triangle's front face is the side from which its vertices run counter-clockwise.
257    /// Triangles too small or too thin for Jolt to collide with reliably are dropped and
258    /// reported in [`DroppedTriangles`]: twice a triangle's area must be above 1e-6 m² plus
259    /// twice the largest change Jolt's 21-bit vertex quantization and `f32` rounding can make
260    /// to it. That margin follows the triangle's own shape and distance from the shape origin,
261    /// the quantization step of the mesh's bounds on each axis and the size of the convex shapes
262    /// it collides with ([`MeshSettings::max_convex_extent`]); with the defaults a strip 1 m long
263    /// near the origin is kept from about 0.15 mm wide along the axes and from 0.26 mm in any
264    /// orientation ([docs/limits.md#triangle-meshes]). Jolt itself
265    /// keeps one copy of duplicate triangles and reorders the rest, so sub-shape ids do not
266    /// follow the input order. Closest-hit rays hit back faces too.
267    ///
268    /// ```
269    /// # use oxijolt::{Shape, Vec3};
270    /// let vertices = [Vec3::ZERO, Vec3::new(0.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 0.0)];
271    /// // The second triangle repeats a vertex.
272    /// let (mesh, dropped) = Shape::new_mesh(&vertices, &[[0, 1, 2], [0, 0, 1]])?;
273    /// assert_eq!(dropped.indices(), [1]);
274    /// # drop(mesh);
275    /// # Ok::<(), oxijolt::ShapeError>(())
276    /// ```
277    ///
278    /// Meshes have no volume. They suit static bodies, and kinematic bodies with an explicit
279    /// [`BodySettings::mass`](crate::BodySettings::mass);
280    /// [`PhysicsWorld::create_body`](crate::PhysicsWorld::create_body) refuses them for
281    /// dynamic bodies.
282    ///
283    /// # Errors
284    /// - [`ShapeError::InvalidValue`]: no vertices or no triangles, more than `i32::MAX` of
285    ///   either, an index beyond `vertices`, or a setting out of range (see [`MeshSettings`]);
286    /// - [`ShapeError::InvalidValue`]: a vertex (referenced or not) that is not finite or
287    ///   has a component beyond [`limits::MAX_SHAPE_EXTENT`] in absolute value;
288    /// - [`ShapeError::Mesh`]: no triangle is left after dropping small, thin and degenerate
289    ///   ones;
290    /// - [`ShapeError::Rejected`]: anything else Jolt refuses.
291    ///
292    /// Building cost grows with the triangle count; see [docs/benchmarks.md] and
293    /// [docs/limits.md#triangle-meshes].
294    ///
295    /// [docs/benchmarks.md]: https://github.com/pockerhead/oxijolt/blob/main/docs/benchmarks.md
296    /// [docs/limits.md#triangle-meshes]: https://github.com/pockerhead/oxijolt/blob/main/docs/limits.md#triangle-meshes
297    pub fn new_mesh_with_settings(
298        vertices: &[Vec3],
299        triangles: &[[u32; 3]],
300        settings: &MeshSettings<'_>,
301    ) -> Result<(Self, DroppedTriangles), ShapeError> {
302        validate_geometry(vertices, triangles)?;
303        settings.validate(triangles.len())?;
304        initialize()?;
305        let (kept, dropped) = collidable_triangles(vertices, triangles, settings.max_convex_extent);
306        if kept.is_empty() {
307            return Err(ShapeError::Mesh(MeshError::NoTriangles));
308        }
309        let jolt_settings = mesh_settings(vertices, triangles, &kept, settings)?;
310        let mesh: *mut JPH_MeshShapeSettings = jolt_settings.as_ptr();
311        // Jolt's own clean-up has run. After `collidable_triangles` it can only drop duplicates,
312        // which leaves a copy; the count is Jolt's own verdict on what is left.
313        // SAFETY: the settings are live, owned by the guard and were created as mesh settings.
314        if unsafe { JPH_MeshShapeSettings_GetTriangleCount(mesh) } == 0 {
315            return Err(ShapeError::Mesh(MeshError::NoTriangles));
316        }
317        let shape = jolt_settings.create()?.within_extent_bounds()?;
318        shape.record_convex_extent(settings.max_convex_extent);
319        Ok((shape, dropped))
320    }
321
322    /// Keeps `extent` in the new mesh's Jolt user data (`Shape::mUserData`), with
323    /// [`CONVEX_EXTENT_TAG`] in the high half and the bits of `extent` in the low half, so that
324    /// [`Shape::new_scaled`] checks the mesh against the extent it was built for. The safe API does
325    /// not expose a shape's user data otherwise.
326    fn record_convex_extent(&self, extent: f32) {
327        let data = CONVEX_EXTENT_TAG | u64::from(extent.to_bits());
328        // SAFETY: the mesh is live and was just created by the caller, which has not handed it
329        // out, so nothing reads it while the user data changes.
330        unsafe { JPH_Shape_SetUserData(self.0.as_ptr(), data) };
331    }
332}
333
334/// Marks the user data of a mesh built by [`Shape::new_mesh_with_settings`].
335const CONVEX_EXTENT_TAG: u64 = 1 << 32;
336
337/// The [`MeshSettings::max_convex_extent`] the mesh or heightfield `leaf` was built for; the
338/// default for heightfields and for meshes built outside [`Shape::new_mesh_with_settings`].
339///
340/// # Safety
341/// `leaf` is a live shape.
342pub(super) unsafe fn convex_extent_of(leaf: *const JPH_Shape) -> f32 {
343    // SAFETY: `leaf` is live (contract); the getter only reads it.
344    let data = unsafe { JPH_Shape_GetUserData(leaf) };
345    if data >> 32 == CONVEX_EXTENT_TAG >> 32 {
346        f32::from_bits(data as u32)
347    } else {
348        MeshSettings::DEFAULT_MAX_CONVEX_EXTENT
349    }
350}
351
352/// The indices of the triangles Jolt can collide with reliably, in input order, and the rest.
353///
354/// Jolt stores vertices quantized to 21 bits over the bounds of the triangles it keeps, one step
355/// per axis. A triangle that fails [`is_collidable`] without quantization fails with any, so the
356/// bounds leave it out; the triangles kept then lie within bounds no larger than those Jolt
357/// quantizes over. See [docs/limits.md#triangle-meshes].
358///
359/// [docs/limits.md#triangle-meshes]: https://github.com/pockerhead/oxijolt/blob/main/docs/limits.md#triangle-meshes
360fn collidable_triangles(
361    vertices: &[Vec3],
362    triangles: &[[u32; 3]],
363    convex_extent: f32,
364) -> (Vec<usize>, DroppedTriangles) {
365    let corners = |triangle: &[u32; 3]| triangle.map(|index| v3(vertices[index as usize]));
366    let candidates = triangles
367        .iter()
368        .map(corners)
369        .filter(|&corners| is_collidable(corners, [0.0; 3], convex_extent));
370    let step = quantization_step(candidates);
371    let mut kept = Vec::new();
372    let mut dropped = DroppedTriangles::default();
373    for (index, triangle) in triangles.iter().enumerate() {
374        let corners = corners(triangle);
375        if is_collidable(corners, step, convex_extent) {
376            kept.push(index);
377        } else {
378            dropped.push(index, corners);
379        }
380    }
381    (kept, dropped)
382}
383
384/// The step of Jolt's vertex quantization on each axis over the bounds of `triangles`; zero for
385/// none.
386fn quantization_step(triangles: impl Iterator<Item = [V3; 3]>) -> V3 {
387    let mut low = [f64::INFINITY; 3];
388    let mut high = [f64::NEG_INFINITY; 3];
389    for vertex in triangles.flatten() {
390        for axis in 0..3 {
391            low[axis] = low[axis].min(vertex[axis]);
392            high[axis] = high[axis].max(vertex[axis]);
393        }
394    }
395    [0, 1, 2].map(|axis| ((high[axis] - low[axis]) / QUANTIZATION_STEPS).max(0.0))
396}
397
398/// How far Jolt's `f32` arithmetic can move a corner of a triangle on each axis when it collides
399/// the triangle with a convex shape (`CollideConvexVsTriangles::Collide`): the transform of the
400/// triangle's own coordinates, at most `distance` from their origin, and the rounding of the
401/// result in the convex shape's space. There every coordinate is at most `convex_extent` plus
402/// the triangle's `longest_edge`, because Jolt only goes on with triangles whose bounds overlap
403/// the convex shape's.
404fn rounding_displacement(distance: f64, convex_extent: f64, longest_edge: f64) -> f64 {
405    let epsilon = f64::from(f32::EPSILON);
406    4.0 * epsilon * distance + epsilon * (convex_extent + longest_edge)
407}
408
409/// Whether the triangle with `corners` keeps a cross product above [`MIN_TRIANGLE_CROSS`] under
410/// Jolt's rounding against convex shapes up to `convex_extent`
411/// ([`MeshSettings::max_convex_extent`]), with a factor [`CROSS_ERROR_MARGIN`] to spare.
412///
413/// Each corner can move by up to `step[axis]` on each axis (the quantization of a mesh being
414/// built, zero for triangles Jolt has stored) plus [`rounding_displacement`]; an edge moves by
415/// the difference of two corner moves. With edges `ab`, `ac` from one corner moved by `e1`,
416/// `e2`, the cross product changes by `ab × e2 + e1 × ac + e1 × e2`. Its length is at least its
417/// component along the unit normal `n`, which changes by `e2 · (n × ab) + e1 · (ac × n) + n ·
418/// (e1 × e2)`: moves within the triangle's plane across an edge shrink it, moves out of the
419/// plane do not. The cross product is the same from every corner, so the smallest of the three
420/// corners' bounds holds, and the rule does not depend on the order of the corners. Jolt's `f32`
421/// cross product, from whichever corner it starts, rounds it once more.
422pub(super) fn is_collidable(corners: [V3; 3], step: V3, convex_extent: f32) -> bool {
423    let [a, b, c] = corners;
424    let normal = cross(sub(b, a), sub(c, a));
425    let twice_area = length(normal);
426    if twice_area < MIN_TRIANGLE_CROSS {
427        return false;
428    }
429    let unit = normal.map(|component| component / twice_area);
430    let distance = corners.map(length).into_iter().fold(0.0, f64::max);
431    // The two edges leaving each corner, in the order that gives the same normal.
432    let edge_pairs = [
433        (sub(b, a), sub(c, a)),
434        (sub(c, b), sub(a, b)),
435        (sub(a, c), sub(b, c)),
436    ];
437    let longest_edge = edge_pairs
438        .iter()
439        .map(|&(edge, _)| length(edge))
440        .fold(0.0, f64::max);
441    let rounding = rounding_displacement(distance, f64::from(convex_extent), longest_edge);
442    let edge_move = step.map(|axis_step| 2.0 * (axis_step + rounding));
443    let shrink = edge_pairs
444        .iter()
445        .map(|&(first, second)| {
446            reach(cross(unit, first), edge_move) + reach(cross(second, unit), edge_move)
447        })
448        .fold(f64::INFINITY, f64::min);
449    let cross_rounding = edge_pairs
450        .iter()
451        .map(|&(first, second)| 2.0 * f64::from(f32::EPSILON) * length(first) * length(second))
452        .fold(0.0, f64::max);
453    let change = shrink + length_sq(edge_move) + cross_rounding;
454    twice_area >= MIN_TRIANGLE_CROSS + CROSS_ERROR_MARGIN * change
455}
456
457/// The largest `|e · direction|` over every `e` with `|e[axis]| <= bound[axis]`.
458fn reach(direction: V3, bound: V3) -> f64 {
459    (0..3).map(|axis| bound[axis] * direction[axis].abs()).sum()
460}
461
462/// Jolt mesh settings holding the validated geometry, of the triangles `kept`, and `settings`.
463fn mesh_settings(
464    vertices: &[Vec3],
465    triangles: &[[u32; 3]],
466    kept: &[usize],
467    settings: &MeshSettings<'_>,
468) -> Result<ShapeSettings, ShapeError> {
469    let jolt_vertices: Vec<JPH_Vec3> = vertices.iter().map(|vertex| vertex.to_jph()).collect();
470    let material_indices = settings.materials.map(|(_, indices)| indices);
471    let jolt_triangles: Vec<JPH_IndexedTriangle> = kept
472        .iter()
473        .map(|&i| {
474            let [i1, i2, i3] = triangles[i];
475            JPH_IndexedTriangle {
476                i1,
477                i2,
478                i3,
479                materialIndex: material_indices.map_or(0, |indices| u32::from(indices[i])),
480                userData: 0,
481            }
482        })
483        .collect();
484    let list: Vec<*const JPH_PhysicsMaterial> = settings
485        .materials
486        .map(|(list, _)| list.iter().map(|material| material.as_ptr()).collect())
487        .unwrap_or_default();
488    // SAFETY: Jolt is initialised. Every array is live for the call and holds the count passed;
489    // the counts fit `i32` and every index is below the vertex count (`validate_geometry`), and
490    // every material index addresses `list`, or is 0 without one (`MeshSettings::validate`).
491    // The materials are live, borrowed by `settings`; Jolt's list takes its own references.
492    // The returned settings hold one reference, which the guard takes over.
493    let jolt_settings = unsafe {
494        ShapeSettings::from_raw(
495            JPH_MeshShapeSettings_Create3(
496                jolt_vertices.as_ptr(),
497                jolt_vertices.len() as u32,
498                jolt_triangles.as_ptr(),
499                jolt_triangles.len() as u32,
500                if list.is_empty() {
501                    null()
502                } else {
503                    list.as_ptr()
504                },
505                list.len() as u32,
506            )
507            .cast(),
508        )
509    }?;
510    let mesh: *mut JPH_MeshShapeSettings = jolt_settings.as_ptr();
511    // SAFETY: the settings are live, owned by the guard, were created as mesh settings and have
512    // not created a shape yet; every value was validated.
513    unsafe {
514        JPH_MeshShapeSettings_SetActiveEdgeCosThresholdAngle(
515            mesh,
516            settings.active_edge_cos_threshold_angle,
517        );
518        JPH_MeshShapeSettings_SetMaxTrianglesPerLeaf(mesh, settings.max_triangles_per_leaf);
519        JPH_MeshShapeSettings_SetBuildQuality(mesh, settings.build_quality.to_jph());
520    }
521    Ok(jolt_settings)
522}
523
524#[cfg(test)]
525mod tests;