use crate::geometry::Geometry;
use crate::gjk::GjkOutcome;
use phyz_math::{Mat3, Vec3};
pub const MAX_MANIFOLD_POINTS: usize = 4;
#[derive(Debug, Clone, Copy)]
pub struct ManifoldPoint {
pub position: Vec3,
pub depth: f64,
}
#[derive(Debug, Clone)]
pub struct Manifold {
pub normal: Vec3,
pub points: Vec<ManifoldPoint>,
}
pub fn contact_manifold(
geom_a: &Geometry,
geom_b: &Geometry,
pos_a: &Vec3,
rot_a: &Mat3,
geom_b_pos: &Vec3,
rot_b: &Mat3,
) -> Option<Manifold> {
contact_manifold_within(geom_a, geom_b, pos_a, rot_a, geom_b_pos, rot_b, 0.0)
}
pub fn contact_manifold_within(
geom_a: &Geometry,
geom_b: &Geometry,
pos_a: &Vec3,
rot_a: &Mat3,
geom_b_pos: &Vec3,
rot_b: &Mat3,
margin: f64,
) -> Option<Manifold> {
let pos_b = geom_b_pos;
let margin = if margin.is_finite() {
margin.max(0.0)
} else {
0.0
};
let (depth, normal) = match crate::gjk::gjk_rot(geom_a, geom_b, pos_a, pos_b, rot_a, rot_b) {
GjkOutcome::Penetrating { simplex } => {
crate::epa::epa_from_simplex(geom_a, geom_b, pos_a, pos_b, rot_a, rot_b, &simplex)?
}
GjkOutcome::Separated { distance, closest } => {
if distance.is_nan() || distance >= margin {
return None;
}
let n = closest.norm();
if n < 1e-12 {
return None;
}
(-distance, -closest / n)
}
GjkOutcome::Indeterminate => return None,
};
if !depth.is_finite() || !is_finite(&normal) || normal.norm() < 0.5 {
return None;
}
let face_a = support_face(geom_a, pos_a, rot_a, &normal);
let face_b = support_face(geom_b, pos_b, rot_b, &(-normal));
let points = match (face_a, face_b) {
(Some(ref_face), Some(inc_face)) => clip_faces(&ref_face, &inc_face, &normal, margin)
.unwrap_or_else(|| {
vec![single_point(
geom_a, geom_b, pos_a, rot_a, pos_b, rot_b, None, None, &normal, depth,
)]
}),
(face_a, face_b) => vec![single_point(
geom_a,
geom_b,
pos_a,
rot_a,
pos_b,
rot_b,
face_a.as_ref(),
face_b.as_ref(),
&normal,
depth,
)],
};
let points = reduce(points);
if points.is_empty() {
return None;
}
Some(Manifold { normal, points })
}
#[allow(clippy::too_many_arguments)]
fn single_point(
geom_a: &Geometry,
geom_b: &Geometry,
pos_a: &Vec3,
rot_a: &Mat3,
pos_b: &Vec3,
rot_b: &Mat3,
face_a: Option<&Face>,
face_b: Option<&Face>,
normal: &Vec3,
depth: f64,
) -> ManifoldPoint {
let mut wa = geom_a.support(normal, pos_a, rot_a);
let mut wb = geom_b.support(&(-*normal), pos_b, rot_b);
match (face_a, face_b) {
(Some(fa), None) => wa = project_onto_face(&wb, fa),
(None, Some(fb)) => wb = project_onto_face(&wa, fb),
_ => {}
}
ManifoldPoint {
position: (wa + wb) * 0.5,
depth,
}
}
fn project_onto_face(p: &Vec3, face: &Face) -> Vec3 {
p - face.normal * (p - face.point).dot(face.normal)
}
struct Face {
verts: Vec<Vec3>,
normal: Vec3,
point: Vec3,
}
fn support_face(geom: &Geometry, pos: &Vec3, rot: &Mat3, dir: &Vec3) -> Option<Face> {
match geom {
Geometry::Box { half_extents } => Some(box_support_face(half_extents, pos, rot, dir)),
Geometry::Plane { normal } => {
let n = (*rot * *normal).normalize();
Some(Face {
verts: Vec::new(),
normal: n,
point: *pos,
})
}
_ => None,
}
}
fn box_support_face(half_extents: &Vec3, pos: &Vec3, rot: &Mat3, dir: &Vec3) -> Face {
let local_dir = rot.transpose() * *dir;
let comps = [local_dir.x, local_dir.y, local_dir.z];
let (axis, sign) = {
let mut best = 0usize;
for (i, c) in comps.iter().enumerate() {
if c.abs() > comps[best].abs() {
best = i;
}
}
(best, if comps[best] >= 0.0 { 1.0 } else { -1.0 })
};
let h = [half_extents.x, half_extents.y, half_extents.z];
let (u, v) = ((axis + 1) % 3, (axis + 2) % 3);
let mut verts = Vec::with_capacity(4);
for (su, sv) in [(1.0, 1.0), (-1.0, 1.0), (-1.0, -1.0), (1.0, -1.0)] {
let mut local = [0.0; 3];
local[axis] = sign * h[axis];
local[u] = su * h[u];
local[v] = sv * h[v];
let l = Vec3::new(local[0], local[1], local[2]);
verts.push(pos + *rot * l);
}
let mut n_local = [0.0; 3];
n_local[axis] = sign;
let normal = (*rot * Vec3::new(n_local[0], n_local[1], n_local[2])).normalize();
let point = verts[0];
Face {
verts,
normal,
point,
}
}
fn clip_faces(
reference: &Face,
incident: &Face,
normal: &Vec3,
margin: f64,
) -> Option<Vec<ManifoldPoint>> {
if incident.verts.is_empty() {
return None;
}
let mut poly = incident.verts.clone();
let n_ref = reference.verts.len();
let centroid = if n_ref > 0 {
reference.verts.iter().fold(Vec3::zeros(), |a, v| a + *v) / n_ref as f64
} else {
Vec3::zeros()
};
for i in 0..n_ref {
let a = reference.verts[i];
let b = reference.verts[(i + 1) % n_ref];
let edge = b - a;
let plane_n = edge.cross(reference.normal);
let nn = plane_n.norm();
if nn < 1e-12 {
continue;
}
let mut plane_n = plane_n / nn;
if (centroid - a).dot(plane_n) < 0.0 {
plane_n = -plane_n;
}
poly = clip_polygon(&poly, &a, &plane_n);
if poly.is_empty() {
return None;
}
}
let ref_point = reference.point;
let out: Vec<ManifoldPoint> = poly
.iter()
.filter_map(|p| {
let sep = (p - ref_point).dot(normal);
if sep > margin.max(1e-9) {
return None;
}
Some(ManifoldPoint {
position: p - *normal * (sep * 0.5),
depth: -sep,
})
})
.collect();
if out.is_empty() { None } else { Some(out) }
}
fn clip_polygon(poly: &[Vec3], point: &Vec3, n: &Vec3) -> Vec<Vec3> {
let mut out = Vec::with_capacity(poly.len() + 2);
if poly.is_empty() {
return out;
}
for i in 0..poly.len() {
let cur = poly[i];
let next = poly[(i + 1) % poly.len()];
let d_cur = (cur - point).dot(n);
let d_next = (next - point).dot(n);
let cur_in = d_cur >= 0.0;
let next_in = d_next >= 0.0;
if cur_in {
out.push(cur);
}
if cur_in != next_in {
let denom = d_cur - d_next;
if denom.abs() > 1e-15 {
let t = d_cur / denom;
out.push(cur + (next - cur) * t);
}
}
}
out
}
fn reduce(mut pts: Vec<ManifoldPoint>) -> Vec<ManifoldPoint> {
pts.retain(|p| p.depth.is_finite() && is_finite(&p.position));
if pts.len() <= MAX_MANIFOLD_POINTS {
pts.sort_by(|a, b| b.depth.total_cmp(&a.depth));
return pts;
}
let mut chosen: Vec<ManifoldPoint> = Vec::with_capacity(MAX_MANIFOLD_POINTS);
let deepest = pts
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.depth.total_cmp(&b.depth))
.map(|(i, _)| i)
.unwrap();
chosen.push(pts.swap_remove(deepest));
while chosen.len() < MAX_MANIFOLD_POINTS && !pts.is_empty() {
let (idx, _) = pts
.iter()
.enumerate()
.map(|(i, p)| {
let d = chosen
.iter()
.map(|c| (c.position - p.position).norm())
.fold(f64::INFINITY, f64::min);
(i, d)
})
.max_by(|(_, a), (_, b)| a.total_cmp(b))
.unwrap();
chosen.push(pts.swap_remove(idx));
}
chosen.sort_by(|a, b| b.depth.total_cmp(&a.depth));
chosen
}
fn is_finite(v: &Vec3) -> bool {
v.x.is_finite() && v.y.is_finite() && v.z.is_finite()
}
#[cfg(test)]
mod tests {
use super::*;
fn unit_box(h: f64) -> Geometry {
Geometry::Box {
half_extents: Vec3::new(h, h, h),
}
}
#[test]
fn box_on_box_gives_four_points() {
let ground = Geometry::Box {
half_extents: Vec3::new(5.0, 5.0, 0.5),
};
let b = unit_box(0.5);
let m = contact_manifold(
&ground,
&b,
&Vec3::zeros(),
&Mat3::identity(),
&Vec3::new(0.0, 0.0, 0.99),
&Mat3::identity(),
)
.expect("overlapping boxes produce a manifold");
assert_eq!(m.points.len(), 4, "expected a 4-point face manifold");
assert!(
(m.normal - Vec3::z()).norm() < 1e-6,
"normal ~ +z, got {:?}",
m.normal
);
for p in &m.points {
assert!(
(p.depth - 0.01).abs() < 1e-6,
"each corner overlaps by 0.01, got {}",
p.depth
);
assert!((p.position.z - 0.495).abs() < 1e-6);
assert!((p.position.x.abs() - 0.5).abs() < 1e-6);
assert!((p.position.y.abs() - 0.5).abs() < 1e-6);
}
}
#[test]
fn box_on_box_points_are_spread() {
let ground = Geometry::Box {
half_extents: Vec3::new(5.0, 5.0, 0.5),
};
let b = unit_box(0.5);
let m = contact_manifold(
&ground,
&b,
&Vec3::zeros(),
&Mat3::identity(),
&Vec3::new(0.0, 0.0, 0.99),
&Mat3::identity(),
)
.unwrap();
let centroid =
m.points.iter().fold(Vec3::zeros(), |a, p| a + p.position) / m.points.len() as f64;
for p in &m.points {
let r = (p.position - centroid).norm();
assert!(r > 0.5, "point too close to centroid: {r}");
}
}
#[test]
fn sphere_on_box_gives_one_point() {
let ground = Geometry::Box {
half_extents: Vec3::new(5.0, 5.0, 0.5),
};
let s = Geometry::Sphere { radius: 0.25 };
let m = contact_manifold(
&ground,
&s,
&Vec3::zeros(),
&Mat3::identity(),
&Vec3::new(0.0, 0.0, 0.74),
&Mat3::identity(),
)
.expect("sphere overlaps box");
assert_eq!(m.points.len(), 1);
assert!((m.normal - Vec3::z()).norm() < 1e-3);
assert!((m.points[0].depth - 0.01).abs() < 1e-3);
assert!(
(m.points[0].position.z - 0.495).abs() < 1e-3,
"contact point z = {}",
m.points[0].position.z
);
}
#[test]
fn tilted_box_contacts_on_an_edge() {
let ground = Geometry::Box {
half_extents: Vec3::new(5.0, 5.0, 0.5),
};
let b = unit_box(0.5);
let angle = 0.3_f64;
let (s, c) = angle.sin_cos();
let rot = Mat3::new(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c);
let drop = 0.5 * (c + s) - 0.01;
let m = contact_manifold(
&ground,
&b,
&Vec3::zeros(),
&Mat3::identity(),
&Vec3::new(0.0, 0.0, 0.5 + drop),
&rot,
)
.expect("tilted box overlaps ground");
assert!((m.normal - Vec3::z()).norm() < 1e-6);
assert!(
m.points.len() <= 2,
"edge contact should not report a full face, got {}",
m.points.len()
);
for p in &m.points {
assert!(p.depth > 0.0 && p.depth < 0.05, "depth {}", p.depth);
}
}
#[test]
fn separated_boxes_have_no_manifold() {
let b = unit_box(0.5);
assert!(
contact_manifold(
&b,
&b,
&Vec3::zeros(),
&Mat3::identity(),
&Vec3::new(0.0, 0.0, 5.0),
&Mat3::identity(),
)
.is_none()
);
}
}