use crate::material::ContactMaterial;
use phyz_collision::{AABB, Collision, sweep_and_prune};
use phyz_math::{SpatialTransformExt, SpatialVec, Vec3};
use phyz_model::{Geometry as ModelGeometry, Heightfield, Model, State};
fn convert_geometry(g: &ModelGeometry) -> phyz_collision::Geometry {
match g {
ModelGeometry::Sphere { radius } => phyz_collision::Geometry::Sphere { radius: *radius },
ModelGeometry::Capsule { radius, length } => phyz_collision::Geometry::Capsule {
radius: *radius,
length: *length,
},
ModelGeometry::Box { half_extents } => phyz_collision::Geometry::Box {
half_extents: *half_extents,
},
ModelGeometry::Cylinder { radius, height } => phyz_collision::Geometry::Cylinder {
radius: *radius,
height: *height,
},
ModelGeometry::Mesh { vertices, faces } => phyz_collision::Geometry::Mesh {
vertices: vertices.clone(),
faces: faces.clone(),
},
ModelGeometry::Plane { normal } => phyz_collision::Geometry::Plane { normal: *normal },
}
}
pub fn find_contacts(model: &Model, state: &State, margin: f64) -> Vec<Collision> {
let shapes = placed_shapes(model, state);
if shapes.len() < 2 {
return Vec::new();
}
let margin = if margin.is_finite() {
margin.max(0.0)
} else {
0.0
};
let aabbs: Vec<AABB> = shapes
.iter()
.map(|s| AABB::from_geometry(&s.geometry, &s.pos, &s.rot).expanded(0.5 * margin))
.collect();
let pairs = sweep_and_prune(&aabbs);
let welds = model.weld_groups();
let mut contacts = Vec::new();
for (si, sj) in pairs {
let (a, b) = (&shapes[si], &shapes[sj]);
if !model.may_collide(a.body, b.body, &welds) {
continue;
}
let Some(manifold) = phyz_collision::contact_manifold_within(
&a.geometry,
&b.geometry,
&a.pos,
&a.rot,
&b.pos,
&b.rot,
margin,
) else {
continue;
};
for point in &manifold.points {
contacts.push(Collision {
body_i: a.body,
body_j: b.body,
contact_point: point.position,
contact_normal: -manifold.normal,
penetration_depth: point.depth,
});
}
}
contacts
}
struct PlacedShape {
body: usize,
geometry: phyz_collision::Geometry,
pos: Vec3,
rot: phyz_math::Mat3,
}
fn placed_shapes(model: &Model, state: &State) -> Vec<PlacedShape> {
let mut out = Vec::new();
for (i, body) in model.bodies.iter().enumerate() {
let Some(xform) = state.body_xform.get(i) else {
continue;
};
if !pos_is_finite(&xform.pos) || !rot_is_finite(&xform.rot) {
continue;
}
let mut push = |geom: &ModelGeometry, origin: &phyz_math::SpatialTransform| {
let sx = shape_world_xform(xform, origin);
if !pos_is_finite(&sx.pos) || !rot_is_finite(&sx.rot) {
return;
}
out.push(PlacedShape {
body: i,
geometry: convert_geometry(geom),
pos: sx.pos,
rot: sx.rot.transpose(),
});
};
if body.collisions.is_empty() {
if let Some(g) = &body.geometry {
push(g, &phyz_math::SpatialTransform::identity());
}
} else {
for inst in &body.collisions {
push(&inst.geometry, &inst.origin);
}
}
}
out
}
fn pos_is_finite(v: &Vec3) -> bool {
v.x.is_finite() && v.y.is_finite() && v.z.is_finite()
}
fn rot_is_finite(m: &phyz_math::Mat3) -> bool {
for i in 0..3 {
for j in 0..3 {
if !m[(i, j)].is_finite() {
return false;
}
}
}
true
}
#[deprecated(
note = "penalty contact is superseded by the convex solve (`assemble` + \
`solve_contacts`), which couples contacts through the Delassus \
operator instead of treating each in isolation"
)]
pub fn contact_forces(
contacts: &[Collision],
state: &State,
materials: &[ContactMaterial],
body_velocities: Option<&[SpatialVec]>,
) -> Vec<SpatialVec> {
let nbodies = state.body_xform.len();
let mut forces = vec![SpatialVec::zero(); nbodies];
for contact in contacts {
let i = contact.body_i;
let j = contact.body_j;
let material = if materials.is_empty() {
&ContactMaterial::default()
} else {
&materials[i.min(materials.len() - 1)]
};
let vel_i = body_velocities
.and_then(|vels| vels.get(i))
.map(|v| v.linear)
.unwrap_or(Vec3::zeros());
let vel_j = if j == Collision::WORLD {
Vec3::zeros()
} else {
body_velocities
.and_then(|vels| vels.get(j))
.map(|v| v.linear)
.unwrap_or(Vec3::zeros())
};
let adapted;
let (query, on_i) = if j == Collision::WORLD {
(contact, true)
} else {
adapted = Collision {
contact_normal: -contact.contact_normal,
..contact.clone()
};
(&adapted, false)
};
#[allow(deprecated)]
let force = crate::compute_contact_force(query, material, &vel_i, &vel_j);
let f_linear = force.linear;
let r_i = contact.contact_point - state.body_xform[i].pos;
if on_i {
forces[i] = forces[i] + SpatialVec::new(r_i.cross(f_linear), f_linear);
} else {
let r_j = contact.contact_point - state.body_xform[j].pos;
forces[i] = forces[i] + SpatialVec::new(r_i.cross(-f_linear), -f_linear);
forces[j] = forces[j] + SpatialVec::new(r_j.cross(f_linear), f_linear);
}
}
forces
}
#[deprecated(
note = "penalty contact is superseded by the convex solve (`assemble` + \
`solve_contacts`), which couples contacts through the Delassus \
operator instead of treating each in isolation"
)]
pub fn contact_forces_implicit(
contacts: &[Collision],
state: &State,
materials: &[ContactMaterial],
body_velocities: Option<&[SpatialVec]>,
masses: &[f64],
dt: f64,
) -> Vec<SpatialVec> {
let nbodies = state.body_xform.len();
let mut forces = vec![SpatialVec::zero(); nbodies];
for contact in contacts {
let i = contact.body_i;
let j = contact.body_j;
let material = if materials.is_empty() {
&ContactMaterial::default()
} else {
&materials[i.min(materials.len() - 1)]
};
let vel_i = body_velocities
.and_then(|vels| vels.get(i))
.map(|v| v.linear)
.unwrap_or(Vec3::zeros());
let vel_j = if j == Collision::WORLD {
Vec3::zeros()
} else {
body_velocities
.and_then(|vels| vels.get(j))
.map(|v| v.linear)
.unwrap_or(Vec3::zeros())
};
let mass_i = masses.get(i).copied().unwrap_or(f64::INFINITY);
let mass_j = if j == Collision::WORLD {
f64::INFINITY
} else {
masses.get(j).copied().unwrap_or(f64::INFINITY)
};
let adapted;
let (query, on_i) = if j == Collision::WORLD {
(contact, true)
} else {
adapted = Collision {
contact_normal: -contact.contact_normal,
..contact.clone()
};
(&adapted, false)
};
#[allow(deprecated)]
let force = crate::compute_contact_force_implicit(
query, material, &vel_i, &vel_j, mass_i, mass_j, dt,
);
let f_linear = force.linear;
let r_i = contact.contact_point - state.body_xform[i].pos;
if on_i {
forces[i] = forces[i] + SpatialVec::new(r_i.cross(f_linear), f_linear);
} else {
let r_j = contact.contact_point - state.body_xform[j].pos;
forces[j] = forces[j] + SpatialVec::new(r_j.cross(f_linear), f_linear);
forces[i] = forces[i] + SpatialVec::new(r_i.cross(-f_linear), -f_linear);
}
}
forces
}
pub fn find_ground_contacts(
state: &State,
geometries: &[Option<ModelGeometry>],
ground_height: f64,
margin: f64,
) -> Vec<Collision> {
let mut contacts = Vec::new();
let margin = sanitize_margin(margin);
let cutoff = ground_height + margin;
for (i, geom_opt) in geometries.iter().enumerate() {
let Some(geom) = geom_opt else { continue };
let xform = &state.body_xform[i];
if !pos_is_finite(&xform.pos) || !rot_is_finite(&xform.rot) {
continue;
}
let Some(candidates) = ground_candidates(geom, xform) else {
continue;
};
emit_ground_hits(&mut contacts, i, candidates, ground_height, cutoff);
}
contacts
}
fn sanitize_margin(margin: f64) -> f64 {
if margin.is_finite() {
margin.max(0.0)
} else {
0.0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum GroundSupport {
Material,
WorldOffset(Vec3),
CylinderRim {
axis: Vec3,
radius: f64,
quarter_turns: u8,
offset: Vec3,
},
}
impl GroundSupport {
pub fn world_offset(&self) -> Vec3 {
match self {
Self::Material => Vec3::zeros(),
Self::WorldOffset(o) => *o,
Self::CylinderRim { offset, .. } => *offset,
}
}
}
#[derive(Clone, Copy, Debug)]
struct GroundCandidate {
point: Vec3,
support: GroundSupport,
}
impl GroundCandidate {
fn material(point: Vec3) -> Self {
Self {
point,
support: GroundSupport::Material,
}
}
}
pub const CYL_AXIS_EPS: f64 = 1e-9;
pub fn cylinder_rim_basis(axis: Vec3) -> Option<(Vec3, Vec3)> {
let rho2 = axis.x * axis.x + axis.y * axis.y;
if rho2.sqrt() <= CYL_AXIS_EPS {
return None;
}
let u = Vec3::new(axis.z * axis.x, axis.z * axis.y, -rho2).normalize();
Some((u, axis.cross(u)))
}
pub fn cylinder_rim_dir(axis: Vec3, quarter_turns: u8) -> Option<Vec3> {
cylinder_rim_basis(axis).map(|(u, w)| match quarter_turns & 3 {
0 => u,
1 => w,
2 => -u,
_ => -w,
})
}
fn ground_candidates(
geom: &ModelGeometry,
xform: &phyz_math::SpatialTransform,
) -> Option<Vec<GroundCandidate>> {
let pos = xform.pos;
let candidates: Vec<GroundCandidate> = match geom {
ModelGeometry::Box { half_extents } => {
let h = half_extents;
let mut v = Vec::with_capacity(8);
for sx in [-1.0, 1.0] {
for sy in [-1.0, 1.0] {
for sz in [-1.0, 1.0] {
v.push(GroundCandidate::material(
xform.body_to_world_point(Vec3::new(sx * h.x, sy * h.y, sz * h.z)),
));
}
}
}
v
}
ModelGeometry::Sphere { radius } => {
let drop = Vec3::new(0.0, 0.0, -*radius);
vec![GroundCandidate {
point: pos + drop,
support: GroundSupport::WorldOffset(drop),
}]
}
ModelGeometry::Capsule { radius, length } => {
let axis = xform.body_to_world_dir(Vec3::new(0.0, 0.0, length * 0.5));
let drop = Vec3::new(0.0, 0.0, -*radius);
vec![
GroundCandidate {
point: pos + axis + drop,
support: GroundSupport::WorldOffset(drop),
},
GroundCandidate {
point: pos - axis + drop,
support: GroundSupport::WorldOffset(drop),
},
]
}
ModelGeometry::Cylinder { radius, height } => {
let axis = xform.body_to_world_dir(Vec3::z());
let half = axis * (height * 0.5);
let mut v = Vec::with_capacity(8);
match cylinder_rim_basis(axis) {
Some((u, w)) => {
for cap_sign in [1.0, -1.0] {
let cap = pos + half * cap_sign;
for (quarter_turns, dir) in [u, w, -u, -w].into_iter().enumerate() {
let offset = dir * *radius;
v.push(GroundCandidate {
point: cap + offset,
support: GroundSupport::CylinderRim {
axis,
radius: *radius,
quarter_turns: quarter_turns as u8,
offset,
},
});
}
}
}
None => {
let ex = xform.body_to_world_dir(Vec3::x()) * *radius;
let ey = xform.body_to_world_dir(Vec3::y()) * *radius;
for cap_sign in [1.0, -1.0] {
let cap = pos + half * cap_sign;
for dir in [ex, ey, -ex, -ey] {
v.push(GroundCandidate::material(cap + dir));
}
}
}
}
v
}
ModelGeometry::Mesh { vertices, .. } => vertices
.iter()
.map(|v| GroundCandidate::material(xform.body_to_world_point(*v)))
.collect(),
ModelGeometry::Plane { .. } => return None,
};
Some(candidates)
}
fn shape_world_xform(
body_xform: &phyz_math::SpatialTransform,
origin: &phyz_math::SpatialTransform,
) -> phyz_math::SpatialTransform {
phyz_math::SpatialTransform::new(
origin.rot.mul_mat(&body_xform.rot),
body_xform.body_to_world_point(origin.pos),
)
}
fn emit_ground_hits(
contacts: &mut Vec<Collision>,
body: usize,
candidates: Vec<GroundCandidate>,
ground_height: f64,
cutoff: f64,
) {
let mut hits: Vec<(f64, Vec3)> = candidates
.into_iter()
.map(|c| c.point)
.filter(|p| p.z.is_finite() && p.z < cutoff)
.map(|p| (ground_height - p.z, p))
.collect();
hits.sort_by(|a, b| b.0.total_cmp(&a.0));
hits.truncate(phyz_collision::MAX_MANIFOLD_POINTS);
for (depth, p) in hits {
contacts.push(Collision {
body_i: body,
body_j: Collision::WORLD, contact_point: Vec3::new(p.x, p.y, ground_height - depth * 0.5),
contact_normal: Vec3::z(),
penetration_depth: depth,
});
}
}
pub fn find_ground_contacts_model(
model: &Model,
state: &State,
ground_height: f64,
margin: f64,
) -> Vec<Collision> {
find_ground_contacts_model_with_support(model, state, ground_height, margin)
.into_iter()
.map(|(c, _)| c)
.collect()
}
pub fn find_ground_contacts_model_with_drop(
model: &Model,
state: &State,
ground_height: f64,
margin: f64,
) -> Vec<(Collision, f64)> {
find_ground_contacts_model_with_support(model, state, ground_height, margin)
.into_iter()
.map(|(c, s)| (c, -s.world_offset().z))
.collect()
}
pub fn find_ground_contacts_model_with_offset(
model: &Model,
state: &State,
ground_height: f64,
margin: f64,
) -> Vec<(Collision, Vec3)> {
find_ground_contacts_model_with_support(model, state, ground_height, margin)
.into_iter()
.map(|(c, s)| (c, s.world_offset()))
.collect()
}
pub fn find_ground_contacts_model_with_support(
model: &Model,
state: &State,
ground_height: f64,
margin: f64,
) -> Vec<(Collision, GroundSupport)> {
let margin = sanitize_margin(margin);
let cutoff = ground_height + margin;
let mut out = Vec::new();
for (i, body) in model.bodies.iter().enumerate() {
let Some(xform) = state.body_xform.get(i) else {
continue;
};
if !pos_is_finite(&xform.pos) || !rot_is_finite(&xform.rot) {
continue;
}
let mut pool: Vec<(f64, Vec3, GroundSupport)> = Vec::new();
let mut push_shape = |geom: &ModelGeometry, sx: &phyz_math::SpatialTransform| {
let Some(candidates) = ground_candidates(geom, sx) else {
return;
};
pool.extend(
candidates
.into_iter()
.filter(|c| c.point.z.is_finite() && c.point.z < cutoff)
.map(|c| (ground_height - c.point.z, c.point, c.support)),
);
};
if body.collisions.is_empty() {
if let Some(geom) = &body.geometry {
push_shape(geom, xform);
}
} else {
for inst in &body.collisions {
push_shape(&inst.geometry, &shape_world_xform(xform, &inst.origin));
}
}
pool.sort_by(|a, b| b.0.total_cmp(&a.0));
pool.truncate(phyz_collision::MAX_MANIFOLD_POINTS);
for (depth, p, support) in pool {
out.push((
Collision {
body_i: i,
body_j: Collision::WORLD, contact_point: Vec3::new(p.x, p.y, ground_height - depth * 0.5),
contact_normal: Vec3::z(),
penetration_depth: depth,
},
support,
));
}
}
out
}
pub fn find_heightfield_contacts_model(
model: &Model,
state: &State,
hf: &Heightfield,
margin: f64,
) -> Vec<Collision> {
find_heightfield_contacts_model_with_support(model, state, hf, margin)
.into_iter()
.map(|(c, _)| c)
.collect()
}
pub fn find_heightfield_contacts_model_with_drop(
model: &Model,
state: &State,
hf: &Heightfield,
margin: f64,
) -> Vec<(Collision, f64)> {
find_heightfield_contacts_model_with_support(model, state, hf, margin)
.into_iter()
.map(|(c, s)| (c, -s.world_offset().z))
.collect()
}
pub fn find_heightfield_contacts_model_with_support(
model: &Model,
state: &State,
hf: &Heightfield,
margin: f64,
) -> Vec<(Collision, GroundSupport)> {
let margin = sanitize_margin(margin);
let mut out = Vec::new();
for (i, body) in model.bodies.iter().enumerate() {
let Some(xform) = state.body_xform.get(i) else {
continue;
};
if !pos_is_finite(&xform.pos) || !rot_is_finite(&xform.rot) {
continue;
}
let mut pool: Vec<(f64, Vec3, Vec3, GroundSupport)> = Vec::new();
let mut push_shape = |geom: &ModelGeometry, sx: &phyz_math::SpatialTransform| {
let Some(candidates) = ground_candidates(geom, sx) else {
return;
};
for c in candidates {
let p = c.point;
if !(p.z.is_finite() && p.x.is_finite() && p.y.is_finite()) {
continue;
}
let n = hf.normal(p.x, p.y);
let depth = n.z * (hf.height(p.x, p.y) - p.z);
if depth > -margin {
pool.push((depth, p, n, c.support));
}
}
};
if body.collisions.is_empty() {
if let Some(geom) = &body.geometry {
push_shape(geom, xform);
}
} else {
for inst in &body.collisions {
push_shape(&inst.geometry, &shape_world_xform(xform, &inst.origin));
}
}
pool.sort_by(|a, b| b.0.total_cmp(&a.0));
pool.truncate(phyz_collision::MAX_MANIFOLD_POINTS);
for (depth, p, n, support) in pool {
out.push((
Collision {
body_i: i,
body_j: Collision::WORLD, contact_point: p + n * (depth * 0.5),
contact_normal: n,
penetration_depth: depth,
},
support,
));
}
}
out
}