Skip to main content

rhusics_core/physics/
resolution.rs

1use std::fmt::Debug;
2use std::marker;
3use std::ops::{Add, Mul, Sub};
4
5use cgmath::num_traits::{Float, NumCast};
6use cgmath::{BaseFloat, EuclideanSpace, InnerSpace, One, Rotation, Zero};
7use collision::Contact;
8
9use super::{Inertia, Mass, Material, PartialCrossProduct, Velocity};
10use {NextFrame, Pose};
11
12const POSITIONAL_CORRECTION_PERCENT: f32 = 0.2;
13const POSITIONAL_CORRECTION_K_SLOP: f32 = 0.01;
14
15/// Changes computed from contact resolution.
16///
17/// Optionally contains the new pose and/or velocity of a body after contact resolution.
18///
19/// ### Type parameters:
20///
21/// - `B`: Transform type (`BodyPose3` or similar)
22/// - `P`: Point type, usually `Point2` or `Point3`
23/// - `R`: Rotational quantity, usually `Basis2` or `Quaternion`
24/// - `A`: Angular velocity, usually `Scalar` or `Vector3`
25#[derive(Debug, PartialEq)]
26pub struct SingleChangeSet<B, P, R, A>
27where
28    P: EuclideanSpace,
29    P::Scalar: BaseFloat,
30    R: Rotation<P>,
31    A: Clone,
32    B: Pose<P, R>,
33{
34    pose: Option<B>,
35    velocity: Option<NextFrame<Velocity<P::Diff, A>>>,
36    m: marker::PhantomData<(P, R)>,
37}
38
39impl<B, P, R, A> Default for SingleChangeSet<B, P, R, A>
40where
41    P: EuclideanSpace,
42    P::Scalar: BaseFloat,
43    R: Rotation<P>,
44    A: Clone,
45    B: Pose<P, R>,
46{
47    fn default() -> Self {
48        Self {
49            pose: None,
50            velocity: None,
51            m: marker::PhantomData,
52        }
53    }
54}
55
56impl<B, P, R, A> SingleChangeSet<B, P, R, A>
57where
58    P: EuclideanSpace,
59    P::Scalar: BaseFloat,
60    R: Rotation<P>,
61    A: Clone,
62    B: Pose<P, R>,
63{
64    #[allow(dead_code)]
65    fn new(pose: Option<B>, velocity: Option<NextFrame<Velocity<P::Diff, A>>>) -> Self {
66        SingleChangeSet {
67            pose,
68            velocity,
69            m: marker::PhantomData,
70        }
71    }
72
73    fn add_pose(&mut self, pose: Option<B>) {
74        self.pose = pose;
75    }
76
77    fn add_velocity(&mut self, velocity: Option<NextFrame<Velocity<P::Diff, A>>>) {
78        self.velocity = velocity;
79    }
80
81    /// Apply any changes to the next frame pose and/or velocity
82    pub fn apply(
83        self,
84        pose: Option<&mut NextFrame<B>>,
85        velocity: Option<&mut NextFrame<Velocity<P::Diff, A>>>,
86    ) {
87        if let (Some(pose), Some(update_pose)) = (pose, self.pose) {
88            pose.value = update_pose;
89        }
90        if let (Some(velocity), Some(update_velocity)) = (velocity, self.velocity) {
91            *velocity = update_velocity;
92        }
93    }
94}
95
96/// Data used for contact resolution
97///
98/// ### Type parameters:
99///
100/// - `B`: Transform type (`BodyPose3` or similar)
101/// - `P`: Point type, usually `Point2` or `Point3`
102/// - `R`: Rotational quantity, usually `Basis2` or `Quaternion`
103/// - `A`: Angular velocity, usually `Scalar` or `Vector3`
104/// - `I`: Inertia, usually `Scalar` or `Matrix3`
105pub struct ResolveData<'a, B, P, R, I, A>
106where
107    P: EuclideanSpace + 'a,
108    P::Scalar: BaseFloat,
109    R: Rotation<P> + 'a,
110    I: 'a,
111    A: Clone + 'a,
112    B: Pose<P, R> + 'a,
113{
114    /// Velocity for next frame
115    pub velocity: Option<&'a NextFrame<Velocity<P::Diff, A>>>,
116    /// Position for next frame
117    pub pose: &'a B,
118    /// Mass
119    pub mass: &'a Mass<P::Scalar, I>,
120    /// Material
121    pub material: &'a Material,
122    m: marker::PhantomData<(P, R)>,
123}
124
125impl<'a, B, P, R, I, A> ResolveData<'a, B, P, R, I, A>
126where
127    P: EuclideanSpace + 'a,
128    P::Scalar: BaseFloat,
129    R: Rotation<P> + 'a,
130    I: 'a,
131    A: Clone + 'a,
132    B: Pose<P, R> + 'a,
133{
134    /// Create resolve data
135    pub fn new(
136        velocity: Option<&'a NextFrame<Velocity<P::Diff, A>>>,
137        pose: &'a B,
138        mass: &'a Mass<P::Scalar, I>,
139        material: &'a Material,
140    ) -> Self {
141        ResolveData {
142            velocity,
143            pose,
144            mass,
145            material,
146            m: marker::PhantomData,
147        }
148    }
149}
150
151/// Perform contact resolution.
152///
153/// Will compute any new poses and/or velocities, by doing impulse resolution of the given contact.
154///
155/// ### Parameters:
156///
157/// - `contact`: The contact; contact normal must point from shape A -> B
158/// - `a`: Resolution data for shape A
159/// - `b`: Resolution data for shape B
160///
161/// ### Returns
162///
163/// Tuple of change sets, first change set is for shape A, second change set for shape B.
164///
165/// ### Type parameters:
166///
167/// - `B`: Transform type (`BodyPose3` or similar)
168/// - `P`: Point type, usually `Point2` or `Point3`
169/// - `R`: Rotational quantity, usually `Basis2` or `Quaternion`
170/// - `A`: Angular velocity, usually `Scalar` or `Vector3`
171/// - `I`: Inertia, usually `Scalar` or `Matrix3`
172/// - `O`: Internal type used for unifying cross products for 2D/3D, usually `Scalar` or `Vector3`
173pub fn resolve_contact<'a, B, P, R, I, A, O>(
174    contact: &Contact<P>,
175    a: &ResolveData<'a, B, P, R, I, A>,
176    b: &ResolveData<'a, B, P, R, I, A>,
177) -> (SingleChangeSet<B, P, R, A>, SingleChangeSet<B, P, R, A>)
178where
179    P: EuclideanSpace + 'a,
180    P::Scalar: BaseFloat,
181    R: Rotation<P> + 'a,
182    P::Diff: Debug + Zero + Clone + InnerSpace + PartialCrossProduct<P::Diff, Output = O>,
183    O: PartialCrossProduct<P::Diff, Output = P::Diff>,
184    A: PartialCrossProduct<P::Diff, Output = P::Diff> + Clone + Zero + 'a,
185    &'a A: Sub<O, Output = A> + Add<O, Output = A>,
186    I: Inertia<Orientation = R> + Mul<O, Output = O>,
187    B: Pose<P, R> + 'a,
188{
189    let a_velocity = a.velocity.map(|v| v.value.clone()).unwrap_or_default();
190    let b_velocity = b.velocity.map(|v| v.value.clone()).unwrap_or_default();
191    let a_inverse_mass = a.mass.inverse_mass();
192    let b_inverse_mass = b.mass.inverse_mass();
193    let total_inverse_mass = a_inverse_mass + b_inverse_mass;
194
195    // Do positional correction, so bodies aren't penetrating as much any longer.
196    let (a_position_new, b_position_new) =
197        positional_correction(contact, a.pose, b.pose, a_inverse_mass, b_inverse_mass);
198
199    let mut a_set = SingleChangeSet::default();
200    a_set.add_pose(a_position_new);
201    let mut b_set = SingleChangeSet::default();
202    b_set.add_pose(b_position_new);
203
204    // This only happens when we have 2 infinite masses colliding. We only do positional correction
205    // for the bodies and return early
206    if total_inverse_mass == P::Scalar::zero() {
207        return (a_set, b_set);
208    }
209
210    let r_a = contact.contact_point - a.pose.transform_point(P::origin());
211    let r_b = contact.contact_point - b.pose.transform_point(P::origin());
212
213    let p_a_dot = *a_velocity.linear() + a_velocity.angular().cross(&r_a);
214    let p_b_dot = *b_velocity.linear() + b_velocity.angular().cross(&r_b);
215
216    let rv = p_b_dot - p_a_dot;
217    let velocity_along_normal = contact.normal.dot(rv);
218
219    // Check if shapes are already separating, if so only do positional correction
220    if velocity_along_normal > P::Scalar::zero() {
221        return (a_set, b_set);
222    }
223
224    // Compute impulse force
225    let a_res: P::Scalar = a.material.restitution();
226    let b_res: P::Scalar = b.material.restitution();
227    let e = a_res.min(b_res);
228    let numerator = -(P::Scalar::one() + e) * velocity_along_normal;
229
230    let a_tensor = a.mass.world_inverse_inertia(&a.pose.rotation());
231    let b_tensor = b.mass.world_inverse_inertia(&b.pose.rotation());
232
233    let term3 = contact
234        .normal
235        .dot((a_tensor * (r_a.cross(&contact.normal))).cross(&r_a));
236    let term4 = contact
237        .normal
238        .dot((b_tensor * (r_b.cross(&contact.normal))).cross(&r_b));
239
240    let j = numerator / (a_inverse_mass + b_inverse_mass + term3 + term4);
241    let impulse = contact.normal * j;
242
243    // Compute new velocities based on mass and the computed impulse
244    let a_velocity_new = a.velocity.map(|v| NextFrame {
245        value: Velocity::new(
246            *v.value.linear() - impulse * a_inverse_mass,
247            v.value.angular() - a_tensor * r_a.cross(&impulse),
248        ),
249    });
250
251    let b_velocity_new = b.velocity.map(|v| NextFrame {
252        value: Velocity::new(
253            *v.value.linear() + impulse * b_inverse_mass,
254            v.value.angular() + b_tensor * r_b.cross(&impulse),
255        ),
256    });
257
258    a_set.add_velocity(a_velocity_new);
259    b_set.add_velocity(b_velocity_new);
260
261    (a_set, b_set)
262}
263
264/// Do positional correction for colliding bodies.
265///
266/// Will only do correction for a percentage of the penetration depth, to avoid stability issues.
267///
268/// ### Parameters:
269///
270/// - `contact`: Contact information, normal must point from A -> B
271/// - `a_position`: World coordinates of center of mass for body A
272/// - `b_position`: World coordinates of center of mass for body B
273/// - `a_inverse_mass`: Inverse mass of body A
274/// - `b_inverse_mass`: Inverse mass of body B
275///
276/// ### Returns:
277///
278/// Tuple with new positions for the given bodies
279///
280/// ### Type parameters:
281///
282/// - `S`: Scalar type
283/// - `B`: Transform type (`BodyPose3` or similar)
284/// - `P`: Positional quantity, usually `Point2` or `Point3`
285/// - `R`: Rotational quantity, usually `Basis2` or `Quaternion`
286fn positional_correction<S, B, P, R>(
287    contact: &Contact<P>,
288    a_position: &B,
289    b_position: &B,
290    a_inverse_mass: S,
291    b_inverse_mass: S,
292) -> (Option<B>, Option<B>)
293where
294    S: BaseFloat,
295    P: EuclideanSpace<Scalar = S>,
296    R: Rotation<P>,
297    P::Diff: Debug + Zero + Clone + InnerSpace,
298    B: Pose<P, R>,
299{
300    let total_inverse_mass = a_inverse_mass + b_inverse_mass;
301    let k_slop: S = NumCast::from(POSITIONAL_CORRECTION_K_SLOP).unwrap();
302    let percent: S = NumCast::from(POSITIONAL_CORRECTION_PERCENT).unwrap();
303    let correction_penetration_depth = contact.penetration_depth - k_slop;
304    let correction_magnitude =
305        correction_penetration_depth.max(S::zero()) / total_inverse_mass * percent;
306    let correction = contact.normal * correction_magnitude;
307    let a_position_new = new_pose(a_position, correction * -a_inverse_mass);
308    let b_position_new = new_pose(b_position, correction * b_inverse_mass);
309    (Some(a_position_new), Some(b_position_new))
310}
311
312fn new_pose<B, P, R>(next_frame: &B, correction: P::Diff) -> B
313where
314    P: EuclideanSpace,
315    P::Scalar: BaseFloat,
316    R: Rotation<P>,
317    P::Diff: Clone,
318    B: Pose<P, R>,
319{
320    B::new(next_frame.position() + correction, next_frame.rotation())
321}
322
323#[cfg(test)]
324mod tests {
325    use cgmath::{Basis2, One, Point2, Vector2};
326    use collision::{CollisionStrategy, Contact};
327
328    use super::*;
329    use collide::ContactEvent;
330    use BodyPose;
331
332    #[test]
333    fn test_resolve_2d_f32() {
334        let mass = Mass::<f32, f32>::new_with_inertia(0.5, 0.);
335        let material = Material::default();
336        let left_velocity = NextFrame {
337            value: Velocity::new(Vector2::<f32>::new(1., 0.), 0.),
338        };
339        let left_pose = BodyPose::new(Point2::origin(), Basis2::one());
340        let right_velocity = NextFrame {
341            value: Velocity::new(Vector2::new(-2., 0.), 0.),
342        };
343        let right_pose = BodyPose::new(Point2::new(1., 0.), Basis2::one());
344        let contact = ContactEvent::new(
345            (1, 2),
346            Contact::new_impl(CollisionStrategy::FullResolution, Vector2::new(1., 0.), 0.5),
347        );
348        let set = resolve_contact(
349            &contact.contact,
350            &ResolveData::new(Some(&left_velocity), &left_pose, &mass, &material),
351            &ResolveData::new(Some(&right_velocity), &right_pose, &mass, &material),
352        );
353        assert_eq!(
354            (
355                SingleChangeSet::new(
356                    Some(BodyPose::new(
357                        Point2::new(-0.04900000075250864, 0.),
358                        Basis2::one()
359                    )),
360                    Some(NextFrame {
361                        value: Velocity::new(Vector2::new(-2., 0.), 0.),
362                    }),
363                ),
364                SingleChangeSet::new(
365                    Some(BodyPose::new(
366                        Point2::new(1.0490000007525087, 0.),
367                        Basis2::one()
368                    )),
369                    Some(NextFrame {
370                        value: Velocity::new(Vector2::new(1., 0.), 0.),
371                    }),
372                )
373            ),
374            set
375        );
376    }
377
378    #[test]
379    fn test_resolve_2d_f64() {
380        let mass = Mass::<f64, f64>::new_with_inertia(0.5, 0.);
381        let material = Material::default();
382        let left_velocity = NextFrame {
383            value: Velocity::new(Vector2::<f64>::new(1., 0.), 0.),
384        };
385        let left_pose = BodyPose::new(Point2::origin(), Basis2::one());
386        let right_velocity = NextFrame {
387            value: Velocity::new(Vector2::new(-2., 0.), 0.),
388        };
389        let right_pose = BodyPose::new(Point2::new(1., 0.), Basis2::one());
390        let contact = ContactEvent::new(
391            (1, 2),
392            Contact::new_impl(CollisionStrategy::FullResolution, Vector2::new(1., 0.), 0.5),
393        );
394        let set = resolve_contact(
395            &contact.contact,
396            &ResolveData::new(Some(&left_velocity), &left_pose, &mass, &material),
397            &ResolveData::new(Some(&right_velocity), &right_pose, &mass, &material),
398        );
399        assert_eq!(
400            (
401                SingleChangeSet::new(
402                    Some(BodyPose::new(
403                        Point2::new(-0.04900000075250864, 0.),
404                        Basis2::one()
405                    )),
406                    Some(NextFrame {
407                        value: Velocity::new(Vector2::new(-2., 0.), 0.),
408                    }),
409                ),
410                SingleChangeSet::new(
411                    Some(BodyPose::new(
412                        Point2::new(1.0490000007525087, 0.),
413                        Basis2::one()
414                    )),
415                    Some(NextFrame {
416                        value: Velocity::new(Vector2::new(1., 0.), 0.),
417                    }),
418                )
419            ),
420            set
421        );
422    }
423}