1use crate::dynamics::{RigidBody, RigidBodyHandle, RigidBodySet};
4use crate::geometry::{ColliderHandle, ColliderSet, Ray};
5use crate::math::{Real, Vector, VectorExt, rotation_from_angle};
6use crate::pipeline::QueryPipeline;
7use crate::prelude::QueryPipelineMut;
8use crate::utils::{CrossProduct, DotProduct};
9
10#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
12#[derive(Clone, Debug)]
13pub struct DynamicRayCastVehicleController {
14 wheels: Vec<Wheel>,
15 forward_ws: Vec<Vector>,
16 axle: Vec<Vector>,
17 pub current_vehicle_speed: Real,
19
20 pub chassis: RigidBodyHandle,
22 pub index_up_axis: usize,
24 pub index_forward_axis: usize,
26}
27
28#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
29#[derive(Copy, Clone, Debug, PartialEq)]
30pub struct WheelTuning {
32 pub suspension_stiffness: Real,
36 pub suspension_compression: Real,
38 pub suspension_damping: Real,
42 pub max_suspension_travel: Real,
44 pub side_friction_stiffness: Real,
46 pub friction_slip: Real,
51 pub max_suspension_force: Real,
53}
54
55impl Default for WheelTuning {
56 fn default() -> Self {
57 Self {
58 suspension_stiffness: 5.88,
59 suspension_compression: 0.83,
60 suspension_damping: 0.88,
61 max_suspension_travel: 5.0,
62 side_friction_stiffness: 1.0,
63 friction_slip: 10.5,
64 max_suspension_force: 6000.0,
65 }
66 }
67}
68
69struct WheelDesc {
71 pub chassis_connection_cs: Vector,
73 pub direction_cs: Vector,
77 pub axle_cs: Vector,
79 pub suspension_rest_length: Real,
81 pub max_suspension_travel: Real,
83 pub radius: Real,
85
86 pub suspension_stiffness: Real,
90 pub damping_compression: Real,
92 pub damping_relaxation: Real,
96 pub friction_slip: Real,
101 pub max_suspension_force: Real,
103 pub side_friction_stiffness: Real,
105}
106
107#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
108#[derive(Copy, Clone, Debug, PartialEq)]
109pub struct Wheel {
111 raycast_info: RayCastInfo,
112
113 center: Vector,
114 wheel_direction_ws: Vector,
115 wheel_axle_ws: Vector,
116
117 pub chassis_connection_point_cs: Vector,
119 pub direction_cs: Vector,
123 pub axle_cs: Vector,
125 pub suspension_rest_length: Real,
127 pub max_suspension_travel: Real,
129 pub radius: Real,
131 pub suspension_stiffness: Real,
135 pub damping_compression: Real,
137 pub damping_relaxation: Real,
141 pub friction_slip: Real,
146 pub side_friction_stiffness: Real,
148 pub rotation: Real,
150 delta_rotation: Real,
151 roll_influence: Real, pub max_suspension_force: Real,
154
155 pub forward_impulse: Real,
157 pub side_impulse: Real,
159
160 pub steering: Real,
162 pub engine_force: Real,
164 pub brake: Real,
166
167 clipped_inv_contact_dot_suspension: Real,
168 suspension_relative_velocity: Real,
169 pub wheel_suspension_force: Real,
171 skid_info: Real,
172}
173
174impl Wheel {
175 fn new(info: WheelDesc) -> Self {
176 Self {
177 raycast_info: RayCastInfo::default(),
178 suspension_rest_length: info.suspension_rest_length,
179 max_suspension_travel: info.max_suspension_travel,
180 radius: info.radius,
181 suspension_stiffness: info.suspension_stiffness,
182 damping_compression: info.damping_compression,
183 damping_relaxation: info.damping_relaxation,
184 chassis_connection_point_cs: info.chassis_connection_cs,
185 direction_cs: info.direction_cs,
186 axle_cs: info.axle_cs,
187 wheel_direction_ws: info.direction_cs,
188 wheel_axle_ws: info.axle_cs,
189 center: Vector::ZERO,
190 friction_slip: info.friction_slip,
191 steering: 0.0,
192 engine_force: 0.0,
193 rotation: 0.0,
194 delta_rotation: 0.0,
195 brake: 0.0,
196 roll_influence: 0.1,
197 clipped_inv_contact_dot_suspension: 0.0,
198 suspension_relative_velocity: 0.0,
199 wheel_suspension_force: 0.0,
200 max_suspension_force: info.max_suspension_force,
201 skid_info: 0.0,
202 side_impulse: 0.0,
203 forward_impulse: 0.0,
204 side_friction_stiffness: info.side_friction_stiffness,
205 }
206 }
207
208 pub fn raycast_info(&self) -> &RayCastInfo {
211 &self.raycast_info
212 }
213
214 pub fn center(&self) -> Vector {
216 self.center
217 }
218
219 pub fn suspension(&self) -> Vector {
221 self.wheel_direction_ws
222 }
223
224 pub fn axle(&self) -> Vector {
226 self.wheel_axle_ws
227 }
228}
229
230#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
233#[derive(Copy, Clone, Debug, PartialEq, Default)]
234pub struct RayCastInfo {
235 pub contact_normal_ws: Vector,
237 pub contact_point_ws: Vector,
239 pub suspension_length: Real,
241 pub hard_point_ws: Vector,
243 pub is_in_contact: bool,
245 pub ground_object: Option<ColliderHandle>,
247}
248
249impl DynamicRayCastVehicleController {
250 pub fn new(chassis: RigidBodyHandle) -> Self {
254 Self {
255 wheels: vec![],
256 forward_ws: vec![],
257 axle: vec![],
258 current_vehicle_speed: 0.0,
259 chassis,
260 index_up_axis: 1,
261 index_forward_axis: 0,
262 }
263 }
264
265 pub fn add_wheel(
270 &mut self,
271 chassis_connection_cs: Vector,
272 direction_cs: Vector,
273 axle_cs: Vector,
274 suspension_rest_length: Real,
275 radius: Real,
276 tuning: &WheelTuning,
277 ) -> &mut Wheel {
278 let ci = WheelDesc {
279 chassis_connection_cs,
280 direction_cs,
281 axle_cs,
282 suspension_rest_length,
283 radius,
284 suspension_stiffness: tuning.suspension_stiffness,
285 damping_compression: tuning.suspension_compression,
286 damping_relaxation: tuning.suspension_damping,
287 friction_slip: tuning.friction_slip,
288 max_suspension_travel: tuning.max_suspension_travel,
289 max_suspension_force: tuning.max_suspension_force,
290 side_friction_stiffness: tuning.side_friction_stiffness,
291 };
292
293 let wheel_id = self.wheels.len();
294 self.wheels.push(Wheel::new(ci));
295
296 &mut self.wheels[wheel_id]
297 }
298
299 #[cfg(feature = "dim2")]
300 fn update_wheel_transform(&mut self, chassis: &RigidBody, wheel_index: usize) {
301 self.update_wheel_transforms_ws(chassis, wheel_index);
302 let wheel = &mut self.wheels[wheel_index];
303 wheel.center = wheel.raycast_info.hard_point_ws
304 + wheel.wheel_direction_ws * wheel.raycast_info.suspension_length;
305 }
306
307 #[cfg(feature = "dim3")]
308 fn update_wheel_transform(&mut self, chassis: &RigidBody, wheel_index: usize) {
309 self.update_wheel_transforms_ws(chassis, wheel_index);
310 let wheel = &mut self.wheels[wheel_index];
311
312 let steering_orn = rotation_from_angle(-wheel.wheel_direction_ws * wheel.steering);
313 wheel.wheel_axle_ws = steering_orn * (chassis.position().rotation * wheel.axle_cs);
314 wheel.center = wheel.raycast_info.hard_point_ws
315 + wheel.wheel_direction_ws * wheel.raycast_info.suspension_length;
316 }
317
318 fn update_wheel_transforms_ws(&mut self, chassis: &RigidBody, wheel_id: usize) {
319 let wheel = &mut self.wheels[wheel_id];
320 wheel.raycast_info.is_in_contact = false;
321
322 let chassis_transform = chassis.position();
323
324 wheel.raycast_info.hard_point_ws = chassis_transform * wheel.chassis_connection_point_cs;
325 wheel.wheel_direction_ws = chassis_transform.rotation * wheel.direction_cs;
326 wheel.wheel_axle_ws = chassis_transform.rotation * wheel.axle_cs;
327 }
328
329 #[profiling::function]
330 fn ray_cast(&mut self, queries: &QueryPipeline, chassis: &RigidBody, wheel_id: usize) {
331 let wheel = &mut self.wheels[wheel_id];
332 let raylen = wheel.suspension_rest_length + wheel.radius;
333 let rayvector = wheel.wheel_direction_ws * raylen;
334 let source = wheel.raycast_info.hard_point_ws;
335 wheel.raycast_info.contact_point_ws = source + rayvector;
336 let ray = Ray::new(source, rayvector);
337 let hit = queries.cast_ray_and_get_normal(&ray, 1.0, true);
338
339 wheel.raycast_info.ground_object = None;
340
341 if let Some((collider_hit, mut hit)) = hit {
342 if hit.time_of_impact == 0.0 {
343 let collider = &queries.colliders[collider_hit];
344 let up_ray = Ray::new(source + rayvector, -rayvector);
345 if let Some(hit2) =
346 collider
347 .shape
348 .cast_ray_and_get_normal(collider.position(), &up_ray, 1.0, false)
349 {
350 hit.normal = -hit2.normal;
351 }
352
353 if hit.normal == Vector::ZERO {
354 hit.normal = -wheel.wheel_direction_ws;
356 }
357 }
358
359 wheel.raycast_info.contact_normal_ws = hit.normal;
360 wheel.raycast_info.is_in_contact = true;
361 wheel.raycast_info.ground_object = Some(collider_hit);
362
363 let hit_distance = hit.time_of_impact * raylen;
364 wheel.raycast_info.suspension_length = hit_distance - wheel.radius;
365
366 let min_suspension_length = wheel.suspension_rest_length - wheel.max_suspension_travel;
368 let max_suspension_length = wheel.suspension_rest_length + wheel.max_suspension_travel;
369 wheel.raycast_info.suspension_length = wheel
370 .raycast_info
371 .suspension_length
372 .clamp(min_suspension_length, max_suspension_length);
373 wheel.raycast_info.contact_point_ws = ray.point_at(hit.time_of_impact);
374
375 let denominator = wheel
376 .raycast_info
377 .contact_normal_ws
378 .dot(wheel.wheel_direction_ws);
379 let chassis_velocity_at_contact_point =
380 chassis.velocity_at_point(wheel.raycast_info.contact_point_ws);
381 let proj_vel = wheel
382 .raycast_info
383 .contact_normal_ws
384 .dot(chassis_velocity_at_contact_point);
385
386 if denominator >= -0.1 {
387 wheel.suspension_relative_velocity = 0.0;
388 wheel.clipped_inv_contact_dot_suspension = 1.0 / 0.1;
389 } else {
390 let inv = -1.0 / denominator;
391 wheel.suspension_relative_velocity = proj_vel * inv;
392 wheel.clipped_inv_contact_dot_suspension = inv;
393 }
394 } else {
395 wheel.raycast_info.suspension_length = wheel.suspension_rest_length;
397 wheel.suspension_relative_velocity = 0.0;
398 wheel.raycast_info.contact_normal_ws = -wheel.wheel_direction_ws;
399 wheel.clipped_inv_contact_dot_suspension = 1.0;
400 }
401 }
402
403 #[profiling::function]
405 pub fn update_vehicle(&mut self, dt: Real, queries: QueryPipelineMut) {
406 let num_wheels = self.wheels.len();
407 let chassis = &queries.bodies[self.chassis];
408
409 for i in 0..num_wheels {
410 self.update_wheel_transform(chassis, i);
411 }
412
413 self.current_vehicle_speed = chassis.linvel().length();
414
415 let forward_w = chassis.position().rotation * Vector::ith(self.index_forward_axis, 1.0);
416
417 if forward_w.dot(chassis.linvel()) < 0.0 {
418 self.current_vehicle_speed *= -1.0;
419 }
420
421 for wheel_id in 0..self.wheels.len() {
426 self.ray_cast(&queries.as_ref(), chassis, wheel_id);
427 }
428
429 let chassis_mass = chassis.mass();
430 self.update_suspension(chassis_mass);
431
432 let chassis = queries
433 .bodies
434 .get_mut_internal_with_modification_tracking(self.chassis)
435 .unwrap();
436
437 for wheel in &mut self.wheels {
438 if wheel.engine_force > 0.0 {
439 chassis.wake_up(true);
440 }
441
442 let mut suspension_force = wheel.wheel_suspension_force;
444
445 if suspension_force > wheel.max_suspension_force {
446 suspension_force = wheel.max_suspension_force;
447 }
448
449 let impulse = wheel.raycast_info.contact_normal_ws * suspension_force * dt;
450 chassis.apply_impulse_at_point(impulse, wheel.raycast_info.contact_point_ws, false);
451 }
452
453 self.update_friction(queries.bodies, queries.colliders, dt);
454
455 let chassis = queries
456 .bodies
457 .get_mut_internal_with_modification_tracking(self.chassis)
458 .unwrap();
459
460 for wheel in &mut self.wheels {
461 let vel = chassis.velocity_at_point(wheel.raycast_info.hard_point_ws);
462
463 if wheel.raycast_info.is_in_contact {
464 let mut fwd =
465 chassis.position().rotation * Vector::ith(self.index_forward_axis, 1.0);
466 let proj = fwd.dot(wheel.raycast_info.contact_normal_ws);
467 fwd -= wheel.raycast_info.contact_normal_ws * proj;
468
469 let proj2 = fwd.dot(vel);
470
471 wheel.delta_rotation = (proj2 * dt) / (wheel.radius);
472 wheel.rotation += wheel.delta_rotation;
473 } else {
474 wheel.rotation += wheel.delta_rotation;
475 }
476
477 wheel.delta_rotation *= 0.99; }
479 }
480
481 pub fn wheels(&self) -> &[Wheel] {
483 &self.wheels
484 }
485
486 pub fn wheels_mut(&mut self) -> &mut [Wheel] {
488 &mut self.wheels
489 }
490
491 fn update_suspension(&mut self, chassis_mass: Real) {
492 for w_it in 0..self.wheels.len() {
493 let wheels = &mut self.wheels[w_it];
494
495 if wheels.raycast_info.is_in_contact {
496 let mut force;
497 {
499 let rest_length = wheels.suspension_rest_length;
500 let current_length = wheels.raycast_info.suspension_length;
501 let length_diff = rest_length - current_length;
502
503 force = wheels.suspension_stiffness
504 * length_diff
505 * wheels.clipped_inv_contact_dot_suspension;
506 }
507
508 {
510 let projected_rel_vel = wheels.suspension_relative_velocity;
511 {
512 let susp_damping = if projected_rel_vel < 0.0 {
513 wheels.damping_compression
514 } else {
515 wheels.damping_relaxation
516 };
517 force -= susp_damping * projected_rel_vel;
518 }
519 }
520
521 wheels.wheel_suspension_force = (force * chassis_mass).max(0.0);
523 } else {
524 wheels.wheel_suspension_force = 0.0;
525 }
526 }
527 }
528
529 #[profiling::function]
530 fn update_friction(&mut self, bodies: &mut RigidBodySet, colliders: &ColliderSet, dt: Real) {
531 let num_wheels = self.wheels.len();
532
533 if num_wheels == 0 {
534 return;
535 }
536
537 self.forward_ws.resize(num_wheels, Default::default());
538 self.axle.resize(num_wheels, Default::default());
539
540 let mut num_wheels_on_ground = 0;
541
542 for wheel in &mut self.wheels {
544 let ground_object = wheel.raycast_info.ground_object;
545
546 if ground_object.is_some() {
547 num_wheels_on_ground += 1;
548 }
549
550 wheel.side_impulse = 0.0;
551 wheel.forward_impulse = 0.0;
552 }
553
554 {
555 for i in 0..num_wheels {
556 let wheel = &mut self.wheels[i];
557 let ground_object = wheel.raycast_info.ground_object;
558
559 if ground_object.is_some() {
560 self.axle[i] = wheel.wheel_axle_ws;
561
562 let surf_normal_ws = wheel.raycast_info.contact_normal_ws;
563 let proj = self.axle[i].dot(surf_normal_ws);
564 self.axle[i] -= surf_normal_ws * proj;
565 self.axle[i] = self.axle[i].normalize_or_zero();
566 self.forward_ws[i] = surf_normal_ws.cross(self.axle[i]).normalize_or_zero();
567
568 if let Some(ground_body) = ground_object
569 .and_then(|h| colliders[h].parent())
570 .map(|h| &bodies[h])
571 .filter(|b| b.is_dynamic())
572 {
573 wheel.side_impulse = resolve_single_bilateral(
574 &bodies[self.chassis],
575 wheel.raycast_info.contact_point_ws,
576 ground_body,
577 wheel.raycast_info.contact_point_ws,
578 self.axle[i],
579 );
580 } else {
581 wheel.side_impulse = resolve_single_unilateral(
582 &bodies[self.chassis],
583 wheel.raycast_info.contact_point_ws,
584 self.axle[i],
585 );
586 }
587
588 wheel.side_impulse *= wheel.side_friction_stiffness;
589 }
590 }
591 }
592
593 let side_factor = 1.0;
594 let fwd_factor = 0.5;
595
596 let mut sliding = false;
597 {
598 for wheel_id in 0..num_wheels {
599 let wheel = &mut self.wheels[wheel_id];
600 let ground_object = wheel.raycast_info.ground_object;
601
602 let mut rolling_friction = 0.0;
603
604 if ground_object.is_some() {
605 if wheel.engine_force != 0.0 {
606 rolling_friction = wheel.engine_force * dt;
607 } else {
608 let default_rolling_friction_impulse = 0.0;
609 let max_impulse = if wheel.brake != 0.0 {
610 wheel.brake
611 } else {
612 default_rolling_friction_impulse
613 };
614 let contact_pt = WheelContactPoint::new(
615 &bodies[self.chassis],
616 ground_object
617 .and_then(|h| colliders[h].parent())
618 .map(|h| &bodies[h]),
619 wheel.raycast_info.contact_point_ws,
620 self.forward_ws[wheel_id],
621 max_impulse,
622 );
623 assert!(num_wheels_on_ground > 0);
624 rolling_friction = contact_pt.calc_rolling_friction(num_wheels_on_ground);
625 }
626 }
627
628 wheel.forward_impulse = 0.0;
631 wheel.skid_info = 1.0;
632
633 if ground_object.is_some() {
634 let max_imp = wheel.wheel_suspension_force * dt * wheel.friction_slip;
635 let max_imp_side = max_imp;
636 let max_imp_squared = max_imp * max_imp_side;
637 assert!(max_imp_squared >= 0.0);
638
639 wheel.forward_impulse = rolling_friction;
640
641 let x = wheel.forward_impulse * fwd_factor;
642 let y = wheel.side_impulse * side_factor;
643
644 let impulse_squared = x * x + y * y;
645
646 if impulse_squared > max_imp_squared {
647 sliding = true;
648
649 let factor = max_imp * crate::utils::inv(impulse_squared.sqrt());
650 wheel.skid_info *= factor;
651 }
652 }
653 }
654 }
655
656 if sliding {
657 for wheel in &mut self.wheels {
658 if wheel.side_impulse != 0.0 && wheel.skid_info < 1.0 {
659 wheel.forward_impulse *= wheel.skid_info;
660 wheel.side_impulse *= wheel.skid_info;
661 }
662 }
663 }
664
665 {
667 let chassis = bodies
668 .get_mut_internal_with_modification_tracking(self.chassis)
669 .unwrap();
670
671 for wheel_id in 0..num_wheels {
672 let wheel = &self.wheels[wheel_id];
673 let mut impulse_point = wheel.raycast_info.contact_point_ws;
674
675 if wheel.forward_impulse != 0.0 {
676 chassis.apply_impulse_at_point(
677 self.forward_ws[wheel_id] * wheel.forward_impulse,
678 impulse_point,
679 false,
680 );
681 }
682 if wheel.side_impulse != 0.0 {
683 let side_impulse = self.axle[wheel_id] * wheel.side_impulse;
684
685 let v_chassis_world_up =
686 chassis.position().rotation * Vector::ith(self.index_up_axis, 1.0);
687 impulse_point -= v_chassis_world_up
688 * (v_chassis_world_up.dot(impulse_point - chassis.center_of_mass())
689 * (1.0 - wheel.roll_influence));
690
691 chassis.apply_impulse_at_point(side_impulse, impulse_point, false);
692
693 }
701 }
702 }
703 }
704}
705
706struct WheelContactPoint<'a> {
707 body0: &'a RigidBody,
708 body1: Option<&'a RigidBody>,
709 friction_position_world: Vector,
710 friction_direction_world: Vector,
711 jac_diag_ab_inv: Real,
712 max_impulse: Real,
713}
714
715impl<'a> WheelContactPoint<'a> {
716 pub fn new(
717 body0: &'a RigidBody,
718 body1: Option<&'a RigidBody>,
719 friction_position_world: Vector,
720 friction_direction_world: Vector,
721 max_impulse: Real,
722 ) -> Self {
723 fn impulse_denominator(body: &RigidBody, pos: Vector, n: Vector) -> Real {
724 let dpt = pos - body.center_of_mass();
725 let gcross = dpt.gcross(n);
726 let v = (body.mprops.effective_world_inv_inertia * gcross).gcross(dpt);
727 body.mprops.local_mprops.inv_mass + n.dot(v)
729 }
730 let denom0 = impulse_denominator(body0, friction_position_world, friction_direction_world);
731 let denom1 = body1
732 .map(|body1| {
733 impulse_denominator(body1, friction_position_world, friction_direction_world)
734 })
735 .unwrap_or(0.0);
736 let relaxation = 1.0;
737 let jac_diag_ab_inv = relaxation / (denom0 + denom1);
738
739 Self {
740 body0,
741 body1,
742 friction_position_world,
743 friction_direction_world,
744 jac_diag_ab_inv,
745 max_impulse,
746 }
747 }
748
749 pub fn calc_rolling_friction(&self, num_wheels_on_ground: usize) -> Real {
750 let contact_pos_world = self.friction_position_world;
751 let max_impulse = self.max_impulse;
752
753 let vel1 = self.body0.velocity_at_point(contact_pos_world);
754 let vel2 = self
755 .body1
756 .map(|b| b.velocity_at_point(contact_pos_world))
757 .unwrap_or_default();
758 let vel = vel1 - vel2;
759 let vrel = self.friction_direction_world.dot(vel);
760
761 (-vrel * self.jac_diag_ab_inv / (num_wheels_on_ground as Real))
763 .clamp(-max_impulse, max_impulse)
764 }
765}
766
767fn resolve_single_bilateral(
768 body1: &RigidBody,
769 pt1: Vector,
770 body2: &RigidBody,
771 pt2: Vector,
772 normal: Vector,
773) -> Real {
774 let vel1 = body1.velocity_at_point(pt1);
775 let vel2 = body2.velocity_at_point(pt2);
776 let dvel = vel1 - vel2;
777
778 let dpt1 = pt1 - body1.center_of_mass();
779 let dpt2 = pt2 - body2.center_of_mass();
780 let aj = dpt1.gcross(normal);
781 let bj = dpt2.gcross(-normal);
782 let iaj = body1.mprops.effective_world_inv_inertia * aj;
783 let ibj = body2.mprops.effective_world_inv_inertia * bj;
784
785 let im1 = body1.mprops.local_mprops.inv_mass;
787 let im2 = body2.mprops.local_mprops.inv_mass;
788
789 let jac_diag_ab = im1 + im2 + iaj.gdot(iaj) + ibj.gdot(ibj);
790 let jac_diag_ab_inv = crate::utils::inv(jac_diag_ab);
791 let rel_vel = normal.dot(dvel);
792
793 let contact_damping = 0.2;
795 -contact_damping * rel_vel * jac_diag_ab_inv
796}
797
798fn resolve_single_unilateral(body1: &RigidBody, pt1: Vector, normal: Vector) -> Real {
799 let vel1 = body1.velocity_at_point(pt1);
800 let dvel = vel1;
801 let dpt1 = pt1 - body1.center_of_mass();
802 let aj = dpt1.gcross(normal);
803 let iaj = body1.mprops.effective_world_inv_inertia * aj;
804
805 let im1 = body1.mprops.local_mprops.inv_mass;
807 let jac_diag_ab = im1 + iaj.gdot(iaj);
808 let jac_diag_ab_inv = crate::utils::inv(jac_diag_ab);
809 let rel_vel = normal.dot(dvel);
810
811 let contact_damping = 0.2;
813 -contact_damping * rel_vel * jac_diag_ab_inv
814}