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