use crate::math::{Fix128, Vec3Fix};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ClothFluidCoupling {
pub drag_coefficient: Fix128,
pub buoyancy_factor: Fix128,
pub surface_tension: Fix128,
}
impl Default for ClothFluidCoupling {
fn default() -> Self {
Self {
drag_coefficient: Fix128::from_ratio(1, 2),
buoyancy_factor: Fix128::from_ratio(1, 10),
surface_tension: Fix128::from_ratio(1, 100),
}
}
}
pub fn apply_fluid_forces_to_cloth(
coupling: &ClothFluidCoupling,
cloth_positions: &[Vec3Fix],
cloth_velocities: &mut [Vec3Fix],
fluid_positions: &[Vec3Fix],
fluid_velocities: &[Vec3Fix],
fluid_density: Fix128,
dt: Fix128,
) {
let _ = apply_fluid_forces_to_cloth_with_residual(
coupling,
cloth_positions,
cloth_velocities,
fluid_positions,
fluid_velocities,
fluid_density,
dt,
);
}
pub fn apply_fluid_forces_to_cloth_with_residual(
coupling: &ClothFluidCoupling,
cloth_positions: &[Vec3Fix],
cloth_velocities: &mut [Vec3Fix],
fluid_positions: &[Vec3Fix],
fluid_velocities: &[Vec3Fix],
fluid_density: Fix128,
dt: Fix128,
) -> Fix128 {
let mut interface_force = Fix128::ZERO;
if fluid_positions.is_empty() || cloth_positions.is_empty() {
return interface_force;
}
let interaction_radius = Fix128::from_ratio(1, 2);
let radius_sq = interaction_radius * interaction_radius;
let buoyancy_dir = Vec3Fix::new(Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
for ci in 0..cloth_positions.len() {
let cp = cloth_positions[ci];
let cv = cloth_velocities[ci];
let mut avg_fluid_vel = Vec3Fix::ZERO;
let mut fluid_centroid = Vec3Fix::ZERO;
let mut neighbor_count = Fix128::ZERO;
for fi in 0..fluid_positions.len() {
let diff = fluid_positions[fi] - cp;
let dist_sq = diff.dot(diff);
if dist_sq < radius_sq {
avg_fluid_vel = avg_fluid_vel + fluid_velocities[fi];
fluid_centroid = fluid_centroid + fluid_positions[fi];
neighbor_count = neighbor_count + Fix128::ONE;
}
}
if neighbor_count.is_zero() {
continue;
}
avg_fluid_vel = avg_fluid_vel / neighbor_count;
fluid_centroid = fluid_centroid / neighbor_count;
let relative_vel = cv - avg_fluid_vel;
let density_factor = neighbor_count * fluid_density;
let drag_force = relative_vel * (coupling.drag_coefficient * density_factor);
let buoyancy_force = buoyancy_dir * (coupling.buoyancy_factor * neighbor_count);
let tension_force = (fluid_centroid - cp) * coupling.surface_tension;
let drag_rate_dt = coupling.drag_coefficient * density_factor * dt;
let drag_delta = if drag_rate_dt >= Fix128::ZERO {
relative_vel * (drag_rate_dt / (Fix128::ONE + drag_rate_dt))
} else {
drag_force * dt
};
cloth_velocities[ci] = cv - drag_delta + buoyancy_force * dt + tension_force * dt;
let net = buoyancy_force + tension_force - drag_force;
for component in [net.x, net.y, net.z] {
let magnitude = component.abs();
if magnitude > interface_force {
interface_force = magnitude;
}
}
}
interface_force
}
pub fn apply_cloth_boundary_to_fluid(
cloth_positions: &[Vec3Fix],
cloth_normals: &[Vec3Fix],
fluid_positions: &[Vec3Fix],
fluid_velocities: &mut [Vec3Fix],
repulsion_strength: Fix128,
) {
let _ = apply_cloth_boundary_to_fluid_with_residual(
cloth_positions,
cloth_normals,
fluid_positions,
fluid_velocities,
repulsion_strength,
);
}
pub fn apply_cloth_boundary_to_fluid_with_residual(
cloth_positions: &[Vec3Fix],
cloth_normals: &[Vec3Fix],
fluid_positions: &[Vec3Fix],
fluid_velocities: &mut [Vec3Fix],
repulsion_strength: Fix128,
) -> Fix128 {
let mut interface_correction = Fix128::ZERO;
if cloth_positions.is_empty() || fluid_positions.is_empty() {
return interface_correction;
}
let repulsion_radius = Fix128::from_ratio(1, 4);
let radius_sq = repulsion_radius * repulsion_radius;
for fi in 0..fluid_positions.len() {
let fp = fluid_positions[fi];
let before = fluid_velocities[fi];
for ci in 0..cloth_positions.len() {
let cp = cloth_positions[ci];
let normal = if ci < cloth_normals.len() {
cloth_normals[ci]
} else {
Vec3Fix::UNIT_Y
};
let diff = fp - cp;
let dist_sq = diff.dot(diff);
if dist_sq >= radius_sq || dist_sq.is_zero() {
continue;
}
let proj = diff.dot(normal);
let dist = dist_sq.sqrt();
let inv_dist = Fix128::ONE / dist;
let overlap = repulsion_radius - dist;
let force_mag = repulsion_strength * overlap * inv_dist;
let correction = normal * force_mag;
if proj.is_negative() {
fluid_velocities[fi] = fluid_velocities[fi] - correction;
} else {
fluid_velocities[fi] = fluid_velocities[fi] + correction;
}
}
let dv = fluid_velocities[fi] - before;
for component in [dv.x, dv.y, dv.z] {
let magnitude = component.abs();
if magnitude > interface_correction {
interface_correction = magnitude;
}
}
}
interface_correction
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn test_default_coupling() {
let coupling = ClothFluidCoupling::default();
assert!(!coupling.drag_coefficient.is_zero());
assert!(!coupling.buoyancy_factor.is_zero());
}
#[test]
fn test_no_fluid_no_effect() {
let coupling = ClothFluidCoupling::default();
let cloth_pos = vec![Vec3Fix::ZERO];
let mut cloth_vel = vec![Vec3Fix::from_int(1, 0, 0)];
let original_vel = cloth_vel[0];
apply_fluid_forces_to_cloth(
&coupling,
&cloth_pos,
&mut cloth_vel,
&[],
&[],
Fix128::ONE,
Fix128::from_ratio(1, 60),
);
assert_eq!(cloth_vel[0].x.hi, original_vel.x.hi);
}
#[test]
fn test_no_cloth_no_effect() {
let coupling = ClothFluidCoupling::default();
apply_fluid_forces_to_cloth(
&coupling,
&[],
&mut [],
&[Vec3Fix::ZERO],
&[Vec3Fix::ZERO],
Fix128::ONE,
Fix128::from_ratio(1, 60),
);
}
#[test]
fn test_drag_slows_cloth() {
let coupling = ClothFluidCoupling {
drag_coefficient: Fix128::ONE,
buoyancy_factor: Fix128::ZERO,
surface_tension: Fix128::ZERO,
};
let cloth_pos = vec![Vec3Fix::ZERO];
let mut cloth_vel = vec![Vec3Fix::from_int(10, 0, 0)];
let fluid_pos = vec![Vec3Fix::ZERO]; let fluid_vel = vec![Vec3Fix::ZERO];
apply_fluid_forces_to_cloth(
&coupling,
&cloth_pos,
&mut cloth_vel,
&fluid_pos,
&fluid_vel,
Fix128::ONE,
Fix128::from_ratio(1, 60),
);
assert!(cloth_vel[0].x < Fix128::from_int(10));
}
#[test]
fn test_buoyancy_adds_upward_force() {
let coupling = ClothFluidCoupling {
drag_coefficient: Fix128::ZERO,
buoyancy_factor: Fix128::ONE,
surface_tension: Fix128::ZERO,
};
let cloth_pos = vec![Vec3Fix::ZERO];
let mut cloth_vel = vec![Vec3Fix::ZERO];
let fluid_pos = vec![Vec3Fix::ZERO];
let fluid_vel = vec![Vec3Fix::ZERO];
apply_fluid_forces_to_cloth(
&coupling,
&cloth_pos,
&mut cloth_vel,
&fluid_pos,
&fluid_vel,
Fix128::ONE,
Fix128::from_ratio(1, 10),
);
assert!(cloth_vel[0].y > Fix128::ZERO);
}
#[test]
fn test_boundary_repulsion() {
let cloth_pos = vec![Vec3Fix::ZERO];
let cloth_normals = vec![Vec3Fix::UNIT_Y];
let fluid_pos = vec![Vec3Fix::new(
Fix128::ZERO,
Fix128::from_ratio(1, 10),
Fix128::ZERO,
)];
let mut fluid_vel = vec![Vec3Fix::new(
Fix128::ZERO,
Fix128::from_int(-5),
Fix128::ZERO,
)];
apply_cloth_boundary_to_fluid(
&cloth_pos,
&cloth_normals,
&fluid_pos,
&mut fluid_vel,
Fix128::from_int(10),
);
assert!(fluid_vel[0].y > Fix128::from_int(-5));
}
#[test]
fn test_boundary_no_effect_far_away() {
let cloth_pos = vec![Vec3Fix::ZERO];
let cloth_normals = vec![Vec3Fix::UNIT_Y];
let fluid_pos = vec![Vec3Fix::from_int(100, 100, 100)];
let mut fluid_vel = vec![Vec3Fix::from_int(1, -1, 0)];
let original_vel = fluid_vel[0];
apply_cloth_boundary_to_fluid(
&cloth_pos,
&cloth_normals,
&fluid_pos,
&mut fluid_vel,
Fix128::ONE,
);
assert_eq!(fluid_vel[0].x.hi, original_vel.x.hi);
assert_eq!(fluid_vel[0].y.hi, original_vel.y.hi);
}
#[test]
fn test_zero_dt_no_effect() {
let coupling = ClothFluidCoupling::default();
let cloth_pos = vec![Vec3Fix::ZERO];
let mut cloth_vel = vec![Vec3Fix::from_int(5, 0, 0)];
let original = cloth_vel[0];
apply_fluid_forces_to_cloth(
&coupling,
&cloth_pos,
&mut cloth_vel,
&[Vec3Fix::ZERO],
&[Vec3Fix::ZERO],
Fix128::ONE,
Fix128::ZERO,
);
assert_eq!(cloth_vel[0].x.hi, original.x.hi);
}
fn coupled_scene() -> (
ClothFluidCoupling,
Vec<Vec3Fix>,
Vec<Vec3Fix>,
Vec<Vec3Fix>,
Vec<Vec3Fix>,
) {
let coupling = ClothFluidCoupling::default();
let quarter = Fix128::from_ratio(1, 4);
let eighth = Fix128::from_ratio(1, 8);
let cloth_pos = vec![
Vec3Fix::ZERO,
Vec3Fix::new(quarter, Fix128::ZERO, Fix128::ZERO),
];
let cloth_vel = vec![Vec3Fix::from_int(3, 0, 0), Vec3Fix::from_int(0, -2, 0)];
let fluid_pos = vec![
Vec3Fix::ZERO,
Vec3Fix::new(eighth, Fix128::ZERO, Fix128::ZERO),
];
let fluid_vel = vec![Vec3Fix::from_int(0, 1, 0), Vec3Fix::from_int(1, 0, 0)];
(coupling, cloth_pos, cloth_vel, fluid_pos, fluid_vel)
}
#[test]
fn the_wrapper_and_the_reporting_variant_leave_identical_state() {
let (coupling, cloth_pos, cloth_vel, fluid_pos, fluid_vel) = coupled_scene();
let dt = Fix128::from_ratio(1, 60);
let mut through_wrapper = cloth_vel.clone();
apply_fluid_forces_to_cloth(
&coupling,
&cloth_pos,
&mut through_wrapper,
&fluid_pos,
&fluid_vel,
Fix128::ONE,
dt,
);
let mut through_reporting = cloth_vel.clone();
let force = apply_fluid_forces_to_cloth_with_residual(
&coupling,
&cloth_pos,
&mut through_reporting,
&fluid_pos,
&fluid_vel,
Fix128::ONE,
dt,
);
assert_eq!(through_wrapper, through_reporting);
assert!(
force > Fix128::ZERO,
"this scene must exert a force, or the comparison above is vacuous"
);
assert_ne!(through_wrapper, cloth_vel);
}
#[test]
fn the_boundary_wrapper_and_its_reporting_variant_leave_identical_state() {
let (_, cloth_pos, _, fluid_pos, fluid_vel) = coupled_scene();
let normals = vec![Vec3Fix::UNIT_Y, Vec3Fix::UNIT_Y];
let strength = Fix128::from_ratio(1, 4);
let mut through_wrapper = fluid_vel.clone();
apply_cloth_boundary_to_fluid(
&cloth_pos,
&normals,
&fluid_pos,
&mut through_wrapper,
strength,
);
let mut through_reporting = fluid_vel.clone();
let correction = apply_cloth_boundary_to_fluid_with_residual(
&cloth_pos,
&normals,
&fluid_pos,
&mut through_reporting,
strength,
);
assert_eq!(through_wrapper, through_reporting);
assert!(
correction > Fix128::ZERO,
"this scene must repel, or the comparison above is vacuous"
);
assert_ne!(through_wrapper, fluid_vel);
}
#[test]
fn the_reported_force_does_not_move_when_the_step_does() {
let (coupling, cloth_pos, cloth_vel, fluid_pos, fluid_vel) = coupled_scene();
let mut coarse_state = cloth_vel.clone();
let coarse_force = apply_fluid_forces_to_cloth_with_residual(
&coupling,
&cloth_pos,
&mut coarse_state,
&fluid_pos,
&fluid_vel,
Fix128::ONE,
Fix128::from_ratio(1, 60),
);
let mut fine_state = cloth_vel.clone();
let fine_force = apply_fluid_forces_to_cloth_with_residual(
&coupling,
&cloth_pos,
&mut fine_state,
&fluid_pos,
&fluid_vel,
Fix128::ONE,
Fix128::from_ratio(1, 120),
);
assert_eq!(
coarse_force, fine_force,
"the reported force must not depend on the step"
);
let coarse_change = (coarse_state[0] - cloth_vel[0]).x.abs();
let fine_change = (fine_state[0] - cloth_vel[0]).x.abs();
assert!(
fine_change < coarse_change,
"halving the step must shrink what is applied: {coarse_change:?} -> {fine_change:?}"
);
}
#[test]
fn an_empty_scene_reports_no_interface_force() {
let (coupling, cloth_pos, cloth_vel, _, _) = coupled_scene();
let mut state = cloth_vel.clone();
let force = apply_fluid_forces_to_cloth_with_residual(
&coupling,
&cloth_pos,
&mut state,
&[],
&[],
Fix128::ONE,
Fix128::from_ratio(1, 60),
);
assert_eq!(force, Fix128::ZERO);
assert_eq!(state, cloth_vel);
}
}