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)]
7#[non_exhaustive]
8pub struct Collider {
9    pub shape: ColliderShape,
10    pub is_trigger: bool,
11    pub material: PhysicsMaterial,
12    pub collision_layer: CollisionLayer,
13}
14
15impl Default for Collider {
16    fn default() -> Self {
17        Self {
18            shape: ColliderShape::Sphere(SphereShape { radius: 0.5 }),
19            is_trigger: false,
20            material: PhysicsMaterial::default(),
21            collision_layer: CollisionLayer::default(),
22        }
23    }
24}
25
26impl Collider {
27    /// Build a collider from a raw [`ColliderShape`], using the default
28    /// material and collision layer with `is_trigger = false`.
29    ///
30    /// This is the canonical constructor for turning a bare shape into a
31    /// [`Collider`]: the struct is `#[non_exhaustive]`, so it cannot be built
32    /// with a struct literal from outside this crate. Combine with the
33    /// `with_*` builder methods to override defaults.
34    pub fn from_shape(shape: ColliderShape) -> Self {
35        Self {
36            shape,
37            ..Default::default()
38        }
39    }
40
41    /// Calculate AABB for this collider at given transform
42    pub fn compute_aabb(&self, position: Vec3, rotation: Quat) -> gizmo_math::Aabb {
43        match &self.shape {
44            ColliderShape::Sphere(s) => {
45                let radius_vec = Vec3::splat(s.radius);
46                gizmo_math::Aabb::from_center_half_extents(position, radius_vec)
47            }
48            ColliderShape::Box(b) => {
49                // Rotate the half extents to get world-space AABB
50                let corners = [
51                    Vec3::new(b.half_extents.x, b.half_extents.y, b.half_extents.z),
52                    Vec3::new(-b.half_extents.x, b.half_extents.y, b.half_extents.z),
53                    Vec3::new(b.half_extents.x, -b.half_extents.y, b.half_extents.z),
54                    Vec3::new(b.half_extents.x, b.half_extents.y, -b.half_extents.z),
55                    Vec3::new(-b.half_extents.x, -b.half_extents.y, b.half_extents.z),
56                    Vec3::new(-b.half_extents.x, b.half_extents.y, -b.half_extents.z),
57                    Vec3::new(b.half_extents.x, -b.half_extents.y, -b.half_extents.z),
58                    Vec3::new(-b.half_extents.x, -b.half_extents.y, -b.half_extents.z),
59                ];
60
61                let mut min = Vec3::splat(f32::INFINITY);
62                let mut max = Vec3::splat(f32::NEG_INFINITY);
63
64                for corner in &corners {
65                    let rotated = rotation * (*corner);
66                    let world_pos = position + rotated;
67                    min = min.min(world_pos);
68                    max = max.max(world_pos);
69                }
70
71                gizmo_math::Aabb::new(min, max)
72            }
73            ColliderShape::Capsule(c) => {
74                let axis = rotation * Vec3::Y;
75                let half_height_vec = axis * c.half_height;
76                let radius_vec = Vec3::splat(c.radius);
77                let extent = half_height_vec.abs() + radius_vec;
78                gizmo_math::Aabb::from_center_half_extents(position, extent)
79            }
80            ColliderShape::Plane(_) => {
81                // Infinite plane - use a very large AABB
82                let large = 10000.0;
83                gizmo_math::Aabb::new(position - Vec3::splat(large), position + Vec3::splat(large))
84            }
85            ColliderShape::TriMesh(tm) => {
86                let mut min = Vec3::splat(f32::INFINITY);
87                let mut max = Vec3::splat(f32::NEG_INFINITY);
88                for v in tm.vertices.iter() {
89                    let world_pos = position + rotation * (*v);
90                    min = min.min(world_pos);
91                    max = max.max(world_pos);
92                }
93                gizmo_math::Aabb::new(min, max)
94            }
95            ColliderShape::ConvexHull(ch) => {
96                let mut min = Vec3::splat(f32::INFINITY);
97                let mut max = Vec3::splat(f32::NEG_INFINITY);
98                for v in ch.vertices.iter() {
99                    let world_pos = position + rotation * (*v);
100                    min = min.min(world_pos);
101                    max = max.max(world_pos);
102                }
103                gizmo_math::Aabb::new(min, max)
104            }
105            ColliderShape::Compound(shapes) => {
106                let mut min = Vec3::splat(f32::INFINITY);
107                let mut max = Vec3::splat(f32::NEG_INFINITY);
108                for (local_t, sub_shape) in shapes {
109                    let world_pos = position + rotation.mul_vec3(local_t.position);
110                    let world_rot = rotation * local_t.rotation;
111
112                    let temp_col = Collider {
113                        shape: (**sub_shape).clone(),
114                        ..Default::default()
115                    };
116                    let sub_aabb = temp_col.compute_aabb(world_pos, world_rot);
117                    min = min.min(sub_aabb.min.into());
118                    max = max.max(sub_aabb.max.into());
119                }
120                gizmo_math::Aabb::new(min, max)
121            }
122        }
123    }
124
125    pub fn plane(normal: Vec3, distance: f32) -> Self {
126        Self {
127            shape: ColliderShape::Plane(PlaneShape { normal, distance }),
128            ..Default::default()
129        }
130    }
131
132    pub fn sphere(radius: f32) -> Self {
133        Self {
134            shape: ColliderShape::Sphere(SphereShape { radius }),
135            ..Default::default()
136        }
137    }
138
139    pub fn box_collider(half_extents: Vec3) -> Self {
140        Self {
141            shape: ColliderShape::Box(BoxShape { half_extents }),
142            ..Default::default()
143        }
144    }
145
146    pub fn offset_box(offset: Vec3, half_extents: Vec3) -> Self {
147        Self {
148            shape: ColliderShape::Compound(vec![(
149                Transform::new(offset),
150                Box::new(ColliderShape::Box(BoxShape { half_extents })),
151            )]),
152            ..Default::default()
153        }
154    }
155
156    pub fn capsule(radius: f32, half_height: f32) -> Self {
157        Self {
158            shape: ColliderShape::Capsule(CapsuleShape {
159                radius,
160                half_height,
161            }),
162            ..Default::default()
163        }
164    }
165
166    pub fn convex_hull(points: &[Vec3]) -> Self {
167        let hull = crate::quickhull::compute_convex_hull(points);
168        Self {
169            shape: ColliderShape::ConvexHull(ConvexHullShape {
170                vertices: std::sync::Arc::new(hull.vertices),
171                faces: std::sync::Arc::new(hull.faces),
172            }),
173            ..Default::default()
174        }
175    }
176
177    pub fn with_trigger(mut self, is_trigger: bool) -> Self {
178        self.is_trigger = is_trigger;
179        self
180    }
181
182    pub fn with_material(mut self, material: PhysicsMaterial) -> Self {
183        self.material = material;
184        self
185    }
186
187    /// Zıplaklık (restitution) kısayolu — tam malzeme kurmadan tek satırda ayarla.
188    /// Örn: `Collider::sphere(0.5).with_restitution(0.9)` (defalarca zıplayan top).
189    pub fn with_restitution(mut self, restitution: f32) -> Self {
190        self.material.restitution = restitution.clamp(0.0, 1.0);
191        self
192    }
193
194    /// Sürtünme kısayolu (statik = dinamik = `friction`).
195    pub fn with_friction(mut self, friction: f32) -> Self {
196        let f = friction.max(0.0);
197        self.material.static_friction = f;
198        self.material.dynamic_friction = f;
199        self
200    }
201
202    // Backwards compatibility wrappers
203    pub fn aabb(half_extents: Vec3) -> Self {
204        Self::box_collider(half_extents)
205    }
206
207    pub fn new_sphere(radius: f32) -> Self {
208        Self::sphere(radius)
209    }
210
211    pub fn new_aabb(x: f32, y: f32, z: f32) -> Self {
212        Self::box_collider(Vec3::new(x, y, z))
213    }
214
215    pub fn new_capsule(radius: f32, half_height: f32) -> Self {
216        Self::capsule(radius, half_height)
217    }
218
219    pub fn with_layer(mut self, layer: CollisionLayer) -> Self {
220        self.collision_layer = layer;
221        self
222    }
223
224    pub fn volume(&self) -> f32 {
225        match &self.shape {
226            ColliderShape::Sphere(s) => (4.0 / 3.0) * std::f32::consts::PI * s.radius.powi(3),
227            ColliderShape::Box(b) => 8.0 * b.half_extents.x * b.half_extents.y * b.half_extents.z,
228            ColliderShape::Capsule(c) => {
229                let cylinder_vol = std::f32::consts::PI * c.radius.powi(2) * (c.half_height * 2.0);
230                let sphere_vol = (4.0 / 3.0) * std::f32::consts::PI * c.radius.powi(3);
231                cylinder_vol + sphere_vol
232            }
233            ColliderShape::Plane(_) => f32::MAX, // Safe value instead of INFINITY for inertia calculations
234            ColliderShape::TriMesh(_)
235            | ColliderShape::ConvexHull(_)
236            | ColliderShape::Compound(_) => {
237                let aabb = self.compute_aabb(Vec3::ZERO, Quat::IDENTITY);
238                let e = aabb.max - aabb.min;
239                e.x * e.y * e.z * 0.5 // Approximate volume from AABB
240            }
241        }
242    }
243
244    pub fn extents_y(&self) -> f32 {
245        match &self.shape {
246            ColliderShape::Sphere(s) => s.radius,
247            ColliderShape::Box(b) => b.half_extents.y,
248            ColliderShape::Capsule(c) => c.half_height + c.radius,
249            ColliderShape::Plane(_) => 0.0,
250            ColliderShape::TriMesh(_)
251            | ColliderShape::ConvexHull(_)
252            | ColliderShape::Compound(_) => {
253                let aabb = self.compute_aabb(Vec3::ZERO, Quat::IDENTITY);
254                (aabb.max.y - aabb.min.y) * 0.5
255            }
256        }
257    }
258}
259
260// NOT `#[non_exhaustive]`: the engine's own crates (gizmo-physics-rigid) match
261// this exhaustively to compute inertia / AABB / narrowphase dispatch. Adding a
262// new collider shape is inherently a breaking change (it needs solver support),
263// so a major version bump is appropriate — and exhaustive matching lets the
264// compiler flag every site that must handle the new shape.
265#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
266pub enum ColliderShape {
267    Sphere(SphereShape),
268    Box(BoxShape),
269    Capsule(CapsuleShape),
270    Plane(PlaneShape),
271    TriMesh(TriMeshShape),
272    ConvexHull(ConvexHullShape),
273    Compound(Vec<(Transform, Box<ColliderShape>)>),
274}
275
276#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
277pub struct SphereShape {
278    pub radius: f32,
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
282pub struct BoxShape {
283    pub half_extents: Vec3,
284}
285
286#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
287pub struct CapsuleShape {
288    pub radius: f32,
289    pub half_height: f32, // Height of cylindrical part (not including hemispheres)
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
293pub struct PlaneShape {
294    pub normal: Vec3,
295    pub distance: f32,
296}
297
298#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
299#[serde(into = "TriMeshShapeData", from = "TriMeshShapeData")]
300pub struct TriMeshShape {
301    pub vertices: std::sync::Arc<Vec<Vec3>>,
302    pub indices: std::sync::Arc<Vec<u32>>,
303    #[serde(skip)]
304    pub bvh: std::sync::Arc<crate::bvh::BvhTree>,
305}
306
307#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
308struct TriMeshShapeData {
309    vertices: Vec<Vec3>,
310    indices: Vec<u32>,
311}
312
313impl From<TriMeshShapeData> for TriMeshShape {
314    fn from(mut data: TriMeshShapeData) -> Self {
315        let bvh = crate::bvh::BvhTree::build(&data.vertices, &mut data.indices).unwrap_or_default();
316        Self {
317            vertices: std::sync::Arc::new(data.vertices),
318            indices: std::sync::Arc::new(data.indices),
319            bvh: std::sync::Arc::new(bvh),
320        }
321    }
322}
323
324impl From<TriMeshShape> for TriMeshShapeData {
325    fn from(shape: TriMeshShape) -> Self {
326        Self {
327            vertices: (*shape.vertices).clone(),
328            indices: (*shape.indices).clone(),
329        }
330    }
331}
332
333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334#[serde(into = "ConvexHullShapeData", from = "ConvexHullShapeData")]
335pub struct ConvexHullShape {
336    pub vertices: std::sync::Arc<Vec<Vec3>>,
337    pub faces: std::sync::Arc<Vec<[u32; 3]>>,
338}
339
340#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
341struct ConvexHullShapeData {
342    points: Vec<Vec3>, // These are raw points, we rebuild the hull on load
343}
344
345impl From<ConvexHullShapeData> for ConvexHullShape {
346    fn from(data: ConvexHullShapeData) -> Self {
347        let hull = crate::quickhull::compute_convex_hull(&data.points);
348        Self {
349            vertices: std::sync::Arc::new(hull.vertices),
350            faces: std::sync::Arc::new(hull.faces),
351        }
352    }
353}
354
355impl From<ConvexHullShape> for ConvexHullShapeData {
356    fn from(shape: ConvexHullShape) -> Self {
357        Self {
358            points: (*shape.vertices).clone(),
359        }
360    }
361}
362
363gizmo_core::impl_component!(Collider);