use crate::material::ContactMaterial;
use phyz_math::Vec3;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContactCoupling {
Full,
BlockDiagonal,
PerBody,
}
#[derive(Debug, Clone, Copy)]
pub struct ContactSolverConfig {
pub coupling: ContactCoupling,
pub newton: bool,
pub regularization: f64,
pub tolerance: f64,
pub max_iterations: usize,
pub relaxation: f64,
pub restitution_threshold: f64,
pub mujoco_compat: bool,
}
impl Default for ContactSolverConfig {
fn default() -> Self {
Self::simulation()
}
}
impl ContactSolverConfig {
pub fn simulation() -> Self {
Self {
coupling: ContactCoupling::Full,
newton: true,
regularization: 1e-6,
tolerance: 1e-10,
max_iterations: 1000,
relaxation: 1.0,
restitution_threshold: 0.05,
mujoco_compat: false,
}
}
pub fn gradients() -> Self {
Self {
coupling: ContactCoupling::Full,
newton: true,
regularization: 1e-3,
tolerance: 1e-12,
max_iterations: 2000,
relaxation: 1.0,
restitution_threshold: 0.05,
mujoco_compat: false,
}
}
pub fn mujoco_compat() -> Self {
Self {
mujoco_compat: true,
..Self::simulation()
}
}
pub fn gpu_equivalent() -> Self {
Self {
coupling: ContactCoupling::BlockDiagonal,
newton: false,
tolerance: 0.0,
max_iterations: GPU_SWEEPS,
..Self::simulation()
}
}
}
pub const GPU_SWEEPS: usize = 16;
#[derive(Debug, Clone, Copy)]
pub struct ContactRow {
pub mu: f64,
pub restitution: f64,
pub depth: f64,
pub bias: f64,
pub impedance: f64,
pub dimpedance_ddepth: f64,
pub impact: f64,
}
pub const IMPACT_IMPEDANCE: f64 = 0.999;
impl ContactRow {
pub fn from_material(
material: &ContactMaterial,
depth: f64,
dt: f64,
restitution: f64,
) -> Self {
let violation = depth.max(0.0);
let d = material.impedance_at(depth);
let bias = if dt > 0.0 {
d * material.solref.error_reduction(dt) * violation / dt
} else {
0.0
};
Self {
mu: material.friction,
restitution,
depth,
bias,
impedance: d,
dimpedance_ddepth: material.dimpedance_ddepth(depth),
impact: 0.0,
}
}
pub fn with_impact(mut self, impact: f64) -> Self {
let impact = impact.clamp(0.0, 1.0);
self.bias *= 1.0 - impact;
self.impact = impact;
self
}
pub fn effective_impedance(&self) -> f64 {
self.impedance + self.impact * (IMPACT_IMPEDANCE - self.impedance).max(0.0)
}
}
impl Default for ContactRow {
fn default() -> Self {
Self {
mu: 0.0,
restitution: 0.0,
depth: 0.0,
bias: 0.0,
impedance: 1.0,
dimpedance_ddepth: 0.0,
impact: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct ContactProblem {
pub n: usize,
pub delassus: Vec<f64>,
pub free_velocity: Vec<f64>,
pub rows: Vec<ContactRow>,
pub bodies: Vec<(usize, usize)>,
}
#[derive(Debug, Clone)]
pub struct ContactSolution {
pub impulses: Vec<Vec3>,
pub iterations: usize,
pub residual: f64,
pub converged: bool,
}
impl ContactProblem {
pub fn effective_restitution(e: f64, approach_speed: f64, v_rest: f64) -> f64 {
if v_rest <= 0.0 {
return e;
}
let s = approach_speed.abs();
if s <= v_rest {
return 0.0;
}
if s >= 2.0 * v_rest {
return e;
}
let t = (s - v_rest) / v_rest;
e * t * t * (3.0 - 2.0 * t)
}
pub fn impact_weight(approach_speed: f64, v_rest: f64) -> f64 {
Self::effective_restitution(1.0, approach_speed, v_rest)
}
}
pub fn regularization_diag(
problem: &ContactProblem,
c: usize,
config: &ContactSolverConfig,
) -> [f64; 3] {
let dim = 3 * problem.n;
let base = 3 * c;
let d = problem.rows[c].effective_impedance().clamp(1e-6, 1.0);
let scale = (1.0 - d) / d;
let a_nn = problem.delassus[base * dim + base];
let normal = (scale * a_nn).max(config.regularization);
let tangent = if config.mujoco_compat {
normal
} else {
config.regularization
};
[normal, tangent, tangent]
}
pub fn regularization_depth_derivative(
problem: &ContactProblem,
c: usize,
config: &ContactSolverConfig,
) -> [f64; 3] {
let dim = 3 * problem.n;
let base = 3 * c;
let row = &problem.rows[c];
let d_raw = row.effective_impedance();
let d = d_raw.clamp(1e-6, 1.0);
let dd = if d_raw > 1e-6 && d_raw < 1.0 {
(1.0 - row.impact) * row.dimpedance_ddepth
} else {
0.0
};
let a_nn = problem.delassus[base * dim + base];
let normal = if (1.0 - d) / d * a_nn > config.regularization {
-a_nn * dd / (d * d)
} else {
0.0
};
let tangent = if config.mujoco_compat { normal } else { 0.0 };
[normal, tangent, tangent]
}
#[allow(clippy::needless_range_loop)]
pub fn solve_contacts(problem: &ContactProblem, config: &ContactSolverConfig) -> ContactSolution {
solve_contacts_warm(problem, config, &[])
}
#[allow(clippy::needless_range_loop)]
pub fn solve_contacts_warm(
problem: &ContactProblem,
config: &ContactSolverConfig,
initial: &[Vec3],
) -> ContactSolution {
let n = problem.n;
let mut f = vec![Vec3::zeros(); n];
for (slot, seed) in f.iter_mut().zip(initial) {
*slot = *seed;
}
if n == 0 {
return ContactSolution {
impulses: f,
iterations: 0,
residual: 0.0,
converged: true,
};
}
let dim = 3 * n;
debug_assert_eq!(problem.delassus.len(), dim * dim);
debug_assert_eq!(problem.free_velocity.len(), dim);
let a = &problem.delassus;
let at = |i: usize, j: usize| a[i * dim + j];
let blocks: Vec<[[f64; 3]; 3]> = (0..n)
.map(|c| {
let base = 3 * c;
let reg = regularization_diag(problem, c, config);
let mut m = [[0.0; 3]; 3];
for (r, row) in m.iter_mut().enumerate() {
for (col, e) in row.iter_mut().enumerate() {
*e = at(base + r, base + col);
if r == col {
*e += reg[r];
}
}
}
m
})
.collect();
let mut iterations = 0;
let normal_warmup = WARMUP_SWEEPS.min(config.max_iterations);
for _ in 0..normal_warmup {
iterations += 1;
if sweep(problem, config, &blocks, &mut f, true) < config.tolerance {
break;
}
}
let mut residual = f64::INFINITY;
while iterations < config.max_iterations.min(2 * WARMUP_SWEEPS) {
iterations += 1;
residual = sweep(problem, config, &blocks, &mut f, false);
if residual < config.tolerance {
return ContactSolution {
impulses: f,
iterations,
residual,
converged: true,
};
}
}
let mut newton_solves = 0;
let mut stalls = 0;
while iterations < config.max_iterations {
let entry_residual = residual;
if config.newton
&& newton_solves < NEWTON_ATTEMPTS
&& let Some(candidate) = newton_step(problem, config, &f)
{
newton_solves += 1;
iterations += 1;
let mut alpha = 1.0;
for _ in 0..LINE_SEARCH_STEPS {
let mut trial: Vec<Vec3> = f
.iter()
.zip(&candidate)
.map(|(cur, cand)| *cur + (*cand - *cur) * alpha)
.collect();
iterations += 1;
let trial_residual = sweep(problem, config, &blocks, &mut trial, false);
if trial_residual < residual {
residual = trial_residual;
f = trial;
break;
}
alpha *= 0.5;
}
if residual < config.tolerance {
break;
}
}
for _ in 0..PGS_BLOCK {
if iterations >= config.max_iterations {
break;
}
iterations += 1;
residual = sweep(problem, config, &blocks, &mut f, false);
if residual < config.tolerance {
break;
}
}
if residual < config.tolerance {
break;
}
if residual > entry_residual * STALL_RATIO {
stalls += 1;
if stalls >= STALL_BLOCKS {
break;
}
} else {
stalls = 0;
}
}
ContactSolution {
impulses: f,
iterations,
residual,
converged: residual < config.tolerance,
}
}
const WARMUP_SWEEPS: usize = 8;
const NEWTON_ATTEMPTS: usize = 24;
const LINE_SEARCH_STEPS: usize = 3;
const PGS_BLOCK: usize = 4;
const STALL_RATIO: f64 = 0.99;
const STALL_BLOCKS: usize = 3;
#[allow(clippy::needless_range_loop)]
fn shares_body(a: (usize, usize), b: (usize, usize)) -> bool {
let real = |x: usize| x != usize::MAX;
(real(a.0) && (a.0 == b.0 || a.0 == b.1)) || (real(a.1) && (a.1 == b.0 || a.1 == b.1))
}
fn sweep(
problem: &ContactProblem,
config: &ContactSolverConfig,
blocks: &[[[f64; 3]; 3]],
f: &mut [Vec3],
normals_only: bool,
) -> f64 {
let n = problem.n;
let dim = 3 * n;
let a = &problem.delassus;
let at = |i: usize, j: usize| a[i * dim + j];
let mut max_move: f64 = 0.0;
for c in 0..n {
let base = 3 * c;
let mut r = [0.0f64; 3];
for (row, r_row) in r.iter_mut().enumerate() {
let mut acc = problem.free_velocity[base + row];
if config.coupling != ContactCoupling::BlockDiagonal {
for (k, f_k) in f.iter().enumerate().take(n) {
if k == c {
continue;
}
if config.coupling == ContactCoupling::PerBody
&& problem.bodies.len() == n
&& !shares_body(problem.bodies[c], problem.bodies[k])
{
continue;
}
let kb = 3 * k;
acc += at(base + row, kb) * f_k.x
+ at(base + row, kb + 1) * f_k.y
+ at(base + row, kb + 2) * f_k.z;
}
}
*r_row = acc;
}
let row = problem.rows[c];
let a_nn = blocks[c][0][0];
let (a_nu, a_nw) = (blocks[c][0][1], blocks[c][0][2]);
let f_n = if a_nn > 0.0 {
((row.bias - r[0] - a_nu * f[c].y - a_nw * f[c].z) / a_nn).max(0.0)
} else {
0.0
};
let (a_un, a_wn) = (blocks[c][1][0], blocks[c][2][0]);
let r_u = r[1] + a_un * f_n;
let r_w = r[2] + a_wn * f_n;
let (m00, m01) = (blocks[c][1][1], blocks[c][1][2]);
let (m10, m11) = (blocks[c][2][1], blocks[c][2][2]);
let det = m00 * m11 - m01 * m10;
let (mut t_u, mut t_w) = if !normals_only {
if det.abs() > 1e-18 {
(
-(m11 * r_u - m01 * r_w) / det,
-(m00 * r_w - m10 * r_u) / det,
)
} else {
(0.0, 0.0)
}
} else {
(f[c].y, f[c].z)
};
let limit = row.mu * f_n;
let t_norm = (t_u * t_u + t_w * t_w).sqrt();
if t_norm > limit {
if t_norm > 0.0 {
let s = limit / t_norm;
t_u *= s;
t_w *= s;
} else {
t_u = 0.0;
t_w = 0.0;
}
}
let target = Vec3::new(f_n, t_u, t_w);
debug_assert!(
crate::cone::in_cone(target, row.mu, 1e-9 * (1.0 + target.norm())),
"staged solve must land in the friction cone"
);
let next = f[c] + (target - f[c]) * config.relaxation;
max_move = max_move.max((next - f[c]).norm());
f[c] = next;
}
max_move
}
#[allow(clippy::needless_range_loop)]
fn newton_step(
problem: &ContactProblem,
config: &ContactSolverConfig,
f: &[Vec3],
) -> Option<Vec<Vec3>> {
use crate::gradient::ContactRegime;
let n = problem.n;
let dim = 3 * n;
let regimes = crate::gradient::classify_impulses(problem, f, 1e-7);
let mut k = crate::gradient::kkt_matrix(problem, config, ®imes, f);
let mut rhs = vec![0.0; dim];
for c in 0..n {
let base = 3 * c;
match regimes[c] {
ContactRegime::Separating => {}
ContactRegime::Sticking => {
rhs[base] = problem.rows[c].bias - problem.free_velocity[base];
rhs[base + 1] = -problem.free_velocity[base + 1];
rhs[base + 2] = -problem.free_velocity[base + 2];
}
ContactRegime::Sliding => {
rhs[base] = problem.rows[c].bias - problem.free_velocity[base];
}
}
}
crate::gradient::solve_dense(&mut k, &mut rhs, dim, 1)?;
if rhs.iter().any(|v| !v.is_finite()) {
return None;
}
Some(
(0..n)
.map(|c| {
let base = 3 * c;
let f_n = rhs[base].max(0.0);
let (mut t_u, mut t_w) = (rhs[base + 1], rhs[base + 2]);
let limit = problem.rows[c].mu * f_n;
let t_norm = (t_u * t_u + t_w * t_w).sqrt();
if t_norm > limit {
let s = if t_norm > 0.0 { limit / t_norm } else { 0.0 };
t_u *= s;
t_w *= s;
}
Vec3::new(f_n, t_u, t_w)
})
.collect(),
)
}
pub fn point_mass_problem(
mass: f64,
free_vel: Vec3,
material: &ContactMaterial,
depth: f64,
dt: f64,
restitution_threshold: f64,
) -> ContactProblem {
let inv_m = 1.0 / mass;
let mut delassus = vec![0.0; 9];
for i in 0..3 {
delassus[i * 3 + i] = inv_m;
}
let approach = free_vel.x.min(0.0).abs();
let e = ContactProblem::effective_restitution(
material.restitution,
approach,
restitution_threshold,
);
let impact = ContactProblem::impact_weight(approach, restitution_threshold);
ContactProblem {
n: 1,
delassus,
free_velocity: vec![free_vel.x * (1.0 + e), free_vel.y, free_vel.z],
rows: vec![ContactRow::from_material(material, depth, dt, e).with_impact(impact)],
bodies: vec![(0, usize::MAX)],
}
}