Skip to main content

gizmo_physics_rigid/components/
rigid_body.rs

1use gizmo_math::{Mat3, Quat, Vec3};
2use serde::{Deserialize, Serialize};
3
4use super::Velocity;
5use gizmo_physics_core::{Collider, ColliderShape};
6
7#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
8pub enum BodyType {
9    Dynamic,   // Fully simulated
10    Kinematic, // Moved by user, affects others
11    Static,    // Never moves
12}
13
14#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
15pub struct RigidBody {
16    pub body_type: BodyType,
17    pub mass: f32,
18    pub restitution: f32,
19    pub friction: f32,
20    pub linear_damping: f32,
21    pub angular_damping: f32,
22    pub use_gravity: bool,
23    pub is_sleeping: bool,
24    pub ccd_enabled: bool,
25    pub local_inertia: Vec3,
26    pub lock_rotation_x: bool,
27    pub lock_rotation_y: bool,
28    pub lock_rotation_z: bool,
29    pub lock_translation_x: bool,
30    pub lock_translation_y: bool,
31    pub lock_translation_z: bool,
32    pub sleep_counter: u32, // Frames below sleep threshold
33    pub center_of_mass: Vec3,
34    pub fracture_threshold: Option<f32>, // Impulse threshold for fracturing
35    pub force_accumulator: Vec3,
36    pub torque_accumulator: Vec3,
37}
38
39impl Default for RigidBody {
40    fn default() -> Self {
41        Self {
42            body_type: BodyType::Dynamic,
43            mass: 1.0,
44            restitution: 0.5,
45            friction: 0.5,
46            linear_damping: 0.01,
47            angular_damping: 0.05,
48            use_gravity: true,
49            is_sleeping: false,
50            ccd_enabled: false,
51            local_inertia: Vec3::splat(1.0),
52            lock_rotation_x: false,
53            lock_rotation_y: false,
54            lock_rotation_z: false,
55            lock_translation_x: false,
56            lock_translation_y: false,
57            lock_translation_z: false,
58            sleep_counter: 0,
59            center_of_mass: Vec3::ZERO,
60            fracture_threshold: None,
61            force_accumulator: Vec3::ZERO,
62            torque_accumulator: Vec3::ZERO,
63        }
64    }
65}
66
67impl RigidBody {
68    pub fn new(mass: f32, restitution: f32, friction: f32, use_gravity: bool) -> Self {
69        Self {
70            mass,
71            restitution,
72            friction,
73            use_gravity,
74            ..Default::default()
75        }
76    }
77
78    pub fn new_static() -> Self {
79        Self {
80            body_type: BodyType::Static,
81            mass: 0.0,
82            restitution: 0.0,
83            friction: 1.0,
84            linear_damping: 0.0,
85            angular_damping: 0.0,
86            use_gravity: false,
87            is_sleeping: true,
88            local_inertia: Vec3::ZERO,
89            lock_rotation_x: true,
90            lock_rotation_y: true,
91            lock_rotation_z: true,
92            lock_translation_x: true,
93            lock_translation_y: true,
94            lock_translation_z: true,
95            ..Default::default()
96        }
97    }
98
99    pub fn new_kinematic() -> Self {
100        Self {
101            body_type: BodyType::Kinematic,
102            mass: 0.0,
103            restitution: 0.0,
104            friction: 0.5,
105            linear_damping: 0.0,
106            angular_damping: 0.0,
107            use_gravity: false,
108            ccd_enabled: true,
109            local_inertia: Vec3::ZERO,
110            ..Default::default()
111        }
112    }
113
114    pub fn with_fracture_threshold(mut self, threshold: f32) -> Self {
115        self.fracture_threshold = Some(threshold);
116        self
117    }
118
119    pub fn wake_up(&mut self) {
120        self.is_sleeping = false;
121        self.sleep_counter = 0;
122    }
123
124    pub fn can_sleep(&self, velocity: &Velocity) -> bool {
125        if self.is_kinematic() {
126            return false; // Kinematic bodies never sleep — user controls their motion
127        }
128        if !self.is_dynamic() {
129            return true; // Static bodies are always "asleep"
130        }
131
132        const SLEEP_LINEAR_THRESHOLD: f32 = 0.05;
133        const SLEEP_ANGULAR_THRESHOLD: f32 = 0.05;
134
135        velocity.linear.length_squared() < SLEEP_LINEAR_THRESHOLD * SLEEP_LINEAR_THRESHOLD
136            && velocity.angular.length_squared() < SLEEP_ANGULAR_THRESHOLD * SLEEP_ANGULAR_THRESHOLD
137    }
138
139    pub fn update_sleep_state(&mut self, velocity: &Velocity) {
140        const SLEEP_FRAMES_REQUIRED: u32 = 60; // ~1 second at 60fps
141
142        if self.can_sleep(velocity) {
143            self.sleep_counter += 1;
144            if self.sleep_counter >= SLEEP_FRAMES_REQUIRED {
145                self.is_sleeping = true;
146            }
147        } else {
148            self.sleep_counter = 0;
149            self.is_sleeping = false;
150        }
151    }
152
153    #[inline]
154    pub fn is_dynamic(&self) -> bool {
155        matches!(self.body_type, BodyType::Dynamic)
156    }
157
158    #[inline]
159    pub fn is_kinematic(&self) -> bool {
160        matches!(self.body_type, BodyType::Kinematic)
161    }
162
163    #[inline]
164    pub fn is_static(&self) -> bool {
165        matches!(self.body_type, BodyType::Static)
166    }
167
168    #[inline]
169    pub fn enforce_locks(&self, vel: &mut Velocity) {
170        if self.lock_translation_x {
171            vel.linear.x = 0.0;
172        }
173        if self.lock_translation_y {
174            vel.linear.y = 0.0;
175        }
176        if self.lock_translation_z {
177            vel.linear.z = 0.0;
178        }
179        if self.lock_rotation_x {
180            vel.angular.x = 0.0;
181        }
182        if self.lock_rotation_y {
183            vel.angular.y = 0.0;
184        }
185        if self.lock_rotation_z {
186            vel.angular.z = 0.0;
187        }
188    }
189
190    #[inline]
191    pub fn inv_mass(&self) -> f32 {
192        if self.mass == 0.0 || !self.is_dynamic() {
193            0.0
194        } else {
195            1.0 / self.mass
196        }
197    }
198
199    #[inline]
200    pub fn inv_local_inertia(&self) -> Vec3 {
201        if self.mass == 0.0 || !self.is_dynamic() {
202            Vec3::ZERO
203        } else {
204            Vec3::new(
205                if self.local_inertia.x == 0.0 {
206                    0.0
207                } else {
208                    1.0 / self.local_inertia.x
209                },
210                if self.local_inertia.y == 0.0 {
211                    0.0
212                } else {
213                    1.0 / self.local_inertia.y
214                },
215                if self.local_inertia.z == 0.0 {
216                    0.0
217                } else {
218                    1.0 / self.local_inertia.z
219                },
220            )
221        }
222    }
223
224    /// Get inverse world-space inertia tensor
225    pub fn inv_world_inertia_tensor_identity(&self) -> Mat3 {
226        Mat3::from_diagonal(self.inv_local_inertia())
227    }
228
229    /// Get world-space inertia tensor from local inertia and rotation
230    pub fn world_inertia_tensor(&self, rotation: Quat) -> Mat3 {
231        let rot_mat = Mat3::from_quat(rotation);
232        let local_inertia_mat = Mat3::from_diagonal(self.local_inertia);
233        rot_mat * local_inertia_mat * rot_mat.transpose()
234    }
235
236    /// Get inverse world-space inertia tensor
237    pub fn inv_world_inertia_tensor(&self, rotation: Quat) -> Mat3 {
238        if self.mass == 0.0 || !self.is_dynamic() {
239            return Mat3::ZERO;
240        }
241        let rot_mat = Mat3::from_quat(rotation);
242        let inv_local = Mat3::from_diagonal(self.inv_local_inertia());
243        let mut inv_world = rot_mat * inv_local * rot_mat.transpose();
244
245        // Zero out locked world axes
246        if self.lock_rotation_x {
247            inv_world.x_axis = Vec3::ZERO;
248            inv_world.y_axis.x = 0.0;
249            inv_world.z_axis.x = 0.0;
250        }
251        if self.lock_rotation_y {
252            inv_world.y_axis = Vec3::ZERO;
253            inv_world.x_axis.y = 0.0;
254            inv_world.z_axis.y = 0.0;
255        }
256        if self.lock_rotation_z {
257            inv_world.z_axis = Vec3::ZERO;
258            inv_world.x_axis.z = 0.0;
259            inv_world.y_axis.z = 0.0;
260        }
261
262        inv_world
263    }
264
265    pub fn clear_forces(&mut self) {
266        self.force_accumulator = Vec3::ZERO;
267        self.torque_accumulator = Vec3::ZERO;
268    }
269
270    pub fn calculate_box_inertia(&mut self, w: f32, h: f32, d: f32) {
271        let m = self.mass;
272        self.local_inertia = Vec3::new(
273            (m / 12.0) * (h * h + d * d),
274            (m / 12.0) * (w * w + d * d),
275            (m / 12.0) * (w * w + h * h),
276        );
277    }
278
279    pub fn calculate_sphere_inertia(&mut self, r: f32) {
280        let i = 0.4 * self.mass * r * r;
281        self.local_inertia = Vec3::splat(i);
282    }
283
284    pub fn calculate_capsule_inertia(&mut self, r: f32, half_h: f32) {
285        let m = self.mass;
286        let h = half_h * 2.0;
287        let vol_cyl = std::f32::consts::PI * r * r * h;
288        let vol_sph = 4.0 / 3.0 * std::f32::consts::PI * r * r * r;
289        let total_vol = vol_cyl + vol_sph;
290
291        let m_cyl = if total_vol > 0.0 {
292            m * vol_cyl / total_vol
293        } else {
294            0.0
295        };
296        let m_sph = if total_vol > 0.0 {
297            m * vol_sph / total_vol
298        } else {
299            0.0
300        };
301
302        let i_y = m_cyl * (r * r) / 2.0 + m_sph * 2.0 * (r * r) / 5.0;
303        let i_cyl_xz = m_cyl * (3.0 * r * r + h * h) / 12.0;
304        let i_sph_xz =
305            m_sph * (0.4 * r * r + half_h * half_h + 0.75 * r * half_h + 0.140625 * r * r);
306        let i_xz = i_cyl_xz + i_sph_xz;
307
308        self.local_inertia = Vec3::new(i_xz, i_y, i_xz);
309    }
310
311    pub fn update_inertia_from_collider(&mut self, collider: &Collider) {
312        match &collider.shape {
313            ColliderShape::Box(b) => {
314                let w = b.half_extents.x * 2.0;
315                let h = b.half_extents.y * 2.0;
316                let d = b.half_extents.z * 2.0;
317                self.calculate_box_inertia(w, h, d);
318            }
319            ColliderShape::Sphere(s) => {
320                self.calculate_sphere_inertia(s.radius);
321            }
322            ColliderShape::Capsule(c) => {
323                self.calculate_capsule_inertia(c.radius, c.half_height);
324            }
325            ColliderShape::Plane(_) => {
326                self.local_inertia = Vec3::splat(f32::INFINITY);
327            }
328            ColliderShape::TriMesh(_) | ColliderShape::ConvexHull(_) => {
329                self.calculate_box_inertia(1.0, 1.0, 1.0);
330            }
331            ColliderShape::Compound(shapes) => {
332                let mut total_vol = 0.0;
333                let mut vols = Vec::with_capacity(shapes.len());
334                for (_, sub_shape) in shapes {
335                    let temp_col = Collider {
336                        shape: (**sub_shape).clone(),
337                        ..Default::default()
338                    };
339                    let v = temp_col.volume();
340                    vols.push(v);
341                    total_vol += v;
342                }
343
344                if total_vol > 0.0 {
345                    let mut com = Vec3::ZERO;
346                    for (i, (local_t, _)) in shapes.iter().enumerate() {
347                        let mass_i = (vols[i] / total_vol) * self.mass;
348                        com += local_t.position * mass_i;
349                    }
350                    if self.mass > 0.0 {
351                        self.center_of_mass = com / self.mass;
352                    }
353
354                    let mut inertia = Vec3::ZERO;
355                    for (i, (local_t, sub_shape)) in shapes.iter().enumerate() {
356                        let mass_i = (vols[i] / total_vol) * self.mass;
357
358                        let mut temp_rb = RigidBody {
359                            mass: mass_i,
360                            ..Default::default()
361                        };
362                        let temp_col = Collider {
363                            shape: (**sub_shape).clone(),
364                            ..Default::default()
365                        };
366                        temp_rb.update_inertia_from_collider(&temp_col);
367
368                        let d = local_t.position - self.center_of_mass;
369                        let d_sq = d.length_squared();
370
371                        inertia.x += temp_rb.local_inertia.x + mass_i * (d_sq - d.x * d.x);
372                        inertia.y += temp_rb.local_inertia.y + mass_i * (d_sq - d.y * d.y);
373                        inertia.z += temp_rb.local_inertia.z + mass_i * (d_sq - d.z * d.z);
374                    }
375                    self.local_inertia = inertia;
376                } else {
377                    self.calculate_box_inertia(1.0, 1.0, 1.0);
378                }
379            }
380        }
381    }
382}
383
384gizmo_core::impl_component!(RigidBody);