use oxiphysics_collision::broadphase::SweepAndPrune;
use oxiphysics_collision::ccd::{ToiResult, time_of_impact};
use oxiphysics_collision::{BroadPhase, CollisionPair, ContactManifold, NarrowPhaseDispatcher};
use oxiphysics_core::math::{Real, Vec3};
use oxiphysics_core::{Aabb, PhysicsConfig, Transform};
use oxiphysics_rigid::{BodyState, ColliderSet, RigidBodySet};
use std::collections::HashMap;
use std::time::{Duration, Instant};
type ContactKey = (usize, usize);
const CCD_SPEED_THRESHOLD: Real = 0.5;
#[derive(Debug, Clone, Default)]
pub struct StageTimings {
pub forces: Duration,
pub broadphase: Duration,
pub narrowphase: Duration,
pub solver: Duration,
pub integration: Duration,
pub sleep_check: Duration,
}
impl StageTimings {
pub fn reset(&mut self) {
*self = Self::default();
}
pub fn total(&self) -> Duration {
self.forces
+ self.broadphase
+ self.narrowphase
+ self.solver
+ self.integration
+ self.sleep_check
}
}
#[derive(Debug, Clone, Default)]
pub struct PipelineStats {
pub step_count: u64,
pub last_broadphase_pairs: usize,
pub last_contact_manifolds: usize,
pub timings: StageTimings,
}
impl PipelineStats {
#[allow(dead_code)]
pub fn avg_stage_time(&self, stage: &str) -> Duration {
if self.step_count == 0 {
return Duration::ZERO;
}
let n = self.step_count as u32;
match stage {
"forces" => self.timings.forces / n,
"broadphase" => self.timings.broadphase / n,
"narrowphase" => self.timings.narrowphase / n,
"solver" => self.timings.solver / n,
"integration" => self.timings.integration / n,
"sleep_check" => self.timings.sleep_check / n,
_ => Duration::ZERO,
}
}
}
pub type StageCallback = Box<dyn Fn(&str, Duration) + Send + Sync>;
#[derive(Debug, Clone, Default)]
pub struct PipelineConfigBuilder {
config: PhysicsConfig,
}
impl PipelineConfigBuilder {
pub fn new() -> Self {
Self {
config: PhysicsConfig::default(),
}
}
pub fn gravity(mut self, g: Vec3) -> Self {
self.config.gravity = g;
self
}
pub fn solver_iterations(mut self, n: u32) -> Self {
self.config.solver_iterations = n;
self
}
pub fn ccd_enabled(mut self, enabled: bool) -> Self {
self.config.ccd_enabled = enabled;
self
}
pub fn linear_sleep_threshold(mut self, v: Real) -> Self {
self.config.linear_sleep_threshold = v;
self
}
pub fn angular_sleep_threshold(mut self, v: Real) -> Self {
self.config.angular_sleep_threshold = v;
self
}
pub fn time_before_sleep(mut self, t: Real) -> Self {
self.config.time_before_sleep = t;
self
}
pub fn build(self) -> PhysicsConfig {
self.config
}
}
#[derive(Debug, Clone)]
pub struct PipelineCheckpoint {
pub time: Real,
pub contact_cache: HashMap<ContactKey, f64>,
}
#[derive(Debug, Clone)]
pub struct SphRigidCouplingParams {
pub fluid_density: Real,
pub coupling_alpha: Real,
pub region_half_extents: Vec3,
pub region_centre: Vec3,
}
impl Default for SphRigidCouplingParams {
fn default() -> Self {
Self {
fluid_density: 1000.0,
coupling_alpha: 1.0,
region_half_extents: Vec3::new(5.0, 5.0, 5.0),
region_centre: Vec3::zeros(),
}
}
}
#[derive(Debug, Clone)]
pub struct StageProfile {
pub step: u64,
pub sim_time: Real,
pub duration: Duration,
pub stage: &'static str,
}
pub struct PhysicsPipeline {
pub config: PhysicsConfig,
pub time: Real,
broadphase: SweepAndPrune,
#[allow(dead_code)]
contact_cache: HashMap<ContactKey, f64>,
pub stats: PipelineStats,
#[allow(dead_code)]
stage_callback: Option<StageCallback>,
#[allow(dead_code)]
profile_history: Vec<StageProfile>,
#[allow(dead_code)]
profile_history_limit: usize,
pub profiling_enabled: bool,
#[allow(dead_code)]
sph_rigid_coupling: Option<SphRigidCouplingParams>,
}
impl PhysicsPipeline {
pub fn new() -> Self {
Self {
config: PhysicsConfig::default(),
time: 0.0,
broadphase: SweepAndPrune::default(),
contact_cache: HashMap::new(),
stats: PipelineStats::default(),
stage_callback: None,
profile_history: Vec::new(),
profile_history_limit: 1024,
profiling_enabled: false,
sph_rigid_coupling: None,
}
}
pub fn with_config(config: PhysicsConfig) -> Self {
Self {
config,
time: 0.0,
broadphase: SweepAndPrune::default(),
contact_cache: HashMap::new(),
stats: PipelineStats::default(),
stage_callback: None,
profile_history: Vec::new(),
profile_history_limit: 1024,
profiling_enabled: false,
sph_rigid_coupling: None,
}
}
pub fn builder() -> PipelineBuilder {
PipelineBuilder::new()
}
pub fn set_stage_callback(&mut self, cb: StageCallback) {
self.stage_callback = Some(cb);
}
pub fn clear_stage_callback(&mut self) {
self.stage_callback = None;
}
pub fn enable_profiling(&mut self, limit: Option<usize>) {
self.profiling_enabled = true;
if let Some(l) = limit {
self.profile_history_limit = l;
}
}
pub fn disable_profiling(&mut self) {
self.profiling_enabled = false;
}
pub fn drain_profiles(&mut self) -> Vec<StageProfile> {
std::mem::take(&mut self.profile_history)
}
pub fn set_sph_rigid_coupling(&mut self, params: SphRigidCouplingParams) {
self.sph_rigid_coupling = Some(params);
}
pub fn clear_sph_rigid_coupling(&mut self) {
self.sph_rigid_coupling = None;
}
pub fn checkpoint(&self) -> PipelineCheckpoint {
PipelineCheckpoint {
time: self.time,
contact_cache: self.contact_cache.clone(),
}
}
pub fn restore_checkpoint(&mut self, ckpt: PipelineCheckpoint) {
self.time = ckpt.time;
self.contact_cache = ckpt.contact_cache;
}
pub fn reset_stats(&mut self) {
self.stats = PipelineStats::default();
}
fn record_stage(&mut self, stage: &'static str, duration: Duration) {
match stage {
"forces" => self.stats.timings.forces += duration,
"broadphase" => self.stats.timings.broadphase += duration,
"narrowphase" => self.stats.timings.narrowphase += duration,
"solver" => self.stats.timings.solver += duration,
"integration" => self.stats.timings.integration += duration,
"sleep_check" => self.stats.timings.sleep_check += duration,
_ => {}
}
if self.profiling_enabled {
let rec = StageProfile {
step: self.stats.step_count,
sim_time: self.time,
duration,
stage,
};
if self.profile_history.len() >= self.profile_history_limit {
self.profile_history.remove(0);
}
self.profile_history.push(rec);
}
}
fn fire_callback(&self, stage: &str, duration: Duration) {
if let Some(cb) = &self.stage_callback {
cb(stage, duration);
}
}
pub fn step(&mut self, dt: Real, bodies: &mut RigidBodySet, colliders: &ColliderSet) {
let gravity = self.config.gravity;
let t0 = Instant::now();
for (_, body) in bodies.iter_mut() {
body.integrate_forces(dt, &gravity);
}
let d_forces = t0.elapsed();
self.record_stage("forces", d_forces);
self.fire_callback("forces", d_forces);
if self.config.ccd_enabled {
self.step_with_ccd(dt, bodies, colliders);
} else {
self.step_normal(dt, bodies, colliders);
}
let t_sleep = Instant::now();
for (_, body) in bodies.iter_mut() {
body.check_sleep(
dt,
self.config.linear_sleep_threshold,
self.config.angular_sleep_threshold,
self.config.time_before_sleep,
);
}
let d_sleep = t_sleep.elapsed();
self.record_stage("sleep_check", d_sleep);
self.fire_callback("sleep_check", d_sleep);
self.time += dt;
self.stats.step_count += 1;
}
fn step_normal(&mut self, dt: Real, bodies: &mut RigidBodySet, colliders: &ColliderSet) {
let t_np = Instant::now();
let manifolds = self.detect_collisions(bodies, colliders);
let d_np = t_np.elapsed();
self.stats.last_contact_manifolds = manifolds.len();
self.record_stage("narrowphase", d_np);
self.fire_callback("narrowphase", d_np);
let t_sol = Instant::now();
self.solve_contacts(&manifolds, bodies, colliders, dt);
let d_sol = t_sol.elapsed();
self.record_stage("solver", d_sol);
self.fire_callback("solver", d_sol);
let t_int = Instant::now();
for (_, body) in bodies.iter_mut() {
body.integrate_velocity(dt);
}
let d_int = t_int.elapsed();
self.record_stage("integration", d_int);
self.fire_callback("integration", d_int);
}
fn step_with_ccd(&mut self, dt: Real, bodies: &mut RigidBodySet, colliders: &ColliderSet) {
let t_bp = Instant::now();
let ccd_hits = self.detect_collisions_ccd(dt, bodies, colliders);
let d_bp = t_bp.elapsed();
self.stats.last_broadphase_pairs = ccd_hits.len();
self.record_stage("broadphase", d_bp);
self.fire_callback("broadphase", d_bp);
let t_np = Instant::now();
let earliest = ccd_hits
.iter()
.filter(|(toi_result, _, _)| toi_result.toi < 1.0)
.min_by(|(a, _, _), (b, _, _)| {
a.toi
.partial_cmp(&b.toi)
.unwrap_or(std::cmp::Ordering::Equal)
});
let d_np = t_np.elapsed();
self.record_stage("narrowphase", d_np);
self.fire_callback("narrowphase", d_np);
if let Some((toi_result, col_a, col_b)) = earliest {
let t = toi_result.toi.clamp(0.0, 1.0);
let dt_first = t * dt;
let dt_rest = (1.0 - t) * dt;
let t_int = Instant::now();
for (_, body) in bodies.iter_mut() {
body.integrate_velocity(dt_first);
}
let d_int = t_int.elapsed();
self.record_stage("integration", d_int);
self.fire_callback("integration", d_int);
let t_sol = Instant::now();
self.apply_ccd_impulse(toi_result, *col_a, *col_b, bodies, colliders);
let d_sol = t_sol.elapsed();
self.record_stage("solver", d_sol);
self.fire_callback("solver", d_sol);
if dt_rest > 1e-10 {
let t_int2 = Instant::now();
for (_, body) in bodies.iter_mut() {
body.integrate_velocity(dt_rest);
}
let d_int2 = t_int2.elapsed();
self.record_stage("integration", d_int2);
self.fire_callback("integration", d_int2);
}
} else {
let t_np2 = Instant::now();
let manifolds = self.detect_collisions(bodies, colliders);
let d_np2 = t_np2.elapsed();
self.stats.last_contact_manifolds = manifolds.len();
self.record_stage("narrowphase", d_np2);
self.fire_callback("narrowphase", d_np2);
let t_sol = Instant::now();
self.solve_contacts(&manifolds, bodies, colliders, dt);
let d_sol = t_sol.elapsed();
self.record_stage("solver", d_sol);
self.fire_callback("solver", d_sol);
let t_int = Instant::now();
for (_, body) in bodies.iter_mut() {
body.integrate_velocity(dt);
}
let d_int = t_int.elapsed();
self.record_stage("integration", d_int);
self.fire_callback("integration", d_int);
}
}
#[allow(dead_code)]
fn detect_collisions_ccd(
&self,
dt: Real,
bodies: &RigidBodySet,
colliders: &ColliderSet,
) -> Vec<(ToiResult, usize, usize)> {
let collider_data: Vec<_> = colliders.iter().collect();
if collider_data.is_empty() {
return Vec::new();
}
let swept_aabbs: Vec<Aabb> = collider_data
.iter()
.map(|(_, col)| {
let body = col.body_handle.and_then(|h| bodies.get(h));
let body_t_start = body.map(|b| b.transform.clone()).unwrap_or_default();
let body_t_end = body
.map(|b| {
let end_pos = b.transform.position + b.velocity * dt;
Transform::from_position(end_pos)
})
.unwrap_or_default();
let local_aabb = col.shape.bounding_box();
let world_t_start = col.world_transform(&body_t_start);
let aabb_start = Aabb::new(
local_aabb.min + world_t_start.position,
local_aabb.max + world_t_start.position,
);
let world_t_end = col.world_transform(&body_t_end);
let aabb_end = Aabb::new(
local_aabb.min + world_t_end.position,
local_aabb.max + world_t_end.position,
);
aabb_start.merge(&aabb_end)
})
.collect();
let pairs = self.broadphase.find_pairs(&swept_aabbs);
let mut results = Vec::new();
for pair in &pairs {
let (_, col_a) = &collider_data[pair.a];
let (_, col_b) = &collider_data[pair.b];
if col_a.is_sensor && col_b.is_sensor {
continue;
}
let body_a = col_a.body_handle.and_then(|h| bodies.get(h));
let body_b = col_b.body_handle.and_then(|h| bodies.get(h));
let vel_a = body_a.map(|b| b.velocity).unwrap_or(Vec3::zeros());
let vel_b = body_b.map(|b| b.velocity).unwrap_or(Vec3::zeros());
let rel_speed = (vel_a - vel_b).norm() * dt;
if rel_speed < CCD_SPEED_THRESHOLD {
continue;
}
let body_t_a_start = body_a.map(|b| b.transform.clone()).unwrap_or_default();
let body_t_a_end = body_a
.map(|b| {
let end_pos = b.transform.position + b.velocity * dt;
Transform::from_position(end_pos)
})
.unwrap_or_default();
let body_t_b_start = body_b.map(|b| b.transform.clone()).unwrap_or_default();
let body_t_b_end = body_b
.map(|b| {
let end_pos = b.transform.position + b.velocity * dt;
Transform::from_position(end_pos)
})
.unwrap_or_default();
let t_a_start = col_a.world_transform(&body_t_a_start);
let t_a_end = col_a.world_transform(&body_t_a_end);
let t_b_start = col_b.world_transform(&body_t_b_start);
let t_b_end = col_b.world_transform(&body_t_b_end);
if let Some(toi) = time_of_impact(
col_a.shape.as_ref(),
&t_a_start,
&t_a_end,
col_b.shape.as_ref(),
&t_b_start,
&t_b_end,
) {
results.push((toi, pair.a, pair.b));
}
}
results
}
fn apply_ccd_impulse(
&self,
toi_result: &ToiResult,
col_idx_a: usize,
col_idx_b: usize,
bodies: &mut RigidBodySet,
colliders: &ColliderSet,
) {
let collider_data: Vec<_> = colliders.iter().collect();
let (_, col_a) = &collider_data[col_idx_a];
let (_, col_b) = &collider_data[col_idx_b];
let ha = col_a.body_handle;
let hb = col_b.body_handle;
let restitution = (col_a.restitution + col_b.restitution) * 0.5;
let normal = -toi_result.normal;
let cp = (toi_result.witness_a + toi_result.witness_b) * 0.5;
let (vel_a, omega_a, inv_mass_a, inv_inertia_a, pos_a, dyn_a) = ha
.and_then(|h| bodies.get(h))
.map(|b| {
(
b.velocity,
b.angular_velocity,
b.inverse_mass,
b.world_inverse_inertia,
b.transform.position,
b.is_dynamic(),
)
})
.unwrap_or((
Vec3::zeros(),
Vec3::zeros(),
0.0,
oxiphysics_core::math::Mat3::zeros(),
Vec3::zeros(),
false,
));
let (vel_b, omega_b, inv_mass_b, inv_inertia_b, pos_b, dyn_b) = hb
.and_then(|h| bodies.get(h))
.map(|b| {
(
b.velocity,
b.angular_velocity,
b.inverse_mass,
b.world_inverse_inertia,
b.transform.position,
b.is_dynamic(),
)
})
.unwrap_or((
Vec3::zeros(),
Vec3::zeros(),
0.0,
oxiphysics_core::math::Mat3::zeros(),
Vec3::zeros(),
false,
));
let r_a = cp - pos_a;
let r_b = cp - pos_b;
let vcp_a = vel_a + omega_a.cross(&r_a);
let vcp_b = vel_b + omega_b.cross(&r_b);
let rel_vel = vcp_a - vcp_b;
let normal_vel = rel_vel.dot(&normal);
if normal_vel >= 0.0 {
return;
}
let rn_a = r_a.cross(&normal);
let rn_b = r_b.cross(&normal);
let ang_a = rn_a.dot(&(inv_inertia_a * rn_a));
let ang_b = rn_b.dot(&(inv_inertia_b * rn_b));
let eff_mass_denom = inv_mass_a + inv_mass_b + ang_a + ang_b;
if eff_mass_denom < 1e-10 {
return;
}
let j_normal = -(1.0 + restitution) * normal_vel / eff_mass_denom;
let normal_impulse = normal * j_normal;
if let Some(h) = ha
&& let Some(body) = bodies.get_mut(h)
&& dyn_a
{
body.velocity += normal_impulse * body.inverse_mass;
body.angular_velocity += body.world_inverse_inertia * r_a.cross(&normal_impulse);
body.wake_up();
}
if let Some(h) = hb
&& let Some(body) = bodies.get_mut(h)
&& dyn_b
{
body.velocity -= normal_impulse * body.inverse_mass;
body.angular_velocity -= body.world_inverse_inertia * r_b.cross(&normal_impulse);
body.wake_up();
}
}
fn detect_collisions(
&self,
bodies: &RigidBodySet,
colliders: &ColliderSet,
) -> Vec<(ContactManifold, usize, usize)> {
let collider_data: Vec<_> = colliders.iter().collect();
if collider_data.is_empty() {
return Vec::new();
}
let aabbs: Vec<Aabb> = collider_data
.iter()
.map(|(_, col)| {
let body_t = col
.body_handle
.and_then(|h| bodies.get(h))
.map(|b| b.transform.clone())
.unwrap_or_default();
let world_t = col.world_transform(&body_t);
let local_aabb = col.shape.bounding_box();
Aabb::new(
local_aabb.min + world_t.position,
local_aabb.max + world_t.position,
)
})
.collect();
let pairs = self.broadphase.find_pairs(&aabbs);
let mut manifolds = Vec::new();
for pair in &pairs {
let (_, col_a) = &collider_data[pair.a];
let (_, col_b) = &collider_data[pair.b];
if col_a.is_sensor && col_b.is_sensor {
continue;
}
let body_t_a = col_a
.body_handle
.and_then(|h| bodies.get(h))
.map(|b| b.transform.clone())
.unwrap_or_default();
let body_t_b = col_b
.body_handle
.and_then(|h| bodies.get(h))
.map(|b| b.transform.clone())
.unwrap_or_default();
let t_a = col_a.world_transform(&body_t_a);
let t_b = col_b.world_transform(&body_t_b);
if let Some(manifold) = NarrowPhaseDispatcher::generate_contacts(
col_a.shape.as_ref(),
&t_a,
col_b.shape.as_ref(),
&t_b,
CollisionPair::new(pair.a, pair.b),
) {
manifolds.push((manifold, pair.a, pair.b));
}
}
manifolds
}
fn solve_contacts(
&mut self,
manifolds: &[(ContactManifold, usize, usize)],
bodies: &mut RigidBodySet,
colliders: &ColliderSet,
_dt: Real,
) {
let collider_data: Vec<_> = colliders.iter().collect();
let mut active_keys: HashMap<ContactKey, f64> = HashMap::new();
for (manifold, col_idx_a, col_idx_b) in manifolds {
let key: ContactKey = if col_idx_a <= col_idx_b {
(*col_idx_a, *col_idx_b)
} else {
(*col_idx_b, *col_idx_a)
};
let (_, col_a) = &collider_data[*col_idx_a];
let (_, col_b) = &collider_data[*col_idx_b];
let ha = col_a.body_handle;
let hb = col_b.body_handle;
if let Some(&cached_impulse) = self.contact_cache.get(&key) {
let n_contacts = manifold.contacts.len();
if n_contacts == 0 {
continue;
}
let impulse_per_contact = cached_impulse / n_contacts as f64;
for contact in &manifold.contacts {
let cp = contact.point();
let normal = contact.normal;
let (inv_mass_a, _inv_inertia_a, pos_a, dyn_a) = ha
.and_then(|h| bodies.get(h))
.map(|b| {
(
b.inverse_mass,
b.world_inverse_inertia,
b.transform.position,
b.is_dynamic(),
)
})
.unwrap_or((
0.0,
oxiphysics_core::math::Mat3::zeros(),
Vec3::zeros(),
false,
));
let (inv_mass_b, _inv_inertia_b, pos_b, dyn_b) = hb
.and_then(|h| bodies.get(h))
.map(|b| {
(
b.inverse_mass,
b.world_inverse_inertia,
b.transform.position,
b.is_dynamic(),
)
})
.unwrap_or((
0.0,
oxiphysics_core::math::Mat3::zeros(),
Vec3::zeros(),
false,
));
let r_a = cp - pos_a;
let r_b = cp - pos_b;
let warm_impulse = normal * impulse_per_contact;
if let Some(h) = ha
&& let Some(body) = bodies.get_mut(h)
&& dyn_a
{
body.velocity += warm_impulse * inv_mass_a;
body.angular_velocity +=
body.world_inverse_inertia * r_a.cross(&warm_impulse);
body.wake_up();
}
if let Some(h) = hb
&& let Some(body) = bodies.get_mut(h)
&& dyn_b
{
body.velocity -= warm_impulse * inv_mass_b;
body.angular_velocity -=
body.world_inverse_inertia * r_b.cross(&warm_impulse);
body.wake_up();
}
}
}
active_keys.insert(key, 0.0);
}
let solver_iterations = self.config.solver_iterations;
for iteration in 0..solver_iterations {
let is_last = iteration + 1 == solver_iterations;
for (manifold, col_idx_a, col_idx_b) in manifolds {
let key: ContactKey = if col_idx_a <= col_idx_b {
(*col_idx_a, *col_idx_b)
} else {
(*col_idx_b, *col_idx_a)
};
let (_, col_a) = &collider_data[*col_idx_a];
let (_, col_b) = &collider_data[*col_idx_b];
let ha = col_a.body_handle;
let hb = col_b.body_handle;
let restitution = (col_a.restitution + col_b.restitution) * 0.5;
let friction = (col_a.friction + col_b.friction) * 0.5;
for contact in &manifold.contacts {
let cp = contact.point();
let (vel_a, omega_a, inv_mass_a, inv_inertia_a, pos_a, dyn_a) = ha
.and_then(|h| bodies.get(h))
.map(|b| {
(
b.velocity,
b.angular_velocity,
b.inverse_mass,
b.world_inverse_inertia,
b.transform.position,
b.is_dynamic(),
)
})
.unwrap_or((
Vec3::zeros(),
Vec3::zeros(),
0.0,
oxiphysics_core::math::Mat3::zeros(),
Vec3::zeros(),
false,
));
let (vel_b, omega_b, inv_mass_b, inv_inertia_b, pos_b, dyn_b) = hb
.and_then(|h| bodies.get(h))
.map(|b| {
(
b.velocity,
b.angular_velocity,
b.inverse_mass,
b.world_inverse_inertia,
b.transform.position,
b.is_dynamic(),
)
})
.unwrap_or((
Vec3::zeros(),
Vec3::zeros(),
0.0,
oxiphysics_core::math::Mat3::zeros(),
Vec3::zeros(),
false,
));
let r_a = cp - pos_a;
let r_b = cp - pos_b;
let vcp_a = vel_a + omega_a.cross(&r_a);
let vcp_b = vel_b + omega_b.cross(&r_b);
let rel_vel = vcp_a - vcp_b;
let normal_vel = rel_vel.dot(&contact.normal);
if normal_vel > 0.0 {
continue;
}
let rn_a = r_a.cross(&contact.normal);
let rn_b = r_b.cross(&contact.normal);
let ang_a = rn_a.dot(&(inv_inertia_a * rn_a));
let ang_b = rn_b.dot(&(inv_inertia_b * rn_b));
let eff_mass_denom = inv_mass_a + inv_mass_b + ang_a + ang_b;
if eff_mass_denom < 1e-10 {
continue;
}
let j_normal = -(1.0 + restitution) * normal_vel / eff_mass_denom;
let normal_impulse = contact.normal * j_normal;
if is_last {
*active_keys.entry(key).or_insert(0.0) += j_normal;
}
if let Some(h) = ha
&& let Some(body) = bodies.get_mut(h)
&& dyn_a
{
body.velocity += normal_impulse * body.inverse_mass;
body.angular_velocity +=
body.world_inverse_inertia * r_a.cross(&normal_impulse);
body.wake_up();
}
if let Some(h) = hb
&& let Some(body) = bodies.get_mut(h)
&& dyn_b
{
body.velocity -= normal_impulse * body.inverse_mass;
body.angular_velocity -=
body.world_inverse_inertia * r_b.cross(&normal_impulse);
body.wake_up();
}
let tangent_vel = rel_vel - contact.normal * normal_vel;
let tangent_speed = tangent_vel.norm();
if tangent_speed > 1e-10 {
let tangent = tangent_vel / tangent_speed;
let rt_a = r_a.cross(&tangent);
let rt_b = r_b.cross(&tangent);
let ang_t_a = rt_a.dot(&(inv_inertia_a * rt_a));
let ang_t_b = rt_b.dot(&(inv_inertia_b * rt_b));
let eff_mass_t = inv_mass_a + inv_mass_b + ang_t_a + ang_t_b;
let j_tangent = if eff_mass_t > 1e-10 {
(-tangent_speed / eff_mass_t).max(-friction * j_normal)
} else {
0.0
};
let friction_impulse = tangent * j_tangent;
if let Some(h) = ha
&& let Some(body) = bodies.get_mut(h)
&& dyn_a
{
body.velocity += friction_impulse * body.inverse_mass;
body.angular_velocity +=
body.world_inverse_inertia * r_a.cross(&friction_impulse);
}
if let Some(h) = hb
&& let Some(body) = bodies.get_mut(h)
&& dyn_b
{
body.velocity -= friction_impulse * body.inverse_mass;
body.angular_velocity -=
body.world_inverse_inertia * r_b.cross(&friction_impulse);
}
}
let slop = 0.005;
let baumgarte = 0.2;
let corr_mag = baumgarte * (contact.depth - slop).max(0.0) / eff_mass_denom;
let correction = contact.normal * corr_mag;
if let Some(h) = ha
&& let Some(body) = bodies.get_mut(h)
&& dyn_a
{
body.transform.position += correction * body.inverse_mass;
}
if let Some(h) = hb
&& let Some(body) = bodies.get_mut(h)
&& dyn_b
{
body.transform.position -= correction * body.inverse_mass;
}
}
}
}
self.contact_cache = active_keys;
}
}
impl Default for PhysicsPipeline {
fn default() -> Self {
Self::new()
}
}
pub struct PipelineBuilder {
config_builder: PipelineConfigBuilder,
profiling: bool,
profile_limit: Option<usize>,
sph_coupling: Option<SphRigidCouplingParams>,
}
impl PipelineBuilder {
pub fn new() -> Self {
Self {
config_builder: PipelineConfigBuilder::new(),
profiling: false,
profile_limit: None,
sph_coupling: None,
}
}
pub fn gravity(mut self, g: Vec3) -> Self {
self.config_builder = self.config_builder.gravity(g);
self
}
pub fn ccd_enabled(mut self, enabled: bool) -> Self {
self.config_builder = self.config_builder.ccd_enabled(enabled);
self
}
pub fn solver_iterations(mut self, n: u32) -> Self {
self.config_builder = self.config_builder.solver_iterations(n);
self
}
pub fn profiling(mut self, enabled: bool) -> Self {
self.profiling = enabled;
self
}
pub fn profile_limit(mut self, limit: usize) -> Self {
self.profile_limit = Some(limit);
self
}
pub fn sph_rigid_coupling(mut self, params: SphRigidCouplingParams) -> Self {
self.sph_coupling = Some(params);
self
}
pub fn finish(self) -> PhysicsPipeline {
let config = self.config_builder.build();
let mut pipeline = PhysicsPipeline::with_config(config);
if self.profiling {
pipeline.enable_profiling(self.profile_limit);
}
if let Some(params) = self.sph_coupling {
pipeline.set_sph_rigid_coupling(params);
}
pipeline
}
}
impl Default for PipelineBuilder {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
pub fn apply_buoyancy_impulse(
bodies: &mut RigidBodySet,
coupling: &SphRigidCouplingParams,
effective_radius: Real,
g_magnitude: Real,
dt: Real,
) {
let half = coupling.region_half_extents;
let centre = coupling.region_centre;
let rho_f = coupling.fluid_density;
let alpha = coupling.coupling_alpha;
let submerged_vol = (4.0 / 3.0) * std::f64::consts::PI * effective_radius.powi(3);
let buoyancy_force_mag = rho_f * g_magnitude * submerged_vol * alpha;
for (_, body) in bodies.iter_mut() {
if !body.is_dynamic() {
continue;
}
let p = body.transform.position;
if (p.x - centre.x).abs() <= half.x
&& (p.y - centre.y).abs() <= half.y
&& (p.z - centre.z).abs() <= half.z
{
let impulse = Vec3::new(0.0, buoyancy_force_mag * dt, 0.0);
body.velocity += impulse * body.inverse_mass;
}
}
}
#[allow(dead_code)]
pub fn total_kinetic_energy(bodies: &RigidBodySet) -> Real {
let mut ek = 0.0;
for (_, body) in bodies.iter() {
if body.is_dynamic() {
let m = if body.inverse_mass > 1e-15 {
1.0 / body.inverse_mass
} else {
0.0
};
ek += 0.5 * m * body.velocity.norm_squared();
}
}
ek
}
#[allow(dead_code)]
pub fn count_awake_bodies(bodies: &RigidBodySet) -> usize {
bodies
.iter()
.filter(|(_, b)| b.is_dynamic() && b.state != BodyState::Sleeping)
.count()
}
#[cfg(test)]
mod tests {
use super::*;
use oxiphysics_core::Transform;
use oxiphysics_geometry::{BoxShape, Shape, Sphere};
use oxiphysics_rigid::{Collider, RigidBody};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
#[test]
fn test_pipeline_gravity() {
let mut pipeline = PhysicsPipeline::new();
let mut bodies = RigidBodySet::new();
let colliders = ColliderSet::new();
let mut body = RigidBody::new(1.0);
body.transform = Transform::from_position(Vec3::new(0.0, 10.0, 0.0));
body.linear_damping = 0.0;
let h = bodies.insert(body);
for _ in 0..60 {
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
}
let body = bodies.get(h).unwrap();
assert!(
body.transform.position.y < 6.0,
"y={}",
body.transform.position.y
);
assert!(body.velocity.y < -5.0, "vy={}", body.velocity.y);
}
#[test]
fn test_pipeline_sphere_on_floor() {
let mut pipeline = PhysicsPipeline::new();
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let mut sphere_body = RigidBody::new(1.0);
sphere_body.transform = Transform::from_position(Vec3::new(0.0, 5.0, 0.0));
sphere_body.linear_damping = 0.0;
let sh = bodies.insert(sphere_body);
let sphere_shape: Arc<dyn Shape> = Arc::new(Sphere::new(0.5));
colliders.insert(
Collider::new(sphere_shape)
.with_body(sh)
.with_restitution(0.5),
);
let mut floor = RigidBody::new_static();
floor.transform = Transform::from_position(Vec3::new(0.0, -0.5, 0.0));
let fh = bodies.insert(floor);
let floor_shape: Arc<dyn Shape> = Arc::new(BoxShape::new(Vec3::new(10.0, 0.5, 10.0)));
colliders.insert(Collider::new(floor_shape).with_body(fh));
for _ in 0..300 {
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
}
let sphere = bodies.get(sh).unwrap();
assert!(
sphere.transform.position.y > -1.0 && sphere.transform.position.y < 5.0,
"Sphere y={} should be near floor",
sphere.transform.position.y
);
}
#[test]
fn test_pipeline_no_colliders() {
let mut pipeline = PhysicsPipeline::new();
let mut bodies = RigidBodySet::new();
let colliders = ColliderSet::new();
bodies.insert(RigidBody::new(1.0));
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
}
#[test]
fn test_pipeline_ccd_no_tunnel() {
let config = PhysicsConfig {
ccd_enabled: true,
gravity: Vec3::zeros(),
..PhysicsConfig::default()
};
let mut pipeline = PhysicsPipeline::with_config(config);
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let mut body_a = RigidBody::new(1.0);
body_a.transform = Transform::from_position(Vec3::new(-5.0, 0.0, 0.0));
body_a.velocity = Vec3::new(100.0, 0.0, 0.0);
body_a.linear_damping = 0.0;
let ha = bodies.insert(body_a);
let shape_a: Arc<dyn Shape> = Arc::new(Sphere::new(0.5));
colliders.insert(Collider::new(shape_a).with_body(ha).with_restitution(1.0));
let mut body_b = RigidBody::new(1.0);
body_b.transform = Transform::from_position(Vec3::new(5.0, 0.0, 0.0));
body_b.velocity = Vec3::new(-100.0, 0.0, 0.0);
body_b.linear_damping = 0.0;
let hb = bodies.insert(body_b);
let shape_b: Arc<dyn Shape> = Arc::new(Sphere::new(0.5));
colliders.insert(Collider::new(shape_b).with_body(hb).with_restitution(1.0));
let dt = 0.1_f64;
let x_a_initial = bodies.get(ha).unwrap().transform.position.x;
let x_b_initial = bodies.get(hb).unwrap().transform.position.x;
pipeline.step(dt, &mut bodies, &colliders);
let vel_a_after = bodies.get(ha).unwrap().velocity.x;
let vel_b_after = bodies.get(hb).unwrap().velocity.x;
let x_a_after = bodies.get(ha).unwrap().transform.position.x;
let x_b_after = bodies.get(hb).unwrap().transform.position.x;
assert!(
vel_a_after < 0.0,
"Sphere A velocity should be negative after collision, got {}",
vel_a_after
);
assert!(
vel_b_after > 0.0,
"Sphere B velocity should be positive after collision, got {}",
vel_b_after
);
assert!(
x_a_after <= x_b_after + 1.0 + 1e-3,
"Sphere A (x={}) tunneled past sphere B (x={})",
x_a_after,
x_b_after
);
assert!(
(x_a_after - x_a_initial).abs() > 0.01,
"Sphere A did not move"
);
assert!(
(x_b_after - x_b_initial).abs() > 0.01,
"Sphere B did not move"
);
}
#[test]
fn test_warmstarting_converges_faster() {
let config = PhysicsConfig {
gravity: Vec3::new(0.0, -9.81, 0.0),
solver_iterations: 1,
ccd_enabled: false,
..PhysicsConfig::default()
};
let make_scene = |cfg: PhysicsConfig| {
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let mut floor = RigidBody::new_static();
floor.transform = Transform::from_position(Vec3::new(0.0, -0.5, 0.0));
let fh = bodies.insert(floor);
let floor_shape: Arc<dyn Shape> = Arc::new(BoxShape::new(Vec3::new(10.0, 0.5, 10.0)));
colliders.insert(Collider::new(floor_shape).with_body(fh));
let mut boxb = RigidBody::new(1.0);
boxb.transform = Transform::from_position(Vec3::new(0.0, 0.48, 0.0));
boxb.linear_damping = 0.0;
let bh = bodies.insert(boxb);
let box_shape: Arc<dyn Shape> = Arc::new(BoxShape::new(Vec3::new(0.5, 0.5, 0.5)));
colliders.insert(Collider::new(box_shape).with_body(bh));
(PhysicsPipeline::with_config(cfg), bodies, colliders, bh)
};
let (mut pipe_cold, mut bodies_cold, colliders_cold, bh_cold) = make_scene(config.clone());
pipe_cold.step(1.0 / 60.0, &mut bodies_cold, &colliders_cold);
pipe_cold.contact_cache.clear();
let vy_before_cold = bodies_cold.get(bh_cold).unwrap().velocity.y;
pipe_cold.step(1.0 / 60.0, &mut bodies_cold, &colliders_cold);
let vy_after_cold = bodies_cold.get(bh_cold).unwrap().velocity.y;
let residual_cold = vy_after_cold.abs();
let (mut pipe_warm, mut bodies_warm, colliders_warm, bh_warm) = make_scene(config.clone());
pipe_warm.step(1.0 / 60.0, &mut bodies_warm, &colliders_warm);
let _vy_before_warm = bodies_warm.get(bh_warm).unwrap().velocity.y;
pipe_warm.step(1.0 / 60.0, &mut bodies_warm, &colliders_warm);
let vy_after_warm = bodies_warm.get(bh_warm).unwrap().velocity.y;
let residual_warm = vy_after_warm.abs();
assert!(
!pipe_warm.contact_cache.is_empty(),
"contact_cache should be populated after a contact step"
);
assert!(
residual_warm <= residual_cold + 1e-9,
"warmstarted residual ({}) should be <= cold residual ({}); vy_before_cold={}",
residual_warm,
residual_cold,
vy_before_cold,
);
}
#[test]
fn test_warmstart_cache_cleared_on_separation() {
let config = PhysicsConfig {
gravity: Vec3::zeros(),
solver_iterations: 4,
ccd_enabled: false,
..PhysicsConfig::default()
};
let mut pipeline = PhysicsPipeline::with_config(config);
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let mut floor = RigidBody::new_static();
floor.transform = Transform::from_position(Vec3::new(0.0, -0.5, 0.0));
let fh = bodies.insert(floor);
let floor_shape: Arc<dyn Shape> = Arc::new(BoxShape::new(Vec3::new(10.0, 0.5, 10.0)));
colliders.insert(Collider::new(floor_shape).with_body(fh));
let mut boxb = RigidBody::new(1.0);
boxb.transform = Transform::from_position(Vec3::new(0.0, 0.48, 0.0));
boxb.linear_damping = 0.0;
let bh = bodies.insert(boxb);
let box_shape: Arc<dyn Shape> = Arc::new(BoxShape::new(Vec3::new(0.5, 0.5, 0.5)));
colliders.insert(Collider::new(box_shape).with_body(bh));
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
assert!(
!pipeline.contact_cache.is_empty(),
"cache should be non-empty while bodies are in contact"
);
bodies.get_mut(bh).unwrap().transform.position.y = 100.0;
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
assert!(
pipeline.contact_cache.is_empty(),
"cache should be empty after bodies separate; found {:?}",
pipeline.contact_cache
);
}
#[test]
fn test_pipeline_stats_step_count() {
let mut pipeline = PhysicsPipeline::new();
let mut bodies = RigidBodySet::new();
let colliders = ColliderSet::new();
bodies.insert(RigidBody::new(1.0));
assert_eq!(pipeline.stats.step_count, 0);
for i in 1..=5u64 {
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
assert_eq!(pipeline.stats.step_count, i);
}
}
#[test]
fn test_pipeline_stats_timing_accumulates() {
let mut pipeline = PhysicsPipeline::new();
let mut bodies = RigidBodySet::new();
let colliders = ColliderSet::new();
bodies.insert(RigidBody::new(1.0));
for _ in 0..10 {
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
}
let total = pipeline.stats.timings.total();
assert!(total >= Duration::ZERO);
}
#[test]
fn test_pipeline_stats_reset() {
let mut pipeline = PhysicsPipeline::new();
let mut bodies = RigidBodySet::new();
let colliders = ColliderSet::new();
bodies.insert(RigidBody::new(1.0));
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
assert_eq!(pipeline.stats.step_count, 1);
pipeline.reset_stats();
assert_eq!(pipeline.stats.step_count, 0);
}
#[test]
fn test_pipeline_builder() {
let pipeline = PhysicsPipeline::builder()
.gravity(Vec3::new(0.0, -9.81, 0.0))
.ccd_enabled(false)
.solver_iterations(8_u32)
.profiling(true)
.finish();
assert!((pipeline.config.gravity.y + 9.81).abs() < 1e-12);
assert!(!pipeline.config.ccd_enabled);
assert_eq!(pipeline.config.solver_iterations, 8_u32);
assert!(pipeline.profiling_enabled);
}
#[test]
fn test_pipeline_checkpoint_restore() {
let mut pipeline = PhysicsPipeline::new();
let mut bodies = RigidBodySet::new();
let mut colliders = ColliderSet::new();
let mut floor = RigidBody::new_static();
floor.transform = Transform::from_position(Vec3::new(0.0, -0.5, 0.0));
let fh = bodies.insert(floor);
let floor_shape: Arc<dyn Shape> = Arc::new(BoxShape::new(Vec3::new(10.0, 0.5, 10.0)));
colliders.insert(Collider::new(floor_shape).with_body(fh));
let mut boxb = RigidBody::new(1.0);
boxb.transform = Transform::from_position(Vec3::new(0.0, 0.48, 0.0));
let bh = bodies.insert(boxb);
let box_shape: Arc<dyn Shape> = Arc::new(BoxShape::new(Vec3::new(0.5, 0.5, 0.5)));
colliders.insert(Collider::new(box_shape).with_body(bh));
for _ in 0..5 {
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
}
let checkpoint = pipeline.checkpoint();
let time_at_checkpoint = checkpoint.time;
for _ in 0..10 {
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
}
assert!(pipeline.time > time_at_checkpoint);
pipeline.restore_checkpoint(checkpoint);
assert!((pipeline.time - time_at_checkpoint).abs() < 1e-14);
}
#[test]
fn test_stage_callback_fires() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = Arc::clone(&counter);
let mut pipeline = PhysicsPipeline::new();
pipeline.set_stage_callback(Box::new(move |_stage, _dur| {
counter_clone.fetch_add(1, Ordering::Relaxed);
}));
let mut bodies = RigidBodySet::new();
let colliders = ColliderSet::new();
bodies.insert(RigidBody::new(1.0));
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
assert!(counter.load(Ordering::Relaxed) >= 2);
}
#[test]
fn test_profiling_records_stages() {
let mut pipeline = PhysicsPipeline::new();
pipeline.enable_profiling(Some(128));
let mut bodies = RigidBodySet::new();
let colliders = ColliderSet::new();
bodies.insert(RigidBody::new(1.0));
pipeline.step(1.0 / 60.0, &mut bodies, &colliders);
let profiles = pipeline.drain_profiles();
assert!(
!profiles.is_empty(),
"profiling should record at least one stage"
);
assert!(profiles.iter().all(|p| p.step == 0));
}
#[test]
fn test_sph_rigid_coupling_params_default() {
let params = SphRigidCouplingParams::default();
assert!((params.fluid_density - 1000.0).abs() < 1e-12);
assert!((params.coupling_alpha - 1.0).abs() < 1e-12);
}
#[test]
fn test_pipeline_config_builder() {
let cfg = PipelineConfigBuilder::new()
.gravity(Vec3::new(0.0, -9.81, 0.0))
.solver_iterations(12_u32)
.ccd_enabled(true)
.build();
assert!((cfg.gravity.y + 9.81).abs() < 1e-12);
assert_eq!(cfg.solver_iterations, 12_u32);
assert!(cfg.ccd_enabled);
}
#[test]
fn test_total_kinetic_energy_zero_static() {
let mut bodies = RigidBodySet::new();
let mut floor = RigidBody::new_static();
floor.transform = Transform::from_position(Vec3::zeros());
bodies.insert(floor);
let ek = total_kinetic_energy(&bodies);
assert!(
ek.abs() < 1e-14,
"static body should contribute zero kinetic energy"
);
}
#[test]
fn test_total_kinetic_energy_moving_body() {
let mut bodies = RigidBodySet::new();
let mut body = RigidBody::new(2.0);
body.velocity = Vec3::new(3.0, 4.0, 0.0); bodies.insert(body);
let ek = total_kinetic_energy(&bodies);
assert!((ek - 25.0).abs() < 1e-9, "KE should be 25 J, got {}", ek);
}
#[test]
fn test_count_awake_bodies() {
let mut bodies = RigidBodySet::new();
bodies.insert(RigidBody::new(1.0)); let mut floor = RigidBody::new_static();
floor.transform = Transform::from_position(Vec3::zeros());
bodies.insert(floor);
let awake = count_awake_bodies(&bodies);
assert_eq!(awake, 1, "only the dynamic body should be awake");
}
#[test]
fn test_apply_buoyancy_impulse() {
let coupling = SphRigidCouplingParams {
fluid_density: 1000.0,
coupling_alpha: 1.0,
region_half_extents: Vec3::new(10.0, 10.0, 10.0),
region_centre: Vec3::zeros(),
};
let mut bodies = RigidBodySet::new();
let mut body = RigidBody::new(1.0);
body.velocity = Vec3::zeros();
body.transform = Transform::from_position(Vec3::zeros());
let bh = bodies.insert(body);
apply_buoyancy_impulse(&mut bodies, &coupling, 0.5, 9.81, 1.0 / 60.0);
let vy = bodies.get(bh).unwrap().velocity.y;
assert!(vy > 0.0, "buoyancy should push body upward, got vy={}", vy);
}
}