#![warn(missing_docs)]
#![allow(ambiguous_glob_reexports)]
#![allow(dead_code)]
mod error;
pub use error::*;
pub mod aerodynamics;
pub mod cloth;
pub mod constraint;
pub mod crack_propagation;
pub mod fem_soft;
pub mod fracture;
pub mod fracture_dynamics;
pub mod inflatable;
pub mod particle;
pub mod pbd_system;
pub mod rope;
pub mod shape_matching;
pub mod solver;
pub mod volume;
pub mod xpbd;
pub use aerodynamics::AerodynamicsModel;
pub use cloth::*;
pub use constraint::{
BendingConstraint, CollisionConstraint, DistanceConstraint, SoftConstraint, VolumeConstraint,
};
pub use fem_soft::{CorotationalElement, FemSoftBody};
pub use fracture_dynamics::*;
pub use inflatable::*;
pub use particle::{SoftBody, SoftParticle};
pub use rope::{HairStrand, HairSystem, Rope, RopeSolver, XpbdRope};
pub use shape_matching::ShapeMatching;
pub use solver::XpbdSolver;
pub trait SoftBodySolver {
fn init(&mut self);
}
#[cfg(test)]
mod tests {
use super::*;
use oxiphysics_core::math::Mat3;
use oxiphysics_core::math::{Real, Vec3};
const EPS: Real = 1e-6;
#[test]
fn test_distance_constraint_maintains_rest_length() {
let mut particles = vec![
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(2.0, 0.0, 0.0), 1.0),
];
let rest = 1.0;
let mut dc = DistanceConstraint::new(0, 1, rest, 0.0);
for _ in 0..100 {
dc.reset_lambda();
dc.project(&mut particles, 1.0 / 60.0);
}
let dist = (particles[0].position - particles[1].position).norm();
assert!(
(dist - rest).abs() < 0.01,
"Distance {dist} should be close to rest length {rest}"
);
}
#[test]
fn test_two_particles_converge() {
let mut particles = vec![
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(5.0, 0.0, 0.0), 1.0),
];
let rest = 1.0;
let mut dc = DistanceConstraint::new(0, 1, rest, 0.0);
for _ in 0..200 {
dc.reset_lambda();
dc.project(&mut particles, 1.0 / 60.0);
}
let dist = (particles[0].position - particles[1].position).norm();
assert!(
(dist - rest).abs() < 0.05,
"Particles should converge to rest length"
);
}
#[test]
fn test_volume_constraint_preserves_volume() {
let mut particles = vec![
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(1.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 1.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 0.0, 1.0), 1.0),
];
let rest_vol = VolumeConstraint::compute_tet_volume(
&particles[0].position,
&particles[1].position,
&particles[2].position,
&particles[3].position,
);
particles[3].position = Vec3::new(0.0, 0.0, 1.5);
let mut vc = VolumeConstraint::new([0, 1, 2, 3], rest_vol, 0.0);
for _ in 0..100 {
vc.reset_lambda();
vc.project(&mut particles, 1.0 / 60.0);
}
let new_vol = VolumeConstraint::compute_tet_volume(
&particles[0].position,
&particles[1].position,
&particles[2].position,
&particles[3].position,
);
assert!(
(new_vol - rest_vol).abs() < 0.01,
"Volume {new_vol} should be close to rest volume {rest_vol}"
);
}
#[test]
fn test_xpbd_solver_runs() {
let mut body = SoftBody::from_particles(vec![
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(1.0, 0.0, 0.0), 1.0),
]);
body.apply_force(&Vec3::new(0.0, -9.81, 0.0));
let mut constraints: Vec<Box<dyn SoftConstraint>> = vec![Box::new(
DistanceConstraint::from_particles(0, 1, &body.particles, 0.0),
)];
let mut solver = XpbdSolver::new(5);
solver.solve(&mut body, &mut constraints, 1.0 / 60.0);
let y0 = body.particles[0].position.y;
assert!(y0 < 0.0, "Particle should have fallen under gravity");
}
#[test]
fn test_cloth_mesh_topology() {
let cloth = XpbdClothMesh::new(4, 3, 3.0, 2.0, 1.0, 0.001);
assert_eq!(cloth.num_particles(), 12); assert_eq!(cloth.num_triangles(), 12);
assert!(!cloth.distance_constraints.is_empty());
assert!(!cloth.bending_constraints.is_empty());
}
#[test]
fn test_wind_force_direction() {
let mut particles = vec![
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(1.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 0.0, 1.0), 1.0),
];
let tris = vec![[0, 1, 2]];
let wind = Vec3::new(0.0, -1.0, 0.0); apply_wind(&mut particles, &wind, &tris);
let total_fy: Real = particles.iter().map(|p| p.external_force.y).sum();
assert!(
total_fy.abs() > EPS,
"Wind should produce a Y-direction force, got {total_fy}"
);
}
#[test]
fn test_rope_creation() {
let rope = XpbdRope::new(
Vec3::new(0.0, 10.0, 0.0),
Vec3::new(0.0, 0.0, 0.0),
10,
0.5,
0.0,
);
assert_eq!(rope.num_particles(), 11);
assert_eq!(rope.num_segments(), 10);
assert!(rope.body.particles[0].is_static());
}
#[test]
fn test_corotational_identity_zero_force() {
let particles = vec![
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(1.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 1.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 0.0, 1.0), 1.0),
];
let elem = CorotationalElement::new([0, 1, 2, 3], &particles, 1000.0, 0.3);
let forces = elem.compute_forces(&particles);
for (k, f) in forces.iter().enumerate() {
assert!(
f.norm() < 1e-8,
"Force on vertex {k} should be zero at rest, got {f:?}"
);
}
}
#[test]
fn test_bending_constraint_projection() {
let mut particles = vec![
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(1.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.5, 1.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.5, -1.0, 0.0), 1.0),
];
let mut bc = BendingConstraint::from_particles([0, 1, 2, 3], &particles, 0.0);
particles[2].position = Vec3::new(0.5, 1.0, 0.5);
let old_pos = particles[2].position;
for _ in 0..50 {
bc.reset_lambda();
bc.project(&mut particles, 1.0 / 60.0);
}
let moved = (particles[2].position - old_pos).norm();
assert!(
moved > 1e-8,
"Bending constraint should move perturbed wing vertex"
);
}
#[test]
fn test_particle_free_fall() {
let mut body =
SoftBody::from_particles(vec![SoftParticle::new(Vec3::new(0.0, 10.0, 0.0), 1.0)]);
body.apply_force(&Vec3::new(0.0, -9.81, 0.0));
let mut constraints: Vec<Box<dyn SoftConstraint>> = Vec::new();
let mut solver = XpbdSolver::new(1);
let dt = 1.0 / 60.0;
for _ in 0..60 {
solver.solve(&mut body, &mut constraints, dt);
}
let y = body.particles[0].position.y;
assert!(
y < 9.0,
"Particle should have fallen under gravity, y = {y}"
);
}
#[test]
fn test_collision_constraint_plane() {
let mut particles = vec![SoftParticle::new(Vec3::new(0.0, -1.0, 0.0), 1.0)];
let mut cc =
CollisionConstraint::new(0, Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 0.0, 0.0));
cc.project(&mut particles, 1.0 / 60.0);
assert!(
particles[0].position.y >= -EPS,
"Particle should be pushed above the plane"
);
}
#[test]
fn test_fem_soft_body_step() {
let particles = vec![
SoftParticle::new_static(Vec3::new(0.0, 0.0, 0.0)),
SoftParticle::new(Vec3::new(1.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 1.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 0.0, 1.0), 1.0),
];
let elem = CorotationalElement::new([0, 1, 2, 3], &particles, 1000.0, 0.3);
let mut fem = FemSoftBody::new(particles, vec![elem], 0.01);
let gravity = Vec3::new(0.0, -9.81, 0.0);
for _ in 0..10 {
fem.step(1.0 / 60.0, &gravity);
}
assert!(
fem.particles[1].position != Vec3::new(1.0, 0.0, 0.0),
"Dynamic particle should have moved"
);
}
#[test]
fn test_corotational_polar_decomp() {
use oxiphysics_core::math::Mat3;
let rest_particles = vec![
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(1.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 1.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 0.0, 1.0), 1.0),
];
let elem = CorotationalElement::new([0, 1, 2, 3], &rest_particles, 1000.0, 0.3);
let angle: Real = std::f64::consts::FRAC_PI_4; let cos_a = angle.cos();
let sin_a = angle.sin();
let rz = Mat3::new(cos_a, -sin_a, 0.0, sin_a, cos_a, 0.0, 0.0, 0.0, 1.0);
let rotated_particles: Vec<SoftParticle> = rest_particles
.iter()
.map(|p| SoftParticle::new(rz * p.position, 1.0))
.collect();
let forces = elem.compute_forces(&rotated_particles);
for (k, f) in forces.iter().enumerate() {
assert!(
f.norm() < 1e-8,
"Force on vertex {k} should be zero for pure rotation, got {f:?}"
);
}
}
#[test]
fn test_cloth_gravity_sag() {
let mut cloth = XpbdClothMesh::new(3, 3, 2.0, 2.0, 1.0, 1e-4);
cloth.pin(0, 0);
cloth.pin(2, 0);
cloth.pin(0, 2);
cloth.pin(2, 2);
let gravity = Vec3::new(0.0, -9.81, 0.0);
let dt = 1.0 / 60.0;
let mut solver = XpbdSolver::new(10);
let mut constraints: Vec<Box<dyn SoftConstraint>> = cloth
.distance_constraints
.iter()
.cloned()
.map(|c| Box::new(c) as Box<dyn SoftConstraint>)
.chain(
cloth
.bending_constraints
.iter()
.cloned()
.map(|c| Box::new(c) as Box<dyn SoftConstraint>),
)
.collect();
for _ in 0..100 {
cloth.body.apply_force(&gravity);
solver.solve(&mut cloth.body, &mut constraints, dt);
cloth.body.clear_forces();
}
let center_y = cloth.body.particles[4].position.y;
assert!(
center_y < -0.01,
"Center particle should sag below y=0 under gravity, got y={center_y}"
);
}
#[test]
fn test_aero_zero_wind() {
let aero = AerodynamicsModel::new(1.225, 1.0, 0.5);
let zero = Vec3::zeros();
let (f0, f1, f2) = aero.triangle_force(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
zero,
zero,
zero,
zero, );
assert!(
f0.norm() < 1e-10,
"f0 should be zero with zero wind, got {:?}",
f0
);
assert!(
f1.norm() < 1e-10,
"f1 should be zero with zero wind, got {:?}",
f1
);
assert!(
f2.norm() < 1e-10,
"f2 should be zero with zero wind, got {:?}",
f2
);
}
#[test]
fn test_aero_head_on_wind() {
let cd = 1.5;
let cl = 0.8;
let aero = AerodynamicsModel::new(1.225, cd, cl);
let zero = Vec3::zeros();
let wind = Vec3::new(0.0, 10.0, 0.0); let (f0, f1, f2) = aero.triangle_force(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
zero,
zero,
zero,
wind,
);
let f_total = f0 + f1 + f2;
assert!(
f_total.x.abs() < 1e-10 && f_total.z.abs() < 1e-10,
"Head-on wind should produce only Y force, got {:?}",
f_total
);
assert!(
f_total.y > 1e-10,
"Head-on wind should produce positive Y drag, got {:?}",
f_total
);
}
#[test]
fn test_aero_force_scales_with_rho() {
let aero1 = AerodynamicsModel::new(1.0, 1.0, 0.5);
let aero2 = AerodynamicsModel::new(2.0, 1.0, 0.5);
let zero = Vec3::zeros();
let wind = Vec3::new(5.0, 3.0, 1.0);
let v0 = Vec3::new(0.0, 0.0, 0.0);
let v1 = Vec3::new(1.0, 0.0, 0.0);
let v2 = Vec3::new(0.0, 0.0, 1.0);
let (a0, a1, a2) = aero1.triangle_force(v0, v1, v2, zero, zero, zero, wind);
let (b0, b1, b2) = aero2.triangle_force(v0, v1, v2, zero, zero, zero, wind);
let fa = (a0 + a1 + a2).norm();
let fb = (b0 + b1 + b2).norm();
assert!(
(fb - 2.0 * fa).abs() < 1e-10,
"Doubling rho should double force: fa={fa}, fb={fb}"
);
}
#[test]
fn test_shape_matching_rigid_body() {
let rest = vec![
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, -1.0, 0.0),
];
let masses = vec![1.0f64; 4];
let sm = ShapeMatching::new(&rest, &masses, 1.0);
let rz90 = Mat3::new(0.0, -1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);
let current: Vec<Vec3> = rest.iter().map(|p| rz90 * p).collect();
let goals = sm.goal_positions(¤t);
for (i, (goal, cur)) in goals.iter().zip(current.iter()).enumerate() {
assert!(
(goal - cur).norm() < 1e-6,
"Particle {i}: goal {:?} should equal current {:?}",
goal,
cur
);
}
}
#[test]
fn test_shape_matching_stiffness_zero() {
let rest = vec![
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(-1.0, 0.0, 0.0),
Vec3::new(0.0, 2.0, 0.0),
];
let masses = vec![1.0f64; 3];
let sm = ShapeMatching::new(&rest, &masses, 0.0);
let current = vec![
Vec3::new(3.0, 1.0, -2.0),
Vec3::new(-2.0, 0.5, 1.0),
Vec3::new(0.5, 4.0, 0.3),
];
let goals = sm.goal_positions(¤t);
for (i, (goal, cur)) in goals.iter().zip(current.iter()).enumerate() {
assert!(
(goal - cur).norm() < 1e-10,
"With stiffness=0 goal {i} {:?} should equal current {:?}",
goal,
cur
);
}
}
#[test]
fn test_shape_matching_identity() {
let rest = vec![
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
Vec3::new(0.0, 0.0, 1.0),
];
let masses = vec![1.0f64; 4];
let sm = ShapeMatching::new(&rest, &masses, 1.0);
let goals = sm.goal_positions(&rest);
for (i, (goal, cur)) in goals.iter().zip(rest.iter()).enumerate() {
assert!(
(goal - cur).norm() < 1e-6,
"Particle {i}: goal {:?} should equal rest {:?}",
goal,
cur
);
}
}
#[test]
fn test_distance_constraint_correction() {
let mut particles = vec![
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0), ];
let rest = 1.5;
particles[1].position = Vec3::new(1e-6, 0.0, 0.0);
let mut dc = DistanceConstraint::new(0, 1, rest, 0.0);
for _ in 0..200 {
dc.reset_lambda();
dc.project(&mut particles, 1.0 / 60.0);
}
let dist = (particles[0].position - particles[1].position).norm();
assert!(
(dist - rest).abs() < 0.1,
"Particles too close should be pushed to rest length {rest}, got dist={dist}"
);
}
#[test]
fn test_volume_constraint_preserve() {
let mut particles = vec![
SoftParticle::new(Vec3::new(0.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(2.0, 0.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 2.0, 0.0), 1.0),
SoftParticle::new(Vec3::new(0.0, 0.0, 2.0), 1.0),
];
let rest_vol = VolumeConstraint::compute_tet_volume(
&particles[0].position,
&particles[1].position,
&particles[2].position,
&particles[3].position,
);
particles[3].position = Vec3::new(0.0, 0.0, 0.5);
let mut vc = VolumeConstraint::new([0, 1, 2, 3], rest_vol, 0.0);
for _ in 0..200 {
vc.reset_lambda();
vc.project(&mut particles, 1.0 / 60.0);
}
let final_vol = VolumeConstraint::compute_tet_volume(
&particles[0].position,
&particles[1].position,
&particles[2].position,
&particles[3].position,
);
let relative_error = ((final_vol - rest_vol) / rest_vol).abs();
assert!(
relative_error < 0.05,
"Volume after constraint should be close to rest ({rest_vol}), got {final_vol} (error={relative_error:.3})"
);
}
}
pub mod active_matter;
pub mod active_origami;
pub mod bio_softbody;
pub mod bioinspired;
pub mod biomech_simulation;
pub mod cable_nets;
pub mod cloth_advanced;
pub mod cloth_simulation;
pub mod collision_response;
pub mod constraint_visualization;
pub mod cosserat_rods;
pub mod cosserat_softbody;
pub mod elastic_wave;
pub mod electroactive_softbody;
pub mod fluid_film_softbody;
pub mod food_physics;
pub mod granular_softbody;
pub mod growth_mechanics;
pub mod hair;
pub mod hair_fur;
pub mod hair_sim;
pub mod hair_softbody;
pub mod haptic_softbody;
pub mod liquid_sim;
pub mod material_point;
pub mod membrane_biophysics;
pub mod membrane_softbody;
pub mod metamaterial_softbody;
pub mod metamorphic_mesh;
pub mod morphogenesis_softbody;
pub mod mud_snow;
pub mod muscle_sim;
pub mod muscle_simulation;
pub mod muscle_softbody;
pub mod muscle_tendon;
pub mod neural_deform;
pub mod neural_soft;
pub mod neural_softbody;
pub mod numerical_softbody;
pub mod origami_folding;
pub mod origami_mech;
pub mod origami_mechanics;
pub mod origami_softbody;
pub mod pbd_constraints;
pub mod peridynamics;
pub mod position_based_fluids;
pub mod pressure_volume;
pub mod rigid_soft_coupling;
pub mod rod_mechanics;
pub mod rope_simulation;
pub mod sand_sim;
pub mod smart_materials;
pub mod soft_gripper;
pub mod surgery_simulation;
pub mod surgical_simulation;
pub mod tearing;
pub mod tendon_softbody;
pub mod textile_simulation;
pub mod tissue_sim;
pub mod topology_opt;
pub mod topology_optimization;
pub mod underwater_softbody;
pub mod wrinkling;