scirs2_spatial/collision/
response.rs

1//! Collision response algorithms
2//!
3//! This module provides algorithms for calculating appropriate reactions
4//! after a collision has been detected, such as computing impulses and
5//! resolving penetration.
6
7use super::shapes::{Box3D, Sphere, Triangle3D};
8
9/// Information about a collision
10#[derive(Debug, Clone)]
11pub struct CollisionInfo {
12    /// Point of collision in world space
13    pub contact_point: [f64; 3],
14    /// Normal vector at the point of collision (pointing from first object to second)
15    pub normal: [f64; 3],
16    /// Penetration depth (positive indicates overlap)
17    pub penetration: f64,
18}
19
20/// Calculates the collision response impulse for two spheres
21///
22/// # Arguments
23///
24/// * `sphere1_pos` - Position of the first sphere
25/// * `sphere1_vel` - Velocity of the first sphere
26/// * `sphere1_mass` - Mass of the first sphere
27/// * `sphere2_pos` - Position of the second sphere
28/// * `sphere2_vel` - Velocity of the second sphere
29/// * `sphere2_mass` - Mass of the second sphere
30/// * `restitution` - Coefficient of restitution (0 = inelastic, 1 = elastic)
31///
32/// # Returns
33///
34/// A tuple containing the impulse vectors for the first and second spheres
35#[allow(dead_code)]
36#[allow(clippy::too_many_arguments)]
37pub fn sphere_sphere_impulse(
38    sphere1_pos: &[f64; 3],
39    sphere1_vel: &[f64; 3],
40    sphere1_mass: f64,
41    sphere2_pos: &[f64; 3],
42    sphere2_vel: &[f64; 3],
43    sphere2_mass: f64,
44    restitution: f64,
45) -> ([f64; 3], [f64; 3]) {
46    // Calculate collision normal
47    let normal = [
48        sphere2_pos[0] - sphere1_pos[0],
49        sphere2_pos[1] - sphere1_pos[1],
50        sphere2_pos[2] - sphere1_pos[2],
51    ];
52
53    // Normalize the normal
54    let normal_length =
55        (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
56    if normal_length < 1e-10 {
57        // Spheres are at the same position, use an arbitrary normal
58        return ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
59    }
60
61    let unit_normal = [
62        normal[0] / normal_length,
63        normal[1] / normal_length,
64        normal[2] / normal_length,
65    ];
66
67    // Calculate relative velocity
68    let relative_velocity = [
69        sphere2_vel[0] - sphere1_vel[0],
70        sphere2_vel[1] - sphere1_vel[1],
71        sphere2_vel[2] - sphere1_vel[2],
72    ];
73
74    // Calculate velocity along the normal
75    let velocity_along_normal = relative_velocity[0] * unit_normal[0]
76        + relative_velocity[1] * unit_normal[1]
77        + relative_velocity[2] * unit_normal[2];
78
79    // If the spheres are moving away from each other, no impulse is needed
80    if velocity_along_normal > 0.0 {
81        return ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
82    }
83
84    // Calculate the impulse scalar
85    let inverse_mass1 = if sphere1_mass == 0.0 {
86        0.0
87    } else {
88        1.0 / sphere1_mass
89    };
90    let inverse_mass2 = if sphere2_mass == 0.0 {
91        0.0
92    } else {
93        1.0 / sphere2_mass
94    };
95
96    let impulse_scalar =
97        -(1.0 + restitution) * velocity_along_normal / (inverse_mass1 + inverse_mass2);
98
99    // Calculate the impulse vectors
100    let impulse1 = [
101        -impulse_scalar * unit_normal[0] * inverse_mass1,
102        -impulse_scalar * unit_normal[1] * inverse_mass1,
103        -impulse_scalar * unit_normal[2] * inverse_mass1,
104    ];
105
106    let impulse2 = [
107        impulse_scalar * unit_normal[0] * inverse_mass2,
108        impulse_scalar * unit_normal[1] * inverse_mass2,
109        impulse_scalar * unit_normal[2] * inverse_mass2,
110    ];
111
112    (impulse1, impulse2)
113}
114
115/// Resolves the penetration between two spheres
116///
117/// # Arguments
118///
119/// * `sphere1` - The first sphere
120/// * `sphere1_mass` - Mass of the first sphere
121/// * `sphere2` - The second sphere
122/// * `sphere2_mass` - Mass of the second sphere
123///
124/// # Returns
125///
126/// A tuple containing the position adjustments for the first and second spheres
127#[allow(dead_code)]
128pub fn resolve_sphere_sphere_penetration(
129    sphere1: &Sphere,
130    sphere1_mass: f64,
131    sphere2: &Sphere,
132    sphere2_mass: f64,
133) -> ([f64; 3], [f64; 3]) {
134    // Calculate distance between spheres
135    let normal = [
136        sphere2.center[0] - sphere1.center[0],
137        sphere2.center[1] - sphere1.center[1],
138        sphere2.center[2] - sphere1.center[2],
139    ];
140
141    let distance = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
142
143    // If no penetration, no adjustment needed
144    if distance >= sphere1.radius + sphere2.radius {
145        return ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
146    }
147
148    // Calculate penetration depth
149    let penetration = sphere1.radius + sphere2.radius - distance;
150
151    // Normalize the normal
152    let unit_normal = if distance < 1e-10 {
153        // Spheres are at the same position, use an arbitrary normal
154        [1.0, 0.0, 0.0]
155    } else {
156        [
157            normal[0] / distance,
158            normal[1] / distance,
159            normal[2] / distance,
160        ]
161    };
162
163    // Calculate _mass factors
164    let total_mass = sphere1_mass + sphere2_mass;
165    let mass_ratio1 = if total_mass == 0.0 {
166        0.5
167    } else {
168        sphere2_mass / total_mass
169    };
170    let mass_ratio2 = if total_mass == 0.0 {
171        0.5
172    } else {
173        sphere1_mass / total_mass
174    };
175
176    // Adjust positions based on _mass ratio
177    let adjustment1 = [
178        -unit_normal[0] * penetration * mass_ratio1,
179        -unit_normal[1] * penetration * mass_ratio1,
180        -unit_normal[2] * penetration * mass_ratio1,
181    ];
182
183    let adjustment2 = [
184        unit_normal[0] * penetration * mass_ratio2,
185        unit_normal[1] * penetration * mass_ratio2,
186        unit_normal[2] * penetration * mass_ratio2,
187    ];
188
189    (adjustment1, adjustment2)
190}
191
192/// Calculates the collision response impulse for a sphere and a box
193///
194/// # Arguments
195///
196/// * `sphere_pos` - Position of the sphere
197/// * `sphere_vel` - Velocity of the sphere
198/// * `sphere_mass` - Mass of the sphere
199/// * `box_pos` - Position of the box (center)
200/// * `box_vel` - Velocity of the box
201/// * `box_mass` - Mass of the box
202/// * `box_dims` - Dimensions of the box [width, height, depth]
203/// * `collision_info` - Information about the collision
204/// * `restitution` - Coefficient of restitution (0 = inelastic, 1 = elastic)
205///
206/// # Returns
207///
208/// A tuple containing the impulse vectors for the sphere and the box
209#[allow(clippy::too_many_arguments)]
210#[allow(dead_code)]
211pub fn sphere_box_impulse(
212    _sphere_pos: &[f64; 3],
213    sphere_vel: &[f64; 3],
214    sphere_mass: f64,
215    _box_pos: &[f64; 3],
216    box_vel: &[f64; 3],
217    box_mass: f64,
218    _box_dims: &[f64; 3],
219    collision_info: &CollisionInfo,
220    restitution: f64,
221) -> ([f64; 3], [f64; 3]) {
222    // Calculate relative velocity
223    let relative_velocity = [
224        sphere_vel[0] - box_vel[0],
225        sphere_vel[1] - box_vel[1],
226        sphere_vel[2] - box_vel[2],
227    ];
228
229    // Calculate velocity along the normal
230    let velocity_along_normal = relative_velocity[0] * collision_info.normal[0]
231        + relative_velocity[1] * collision_info.normal[1]
232        + relative_velocity[2] * collision_info.normal[2];
233
234    // If the objects are moving away from each other, no impulse is needed
235    if velocity_along_normal > 0.0 {
236        return ([0.0, 0.0, 0.0], [0.0, 0.0, 0.0]);
237    }
238
239    // Calculate the impulse scalar
240    let inverse_mass1 = if sphere_mass == 0.0 {
241        0.0
242    } else {
243        1.0 / sphere_mass
244    };
245    let inverse_mass2 = if box_mass == 0.0 { 0.0 } else { 1.0 / box_mass };
246
247    let impulse_scalar =
248        -(1.0 + restitution) * velocity_along_normal / (inverse_mass1 + inverse_mass2);
249
250    // Calculate the impulse vectors
251    let impulse1 = [
252        impulse_scalar * collision_info.normal[0] * inverse_mass1,
253        impulse_scalar * collision_info.normal[1] * inverse_mass1,
254        impulse_scalar * collision_info.normal[2] * inverse_mass1,
255    ];
256
257    let impulse2 = [
258        -impulse_scalar * collision_info.normal[0] * inverse_mass2,
259        -impulse_scalar * collision_info.normal[1] * inverse_mass2,
260        -impulse_scalar * collision_info.normal[2] * inverse_mass2,
261    ];
262
263    (impulse1, impulse2)
264}
265
266/// Finds the collision information for a sphere and a box
267///
268/// # Arguments
269///
270/// * `sphere` - The sphere
271/// * `box3d` - The box
272///
273/// # Returns
274///
275/// Collision information if a collision occurs, None otherwise
276#[allow(dead_code)]
277pub fn find_sphere_box_collision(sphere: &Sphere, box3d: &Box3D) -> Option<CollisionInfo> {
278    // Find the closest point on the box to the sphere center
279    let closest_x = sphere.center[0].max(box3d.min[0]).min(box3d.max[0]);
280    let closest_y = sphere.center[1].max(box3d.min[1]).min(box3d.max[1]);
281    let closest_z = sphere.center[2].max(box3d.min[2]).min(box3d.max[2]);
282
283    let closest_point = [closest_x, closest_y, closest_z];
284
285    // Calculate distance from the closest point to the sphere center
286    let dx = sphere.center[0] - closest_point[0];
287    let dy = sphere.center[1] - closest_point[1];
288    let dz = sphere.center[2] - closest_point[2];
289    let distance_squared = dx * dx + dy * dy + dz * dz;
290
291    // Check if collision occurs
292    if distance_squared > sphere.radius * sphere.radius {
293        return None;
294    }
295
296    let distance = distance_squared.sqrt();
297
298    // Normal points from the box to the sphere
299    let normal = if distance < 1e-10 {
300        // Sphere center is on the box surface, use the face normal
301        // Determine which face is closest to the sphere center
302        let box_center = [
303            (box3d.min[0] + box3d.max[0]) * 0.5,
304            (box3d.min[1] + box3d.max[1]) * 0.5,
305            (box3d.min[2] + box3d.max[2]) * 0.5,
306        ];
307
308        let half_width = (box3d.max[0] - box3d.min[0]) * 0.5;
309        let half_height = (box3d.max[1] - box3d.min[1]) * 0.5;
310        let half_depth = (box3d.max[2] - box3d.min[2]) * 0.5;
311
312        let dx = (sphere.center[0] - box_center[0]).abs() / half_width;
313        let dy = (sphere.center[1] - box_center[1]).abs() / half_height;
314        let dz = (sphere.center[2] - box_center[2]).abs() / half_depth;
315
316        if dx > dy && dx > dz {
317            [(sphere.center[0] - box_center[0]).signum(), 0.0, 0.0]
318        } else if dy > dz {
319            [0.0, (sphere.center[1] - box_center[1]).signum(), 0.0]
320        } else {
321            [0.0, 0.0, (sphere.center[2] - box_center[2]).signum()]
322        }
323    } else {
324        [dx / distance, dy / distance, dz / distance]
325    };
326
327    let penetration = sphere.radius - distance;
328
329    Some(CollisionInfo {
330        contact_point: closest_point,
331        normal,
332        penetration,
333    })
334}
335
336/// Finds the collision information for a sphere and a triangle
337///
338/// # Arguments
339///
340/// * `sphere` - The sphere
341/// * `triangle` - The triangle
342///
343/// # Returns
344///
345/// Collision information if a collision occurs, None otherwise
346#[allow(dead_code)]
347pub fn find_sphere_triangle_collision(
348    sphere: &Sphere,
349    triangle: &Triangle3D,
350) -> Option<CollisionInfo> {
351    // First, calculate the normal vector of the triangle
352    let edge1 = [
353        triangle.v2[0] - triangle.v1[0],
354        triangle.v2[1] - triangle.v1[1],
355        triangle.v2[2] - triangle.v1[2],
356    ];
357
358    let edge2 = [
359        triangle.v3[0] - triangle.v1[0],
360        triangle.v3[1] - triangle.v1[1],
361        triangle.v3[2] - triangle.v1[2],
362    ];
363
364    // Cross product of the two edges gives the normal
365    let normal = [
366        edge1[1] * edge2[2] - edge1[2] * edge2[1],
367        edge1[2] * edge2[0] - edge1[0] * edge2[2],
368        edge1[0] * edge2[1] - edge1[1] * edge2[0],
369    ];
370
371    // Normalize the normal
372    let normal_length =
373        (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]).sqrt();
374    if normal_length < 1e-10 {
375        return None; // Degenerate triangle
376    }
377
378    let normalized_normal = [
379        normal[0] / normal_length,
380        normal[1] / normal_length,
381        normal[2] / normal_length,
382    ];
383
384    // Calculate distance from sphere center to the plane of the triangle
385    let dist_to_plane = (sphere.center[0] - triangle.v1[0]) * normalized_normal[0]
386        + (sphere.center[1] - triangle.v1[1]) * normalized_normal[1]
387        + (sphere.center[2] - triangle.v1[2]) * normalized_normal[2];
388
389    // If the sphere is too far from the plane, no collision
390    if dist_to_plane.abs() > sphere.radius {
391        return None;
392    }
393
394    // Project sphere center onto the triangle's plane
395    let projected_center = [
396        sphere.center[0] - dist_to_plane * normalized_normal[0],
397        sphere.center[1] - dist_to_plane * normalized_normal[1],
398        sphere.center[2] - dist_to_plane * normalized_normal[2],
399    ];
400
401    // Check if the projected center is inside the triangle
402    let inside_triangle =
403        super::narrowphase::point_triangle3d_collision(&projected_center, triangle);
404
405    // If the projected center is inside the triangle, collision normal is the plane normal
406    if inside_triangle {
407        let contact_normal = if dist_to_plane > 0.0 {
408            normalized_normal
409        } else {
410            [
411                -normalized_normal[0],
412                -normalized_normal[1],
413                -normalized_normal[2],
414            ]
415        };
416
417        return Some(CollisionInfo {
418            contact_point: [
419                sphere.center[0] - contact_normal[0] * sphere.radius,
420                sphere.center[1] - contact_normal[1] * sphere.radius,
421                sphere.center[2] - contact_normal[2] * sphere.radius,
422            ],
423            normal: contact_normal,
424            penetration: sphere.radius - dist_to_plane.abs(),
425        });
426    }
427
428    // If not inside, find the closest point on the triangle edges
429    let edges = [
430        (triangle.v1, triangle.v2),
431        (triangle.v2, triangle.v3),
432        (triangle.v3, triangle.v1),
433    ];
434
435    let mut closest_point = [0.0, 0.0, 0.0];
436    let mut min_distance = f64::INFINITY;
437
438    for &(start, end) in &edges {
439        // Vector from start to end of the edge
440        let edge_vec = [end[0] - start[0], end[1] - start[1], end[2] - start[2]];
441
442        // Vector from start of the edge to the sphere center
443        let sphere_vec = [
444            sphere.center[0] - start[0],
445            sphere.center[1] - start[1],
446            sphere.center[2] - start[2],
447        ];
448
449        // Calculate the length of the edge
450        let edge_length_squared =
451            edge_vec[0] * edge_vec[0] + edge_vec[1] * edge_vec[1] + edge_vec[2] * edge_vec[2];
452
453        // Calculate the projection of sphere_vec onto edge_vec
454        let t = (sphere_vec[0] * edge_vec[0]
455            + sphere_vec[1] * edge_vec[1]
456            + sphere_vec[2] * edge_vec[2])
457            / edge_length_squared;
458
459        // Clamp t to the edge
460        let t_clamped = t.clamp(0.0, 1.0);
461
462        // Calculate the closest point on the edge to the sphere center
463        let point = [
464            start[0] + t_clamped * edge_vec[0],
465            start[1] + t_clamped * edge_vec[1],
466            start[2] + t_clamped * edge_vec[2],
467        ];
468
469        // Calculate the distance from the closest point to the sphere center
470        let dx = sphere.center[0] - point[0];
471        let dy = sphere.center[1] - point[1];
472        let dz = sphere.center[2] - point[2];
473        let distance_squared = dx * dx + dy * dy + dz * dz;
474
475        if distance_squared < min_distance {
476            min_distance = distance_squared;
477            closest_point = point;
478        }
479    }
480
481    // Also check the vertices
482    let vertices = [triangle.v1, triangle.v2, triangle.v3];
483
484    for &vertex in &vertices {
485        let dx = sphere.center[0] - vertex[0];
486        let dy = sphere.center[1] - vertex[1];
487        let dz = sphere.center[2] - vertex[2];
488        let distance_squared = dx * dx + dy * dy + dz * dz;
489
490        if distance_squared < min_distance {
491            min_distance = distance_squared;
492            closest_point = vertex;
493        }
494    }
495
496    // Check if the closest point is within the sphere's radius
497    if min_distance > sphere.radius * sphere.radius {
498        return None;
499    }
500
501    let distance = min_distance.sqrt();
502    let normal = [
503        (sphere.center[0] - closest_point[0]) / distance,
504        (sphere.center[1] - closest_point[1]) / distance,
505        (sphere.center[2] - closest_point[2]) / distance,
506    ];
507
508    Some(CollisionInfo {
509        contact_point: closest_point,
510        normal,
511        penetration: sphere.radius - distance,
512    })
513}