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