Skip to main content

linear_sim/constraint/
contact.rs

1use derive_more::From;
2use either::Either;
3
4#[cfg(feature = "derive_serdes")]
5use serde::{Deserialize, Serialize};
6
7use crate::collision::Proximity;
8use crate::object;
9use crate::math::*;
10
11use super::{Constraint, Impulses, PostImpulses};
12
13/// Distance criterea for contacts and collisions (TOI contacts).
14///
15/// Continuous collision detection will aim to return the TOI for when the objects are
16/// at a distance approximately `0.5 * CONTACT_DISTANCE` in order to prevent
17/// intersection due to numerical error.
18///
19/// Post-impulse position correction will also move objects apart to a distance of
20/// `0.5 * CONTACT_DISTANCE`.
21pub const CONTACT_DISTANCE : f64 = 0.005;   // 5mm
22/// Velocity for determination of whether a normal impulse should be applied
23pub const RESTING_VELOCITY : f64 = (1.0/120.0) * CONTACT_DISTANCE;
24
25/// A contact constraint
26#[cfg_attr(feature = "derive_serdes", derive(Deserialize, Serialize))]
27#[derive(Clone, Debug, PartialEq, From)]
28pub struct Contact {
29  pub normal   : Unit3 <f64>,
30  pub distance : f64
31}
32
33impl Contact {
34  #[inline]
35  pub const fn new (normal : Unit3 <f64>, distance : f64) -> Self {
36    Contact { normal, distance }
37  }
38
39  pub(crate) fn constrain_velocity_with_restitution (&self,
40    mut object_a     : Either <&object::Static, &mut object::Dynamic>,
41    object_b         : &mut object::Dynamic,
42    pseudovelocity_a : Option <Vector3 <f64>>,
43    pseudovelocity_b : Vector3 <f64>,
44    // 0.0 for a resting contact
45    restitution      : Normalized <f64>
46  ) -> Option <Impulses> {
47    let velocity_effective_a = match &object_a {
48      Either::Left (_) => {
49        debug_assert!(pseudovelocity_a.is_none());
50        Vector3::zero()
51      }
52      Either::Right (object_a) => object_a.derivatives.velocity +
53        pseudovelocity_a.unwrap_or (Vector3::zero())
54    };
55    let velocity_effective_b        = object_b.derivatives.velocity + pseudovelocity_b;
56    let velocity_effective_relative = velocity_effective_a - velocity_effective_b;
57    let velocity_normal_component   = velocity_effective_relative.dot (*self.normal);
58    let mut impulses        = None;
59    let mut mass_relative_a = 0.0;
60    let mut mass_relative_b = 0.0;
61    if velocity_normal_component < 0.0 {
62      let velocity_normal = velocity_normal_component * *self.normal;
63      // normal impulse
64      let impulse_normal = velocity_normal + *restitution * velocity_normal;
65      let impulse_normal_magnitude2 = impulse_normal.magnitude_squared();
66      if impulse_normal_magnitude2 < RESTING_VELOCITY * RESTING_VELOCITY {
67        return None
68      }
69      let (impulse_normal_a, impulse_normal_b) = match &mut object_a {
70        Either::Right (object_a) => {
71          let mass_combined    = object_a.mass + object_b.mass;
72          mass_relative_a      = object_a.mass.mass() * mass_combined.mass_reciprocal();
73          mass_relative_b      = object_b.mass.mass() * mass_combined.mass_reciprocal();
74          let impulse_normal_a = -mass_relative_b * impulse_normal;
75          let impulse_normal_b = mass_relative_a * impulse_normal;
76          object_a.derivatives.velocity += impulse_normal_a;
77          object_b.derivatives.velocity += impulse_normal_b;
78          (impulse_normal_a, impulse_normal_b)
79        }
80        Either::Left (_) => {
81          object_b.derivatives.velocity += impulse_normal;
82          (Vector3::zero(), impulse_normal)
83        }
84      };
85      // friction (tangent) impulse
86      let (impulse_tangent_a, impulse_tangent_b) = {
87        let velocity_tangent = velocity_effective_relative - velocity_normal;
88        let velocity_tangent_magnitude2 = velocity_tangent.magnitude_squared();
89        if velocity_tangent_magnitude2 > 0.0 {
90          let friction_a = match &object_a {
91            Either::Left  (x) => x.material.friction,
92            Either::Right (x) => x.material.friction
93          };
94          let friction_coefficient = friction_a * object_b.material.friction;
95          let impulse_tangent_potential_magnitude2 =
96            *friction_coefficient * impulse_normal_magnitude2;
97          let impulse_tangent = if impulse_tangent_potential_magnitude2 <
98            velocity_tangent_magnitude2
99          {
100            (impulse_tangent_potential_magnitude2.sqrt()
101            / velocity_tangent_magnitude2.sqrt()) * velocity_tangent
102          } else {
103            velocity_tangent
104          };
105          match object_a {
106            Either::Right (object_a) => {
107              let impulse_tangent_a = -mass_relative_b * impulse_tangent;
108              let impulse_tangent_b = mass_relative_a * impulse_tangent;
109              object_a.derivatives.velocity += impulse_tangent_a;
110              object_b.derivatives.velocity += impulse_tangent_b;
111              (impulse_tangent_a, impulse_tangent_b)
112            }
113            Either::Left (_) => {
114              object_b.derivatives.velocity += impulse_tangent;
115              (Vector3::zero(), impulse_tangent)
116            }
117          }
118        } else {
119          (Vector3::zero(), Vector3::zero())
120        }
121      };
122      impulses = Some (Impulses {
123        impulse_normal_a, impulse_normal_b,
124        impulse_tangent_a, impulse_tangent_b
125      });
126    }
127    impulses
128  }
129}
130
131impl Constraint for Contact {
132  /// Resting contact: restitution == 0.0
133  fn constrain_velocity (&self,
134    object_a         : Either <&object::Static, &mut object::Dynamic>,
135    object_b         : &mut object::Dynamic,
136    pseudovelocity_a : Option <Vector3 <f64>>,
137    pseudovelocity_b : Vector3 <f64>
138  ) -> Option <Impulses> {
139    self.constrain_velocity_with_restitution (
140      object_a, object_b, pseudovelocity_a, pseudovelocity_b, Normalized::zero())
141  }
142
143  /// Queries current proximity and applies pseudovelocity impulse at `T-t_reverse` to
144  /// correct position.
145  fn constrain_position (&mut self,
146    t_reverse        : f64,
147    mut object_a     : Either <&object::Static, &mut object::Dynamic>,
148    object_b         : &mut object::Dynamic,
149    pseudovelocity_a : Option <&mut Vector3 <f64>>,
150    pseudovelocity_b : &mut Vector3 <f64>
151  ) -> Option <PostImpulses> {
152    let t_forward     = -t_reverse;
153    let mut impulses  = None;
154    let mut proximity = match &object_a {
155      Either::Left  (object_a) => Proximity::query (*object_a, object_b),
156      Either::Right (object_a) => Proximity::query (*object_a, object_b)
157    };
158    log::trace!(distance=proximity.distance; "post impulse contact @ T==1.0");
159    if cfg!(debug_assertions) {
160      approx::assert_abs_diff_eq!(proximity.normal.cross (proximity.half_axis),
161        Vector3::zero());
162    }
163    let normal = proximity.normal;
164    if proximity.distance < 0.0 {
165      let a_to_b = -proximity.distance * *proximity.normal;
166      let pseudo_impulse = (a_to_b + 0.5 * CONTACT_DISTANCE * *proximity.normal)
167        / t_forward;
168      let (pseudo_impulse_a, pseudo_impulse_b) = match &mut object_a {
169        Either::Left  (object_a) => {
170          object::lerp (object_b, *pseudovelocity_b, t_reverse);
171          let pseudo_impulse_b = -pseudo_impulse;
172          *pseudovelocity_b += pseudo_impulse_b;
173          object::lerp (object_b, *pseudovelocity_b, t_forward);
174          proximity = Proximity::query (*object_a, object_b);
175          (Vector3::zero(), pseudo_impulse_b)
176        }
177        Either::Right (object_a) => {
178          let pseudovelocity_a = pseudovelocity_a.unwrap();
179          object::lerp (*object_a, *pseudovelocity_a, t_reverse);
180          object::lerp (object_b, *pseudovelocity_b, t_reverse);
181          let mass_combined    = object_a.mass + object_b.mass;
182          let mass_relative_a  = object_a.mass.mass() * mass_combined.mass_reciprocal();
183          let mass_relative_b  = object_b.mass.mass() * mass_combined.mass_reciprocal();
184          let pseudo_impulse_a = mass_relative_b * pseudo_impulse;
185          let pseudo_impulse_b = -mass_relative_a * pseudo_impulse;
186          *pseudovelocity_a += pseudo_impulse_a;
187          *pseudovelocity_b += pseudo_impulse_b;
188          object::lerp (*object_a, *pseudovelocity_a, t_forward);
189          object::lerp (object_b, *pseudovelocity_b, t_forward);
190          proximity = Proximity::query (*object_a, object_b);
191          (pseudo_impulse_a, pseudo_impulse_b)
192        }
193      };
194      log::trace!(distance=proximity.distance; "position corrected distance @ T==1.0");
195      debug_assert!(proximity.distance >= 0.0);
196      if cfg!(debug_assertions) {
197        approx::assert_relative_eq!(*proximity.normal, *normal, epsilon=0.000_000_1);
198        approx::assert_abs_diff_eq!(
199          proximity.distance, 0.5 * CONTACT_DISTANCE, epsilon=0.000_000_001);
200      }
201      impulses = Some (PostImpulses { pseudo_impulse_a, pseudo_impulse_b });
202    }
203    *self = Contact { normal: proximity.normal, distance: proximity.distance };
204    impulses
205  }
206}
207
208impl Eq for Contact { }
209#[expect(clippy::non_canonical_partial_ord_impl)]
210impl PartialOrd for Contact {
211  /// Panics: should not be called; this trait is implemented so that narrow TOI
212  /// results can be automatically be sorted
213  fn partial_cmp (&self, _rhs : &Self) -> Option <std::cmp::Ordering> {
214    // TODO: compiler hint ?
215    unreachable!()
216  }
217}
218impl Ord for Contact {
219  fn cmp (&self, _other : &Self) -> std::cmp::Ordering {
220    // TODO: compiler hint ?
221    unreachable!()
222  }
223}
224
225impl TryFrom <Proximity> for Contact {
226  type Error = ();
227  fn try_from (Proximity { distance, normal, .. } : Proximity)
228    -> Result <Self, Self::Error>
229  {
230    if distance >= 0.0 && distance < CONTACT_DISTANCE {
231      Ok (Contact { normal, distance })
232    } else {
233      Err (())
234    }
235  }
236}