1use core::num::NonZeroU32;
4
5use bevy::prelude::*;
6use parry::shape::{SharedShape, TypedShape};
7use thiserror::Error;
8
9use crate::prelude::*;
10
11#[derive(Debug, Clone)]
52pub struct TrimeshBuilder {
53 pub shape: SharedShape,
55 pub position: Position,
57 pub rotation: Rotation,
59 pub fail_on_compound_error: bool,
62 pub fallback_subdivisions: NonZeroU32,
65 pub sphere_subdivisions: Option<(NonZeroU32, NonZeroU32)>,
68 pub capsule_subdivision: Option<(NonZeroU32, NonZeroU32)>,
71 pub cylinder_subdivisions: Option<NonZeroU32>,
74 pub cone_subdivisions: Option<NonZeroU32>,
77}
78
79#[derive(Debug, Clone, PartialEq, Reflect, Default)]
81pub struct Trimesh {
82 pub vertices: Vec<Vector>,
84 pub indices: Vec<[u32; 3]>,
86}
87
88impl Trimesh {
89 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#[derive(Debug, Error)]
105pub enum TrimeshBuilderError {
106 #[error("Unsupported shape type: {0}")]
108 UnsupportedShape(String),
109}
110
111impl TrimeshBuilder {
112 pub fn new(shape: SharedShape) -> Self {
114 TrimeshBuilder {
115 shape,
116 position: default(),
117 rotation: default(),
118 fail_on_compound_error: true,
119 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 pub fn translated(&mut self, position: impl Into<Position>) -> &mut Self {
130 self.position.0 += position.into().0;
131 self
132 }
133
134 pub fn rotated(&mut self, rotation: impl Into<Rotation>) -> &mut Self {
136 self.rotation = rotation.into() * self.rotation;
137 self
138 }
139
140 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 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 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 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 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 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 pub fn build(&self) -> Result<Trimesh, TrimeshBuilderError> {
229 let (vertices, indices) = match self.shape.as_typed_shape() {
230 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 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 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 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 Ok(compound_trimesh)
285 },
286 );
287 }
288 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 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 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}