Skip to main content

gizmo_physics_core/components/
collider.rs

1use gizmo_math::{Quat, Vec3};
2use serde::{Deserialize, Serialize};
3
4use super::{CollisionLayer, PhysicsMaterial, Transform};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7pub struct Collider {
8    pub shape: ColliderShape,
9    pub is_trigger: bool,
10    pub material: PhysicsMaterial,
11    pub collision_layer: CollisionLayer,
12}
13
14impl Default for Collider {
15    fn default() -> Self {
16        Self {
17            shape: ColliderShape::Sphere(SphereShape { radius: 0.5 }),
18            is_trigger: false,
19            material: PhysicsMaterial::default(),
20            collision_layer: CollisionLayer::default(),
21        }
22    }
23}
24
25impl Collider {
26    /// Calculate AABB for this collider at given transform
27    pub fn compute_aabb(&self, position: Vec3, rotation: Quat) -> gizmo_math::Aabb {
28        match &self.shape {
29            ColliderShape::Sphere(s) => {
30                let radius_vec = Vec3::splat(s.radius);
31                gizmo_math::Aabb::from_center_half_extents(position, radius_vec)
32            }
33            ColliderShape::Box(b) => {
34                // Rotate the half extents to get world-space AABB
35                let corners = [
36                    Vec3::new(b.half_extents.x, b.half_extents.y, b.half_extents.z),
37                    Vec3::new(-b.half_extents.x, b.half_extents.y, b.half_extents.z),
38                    Vec3::new(b.half_extents.x, -b.half_extents.y, b.half_extents.z),
39                    Vec3::new(b.half_extents.x, b.half_extents.y, -b.half_extents.z),
40                    Vec3::new(-b.half_extents.x, -b.half_extents.y, b.half_extents.z),
41                    Vec3::new(-b.half_extents.x, b.half_extents.y, -b.half_extents.z),
42                    Vec3::new(b.half_extents.x, -b.half_extents.y, -b.half_extents.z),
43                    Vec3::new(-b.half_extents.x, -b.half_extents.y, -b.half_extents.z),
44                ];
45
46                let mut min = Vec3::splat(f32::INFINITY);
47                let mut max = Vec3::splat(f32::NEG_INFINITY);
48
49                for corner in &corners {
50                    let rotated = rotation * (*corner);
51                    let world_pos = position + rotated;
52                    min = min.min(world_pos);
53                    max = max.max(world_pos);
54                }
55
56                gizmo_math::Aabb::new(min, max)
57            }
58            ColliderShape::Capsule(c) => {
59                let axis = rotation * Vec3::Y;
60                let half_height_vec = axis * c.half_height;
61                let radius_vec = Vec3::splat(c.radius);
62                let extent = half_height_vec.abs() + radius_vec;
63                gizmo_math::Aabb::from_center_half_extents(position, extent)
64            }
65            ColliderShape::Plane(_) => {
66                // Infinite plane - use a very large AABB
67                let large = 10000.0;
68                gizmo_math::Aabb::new(position - Vec3::splat(large), position + Vec3::splat(large))
69            }
70            ColliderShape::TriMesh(tm) => {
71                let mut min = Vec3::splat(f32::INFINITY);
72                let mut max = Vec3::splat(f32::NEG_INFINITY);
73                for v in tm.vertices.iter() {
74                    let world_pos = position + rotation * (*v);
75                    min = min.min(world_pos);
76                    max = max.max(world_pos);
77                }
78                gizmo_math::Aabb::new(min, max)
79            }
80            ColliderShape::ConvexHull(ch) => {
81                let mut min = Vec3::splat(f32::INFINITY);
82                let mut max = Vec3::splat(f32::NEG_INFINITY);
83                for v in ch.vertices.iter() {
84                    let world_pos = position + rotation * (*v);
85                    min = min.min(world_pos);
86                    max = max.max(world_pos);
87                }
88                gizmo_math::Aabb::new(min, max)
89            }
90            ColliderShape::Compound(shapes) => {
91                let mut min = Vec3::splat(f32::INFINITY);
92                let mut max = Vec3::splat(f32::NEG_INFINITY);
93                for (local_t, sub_shape) in shapes {
94                    let world_pos = position + rotation.mul_vec3(local_t.position);
95                    let world_rot = rotation * local_t.rotation;
96
97                    let temp_col = Collider {
98                        shape: (**sub_shape).clone(),
99                        ..Default::default()
100                    };
101                    let sub_aabb = temp_col.compute_aabb(world_pos, world_rot);
102                    min = min.min(sub_aabb.min.into());
103                    max = max.max(sub_aabb.max.into());
104                }
105                gizmo_math::Aabb::new(min, max)
106            }
107        }
108    }
109
110    pub fn plane(normal: Vec3, distance: f32) -> Self {
111        Self {
112            shape: ColliderShape::Plane(PlaneShape { normal, distance }),
113            ..Default::default()
114        }
115    }
116
117    pub fn sphere(radius: f32) -> Self {
118        Self {
119            shape: ColliderShape::Sphere(SphereShape { radius }),
120            ..Default::default()
121        }
122    }
123
124    pub fn box_collider(half_extents: Vec3) -> Self {
125        Self {
126            shape: ColliderShape::Box(BoxShape { half_extents }),
127            ..Default::default()
128        }
129    }
130
131    pub fn offset_box(offset: Vec3, half_extents: Vec3) -> Self {
132        Self {
133            shape: ColliderShape::Compound(vec![(
134                Transform::new(offset),
135                Box::new(ColliderShape::Box(BoxShape { half_extents })),
136            )]),
137            ..Default::default()
138        }
139    }
140
141    pub fn capsule(radius: f32, half_height: f32) -> Self {
142        Self {
143            shape: ColliderShape::Capsule(CapsuleShape {
144                radius,
145                half_height,
146            }),
147            ..Default::default()
148        }
149    }
150
151    pub fn convex_hull(points: &[Vec3]) -> Self {
152        let hull = crate::quickhull::compute_convex_hull(points);
153        Self {
154            shape: ColliderShape::ConvexHull(ConvexHullShape {
155                vertices: std::sync::Arc::new(hull.vertices),
156                faces: std::sync::Arc::new(hull.faces),
157            }),
158            ..Default::default()
159        }
160    }
161
162    pub fn with_trigger(mut self, is_trigger: bool) -> Self {
163        self.is_trigger = is_trigger;
164        self
165    }
166
167    pub fn with_material(mut self, material: PhysicsMaterial) -> Self {
168        self.material = material;
169        self
170    }
171
172    // Backwards compatibility wrappers
173    pub fn aabb(half_extents: Vec3) -> Self {
174        Self::box_collider(half_extents)
175    }
176
177    pub fn new_sphere(radius: f32) -> Self {
178        Self::sphere(radius)
179    }
180
181    pub fn new_aabb(x: f32, y: f32, z: f32) -> Self {
182        Self::box_collider(Vec3::new(x, y, z))
183    }
184
185    pub fn new_capsule(radius: f32, half_height: f32) -> Self {
186        Self::capsule(radius, half_height)
187    }
188
189    pub fn with_layer(mut self, layer: CollisionLayer) -> Self {
190        self.collision_layer = layer;
191        self
192    }
193
194    pub fn volume(&self) -> f32 {
195        match &self.shape {
196            ColliderShape::Sphere(s) => (4.0 / 3.0) * std::f32::consts::PI * s.radius.powi(3),
197            ColliderShape::Box(b) => 8.0 * b.half_extents.x * b.half_extents.y * b.half_extents.z,
198            ColliderShape::Capsule(c) => {
199                let cylinder_vol = std::f32::consts::PI * c.radius.powi(2) * (c.half_height * 2.0);
200                let sphere_vol = (4.0 / 3.0) * std::f32::consts::PI * c.radius.powi(3);
201                cylinder_vol + sphere_vol
202            }
203            ColliderShape::Plane(_) => f32::MAX, // Safe value instead of INFINITY for inertia calculations
204            ColliderShape::TriMesh(_)
205            | ColliderShape::ConvexHull(_)
206            | ColliderShape::Compound(_) => {
207                let aabb = self.compute_aabb(Vec3::ZERO, Quat::IDENTITY);
208                let e = aabb.max - aabb.min;
209                e.x * e.y * e.z * 0.5 // Approximate volume from AABB
210            }
211        }
212    }
213
214    pub fn extents_y(&self) -> f32 {
215        match &self.shape {
216            ColliderShape::Sphere(s) => s.radius,
217            ColliderShape::Box(b) => b.half_extents.y,
218            ColliderShape::Capsule(c) => c.half_height + c.radius,
219            ColliderShape::Plane(_) => 0.0,
220            ColliderShape::TriMesh(_)
221            | ColliderShape::ConvexHull(_)
222            | ColliderShape::Compound(_) => {
223                let aabb = self.compute_aabb(Vec3::ZERO, Quat::IDENTITY);
224                (aabb.max.y - aabb.min.y) * 0.5
225            }
226        }
227    }
228}
229
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
231pub enum ColliderShape {
232    Sphere(SphereShape),
233    Box(BoxShape),
234    Capsule(CapsuleShape),
235    Plane(PlaneShape),
236    TriMesh(TriMeshShape),
237    ConvexHull(ConvexHullShape),
238    Compound(Vec<(Transform, Box<ColliderShape>)>),
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
242pub struct SphereShape {
243    pub radius: f32,
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
247pub struct BoxShape {
248    pub half_extents: Vec3,
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
252pub struct CapsuleShape {
253    pub radius: f32,
254    pub half_height: f32, // Height of cylindrical part (not including hemispheres)
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
258pub struct PlaneShape {
259    pub normal: Vec3,
260    pub distance: f32,
261}
262
263#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
264#[serde(into = "TriMeshShapeData", from = "TriMeshShapeData")]
265pub struct TriMeshShape {
266    pub vertices: std::sync::Arc<Vec<Vec3>>,
267    pub indices: std::sync::Arc<Vec<u32>>,
268    #[serde(skip)]
269    pub bvh: std::sync::Arc<crate::bvh::BvhTree>,
270}
271
272#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
273struct TriMeshShapeData {
274    vertices: Vec<Vec3>,
275    indices: Vec<u32>,
276}
277
278impl From<TriMeshShapeData> for TriMeshShape {
279    fn from(mut data: TriMeshShapeData) -> Self {
280        let bvh = crate::bvh::BvhTree::build(&data.vertices, &mut data.indices).unwrap_or_default();
281        Self {
282            vertices: std::sync::Arc::new(data.vertices),
283            indices: std::sync::Arc::new(data.indices),
284            bvh: std::sync::Arc::new(bvh),
285        }
286    }
287}
288
289impl From<TriMeshShape> for TriMeshShapeData {
290    fn from(shape: TriMeshShape) -> Self {
291        Self {
292            vertices: (*shape.vertices).clone(),
293            indices: (*shape.indices).clone(),
294        }
295    }
296}
297
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299#[serde(into = "ConvexHullShapeData", from = "ConvexHullShapeData")]
300pub struct ConvexHullShape {
301    pub vertices: std::sync::Arc<Vec<Vec3>>,
302    pub faces: std::sync::Arc<Vec<[u32; 3]>>,
303}
304
305#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
306struct ConvexHullShapeData {
307    points: Vec<Vec3>, // These are raw points, we rebuild the hull on load
308}
309
310impl From<ConvexHullShapeData> for ConvexHullShape {
311    fn from(data: ConvexHullShapeData) -> Self {
312        let hull = crate::quickhull::compute_convex_hull(&data.points);
313        Self {
314            vertices: std::sync::Arc::new(hull.vertices),
315            faces: std::sync::Arc::new(hull.faces),
316        }
317    }
318}
319
320impl From<ConvexHullShape> for ConvexHullShapeData {
321    fn from(shape: ConvexHullShape) -> Self {
322        Self {
323            points: (*shape.vertices).clone(),
324        }
325    }
326}
327
328gizmo_core::impl_component!(Collider);