use he3d_math::scalar::sqrt;
use he3d_math::{Quat, Vec3};
use crate::contact::{MAX_MANIFOLD_POINTS, Manifold, ManifoldPoint};
const MAX_POLYGON_POINTS: usize = 8;
const POLYGON_CORNER_COUNT: u32 = 4;
const CLIP_PLANE_COUNT: u32 = 4;
const INCIDENT_SIDE_SHIFT: u32 = 16;
const BOX_FACE_SIDE: u32 = 0;
const TRIANGLE_SIDE: u32 = 1;
fn tagged(feature: u32, side: u32) -> u32 {
feature | (side << INCIDENT_SIDE_SHIFT)
}
const BOX_BOX_EDGE_BIAS: f32 = 1.02;
const BOX_TRIANGLE_FACE_BIAS: f32 = 1.02;
const BOX_TRIANGLE_EDGE_BIAS: f32 = 1.05;
const AXIS_BIAS_SLACK: f32 = 1e-4;
const CONTACT_TOLERANCE: f32 = 1e-3;
const CLIP_ENDPOINT_MARGIN: f32 = 1e-4;
const VERTEX_WELD_DISTANCE: f32 = 1e-4;
const CLIP_PLANE_MARGIN: f32 = 1e-4;
#[derive(Clone, Copy, Debug)]
pub(crate) struct BoxFrame {
center: Vec3,
axes: [Vec3; 3],
half: Vec3,
}
impl BoxFrame {
pub(crate) fn new(center: Vec3, orientation: Quat, half: Vec3) -> Self {
Self {
center,
axes: [
orientation * Vec3::X,
orientation * Vec3::Y,
orientation * Vec3::Z,
],
half,
}
}
fn radius(&self, axis: Vec3) -> f32 {
self.axes[0].dot(axis).abs() * self.half.x
+ self.axes[1].dot(axis).abs() * self.half.y
+ self.axes[2].dot(axis).abs() * self.half.z
}
fn face(&self, axis: usize, sign: f32) -> [Vec3; 4] {
let normal = axis_of(self.axes, axis) * sign;
let face_center = self.center + normal * component(self.half, axis);
let first = axis_of(self.axes, axis + 1) * component(self.half, axis + 1);
let second = axis_of(self.axes, axis + 2) * component(self.half, axis + 2);
[
face_center + first + second,
face_center - first + second,
face_center - first - second,
face_center + first - second,
]
}
fn support(&self, direction: Vec3) -> Vec3 {
self.center
+ self.axes[0] * (self.half.x * sign_of(self.axes[0].dot(direction)))
+ self.axes[1] * (self.half.y * sign_of(self.axes[1].dot(direction)))
+ self.axes[2] * (self.half.z * sign_of(self.axes[2].dot(direction)))
}
fn edge_through(&self, corner: Vec3, axis: usize) -> [Vec3; 2] {
let direction = axis_of(self.axes, axis);
let signed_extent = (corner - self.center).dot(direction);
[corner, corner - direction * (2.0 * signed_extent)]
}
}
#[derive(Clone, Copy, Debug)]
struct Polygon {
points: [Vec3; MAX_POLYGON_POINTS],
features: [u32; MAX_POLYGON_POINTS],
count: usize,
}
impl Polygon {
fn new(points: &[Vec3]) -> Self {
let mut polygon = Self {
points: [Vec3::ZERO; MAX_POLYGON_POINTS],
features: [0; MAX_POLYGON_POINTS],
count: 0,
};
for (index, &point) in points.iter().enumerate() {
polygon.push(point, u32::try_from(index).unwrap_or(0));
}
polygon
}
fn push(&mut self, point: Vec3, feature: u32) {
if self.count < MAX_POLYGON_POINTS {
self.points[self.count] = point;
self.features[self.count] = feature;
self.count += 1;
}
}
fn as_slice(&self) -> &[Vec3] {
&self.points[..self.count]
}
fn feature_at(&self, index: usize) -> u32 {
self.features[index]
}
}
fn clipped_feature(from: u32, plane: u32) -> u32 {
POLYGON_CORNER_COUNT + from * CLIP_PLANE_COUNT + plane
}
fn clip_polygon(polygon: &Polygon, normal: Vec3, offset: f32, plane: u32) -> Polygon {
let mut output = Polygon {
points: [Vec3::ZERO; MAX_POLYGON_POINTS],
features: [0; MAX_POLYGON_POINTS],
count: 0,
};
let source = polygon.as_slice();
if source.is_empty() {
return output;
}
for index in 0..source.len() {
let current = source[index];
let current_feature = polygon.feature_at(index);
let next = source[(index + 1) % source.len()];
let current_distance = normal.dot(current) - offset;
let next_distance = normal.dot(next) - offset;
let current_inside = current_distance <= 0.0;
if current_inside {
output.push(current, current_feature);
}
if current_inside != (next_distance <= 0.0) {
let span = current_distance - next_distance;
if span.abs() > f32::EPSILON {
let t = current_distance / span;
if t > CLIP_ENDPOINT_MARGIN && t < 1.0 - CLIP_ENDPOINT_MARGIN {
output.push(
current + (next - current) * t,
clipped_feature(current_feature, plane),
);
}
}
}
}
output
}
#[derive(Clone, Copy, Debug)]
enum SatAxis {
FaceA(usize),
FaceB(usize),
Edge(usize, usize),
}
#[derive(Clone, Copy, Debug)]
struct Best {
overlap: f32,
axis: Option<SatAxis>,
}
enum AxisOutcome {
Separated,
Overlapping {
overlap: f32,
axis: SatAxis,
preferred: bool,
},
}
fn test_axis(
a: &BoxFrame,
b: &BoxFrame,
delta: Vec3,
axis: Vec3,
candidate: SatAxis,
is_edge: bool,
best_overlap: f32,
) -> AxisOutcome {
let overlap = a.radius(axis) + b.radius(axis) - delta.dot(axis).abs();
if overlap <= 0.0 {
return AxisOutcome::Separated;
}
let biased = if is_edge {
overlap * BOX_BOX_EDGE_BIAS + AXIS_BIAS_SLACK
} else {
overlap
};
AxisOutcome::Overlapping {
overlap,
axis: candidate,
preferred: biased < best_overlap,
}
}
fn box_box(a: &BoxFrame, b: &BoxFrame) -> Option<Manifold> {
let delta = b.center - a.center;
let mut best = Best {
overlap: f32::MAX,
axis: None,
};
for index in 0..3 {
match test_axis(
a,
b,
delta,
a.axes[index],
SatAxis::FaceA(index),
false,
best.overlap,
) {
AxisOutcome::Separated => return None,
AxisOutcome::Overlapping {
overlap,
axis,
preferred,
} => {
if preferred {
best = Best {
overlap,
axis: Some(axis),
};
}
}
}
}
for index in 0..3 {
match test_axis(
a,
b,
delta,
b.axes[index],
SatAxis::FaceB(index),
false,
best.overlap,
) {
AxisOutcome::Separated => return None,
AxisOutcome::Overlapping {
overlap,
axis,
preferred,
} => {
if preferred {
best = Best {
overlap,
axis: Some(axis),
};
}
}
}
}
for i in 0..3 {
for j in 0..3 {
let raw = a.axes[i].cross(b.axes[j]);
let length = raw.length();
if length < 1e-5 {
continue;
}
match test_axis(
a,
b,
delta,
raw / length,
SatAxis::Edge(i, j),
true,
best.overlap,
) {
AxisOutcome::Separated => return None,
AxisOutcome::Overlapping {
overlap,
axis,
preferred,
} => {
if preferred {
best = Best {
overlap,
axis: Some(axis),
};
}
}
}
}
}
let axis = best.axis?;
Some(build_manifold(a, b, delta, axis))
}
fn build_manifold(a: &BoxFrame, b: &BoxFrame, delta: Vec3, axis: SatAxis) -> Manifold {
match axis {
SatAxis::FaceA(index) => {
let sign = sign_of(delta.dot(a.axes[index]));
face_manifold(a, index, sign, b, false)
}
SatAxis::FaceB(index) => {
let sign = sign_of(delta.dot(b.axes[index]));
face_manifold(b, index, -sign, a, true)
}
SatAxis::Edge(i, j) => edge_manifold(a, i, b, j, delta),
}
}
fn face_manifold(
reference: &BoxFrame,
reference_axis: usize,
reference_sign: f32,
incident: &BoxFrame,
flip: bool,
) -> Manifold {
let reference_normal = axis_of(reference.axes, reference_axis) * reference_sign;
let reference_face = reference.face(reference_axis, reference_sign);
let mut incident_axis = 0;
let mut largest = -1.0_f32;
for index in 0..3 {
let alignment = axis_of(incident.axes, index).dot(reference_normal).abs();
if alignment > largest {
largest = alignment;
incident_axis = index;
}
}
let incident_sign = -sign_of(axis_of(incident.axes, incident_axis).dot(reference_normal));
let incident_face = incident.face(incident_axis, incident_sign);
let mut polygon = Polygon::new(&incident_face);
for step in 1..3 {
let side_axis = (reference_axis + step) % 3;
let direction = axis_of(reference.axes, side_axis);
let extent = component(reference.half, side_axis);
let centre_offset = direction.dot(reference.center);
let plane = u32::try_from((step - 1) * 2).unwrap_or(0);
polygon = clip_polygon(
&polygon,
direction,
centre_offset + extent + CLIP_PLANE_MARGIN,
plane,
);
polygon = clip_polygon(
&polygon,
-direction,
-centre_offset + extent + CLIP_PLANE_MARGIN,
plane + 1,
);
}
let plane_offset = reference_normal.dot(reference_face[0]);
let mut collected = [ManifoldPoint::default(); MAX_MANIFOLD_POINTS];
let mut count = 0;
for (&point, &feature) in polygon.points[..polygon.count]
.iter()
.zip(&polygon.features[..polygon.count])
{
let separation = reference_normal.dot(point) - plane_offset;
if separation > CONTACT_TOLERANCE || count >= MAX_MANIFOLD_POINTS {
continue;
}
let duplicate = collected[..count].iter().any(|existing| {
(existing.position - point).length_squared() < VERTEX_WELD_DISTANCE * VERTEX_WELD_DISTANCE
});
if duplicate {
continue;
}
collected[count] = ManifoldPoint {
position: point,
depth: (-separation).max(0.0),
feature: tagged(feature, u32::from(flip)),
};
count += 1;
}
collected[..count].sort_unstable_by(|left, right| {
left
.position
.x
.total_cmp(&right.position.x)
.then(left.position.z.total_cmp(&right.position.z))
.then(left.position.y.total_cmp(&right.position.y))
});
let mut manifold = Manifold {
normal: if flip {
-reference_normal
} else {
reference_normal
},
..Manifold::empty()
};
for point in collected[..count].iter().copied() {
manifold.push(point);
}
manifold
}
fn edge_manifold(
a: &BoxFrame,
axis_a: usize,
b: &BoxFrame,
axis_b: usize,
delta: Vec3,
) -> Manifold {
let mut normal = a.axes[axis_a].cross(b.axes[axis_b]);
if normal.length_squared() <= 0.0 {
return Manifold::empty();
}
normal = normal.normalize();
if normal.dot(delta) < 0.0 {
normal = -normal;
}
let corner_a = a.support(normal);
let corner_b = b.support(-normal);
let edge_a = a.edge_through(corner_a, axis_a);
let edge_b = b.edge_through(corner_b, axis_b);
let (point_a, point_b) = closest_points(edge_a, edge_b);
let overlap = a.radius(normal) + b.radius(normal) - delta.dot(normal).abs();
let mut manifold = Manifold {
normal,
..Manifold::empty()
};
if overlap > 0.0 {
manifold.push(ManifoldPoint {
position: (point_a + point_b) * 0.5,
depth: overlap,
feature: feature_id(1, axis_a, axis_b, 0),
});
}
manifold
}
fn closest_points(first: [Vec3; 2], second: [Vec3; 2]) -> (Vec3, Vec3) {
let first_delta = first[1] - first[0];
let second_delta = second[1] - second[0];
let between = first[0] - second[0];
let first_length_squared = first_delta.dot(first_delta);
let second_length_squared = second_delta.dot(second_delta);
let second_projection = second_delta.dot(between);
if first_length_squared <= f32::EPSILON && second_length_squared <= f32::EPSILON {
return (first[0], second[0]);
}
if first_length_squared <= f32::EPSILON {
let along_second = (second_projection / second_length_squared).clamp(0.0, 1.0);
return (first[0], second[0] + second_delta * along_second);
}
let first_projection = first_delta.dot(between);
let cross_projection = first_delta.dot(second_delta);
let denominator =
first_length_squared * second_length_squared - cross_projection * cross_projection;
let mut along_first = if denominator.abs() > f32::EPSILON {
((cross_projection * second_projection - first_projection * second_length_squared)
/ denominator)
.clamp(0.0, 1.0)
} else {
0.0
};
let mut along_second = if second_length_squared <= f32::EPSILON {
0.0
} else {
(cross_projection * along_first + second_projection) / second_length_squared
};
if along_second < 0.0 {
along_second = 0.0;
along_first = (-first_projection / first_length_squared).clamp(0.0, 1.0);
} else if along_second > 1.0 {
along_second = 1.0;
along_first = ((cross_projection - first_projection) / first_length_squared).clamp(0.0, 1.0);
}
(
first[0] + first_delta * along_first,
second[0] + second_delta * along_second,
)
}
fn sphere_box(
sphere_center: Vec3,
sphere_radius: f32,
box_frame: &BoxFrame,
sphere_first: bool,
) -> Option<Manifold> {
let orientation = quat_from_axes(box_frame.axes);
let local = orientation.inverse() * (sphere_center - box_frame.center);
let clamped = local.clamp(-box_frame.half, box_frame.half);
let outside = local - clamped;
let distance_squared = outside.length_squared();
let (local_normal, depth) = if distance_squared > f32::EPSILON {
let distance = sqrt(distance_squared);
if distance > sphere_radius {
return None;
}
(outside / distance, sphere_radius - distance)
} else {
let mut axis = 0;
let mut best = f32::MAX;
for index in 0..3 {
let distance = component(box_frame.half, index) - component(local, index).abs();
if distance < best {
best = distance;
axis = index;
}
}
let sign = sign_of(component(local, axis));
let mut normal = Vec3::ZERO;
set_component(&mut normal, axis, sign);
(normal, sphere_radius + best)
};
let outward = box_frame.axes[0] * local_normal.x
+ box_frame.axes[1] * local_normal.y
+ box_frame.axes[2] * local_normal.z;
let normal = if sphere_first { -outward } else { outward };
let position = sphere_center - outward * (sphere_radius - depth * 0.5);
let mut manifold = Manifold {
normal,
..Manifold::empty()
};
manifold.push(ManifoldPoint {
position,
depth,
feature: feature_id(2, 0, 0, 0),
});
Some(manifold)
}
fn sphere_sphere(center_a: Vec3, radius_a: f32, center_b: Vec3, radius_b: f32) -> Option<Manifold> {
let delta = center_b - center_a;
let distance_squared = delta.length_squared();
let reach = radius_a + radius_b;
if distance_squared > reach * reach {
return None;
}
let distance = sqrt(distance_squared);
let normal = if distance > 1e-6 {
delta / distance
} else {
Vec3::Y
};
let depth = reach - distance;
let mut manifold = Manifold {
normal,
..Manifold::empty()
};
manifold.push(ManifoldPoint {
position: center_a + normal * (radius_a - depth * 0.5),
depth,
feature: feature_id(3, 0, 0, 0),
});
Some(manifold)
}
pub(crate) fn collide(
shape_a: crate::shape::Shape,
position_a: Vec3,
orientation_a: Quat,
shape_b: crate::shape::Shape,
position_b: Vec3,
orientation_b: Quat,
) -> Option<Manifold> {
use crate::shape::Shape;
match (shape_a, shape_b) {
(Shape::Sphere { radius: ra }, Shape::Sphere { radius: rb }) => {
sphere_sphere(position_a, ra, position_b, rb)
}
(Shape::Sphere { radius }, Shape::Box { half_extents }) => {
let frame = BoxFrame::new(position_b, orientation_b, half_extents);
sphere_box(position_a, radius, &frame, true)
}
(Shape::Box { half_extents }, Shape::Sphere { radius }) => {
let frame = BoxFrame::new(position_a, orientation_a, half_extents);
sphere_box(position_b, radius, &frame, false)
}
(
Shape::Box {
half_extents: half_a,
},
Shape::Box {
half_extents: half_b,
},
) => {
let frame_a = BoxFrame::new(position_a, orientation_a, half_a);
let frame_b = BoxFrame::new(position_b, orientation_b, half_b);
box_box(&frame_a, &frame_b)
}
}
}
fn quat_from_axes(axes: [Vec3; 3]) -> Quat {
Quat::from_mat3(&he3d_math::glam::Mat3::from_cols(axes[0], axes[1], axes[2]))
}
fn component(vector: Vec3, index: usize) -> f32 {
match index % 3 {
0 => vector.x,
1 => vector.y,
_ => vector.z,
}
}
fn axis_of(axes: [Vec3; 3], index: usize) -> Vec3 {
axes[index % 3]
}
fn set_component(vector: &mut Vec3, index: usize, value: f32) {
match index % 3 {
0 => vector.x = value,
1 => vector.y = value,
_ => vector.z = value,
}
}
fn sign_of(value: f32) -> f32 {
if value >= 0.0 { 1.0 } else { -1.0 }
}
fn feature_id(kind: u32, first: usize, second: usize, index: usize) -> u32 {
let first = u32::try_from(first).unwrap_or(0) & 7;
let second = u32::try_from(second).unwrap_or(0) & 7;
let index = u32::try_from(index).unwrap_or(0) & 7;
(kind << 9) | (first << 6) | (second << 3) | index
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct TriangleHit {
pub manifold: Manifold,
pub source_edge: Option<usize>,
}
#[derive(Clone, Copy, Debug)]
enum TriangleAxis {
BoxFace(usize),
Edge(usize, Vec3),
}
pub(crate) fn box_triangle(box_frame: &BoxFrame, triangle: [Vec3; 3]) -> Option<TriangleHit> {
let edges = [
triangle[1] - triangle[0],
triangle[2] - triangle[1],
triangle[0] - triangle[2],
];
let raw_normal = edges[0].cross(edges[1]);
let normal_length = raw_normal.length();
if normal_length <= 1e-12 {
return None;
}
let triangle_normal = raw_normal / normal_length;
let face_overlap = triangle_overlap(box_frame, &triangle, triangle_normal);
if face_overlap < 0.0 {
return None;
}
let face_manifold = triangle_face_manifold(box_frame, &triangle, triangle_normal);
if face_manifold.count > 0 {
return Some(TriangleHit {
manifold: face_manifold,
source_edge: None,
});
}
let mut best_biased = f32::MAX;
let mut best_overlap = 0.0_f32;
let mut best = TriangleAxis::BoxFace(0);
for index in 0..3 {
let overlap = triangle_overlap(box_frame, &triangle, box_frame.axes[index]);
if overlap < 0.0 {
return None;
}
let biased = overlap * BOX_TRIANGLE_FACE_BIAS + AXIS_BIAS_SLACK;
if biased < best_biased {
best_biased = biased;
best_overlap = overlap;
best = TriangleAxis::BoxFace(index);
}
}
for index in 0..3 {
for (edge_index, edge) in edges.iter().enumerate() {
let raw = box_frame.axes[index].cross(*edge);
let length = raw.length();
if length < 1e-5 {
continue;
}
let axis = raw / length;
let overlap = triangle_overlap(box_frame, &triangle, axis);
if overlap < 0.0 {
return None;
}
let biased = overlap * BOX_TRIANGLE_EDGE_BIAS + AXIS_BIAS_SLACK;
if biased < best_biased {
best_biased = biased;
best_overlap = overlap;
best = TriangleAxis::Edge(edge_index, axis);
}
}
}
let manifold = match best {
TriangleAxis::BoxFace(index) => box_face_manifold(box_frame, &triangle, index),
TriangleAxis::Edge(edge_index, axis) => {
edge_triangle_manifold(box_frame, &triangle, edge_index, axis, best_overlap)
}
};
Some(TriangleHit {
manifold,
source_edge: match best {
TriangleAxis::Edge(edge_index, _) => Some(edge_index),
TriangleAxis::BoxFace(_) => None,
},
})
}
fn triangle_overlap(box_frame: &BoxFrame, triangle: &[Vec3; 3], axis: Vec3) -> f32 {
let box_center = box_frame.center.dot(axis);
let box_radius = box_frame.radius(axis);
let mut triangle_min = f32::MAX;
let mut triangle_max = f32::MIN;
for vertex in triangle {
let projected = vertex.dot(axis);
triangle_min = triangle_min.min(projected);
triangle_max = triangle_max.max(projected);
}
(box_center + box_radius - triangle_min).min(triangle_max - (box_center - box_radius))
}
fn triangle_edge_planes(triangle: &[Vec3; 3], normal: Vec3) -> [(Vec3, f32); 3] {
let centroid = (triangle[0] + triangle[1] + triangle[2]) / 3.0;
let mut planes = [(Vec3::ZERO, 0.0); 3];
for index in 0..3 {
let first = triangle[index];
let second = triangle[(index + 1) % 3];
let raw = normal.cross(second - first);
let length = raw.length();
let mut plane_normal = if length > 1e-12 {
raw / length
} else {
Vec3::ZERO
};
let mut offset = plane_normal.dot(first);
if plane_normal.dot(centroid) > offset {
plane_normal = -plane_normal;
offset = -offset;
}
planes[index] = (plane_normal, offset);
}
planes
}
fn manifold_from(
mut points: [ManifoldPoint; MAX_MANIFOLD_POINTS],
count: usize,
normal: Vec3,
) -> Manifold {
let live = &mut points[..count];
live.sort_unstable_by(|left, right| {
left
.position
.x
.total_cmp(&right.position.x)
.then(left.position.z.total_cmp(&right.position.z))
.then(left.position.y.total_cmp(&right.position.y))
});
let mut manifold = Manifold {
normal,
..Manifold::empty()
};
for point in live.iter().copied() {
let duplicate = manifold.points[..manifold.count].iter().any(|existing| {
(existing.position - point.position).length_squared()
< VERTEX_WELD_DISTANCE * VERTEX_WELD_DISTANCE
});
if duplicate {
continue;
}
manifold.push(point);
}
manifold
}
fn triangle_face_manifold(
box_frame: &BoxFrame,
triangle: &[Vec3; 3],
triangle_normal: Vec3,
) -> Manifold {
let triangle_center = (triangle[0] + triangle[1] + triangle[2]) / 3.0;
let up = if triangle_normal.dot(box_frame.center - triangle_center) < 0.0 {
-triangle_normal
} else {
triangle_normal
};
let mut face_axis = 0;
let mut largest = -1.0_f32;
for index in 0..3 {
let alignment = box_frame.axes[index].dot(up).abs();
if alignment > largest {
largest = alignment;
face_axis = index;
}
}
let face_sign = -sign_of(box_frame.axes[face_axis].dot(up));
let face = box_frame.face(face_axis, face_sign);
let mut polygon = Polygon::new(&face);
for (index, (plane_normal, offset)) in triangle_edge_planes(triangle, triangle_normal)
.into_iter()
.enumerate()
{
if plane_normal.length_squared() > 0.0 {
let plane = u32::try_from(index).unwrap_or(0);
polygon = clip_polygon(&polygon, plane_normal, offset, plane);
}
}
let plane_offset = up.dot(triangle[0]);
let mut collected = [ManifoldPoint::default(); MAX_MANIFOLD_POINTS];
let mut count = 0;
for (&point, &feature) in polygon.points[..polygon.count]
.iter()
.zip(&polygon.features[..polygon.count])
{
let separation = up.dot(point) - plane_offset;
if separation > CONTACT_TOLERANCE || count >= MAX_MANIFOLD_POINTS {
continue;
}
collected[count] = ManifoldPoint {
position: point,
depth: (-separation).max(0.0),
feature: tagged(feature, BOX_FACE_SIDE),
};
count += 1;
}
manifold_from(collected, count, -up)
}
fn box_face_manifold(box_frame: &BoxFrame, triangle: &[Vec3; 3], face_axis: usize) -> Manifold {
let triangle_center = (triangle[0] + triangle[1] + triangle[2]) / 3.0;
let axis = box_frame.axes[face_axis];
let sign = sign_of(axis.dot(triangle_center - box_frame.center));
let reference_normal = axis * sign;
let reference_face = box_frame.face(face_axis, sign);
let mut polygon = Polygon::new(triangle);
for step in 1..3 {
let side_axis = (face_axis + step) % 3;
let direction = box_frame.axes[side_axis];
let extent = component(box_frame.half, side_axis);
let centre_offset = direction.dot(box_frame.center);
let plane = u32::try_from((step - 1) * 2).unwrap_or(0);
polygon = clip_polygon(
&polygon,
direction,
centre_offset + extent + CLIP_PLANE_MARGIN,
plane,
);
polygon = clip_polygon(
&polygon,
-direction,
-centre_offset + extent + CLIP_PLANE_MARGIN,
plane + 1,
);
}
let plane_offset = reference_normal.dot(reference_face[0]);
let mut collected = [ManifoldPoint::default(); MAX_MANIFOLD_POINTS];
let mut count = 0;
for (&point, &feature) in polygon.points[..polygon.count]
.iter()
.zip(&polygon.features[..polygon.count])
{
let separation = reference_normal.dot(point) - plane_offset;
if separation > CONTACT_TOLERANCE || count >= MAX_MANIFOLD_POINTS {
continue;
}
collected[count] = ManifoldPoint {
position: point,
depth: (-separation).max(0.0),
feature: tagged(feature, TRIANGLE_SIDE),
};
count += 1;
}
manifold_from(collected, count, reference_normal)
}
fn edge_triangle_manifold(
box_frame: &BoxFrame,
triangle: &[Vec3; 3],
edge_index: usize,
axis: Vec3,
overlap: f32,
) -> Manifold {
let triangle_center = (triangle[0] + triangle[1] + triangle[2]) / 3.0;
let normal = if axis.dot(triangle_center - box_frame.center) < 0.0 {
-axis
} else {
axis
};
let edge_direction = triangle[(edge_index + 1) % 3] - triangle[edge_index];
let mut best_box_axis = 0;
let mut best_alignment = f32::MAX;
for index in 0..3 {
let alignment = box_frame.axes[index].cross(edge_direction).length();
if alignment < best_alignment {
best_alignment = alignment;
best_box_axis = index;
}
}
let box_corner = box_frame.support(-normal);
let box_edge = box_frame.edge_through(box_corner, best_box_axis);
let triangle_edge = [triangle[edge_index], triangle[(edge_index + 1) % 3]];
let (point_on_box, point_on_triangle) = closest_points(box_edge, triangle_edge);
let mut manifold = Manifold {
normal,
..Manifold::empty()
};
if overlap > 0.0 {
manifold.push(ManifoldPoint {
position: (point_on_box + point_on_triangle) * 0.5,
depth: overlap,
feature: 0,
});
}
manifold
}
pub(crate) fn sphere_triangle(centre: Vec3, radius: f32, triangle: [Vec3; 3]) -> Option<Manifold> {
let closest = closest_point_on_triangle(centre, triangle[0], triangle[1], triangle[2]);
let offset = centre - closest;
let distance_squared = offset.length_squared();
if distance_squared > radius * radius {
return None;
}
let distance = sqrt(distance_squared);
let toward_sphere = if distance > 1e-6 {
offset / distance
} else {
let mut normal = (triangle[1] - triangle[0]).cross(triangle[2] - triangle[0]);
let length = normal.length();
if length <= 1e-12 {
return None;
}
normal /= length;
if normal.dot(centre - triangle[0]) < 0.0 {
-normal
} else {
normal
}
};
let mut manifold = Manifold {
normal: -toward_sphere,
..Manifold::empty()
};
manifold.push(ManifoldPoint {
position: closest,
depth: radius - distance,
feature: 0,
});
Some(manifold)
}
#[expect(clippy::similar_names)]
fn closest_point_on_triangle(point: Vec3, first: Vec3, second: Vec3, third: Vec3) -> Vec3 {
let edge_ab = second - first;
let edge_ac = third - first;
let to_point = point - first;
let d1 = edge_ab.dot(to_point);
let d2 = edge_ac.dot(to_point);
if d1 <= 0.0 && d2 <= 0.0 {
return first;
}
let to_point_b = point - second;
let d3 = edge_ab.dot(to_point_b);
let d4 = edge_ac.dot(to_point_b);
if d3 >= 0.0 && d4 <= d3 {
return second;
}
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
let v = d1 / (d1 - d3);
return first + edge_ab * v;
}
let to_point_c = point - third;
let d5 = edge_ab.dot(to_point_c);
let d6 = edge_ac.dot(to_point_c);
if d6 >= 0.0 && d5 <= d6 {
return third;
}
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
let w = d2 / (d2 - d6);
return first + edge_ac * w;
}
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
return second + (third - second) * w;
}
let denominator = va + vb + vc;
if denominator.abs() <= f32::EPSILON {
return first;
}
let v = vb / denominator;
let w = vc / denominator;
first + edge_ab * v + edge_ac * w
}
#[cfg(test)]
mod tests {
use super::{BoxFrame, CLIP_PLANE_COUNT, POLYGON_CORNER_COUNT, clipped_feature, collide};
use crate::shape::Shape;
use he3d_math::scalar::{cos, sin};
use he3d_math::{Quat, Vec3};
fn boxed(half: Vec3) -> Shape {
Shape::cuboid(half)
}
#[test]
fn spheres_that_are_apart_do_not_collide() {
let result = collide(
Shape::sphere(0.5),
Vec3::ZERO,
Quat::IDENTITY,
Shape::sphere(0.5),
Vec3::new(2.0, 0.0, 0.0),
Quat::IDENTITY,
);
assert!(result.is_none());
}
#[test]
fn overlapping_spheres_report_depth_and_normal() {
let manifold = collide(
Shape::sphere(0.5),
Vec3::ZERO,
Quat::IDENTITY,
Shape::sphere(0.5),
Vec3::new(0.8, 0.0, 0.0),
Quat::IDENTITY,
)
.expect("spheres overlap");
assert_eq!(manifold.count, 1);
assert!((manifold.normal.x - 1.0).abs() < 1e-5);
assert!((manifold.points[0].depth - 0.2).abs() < 1e-5);
}
#[test]
fn touching_spheres_report_zero_depth() {
let manifold = collide(
Shape::sphere(0.5),
Vec3::ZERO,
Quat::IDENTITY,
Shape::sphere(0.5),
Vec3::new(1.0, 0.0, 0.0),
Quat::IDENTITY,
)
.expect("spheres touch");
assert!(manifold.points[0].depth.abs() < 1e-5);
}
#[test]
fn a_sphere_resting_on_a_box_leans_upward() {
let manifold = collide(
Shape::sphere(0.5),
Vec3::new(0.0, 1.2, 0.0),
Quat::IDENTITY,
boxed(Vec3::new(2.0, 1.0, 2.0)),
Vec3::ZERO,
Quat::IDENTITY,
)
.expect("sphere overlaps the box top");
assert!(manifold.normal.y < 0.0, "got {:?}", manifold.normal);
assert!((manifold.points[0].depth - 0.3).abs() < 1e-4);
}
#[test]
fn a_sphere_inside_a_box_escapes_through_the_nearest_face() {
let manifold = collide(
Shape::sphere(0.1),
Vec3::new(0.0, 0.9, 0.0),
Quat::IDENTITY,
boxed(Vec3::splat(1.0)),
Vec3::ZERO,
Quat::IDENTITY,
)
.expect("sphere is inside the box");
assert!(manifold.normal.y < 0.0, "got {:?}", manifold.normal);
assert!(manifold.points[0].depth > 0.0);
}
#[test]
fn separated_boxes_do_not_collide() {
let result = collide(
boxed(Vec3::splat(0.5)),
Vec3::ZERO,
Quat::IDENTITY,
boxed(Vec3::splat(0.5)),
Vec3::new(2.0, 0.0, 0.0),
Quat::IDENTITY,
);
assert!(result.is_none());
}
#[test]
fn stacked_boxes_produce_a_four_point_face_manifold() {
let manifold = collide(
boxed(Vec3::splat(0.5)),
Vec3::ZERO,
Quat::IDENTITY,
boxed(Vec3::splat(0.5)),
Vec3::new(0.0, 0.9, 0.0),
Quat::IDENTITY,
)
.expect("boxes overlap");
assert_eq!(manifold.count, 4, "a face contact needs four points");
assert!(manifold.normal.y > 0.0, "got {:?}", manifold.normal);
for point in &manifold.points[..manifold.count] {
assert!((point.depth - 0.1).abs() < 1e-4, "got {}", point.depth);
}
}
#[test]
fn a_rotated_box_pair_still_reports_contact() {
let manifold = collide(
boxed(Vec3::splat(0.5)),
Vec3::ZERO,
Quat::from_rotation_z(core::f32::consts::FRAC_PI_4),
boxed(Vec3::splat(0.5)),
Vec3::new(0.0, 1.1, 0.0),
Quat::IDENTITY,
);
let manifold = manifold.expect("rotated boxes overlap");
assert!(manifold.count >= 1);
assert!(manifold.normal.y > 0.0, "got {:?}", manifold.normal);
}
#[test]
fn a_deeply_overlapping_pair_is_resolved_along_one_axis() {
let manifold = collide(
boxed(Vec3::splat(0.5)),
Vec3::ZERO,
Quat::IDENTITY,
boxed(Vec3::splat(0.5)),
Vec3::new(0.9, 0.0, 0.0),
Quat::IDENTITY,
)
.expect("boxes overlap");
assert!(
(manifold.normal.x.abs() - 1.0).abs() < 1e-4,
"expected the X axis, got {:?}",
manifold.normal
);
}
#[test]
fn a_box_resting_on_a_triangle_reports_a_face_contact() {
let triangle = [
Vec3::new(-10.0, 0.0, -10.0),
Vec3::new(10.0, 0.0, -10.0),
Vec3::new(0.0, 0.0, 10.0),
];
let frame = BoxFrame::new(Vec3::new(0.0, 0.45, 0.0), Quat::IDENTITY, Vec3::splat(0.5));
let hit = super::box_triangle(&frame, triangle).expect("the box overlaps the triangle");
assert_eq!(hit.source_edge, None, "a flat rest is a face contact");
assert_eq!(hit.manifold.count, 4, "all four box corners touch");
assert!(
hit.manifold.normal.y < 0.0,
"the normal points from the box toward the triangle, got {:?}",
hit.manifold.normal
);
for point in &hit.manifold.points[..hit.manifold.count] {
assert!(
(point.depth - 0.05).abs() < 1e-3,
"got depth {}",
point.depth
);
}
}
#[test]
fn a_box_above_a_triangle_does_not_collide() {
let triangle = [
Vec3::new(-10.0, 0.0, -10.0),
Vec3::new(-10.0, 0.0, 10.0),
Vec3::new(10.0, 0.0, -10.0),
];
let frame = BoxFrame::new(Vec3::new(0.0, 5.0, 0.0), Quat::IDENTITY, Vec3::splat(0.5));
assert!(super::box_triangle(&frame, triangle).is_none());
}
#[test]
fn a_box_beside_a_triangle_does_not_collide() {
let triangle = [
Vec3::new(-1.0, 0.0, -1.0),
Vec3::new(-1.0, 0.0, 1.0),
Vec3::new(1.0, 0.0, -1.0),
];
let frame = BoxFrame::new(Vec3::new(20.0, 0.0, 0.0), Quat::IDENTITY, Vec3::splat(0.5));
assert!(super::box_triangle(&frame, triangle).is_none());
}
#[test]
fn a_box_baulks_on_a_small_triangle_beneath_it() {
let triangle = [
Vec3::new(-0.1, 0.0, -0.1),
Vec3::new(-0.1, 0.0, 0.1),
Vec3::new(0.1, 0.0, -0.1),
];
let frame = BoxFrame::new(Vec3::new(0.0, 0.4, 0.0), Quat::IDENTITY, Vec3::splat(0.5));
let hit = super::box_triangle(&frame, triangle).expect("the triangle is inside the box");
assert!(hit.manifold.count >= 1);
assert!(hit.manifold.normal.y < 0.0, "got {:?}", hit.manifold.normal);
}
#[test]
fn a_degenerate_triangle_never_collides() {
let triangle = [
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(2.0, 0.0, 0.0),
];
let frame = BoxFrame::new(Vec3::ZERO, Quat::IDENTITY, Vec3::splat(0.5));
assert!(super::box_triangle(&frame, triangle).is_none());
}
#[test]
fn box_frames_project_correctly_onto_their_own_axes() {
let frame = BoxFrame::new(Vec3::ZERO, Quat::IDENTITY, Vec3::new(1.0, 2.0, 3.0));
assert!((frame.radius(Vec3::X) - 1.0).abs() < 1e-6);
assert!((frame.radius(Vec3::Y) - 2.0).abs() < 1e-6);
assert!((frame.radius(Vec3::Z) - 3.0).abs() < 1e-6);
let diagonal = Vec3::new(1.0, 1.0, 0.0).normalize();
let expected = (1.0 + 2.0) / 2.0_f32.sqrt();
assert!((frame.radius(diagonal) - expected).abs() < 1e-5);
}
#[test]
fn a_face_polygon_is_wound_consistently() {
let frame = BoxFrame::new(Vec3::ZERO, Quat::IDENTITY, Vec3::splat(1.0));
let top = frame.face(1, 1.0);
for corner in top {
assert!((corner.y - 1.0).abs() < 1e-6);
}
}
#[test]
fn a_cut_vertex_is_named_by_the_geometry_that_made_it() {
let mut seen = [false; 64];
for corner in 0..POLYGON_CORNER_COUNT {
seen[corner as usize] = true;
}
for from in 0..4 {
for plane in 0..CLIP_PLANE_COUNT {
let feature = clipped_feature(from, plane);
assert!(
feature >= POLYGON_CORNER_COUNT,
"a crossing must not take a corner's name: {feature}"
);
let feature = feature as usize;
assert!(
feature < seen.len() && !seen[feature],
"feature {feature} from vertex {from} and plane {plane} collides"
);
seen[feature] = true;
}
}
}
fn tilted_cube_on_slab(angle: f32, penetration: f32) -> crate::contact::Manifold {
let slab_top = 0.5;
let center = slab_top + 0.5 * cos(angle) + 0.5 * sin(angle).abs() - penetration;
collide(
boxed(Vec3::new(5.0, 0.5, 5.0)),
Vec3::ZERO,
Quat::IDENTITY,
boxed(Vec3::splat(0.5)),
Vec3::new(0.0, center, 0.0),
Quat::from_rotation_z(angle),
)
.expect("the cube is pressed into the slab")
}
fn sideways(
first: &crate::contact::ManifoldPoint,
second: &crate::contact::ManifoldPoint,
) -> f32 {
((first.position.x - second.position.x).powi(2)
+ (first.position.z - second.position.z).powi(2))
.sqrt()
}
#[test]
fn a_contact_corner_keeps_its_identity_when_the_manifold_shrinks() {
for tilt in [0.002_f32, -0.002] {
let flat = tilted_cube_on_slab(tilt, 0.005);
let edged = tilted_cube_on_slab(tilt * 10.0, 0.005);
assert_eq!(
flat.count, 4,
"tilt {tilt}: a flat tilt puts four corners down"
);
assert_eq!(
edged.count, 2,
"tilt {tilt}: an edge tilt puts two corners down"
);
for point in &edged.points[..edged.count] {
let counterpart = flat.points[..flat.count]
.iter()
.min_by(|left, right| sideways(left, point).total_cmp(&sideways(right, point)))
.expect("the flat manifold has points");
assert!(
sideways(counterpart, point) < 0.1,
"the edged point at {:?} has no counterpart on the flat face",
point.position
);
assert_eq!(
counterpart.feature, point.feature,
"the corner at {:?} was renamed as the manifold shrank",
point.position
);
}
}
}
}