1use crate::body::{BodyId, RigidBody};
4use crate::collision::VectorOps;
5use crate::integrator::PhysicsSpace;
6use crate::response::Contact;
7use crate::response::FRICTION_COEFF;
8
9pub const MAX_POINTS: usize = 4;
10
11pub const CONTACT_BREAK_DISTANCE: f32 = 0.02;
13
14const CONTACT_BREAK_DISTANCE_SQ: f32 = CONTACT_BREAK_DISTANCE * CONTACT_BREAK_DISTANCE;
15
16const MERGE_RADIUS_SQ: f32 = CONTACT_BREAK_DISTANCE_SQ;
17
18#[cfg_attr(feature = "persist", derive(serde::Serialize, serde::Deserialize))]
19#[cfg_attr(
20 feature = "persist",
21 serde(bound(
22 serialize = "S::Point: serde::Serialize, S::Vector: serde::Serialize, S::Iso: serde::Serialize, S::AngVel: serde::Serialize, S::Inertia: serde::Serialize",
23 deserialize = "S::Point: serde::Deserialize<'de>, S::Vector: serde::Deserialize<'de>, S::Iso: serde::Deserialize<'de>, S::AngVel: serde::Deserialize<'de>, S::Inertia: serde::Deserialize<'de>"
24 ))
25)]
26pub struct ContactPoint<S: PhysicsSpace> {
27 pub world_point: S::Point,
28 pub anchor_a: S::Vector,
30 pub anchor_b: S::Vector,
32 pub normal: S::Vector,
34 pub penetration: f32,
35 pub normal_impulse: f32,
36 pub tangent_dir: S::Vector,
38 pub tangent_impulse: f32,
40 pub velocity_bias: f32,
42}
43
44impl<S: PhysicsSpace> Clone for ContactPoint<S> {
45 fn clone(&self) -> Self {
46 *self
47 }
48}
49
50impl<S: PhysicsSpace> Copy for ContactPoint<S> {}
51
52#[cfg_attr(feature = "persist", derive(serde::Serialize, serde::Deserialize))]
53#[cfg_attr(
54 feature = "persist",
55 serde(bound(
56 serialize = "S::Point: serde::Serialize, S::Vector: serde::Serialize, S::Iso: serde::Serialize, S::AngVel: serde::Serialize, S::Inertia: serde::Serialize",
57 deserialize = "S::Point: serde::Deserialize<'de>, S::Vector: serde::Deserialize<'de>, S::Iso: serde::Deserialize<'de>, S::AngVel: serde::Deserialize<'de>, S::Inertia: serde::Deserialize<'de>"
58 ))
59)]
60pub struct Manifold<S: PhysicsSpace> {
61 pub body_a: BodyId,
63 pub body_b: BodyId,
64 pub restitution: f32,
66 pub points: Vec<ContactPoint<S>>,
67}
68
69impl<S: PhysicsSpace> Clone for Manifold<S> {
70 fn clone(&self) -> Self {
71 Self {
72 body_a: self.body_a,
73 body_b: self.body_b,
74 restitution: self.restitution,
75 points: self.points.clone(),
76 }
77 }
78
79 fn clone_from(&mut self, source: &Self) {
80 self.body_a = source.body_a;
81 self.body_b = source.body_b;
82 self.restitution = source.restitution;
83 self.points.clone_from(&source.points);
84 }
85}
86
87impl<S: PhysicsSpace> Manifold<S>
88where
89 S::Vector: VectorOps,
90{
91 pub fn new(body_a: BodyId, body_b: BodyId, restitution: f32) -> Self {
92 debug_assert!(body_a < body_b);
93 Self {
94 body_a,
95 body_b,
96 restitution,
97 points: Vec::with_capacity(MAX_POINTS),
98 }
99 }
100
101 pub(crate) fn reset(&mut self, body_a: BodyId, body_b: BodyId, restitution: f32) {
102 debug_assert!(body_a < body_b);
103 self.body_a = body_a;
104 self.body_b = body_b;
105 self.restitution = restitution;
106 self.points.clear();
107 }
108
109 pub(crate) fn is_valid(&self, space: &S, body_a: &RigidBody<S>, body_b: &RigidBody<S>) -> bool
110 where
111 S::Point: Copy + std::ops::Sub<Output = S::Vector>,
112 {
113 if !self.restitution.is_finite()
114 || self.restitution < 0.0
115 || self.points.is_empty()
116 || self.points.len() > MAX_POINTS
117 {
118 return false;
119 }
120 self.points.iter().all(|point| {
121 let normal_length = VectorOps::length_squared(point.normal);
122 let tangent_length = VectorOps::length_squared(point.tangent_dir);
123 if !space.valid_point(point.world_point)
124 || !space.valid_vector(point.anchor_a)
125 || !space.valid_vector(point.anchor_b)
126 || !space.valid_vector(point.normal)
127 || !space.valid_vector(point.tangent_dir)
128 || (normal_length - 1.0).abs() > 1.0e-4
129 || !point.penetration.is_finite()
130 || !point.normal_impulse.is_finite()
131 || point.normal_impulse < 0.0
132 || !point.tangent_impulse.is_finite()
133 || point.tangent_impulse < 0.0
134 || !point.velocity_bias.is_finite()
135 {
136 return false;
137 }
138 if point.tangent_impulse == 0.0 {
139 if tangent_length != 0.0 {
140 return false;
141 }
142 } else {
143 let limit = point.normal_impulse * FRICTION_COEFF;
144 let slack = f32::EPSILON * limit.abs().max(1.0);
145 if (tangent_length - 1.0).abs() > 1.0e-4
146 || VectorOps::dot(point.normal, point.tangent_dir).abs() > 1.0e-4
147 || point.tangent_impulse > limit + slack
148 {
149 return false;
150 }
151 }
152 let anchor_a = anchor_world_point(space, body_a, point.anchor_a);
153 let anchor_b = anchor_world_point(space, body_b, point.anchor_b);
154 if !space.valid_point(anchor_a) || !space.valid_point(anchor_b) {
155 return false;
156 }
157 let gap = space.log(anchor_a, anchor_b);
158 if !space.valid_vector(gap) {
159 return false;
160 }
161 let midpoint = space.exp(anchor_a, gap * 0.5);
162 let midpoint_error = space.log(midpoint, point.world_point);
163 let penetration_error = point.penetration + VectorOps::dot(gap, point.normal);
164 space.valid_point(midpoint)
165 && space.valid_vector(midpoint_error)
166 && VectorOps::length(midpoint_error) <= CONTACT_BREAK_DISTANCE
167 && penetration_error.is_finite()
168 && penetration_error.abs() <= CONTACT_BREAK_DISTANCE
169 })
170 }
171
172 pub fn refresh(&mut self, space: &S, a: &RigidBody<S>, b: &RigidBody<S>) {
174 self.points.retain_mut(|cp| {
175 let pa = anchor_world_point(space, a, cp.anchor_a);
176 let pb = anchor_world_point(space, b, cp.anchor_b);
177 let gap = space.log(pa, pb);
178 let separation = VectorOps::dot(gap, cp.normal);
179 if separation > CONTACT_BREAK_DISTANCE {
180 return false;
181 }
182 let tangential = gap - cp.normal * separation;
183 if VectorOps::length_squared(tangential) > CONTACT_BREAK_DISTANCE_SQ {
184 return false;
185 }
186 cp.world_point = space.exp(pa, gap * 0.5);
187 cp.penetration = -separation;
188 true
189 });
190 }
191
192 pub fn add_or_update(
194 &mut self,
195 space: &S,
196 a: &RigidBody<S>,
197 b: &RigidBody<S>,
198 contact: Contact<S>,
199 ) where
200 S::Point: Copy + std::ops::Sub<Output = S::Vector>,
201 {
202 let new_point = contact.point;
203 let half_gap = contact.normal * (0.5 * contact.penetration);
204 let anchor_a = local_anchor(space, a, space.exp(new_point, half_gap));
205 let anchor_b = local_anchor(space, b, space.exp(new_point, -half_gap));
206
207 for cp in &mut self.points {
208 let delta = new_point - cp.world_point;
209 if VectorOps::length_squared(delta) < MERGE_RADIUS_SQ {
210 cp.world_point = new_point;
211 cp.anchor_a = anchor_a;
212 cp.anchor_b = anchor_b;
213 cp.normal_impulse *= VectorOps::dot(cp.normal, contact.normal).max(0.0);
214 cp.tangent_impulse = 0.0;
215 cp.tangent_dir = VectorOps::zero();
216 cp.normal = contact.normal;
217 cp.penetration = contact.penetration;
218 return;
219 }
220 }
221
222 let fresh = ContactPoint {
223 world_point: new_point,
224 anchor_a,
225 anchor_b,
226 normal: contact.normal,
227 penetration: contact.penetration,
228 normal_impulse: 0.0,
229 tangent_dir: VectorOps::zero(),
230 tangent_impulse: 0.0,
231 velocity_bias: 0.0,
232 };
233
234 if self.points.len() < MAX_POINTS {
235 self.points.push(fresh);
236 } else {
237 if let Some((worst, _)) = self.points.iter().enumerate().min_by(|(_, a), (_, b)| {
238 let sa = a.normal_impulse + a.tangent_impulse.abs();
239 let sb = b.normal_impulse + b.tangent_impulse.abs();
240 sa.partial_cmp(&sb).unwrap_or(std::cmp::Ordering::Equal)
241 }) {
242 self.points[worst] = fresh;
243 }
244 }
245 }
246}
247
248fn local_anchor<S: PhysicsSpace>(space: &S, body: &RigidBody<S>, p: S::Point) -> S::Vector {
250 let lever = space.log(body.position, p);
251 space.iso_transport(space.iso_inverse(body.orientation), body.position, lever)
252}
253
254fn anchor_world_point<S: PhysicsSpace>(
255 space: &S,
256 body: &RigidBody<S>,
257 anchor: S::Vector,
258) -> S::Point {
259 let lever = space.iso_transport(body.orientation, body.position, anchor);
260 space.exp(body.position, lever)
261}
262
263pub const DEFAULT_PGS_ITERS: usize = 8;
264
265pub const BAUMGARTE_BETA: f32 = 0.2;
266
267pub const PENETRATION_SLOP: f32 = 0.005;
269
270pub const MAX_LINEAR_CORRECTION: f32 = 0.5;
271
272pub const RESTITUTION_THRESHOLD: f32 = 1.0;
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278 use crate::body::BodyDef;
279 use crate::collider::Collider;
280 use crate::geometry::GeometryStore;
281 use glam::Vec2;
282 use loam_math::{Bivector, Bivector2, EuclideanR2};
283
284 const SPACE: EuclideanR2 = EuclideanR2;
285
286 fn contact(point: Vec2, normal: Vec2, penetration: f32) -> Contact<EuclideanR2> {
287 Contact {
288 normal,
289 point,
290 penetration,
291 restitution: 0.0,
292 }
293 }
294
295 fn body(position: Vec2) -> RigidBody<EuclideanR2> {
296 BodyDef::new(
297 position,
298 Vec2::ZERO,
299 Collider::sphere_at_origin(1.0),
300 1.0,
301 1.0,
302 &SPACE,
303 )
304 .unwrap()
305 .into_row(&mut GeometryStore::default(), &SPACE)
306 }
307
308 fn resting_pair() -> (RigidBody<EuclideanR2>, RigidBody<EuclideanR2>) {
309 (body(Vec2::new(0.0, 1.0)), body(Vec2::new(0.0, -1.0)))
310 }
311
312 #[test]
313 fn merged_normal_projects_the_warm_start_without_reversing_it() {
314 let (a, b) = resting_pair();
315 for (normal, expected) in [
316 (Vec2::Y, 4.2),
317 (-Vec2::Y, 0.0),
318 (Vec2::X, 0.0),
319 (Vec2::new(0.8, 0.6), 2.52),
320 ] {
321 let mut manifold = Manifold::new(BodyId::forge(0, 0), BodyId::forge(1, 0), 0.0);
322 manifold.add_or_update(&SPACE, &a, &b, contact(Vec2::ZERO, Vec2::Y, 0.01));
323 manifold.points[0].normal_impulse = 4.2;
324 let point = Vec2::new(0.01, 0.0);
325 manifold.add_or_update(&SPACE, &a, &b, contact(point, normal, 0.05));
326 assert_eq!(manifold.points.len(), 1);
327 let merged = &manifold.points[0];
328 assert_eq!(merged.world_point, point);
329 assert_eq!(merged.normal, normal);
330 assert_eq!(merged.penetration, 0.05);
331 assert!((merged.normal_impulse - expected).abs() < 1e-6);
332 }
333 }
334
335 #[test]
336 fn add_at_max_points_evicts_weakest_slot() {
337 let (a, b) = resting_pair();
338 let mut m: Manifold<EuclideanR2> =
339 Manifold::new(BodyId::forge(0, 0), BodyId::forge(1, 0), 0.0);
340 let bases = [
341 Vec2::new(0.0, 0.0),
342 Vec2::new(1.0, 0.0),
343 Vec2::new(2.0, 0.0),
344 Vec2::new(3.0, 0.0),
345 ];
346 for &p in &bases {
347 m.add_or_update(&SPACE, &a, &b, contact(p, Vec2::Y, 0.0));
348 }
349 assert_eq!(m.points.len(), MAX_POINTS);
350
351 m.points[0].normal_impulse = 5.0;
352 m.points[1].normal_impulse = 3.0;
353 m.points[2].normal_impulse = 0.5;
354 m.points[3].normal_impulse = 4.0;
355 m.points[1].tangent_impulse = -2.0;
356 m.points[2].tangent_impulse = 0.1;
357 m.points[3].tangent_impulse = 1.0;
358
359 let intruder = Vec2::new(10.0, 10.0);
360 m.add_or_update(&SPACE, &a, &b, contact(intruder, Vec2::Y, 0.0));
361
362 assert_eq!(m.points.len(), MAX_POINTS, "size must stay capped");
363 assert!(
364 m.points.iter().any(|cp| cp.world_point == intruder),
365 "new contact must be present",
366 );
367 assert!(
368 m.points.iter().all(|cp| cp.world_point != bases[2]),
369 "the lowest-impulse slot must be evicted",
370 );
371 assert!(
372 m.points.iter().any(|cp| cp.world_point == bases[0]),
373 "high-impulse slots must be retained",
374 );
375 }
376
377 #[test]
378 fn new_slot_below_capacity_leaves_others_intact() {
379 let (a, b) = resting_pair();
380 let mut m: Manifold<EuclideanR2> =
381 Manifold::new(BodyId::forge(0, 0), BodyId::forge(1, 0), 0.0);
382 m.add_or_update(&SPACE, &a, &b, contact(Vec2::ZERO, Vec2::Y, 0.0));
383 m.points[0].normal_impulse = 9.0;
384 m.points[0].tangent_impulse = 0.5;
385 m.points[0].tangent_dir = Vec2::X;
386
387 m.add_or_update(&SPACE, &a, &b, contact(Vec2::new(1.0, 0.0), Vec2::Y, 0.0));
388
389 assert_eq!(m.points.len(), 2);
390 let original = &m.points[0];
391 assert_eq!(original.world_point, Vec2::ZERO, "original geometry intact");
392 assert!((original.normal_impulse - 9.0).abs() < 1e-6);
393 assert!((original.tangent_impulse - 0.5).abs() < 1e-6);
394 assert_eq!(original.tangent_dir, Vec2::X);
395 let added = &m.points[1];
396 assert_eq!(added.normal_impulse, 0.0, "fresh slot starts at zero");
397 assert_eq!(added.tangent_impulse, 0.0);
398 }
399
400 fn touching_manifold(
401 penetration: f32,
402 ) -> (
403 Manifold<EuclideanR2>,
404 RigidBody<EuclideanR2>,
405 RigidBody<EuclideanR2>,
406 ) {
407 let (a, b) = resting_pair();
408 let mut m: Manifold<EuclideanR2> =
409 Manifold::new(BodyId::forge(0, 0), BodyId::forge(1, 0), 0.0);
410 m.add_or_update(&SPACE, &a, &b, contact(Vec2::ZERO, -Vec2::Y, penetration));
411 (m, a, b)
412 }
413
414 #[test]
415 fn refresh_reads_back_the_penetration_the_narrowphase_reported() {
416 let (mut m, a, b) = touching_manifold(0.006);
417 m.refresh(&SPACE, &a, &b);
418 assert_eq!(m.points.len(), 1, "an unmoved contact must survive");
419 assert!(
420 (m.points[0].penetration - 0.006).abs() < 1e-6,
421 "refresh reported {} for a 0.006 penetration, so it is measuring \
422 drift and not separation",
423 m.points[0].penetration,
424 );
425 assert!(
426 (m.points[0].world_point - Vec2::ZERO).length() < 1e-6,
427 "an unmoved contact moved to {}",
428 m.points[0].world_point,
429 );
430 }
431
432 #[test]
433 fn refresh_drops_a_point_the_body_has_lifted_off() {
434 let (mut m, mut a, b) = touching_manifold(0.006);
435 m.points[0].normal_impulse = 3.0;
436 a.position.y += 0.006 + CONTACT_BREAK_DISTANCE + 1e-4;
437 m.refresh(&SPACE, &a, &b);
438 assert!(
439 m.points.is_empty(),
440 "a point past the break distance was solved again with stale \
441 geometry: separation {}",
442 -m.points[0].penetration,
443 );
444 }
445
446 #[test]
447 fn refresh_keeps_a_lifted_point_inside_the_break_distance_with_its_impulse() {
448 let (mut m, mut a, b) = touching_manifold(0.006);
449 m.points[0].normal_impulse = 3.0;
450 m.points[0].tangent_impulse = -0.5;
451 let lift = 0.006 + 0.5 * CONTACT_BREAK_DISTANCE;
452 a.position.y += lift;
453 m.refresh(&SPACE, &a, &b);
454 assert_eq!(m.points.len(), 1, "the point is still inside the band");
455 assert!(
456 (m.points[0].penetration + (lift - 0.006)).abs() < 1e-6,
457 "penetration {} does not track the lift",
458 m.points[0].penetration,
459 );
460 assert!(
461 (m.points[0].normal_impulse - 3.0).abs() < 1e-6
462 && (m.points[0].tangent_impulse + 0.5).abs() < 1e-6,
463 "a surviving point lost its warm-start impulses",
464 );
465 }
466
467 #[test]
468 fn refresh_drops_a_point_the_bodies_slid_apart_across_the_normal() {
469 let (mut m, mut a, b) = touching_manifold(0.006);
470 a.position.x += CONTACT_BREAK_DISTANCE + 1e-4;
471 m.refresh(&SPACE, &a, &b);
472 assert!(
473 m.points.is_empty(),
474 "a point the body slid off kept describing the feature it left",
475 );
476 }
477
478 #[test]
479 fn refresh_follows_a_point_through_a_body_rotation() {
480 let (mut m, mut a, b) = touching_manifold(0.006);
481 a.orientation.rotation = Bivector2(std::f32::consts::FRAC_PI_4).exp();
482 m.refresh(&SPACE, &a, &b);
483 assert!(
484 m.points.is_empty(),
485 "a rotation that swept the contact a full radius away left it in \
486 the manifold",
487 );
488
489 let (mut m, mut a, b) = touching_manifold(0.006);
490 let theta = 0.5 * CONTACT_BREAK_DISTANCE;
491 a.orientation.rotation = Bivector2(theta).exp();
492 m.refresh(&SPACE, &a, &b);
493 assert_eq!(m.points.len(), 1, "a tiny rotation must not break contact");
494 let expected = 0.006 - (1.0 - theta.cos());
495 assert!(
496 (m.points[0].penetration - expected).abs() < 2e-6,
497 "penetration {} does not track the turn's sagitta {expected}",
498 m.points[0].penetration,
499 );
500 }
501
502 #[test]
503 fn refresh_retains_in_slot_order() {
504 let (a, b) = resting_pair();
505 let mut m: Manifold<EuclideanR2> =
506 Manifold::new(BodyId::forge(0, 0), BodyId::forge(1, 0), 0.0);
507 let kept = [Vec2::new(-0.5, 0.0), Vec2::new(0.5, 0.0)];
508 m.add_or_update(&SPACE, &a, &b, contact(kept[0], -Vec2::Y, 0.006));
509 m.add_or_update(&SPACE, &a, &b, contact(Vec2::ZERO, -Vec2::Y, -0.1));
510 m.add_or_update(&SPACE, &a, &b, contact(kept[1], -Vec2::Y, 0.006));
511
512 m.refresh(&SPACE, &a, &b);
513
514 let surviving: Vec<Vec2> = m.points.iter().map(|cp| cp.world_point).collect();
515 assert_eq!(
516 surviving.len(),
517 2,
518 "the separated slot must be the only one dropped"
519 );
520 for (slot, expected) in surviving.iter().zip(kept) {
521 assert!(
522 (*slot - expected).length() < 1e-6,
523 "retain reordered the slots: {surviving:?}",
524 );
525 }
526 }
527}