Skip to main content

oxiphysics_gpu/kernels/rigid/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6/// Kernel that integrates positions given velocities.
7pub struct IntegratePositionKernel;
8/// Projected Gauss-Seidel (PGS) constraint solver kernel.
9///
10/// Iteratively solves positional constraints using position-based dynamics
11/// (PBD) style updates.
12pub struct ConstraintSolverKernel;
13impl ConstraintSolverKernel {
14    /// Solve distance constraints for one iteration.
15    ///
16    /// Updates positions in-place to satisfy the constraints.
17    /// `inv_masses[i]` is the inverse mass of body i.
18    pub fn solve_distance_constraints(
19        positions: &mut [[f64; 3]],
20        inv_masses: &[f64],
21        constraints: &[DistanceConstraint],
22        dt: f64,
23    ) {
24        let alpha = 1.0 / (dt * dt);
25        for c in constraints {
26            let a = c.body_a;
27            let b = c.body_b;
28            let w_a = inv_masses[a];
29            let w_b = inv_masses[b];
30            let w_total = w_a + w_b;
31            if w_total < 1e-30 {
32                continue;
33            }
34            let dx = [
35                positions[b][0] - positions[a][0],
36                positions[b][1] - positions[a][1],
37                positions[b][2] - positions[a][2],
38            ];
39            let dist = (dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2]).sqrt();
40            if dist < 1e-30 {
41                continue;
42            }
43            let err = dist - c.rest_length;
44            let tilde_compliance = c.compliance * alpha;
45            let lambda = -err / (w_total + tilde_compliance);
46            let correction = lambda / dist;
47            for k in 0..3 {
48                positions[a][k] -= w_a * correction * dx[k];
49                positions[b][k] += w_b * correction * dx[k];
50            }
51        }
52    }
53}
54/// Sleeping parameters used by [`SleepTest`].
55#[derive(Debug, Clone, Copy)]
56pub struct SleepParams {
57    /// Linear velocity threshold below which the body is considered dormant.
58    pub linear_threshold: f64,
59    /// Angular velocity threshold.
60    pub angular_threshold: f64,
61    /// Number of consecutive frames below threshold before the body sleeps.
62    pub sleep_frames: u32,
63}
64/// Full rigid body state for one body.
65///
66/// Orientation is stored as a unit quaternion `[qx, qy, qz, qw]`.
67#[derive(Debug, Clone, Copy, PartialEq)]
68pub struct RigidBodyState {
69    /// Centre-of-mass position.
70    pub position: [f64; 3],
71    /// Linear velocity.
72    pub velocity: [f64; 3],
73    /// Orientation quaternion `[qx, qy, qz, qw]`.
74    pub orientation: [f64; 4],
75    /// Angular velocity in world frame.
76    pub angular_velocity: [f64; 3],
77    /// Inverse mass (0 = infinite mass / static body).
78    pub inverse_mass: f64,
79}
80impl RigidBodyState {
81    /// Construct a `RigidBodyState` at rest.
82    pub fn at_rest(position: [f64; 3], inverse_mass: f64) -> Self {
83        Self {
84            position,
85            velocity: [0.0; 3],
86            orientation: [0.0, 0.0, 0.0, 1.0],
87            angular_velocity: [0.0; 3],
88            inverse_mass,
89        }
90    }
91}
92/// Sleeping state for a rigid body.
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum SleepState {
95    /// Body is fully awake and integrated every step.
96    Awake,
97    /// Body is sleeping and skipped during integration.
98    Sleeping,
99}
100/// Processes a batch of contacts and accumulates impulses.
101///
102/// This is the CPU-side mock of the GPU contact-batch dispatch kernel.
103/// In a real GPU implementation each contact would be handled by one thread;
104/// atomic adds would accumulate the impulses into per-body registers.
105pub struct ContactBatchProcessor {
106    /// Coefficient of restitution for all contacts.
107    pub restitution: f64,
108    /// Positional correction fraction (Baumgarte stabilisation).
109    pub baumgarte: f64,
110}
111impl ContactBatchProcessor {
112    /// Create a new processor.
113    pub fn new(restitution: f64, baumgarte: f64) -> Self {
114        Self {
115            restitution,
116            baumgarte,
117        }
118    }
119    /// Process all contacts and return accumulated impulses for each body.
120    ///
121    /// `n_bodies` is the total body count; the returned `Vec` has one entry
122    /// per body, initialised to zero and populated by this function.
123    pub fn process_contacts(
124        &self,
125        contacts: &[ContactPoint],
126        positions: &[[f64; 3]],
127        velocities: &[[f64; 3]],
128        inv_masses: &[f64],
129        n_bodies: usize,
130        dt: f64,
131    ) -> Vec<AccumulatedImpulse> {
132        let mut impulses = vec![AccumulatedImpulse::default(); n_bodies];
133        for c in contacts {
134            let a = c.body_a;
135            let b = c.body_b;
136            let w_a = inv_masses[a];
137            let w_b = inv_masses[b];
138            let w_total = w_a + w_b;
139            if w_total < 1e-30 {
140                continue;
141            }
142            let corr = self.baumgarte * c.depth / (w_total * dt);
143            let rel_vel: f64 = (0..3)
144                .map(|k| (velocities[b][k] - velocities[a][k]) * c.normal[k])
145                .sum();
146            let j = if rel_vel < 0.0 {
147                -(1.0 + self.restitution) * rel_vel / w_total
148            } else {
149                0.0
150            };
151            let impulse_a = [
152                -w_a * (j + corr) * c.normal[0],
153                -w_a * (j + corr) * c.normal[1],
154                -w_a * (j + corr) * c.normal[2],
155            ];
156            let impulse_b = [
157                w_b * (j + corr) * c.normal[0],
158                w_b * (j + corr) * c.normal[1],
159                w_b * (j + corr) * c.normal[2],
160            ];
161            for k in 0..3 {
162                impulses[a].linear[k] += impulse_a[k];
163                impulses[b].linear[k] += impulse_b[k];
164            }
165            let ra = [
166                c.position[0] - positions[a][0],
167                c.position[1] - positions[a][1],
168                c.position[2] - positions[a][2],
169            ];
170            let rb = [
171                c.position[0] - positions[b][0],
172                c.position[1] - positions[b][1],
173                c.position[2] - positions[b][2],
174            ];
175            let ang_a = [
176                ra[1] * impulse_a[2] - ra[2] * impulse_a[1],
177                ra[2] * impulse_a[0] - ra[0] * impulse_a[2],
178                ra[0] * impulse_a[1] - ra[1] * impulse_a[0],
179            ];
180            let ang_b = [
181                rb[1] * impulse_b[2] - rb[2] * impulse_b[1],
182                rb[2] * impulse_b[0] - rb[0] * impulse_b[2],
183                rb[0] * impulse_b[1] - rb[1] * impulse_b[0],
184            ];
185            impulses[a].add_angular(ang_a);
186            impulses[b].add_angular(ang_b);
187        }
188        impulses
189    }
190}
191/// Island (connected component) solver for the constraint graph.
192///
193/// Bodies connected by constraints form "islands" that can be solved
194/// independently, enabling parallel execution.
195pub struct IslandSolver {
196    /// Island ID for each body. Bodies with the same ID are in the same island.
197    pub island_ids: Vec<usize>,
198    /// Number of islands.
199    pub num_islands: usize,
200}
201impl IslandSolver {
202    /// Build islands from constraints using union-find.
203    pub fn build(num_bodies: usize, constraints: &[(usize, usize)]) -> Self {
204        let mut parent: Vec<usize> = (0..num_bodies).collect();
205        let mut rank = vec![0usize; num_bodies];
206        let find = |parent: &mut Vec<usize>, mut x: usize| -> usize {
207            while parent[x] != x {
208                parent[x] = parent[parent[x]];
209                x = parent[x];
210            }
211            x
212        };
213        for &(a, b) in constraints {
214            let ra = find(&mut parent, a);
215            let rb = find(&mut parent, b);
216            if ra != rb {
217                if rank[ra] < rank[rb] {
218                    parent[ra] = rb;
219                } else if rank[ra] > rank[rb] {
220                    parent[rb] = ra;
221                } else {
222                    parent[rb] = ra;
223                    rank[ra] += 1;
224                }
225            }
226        }
227        let mut island_map = std::collections::HashMap::new();
228        let mut island_ids = vec![0usize; num_bodies];
229        let mut next_id = 0usize;
230        for (i, island_id) in island_ids.iter_mut().enumerate() {
231            let root = find(&mut parent, i);
232            let id = *island_map.entry(root).or_insert_with(|| {
233                let id = next_id;
234                next_id += 1;
235                id
236            });
237            *island_id = id;
238        }
239        Self {
240            island_ids,
241            num_islands: next_id,
242        }
243    }
244    /// Get all body indices belonging to a specific island.
245    pub fn bodies_in_island(&self, island_id: usize) -> Vec<usize> {
246        self.island_ids
247            .iter()
248            .enumerate()
249            .filter(|&(_, id)| *id == island_id)
250            .map(|(i, _)| i)
251            .collect()
252    }
253}
254/// Tests whether a rigid body should transition to or from sleeping.
255pub struct SleepTest {
256    /// Per-body consecutive dormant frame counter.
257    pub dormant_frames: Vec<u32>,
258    /// Per-body current sleep state.
259    pub sleep_states: Vec<SleepState>,
260}
261impl SleepTest {
262    /// Create a new sleep test for `n` bodies (all awake initially).
263    pub fn new(n: usize) -> Self {
264        Self {
265            dormant_frames: vec![0; n],
266            sleep_states: vec![SleepState::Awake; n],
267        }
268    }
269    /// Update sleep states for all bodies given the current states.
270    ///
271    /// Returns the number of bodies that transitioned to sleep this step.
272    pub fn update(&mut self, states: &[RigidBodyState], params: &SleepParams) -> usize {
273        let mut newly_sleeping = 0;
274        for (i, s) in states.iter().enumerate() {
275            if s.inverse_mass < 1e-30 {
276                self.sleep_states[i] = SleepState::Sleeping;
277                continue;
278            }
279            let lin_sq: f64 = s.velocity.iter().map(|v| v * v).sum();
280            let ang_sq: f64 = s.angular_velocity.iter().map(|w| w * w).sum();
281            let dormant = lin_sq < params.linear_threshold * params.linear_threshold
282                && ang_sq < params.angular_threshold * params.angular_threshold;
283            if dormant {
284                self.dormant_frames[i] += 1;
285                if self.dormant_frames[i] >= params.sleep_frames
286                    && self.sleep_states[i] == SleepState::Awake
287                {
288                    self.sleep_states[i] = SleepState::Sleeping;
289                    newly_sleeping += 1;
290                }
291            } else {
292                self.dormant_frames[i] = 0;
293                self.sleep_states[i] = SleepState::Awake;
294            }
295        }
296        newly_sleeping
297    }
298    /// Wake all bodies.
299    pub fn wake_all(&mut self) {
300        for s in &mut self.sleep_states {
301            *s = SleepState::Awake;
302        }
303        for f in &mut self.dormant_frames {
304            *f = 0;
305        }
306    }
307    /// Return the number of sleeping bodies.
308    pub fn sleeping_count(&self) -> usize {
309        self.sleep_states
310            .iter()
311            .filter(|&&s| s == SleepState::Sleeping)
312            .count()
313    }
314}
315/// Accumulated impulse for one body during one solve step.
316#[derive(Debug, Clone, Copy, Default)]
317pub struct AccumulatedImpulse {
318    /// Linear impulse (Δ momentum).
319    pub linear: [f64; 3],
320    /// Angular impulse (Δ angular momentum).
321    pub angular: [f64; 3],
322}
323impl AccumulatedImpulse {
324    /// Add a linear impulse contribution.
325    pub fn add_linear(&mut self, impulse: [f64; 3]) {
326        for (l, &imp) in self.linear.iter_mut().zip(impulse.iter()) {
327            *l += imp;
328        }
329    }
330    /// Add an angular impulse contribution.
331    pub fn add_angular(&mut self, impulse: [f64; 3]) {
332        for (a, &imp) in self.angular.iter_mut().zip(impulse.iter()) {
333            *a += imp;
334        }
335    }
336    /// Apply the accumulated impulse to a body state.
337    pub fn apply(&self, state: &mut RigidBodyState) {
338        let im = state.inverse_mass;
339        for k in 0..3 {
340            state.velocity[k] += self.linear[k] * im;
341            state.angular_velocity[k] += self.angular[k] * im;
342        }
343    }
344    /// L2 norm of the linear impulse.
345    pub fn linear_magnitude(&self) -> f64 {
346        self.linear.iter().map(|v| v * v).sum::<f64>().sqrt()
347    }
348    /// L2 norm of the angular impulse.
349    pub fn angular_magnitude(&self) -> f64 {
350        self.angular.iter().map(|v| v * v).sum::<f64>().sqrt()
351    }
352}
353/// Kernel that integrates velocities given forces and masses.
354pub struct IntegrateVelocityKernel;
355/// A distance constraint between two bodies.
356#[derive(Debug, Clone, Copy)]
357pub struct DistanceConstraint {
358    /// Index of body A.
359    pub body_a: usize,
360    /// Index of body B.
361    pub body_b: usize,
362    /// Rest length of the constraint.
363    pub rest_length: f64,
364    /// Compliance (inverse stiffness). 0 = rigid.
365    pub compliance: f64,
366}
367/// Rigid body data in Structure-of-Arrays (SoA) layout.
368///
369/// This layout maps directly to GPU buffer uploads where each quantity
370/// (position, velocity, quaternion, etc.) occupies a contiguous memory region,
371/// enabling vectorized (SIMD-width) processing in CUDA/WGSL kernels.
372pub struct SoaRigidBody {
373    /// Number of bodies.
374    pub count: usize,
375    pub pos_x: Vec<f64>,
376    pub pos_y: Vec<f64>,
377    pub pos_z: Vec<f64>,
378    pub vel_x: Vec<f64>,
379    pub vel_y: Vec<f64>,
380    pub vel_z: Vec<f64>,
381    pub quat_x: Vec<f64>,
382    pub quat_y: Vec<f64>,
383    pub quat_z: Vec<f64>,
384    pub quat_w: Vec<f64>,
385    pub omega_x: Vec<f64>,
386    pub omega_y: Vec<f64>,
387    pub omega_z: Vec<f64>,
388    pub inv_mass: Vec<f64>,
389}
390impl SoaRigidBody {
391    /// Build an SoA buffer from a slice of `RigidBodyState` values.
392    pub fn from_slice(states: &[RigidBodyState]) -> Self {
393        let n = states.len();
394        let mut s = Self {
395            count: n,
396            pos_x: Vec::with_capacity(n),
397            pos_y: Vec::with_capacity(n),
398            pos_z: Vec::with_capacity(n),
399            vel_x: Vec::with_capacity(n),
400            vel_y: Vec::with_capacity(n),
401            vel_z: Vec::with_capacity(n),
402            quat_x: Vec::with_capacity(n),
403            quat_y: Vec::with_capacity(n),
404            quat_z: Vec::with_capacity(n),
405            quat_w: Vec::with_capacity(n),
406            omega_x: Vec::with_capacity(n),
407            omega_y: Vec::with_capacity(n),
408            omega_z: Vec::with_capacity(n),
409            inv_mass: Vec::with_capacity(n),
410        };
411        for rb in states {
412            s.pos_x.push(rb.position[0]);
413            s.pos_y.push(rb.position[1]);
414            s.pos_z.push(rb.position[2]);
415            s.vel_x.push(rb.velocity[0]);
416            s.vel_y.push(rb.velocity[1]);
417            s.vel_z.push(rb.velocity[2]);
418            s.quat_x.push(rb.orientation[0]);
419            s.quat_y.push(rb.orientation[1]);
420            s.quat_z.push(rb.orientation[2]);
421            s.quat_w.push(rb.orientation[3]);
422            s.omega_x.push(rb.angular_velocity[0]);
423            s.omega_y.push(rb.angular_velocity[1]);
424            s.omega_z.push(rb.angular_velocity[2]);
425            s.inv_mass.push(rb.inverse_mass);
426        }
427        s
428    }
429    /// Convert back to a `Vec<RigidBodyState>`.
430    pub fn to_vec(&self) -> Vec<RigidBodyState> {
431        (0..self.count)
432            .map(|i| RigidBodyState {
433                position: [self.pos_x[i], self.pos_y[i], self.pos_z[i]],
434                velocity: [self.vel_x[i], self.vel_y[i], self.vel_z[i]],
435                orientation: [
436                    self.quat_x[i],
437                    self.quat_y[i],
438                    self.quat_z[i],
439                    self.quat_w[i],
440                ],
441                angular_velocity: [self.omega_x[i], self.omega_y[i], self.omega_z[i]],
442                inverse_mass: self.inv_mass[i],
443            })
444            .collect()
445    }
446    /// Integrate all bodies using Euler stepping (SIMD-friendly loop).
447    ///
448    /// Processes all bodies in a single loop over the SoA arrays, mimicking
449    /// how a GPU kernel would launch one thread per body.
450    pub fn integrate_euler(&mut self, forces: &[[f64; 3]], torques: &[[f64; 3]], dt: f64) {
451        for i in 0..self.count {
452            let im = self.inv_mass[i];
453            let ax = forces[i][0] * im;
454            let ay = forces[i][1] * im;
455            let az = forces[i][2] * im;
456            self.vel_x[i] += ax * dt;
457            self.vel_y[i] += ay * dt;
458            self.vel_z[i] += az * dt;
459            self.pos_x[i] += self.vel_x[i] * dt;
460            self.pos_y[i] += self.vel_y[i] * dt;
461            self.pos_z[i] += self.vel_z[i] * dt;
462            let alpha_x = torques[i][0] * im;
463            let alpha_y = torques[i][1] * im;
464            let alpha_z = torques[i][2] * im;
465            self.omega_x[i] += alpha_x * dt;
466            self.omega_y[i] += alpha_y * dt;
467            self.omega_z[i] += alpha_z * dt;
468            let q = [
469                self.quat_x[i],
470                self.quat_y[i],
471                self.quat_z[i],
472                self.quat_w[i],
473            ];
474            let omega = [self.omega_x[i], self.omega_y[i], self.omega_z[i]];
475            let dq = quat_derivative(q, omega);
476            self.quat_x[i] += dq[0] * dt;
477            self.quat_y[i] += dq[1] * dt;
478            self.quat_z[i] += dq[2] * dt;
479            self.quat_w[i] += dq[3] * dt;
480            let qn = quat_normalise([
481                self.quat_x[i],
482                self.quat_y[i],
483                self.quat_z[i],
484                self.quat_w[i],
485            ]);
486            self.quat_x[i] = qn[0];
487            self.quat_y[i] = qn[1];
488            self.quat_z[i] = qn[2];
489            self.quat_w[i] = qn[3];
490        }
491    }
492}
493/// Axis-aligned bounding box for broadphase.
494#[derive(Debug, Clone, Copy)]
495pub struct Aabb {
496    pub min: [f64; 3],
497    pub max: [f64; 3],
498}
499impl Aabb {
500    /// Create an AABB centred at `position` with `half_extents`.
501    pub fn from_center(position: [f64; 3], half_extents: [f64; 3]) -> Self {
502        Self {
503            min: [
504                position[0] - half_extents[0],
505                position[1] - half_extents[1],
506                position[2] - half_extents[2],
507            ],
508            max: [
509                position[0] + half_extents[0],
510                position[1] + half_extents[1],
511                position[2] + half_extents[2],
512            ],
513        }
514    }
515    /// Check if two AABBs overlap.
516    pub fn overlaps(&self, other: &Aabb) -> bool {
517        self.min[0] <= other.max[0]
518            && self.max[0] >= other.min[0]
519            && self.min[1] <= other.max[1]
520            && self.max[1] >= other.min[1]
521            && self.min[2] <= other.max[2]
522            && self.max[2] >= other.min[2]
523    }
524    /// Expand the AABB by a margin in all directions.
525    pub fn expand(&mut self, margin: f64) {
526        for k in 0..3 {
527            self.min[k] -= margin;
528            self.max[k] += margin;
529        }
530    }
531    /// Compute the volume of the AABB.
532    pub fn volume(&self) -> f64 {
533        (self.max[0] - self.min[0]) * (self.max[1] - self.min[1]) * (self.max[2] - self.min[2])
534    }
535}
536/// Broadphase update kernel: recomputes AABBs from body positions and
537/// finds overlapping pairs using a brute-force sweep.
538pub struct BroadphaseUpdateKernel;
539impl BroadphaseUpdateKernel {
540    /// Update AABBs from body states and find overlapping pairs.
541    ///
542    /// `half_extents[i]` is the half-extent of body i's AABB.
543    /// Returns a list of overlapping body pairs `(i, j)` with `i < j`.
544    pub fn find_overlapping_pairs(
545        positions: &[[f64; 3]],
546        half_extents: &[[f64; 3]],
547        margin: f64,
548    ) -> Vec<(usize, usize)> {
549        let n = positions.len();
550        let mut aabbs: Vec<Aabb> = Vec::with_capacity(n);
551        for i in 0..n {
552            let mut aabb = Aabb::from_center(positions[i], half_extents[i]);
553            aabb.expand(margin);
554            aabbs.push(aabb);
555        }
556        let mut pairs = Vec::new();
557        for i in 0..n {
558            for j in (i + 1)..n {
559                if aabbs[i].overlaps(&aabbs[j]) {
560                    pairs.push((i, j));
561                }
562            }
563        }
564        pairs
565    }
566}
567/// A contact point between two bodies.
568#[derive(Debug, Clone, Copy)]
569pub struct ContactPoint {
570    /// Contact position in world space.
571    pub position: [f64; 3],
572    /// Contact normal (pointing from body A to body B).
573    pub normal: [f64; 3],
574    /// Penetration depth (positive means overlap).
575    pub depth: f64,
576    /// Index of body A.
577    pub body_a: usize,
578    /// Index of body B.
579    pub body_b: usize,
580}
581/// Semi-implicit Euler integration kernel.
582///
583/// Updates velocity first, then uses the new velocity to update position.
584/// This is symplectic and better preserves energy than standard Euler.
585pub struct SemiImplicitEulerKernel;
586/// A kernel that normalizes a batch of quaternions to unit length.
587///
588/// In a real CUDA kernel each thread handles one quaternion; this mock
589/// processes them in a sequential loop with the same memory access pattern.
590pub struct QuaternionNormKernel;
591impl QuaternionNormKernel {
592    /// Normalize a batch of quaternions to unit length.
593    pub fn normalize_batch(quats: &[[f64; 4]]) -> Vec<[f64; 4]> {
594        quats.iter().map(|&q| quat_normalise(q)).collect()
595    }
596    /// In-place normalize a batch of quaternions.
597    pub fn normalize_in_place(quats: &mut [[f64; 4]]) {
598        for q in quats.iter_mut() {
599            *q = quat_normalise(*q);
600        }
601    }
602    /// Count how many quaternions deviate from unit length by more than `tol`.
603    pub fn count_denormalized(quats: &[[f64; 4]], tol: f64) -> usize {
604        quats
605            .iter()
606            .filter(|&&q| {
607                let len = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
608                (len - 1.0).abs() > tol
609            })
610            .count()
611    }
612}
613/// Contact generation kernel: generates contact points between
614/// sphere-shaped bodies.
615pub struct ContactGenerationKernel;
616impl ContactGenerationKernel {
617    /// Generate contacts between sphere bodies.
618    ///
619    /// `positions[i]` is the position of body i.
620    /// `radii[i]` is the radius of body i.
621    /// `pairs` is the list of potentially overlapping pairs from broadphase.
622    pub fn generate_sphere_contacts(
623        positions: &[[f64; 3]],
624        radii: &[f64],
625        pairs: &[(usize, usize)],
626    ) -> Vec<ContactPoint> {
627        let mut contacts = Vec::new();
628        for &(a, b) in pairs {
629            let dx = [
630                positions[b][0] - positions[a][0],
631                positions[b][1] - positions[a][1],
632                positions[b][2] - positions[a][2],
633            ];
634            let dist_sq = dx[0] * dx[0] + dx[1] * dx[1] + dx[2] * dx[2];
635            let sum_r = radii[a] + radii[b];
636            if dist_sq < sum_r * sum_r {
637                let dist = dist_sq.sqrt();
638                let depth = sum_r - dist;
639                let normal = if dist > 1e-14 {
640                    [dx[0] / dist, dx[1] / dist, dx[2] / dist]
641                } else {
642                    [0.0, 1.0, 0.0]
643                };
644                let contact_pos = [
645                    positions[a][0] + normal[0] * radii[a],
646                    positions[a][1] + normal[1] * radii[a],
647                    positions[a][2] + normal[2] * radii[a],
648                ];
649                contacts.push(ContactPoint {
650                    position: contact_pos,
651                    normal,
652                    depth,
653                    body_a: a,
654                    body_b: b,
655                });
656            }
657        }
658        contacts
659    }
660    /// Apply contact impulses to resolve penetration.
661    ///
662    /// Uses a simple position-correction approach.
663    pub fn resolve_contacts(
664        positions: &mut [[f64; 3]],
665        velocities: &mut [[f64; 3]],
666        inv_masses: &[f64],
667        contacts: &[ContactPoint],
668        restitution: f64,
669    ) {
670        for c in contacts {
671            let a = c.body_a;
672            let b = c.body_b;
673            let w_a = inv_masses[a];
674            let w_b = inv_masses[b];
675            let w_total = w_a + w_b;
676            if w_total < 1e-30 {
677                continue;
678            }
679            let correction = c.depth / w_total;
680            for (k, &n_k) in c.normal.iter().enumerate() {
681                positions[a][k] -= w_a * correction * n_k;
682                positions[b][k] += w_b * correction * n_k;
683            }
684            let rel_vel = [
685                velocities[b][0] - velocities[a][0],
686                velocities[b][1] - velocities[a][1],
687                velocities[b][2] - velocities[a][2],
688            ];
689            let vel_along_normal =
690                rel_vel[0] * c.normal[0] + rel_vel[1] * c.normal[1] + rel_vel[2] * c.normal[2];
691            if vel_along_normal < 0.0 {
692                let j = -(1.0 + restitution) * vel_along_normal / w_total;
693                for (k, &n_k) in c.normal.iter().enumerate() {
694                    velocities[a][k] -= w_a * j * n_k;
695                    velocities[b][k] += w_b * j * n_k;
696                }
697            }
698        }
699    }
700}