1use super::collision::Contact;
18use crate::map::{DEFAULT_FRICTION, DEFAULT_RESTITUTION};
19use crate::physics::{clamp, normalize_angle};
20
21#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct Surface {
24 pub friction: f32,
25 pub restitution: f32,
26}
27
28pub const MAP_SURFACE: Surface = Surface {
31 friction: DEFAULT_FRICTION,
32 restitution: DEFAULT_RESTITUTION,
33};
34
35#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct Body {
38 pub x: f32,
39 pub y: f32,
40 pub angle: f32,
41 pub vx: f32,
42 pub vy: f32,
43 pub angvel: f32,
44 pub inv_mass: f32,
45 pub inv_inertia: f32,
46 pub linear_damping: f32,
47 pub angular_damping: f32,
48}
49
50impl Default for Body {
51 fn default() -> Self {
52 Self {
53 x: 0.0,
54 y: 0.0,
55 angle: 0.0,
56 vx: 0.0,
57 vy: 0.0,
58 angvel: 0.0,
59 inv_mass: 0.0,
60 inv_inertia: 0.0,
61 linear_damping: 0.0,
62 angular_damping: 0.0,
63 }
64 }
65}
66
67#[derive(Clone, Copy, Debug, PartialEq)]
69pub struct MassProperties {
70 pub inv_mass: f32,
71 pub inv_inertia: f32,
72}
73
74pub fn combine_surfaces(a: &Surface, b: &Surface) -> Surface {
78 Surface {
79 friction: (a.friction + b.friction) / 2.0,
80 restitution: (a.restitution + b.restitution) / 2.0,
81 }
82}
83
84pub fn box_mass_properties(width: f32, height: f32, density: f32) -> MassProperties {
88 let mass = density * width * height;
89 let inertia = (mass * (width * width + height * height)) / 12.0;
90
91 MassProperties {
92 inv_mass: if mass > 0.0 { 1.0 / mass } else { 0.0 },
93 inv_inertia: if inertia > 0.0 { 1.0 / inertia } else { 0.0 },
94 }
95}
96
97pub fn integrate(body: &mut Body, dt: f32) {
102 body.x += body.vx * dt;
103 body.y += body.vy * dt;
104 body.angle = normalize_angle(body.angle + body.angvel * dt);
105
106 let linear = 1.0 / (1.0 + dt * body.linear_damping);
107 let angular = 1.0 / (1.0 + dt * body.angular_damping);
108
109 body.vx *= linear;
110 body.vy *= linear;
111 body.angvel *= angular;
112}
113
114pub fn separate_bodies(a: &mut Body, b: &mut Body, contact: &Contact) {
119 let total = a.inv_mass + b.inv_mass;
120
121 if total == 0.0 {
123 return;
124 }
125
126 a.x -= (contact.nx * contact.depth * a.inv_mass) / total;
127 a.y -= (contact.ny * contact.depth * a.inv_mass) / total;
128 b.x += (contact.nx * contact.depth * b.inv_mass) / total;
129 b.y += (contact.ny * contact.depth * b.inv_mass) / total;
130}
131
132pub fn apply_contact_impulse(a: &mut Body, b: &mut Body, contact: &Contact, surface: &Surface) {
137 let Contact { nx, ny, cx, cy, .. } = *contact;
138
139 if a.inv_mass + b.inv_mass == 0.0 {
140 return;
141 }
142
143 let rax = cx - a.x;
145 let ray = cy - a.y;
146 let rbx = cx - b.x;
147 let rby = cy - b.y;
148
149 let vax = a.vx - a.angvel * ray;
151 let vay = a.vy + a.angvel * rax;
152 let vbx = b.vx - b.angvel * rby;
153 let vby = b.vy + b.angvel * rbx;
154
155 let vrel_x = vbx - vax;
156 let vrel_y = vby - vay;
157 let vn = vrel_x * nx + vrel_y * ny;
158
159 if vn >= 0.0 {
161 return;
162 }
163
164 let rn_a = rax * ny - ray * nx;
165 let rn_b = rbx * ny - rby * nx;
166 let eff_n = a.inv_mass + b.inv_mass + rn_a * rn_a * a.inv_inertia + rn_b * rn_b * b.inv_inertia;
167
168 if eff_n == 0.0 {
169 return;
170 }
171
172 let jn = (-(1.0 + surface.restitution) * vn) / eff_n;
173
174 a.vx -= nx * jn * a.inv_mass;
175 a.vy -= ny * jn * a.inv_mass;
176 a.angvel -= rn_a * jn * a.inv_inertia;
177 b.vx += nx * jn * b.inv_mass;
178 b.vy += ny * jn * b.inv_mass;
179 b.angvel += rn_b * jn * b.inv_inertia;
180
181 let tx = -ny;
183 let ty = nx;
184 let vt = vrel_x * tx + vrel_y * ty;
185
186 let rt_a = rax * ty - ray * tx;
187 let rt_b = rbx * ty - rby * tx;
188 let eff_t = a.inv_mass + b.inv_mass + rt_a * rt_a * a.inv_inertia + rt_b * rt_b * b.inv_inertia;
189
190 if eff_t == 0.0 {
191 return;
192 }
193
194 let max_friction = surface.friction * jn;
195 let jt = clamp(-vt / eff_t, -max_friction, max_friction);
196
197 a.vx -= tx * jt * a.inv_mass;
198 a.vy -= ty * jt * a.inv_mass;
199 a.angvel -= rt_a * jt * a.inv_inertia;
200 b.vx += tx * jt * b.inv_mass;
201 b.vy += ty * jt * b.inv_mass;
202 b.angvel += rt_b * jt * b.inv_inertia;
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn resolve_contact(a: &mut Body, b: &mut Body, contact: &Contact, surface: &Surface) {
212 separate_bodies(a, b, contact);
213 apply_contact_impulse(a, b, contact, surface);
214 }
215
216 fn body() -> Body {
217 Body {
218 inv_mass: 1.0,
219 inv_inertia: 1.0,
220 ..Body::default()
221 }
222 }
223
224 fn static_body() -> Body {
225 Body::default()
226 }
227
228 fn smooth(restitution: f32) -> Surface {
229 Surface {
230 friction: 0.0,
231 restitution,
232 }
233 }
234
235 fn head_on() -> Contact {
237 Contact {
238 nx: 1.0,
239 ny: 0.0,
240 depth: 2.0,
241 cx: 5.0,
242 cy: 0.0,
243 }
244 }
245
246 #[test]
247 fn integrate_moves_by_undamped_velocity() {
248 let dt = 1.0 / 120.0;
249 let mut b = Body {
250 vx: 100.0,
251 vy: -40.0,
252 angvel: 2.0,
253 linear_damping: 3.0,
254 angular_damping: 100.0,
255 ..body()
256 };
257
258 integrate(&mut b, dt);
259
260 assert!((b.x - 100.0 * dt).abs() < 1e-6);
262 assert!((b.y + 40.0 * dt).abs() < 1e-6);
263 assert!((b.angle - 2.0 * dt).abs() < 1e-6);
264
265 assert!((b.vx - 100.0 / (1.0 + dt * 3.0)).abs() < 1e-4);
267 assert!((b.vy + 40.0 / (1.0 + dt * 3.0)).abs() < 1e-4);
268 assert!((b.angvel - 2.0 / (1.0 + dt * 100.0)).abs() < 1e-6);
269 }
270
271 #[test]
272 fn integrate_normalizes_the_angle() {
273 let mut b = Body {
274 angle: 3.1,
275 angvel: 10.0,
276 ..body()
277 };
278
279 integrate(&mut b, 0.1);
280
281 assert!(b.angle >= -core::f32::consts::PI);
282 assert!(b.angle <= core::f32::consts::PI);
283 }
284
285 #[test]
286 fn zero_damping_keeps_velocity() {
287 let mut b = Body {
288 vx: 7.0,
289 angvel: 3.0,
290 ..body()
291 };
292
293 integrate(&mut b, 0.5);
294
295 assert_eq!(b.vx, 7.0);
296 assert_eq!(b.angvel, 3.0);
297 }
298
299 #[test]
300 fn equal_bodies_conserve_momentum() {
301 let mut a = Body { vx: 10.0, ..body() };
302 let mut b = Body { x: 8.0, ..body() };
303 let before = a.vx + b.vx;
304
305 resolve_contact(&mut a, &mut b, &head_on(), &smooth(0.0));
306
307 assert!((a.vx + b.vx - before).abs() < 1e-5);
308 }
309
310 #[test]
311 fn restitution_zero_kills_the_closing_velocity() {
312 let mut a = Body { vx: 10.0, ..body() };
313 let mut b = Body { x: 8.0, ..body() };
314
315 resolve_contact(&mut a, &mut b, &head_on(), &smooth(0.0));
316
317 assert!((b.vx - a.vx).abs() < 1e-5);
318 }
319
320 #[test]
321 fn restitution_one_swaps_velocities() {
322 let mut a = Body { vx: 10.0, ..body() };
323 let mut b = Body { x: 8.0, ..body() };
324
325 resolve_contact(&mut a, &mut b, &head_on(), &smooth(1.0));
326
327 assert!(a.vx.abs() < 1e-5);
328 assert!((b.vx - 10.0).abs() < 1e-5);
329 }
330
331 #[test]
332 fn static_body_neither_moves_nor_accelerates() {
333 let mut a = Body { vx: 10.0, ..body() };
334 let mut wall = Body {
335 x: 8.0,
336 ..static_body()
337 };
338
339 resolve_contact(&mut a, &mut wall, &head_on(), &smooth(0.0));
340
341 assert_eq!(wall.x, 8.0);
342 assert_eq!(wall.vx, 0.0);
343 assert_eq!(wall.angvel, 0.0);
344
345 assert!((a.x + 2.0).abs() < 1e-5);
347 assert!(a.vx.abs() < 1e-5);
348 }
349
350 #[test]
351 fn two_static_bodies_are_a_no_op() {
352 let mut a = static_body();
353 let mut b = Body {
354 x: 8.0,
355 ..static_body()
356 };
357
358 resolve_contact(&mut a, &mut b, &head_on(), &smooth(0.0));
359
360 assert_eq!(a, static_body());
361 }
362
363 #[test]
364 fn separating_bodies_get_position_correction_but_no_impulse() {
365 let mut a = Body { vx: -5.0, ..body() };
366 let mut b = Body {
367 x: 8.0,
368 vx: 5.0,
369 ..body()
370 };
371
372 resolve_contact(&mut a, &mut b, &head_on(), &smooth(0.0));
373
374 assert_eq!(a.vx, -5.0);
375 assert_eq!(b.vx, 5.0);
376 assert!((a.x + 1.0).abs() < 1e-5);
378 assert!((b.x - 9.0).abs() < 1e-5);
379 }
380
381 #[test]
382 fn position_correction_scales_with_inverse_mass() {
383 let mut light = Body {
384 inv_mass: 4.0,
385 ..body()
386 };
387 let mut heavy = Body { x: 8.0, ..body() };
388
389 resolve_contact(&mut light, &mut heavy, &head_on(), &smooth(0.0));
390
391 assert!((light.x + 1.6).abs() < 1e-5);
393 assert!((heavy.x - 8.4).abs() < 1e-5);
394 }
395
396 #[test]
397 fn friction_is_clamped_by_the_coulomb_cone() {
398 let contact = Contact {
400 depth: 0.1,
401 ..head_on()
402 };
403 let mut a = Body {
404 vx: 1.0,
405 vy: 100.0,
406 inv_inertia: 0.0,
407 ..body()
408 };
409 let mut b = Body {
410 x: 8.0,
411 inv_inertia: 0.0,
412 ..body()
413 };
414
415 resolve_contact(
416 &mut a,
417 &mut b,
418 &contact,
419 &Surface {
420 friction: 0.2,
421 restitution: 0.0,
422 },
423 );
424
425 let jn = 0.5;
427
428 assert!(b.vy.abs() <= 0.2 * jn + 1e-6);
429 assert!(b.vy > 0.0);
430 }
431
432 #[test]
433 fn zero_friction_keeps_the_tangential_velocity() {
434 let contact = Contact {
435 depth: 0.1,
436 ..head_on()
437 };
438 let mut a = Body {
439 vx: 1.0,
440 vy: 100.0,
441 inv_inertia: 0.0,
442 ..body()
443 };
444 let mut b = Body {
445 x: 8.0,
446 inv_inertia: 0.0,
447 ..body()
448 };
449
450 resolve_contact(&mut a, &mut b, &contact, &smooth(0.0));
451
452 assert_eq!(b.vy, 0.0);
453 assert_eq!(a.vy, 100.0);
454 }
455
456 #[test]
457 fn off_center_hit_spins_both_bodies() {
458 let contact = Contact {
460 depth: 1.0,
461 cy: 4.0,
462 ..head_on()
463 };
464 let mut a = Body { vx: 10.0, ..body() };
465 let mut b = Body { x: 8.0, ..body() };
466
467 resolve_contact(&mut a, &mut b, &contact, &smooth(0.0));
468
469 assert_ne!(a.angvel, 0.0);
470 assert_ne!(b.angvel, 0.0);
471 }
472
473 #[test]
474 fn head_on_hit_does_not_spin() {
475 let contact = Contact {
476 depth: 1.0,
477 ..head_on()
478 };
479 let mut a = Body { vx: 10.0, ..body() };
480 let mut b = Body { x: 8.0, ..body() };
481
482 resolve_contact(&mut a, &mut b, &contact, &smooth(0.0));
483
484 assert!(a.angvel.abs() < 1e-6);
485 assert!(b.angvel.abs() < 1e-6);
486 }
487
488 #[test]
489 fn separate_bodies_touches_positions_only() {
490 let mut a = Body { vx: 10.0, ..body() };
491 let mut b = Body { x: 8.0, ..body() };
492
493 separate_bodies(&mut a, &mut b, &head_on());
494
495 assert!((a.x + 1.0).abs() < 1e-6);
496 assert!((b.x - 9.0).abs() < 1e-6);
497 assert_eq!(a.vx, 10.0);
498 assert_eq!(b.vx, 0.0);
499 }
500
501 #[test]
502 fn apply_contact_impulse_touches_velocities_only() {
503 let mut a = Body { vx: 10.0, ..body() };
504 let mut b = Body { x: 8.0, ..body() };
505
506 apply_contact_impulse(&mut a, &mut b, &head_on(), &smooth(0.0));
507
508 assert_eq!(a.x, 0.0);
509 assert_eq!(b.x, 8.0);
510 assert!((a.vx - 5.0).abs() < 1e-5);
511 assert!((b.vx - 5.0).abs() < 1e-5);
512 }
513
514 #[test]
517 fn solver_iterations_do_not_multiply_the_correction() {
518 let mut a = Body { vx: 10.0, ..body() };
519 let mut b = Body { x: 8.0, ..body() };
520
521 separate_bodies(&mut a, &mut b, &head_on());
522
523 let (ax, bx) = (a.x, b.x);
524
525 for _ in 0..4 {
526 apply_contact_impulse(&mut a, &mut b, &head_on(), &smooth(0.0));
527 }
528
529 assert_eq!(a.x, ax);
530 assert_eq!(b.x, bx);
531 }
532
533 #[test]
534 fn rectangle_mass_and_inertia() {
535 let props = box_mass_properties(8.0, 6.0, 200.0);
536 let mass = 200.0 * 8.0 * 6.0;
537
538 assert!((1.0 / props.inv_mass - mass).abs() < 1e-1);
539 assert!((1.0 / props.inv_inertia - (mass * (64.0 + 36.0)) / 12.0).abs() < 1.0);
540 }
541
542 #[test]
543 fn zero_density_or_size_is_static() {
544 let zero = MassProperties {
545 inv_mass: 0.0,
546 inv_inertia: 0.0,
547 };
548
549 assert_eq!(box_mass_properties(8.0, 6.0, 0.0), zero);
550 assert_eq!(box_mass_properties(0.0, 0.0, 200.0), zero);
551 }
552
553 #[test]
554 fn surfaces_combine_by_the_average_rule() {
555 let tank = Surface {
556 friction: 0.5,
557 restitution: 0.1,
558 };
559
560 assert_eq!(
561 combine_surfaces(&tank, &MAP_SURFACE),
562 Surface {
563 friction: 0.35,
564 restitution: 0.05,
565 }
566 );
567 }
568}