#![allow(missing_docs)]
mod constraints;
mod extensions;
mod fem;
mod geometry;
mod lbm;
mod materials;
mod math_helpers;
mod py_bindings;
mod sph;
mod stats;
#[cfg(test)]
mod tests;
pub use constraints::{ConstraintType, PyConstraint};
pub use extensions::{ContactPair, InertiaTensor, PyRigidBody};
pub use fem::{FemBarElement, PyFemAssembly};
pub use geometry::{PyAabb, PyConvexHull, PySphere};
pub use lbm::{PyLbmConfig, PyLbmGrid};
pub use materials::{MaterialClass, PyMaterial};
pub use math_helpers::{
array_to_vec3, quat_conjugate, quat_from_axis_angle, quat_mul, quat_normalize, quat_rotate_vec,
vec3_to_array,
};
pub use py_bindings::{
MaterialProperties, PyFemBinding, PyLbmBinding, PyMdBinding, PySphBinding, py_p_wave_speed,
py_query_material, py_s_wave_speed,
};
pub use sph::{PySphConfig, PySphSim};
pub use stats::SimStats;
use crate::types::{
PyColliderDesc, PyColliderShape, PyContactResult, PyRigidBodyConfig, PyRigidBodyDesc,
PySimConfig, PyVec3,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct InternalBody {
position: [f64; 3],
velocity: [f64; 3],
orientation: [f64; 4],
angular_velocity: [f64; 3],
mass: f64,
is_static: bool,
is_kinematic: bool,
is_sleeping: bool,
can_sleep: bool,
friction: f64,
restitution: f64,
linear_damping: f64,
angular_damping: f64,
accumulated_force: [f64; 3],
accumulated_torque: [f64; 3],
sleep_timer: f64,
shapes: Vec<PyColliderShape>,
tag: Option<String>,
}
impl InternalBody {
fn from_config(config: &PyRigidBodyConfig) -> Self {
Self {
position: config.position,
velocity: config.velocity,
orientation: config.orientation,
angular_velocity: config.angular_velocity,
mass: config.mass,
is_static: config.is_static,
is_kinematic: config.is_kinematic,
is_sleeping: false,
can_sleep: config.can_sleep,
friction: config.friction,
restitution: config.restitution,
linear_damping: config.linear_damping,
angular_damping: config.angular_damping,
accumulated_force: [0.0; 3],
accumulated_torque: [0.0; 3],
sleep_timer: 0.0,
shapes: config.shapes.clone(),
tag: config.tag.clone(),
}
}
fn inv_mass(&self) -> f64 {
if self.is_static || self.is_kinematic || self.mass <= 0.0 {
0.0
} else {
1.0 / self.mass
}
}
fn is_dynamic(&self) -> bool {
!self.is_static && !self.is_kinematic
}
}
#[derive(Debug, Clone)]
struct Slot {
body: Option<InternalBody>,
generation: u32,
}
impl Slot {
#[allow(dead_code)]
fn empty() -> Self {
Self {
body: None,
generation: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct PyPhysicsWorld {
slots: Vec<Slot>,
free_list: Vec<u32>,
config: PySimConfig,
time: f64,
contacts: Vec<PyContactResult>,
constraints: Vec<PyConstraint>,
}
impl PyPhysicsWorld {
pub fn new(config: PySimConfig) -> Self {
Self {
slots: Vec::new(),
free_list: Vec::new(),
config,
time: 0.0,
contacts: Vec::new(),
constraints: Vec::new(),
}
}
pub fn add_rigid_body(&mut self, config: PyRigidBodyConfig) -> u32 {
let body = InternalBody::from_config(&config);
if let Some(idx) = self.free_list.pop() {
let slot = &mut self.slots[idx as usize];
slot.body = Some(body);
idx
} else {
let idx = self.slots.len() as u32;
self.slots.push(Slot {
body: Some(body),
generation: 0,
});
idx
}
}
pub fn add_body_legacy(&mut self, desc: &PyRigidBodyDesc) -> u32 {
let config = PyRigidBodyConfig {
mass: desc.mass,
position: [desc.position.x, desc.position.y, desc.position.z],
velocity: [0.0; 3],
orientation: [0.0, 0.0, 0.0, 1.0],
angular_velocity: [0.0; 3],
shapes: vec![],
friction: 0.5,
restitution: 0.3,
is_static: desc.is_static,
is_kinematic: false,
can_sleep: true,
linear_damping: 0.0,
angular_damping: 0.0,
tag: None,
};
self.add_rigid_body(config)
}
pub fn add_collider(&mut self, handle: u32, desc: &PyColliderDesc) {
if let Some(body) = self.get_body_mut(handle) {
let shape = match desc.shape_type.as_str() {
"sphere" => {
let r = desc.radius.unwrap_or(0.5);
PyColliderShape::Sphere { radius: r }
}
"box" => {
let he = desc.half_extents.unwrap_or(PyVec3::new(0.5, 0.5, 0.5));
PyColliderShape::Box {
half_extents: [he.x, he.y, he.z],
}
}
_ => PyColliderShape::Sphere { radius: 0.5 },
};
body.shapes.push(shape);
}
}
pub fn remove_body(&mut self, handle: u32) -> bool {
if let Some(slot) = self.slots.get_mut(handle as usize)
&& slot.body.is_some()
{
slot.body = None;
slot.generation = slot.generation.wrapping_add(1);
self.free_list.push(handle);
return true;
}
false
}
pub fn get_position(&self, handle: u32) -> Option<[f64; 3]> {
self.get_body(handle).map(|b| b.position)
}
pub fn get_velocity(&self, handle: u32) -> Option<[f64; 3]> {
self.get_body(handle).map(|b| b.velocity)
}
pub fn get_orientation(&self, handle: u32) -> Option<[f64; 4]> {
self.get_body(handle).map(|b| b.orientation)
}
pub fn get_angular_velocity(&self, handle: u32) -> Option<[f64; 3]> {
self.get_body(handle).map(|b| b.angular_velocity)
}
pub fn is_sleeping(&self, handle: u32) -> bool {
self.get_body(handle)
.map(|b| b.is_sleeping)
.unwrap_or(false)
}
pub fn get_tag(&self, handle: u32) -> Option<String> {
self.get_body(handle).and_then(|b| b.tag.clone())
}
pub fn set_position(&mut self, handle: u32, pos: [f64; 3]) {
if let Some(body) = self.get_body_mut(handle) {
body.position = pos;
body.is_sleeping = false;
}
}
pub fn set_velocity(&mut self, handle: u32, vel: [f64; 3]) {
if let Some(body) = self.get_body_mut(handle) {
body.velocity = vel;
body.is_sleeping = false;
}
}
pub fn set_orientation(&mut self, handle: u32, orientation: [f64; 4]) {
if let Some(body) = self.get_body_mut(handle) {
body.orientation = normalize_quat(orientation);
body.is_sleeping = false;
}
}
pub fn set_angular_velocity(&mut self, handle: u32, omega: [f64; 3]) {
if let Some(body) = self.get_body_mut(handle) {
body.angular_velocity = omega;
body.is_sleeping = false;
}
}
pub fn apply_force(&mut self, handle: u32, force: [f64; 3], point: Option<[f64; 3]>) {
if let Some(body) = self.get_body_mut(handle) {
if !body.is_dynamic() {
return;
}
body.accumulated_force[0] += force[0];
body.accumulated_force[1] += force[1];
body.accumulated_force[2] += force[2];
if let Some(pt) = point {
let r = [
pt[0] - body.position[0],
pt[1] - body.position[1],
pt[2] - body.position[2],
];
let torque = cross3(r, force);
body.accumulated_torque[0] += torque[0];
body.accumulated_torque[1] += torque[1];
body.accumulated_torque[2] += torque[2];
}
body.is_sleeping = false;
}
}
pub fn apply_impulse(&mut self, handle: u32, impulse: [f64; 3], point: Option<[f64; 3]>) {
if let Some(body) = self.get_body_mut(handle) {
if !body.is_dynamic() {
return;
}
let inv_m = body.inv_mass();
body.velocity[0] += impulse[0] * inv_m;
body.velocity[1] += impulse[1] * inv_m;
body.velocity[2] += impulse[2] * inv_m;
if let Some(pt) = point {
let r = [
pt[0] - body.position[0],
pt[1] - body.position[1],
pt[2] - body.position[2],
];
let ang_impulse = cross3(r, impulse);
let approx_inertia = approximate_inertia(body);
let inv_i = if approx_inertia > 1e-12 {
1.0 / approx_inertia
} else {
0.0
};
body.angular_velocity[0] += ang_impulse[0] * inv_i;
body.angular_velocity[1] += ang_impulse[1] * inv_i;
body.angular_velocity[2] += ang_impulse[2] * inv_i;
}
body.is_sleeping = false;
}
}
pub fn apply_torque(&mut self, handle: u32, torque: [f64; 3]) {
if let Some(body) = self.get_body_mut(handle) {
if !body.is_dynamic() {
return;
}
body.accumulated_torque[0] += torque[0];
body.accumulated_torque[1] += torque[1];
body.accumulated_torque[2] += torque[2];
body.is_sleeping = false;
}
}
pub fn wake_body(&mut self, handle: u32) {
if let Some(body) = self.get_body_mut(handle) {
body.is_sleeping = false;
body.sleep_timer = 0.0;
}
}
pub fn sleep_body(&mut self, handle: u32) {
if let Some(body) = self.get_body_mut(handle) {
body.is_sleeping = true;
body.velocity = [0.0; 3];
body.angular_velocity = [0.0; 3];
}
}
pub fn set_gravity(&mut self, g: [f64; 3]) {
self.config.gravity = g;
}
pub fn gravity(&self) -> [f64; 3] {
self.config.gravity
}
pub fn config(&self) -> &PySimConfig {
&self.config
}
pub fn body_count(&self) -> usize {
self.slots.iter().filter(|s| s.body.is_some()).count()
}
pub fn sleeping_count(&self) -> usize {
self.slots
.iter()
.filter_map(|s| s.body.as_ref())
.filter(|b| b.is_sleeping)
.count()
}
pub fn time(&self) -> f64 {
self.time
}
pub fn get_contacts(&self) -> Vec<PyContactResult> {
self.contacts.clone()
}
pub fn all_positions(&self) -> Vec<[f64; 3]> {
self.slots
.iter()
.filter_map(|s| s.body.as_ref().map(|b| b.position))
.collect()
}
pub fn all_velocities(&self) -> Vec<[f64; 3]> {
self.slots
.iter()
.filter_map(|s| s.body.as_ref().map(|b| b.velocity))
.collect()
}
pub fn active_handles(&self) -> Vec<u32> {
self.slots
.iter()
.enumerate()
.filter(|(_, s)| s.body.is_some())
.map(|(i, _)| i as u32)
.collect()
}
pub fn step(&mut self, dt: f64) {
let g = self.config.gravity;
for slot in &mut self.slots {
let body = match slot.body.as_mut() {
Some(b) => b,
None => continue,
};
if body.is_static || body.is_kinematic || body.is_sleeping {
body.accumulated_force = [0.0; 3];
body.accumulated_torque = [0.0; 3];
continue;
}
let inv_m = body.inv_mass();
let total_force = [
body.accumulated_force[0] + g[0] * body.mass,
body.accumulated_force[1] + g[1] * body.mass,
body.accumulated_force[2] + g[2] * body.mass,
];
body.velocity[0] += total_force[0] * inv_m * dt;
body.velocity[1] += total_force[1] * inv_m * dt;
body.velocity[2] += total_force[2] * inv_m * dt;
let lin_damp = (1.0 - body.linear_damping * dt).max(0.0);
body.velocity[0] *= lin_damp;
body.velocity[1] *= lin_damp;
body.velocity[2] *= lin_damp;
body.position[0] += body.velocity[0] * dt;
body.position[1] += body.velocity[1] * dt;
body.position[2] += body.velocity[2] * dt;
let approx_i = approximate_inertia(body);
let inv_i = if approx_i > 1e-12 {
1.0 / approx_i
} else {
0.0
};
body.angular_velocity[0] += body.accumulated_torque[0] * inv_i * dt;
body.angular_velocity[1] += body.accumulated_torque[1] * inv_i * dt;
body.angular_velocity[2] += body.accumulated_torque[2] * inv_i * dt;
let ang_damp = (1.0 - body.angular_damping * dt).max(0.0);
body.angular_velocity[0] *= ang_damp;
body.angular_velocity[1] *= ang_damp;
body.angular_velocity[2] *= ang_damp;
body.orientation = integrate_orientation(body.orientation, body.angular_velocity, dt);
body.accumulated_force = [0.0; 3];
body.accumulated_torque = [0.0; 3];
}
self.contacts.clear();
self.detect_and_resolve_contacts(dt);
self.resolve_constraints();
if self.config.sleep_enabled {
self.update_sleep(dt);
}
self.time += dt;
}
pub fn reset(&mut self) {
self.slots.clear();
self.free_list.clear();
self.contacts.clear();
self.constraints.clear();
self.time = 0.0;
}
#[allow(clippy::too_many_arguments)]
pub fn step_substeps(&mut self, dt: f64, substeps: u32) {
let substeps = substeps.max(1);
let sub_dt = dt / substeps as f64;
for _ in 0..substeps {
self.step(sub_dt);
}
}
pub fn bodies_in_aabb(&self, aabb: [f64; 6]) -> Vec<u32> {
let [xmin, ymin, zmin, xmax, ymax, zmax] = aabb;
self.slots
.iter()
.enumerate()
.filter_map(|(i, slot)| {
let body = slot.body.as_ref()?;
let p = body.position;
if p[0] >= xmin
&& p[0] <= xmax
&& p[1] >= ymin
&& p[1] <= ymax
&& p[2] >= zmin
&& p[2] <= zmax
{
Some(i as u32)
} else {
None
}
})
.collect()
}
pub fn raycast(&self, origin: [f64; 3], dir: [f64; 3], max_dist: f64) -> Option<(u32, f64)> {
let len = (dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]).sqrt();
if len < 1e-12 {
return None;
}
let d = [dir[0] / len, dir[1] / len, dir[2] / len];
let mut best_handle: Option<u32> = None;
let mut best_t = max_dist;
for (i, slot) in self.slots.iter().enumerate() {
let body = match slot.body.as_ref() {
Some(b) => b,
None => continue,
};
let radius = first_sphere_radius(&body.shapes);
if radius <= 0.0 {
continue;
}
let oc = [
origin[0] - body.position[0],
origin[1] - body.position[1],
origin[2] - body.position[2],
];
let b_coeff = oc[0] * d[0] + oc[1] * d[1] + oc[2] * d[2];
let c_coeff = oc[0] * oc[0] + oc[1] * oc[1] + oc[2] * oc[2] - radius * radius;
let discriminant = b_coeff * b_coeff - c_coeff;
if discriminant < 0.0 {
continue;
}
let sqrt_disc = discriminant.sqrt();
let t = -b_coeff - sqrt_disc;
let t = if t >= 0.0 { t } else { -b_coeff + sqrt_disc };
if t >= 0.0 && t < best_t {
best_t = t;
best_handle = Some(i as u32);
}
}
best_handle.map(|h| (h, best_t))
}
pub fn contact_count(&self) -> usize {
self.contacts.len()
}
pub fn get_body_position(&self, handle: usize) -> Option<PyVec3> {
self.get_position(handle as u32).map(PyVec3::from_array)
}
pub fn get_body_velocity(&self, handle: usize) -> Option<PyVec3> {
self.get_velocity(handle as u32).map(PyVec3::from_array)
}
pub fn set_body_velocity(&mut self, handle: usize, vel: PyVec3) {
self.set_velocity(handle as u32, vel.to_array());
}
pub fn num_bodies(&self) -> usize {
self.body_count()
}
pub fn gravity_vec3(&self) -> PyVec3 {
PyVec3::from_array(self.config.gravity)
}
pub fn all_positions_vec3(&self) -> Vec<PyVec3> {
self.all_positions()
.into_iter()
.map(PyVec3::from_array)
.collect()
}
fn get_body(&self, handle: u32) -> Option<&InternalBody> {
self.slots.get(handle as usize)?.body.as_ref()
}
fn get_body_mut(&mut self, handle: u32) -> Option<&mut InternalBody> {
self.slots.get_mut(handle as usize)?.body.as_mut()
}
fn detect_and_resolve_contacts(&mut self, dt: f64) {
let n = self.slots.len();
for i in 0..n {
for j in (i + 1)..n {
let (pos_a, vel_a, mass_a, static_a, sleeping_a, friction_a, rest_a, radius_a) = {
let body = match self.slots[i].body.as_ref() {
Some(b) => b,
None => continue,
};
let radius = first_sphere_radius(&body.shapes);
if radius <= 0.0 {
continue;
}
(
body.position,
body.velocity,
body.mass,
body.is_static,
body.is_sleeping,
body.friction,
body.restitution,
radius,
)
};
let (pos_b, vel_b, mass_b, static_b, sleeping_b, friction_b, rest_b, radius_b) = {
let body = match self.slots[j].body.as_ref() {
Some(b) => b,
None => continue,
};
let radius = first_sphere_radius(&body.shapes);
if radius <= 0.0 {
continue;
}
(
body.position,
body.velocity,
body.mass,
body.is_static,
body.is_sleeping,
body.friction,
body.restitution,
radius,
)
};
let diff = [
pos_b[0] - pos_a[0],
pos_b[1] - pos_a[1],
pos_b[2] - pos_a[2],
];
let dist_sq = diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2];
let min_dist = radius_a + radius_b;
if dist_sq >= min_dist * min_dist {
continue;
}
let dist = dist_sq.sqrt();
let depth = min_dist - dist;
let normal = if dist > 1e-12 {
[diff[0] / dist, diff[1] / dist, diff[2] / dist]
} else {
[0.0, 1.0, 0.0]
};
let cp = [
pos_a[0] + normal[0] * radius_a,
pos_a[1] + normal[1] * radius_a,
pos_a[2] + normal[2] * radius_a,
];
let combined_friction = (friction_a + friction_b) * 0.5;
let combined_rest = (rest_a + rest_b) * 0.5;
let rel_vel = [
vel_a[0] - vel_b[0],
vel_a[1] - vel_b[1],
vel_a[2] - vel_b[2],
];
let rel_vel_normal =
rel_vel[0] * normal[0] + rel_vel[1] * normal[1] + rel_vel[2] * normal[2];
let impulse_mag = if rel_vel_normal < 0.0 {
let inv_m_a = if static_a || mass_a <= 0.0 {
0.0
} else {
1.0 / mass_a
};
let inv_m_b = if static_b || mass_b <= 0.0 {
0.0
} else {
1.0 / mass_b
};
let denom = inv_m_a + inv_m_b;
if denom > 1e-12 {
-(1.0 + combined_rest) * rel_vel_normal / denom
} else {
0.0
}
} else {
0.0
};
let correction_mag = (depth * self.config.baumgarte_factor) / dt.max(1e-6);
let contact = PyContactResult {
body_a: i as u32,
body_b: j as u32,
contact_point: cp,
normal,
depth,
friction: combined_friction,
restitution: combined_rest,
impulse: impulse_mag,
};
self.contacts.push(contact);
if !static_a && !sleeping_a && mass_a > 0.0 {
let inv_m_a = 1.0 / mass_a;
if let Some(ba) = self.slots[i].body.as_mut() {
ba.velocity[0] += impulse_mag * normal[0] * inv_m_a;
ba.velocity[1] += impulse_mag * normal[1] * inv_m_a;
ba.velocity[2] += impulse_mag * normal[2] * inv_m_a;
ba.position[0] -= normal[0] * correction_mag * inv_m_a * dt;
ba.position[1] -= normal[1] * correction_mag * inv_m_a * dt;
ba.position[2] -= normal[2] * correction_mag * inv_m_a * dt;
}
}
if !static_b && !sleeping_b && mass_b > 0.0 {
let inv_m_b = 1.0 / mass_b;
if let Some(bb) = self.slots[j].body.as_mut() {
bb.velocity[0] -= impulse_mag * normal[0] * inv_m_b;
bb.velocity[1] -= impulse_mag * normal[1] * inv_m_b;
bb.velocity[2] -= impulse_mag * normal[2] * inv_m_b;
bb.position[0] += normal[0] * correction_mag * inv_m_b * dt;
bb.position[1] += normal[1] * correction_mag * inv_m_b * dt;
bb.position[2] += normal[2] * correction_mag * inv_m_b * dt;
}
}
}
}
}
fn update_sleep(&mut self, dt: f64) {
let lin_thresh = self.config.linear_sleep_threshold;
let ang_thresh = self.config.angular_sleep_threshold;
let time_thresh = self.config.time_before_sleep;
for slot in &mut self.slots {
let body = match slot.body.as_mut() {
Some(b) => b,
None => continue,
};
if !body.can_sleep || body.is_static || body.is_kinematic {
continue;
}
if body.is_sleeping {
continue;
}
let lin_speed = (body.velocity[0] * body.velocity[0]
+ body.velocity[1] * body.velocity[1]
+ body.velocity[2] * body.velocity[2])
.sqrt();
let ang_speed = (body.angular_velocity[0] * body.angular_velocity[0]
+ body.angular_velocity[1] * body.angular_velocity[1]
+ body.angular_velocity[2] * body.angular_velocity[2])
.sqrt();
if lin_speed < lin_thresh && ang_speed < ang_thresh {
body.sleep_timer += dt;
if body.sleep_timer >= time_thresh {
body.is_sleeping = true;
body.velocity = [0.0; 3];
body.angular_velocity = [0.0; 3];
}
} else {
body.sleep_timer = 0.0;
}
}
}
}
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn normalize_quat(q: [f64; 4]) -> [f64; 4] {
let norm = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
if norm < 1e-12 {
[0.0, 0.0, 0.0, 1.0]
} else {
[q[0] / norm, q[1] / norm, q[2] / norm, q[3] / norm]
}
}
fn integrate_orientation(q: [f64; 4], omega: [f64; 3], dt: f64) -> [f64; 4] {
let (qx, qy, qz, qw) = (q[0], q[1], q[2], q[3]);
let (wx, wy, wz) = (omega[0], omega[1], omega[2]);
let half_dt = 0.5 * dt;
let dqx = half_dt * (qw * wx + qy * wz - qz * wy);
let dqy = half_dt * (qw * wy - qx * wz + qz * wx);
let dqz = half_dt * (qw * wz + qx * wy - qy * wx);
let dqw = half_dt * (-qx * wx - qy * wy - qz * wz);
normalize_quat([qx + dqx, qy + dqy, qz + dqz, qw + dqw])
}
fn approximate_inertia(body: &InternalBody) -> f64 {
if body.mass <= 0.0 {
return 0.0;
}
for shape in &body.shapes {
if let PyColliderShape::Sphere { radius } = shape {
return 0.4 * body.mass * radius * radius;
}
if let PyColliderShape::Box { half_extents } = shape {
let hx = half_extents[0];
let hy = half_extents[1];
let hz = half_extents[2];
let ixx = (1.0 / 3.0) * body.mass * (hy * hy + hz * hz);
let iyy = (1.0 / 3.0) * body.mass * (hx * hx + hz * hz);
let izz = (1.0 / 3.0) * body.mass * (hx * hx + hy * hy);
return (ixx + iyy + izz) / 3.0;
}
}
0.4 * body.mass
}
fn first_sphere_radius(shapes: &[PyColliderShape]) -> f64 {
for shape in shapes {
if let PyColliderShape::Sphere { radius } = shape {
return *radius;
}
}
0.0
}