use crate::math::{Fix128, Vec3Fix};
#[cfg(feature = "std")]
use crate::sdf_collider::SdfCollider;
#[cfg(not(feature = "std"))]
use alloc::vec;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ClothConfig {
pub iterations: usize,
pub substeps: usize,
pub gravity: Vec3Fix,
pub damping: Fix128,
pub stretch_compliance: Fix128,
pub bend_compliance: Fix128,
pub sdf_friction: Fix128,
pub thickness: Fix128,
pub self_collision: bool,
pub self_collision_distance: Fix128,
}
impl Default for ClothConfig {
fn default() -> Self {
Self {
iterations: 8,
substeps: 4,
gravity: Vec3Fix::new(Fix128::ZERO, Fix128::from_int(-10), Fix128::ZERO),
damping: Fix128::from_ratio(99, 100),
stretch_compliance: Fix128::ZERO,
bend_compliance: Fix128::from_ratio(1, 100),
sdf_friction: Fix128::from_ratio(2, 10),
thickness: Fix128::from_ratio(1, 100),
self_collision: false,
self_collision_distance: Fix128::from_ratio(2, 100), }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct EdgeConstraint {
i0: usize,
i1: usize,
rest_length: Fix128,
}
#[derive(Clone, Copy, Debug, PartialEq)]
struct BendConstraint {
i0: usize,
i1: usize,
i2: usize,
i3: usize,
rest_angle: Fix128,
}
#[repr(C, align(64))]
pub struct Cloth {
pub positions: Vec<Vec3Fix>,
pub prev_positions: Vec<Vec3Fix>,
pub velocities: Vec<Vec3Fix>,
pub inv_masses: Vec<Fix128>,
pub triangles: Vec<[usize; 3]>,
edge_constraints: Vec<EdgeConstraint>,
bend_constraints: Vec<BendConstraint>,
pub pinned: Vec<usize>,
pub wind: Vec3Fix,
pub config: ClothConfig,
rest_tether: Option<RestTether>,
rest_positions: Vec<Vec3Fix>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct RestTether {
stiffness: Fix128,
damping: Fix128,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct OscillatorStep {
pos_e: Fix128,
pos_v: Fix128,
pos_a: Fix128,
vel_e: Fix128,
vel_v: Fix128,
vel_a: Fix128,
}
impl OscillatorStep {
fn new(omega_sq: Fix128, gamma: Fix128, h: Fix128) -> Self {
let quarter = Fix128::from_ratio(1, 4);
let p = (gamma * h).half();
let w2h2 = omega_sq * h * h;
let q = w2h2 - p * p;
let h2 = h * h;
let (b_over_h, a, bd, c_over_h2) = if q > quarter {
let (s, k) = sinc_cos(q);
let e = crate::physics2d::exp_neg(p);
let a = e * (k + p * s);
(e * s, a, e * (k - p * s), (Fix128::ONE - a) / w2h2)
} else if q < Fix128::ZERO - quarter {
let r = (Fix128::ZERO - q).sqrt();
let l2 = p + r;
let l1 = w2h2 / l2;
let d = r.double();
let e1 = crate::physics2d::exp_neg(l1);
let e2 = crate::physics2d::exp_neg(l2);
let a = (l2 * e1 - l1 * e2) / d;
let c = if w2h2 >= Fix128::from_ratio(1, 16) {
(Fix128::ONE - a) / w2h2
} else {
(phi1(l1) - phi1(l2)) / d
};
((e1 - e2) / d, a, (l2 * e2 - l1 * e1) / d, c)
} else {
let (s, k) = sinc_cos_series(q);
let e = crate::physics2d::exp_neg(p);
let a = e * (k + p * s);
let c = if w2h2 >= Fix128::from_ratio(1, 16) {
(Fix128::ONE - a) / w2h2
} else {
impulse_integral_series(p.double(), w2h2)
};
(e * s, a, e * (k - p * s), c)
};
let b = h * b_over_h;
Self {
pos_e: a,
pos_v: b,
pos_a: h2 * c_over_h2,
vel_e: Fix128::ZERO - omega_sq * b,
vel_v: bd,
vel_a: b,
}
}
}
fn sinc_cos_series(q: Fix128) -> (Fix128, Fix128) {
let minus_q = Fix128::ZERO - q;
let (mut s, mut k) = (Fix128::ONE, Fix128::ONE);
let (mut ts, mut tk) = (Fix128::ONE, Fix128::ONE);
for n in 1..=14_i64 {
ts = ts * minus_q / Fix128::from_int((2 * n) * (2 * n + 1));
tk = tk * minus_q / Fix128::from_int((2 * n - 1) * (2 * n));
s = s + ts;
k = k + tk;
}
(s, k)
}
fn sinc_cos(q: Fix128) -> (Fix128, Fix128) {
let quarter = Fix128::from_ratio(1, 4);
let mut r = q;
let mut m = 0_u32;
while r > quarter {
r = r.shr_bits(2);
m += 1;
}
let (mut s, mut k) = sinc_cos_series(r);
for _ in 0..m {
s = s * k;
k = (k * k).double() - Fix128::ONE;
}
(s, k)
}
fn phi1(x: Fix128) -> Fix128 {
if x > Fix128::from_ratio(1, 2) {
return (Fix128::ONE - crate::physics2d::exp_neg(x)) / x;
}
let mut sum = Fix128::ONE;
let mut term = Fix128::ONE;
for n in 1..=26_i64 {
term = Fix128::ZERO - term * x / Fix128::from_int(n + 1);
sum = sum + term;
}
sum
}
fn impulse_integral_series(g: Fix128, s: Fix128) -> Fix128 {
let (mut b0, mut b1) = (Fix128::ZERO, Fix128::ONE);
let mut sum = Fix128::ONE.half();
for n in 0..40_i64 {
let b2 = Fix128::ZERO
- (g * b1 * Fix128::from_int(n + 1) + s * b0) / Fix128::from_int((n + 2) * (n + 1));
sum = sum + b2 / Fix128::from_int(n + 3);
b0 = b1;
b1 = b2;
}
sum
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RestPositionsMismatch {
pub expected: usize,
pub got: usize,
}
impl Cloth {
#[must_use]
pub fn new_grid(
origin: Vec3Fix,
width: Fix128,
height: Fix128,
res_x: usize,
res_y: usize,
mass_per_particle: Fix128,
) -> Self {
let n = res_x * res_y;
let inv_mass = if mass_per_particle.is_zero() {
Fix128::ZERO
} else {
Fix128::ONE / mass_per_particle
};
let recip_res_x = if res_x > 1 {
Fix128::ONE / Fix128::from_int((res_x - 1) as i64)
} else {
Fix128::ZERO
};
let recip_res_y = if res_y > 1 {
Fix128::ONE / Fix128::from_int((res_y - 1) as i64)
} else {
Fix128::ZERO
};
let mut positions = Vec::with_capacity(n);
for j in 0..res_y {
for i in 0..res_x {
let u = Fix128::from_int(i as i64) * recip_res_x;
let v = Fix128::from_int(j as i64) * recip_res_y;
positions.push(Vec3Fix::new(
origin.x + width * u,
origin.y,
origin.z + height * v,
));
}
}
let mut triangles = Vec::new();
for j in 0..(res_y - 1) {
for i in 0..(res_x - 1) {
let i00 = j * res_x + i;
let i10 = i00 + 1;
let i01 = i00 + res_x;
let i11 = i01 + 1;
triangles.push([i00, i10, i01]);
triangles.push([i10, i11, i01]);
}
}
let mut cloth = Self {
prev_positions: positions.clone(),
velocities: vec![Vec3Fix::ZERO; n],
positions,
inv_masses: vec![inv_mass; n],
triangles,
edge_constraints: Vec::new(),
bend_constraints: Vec::new(),
pinned: Vec::new(),
wind: Vec3Fix::ZERO,
config: ClothConfig::default(),
rest_tether: None,
rest_positions: Vec::new(),
};
cloth.build_constraints();
cloth
}
fn build_constraints(&mut self) {
let mut edge_set: Vec<(usize, usize)> = Vec::new();
for tri in &self.triangles {
let edges = [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])];
for (a, b) in edges {
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
if !edge_set.contains(&(lo, hi)) {
edge_set.push((lo, hi));
let rest = (self.positions[a] - self.positions[b]).length();
self.edge_constraints.push(EdgeConstraint {
i0: a,
i1: b,
rest_length: rest,
});
}
}
}
for i in 0..self.triangles.len() {
for j in (i + 1)..self.triangles.len() {
if let Some(bend) =
find_shared_edge(&self.triangles[i], &self.triangles[j], &self.positions)
{
self.bend_constraints.push(bend);
}
}
}
}
#[inline(always)]
#[must_use]
pub fn particle_count(&self) -> usize {
self.positions.len()
}
pub fn pin(&mut self, idx: usize) {
self.inv_masses[idx] = Fix128::ZERO;
if !self.pinned.contains(&idx) {
self.pinned.push(idx);
}
}
pub fn pin_top_row(&mut self, res_x: usize) {
for i in 0..res_x {
self.pin(i);
}
}
pub fn set_rest_tether(&mut self, stiffness: Fix128, damping: Fix128) {
if self.rest_positions.len() != self.positions.len() {
self.rest_positions = self.positions.clone();
}
let clamp = |x: Fix128| if x.is_negative() { Fix128::ZERO } else { x };
self.rest_tether = Some(RestTether {
stiffness: clamp(stiffness),
damping: clamp(damping),
});
}
pub fn set_rest_positions(&mut self, rest: &[Vec3Fix]) -> Result<(), RestPositionsMismatch> {
if rest.len() != self.positions.len() {
return Err(RestPositionsMismatch {
expected: self.positions.len(),
got: rest.len(),
});
}
self.rest_positions.clear();
self.rest_positions.extend_from_slice(rest);
Ok(())
}
pub fn clear_rest_tether(&mut self) {
self.rest_tether = None;
}
#[must_use]
pub fn rest_tether(&self) -> Option<(Fix128, Fix128)> {
self.rest_tether.map(|t| (t.stiffness, t.damping))
}
#[must_use]
pub fn rest_positions(&self) -> &[Vec3Fix] {
&self.rest_positions
}
#[inline(always)]
pub fn step(&mut self, dt: Fix128) {
let frame_start = self.snapshot_for_self_contact();
let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
for _ in 0..self.config.substeps {
self.substep(substep_dt);
}
if let Some(start) = frame_start {
self.resolve_self_contact_over_frame(&start, dt);
}
self.apply_frame_damping();
}
#[cfg(feature = "std")]
#[inline(always)]
pub fn step_with_sdf(&mut self, dt: Fix128, sdf_colliders: &[SdfCollider]) {
let frame_start = self.snapshot_for_self_contact();
let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
for _ in 0..self.config.substeps {
self.substep(substep_dt);
self.resolve_sdf_collisions(sdf_colliders);
}
if let Some(start) = frame_start {
self.resolve_self_contact_over_frame(&start, dt);
}
self.apply_frame_damping();
}
fn snapshot_for_self_contact(&self) -> Option<Vec<Vec3Fix>> {
if self.config.self_collision && !self.config.self_collision_distance.is_zero() {
Some(self.positions.clone())
} else {
None
}
}
fn apply_frame_damping(&mut self) {
let d = self.config.damping;
for v in &mut self.velocities {
*v = *v * d;
}
}
#[inline(always)]
fn substep(&mut self, dt: Fix128) {
let n = self.particle_count();
let tether = self
.rest_tether
.filter(|t| !(t.stiffness.is_zero() && t.damping.is_zero()));
let predicted = match tether {
Some(t) => self.predict_with_rest_tether(t, dt),
None => {
for i in 0..n {
if self.inv_masses[i].is_zero() {
continue;
}
self.prev_positions[i] = self.positions[i];
let wind_force = self.compute_wind_force(i);
self.velocities[i] = self.velocities[i]
+ (self.config.gravity + wind_force * self.inv_masses[i]) * dt;
self.positions[i] = self.positions[i] + self.velocities[i] * dt;
}
Vec::new()
}
};
let margin = self.config.self_collision_distance.double();
let (candidates, edge_candidates) = if self.config.self_collision {
(
self.collect_vertex_face_candidates(margin),
self.collect_edge_edge_candidates(margin),
)
} else {
(Vec::new(), Vec::new())
};
for _ in 0..self.config.iterations {
self.solve_edge_constraints(dt);
self.solve_bend_constraints(dt);
if self.config.self_collision {
self.solve_self_collision(&candidates, &edge_candidates);
}
}
let inv_dt = Fix128::ONE / dt;
if predicted.is_empty() {
for i in 0..n {
if self.inv_masses[i].is_zero() {
continue;
}
self.velocities[i] = (self.positions[i] - self.prev_positions[i]) * inv_dt;
}
} else {
for (i, x_pred) in predicted.iter().enumerate() {
if self.inv_masses[i].is_zero() {
continue;
}
self.velocities[i] = self.velocities[i] + (self.positions[i] - *x_pred) * inv_dt;
}
}
}
fn predict_with_rest_tether(&mut self, tether: RestTether, h: Fix128) -> Vec<Vec3Fix> {
let n = self.particle_count();
let mut predicted = Vec::with_capacity(n);
let mut cache: Option<(Fix128, OscillatorStep)> = None;
for i in 0..n {
let w = self.inv_masses[i];
let x = self.positions[i];
if w.is_zero() {
predicted.push(x);
continue;
}
let step = match cache {
Some((cw, s)) if cw == w => s,
_ => {
let s = OscillatorStep::new(tether.stiffness * w, tether.damping * w, h);
cache = Some((w, s));
s
}
};
self.prev_positions[i] = x;
let accel = self.config.gravity + self.compute_wind_force(i) * w;
let rest = self.rest_positions[i];
let e0 = x - rest;
let v0 = self.velocities[i];
let x1 = rest + e0 * step.pos_e + v0 * step.pos_v + accel * step.pos_a;
let v1 = e0 * step.vel_e + v0 * step.vel_v + accel * step.vel_a;
self.positions[i] = x1;
self.velocities[i] = v1;
predicted.push(x1);
}
predicted
}
#[inline(always)]
const fn compute_wind_force(&self, _particle_idx: usize) -> Vec3Fix {
self.wind
}
#[inline(always)]
fn solve_edge_constraints(&mut self, dt: Fix128) {
let compliance = self.config.stretch_compliance / (dt * dt);
for c_idx in 0..self.edge_constraints.len() {
let c = self.edge_constraints[c_idx];
let p0 = self.positions[c.i0];
let p1 = self.positions[c.i1];
let w0 = self.inv_masses[c.i0];
let w1 = self.inv_masses[c.i1];
let w_sum = w0 + w1 + compliance;
if w_sum.is_zero() {
continue;
}
let delta = p1 - p0;
let dist = delta.length();
if dist.is_zero() {
continue;
}
let error = dist - c.rest_length;
let lambda = error / w_sum;
let inv_dist = Fix128::ONE / dist;
let correction = delta * inv_dist * lambda;
if !w0.is_zero() {
self.positions[c.i0] = self.positions[c.i0] + correction * w0;
}
if !w1.is_zero() {
self.positions[c.i1] = self.positions[c.i1] - correction * w1;
}
}
}
#[inline(always)]
#[allow(clippy::many_single_char_names, clippy::similar_names)]
fn solve_bend_constraints(&mut self, dt: Fix128) {
let compliance = self.config.bend_compliance / (dt * dt);
for c_idx in 0..self.bend_constraints.len() {
let c = self.bend_constraints[c_idx];
let p0 = self.positions[c.i0];
let p1 = self.positions[c.i1] - p0;
let p2 = self.positions[c.i2] - p0;
let p3 = self.positions[c.i3] - p0;
let c12 = p1.cross(p2);
let c13 = p1.cross(p3);
let len12 = c12.length();
let len13 = c13.length();
if len12.is_zero() || len13.is_zero() {
continue;
}
let inv12 = Fix128::ONE / len12;
let inv13 = Fix128::ONE / len13;
let n1 = c12 * inv12;
let n2 = c13 * inv13;
let d = n1.dot(n2);
let sin_angle = n1.cross(n2).length();
let angle = Fix128::atan2(sin_angle, d);
let error = angle - c.rest_angle;
let q2 = (p1.cross(n2) + (n1.cross(p1)) * d) * inv12;
let q3 = (p1.cross(n1) + (n2.cross(p1)) * d) * inv13;
let q1 = (p2.cross(n2) + (n1.cross(p2)) * d) * (-inv12)
+ (p3.cross(n1) + (n2.cross(p3)) * d) * (-inv13);
let q0 = -(q1 + q2 + q3);
let w0 = self.inv_masses[c.i0];
let w1 = self.inv_masses[c.i1];
let w2 = self.inv_masses[c.i2];
let w3 = self.inv_masses[c.i3];
let denom = w0 * q0.length_squared()
+ w1 * q1.length_squared()
+ w2 * q2.length_squared()
+ w3 * q3.length_squared()
+ compliance;
if denom.is_zero() {
continue;
}
let scale = sin_angle * error / denom;
if scale.is_zero() {
continue;
}
if !w0.is_zero() {
self.positions[c.i0] = self.positions[c.i0] - q0 * (w0 * scale);
}
if !w1.is_zero() {
self.positions[c.i1] = self.positions[c.i1] - q1 * (w1 * scale);
}
if !w2.is_zero() {
self.positions[c.i2] = self.positions[c.i2] - q2 * (w2 * scale);
}
if !w3.is_zero() {
self.positions[c.i3] = self.positions[c.i3] - q3 * (w3 * scale);
}
}
}
fn collect_vertex_face_candidates(&self, margin: Fix128) -> Vec<(u32, u32)> {
let n = self.particle_count();
let mut out: Vec<(u32, u32)> = Vec::new();
for (ti, tri) in self.triangles.iter().enumerate() {
let (a, b, c) = (
self.positions[tri[0]],
self.positions[tri[1]],
self.positions[tri[2]],
);
let tri_pinned = self.inv_masses[tri[0]].is_zero()
&& self.inv_masses[tri[1]].is_zero()
&& self.inv_masses[tri[2]].is_zero();
let lo = Vec3Fix::new(
min3(a.x, b.x, c.x) - margin,
min3(a.y, b.y, c.y) - margin,
min3(a.z, b.z, c.z) - margin,
);
let hi = Vec3Fix::new(
max3(a.x, b.x, c.x) + margin,
max3(a.y, b.y, c.y) + margin,
max3(a.z, b.z, c.z) + margin,
);
for i in 0..n {
if i == tri[0] || i == tri[1] || i == tri[2] {
continue;
}
if tri_pinned && self.inv_masses[i].is_zero() {
continue; }
let p = self.positions[i];
if p.x < lo.x || p.x > hi.x || p.y < lo.y || p.y > hi.y || p.z < lo.z || p.z > hi.z
{
continue;
}
out.push((i as u32, ti as u32));
}
}
out
}
fn mesh_edges(&self) -> Vec<[u32; 2]> {
let mut edges: Vec<[u32; 2]> = Vec::with_capacity(self.triangles.len() * 3);
for tri in &self.triangles {
for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
let (lo, hi) = if a < b { (a, b) } else { (b, a) };
edges.push([lo as u32, hi as u32]);
}
}
edges.sort_unstable();
edges.dedup();
edges
}
fn collect_edge_edge_candidates(&self, margin: Fix128) -> Vec<[u32; 4]> {
let edges = self.mesh_edges();
let mut out: Vec<[u32; 4]> = Vec::new();
for (ei, e1) in edges.iter().enumerate() {
let (a0, a1) = (e1[0] as usize, e1[1] as usize);
let (pa0, pa1) = (self.positions[a0], self.positions[a1]);
let lo1 = Vec3Fix::new(
min2(pa0.x, pa1.x) - margin,
min2(pa0.y, pa1.y) - margin,
min2(pa0.z, pa1.z) - margin,
);
let hi1 = Vec3Fix::new(
max2(pa0.x, pa1.x) + margin,
max2(pa0.y, pa1.y) + margin,
max2(pa0.z, pa1.z) + margin,
);
let both_pinned_1 = self.inv_masses[a0].is_zero() && self.inv_masses[a1].is_zero();
for e2 in &edges[ei + 1..] {
let (b0, b1) = (e2[0] as usize, e2[1] as usize);
if a0 == b0 || a0 == b1 || a1 == b0 || a1 == b1 {
continue; }
if both_pinned_1 && self.inv_masses[b0].is_zero() && self.inv_masses[b1].is_zero() {
continue; }
let (pb0, pb1) = (self.positions[b0], self.positions[b1]);
if min2(pb0.x, pb1.x) > hi1.x
|| max2(pb0.x, pb1.x) < lo1.x
|| min2(pb0.y, pb1.y) > hi1.y
|| max2(pb0.y, pb1.y) < lo1.y
|| min2(pb0.z, pb1.z) > hi1.z
|| max2(pb0.z, pb1.z) < lo1.z
{
continue;
}
out.push([a0 as u32, a1 as u32, b0 as u32, b1 as u32]);
}
}
out
}
fn collect_swept_edge_edge_candidates(
&self,
start: &[Vec3Fix],
margin: Fix128,
) -> Vec<[u32; 4]> {
let edges = self.mesh_edges();
let n = self.particle_count().min(start.len());
let swept_box = |i: usize, j: usize| {
let pts = [start[i], start[j], self.positions[i], self.positions[j]];
let lo = Vec3Fix::new(
min2(min2(pts[0].x, pts[1].x), min2(pts[2].x, pts[3].x)) - margin,
min2(min2(pts[0].y, pts[1].y), min2(pts[2].y, pts[3].y)) - margin,
min2(min2(pts[0].z, pts[1].z), min2(pts[2].z, pts[3].z)) - margin,
);
let hi = Vec3Fix::new(
max2(max2(pts[0].x, pts[1].x), max2(pts[2].x, pts[3].x)) + margin,
max2(max2(pts[0].y, pts[1].y), max2(pts[2].y, pts[3].y)) + margin,
max2(max2(pts[0].z, pts[1].z), max2(pts[2].z, pts[3].z)) + margin,
);
(lo, hi)
};
let mut out: Vec<[u32; 4]> = Vec::new();
for (ei, e1) in edges.iter().enumerate() {
let (a0, a1) = (e1[0] as usize, e1[1] as usize);
if a0 >= n || a1 >= n {
continue; }
let (lo1, hi1) = swept_box(a0, a1);
let both_pinned_1 = self.inv_masses[a0].is_zero() && self.inv_masses[a1].is_zero();
for e2 in &edges[ei + 1..] {
let (b0, b1) = (e2[0] as usize, e2[1] as usize);
if b0 >= n || b1 >= n {
continue;
}
if a0 == b0 || a0 == b1 || a1 == b0 || a1 == b1 {
continue; }
if both_pinned_1 && self.inv_masses[b0].is_zero() && self.inv_masses[b1].is_zero() {
continue; }
let (lo2, hi2) = swept_box(b0, b1);
if lo2.x > hi1.x
|| hi2.x < lo1.x
|| lo2.y > hi1.y
|| hi2.y < lo1.y
|| lo2.z > hi1.z
|| hi2.z < lo1.z
{
continue;
}
out.push([a0 as u32, a1 as u32, b0 as u32, b1 as u32]);
}
}
out
}
fn accumulate_vertex_face_contacts(
&self,
candidates: &[(u32, u32)],
thickness: Fix128,
delta: &mut [Vec3Fix],
hits: &mut [u32],
) {
let thickness_sq = thickness * thickness;
for &(vi, ti) in candidates {
let vi = vi as usize;
let tri = self.triangles[ti as usize];
let p = self.positions[vi];
let (a, b, c) = (
self.positions[tri[0]],
self.positions[tri[1]],
self.positions[tri[2]],
);
let Some((q, w)) = closest_point_on_triangle(p, a, b, c) else {
continue; };
let diff = p - q;
let dist_sq = diff.length_squared();
if dist_sq >= thickness_sq {
continue;
}
let (normal, dist) = if dist_sq.is_zero() {
match (b - a).cross(c - a).try_normalize() {
Some(nrm) => (nrm, Fix128::ZERO),
None => continue,
}
} else {
let d = dist_sq.sqrt();
(diff * (Fix128::ONE / d), d)
};
let (wp, wa, wb, wc) = (
self.inv_masses[vi],
self.inv_masses[tri[0]],
self.inv_masses[tri[1]],
self.inv_masses[tri[2]],
);
let denom = wp + wa * w[0] * w[0] + wb * w[1] * w[1] + wc * w[2] * w[2];
if denom.is_zero() {
continue;
}
let s = (thickness - dist) / denom;
delta[vi] = delta[vi] + normal * (s * wp);
hits[vi] += 1;
for (k, &idx) in tri.iter().enumerate() {
let wk = self.inv_masses[idx];
delta[idx] = delta[idx] - normal * (s * wk * w[k]);
hits[idx] += 1;
}
}
}
fn accumulate_edge_edge_contacts(
&self,
candidates: &[[u32; 4]],
thickness: Fix128,
delta: &mut [Vec3Fix],
hits: &mut [u32],
) {
let thickness_sq = thickness * thickness;
for &[i0, i1, j0, j1] in candidates {
let (i0, i1, j0, j1) = (i0 as usize, i1 as usize, j0 as usize, j1 as usize);
let (p1, q1) = (self.positions[i0], self.positions[i1]);
let (p2, q2) = (self.positions[j0], self.positions[j1]);
let Some((s, t)) = closest_points_on_segments(p1, q1, p2, q2) else {
continue; };
let (d1, d2) = (q1 - p1, q2 - p2);
let pa = p1 + d1 * s;
let pb = p2 + d2 * t;
let diff = pa - pb;
let dist_sq = diff.length_squared();
if dist_sq >= thickness_sq {
continue;
}
let (normal, dist) = if dist_sq.is_zero() {
match d1.cross(d2).try_normalize() {
Some(nrm) => (nrm, Fix128::ZERO),
None => continue,
}
} else {
let d = dist_sq.sqrt();
(diff * (Fix128::ONE / d), d)
};
let (w0, w1, w2, w3) = (
self.inv_masses[i0],
self.inv_masses[i1],
self.inv_masses[j0],
self.inv_masses[j1],
);
let (u, v) = (Fix128::ONE - s, Fix128::ONE - t);
let denom = w0 * u * u + w1 * s * s + w2 * v * v + w3 * t * t;
if denom.is_zero() {
continue;
}
let lambda = (thickness - dist) / denom;
delta[i0] = delta[i0] + normal * (lambda * w0 * u);
delta[i1] = delta[i1] + normal * (lambda * w1 * s);
delta[j0] = delta[j0] - normal * (lambda * w2 * v);
delta[j1] = delta[j1] - normal * (lambda * w3 * t);
hits[i0] += 1;
hits[i1] += 1;
hits[j0] += 1;
hits[j1] += 1;
}
}
fn solve_self_collision(&mut self, vertex_face: &[(u32, u32)], edge_edge: &[[u32; 4]]) {
let thickness = self.config.self_collision_distance;
if thickness.is_zero() || (vertex_face.is_empty() && edge_edge.is_empty()) {
return;
}
let n = self.particle_count();
let mut delta = vec![Vec3Fix::ZERO; n];
let mut hits = vec![0u32; n];
self.accumulate_vertex_face_contacts(vertex_face, thickness, &mut delta, &mut hits);
self.accumulate_edge_edge_contacts(edge_edge, thickness, &mut delta, &mut hits);
for i in 0..n {
if hits[i] == 0 || self.inv_masses[i].is_zero() {
continue;
}
self.positions[i] = self.positions[i] + delta[i] / Fix128::from_int(i64::from(hits[i]));
}
}
#[cfg(test)]
fn solve_vertex_face_self_collision(&mut self, candidates: &[(u32, u32)]) {
self.solve_self_collision(candidates, &[]);
}
#[cfg(test)]
fn solve_edge_edge_self_collision(&mut self, candidates: &[[u32; 4]]) {
self.solve_self_collision(&[], candidates);
}
const SELF_CONTACT_PASSES: usize = 16;
fn accumulate_swept_edge_edge_repairs(
&self,
candidates: &[[u32; 4]],
start: &[Vec3Fix],
thickness: Fix128,
delta: &mut [Vec3Fix],
hits: &mut [u32],
) -> usize {
let mut corrected = 0usize;
for &[i0, i1, j0, j1] in candidates {
let (i0, i1, j0, j1) = (i0 as usize, i1 as usize, j0 as usize, j1 as usize);
let Some(contact) = edge_pair_frame_contact(
(start[i0], self.positions[i0]),
(start[i1], self.positions[i1]),
(start[j0], self.positions[j0]),
(start[j1], self.positions[j1]),
thickness,
) else {
continue;
};
if contact.gap >= Fix128::ZERO {
continue;
}
let (s, t) = (contact.s, contact.t);
let (u, v) = (Fix128::ONE - s, Fix128::ONE - t);
let (w0, w1, w2, w3) = (
self.inv_masses[i0],
self.inv_masses[i1],
self.inv_masses[j0],
self.inv_masses[j1],
);
let denom = w0 * u * u + w1 * s * s + w2 * v * v + w3 * t * t;
if denom.is_zero() {
continue;
}
let lambda = -contact.gap / denom;
let normal = contact.normal;
delta[i0] = delta[i0] + normal * (lambda * w0 * u);
delta[i1] = delta[i1] + normal * (lambda * w1 * s);
delta[j0] = delta[j0] - normal * (lambda * w2 * v);
delta[j1] = delta[j1] - normal * (lambda * w3 * t);
hits[i0] += 1;
hits[i1] += 1;
hits[j0] += 1;
hits[j1] += 1;
corrected += 1;
}
corrected
}
fn resolve_self_contact_over_frame(&mut self, start: &[Vec3Fix], dt: Fix128) {
let thickness = self.config.self_collision_distance;
let n = self.particle_count();
let inv_dt = if dt.is_zero() {
Fix128::ZERO
} else {
Fix128::ONE / dt
};
let mut delta = vec![Vec3Fix::ZERO; n];
let mut hits = vec![0u32; n];
let mut total = vec![Vec3Fix::ZERO; n];
let edge_pairs = self.collect_swept_edge_edge_candidates(start, thickness);
for _ in 0..Self::SELF_CONTACT_PASSES {
delta.fill(Vec3Fix::ZERO);
hits.fill(0);
let mut found = 0usize;
found += self.accumulate_swept_edge_edge_repairs(
&edge_pairs,
start,
thickness,
&mut delta,
&mut hits,
);
for tri in &self.triangles {
let (a, b, c) = (
self.positions[tri[0]],
self.positions[tri[1]],
self.positions[tri[2]],
);
let lo = Vec3Fix::new(
min3(a.x, b.x, c.x) - thickness,
min3(a.y, b.y, c.y) - thickness,
min3(a.z, b.z, c.z) - thickness,
);
let hi = Vec3Fix::new(
max3(a.x, b.x, c.x) + thickness,
max3(a.y, b.y, c.y) + thickness,
max3(a.z, b.z, c.z) + thickness,
);
let Some(face) = (b - a).cross(c - a).try_normalize() else {
continue; };
for i in 0..n {
if i == tri[0] || i == tri[1] || i == tri[2] {
continue;
}
let p0 = start[i];
let p1 = self.positions[i];
if min2(p0.x, p1.x) > hi.x
|| max2(p0.x, p1.x) < lo.x
|| min2(p0.y, p1.y) > hi.y
|| max2(p0.y, p1.y) < lo.y
|| min2(p0.z, p1.z) > hi.z
|| max2(p0.z, p1.z) < lo.z
{
continue;
}
let Some((t_scaled, det)) = segment_pierces_triangle(p0, p1, a, b, c) else {
continue;
};
let t = t_scaled / det;
let entry = p0 + (p1 - p0) * t;
let Some((q, w)) = closest_point_on_triangle(entry, a, b, c) else {
continue;
};
let normal = if face.dot(p0 - q) < Fix128::ZERO {
-face
} else {
face
};
let (wp, wa, wb, wc) = (
self.inv_masses[i],
self.inv_masses[tri[0]],
self.inv_masses[tri[1]],
self.inv_masses[tri[2]],
);
let denom = wp + wa * w[0] * w[0] + wb * w[1] * w[1] + wc * w[2] * w[2];
if denom.is_zero() {
continue;
}
let c_val = normal.dot(p1 - q) - thickness;
if c_val >= Fix128::ZERO {
continue;
}
let s = c_val / denom;
found += 1;
delta[i] = delta[i] - normal * (s * wp);
hits[i] += 1;
for (k, &idx) in tri.iter().enumerate() {
delta[idx] = delta[idx] + normal * (s * self.inv_masses[idx] * w[k]);
hits[idx] += 1;
}
}
}
if found == 0 {
break;
}
for i in 0..n {
if hits[i] == 0 || self.inv_masses[i].is_zero() {
continue;
}
let applied = delta[i] / Fix128::from_int(i64::from(hits[i]));
self.positions[i] = self.positions[i] + applied;
total[i] = total[i] + applied;
}
}
for (i, moved) in total.iter().enumerate() {
if self.inv_masses[i].is_zero() {
continue;
}
self.velocities[i] = self.velocities[i] + *moved * inv_dt;
}
}
#[must_use]
pub fn remaining_self_contact_crossings(&self, start: &[Vec3Fix]) -> usize {
let n = self.particle_count().min(start.len());
let mut count = 0usize;
for tri in &self.triangles {
let (a, b, c) = (
self.positions[tri[0]],
self.positions[tri[1]],
self.positions[tri[2]],
);
for (i, from) in start.iter().enumerate().take(n) {
if i == tri[0] || i == tri[1] || i == tri[2] {
continue;
}
if segment_pierces_triangle(*from, self.positions[i], a, b, c).is_some() {
count += 1;
}
}
}
let thickness = self.config.self_collision_distance;
for [i0, i1, j0, j1] in self.collect_swept_edge_edge_candidates(start, thickness) {
let (i0, i1, j0, j1) = (i0 as usize, i1 as usize, j0 as usize, j1 as usize);
let Some(contact) = edge_pair_frame_contact(
(start[i0], self.positions[i0]),
(start[i1], self.positions[i1]),
(start[j0], self.positions[j0]),
(start[j1], self.positions[j1]),
thickness,
) else {
continue;
};
if contact.gap < Fix128::ZERO {
count += 1;
}
}
count
}
#[cfg(feature = "std")]
#[inline(always)]
fn resolve_sdf_collisions(&mut self, sdf_colliders: &[SdfCollider]) {
let thickness = self.config.thickness.to_f32();
for i in 0..self.particle_count() {
if self.inv_masses[i].is_zero() {
continue;
}
for sdf in sdf_colliders {
let (lx, ly, lz) = sdf.world_to_local(self.positions[i]);
let dist = sdf.field.distance(lx, ly, lz) * sdf.scale_f32;
if dist < thickness {
let (nx, ny, nz) = sdf.field.normal(lx, ly, lz);
let normal = sdf.local_normal_to_world(nx, ny, nz);
let push = Fix128::from_f32(thickness - dist);
self.positions[i] = self.positions[i] + normal * push;
let vel = self.velocities[i];
let vn = normal * vel.dot(normal);
let vt = vel - vn;
self.velocities[i] = vt * (Fix128::ONE - self.config.sdf_friction);
}
}
}
}
#[must_use]
pub fn compute_normals(&self) -> Vec<Vec3Fix> {
let mut normals = vec![Vec3Fix::ZERO; self.particle_count()];
for tri in &self.triangles {
let e1 = self.positions[tri[1]] - self.positions[tri[0]];
let e2 = self.positions[tri[2]] - self.positions[tri[0]];
let face_normal = e1.cross(e2);
normals[tri[0]] = normals[tri[0]] + face_normal;
normals[tri[1]] = normals[tri[1]] + face_normal;
normals[tri[2]] = normals[tri[2]] + face_normal;
}
for n in &mut normals {
*n = n.normalize();
}
normals
}
}
impl core::fmt::Debug for Cloth {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Cloth")
.field(
"positions",
&format_args!("[{} items]", self.positions.len()),
)
.field(
"prev_positions",
&format_args!("[{} items]", self.prev_positions.len()),
)
.field(
"velocities",
&format_args!("[{} items]", self.velocities.len()),
)
.field(
"inv_masses",
&format_args!("[{} items]", self.inv_masses.len()),
)
.field(
"triangles",
&format_args!("[{} items]", self.triangles.len()),
)
.field(
"edge_constraints",
&format_args!("[{} items]", self.edge_constraints.len()),
)
.field(
"bend_constraints",
&format_args!("[{} items]", self.bend_constraints.len()),
)
.field("pinned", &format_args!("[{} items]", self.pinned.len()))
.field("wind", &self.wind)
.field("config", &self.config)
.field("rest_tether", &self.rest_tether)
.field(
"rest_positions",
&format_args!("[{} items]", self.rest_positions.len()),
)
.finish()
}
}
fn min2(a: Fix128, b: Fix128) -> Fix128 {
if a < b {
a
} else {
b
}
}
fn max2(a: Fix128, b: Fix128) -> Fix128 {
if a > b {
a
} else {
b
}
}
#[allow(clippy::many_single_char_names)]
fn segment_pierces_triangle(
p0: Vec3Fix,
p1: Vec3Fix,
a: Vec3Fix,
b: Vec3Fix,
c: Vec3Fix,
) -> Option<(Fix128, Fix128)> {
let dir = p1 - p0;
let e1 = b - a;
let e2 = c - a;
let h = dir.cross(e2);
let mut det = e1.dot(h);
if det.is_zero() {
return None; }
let s = p0 - a;
let q = s.cross(e1);
let mut u = s.dot(h);
let mut v = dir.dot(q);
let mut t = e2.dot(q);
if det < Fix128::ZERO {
det = Fix128::ZERO - det;
u = Fix128::ZERO - u;
v = Fix128::ZERO - v;
t = Fix128::ZERO - t;
}
if u < Fix128::ZERO || v < Fix128::ZERO || u + v > det {
return None; }
if t < Fix128::ZERO || t > det {
return None; }
Some((t, det))
}
fn min3(a: Fix128, b: Fix128, c: Fix128) -> Fix128 {
let m = if a < b { a } else { b };
if m < c {
m
} else {
c
}
}
fn max3(a: Fix128, b: Fix128, c: Fix128) -> Fix128 {
let m = if a > b { a } else { b };
if m > c {
m
} else {
c
}
}
#[allow(clippy::many_single_char_names, clippy::similar_names)]
fn closest_point_on_triangle(
p: Vec3Fix,
a: Vec3Fix,
b: Vec3Fix,
c: Vec3Fix,
) -> Option<(Vec3Fix, [Fix128; 3])> {
const ZERO: Fix128 = Fix128::ZERO;
const ONE: Fix128 = Fix128::ONE;
let ab = b - a;
let ac = c - a;
let area2 = ab.cross(ac);
if area2.x.is_zero() && area2.y.is_zero() && area2.z.is_zero() {
return None;
}
let ap = p - a;
let d1 = ab.dot(ap);
let d2 = ac.dot(ap);
if d1 <= ZERO && d2 <= ZERO {
return Some((a, [ONE, ZERO, ZERO]));
}
let bp = p - b;
let d3 = ab.dot(bp);
let d4 = ac.dot(bp);
if d3 >= ZERO && d4 <= d3 {
return Some((b, [ZERO, ONE, ZERO]));
}
let vc = d1 * d4 - d3 * d2;
if vc <= ZERO && d1 >= ZERO && d3 <= ZERO {
let den = d1 - d3;
if den.is_zero() {
return None; }
let v = d1 / den;
return Some((a + ab * v, [ONE - v, v, ZERO]));
}
let cp = p - c;
let d5 = ab.dot(cp);
let d6 = ac.dot(cp);
if d6 >= ZERO && d5 <= d6 {
return Some((c, [ZERO, ZERO, ONE]));
}
let vb = d5 * d2 - d1 * d6;
if vb <= ZERO && d2 >= ZERO && d6 <= ZERO {
let den = d2 - d6;
if den.is_zero() {
return None; }
let w = d2 / den;
return Some((a + ac * w, [ONE - w, ZERO, w]));
}
let va = d3 * d6 - d5 * d4;
let e_b = d4 - d3;
let e_c = d5 - d6;
if va <= ZERO && e_b >= ZERO && e_c >= ZERO {
let den = e_b + e_c;
if den.is_zero() {
return None; }
let w = e_b / den;
return Some((b + (c - b) * w, [ZERO, ONE - w, w]));
}
let den = va + vb + vc;
if den.is_zero() {
return None; }
let inv = ONE / den;
let v = vb * inv;
let w = vc * inv;
Some((a + ab * v + ac * w, [ONE - v - w, v, w]))
}
fn closest_points_on_segments(
p1: Vec3Fix,
q1: Vec3Fix,
p2: Vec3Fix,
q2: Vec3Fix,
) -> Option<(Fix128, Fix128)> {
const ZERO: Fix128 = Fix128::ZERO;
const ONE: Fix128 = Fix128::ONE;
let d1 = q1 - p1;
let d2 = q2 - p2;
let a = d1.length_squared();
let e = d2.length_squared();
if a.is_zero() || e.is_zero() {
return None; }
let b = d1.dot(d2);
let denom = a * e - b * b;
if denom.is_zero() {
return None; }
let r = p1 - p2;
let c = d1.dot(r);
let f = d2.dot(r);
let clamp01 = |x: Fix128| {
if x < ZERO {
ZERO
} else if x > ONE {
ONE
} else {
x
}
};
let mut s = clamp01((b * f - c * e) / denom);
let mut t = (b * s + f) / e;
if t < ZERO {
t = ZERO;
s = clamp01(-c / a);
} else if t > ONE {
t = ONE;
s = clamp01((b - c) / a);
}
Some((s, t))
}
#[inline]
fn at_frame_time(start: Vec3Fix, end: Vec3Fix, t: Fix128) -> Vec3Fix {
start + (end - start) * t
}
#[inline]
fn is_interior(x: Fix128) -> bool {
x > Fix128::ZERO && x < Fix128::ONE
}
const COPLANARITY_SUBDIVISION_DEPTH: u32 = 12;
fn coplanarity_control_points(
a0: (Vec3Fix, Vec3Fix),
a1: (Vec3Fix, Vec3Fix),
b0: (Vec3Fix, Vec3Fix),
b1: (Vec3Fix, Vec3Fix),
) -> [Fix128; 4] {
let av = a1.0 - a0.0;
let ad = (a1.1 - a1.0) - (a0.1 - a0.0);
let bv = b1.0 - b0.0;
let bd = (b1.1 - b1.0) - (b0.1 - b0.0);
let cv = b0.0 - a0.0;
let cd = (b0.1 - b0.0) - (a0.1 - a0.0);
let p0 = av.cross(bv);
let p1 = av.cross(bd) + ad.cross(bv);
let p2 = ad.cross(bd);
let k0 = p0.dot(cv);
let k1 = p0.dot(cd) + p1.dot(cv);
let k2 = p1.dot(cd) + p2.dot(cv);
let k3 = p2.dot(cd);
let two = Fix128::from_int(2);
let three = Fix128::from_int(3);
[
three * k0,
three * k0 + k1,
three * k0 + two * k1 + k2,
three * (k0 + k1 + k2 + k3),
]
}
fn bezier_split_half(c: [Fix128; 4]) -> ([Fix128; 4], [Fix128; 4]) {
let h = Fix128::from_ratio(1, 2);
let l1 = (c[0] + c[1]) * h;
let m = (c[1] + c[2]) * h;
let r2 = (c[2] + c[3]) * h;
let l2 = (l1 + m) * h;
let r1 = (m + r2) * h;
let mid = (l2 + r1) * h;
([c[0], l1, l2, mid], [mid, r1, r2, c[3]])
}
fn push_coplanarity_root_times(
c: [Fix128; 4],
lo: Fix128,
hi: Fix128,
depth: u32,
out: &mut [Fix128; 4],
n: &mut usize,
) {
if *n == out.len() {
return;
}
let all_positive = c.iter().all(|v| *v > Fix128::ZERO);
let all_negative = c.iter().all(|v| *v < Fix128::ZERO);
if all_positive || all_negative {
return; }
let mid = (lo + hi) * Fix128::from_ratio(1, 2);
if depth == 0 {
out[*n] = mid;
*n += 1;
return;
}
let (left, right) = bezier_split_half(c);
push_coplanarity_root_times(left, lo, mid, depth - 1, out, n);
push_coplanarity_root_times(right, mid, hi, depth - 1, out, n);
}
struct FrameEdgeContact {
s: Fix128,
t: Fix128,
normal: Vec3Fix,
gap: Fix128,
}
fn edge_pair_frame_contact(
a0: (Vec3Fix, Vec3Fix),
a1: (Vec3Fix, Vec3Fix),
b0: (Vec3Fix, Vec3Fix),
b1: (Vec3Fix, Vec3Fix),
thickness: Fix128,
) -> Option<FrameEdgeContact> {
let control = coplanarity_control_points(a0, a1, b0, b1);
let mut times = [Fix128::ZERO; 4];
let mut found = 0usize;
push_coplanarity_root_times(
control,
Fix128::ZERO,
Fix128::ONE,
COPLANARITY_SUBDIVISION_DEPTH,
&mut times,
&mut found,
);
if found == 0 {
return None; }
let thickness_sq = thickness * thickness;
let mut met = false;
for tau in ×[..found] {
let pa0 = at_frame_time(a0.0, a0.1, *tau);
let pa1 = at_frame_time(a1.0, a1.1, *tau);
let pb0 = at_frame_time(b0.0, b0.1, *tau);
let pb1 = at_frame_time(b1.0, b1.1, *tau);
let Some((s, t)) = closest_points_on_segments(pa0, pa1, pb0, pb1) else {
continue; };
if !is_interior(s) || !is_interior(t) {
continue; }
let ca = pa0 + (pa1 - pa0) * s;
let cb = pb0 + (pb1 - pb0) * t;
if (ca - cb).length_squared() < thickness_sq {
met = true;
break;
}
}
if !met {
return None;
}
let start_difference = closest_points_on_segments(a0.0, a1.0, b0.0, b1.0).map(|(s, t)| {
let ca = a0.0 + (a1.0 - a0.0) * s;
let cb = b0.0 + (b1.0 - b0.0) * t;
ca - cb
});
let normal = match start_difference.and_then(Vec3Fix::try_normalize) {
Some(n) => n,
None => (a1.0 - a0.0).cross(b1.0 - b0.0).try_normalize()?,
};
let (s, t) = closest_points_on_segments(a0.1, a1.1, b0.1, b1.1)?;
let ca = a0.1 + (a1.1 - a0.1) * s;
let cb = b0.1 + (b1.1 - b0.1) * t;
Some(FrameEdgeContact {
s,
t,
normal,
gap: normal.dot(ca - cb),
})
}
fn find_shared_edge(
tri_a: &[usize; 3],
tri_b: &[usize; 3],
positions: &[Vec3Fix],
) -> Option<BendConstraint> {
for ia in 0..3 {
for ib in 0..3 {
let a0 = tri_a[ia];
let a1 = tri_a[(ia + 1) % 3];
let b0 = tri_b[ib];
let b1 = tri_b[(ib + 1) % 3];
if (a0 == b0 && a1 == b1) || (a0 == b1 && a1 == b0) {
let opposite_a = tri_a[(ia + 2) % 3];
let opposite_b = tri_b[(ib + 2) % 3];
let edge = positions[a1] - positions[a0];
let n1 = edge.cross(positions[opposite_a] - positions[a0]);
let n2 = edge.cross(positions[opposite_b] - positions[a0]);
let n1_len = n1.length();
let n2_len = n2.length();
let rest_angle = if n1_len.is_zero() || n2_len.is_zero() {
Fix128::ZERO
} else {
let n1u = n1 / n1_len;
let n2u = n2 / n2_len;
let cos_val = n1u.dot(n2u);
let sin_val = n1u.cross(n2u).length();
Fix128::atan2(sin_val, cos_val)
};
return Some(BendConstraint {
i0: a0,
i1: a1,
i2: opposite_a,
i3: opposite_b,
rest_angle,
});
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(not(feature = "std"))]
use alloc::format;
#[test]
fn test_cloth_creation() {
let cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(2),
Fix128::from_int(2),
5,
5,
Fix128::from_ratio(1, 100),
);
assert_eq!(cloth.particle_count(), 25);
assert!(
!cloth.edge_constraints.is_empty(),
"Should have edge constraints"
);
}
#[test]
fn test_cloth_pinning() {
let mut cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(2),
Fix128::from_int(2),
5,
5,
Fix128::from_ratio(1, 100),
);
cloth.pin_top_row(5);
assert_eq!(cloth.pinned.len(), 5);
for i in 0..5 {
assert!(cloth.inv_masses[i].is_zero(), "Top row should be pinned");
}
}
#[test]
fn test_cloth_drape() {
let mut cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(2),
Fix128::from_int(2),
5,
5,
Fix128::from_ratio(1, 100),
);
cloth.pin_top_row(5);
let dt = Fix128::from_ratio(1, 60);
for _ in 0..120 {
cloth.step(dt);
}
let initial_y = cloth.positions[0].y.to_f32(); let bottom_y = cloth.positions[20].y.to_f32(); assert!(
bottom_y < initial_y,
"Bottom should be below top, top={initial_y}, bottom={bottom_y}"
);
}
#[test]
fn test_cloth_self_collision() {
fn folded(self_collision: bool) -> Fix128 {
const R: usize = 5;
let mut cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(4),
Fix128::from_int(4),
R,
R,
Fix128::from_ratio(1, 100),
);
cloth.config.self_collision = self_collision;
cloth.config.self_collision_distance = Fix128::from_ratio(3, 2);
cloth.config.gravity = Vec3Fix::ZERO;
for x in 0..R {
cloth.positions[3 * R + x] =
Vec3Fix::new(cloth.positions[x].x, Fix128::ONE, Fix128::from_int(2));
cloth.positions[4 * R + x] =
Vec3Fix::new(cloth.positions[x].x, Fix128::ONE, Fix128::ONE);
}
cloth.prev_positions = cloth.positions.clone();
for x in 0..R {
cloth.inv_masses[x] = Fix128::ZERO;
}
cloth.step(Fix128::from_ratio(1, 60));
let mut gap = Fix128::from_int(1 << 20);
for x in 0..R {
let d = (cloth.positions[4 * R + x] - cloth.positions[R + x]).length();
if d < gap {
gap = d;
}
}
gap
}
let gap_on = folded(true);
let gap_off = folded(false);
assert!(
gap_on > gap_off,
"自己接触を有効にしても層が引き離されていない (ON {} <= OFF {})",
gap_on.to_f32(),
gap_off.to_f32()
);
}
#[test]
fn closest_point_clamps_into_the_edge_and_vertex_regions() {
let a = Vec3Fix::from_int(0, 0, 0);
let b = Vec3Fix::from_int(1, 0, 0);
let c = Vec3Fix::from_int(0, 0, 1);
let h = Fix128::from_ratio(1, 2);
let q = Fix128::from_ratio(1, 4);
let two = Fix128::from_int(2);
let neg_h = Fix128::ZERO - h;
let neg_one = Fix128::from_int(-1);
let o = Fix128::ZERO;
let i = Fix128::ONE;
let cases: [(Vec3Fix, Vec3Fix, [Fix128; 3], &str); 7] = [
(
Vec3Fix::new(q, h, q),
Vec3Fix::new(q, o, q),
[h, q, q],
"face",
),
(
Vec3Fix::new(h, h, neg_one),
Vec3Fix::new(h, o, o),
[h, h, o],
"edge ab",
),
(
Vec3Fix::new(i, h, i),
Vec3Fix::new(h, o, h),
[o, h, h],
"edge bc",
),
(
Vec3Fix::new(neg_one, h, h),
Vec3Fix::new(o, o, h),
[h, o, h],
"edge ca",
),
(Vec3Fix::new(neg_h, h, neg_h), a, [i, o, o], "vertex a"),
(Vec3Fix::new(two, h, neg_h), b, [o, i, o], "vertex b"),
(Vec3Fix::new(neg_h, h, two), c, [o, o, i], "vertex c"),
];
for (p, expect_q, expect_w, name) in cases {
let Some((got_q, got_w)) = closest_point_on_triangle(p, a, b, c) else {
panic!("{name}: 退化していない三角形で None が返った");
};
assert_eq!(got_q, expect_q, "{name}: 最近接点");
assert_eq!(got_w, expect_w, "{name}: 重心座標");
assert_eq!(
a * got_w[0] + b * got_w[1] + c * got_w[2],
expect_q,
"{name}: w が最近接点を再構成しない"
);
}
for (x, y, z, name) in [
(a, b, b, "b と c が同一"),
(a, a, a, "3 頂点が同一"),
(a, b, Vec3Fix::from_int(2, 0, 0), "3 頂点が同一直線上"),
] {
assert!(
closest_point_on_triangle(Vec3Fix::new(q, h, q), x, y, z).is_none(),
"{name}: 面積 0 の三角形で Some が返った"
);
}
}
#[test]
fn point_triangle_subsumes_point_point() {
let cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(2),
Fix128::from_int(2),
3,
3,
Fix128::from_ratio(1, 100),
);
let mut edges: Vec<(usize, usize)> = cloth
.edge_constraints
.iter()
.map(|c| (c.i0.min(c.i1), c.i0.max(c.i1)))
.collect();
edges.sort_unstable();
for tri in &cloth.triangles {
for (x, y) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
assert!(
edges.binary_search(&(x.min(y), x.max(y))).is_ok(),
"三角形 {tri:?} の頂点対 ({x}, {y}) が辺拘束に無い"
);
}
}
let a = Vec3Fix::from_int(0, 0, 0);
let b = Vec3Fix::from_int(1, 0, 0);
let c = Vec3Fix::from_int(0, 0, 1);
let probes = [
Vec3Fix::new(
Fix128::from_ratio(1, 4),
Fix128::from_ratio(1, 2),
Fix128::from_ratio(1, 4),
),
Vec3Fix::new(
Fix128::from_int(3),
Fix128::from_int(1),
Fix128::from_int(-2),
),
Vec3Fix::new(
Fix128::from_ratio(-3, 4),
Fix128::ZERO,
Fix128::from_ratio(7, 8),
),
Vec3Fix::new(
Fix128::from_int(2),
Fix128::from_int(2),
Fix128::from_int(2),
),
];
for p in probes {
let (q, _) = closest_point_on_triangle(p, a, b, c).expect("退化していない");
let face_d2 = (p - q).length_squared();
for v in [a, b, c] {
assert!(
face_d2 <= (p - v).length_squared(),
"点 {p:?}: 三角形までの距離² {face_d2:?} が頂点までの距離²より大きい"
);
}
}
}
#[test]
fn self_contact_result_is_independent_of_the_pair_order() {
fn folded_cloth() -> Cloth {
const R: usize = 5;
let mut cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(4),
Fix128::from_int(4),
R,
R,
Fix128::from_ratio(1, 100),
);
cloth.config.self_collision = true;
cloth.config.self_collision_distance = Fix128::from_ratio(3, 2);
for x in 0..R {
cloth.positions[3 * R + x] =
Vec3Fix::new(cloth.positions[x].x, Fix128::ONE, Fix128::from_int(2));
cloth.positions[4 * R + x] =
Vec3Fix::new(cloth.positions[x].x, Fix128::ONE, Fix128::ONE);
}
cloth
}
let mut forward = folded_cloth();
let mut reverse = folded_cloth();
let margin = forward.config.self_collision_distance.double();
let mut candidates = forward.collect_vertex_face_candidates(margin);
assert!(
!candidates.is_empty(),
"折り返した布で接触候補が 0 件 この scene は順序非依存性を試せない"
);
forward.solve_vertex_face_self_collision(&candidates);
candidates.reverse();
reverse.solve_vertex_face_self_collision(&candidates);
assert_eq!(
forward.positions, reverse.positions,
"接触対を逆順に処理すると結果が変わる = 蓄積が順序依存"
);
}
#[test]
fn a_vertex_driven_through_the_sheet_is_pushed_back_to_the_entry_side() {
fn scene(self_collision: bool) -> (Cloth, Vec<Vec3Fix>) {
let mut cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(4),
Fix128::from_int(4),
5,
5,
Fix128::from_ratio(1, 100),
);
cloth.edge_constraints.clear();
cloth.bend_constraints.clear();
cloth.config.gravity = Vec3Fix::ZERO;
cloth.config.damping = Fix128::ONE;
cloth.config.self_collision = self_collision;
cloth.config.self_collision_distance = Fix128::from_ratio(1, 10);
for i in 1..cloth.particle_count() {
cloth.inv_masses[i] = Fix128::ZERO;
}
cloth.positions[0] = Vec3Fix::new(
Fix128::from_ratio(7, 2),
Fix128::from_int(2),
Fix128::from_ratio(13, 4),
);
cloth.prev_positions[0] = cloth.positions[0];
cloth.velocities[0] = Vec3Fix::new(Fix128::ZERO, Fix128::from_int(-240), Fix128::ZERO);
let start = cloth.positions.clone();
cloth.step(Fix128::from_ratio(1, 60));
(cloth, start)
}
let (off, start_off) = scene(false);
assert!(
off.positions[0].y < Fix128::ZERO,
"自己接触 OFF で粒子が布を素通りしなかった (y = {}) この scene は貫通を試せていない",
off.positions[0].y.to_f32()
);
assert!(
off.remaining_self_contact_crossings(&start_off) > 0,
"自己接触 OFF で貫通が検出されない"
);
let (on, start_on) = scene(true);
assert_eq!(
on.remaining_self_contact_crossings(&start_on),
0,
"自己接触 ON で貫通が残った (y = {})",
on.positions[0].y.to_f32()
);
assert!(
on.positions[0].y > Fix128::ZERO,
"自己接触 ON で粒子が布の裏側に居る (y = {}) 押し戻しの向きが入射側でない",
on.positions[0].y.to_f32()
);
}
#[test]
fn candidate_pairs_never_contain_a_vertex_of_their_own_triangle() {
let mut cloth = folded_cloth_with_contacts();
let margin = cloth.config.self_collision_distance.double();
let candidates = cloth.collect_vertex_face_candidates(margin);
assert!(
!candidates.is_empty(),
"折り返した布で接触候補が 0 件 この scene は契約を試せない"
);
for &(vi, ti) in &candidates {
let tri = cloth.triangles[ti as usize];
assert!(
vi as usize != tri[0] && vi as usize != tri[1] && vi as usize != tri[2],
"候補対 ({vi}, {ti}) が三角形 {tri:?} 自身の頂点を含む \
自分の面に対する斥力は delta では相殺するが hits を水増しして \
他の接触の補正を薄める"
);
}
let self_pairs = cloth
.triangles
.iter()
.enumerate()
.map(|(ti, tri)| {
candidates
.iter()
.filter(|&&(vi, cti)| {
cti as usize == ti
&& (vi as usize == tri[0]
|| vi as usize == tri[1]
|| vi as usize == tri[2])
})
.count()
})
.sum::<usize>();
assert_eq!(self_pairs, 0, "自己対が {self_pairs} 件混ざっている");
let before = cloth.positions.clone();
cloth.solve_vertex_face_self_collision(&candidates);
assert!(
before != cloth.positions,
"折り返した布で斥力が 1 bit も発火しない この scene は判別に向かない"
);
}
#[test]
fn the_proximity_projection_restores_exactly_the_contact_thickness() {
let eighth = Fix128::from_ratio(1, 8);
let quarter = Fix128::from_ratio(1, 4);
let sixteenth = Fix128::from_ratio(1, 16);
let mut cloth = Cloth::new_grid(Vec3Fix::ZERO, Fix128::ONE, Fix128::ONE, 2, 2, Fix128::ONE);
cloth.edge_constraints.clear();
cloth.bend_constraints.clear();
cloth.triangles = vec![[1, 2, 3]];
cloth.config.self_collision = true;
cloth.config.self_collision_distance = quarter;
cloth.positions[0] = Vec3Fix::new(quarter, eighth, quarter);
cloth.positions[1] = Vec3Fix::from_int(0, 0, 0);
cloth.positions[2] = Vec3Fix::from_int(1, 0, 0);
cloth.positions[3] = Vec3Fix::from_int(0, 0, 1);
cloth.inv_masses[0] = Fix128::from_ratio(1, 2);
cloth.inv_masses[1] = Fix128::ONE;
cloth.inv_masses[2] = Fix128::from_int(2);
cloth.inv_masses[3] = Fix128::from_int(2);
let candidates = cloth.collect_vertex_face_candidates(quarter.double());
assert_eq!(
candidates,
vec![(0u32, 0u32)],
"候補は (頂点 0, 三角形 0) の 1 件だけであるべき"
);
cloth.solve_vertex_face_self_collision(&candidates);
let neg_sixteenth = -sixteenth;
assert_eq!(
cloth.positions[0],
Vec3Fix::new(quarter, eighth + sixteenth, quarter),
"oracle: Δp = +1/16 (手計算、denom = 1 なので厳密)"
);
for (i, expected) in [
(
1usize,
Vec3Fix::new(Fix128::ZERO, neg_sixteenth, Fix128::ZERO),
),
(2, Vec3Fix::new(Fix128::ONE, neg_sixteenth, Fix128::ZERO)),
(3, Vec3Fix::new(Fix128::ZERO, neg_sixteenth, Fix128::ONE)),
] {
assert_eq!(
cloth.positions[i], expected,
"oracle: 三角形は −1/16 平行移動する (頂点 {i})"
);
}
assert_eq!(
cloth.positions[0].y - cloth.positions[1].y,
quarter,
"oracle: 1 回の投影で隙間が厳密に thickness = 1/4 になる"
);
}
#[test]
fn closest_points_on_segments_matches_the_closed_form_in_the_interior() {
let eighth = Fix128::from_ratio(1, 8);
let half = Fix128::from_ratio(1, 2);
let a0 = Vec3Fix::from_int(-1, 0, 0);
let a1 = Vec3Fix::from_int(1, 0, 0);
let b0 = Vec3Fix::new(Fix128::ZERO, eighth, Fix128::from_int(-1));
let b1 = Vec3Fix::new(Fix128::ZERO, eighth, Fix128::ONE);
let Some((s, t)) = closest_points_on_segments(a0, a1, b0, b1) else {
panic!("直交する 2 線分で最近接対が得られなかった");
};
assert_eq!(s, half, "oracle: s* = 1/2");
assert_eq!(t, half, "oracle: t* = 1/2");
let pa = a0 + (a1 - a0) * s;
let pb = b0 + (b1 - b0) * t;
assert_eq!(pa, Vec3Fix::ZERO, "oracle: 最近接点 A = (0,0,0)");
assert_eq!(
pb,
Vec3Fix::new(Fix128::ZERO, eighth, Fix128::ZERO),
"oracle: 最近接点 B = (0,1/8,0)"
);
assert_eq!(
(pa - pb).length_squared(),
Fix128::from_ratio(1, 64),
"oracle: 最近接距離² = 1/64"
);
}
#[test]
fn closest_points_on_segments_clamps_into_the_endpoint_regions() {
let half = Fix128::from_ratio(1, 2);
let a0 = Vec3Fix::from_int(0, 0, 0);
let a1 = Vec3Fix::from_int(1, 0, 0);
let Some((s, t)) = closest_points_on_segments(
a0,
a1,
Vec3Fix::from_int(2, 1, -1),
Vec3Fix::from_int(2, 1, 1),
) else {
panic!("端点領域の配置で最近接対が得られなかった");
};
assert_eq!(
s,
Fix128::ONE,
"oracle: 素の s = 2 が端点 1 へ clamp される"
);
assert_eq!(t, half, "oracle: t = 1/2 は区間内なので動かない");
let Some((s, t)) = closest_points_on_segments(
a0,
a1,
Vec3Fix::new(half, Fix128::ONE, Fix128::from_int(2)),
Vec3Fix::new(half, Fix128::ONE, Fix128::from_int(3)),
) else {
panic!("端点領域の配置で最近接対が得られなかった");
};
assert_eq!(
t,
Fix128::ZERO,
"oracle: 素の t = −2 が端点 0 へ clamp される"
);
assert_eq!(s, half, "oracle: t を固定して解き直した s = 1/2");
}
#[test]
fn closest_points_on_segments_rejects_degenerate_and_parallel_pairs() {
let o = Vec3Fix::ZERO;
let x1 = Vec3Fix::from_int(1, 0, 0);
let half = Fix128::from_ratio(1, 2);
assert!(
closest_points_on_segments(
o,
o,
Vec3Fix::from_int(0, 1, 0),
Vec3Fix::from_int(1, 1, 0)
)
.is_none(),
"長さ 0 の線分が受け付けられた"
);
assert!(
closest_points_on_segments(
o,
x1,
Vec3Fix::from_int(0, 1, 0),
Vec3Fix::from_int(1, 1, 0)
)
.is_none(),
"平行な 2 線分が受け付けられた 最小は区間なので (s,t) は幾何で決まらない"
);
assert!(
closest_points_on_segments(
o,
x1,
Vec3Fix::new(half, Fix128::ZERO, Fix128::ZERO),
Vec3Fix::new(Fix128::from_ratio(3, 2), Fix128::ZERO, Fix128::ZERO)
)
.is_none(),
"同一直線上の 2 線分が受け付けられた"
);
let tiny = Fix128::from_raw(0, 1 << 31);
assert!(
!tiny.is_zero() && (tiny * tiny).is_zero(),
"この scene の前提 (|d| ≠ 0 だが |d|² が underflow) が崩れた"
);
assert!(
closest_points_on_segments(
o,
Vec3Fix::new(tiny, Fix128::ZERO, Fix128::ZERO),
Vec3Fix::from_int(0, 1, 0),
Vec3Fix::from_int(0, 1, 1)
)
.is_none(),
"長さ² が underflow した線分が受け付けられた"
);
}
#[test]
fn closest_points_on_segments_rejects_a_degenerate_second_segment() {
let o = Vec3Fix::ZERO;
let x4 = Vec3Fix::from_int(4, 0, 0);
let up = Vec3Fix::from_int(0, 1, 0);
let tiny = Fix128::from_raw(0, 1 << 31); assert!(
!tiny.is_zero() && (tiny * tiny).is_zero(),
"この scene の前提 (|d| ≠ 0 だが |d|² が underflow) が崩れた"
);
let b = Fix128::from_int(4) * tiny;
assert!(
!(b * b).is_zero(),
"b² まで underflow した この配置では denom = 0 なので平行判定が先に拾ってしまう"
);
assert!(
closest_points_on_segments(o, x4, up, Vec3Fix::new(tiny, Fix128::ONE, Fix128::ZERO))
.is_none(),
"2 本目の長さ² が underflow した対が受け付けられた \
(e = 0 のまま t = (b·s + f)/e を踏むと 0 除算が silent に 0 を返す)"
);
assert!(
closest_points_on_segments(o, x4, up, up).is_none(),
"2 本目が長さ 0 の線分が受け付けられた"
);
assert!(
closest_points_on_segments(up, Vec3Fix::new(tiny, Fix128::ONE, Fix128::ZERO), o, x4)
.is_none(),
"同じ退化対を引数順を入れ替えると受け付けられた = 判定が非対称"
);
}
#[test]
fn closest_points_on_segments_resolves_s_again_after_pinning_t() {
let half = Fix128::from_ratio(1, 2);
let quarter = Fix128::from_ratio(1, 4);
let eighth = Fix128::from_ratio(1, 8);
let a0 = Vec3Fix::from_int(0, 0, 0);
let a1 = Vec3Fix::from_int(1, 0, 0);
let b0 = Vec3Fix::new(-Fix128::from_ratio(5, 2), eighth, -Fix128::from_ratio(5, 2));
let b1 = Vec3Fix::new(half, eighth, -Fix128::from_ratio(3, 2));
let Some((s, t)) = closest_points_on_segments(a0, a1, b0, b1) else {
panic!("t > 1 の枝で最近接対が得られなかった");
};
assert_eq!(
t,
Fix128::ONE,
"oracle: 素の t = 13/10 が端点 1 へ固定される"
);
assert_eq!(
s, half,
"oracle: t を 1 に固定した後 s は (b − c)/a = 1/2 に解き直される \
(clamp 後の 1 に居残ってはいけない)"
);
assert_eq!(
a0 + (a1 - a0) * s,
Vec3Fix::new(half, Fix128::ZERO, Fix128::ZERO)
);
assert_eq!(b0 + (b1 - b0) * t, b1);
let b0 = Vec3Fix::new(quarter, eighth, -Fix128::from_ratio(5, 2));
let b1 = Vec3Fix::new(
-Fix128::from_ratio(11, 4),
eighth,
-Fix128::from_ratio(11, 2),
);
let Some((s, t)) = closest_points_on_segments(a0, a1, b0, b1) else {
panic!("t < 0 の枝で最近接対が得られなかった");
};
assert_eq!(
t,
Fix128::ZERO,
"oracle: 素の t = −13/24 が端点 0 へ固定される"
);
assert_eq!(
s, quarter,
"oracle: t を 0 に固定した後 s は −c/a = 1/4 に解き直される"
);
assert_eq!(
a0 + (a1 - a0) * s,
Vec3Fix::new(quarter, Fix128::ZERO, Fix128::ZERO)
);
assert_eq!(b0 + (b1 - b0) * t, b0);
}
#[test]
fn edge_edge_candidates_never_pair_two_edges_that_share_a_vertex() {
let cloth = folded_cloth_with_contacts();
let margin = cloth.config.self_collision_distance.double();
let candidates = cloth.collect_edge_edge_candidates(margin);
assert!(
!candidates.is_empty(),
"折り返した布で辺-辺候補が 0 件 この scene は契約を試せない"
);
let shared = candidates
.iter()
.filter(|&&[i0, i1, j0, j1]| i0 == j0 || i0 == j1 || i1 == j0 || i1 == j1)
.count();
assert_eq!(shared, 0, "頂点を共有する辺対が {shared} 件混ざっている");
let edges = cloth.mesh_edges();
let mut sorted = edges.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(edges, sorted, "mesh_edges が昇順かつ重複なしでない");
for e in &edges {
assert!(e[0] < e[1], "辺 {e:?} が [lo, hi] に正規化されていない");
}
}
fn x_crossing_scene(h: Fix128, thickness: Fix128) -> Cloth {
let mut cloth = Cloth::new_grid(Vec3Fix::ZERO, Fix128::ONE, Fix128::ONE, 3, 2, Fix128::ONE);
assert_eq!(cloth.particle_count(), 6);
cloth.edge_constraints.clear();
cloth.bend_constraints.clear();
cloth.triangles = vec![[0, 1, 2], [3, 4, 5]];
cloth.config.gravity = Vec3Fix::ZERO;
cloth.config.damping = Fix128::ONE;
cloth.config.self_collision = true;
cloth.config.self_collision_distance = thickness;
let eight = Fix128::from_int(8);
cloth.positions[0] = Vec3Fix::from_int(-1, 0, 0);
cloth.positions[1] = Vec3Fix::from_int(1, 0, 0);
cloth.positions[2] = Vec3Fix::new(Fix128::ZERO, -eight, Fix128::ZERO);
cloth.positions[3] = Vec3Fix::new(Fix128::ZERO, h, Fix128::from_int(-1));
cloth.positions[4] = Vec3Fix::new(Fix128::ZERO, h, Fix128::ONE);
cloth.positions[5] = Vec3Fix::new(Fix128::ZERO, h + eight, Fix128::ZERO);
cloth.prev_positions = cloth.positions.clone();
cloth.velocities.fill(Vec3Fix::ZERO);
cloth
}
fn x_crossing_signed_gap(cloth: &Cloth) -> Fix128 {
assert_eq!(
cloth.positions[0].y, cloth.positions[1].y,
"辺 (0,1) が x 軸に平行でなくなった この計器は使えない"
);
assert_eq!(
cloth.positions[3].y, cloth.positions[4].y,
"辺 (3,4) が z 軸に平行でなくなった この計器は使えない"
);
cloth.positions[3].y - cloth.positions[0].y
}
#[test]
fn the_edge_edge_projection_restores_exactly_the_contact_thickness() {
let eighth = Fix128::from_ratio(1, 8);
let quarter = Fix128::from_ratio(1, 4);
let sixteenth = Fix128::from_ratio(1, 16);
let mut cloth = x_crossing_scene(eighth, quarter);
let before = cloth.positions.clone();
assert_eq!(
x_crossing_signed_gap(&cloth),
eighth,
"scene の初期隙間は h = 1/8"
);
let margin = quarter.double();
let vf = cloth.collect_vertex_face_candidates(margin);
cloth.solve_vertex_face_self_collision(&vf);
assert_eq!(
cloth.positions,
before,
"頂点-面の段がこの scene を動かした X 字交差の盲点を示す scene になっていない \
(候補 {} 件)",
vf.len()
);
let ee = cloth.collect_edge_edge_candidates(margin);
assert_eq!(
ee.iter()
.filter(|&&[i0, i1, j0, j1]| [i0, i1, j0, j1] == [0, 1, 3, 4])
.count(),
1,
"辺 (0,1) × (3,4) の対が候補に 1 件だけ入っていない (候補 {ee:?})"
);
cloth.solve_edge_edge_self_collision(&ee);
let neg_sixteenth = -sixteenth;
assert_eq!(
cloth.positions[0],
Vec3Fix::new(Fix128::from_int(-1), neg_sixteenth, Fix128::ZERO),
"oracle: Δ₀ = −1/16 (denom = 1 なので厳密)"
);
assert_eq!(
cloth.positions[1],
Vec3Fix::new(Fix128::ONE, neg_sixteenth, Fix128::ZERO),
"oracle: Δ₁ = −1/16"
);
assert_eq!(
cloth.positions[3],
Vec3Fix::new(Fix128::ZERO, eighth + sixteenth, Fix128::from_int(-1)),
"oracle: Δ₃ = +1/16"
);
assert_eq!(
cloth.positions[4],
Vec3Fix::new(Fix128::ZERO, eighth + sixteenth, Fix128::ONE),
"oracle: Δ₄ = +1/16"
);
assert_eq!(
cloth.positions[2], before[2],
"接触に参加しない頂点 2 が動いた (hits = 0 のはず)"
);
assert_eq!(
cloth.positions[5], before[5],
"接触に参加しない頂点 5 が動いた (hits = 0 のはず)"
);
assert_eq!(
x_crossing_signed_gap(&cloth),
quarter,
"oracle: 1 回の投影で辺-辺の隙間が厳密に thickness = 1/4 になる"
);
}
#[test]
fn the_edge_edge_projection_weights_s_t_and_the_inverse_masses_separately() {
let quarter = Fix128::from_ratio(1, 4);
let half = Fix128::from_ratio(1, 2);
let eighth = Fix128::from_ratio(1, 8);
let eight = Fix128::from_int(8);
let scene = || {
let mut c = Cloth::new_grid(Vec3Fix::ZERO, Fix128::ONE, Fix128::ONE, 3, 2, Fix128::ONE);
assert_eq!(c.particle_count(), 6);
c.edge_constraints.clear();
c.bend_constraints.clear();
c.triangles = vec![[0, 1, 2], [3, 4, 5]];
c.config.gravity = Vec3Fix::ZERO;
c.config.damping = Fix128::ONE;
c.config.self_collision = true;
c.config.self_collision_distance = quarter;
c.positions[0] = Vec3Fix::from_int(0, 0, 0);
c.positions[1] = Vec3Fix::from_int(1, 0, 0);
c.positions[2] = Vec3Fix::new(Fix128::ZERO, -eight, Fix128::ZERO);
c.positions[3] = Vec3Fix::new(quarter, eighth, -half);
c.positions[4] = Vec3Fix::new(quarter, eighth, half);
c.positions[5] = Vec3Fix::new(quarter, eighth + eight, Fix128::ZERO);
c.inv_masses[0] = Fix128::ZERO;
c.inv_masses[1] = eight;
c.inv_masses[3] = half;
c.inv_masses[4] = Fix128::from_ratio(3, 2);
c.prev_positions = c.positions.clone();
c.velocities.fill(Vec3Fix::ZERO);
c
};
let probe = scene();
let Some((s, t)) = closest_points_on_segments(
probe.positions[0],
probe.positions[1],
probe.positions[3],
probe.positions[4],
) else {
panic!("辺 (0,1) × (3,4) で最近接対が得られなかった");
};
assert_eq!(s, quarter, "oracle: s = 1/4");
assert_eq!(t, half, "oracle: t = 1/2");
assert!(
s != t,
"この scene は s = t なので s / t の取り違えを試せない"
);
let mut cloth = scene();
let before = cloth.positions.clone();
let margin = quarter.double();
let vf = cloth.collect_vertex_face_candidates(margin);
cloth.solve_vertex_face_self_collision(&vf);
assert_eq!(
cloth.positions,
before,
"頂点-面の段がこの scene を動かした (候補 {} 件) 辺-辺だけを測れていない",
vf.len()
);
let ee = cloth.collect_edge_edge_candidates(margin);
assert_eq!(
ee.iter()
.filter(|&&[i0, i1, j0, j1]| [i0, i1, j0, j1] == [0, 1, 3, 4])
.count(),
1,
"辺 (0,1) × (3,4) の対が候補に 1 件だけ入っていない (候補 {ee:?})"
);
cloth.solve_edge_edge_self_collision(&ee);
let thirty_second = Fix128::from_ratio(1, 32);
assert_eq!(
cloth.positions[0], before[0],
"oracle: 頂点 0 は inv_mass = 0 なので 1 bit も動かない"
);
assert_eq!(
cloth.positions[1],
Vec3Fix::new(Fix128::ONE, -quarter, Fix128::ZERO),
"oracle: Δ₁ = −1/4 (= λ·w₁·s、w₁ = 8 と s = 1/4 の積)"
);
assert_eq!(
cloth.positions[3],
Vec3Fix::new(quarter, eighth + thirty_second, -half),
"oracle: Δ₃ = +1/32 (= λ·w₂·v、w₂ = 1/2 と v = 1 − t = 1/2 の積)"
);
assert_eq!(
cloth.positions[4],
Vec3Fix::new(quarter, eighth + thirty_second * Fix128::from_int(3), half),
"oracle: Δ₄ = +3/32 (= λ·w₃·t、w₃ = 3/2 と t = 1/2 の積)"
);
assert_eq!(
cloth.positions[2], before[2],
"接触に参加しない頂点 2 が動いた (hits = 0 のはず)"
);
assert_eq!(
cloth.positions[5], before[5],
"接触に参加しない頂点 5 が動いた (hits = 0 のはず)"
);
assert!(
(cloth.positions[1].y - before[1].y) != (cloth.positions[3].y - before[3].y)
&& (cloth.positions[3].y - before[3].y) != (cloth.positions[4].y - before[4].y),
"Δ の大きさが揃っている = 逆質量 / s / t の重みが潰れている"
);
}
#[test]
fn edge_edge_result_is_independent_of_the_pair_order() {
let mut forward = folded_cloth_with_contacts();
let mut reverse = folded_cloth_with_contacts();
let margin = forward.config.self_collision_distance.double();
let mut candidates = forward.collect_edge_edge_candidates(margin);
assert!(
candidates.len() > 1,
"辺-辺候補が {} 件では順序非依存性を試せない",
candidates.len()
);
let before = forward.positions.clone();
forward.solve_edge_edge_self_collision(&candidates);
assert!(
forward.positions != before,
"辺-辺の斥力が 1 bit も発火しない この scene は判別に向かない"
);
candidates.reverse();
reverse.solve_edge_edge_self_collision(&candidates);
assert_eq!(
forward.positions, reverse.positions,
"辺-辺の対を逆順に処理すると結果が変わる = 蓄積が順序依存"
);
}
#[test]
fn self_collision_is_independent_of_the_pair_order_across_both_stages() {
let mut forward = folded_cloth_with_contacts();
let mut reverse = folded_cloth_with_contacts();
let margin = forward.config.self_collision_distance.double();
let mut vf = forward.collect_vertex_face_candidates(margin);
let mut ee = forward.collect_edge_edge_candidates(margin);
assert!(
vf.len() > 1 && ee.len() > 1,
"候補が 頂点-面 {} 件 / 辺-辺 {} 件 では順序非依存性を試せない",
vf.len(),
ee.len()
);
let before = forward.positions.clone();
forward.solve_self_collision(&vf, &ee);
assert!(
forward.positions != before,
"2 段を統合した投影が 1 bit も発火しない この scene は判別に向かない"
);
vf.reverse();
ee.reverse();
reverse.solve_self_collision(&vf, &ee);
assert_eq!(
forward.positions, reverse.positions,
"両段の対を逆順に処理すると結果が変わる = 統合 buffer の蓄積が順序依存"
);
}
#[test]
fn the_averaging_denominator_counts_contacts_from_both_stages() {
let eighth = Fix128::from_ratio(1, 8);
let quarter = Fix128::from_ratio(1, 4);
let ee_pair = [[0u32, 1, 3, 4]];
let vf_pair = [(0u32, 1u32)];
let scene = || {
let mut c = x_crossing_scene(eighth, quarter);
c.positions[0] = Vec3Fix::new(-eighth, Fix128::ZERO, Fix128::ZERO);
c.prev_positions = c.positions.clone();
c
};
let base = scene().positions[0];
let margin = quarter.double();
let probe = scene();
assert!(
probe
.collect_vertex_face_candidates(margin)
.contains(&(0, 1)),
"頂点 0 と三角形 1 の対が候補に入らない"
);
assert!(
probe
.collect_edge_edge_candidates(margin)
.contains(&ee_pair[0]),
"辺 (0,1) x (3,4) の対が候補に入らない"
);
let mut vf_only = scene();
vf_only.solve_self_collision(&vf_pair, &[]);
let d_vf = vf_only.positions[0] - base;
let mut ee_only = scene();
ee_only.solve_self_collision(&[], &ee_pair);
let d_ee = ee_only.positions[0] - base;
assert!(
d_vf != Vec3Fix::ZERO && d_ee != Vec3Fix::ZERO,
"片方の段が発火していない (頂点-面 {d_vf:?} / 辺-辺 {d_ee:?}) この scene は判別に向かない"
);
let mut both = scene();
both.solve_self_collision(&vf_pair, &ee_pair);
assert_eq!(
both.positions[0] - base,
(d_vf + d_ee) / Fix128::from_int(2),
"統合 buffer の補正が「段ごとの寄与の和の 1/2」になっていない \
= 平均の分母が両段の件数 2 になっていない \
(段ごとに割ると和そのものが残り、辺-辺が 2 倍過大評価される)"
);
}
#[test]
fn an_edge_pair_that_swaps_sides_in_one_frame_is_seen_by_the_crossings_invariant() {
let half = Fix128::from_ratio(1, 2);
let quarter = Fix128::from_ratio(1, 4);
let dt = Fix128::from_ratio(1, 64);
let run = |self_collision: bool| -> (Fix128, usize) {
let mut cloth = x_crossing_scene(half, quarter);
cloth.config.self_collision = self_collision;
for i in 3..6 {
cloth.velocities[i] =
Vec3Fix::new(Fix128::ZERO, Fix128::from_int(-48), Fix128::ZERO);
}
let start = cloth.positions.clone();
cloth.step(dt);
let crossings = cloth.remaining_self_contact_crossings(&start);
(x_crossing_signed_gap(&cloth), crossings)
};
let (gap_off, crossings_off) = run(false);
assert_eq!(
gap_off,
-quarter,
"自己接触 OFF で符号付き隙間が −1/4 (= 2 辺が入れ替わった) にならない \
この scene は貫通を試せていない (実測 {})",
gap_off.to_f32()
);
assert_eq!(
crossings_off, 1,
"自己接触 OFF で 2 辺が入れ替わった (符号付き隙間 +1/2 → −1/4) のに \
不変量が辺-辺の交差を数えていない 1.5.x 以前の盲点が再発している"
);
let (gap_on, crossings_on) = run(true);
assert!(
gap_on >= quarter,
"自己接触 ON で辺-辺の隙間が thickness = 1/4 を下回った (実測 {})",
gap_on.to_f32()
);
assert_eq!(
crossings_on, 0,
"自己接触 ON で貫通が残った (頂点-面 / 辺-辺 の合算)"
);
}
#[test]
fn the_crossings_invariant_does_not_panic_on_degenerate_input() {
let quarter = Fix128::from_ratio(1, 4);
let cloth = x_crossing_scene(Fix128::from_ratio(1, 2), quarter);
assert_eq!(
cloth.remaining_self_contact_crossings(&[]),
0,
"start が空の時は測れる対が 1 つも無いので 0"
);
for take in [1usize, cloth.particle_count() - 1] {
let short: Vec<Vec3Fix> = cloth.positions.iter().copied().take(take).collect();
let _ = cloth.remaining_self_contact_crossings(&short);
}
let mut long = cloth.positions.clone();
long.push(Vec3Fix::ZERO);
long.push(Vec3Fix::ZERO);
let _ = cloth.remaining_self_contact_crossings(&long);
let mut collapsed = x_crossing_scene(Fix128::from_ratio(1, 2), quarter);
let start = collapsed.positions.clone();
collapsed.positions.fill(Vec3Fix::ZERO);
assert_eq!(
collapsed.remaining_self_contact_crossings(&start),
0,
"長さ 0 の辺しか無い時に対を数えている"
);
let mut zero_thickness = x_crossing_scene(Fix128::from_ratio(1, 2), quarter);
zero_thickness.config.self_collision_distance = Fix128::ZERO;
let start = zero_thickness.positions.clone();
assert_eq!(
zero_thickness.remaining_self_contact_crossings(&start),
0,
"接触厚 0 で辺-辺の交差を数えている"
);
let still = x_crossing_scene(Fix128::from_ratio(1, 2), quarter);
let same = still.positions.clone();
assert_eq!(
still.remaining_self_contact_crossings(&same),
0,
"静止した frame で交差を数えている"
);
let mut no_tris = x_crossing_scene(Fix128::from_ratio(1, 2), quarter);
let start = no_tris.positions.clone();
no_tris.triangles.clear();
assert_eq!(
no_tris.remaining_self_contact_crossings(&start),
0,
"三角形が無いのに対を数えている"
);
}
#[test]
fn a_pair_that_crosses_and_returns_inside_one_interval_is_not_counted() {
let half = Fix128::from_ratio(1, 2);
let quarter = Fix128::from_ratio(1, 4);
let dt = Fix128::from_ratio(1, 64);
let speed = Fix128::from_int(48);
let mut cloth = x_crossing_scene(half, quarter);
cloth.config.self_collision = false; let origin = cloth.positions.clone();
assert_eq!(
x_crossing_signed_gap(&cloth),
half,
"開始時の符号付き隙間が +1/2 でない"
);
for i in 3..6 {
cloth.velocities[i] = Vec3Fix::new(Fix128::ZERO, -speed, Fix128::ZERO);
}
let start1 = cloth.positions.clone();
cloth.step(dt);
assert_eq!(
x_crossing_signed_gap(&cloth),
-quarter,
"frame 1 で 2 辺が入れ替わっていない この scene は往復を試せていない"
);
assert_eq!(
cloth.remaining_self_contact_crossings(&start1),
1,
"frame 1 を単独で測った時に貫通が数えられていない"
);
for i in 3..6 {
cloth.velocities[i] = Vec3Fix::new(Fix128::ZERO, speed, Fix128::ZERO);
}
let start2 = cloth.positions.clone();
cloth.step(dt);
assert_eq!(
x_crossing_signed_gap(&cloth),
half,
"frame 2 で元の隙間 +1/2 に戻っていない 往復が閉じていない"
);
assert_eq!(
cloth.remaining_self_contact_crossings(&start2),
1,
"frame 2 を単独で測った時に戻りの交差が数えられていない"
);
assert_eq!(
cloth.remaining_self_contact_crossings(&origin),
0,
"往復を 1 区間として測った時に 0 以外が返った \
終端の符号で数える規約が変わっている (doc § The invariant is stated at the end)"
);
}
fn folded_cloth_with_contacts() -> Cloth {
const R: usize = 5;
let mut cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(4),
Fix128::from_int(4),
R,
R,
Fix128::from_ratio(1, 100),
);
cloth.config.self_collision = true;
cloth.config.self_collision_distance = Fix128::from_ratio(3, 2);
for x in 0..R {
cloth.positions[3 * R + x] =
Vec3Fix::new(cloth.positions[x].x, Fix128::ONE, Fix128::from_int(2));
cloth.positions[4 * R + x] =
Vec3Fix::new(cloth.positions[x].x, Fix128::ONE, Fix128::ONE);
}
cloth
}
#[test]
fn test_cloth_normals() {
let cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(2),
Fix128::from_int(2),
3,
3,
Fix128::from_ratio(1, 100),
);
let normals = cloth.compute_normals();
assert_eq!(normals.len(), 9);
for n in &normals {
let (_, ny, _) = n.to_f32();
assert!(
ny.abs() > 0.5,
"Normals should point mostly in Y for flat grid"
);
}
}
fn two_triangle_cloth() -> Cloth {
let mut cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::ONE,
Fix128::ONE,
2,
2,
Fix128::from_ratio(1, 100),
);
assert_eq!(cloth.particle_count(), 4);
assert_eq!(
cloth.bend_constraints.len(),
1,
"2x2 grid = 2 triangles = 1 shared edge"
);
cloth.config.bend_compliance = Fix128::ZERO;
cloth
}
fn dihedral(cloth: &Cloth) -> Fix128 {
let c = cloth.bend_constraints[0];
let p0 = cloth.positions[c.i0];
let e = cloth.positions[c.i1] - p0;
let n1 = e.cross(cloth.positions[c.i2] - p0).normalize();
let n2 = e.cross(cloth.positions[c.i3] - p0).normalize();
Fix128::atan2(n1.cross(n2).length(), n1.dot(n2))
}
#[test]
fn bend_flat_rest_is_pi_and_flat_cloth_is_a_fixed_point() {
let mut cloth = two_triangle_cloth();
let rest = cloth.bend_constraints[0].rest_angle;
assert!(
(rest - Fix128::PI).abs() < Fix128::from_ratio(1, 1_000_000),
"rest {rest:?}"
);
let before = cloth.positions.clone();
for _ in 0..50 {
cloth.solve_bend_constraints(Fix128::from_ratio(1, 60));
}
assert_eq!(cloth.positions, before);
}
#[test]
fn bend_folded_cloth_relaxes_monotonically_toward_flat() {
let mut cloth = two_triangle_cloth();
let c = cloth.bend_constraints[0];
cloth.positions[c.i3].y = Fix128::from_ratio(1, 2);
let rest = cloth.bend_constraints[0].rest_angle;
let mut prev_err = (dihedral(&cloth) - rest).abs();
assert!(
prev_err > Fix128::from_ratio(1, 10),
"初期折れ角 {prev_err:?}"
);
for iter in 0..40 {
cloth.solve_bend_constraints(Fix128::from_ratio(1, 60));
let err = (dihedral(&cloth) - rest).abs();
assert!(
err <= prev_err,
"iter {iter}: error grew {prev_err:?} -> {err:?}"
);
prev_err = err;
}
assert!(
prev_err < Fix128::from_ratio(1, 100),
"40 iter 後も {prev_err:?}"
);
for p in &cloth.positions {
assert!(p.length() < Fix128::from_int(4), "{p:?}");
}
}
#[test]
fn bend_pinned_vertices_do_not_move() {
let mut cloth = two_triangle_cloth();
let c = cloth.bend_constraints[0];
cloth.inv_masses[c.i0] = Fix128::ZERO;
cloth.inv_masses[c.i1] = Fix128::ZERO;
cloth.inv_masses[c.i2] = Fix128::ZERO;
cloth.positions[c.i3].y = Fix128::from_ratio(1, 2);
let fixed = [
cloth.positions[c.i0],
cloth.positions[c.i1],
cloth.positions[c.i2],
];
let before_i3 = cloth.positions[c.i3];
for _ in 0..10 {
cloth.solve_bend_constraints(Fix128::from_ratio(1, 60));
}
assert_eq!(cloth.positions[c.i0], fixed[0]);
assert_eq!(cloth.positions[c.i1], fixed[1]);
assert_eq!(cloth.positions[c.i2], fixed[2]);
assert!(cloth.positions[c.i3] != before_i3, "自由頂点だけが動く");
}
#[test]
fn drape_bottom_row_ends_below_pinned_top_and_above_free_fall() {
let mut cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(2),
Fix128::from_int(2),
5,
5,
Fix128::from_ratio(1, 100),
);
cloth.pin_top_row(5);
let start_bottom = cloth.positions[20].y;
let dt = Fix128::from_ratio(1, 60);
for _ in 0..120 {
cloth.step(dt);
}
let bottom = cloth.positions[20].y;
assert!(
bottom < start_bottom,
"下がる: {start_bottom:?} -> {bottom:?}"
);
assert!(bottom < cloth.positions[0].y, "top より下");
assert!(bottom > Fix128::from_int(-20), "発散していない {bottom:?}");
for p in &cloth.positions {
assert!(p.length() < Fix128::from_int(30), "{p:?}");
}
}
#[cfg(feature = "std")]
fn unit_sphere_collider() -> crate::sdf_collider::SdfCollider {
use crate::sdf_collider::{ClosureSdf, SdfCollider};
let field = ClosureSdf::new(
|x, y, z| (x * x + y * y + z * z).sqrt() - 1.0,
|x, y, z| {
let len = (x * x + y * y + z * z).sqrt();
if len > 1e-6 {
(x / len, y / len, z / len)
} else {
(0.0, 1.0, 0.0)
}
},
);
SdfCollider::new_static(
Box::new(field),
Vec3Fix::ZERO,
crate::math::QuatFix::IDENTITY,
)
}
#[cfg(feature = "std")]
fn sphere_dist(p: Vec3Fix) -> f32 {
let (x, y, z) = p.to_f32();
(x * x + y * y + z * z).sqrt() - 1.0
}
#[cfg(feature = "std")]
#[test]
fn step_with_sdf_drapes_cloth_over_unit_sphere_without_penetration() {
let make = || {
Cloth::new_grid(
Vec3Fix::from_int(-1, 2, -1),
Fix128::from_int(2),
Fix128::from_int(2),
5,
5,
Fix128::from_ratio(1, 100),
)
};
let sphere = [unit_sphere_collider()];
let mut cloth = make();
let thickness = cloth.config.thickness.to_f32();
let dt = Fix128::from_ratio(1, 60);
let mut min_dist = f32::MAX;
for frame in 0..60 {
cloth.step_with_sdf(dt, &sphere);
for (i, p) in cloth.positions.iter().enumerate() {
let d = sphere_dist(*p);
min_dist = min_dist.min(d);
assert!(
d >= thickness - 1e-3,
"frame {frame} particle {i} dist {d} < thickness {thickness}"
);
}
}
assert!(
min_dist < thickness + 0.05,
"never touched: min dist {min_dist}"
);
let (cx, cy, cz) = cloth.positions[12].to_f32();
assert!(
(0.99..=1.1).contains(&cy),
"center particle y {cy} (x {cx}, z {cz})"
);
assert!(
cx.abs() < 0.1 && cz.abs() < 0.1,
"center drifted: ({cx}, {cz})"
);
let mut free = make();
for _ in 0..60 {
free.step(dt);
}
let (_, fy, _) = free.positions[12].to_f32();
assert!(
fy < 0.0,
"without SDF the cloth should fall through: y {fy}"
);
let mut a = make();
let mut b = make();
for _ in 0..10 {
a.step_with_sdf(dt, &[]);
b.step(dt);
}
assert_eq!(a.positions, b.positions);
assert_eq!(a.velocities, b.velocities);
}
fn lone_particle() -> Cloth {
let mut c = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::ONE,
Fix128::ONE,
1,
1,
Fix128::from_int(2),
);
c.config.gravity = Vec3Fix::ZERO;
c.config.damping = Fix128::ONE;
c.config.substeps = 1;
c
}
fn sin_cos_ref(x: f64) -> (f64, f64) {
let (mut s, mut c, mut term) = (0.0_f64, 0.0_f64, 1.0_f64);
for n in 0..40_u32 {
match n % 4 {
0 => c += term,
1 => s += term,
2 => c -= term,
_ => s -= term,
}
term *= x / f64::from(n + 1);
}
(s, c)
}
fn oscillator_ref(w2: f64, g: f64, h: f64) -> (f64, f64, f64, f64) {
use crate::det_math::exp64;
let al = g / 2.0;
let disc = w2 - al * al;
let (b, bd) = if disc.abs() < 1e-300 {
(h * exp64(-al * h), exp64(-al * h) * (1.0 - al * h))
} else if disc > 0.0 {
let wd = disc.sqrt();
let (s, c) = sin_cos_ref(wd * h);
let e = exp64(-al * h);
(e * s / wd, e * (c - al * s / wd))
} else {
let r = (-disc).sqrt();
let (l1, l2) = (al - r, al + r);
let (e1, e2) = (exp64(-l1 * h), exp64(-l2 * h));
((e1 - e2) / (l2 - l1), (l2 * e2 - l1 * e1) / (l2 - l1))
};
let a = bd + g * b;
(a, b, (1.0 - a) / w2, bd)
}
#[test]
fn oscillator_step_matches_the_closed_forms() {
let h = Fix128::from_ratio(1, 60);
let hf = 1.0 / 60.0;
let mut checked = 0;
for wh in [3_i64, 9, 12, 20, 60] {
for zeta_milli in [0_i64, 200, 420, 900, 960, 1000, 1040, 1200, 3000] {
let w = Fix128::from_ratio(wh * 60, 10);
let w2 = w * w;
let g = Fix128::from_ratio(2 * zeta_milli, 1000) * w;
let s = OscillatorStep::new(w2, g, h);
let (a, b, c, bd) = oscillator_ref(w2.to_f64(), g.to_f64(), hf);
let tol = 2e-15;
assert!(
(s.pos_e.to_f64() - a).abs() < tol,
"A wh {wh} ζ {zeta_milli}"
);
assert!(
(s.pos_v.to_f64() - b).abs() < tol * hf,
"B wh {wh} ζ {zeta_milli}"
);
assert!(
(s.vel_v.to_f64() - bd).abs() < tol,
"B' wh {wh} ζ {zeta_milli}"
);
assert!(
(s.pos_a.to_f64() - c).abs() < tol * hf * hf,
"C wh {wh} ζ {zeta_milli}: {} vs {c}",
s.pos_a.to_f64()
);
assert_eq!(s.vel_a, s.pos_v);
assert_eq!(s.vel_e, Fix128::ZERO - w2 * s.pos_v);
let det = s.pos_e * s.vel_v - s.pos_v * s.vel_e;
let want = crate::physics2d::exp_neg(g * h);
assert!(
(det - want).abs().to_f64() < 1e-17,
"det wh {wh} ζ {zeta_milli}: {}",
(det - want).to_f64()
);
checked += 1;
}
}
assert_eq!(checked, 45);
}
#[test]
fn oscillator_step_small_arguments() {
const ULP4: f64 = 2.2e-19;
let h = Fix128::from_ratio(1, 1000);
let s = OscillatorStep::new(Fix128::ONE, Fix128::from_ratio(1, 5), h);
let (a, b, c, bd) = oscillator_ref(1.0, 0.2, 1e-3);
assert!((s.pos_e.to_f64() - a).abs() < ULP4);
assert!((s.pos_v.to_f64() - b).abs() < ULP4);
assert!((s.vel_v.to_f64() - bd).abs() < ULP4);
let want_c = 1e-6 / 2.0 - 0.2 * 1e-9 / 6.0
+ (0.04 - 1.0) * 1e-12 / 24.0
+ (0.096 + 0.1) / 12.0 * 1e-15 / 5.0;
assert!(
(s.pos_a.to_f64() - want_c).abs() < ULP4,
"{} vs {want_c} ({c})",
s.pos_a.to_f64()
);
let g = Fix128::from_int(3000);
let s = OscillatorStep::new(Fix128::ZERO, g, h);
let e = crate::det_math::exp64(-3.0);
assert!((s.pos_e.to_f64() - 1.0).abs() < ULP4);
assert!((s.vel_v.to_f64() - e).abs() < ULP4);
assert!((s.pos_v.to_f64() - (1.0 - e) / 3000.0).abs() < ULP4);
let want_c = (1e-3 - (1.0 - e) / 3000.0) / 3000.0;
assert!(
(s.pos_a.to_f64() - want_c).abs() < ULP4,
"{} vs {want_c}",
s.pos_a.to_f64()
);
assert_eq!(phi1(Fix128::ZERO), Fix128::ONE);
let x = Fix128::from_ratio(1, 2);
assert!((phi1(x).to_f64() - (1.0 - crate::det_math::exp64(-0.5)) / 0.5).abs() < 1e-16);
let s = OscillatorStep::new(Fix128::ZERO, Fix128::ZERO, h);
assert_eq!(s.pos_e, Fix128::ONE);
assert_eq!(s.vel_v, Fix128::ONE);
assert!((s.pos_v - h).abs().to_f64() < ULP4);
assert!((s.pos_a.to_f64() - 0.5e-6).abs() < ULP4);
}
#[test]
fn sinc_cos_large_argument() {
for theta in [1.0_f64, 3.0, 40.0, 1000.0, 12_345.0] {
let tf = Fix128::from_f64(theta);
let (s, k) = sinc_cos(tf * tf);
let tq = tf.to_f64();
let r = tq.rem_euclid(2.0 * core::f64::consts::PI);
let r = if r > core::f64::consts::PI {
r - 2.0 * core::f64::consts::PI
} else {
r
};
let (sr, cr) = sin_cos_ref(r);
let tol = 1e-15 + theta * 4e-16;
assert!(
(k.to_f64() - cr).abs() < tol,
"θ {theta}: {} vs {cr}",
k.to_f64()
);
assert!((s.to_f64() * tq - sr).abs() < tol, "θ {theta}");
}
}
#[test]
fn rest_tether_one_substep_is_the_exact_oscillator() {
let mut c = lone_particle();
c.set_rest_tether(Fix128::from_int(72), Fix128::from_int(12));
assert_eq!(
c.rest_tether(),
Some((Fix128::from_int(72), Fix128::from_int(12)))
);
assert_eq!(c.rest_positions(), &[Vec3Fix::ZERO]);
c.positions[0] = Vec3Fix::from_int(1, 0, 0);
c.step(Fix128::from_ratio(1, 60));
let (zw, wd) = (0.05_f64, 0.05 * 3.0_f64.sqrt());
let (s, co) = sin_cos_ref(wd);
let d = crate::det_math::exp64(-zw);
let want = d * (co + zw / wd * s);
let want_v = -d * (36.0 / 60.0 / 60.0) / wd * s * 60.0;
assert!((c.positions[0].x.to_f64() - want).abs() < 1e-15);
assert!((c.velocities[0].x.to_f64() - want_v).abs() < 1e-13);
}
#[test]
fn rest_tether_explicit_rest_clear_and_rejects() {
let mut c = lone_particle();
let err = c.set_rest_positions(&[]).unwrap_err();
assert_eq!(
err,
RestPositionsMismatch {
expected: 1,
got: 0
}
);
c.set_rest_positions(&[Vec3Fix::from_int(0, 4, 0)]).unwrap();
c.set_rest_tether(Fix128::from_int(-1), Fix128::from_int(-1));
assert_eq!(c.rest_tether(), Some((Fix128::ZERO, Fix128::ZERO)));
assert_eq!(c.rest_positions(), &[Vec3Fix::from_int(0, 4, 0)]);
c.set_rest_tether(
Fix128::from_int(1_000_000_000_000),
Fix128::from_int(4_000_000),
);
c.step(Fix128::from_ratio(1, 60));
assert!((c.positions[0].y.to_f64() - 4.0).abs() < 1e-12);
c.clear_rest_tether();
assert_eq!(c.rest_tether(), None);
let mut p = lone_particle();
p.pin(0);
p.set_rest_positions(&[Vec3Fix::from_int(1, 1, 1)]).unwrap();
p.set_rest_tether(Fix128::from_int(50), Fix128::ONE);
p.step(Fix128::from_ratio(1, 60));
assert_eq!(p.positions[0], Vec3Fix::ZERO);
assert!(format!("{p:?}").contains("rest_tether"));
}
}