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,
}
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(),
};
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);
}
}
#[inline(always)]
pub fn step(&mut self, dt: Fix128) {
let substep_dt = dt / Fix128::from_int(self.config.substeps as i64);
for _ in 0..self.config.substeps {
self.substep(substep_dt);
}
self.apply_frame_damping();
}
#[cfg(feature = "std")]
#[inline(always)]
pub fn step_with_sdf(&mut self, dt: Fix128, sdf_colliders: &[SdfCollider]) {
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);
}
self.apply_frame_damping();
}
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();
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;
}
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();
}
}
let inv_dt = Fix128::ONE / dt;
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;
}
}
#[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);
}
}
}
#[inline(always)]
fn solve_self_collision(&mut self) {
let n = self.particle_count();
if n < 2 {
return;
}
let min_dist = self.config.self_collision_distance;
let min_dist_sq = min_dist * min_dist;
let cell_size = min_dist * Fix128::from_int(2);
let inv_cell = if cell_size.is_zero() {
Fix128::ONE
} else {
Fix128::ONE / cell_size
};
let grid_dim: usize = 64;
let grid_dim_i64 = grid_dim as i64;
let half = grid_dim_i64 / 2;
let total_cells = grid_dim * grid_dim * grid_dim;
let mut cell_counts = vec![0usize; total_cells];
let mut cell_particle: Vec<(usize, usize)> = Vec::with_capacity(n);
for i in 0..n {
if self.inv_masses[i].is_zero() {
continue;
}
let p = self.positions[i];
let ix = ((p.x * inv_cell).hi + half).clamp(0, grid_dim_i64 - 1) as usize;
let iy = ((p.y * inv_cell).hi + half).clamp(0, grid_dim_i64 - 1) as usize;
let iz = ((p.z * inv_cell).hi + half).clamp(0, grid_dim_i64 - 1) as usize;
let h = ix + iy * grid_dim + iz * grid_dim * grid_dim;
if h < total_cells {
cell_counts[h] += 1;
cell_particle.push((h, i));
}
}
let mut cell_offsets = vec![0usize; total_cells + 1];
for h in 0..total_cells {
cell_offsets[h + 1] = cell_offsets[h] + cell_counts[h];
cell_counts[h] = 0; }
let total_particles = cell_offsets[total_cells];
let mut indices = vec![0usize; total_particles];
for (h, i) in &cell_particle {
let slot = cell_offsets[*h] + cell_counts[*h];
indices[slot] = *i;
cell_counts[*h] += 1;
}
let mut edge_pairs: Vec<(usize, usize)> = Vec::with_capacity(self.edge_constraints.len());
for c in &self.edge_constraints {
let (lo, hi) = if c.i0 < c.i1 {
(c.i0, c.i1)
} else {
(c.i1, c.i0)
};
edge_pairs.push((lo, hi));
}
edge_pairs.sort_unstable();
for i in 0..n {
if self.inv_masses[i].is_zero() {
continue;
}
let p = self.positions[i];
let cx = ((p.x * inv_cell).hi + half).clamp(0, grid_dim_i64 - 1) as i32;
let cy = ((p.y * inv_cell).hi + half).clamp(0, grid_dim_i64 - 1) as i32;
let cz = ((p.z * inv_cell).hi + half).clamp(0, grid_dim_i64 - 1) as i32;
for dz in -1i32..=1 {
for dy in -1i32..=1 {
for dx in -1i32..=1 {
let nx = cx + dx;
let ny = cy + dy;
let nz = cz + dz;
if nx < 0 || ny < 0 || nz < 0 {
continue;
}
let nx = nx as usize;
let ny = ny as usize;
let nz = nz as usize;
if nx >= grid_dim || ny >= grid_dim || nz >= grid_dim {
continue;
}
let h = nx + ny * grid_dim + nz * grid_dim * grid_dim;
let start = cell_offsets[h];
let end = cell_offsets[h + 1];
for &j in &indices[start..end] {
if j <= i {
continue; }
if self.inv_masses[j].is_zero() {
continue;
}
let (lo, hi) = if i < j { (i, j) } else { (j, i) };
if edge_pairs.binary_search(&(lo, hi)).is_ok() {
continue;
}
let delta = self.positions[j] - self.positions[i];
let dist_sq = delta.length_squared();
if dist_sq < min_dist_sq && !dist_sq.is_zero() {
let dist = dist_sq.sqrt();
let error = min_dist - dist;
let inv_dist = Fix128::ONE / dist;
let normal = delta * inv_dist;
let w_sum = self.inv_masses[i] + self.inv_masses[j];
if w_sum.is_zero() {
continue;
}
let inv_w_sum = Fix128::ONE / w_sum;
let correction = normal * (error * inv_w_sum);
self.positions[i] =
self.positions[i] - correction * self.inv_masses[i];
self.positions[j] =
self.positions[j] + correction * self.inv_masses[j];
}
}
}
}
}
}
}
#[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)
.finish()
}
}
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::*;
#[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() {
let mut cloth = Cloth::new_grid(
Vec3Fix::ZERO,
Fix128::from_int(2),
Fix128::from_int(2),
5,
5,
Fix128::from_ratio(1, 100),
);
cloth.config.self_collision = true;
cloth.config.self_collision_distance = Fix128::from_ratio(5, 100); cloth.pin_top_row(5);
let dt = Fix128::from_ratio(1, 60);
for _ in 0..60 {
cloth.step(dt);
}
let min_d = cloth.config.self_collision_distance.to_f32();
let n = cloth.particle_count();
let mut too_close = 0;
for i in 0..n {
for j in (i + 1)..n {
let d = (cloth.positions[i] - cloth.positions[j]).length().to_f32();
if d < min_d * 0.5 {
too_close += 1;
}
}
}
assert!(
too_close < n,
"Self-collision should prevent extreme particle overlap"
);
}
#[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);
}
}