mod gate;
mod toi;
use alloc::vec::Vec;
use concinnity_memory::Pool;
use super::body::Body;
use super::config::SimConfig;
use super::math::Vec3;
use super::scene::Scene;
use super::sensor::Sensors;
use toi::{Blocked, Probe};
#[derive(Debug, Clone, Copy)]
struct Mover {
slot: u32,
start: Vec3,
motion: Vec3,
driven: bool,
outcome: Option<Outcome>,
}
#[derive(Debug, Clone, Copy)]
enum Outcome {
Stop { target: u32, position: Vec3 },
Shove { target: u32, offset: Vec3 },
}
pub(crate) struct Ccd {
movers: Vec<Mover>,
crossings: Vec<(u32, u32)>,
max_motion_sq: f32,
overflows: u32,
}
impl Ccd {
pub(crate) fn with_capacity(capacity: usize) -> Self {
Ccd {
movers: Vec::with_capacity(capacity),
crossings: Vec::with_capacity(capacity),
max_motion_sq: 0.0,
overflows: 0,
}
}
pub(crate) fn reserved_bytes(&self) -> u64 {
(self.movers.capacity() * size_of::<Mover>()
+ self.crossings.capacity() * size_of::<(u32, u32)>()) as u64
}
#[cfg(test)]
pub(crate) fn overflows(&self) -> u32 {
self.overflows
}
#[cfg(test)]
pub(crate) fn clear_overflows(&mut self) {
self.overflows = 0;
}
pub(crate) fn begin(&mut self) {
self.movers.clear();
self.crossings.clear();
self.max_motion_sq = 0.0;
}
pub(crate) fn observe(&mut self, slot: u32, body: &Body, start: Vec3, ratio: f32) {
let motion = body.position - start;
self.max_motion_sq = self.max_motion_sq.max(motion.length_squared());
let Some(shape) = body.convex() else {
return;
};
if !gate::is_fast(gate::min_extent(shape), motion, ratio) {
return;
}
if self.movers.len() == self.movers.capacity() {
self.overflows = self.overflows.saturating_add(1);
return;
}
self.movers.push(Mover {
slot,
start,
motion,
driven: body.is_kinematic(),
outcome: None,
});
}
pub(crate) fn resolve(&mut self, scene: Scene<'_>, config: &SimConfig, dt: f32) {
if self.movers.is_empty() {
return;
}
let Ccd {
movers,
crossings,
max_motion_sq,
overflows,
} = self;
let expand = libm::sqrtf(*max_motion_sq);
let standoff = 0.5 * config.speculative_margin;
for index in 0..movers.len() {
let mover = movers[index];
let Some(body) = scene.bodies.get_at(mover.slot as usize) else {
continue;
};
let Some(shape) = body.convex() else {
continue;
};
let probe = Probe {
slot: mover.slot,
shape,
rotation: body.orientation,
start: mover.start,
motion: mover.motion,
mask: body.mask,
expand,
};
let recorded = crossings.len();
let blocked = {
let known = &movers[..];
toi::scan(
scene,
&probe,
|slot, target| motion_of(known, dt, slot, target),
|region| {
if crossings.len() == crossings.capacity() {
*overflows = overflows.saturating_add(1);
return;
}
crossings.push((mover.slot, region));
},
)
};
let outcome = blocked.and_then(|hit| outcome_for(scene.bodies, &mover, &hit, standoff));
if let Some(Outcome::Stop { position, .. }) = outcome {
keep_crossed(scene, &probe, position - mover.start, crossings, recorded);
}
movers[index].outcome = outcome;
}
}
pub(crate) fn report_crossings(&mut self, bodies: &Pool<Body>, sensors: &mut Sensors) {
if self.crossings.is_empty() {
return;
}
self.crossings.sort_unstable();
for &(mover, region) in &self.crossings {
sensors.record_pass_through(bodies, mover, region);
}
}
pub(crate) fn apply(&self, bodies: &mut Pool<Body>) {
for mover in &self.movers {
if let Some(Outcome::Stop { target, position }) = mover.outcome {
if let Some(body) = bodies.get_at_mut(mover.slot as usize) {
body.position = position;
}
wake(bodies, target);
}
}
for mover in &self.movers {
if let Some(Outcome::Shove { target, offset }) = mover.outcome {
if let Some(body) = bodies.get_at_mut(target as usize) {
body.position += offset;
}
wake(bodies, target);
}
}
}
#[cfg(test)]
pub(crate) fn mover_count(&self) -> usize {
self.movers.len()
}
}
fn keep_crossed(
scene: Scene<'_>,
probe: &Probe<'_>,
motion: Vec3,
crossings: &mut Vec<(u32, u32)>,
from: usize,
) {
let mut kept = from;
for at in from..crossings.len() {
let entry = crossings[at];
if toi::still_crossed(scene, probe, motion, entry.1) {
crossings[kept] = entry;
kept += 1;
}
}
crossings.truncate(kept);
}
fn outcome_for(
bodies: &Pool<Body>,
mover: &Mover,
hit: &Blocked,
standoff: f32,
) -> Option<Outcome> {
if !mover.driven {
return Some(Outcome::Stop {
target: hit.target,
position: mover.start + mover.motion * hit.toi + hit.normal * standoff,
});
}
let target = bodies.get_at(hit.target as usize)?;
if !target.is_dynamic() {
return None;
}
let remaining = hit.relative_motion.dot(hit.normal) * (1.0 - hit.toi);
Some(Outcome::Shove {
target: hit.target,
offset: hit.normal * (remaining - standoff),
})
}
fn motion_of(movers: &[Mover], dt: f32, slot: u32, body: &Body) -> Vec3 {
match movers.binary_search_by_key(&slot, |mover| mover.slot) {
Ok(at) => movers[at].motion,
Err(_) => body.linear_velocity * dt,
}
}
fn wake(bodies: &mut Pool<Body>, slot: u32) {
if let Some(body) = bodies.get_at_mut(slot as usize)
&& body.is_dynamic()
{
body.wake();
}
}
pub(crate) fn enabled(config: &SimConfig) -> bool {
config.ccd_enabled && config.ccd_motion_ratio > 0.0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sim::math::vec3;
use crate::{ColliderShape, DynamicParams, LayerMask};
const BULLET: ColliderShape = ColliderShape::Ball { radius: 0.1 };
fn params() -> DynamicParams {
DynamicParams {
mass: 1.0,
friction: 0.5,
restitution: 0.0,
gravity_scale: 1.0,
linear_damping: 0.0,
}
}
fn dynamic_at(position: Vec3) -> Body {
Body::dynamic(
BULLET,
position,
super::super::math::Quat::IDENTITY,
params(),
LayerMask::ALL,
)
}
fn mover(slot: u32, motion: Vec3) -> Mover {
Mover {
slot,
start: Vec3::ZERO,
motion,
driven: false,
outcome: None,
}
}
#[test]
fn the_gate_keeps_slow_bodies_out_of_the_working_set() {
let mut ccd = Ccd::with_capacity(4);
ccd.begin();
let mut body = dynamic_at(vec3(0.0, 0.0, 0.01));
ccd.observe(0, &body, Vec3::ZERO, 0.5);
assert_eq!(ccd.mover_count(), 0, "a hundredth of a unit is not fast");
body.position = vec3(0.0, 0.0, 4.0);
ccd.observe(1, &body, Vec3::ZERO, 0.5);
assert_eq!(ccd.mover_count(), 1);
}
#[test]
fn the_widest_travel_is_taken_from_every_body_and_not_just_the_movers() {
let mut ccd = Ccd::with_capacity(4);
ccd.begin();
let slow = dynamic_at(vec3(0.0, 0.0, 0.02));
ccd.observe(0, &slow, Vec3::ZERO, 0.5);
assert_eq!(ccd.mover_count(), 0);
assert!((libm::sqrtf(ccd.max_motion_sq) - 0.02).abs() < 1.0e-6);
}
#[test]
fn a_full_working_set_declines_and_counts() {
let mut ccd = Ccd::with_capacity(1);
ccd.begin();
let body = dynamic_at(vec3(0.0, 0.0, 4.0));
ccd.observe(0, &body, Vec3::ZERO, 0.5);
ccd.observe(1, &body, Vec3::ZERO, 0.5);
assert_eq!(ccd.mover_count(), 1);
assert_eq!(ccd.overflows(), 1);
ccd.clear_overflows();
assert_eq!(ccd.overflows(), 0);
}
#[test]
fn a_measured_mover_reports_its_own_travel_and_everything_else_its_velocity() {
let movers = [mover(1, vec3(0.0, 0.0, 9.0)), mover(4, Vec3::ZERO)];
let mut body = dynamic_at(Vec3::ZERO);
body.linear_velocity = vec3(6.0, 0.0, 0.0);
assert_eq!(motion_of(&movers, 0.5, 1, &body), vec3(0.0, 0.0, 9.0));
assert_eq!(motion_of(&movers, 0.5, 2, &body), vec3(3.0, 0.0, 0.0));
}
const STANDOFF: f32 = 0.01;
#[test]
fn a_free_mover_stops_short_of_what_it_met() {
let bodies: Pool<Body> = Pool::with_capacity(1);
let mut moving = mover(0, vec3(0.0, 0.0, 10.0));
moving.start = vec3(0.0, 0.0, -5.0);
let hit = Blocked {
target: 3,
toi: 0.25,
normal: vec3(0.0, 0.0, -1.0),
relative_motion: vec3(0.0, 0.0, 10.0),
};
match outcome_for(&bodies, &moving, &hit, STANDOFF) {
Some(Outcome::Stop { target, position }) => {
assert_eq!(target, 3);
assert_eq!(position, vec3(0.0, 0.0, -2.5 - STANDOFF));
}
other => panic!("{other:?}"),
}
}
#[test]
fn a_driven_mover_shoves_what_it_would_have_gone_through() {
let mut bodies: Pool<Body> = Pool::with_capacity(1);
bodies.insert(dynamic_at(Vec3::ZERO)).expect("room");
let mut driven = mover(1, vec3(0.0, 0.0, 8.0));
driven.driven = true;
let hit = Blocked {
target: 0,
toi: 0.5,
normal: vec3(0.0, 0.0, -1.0),
relative_motion: vec3(0.0, 0.0, 8.0),
};
match outcome_for(&bodies, &driven, &hit, STANDOFF) {
Some(Outcome::Shove { target, offset }) => {
assert_eq!(target, 0);
assert_eq!(offset, vec3(0.0, 0.0, 4.0 + STANDOFF));
}
other => panic!("{other:?}"),
}
}
#[test]
fn a_driven_mover_meeting_immovable_geometry_asks_for_nothing() {
let mut bodies: Pool<Body> = Pool::with_capacity(1);
bodies
.insert(Body::fixed(
BULLET,
Vec3::ZERO,
super::super::math::Quat::IDENTITY,
0.5,
LayerMask::ALL,
))
.expect("room");
let mut driven = mover(1, vec3(0.0, 0.0, 8.0));
driven.driven = true;
let hit = Blocked {
target: 0,
toi: 0.5,
normal: vec3(0.0, 0.0, -1.0),
relative_motion: vec3(0.0, 0.0, 8.0),
};
assert!(outcome_for(&bodies, &driven, &hit, STANDOFF).is_none());
}
#[test]
fn the_stage_is_off_when_either_knob_turns_it_off() {
assert!(enabled(&SimConfig::default()));
assert!(!enabled(&SimConfig {
ccd_enabled: false,
..SimConfig::default()
}));
assert!(!enabled(&SimConfig {
ccd_motion_ratio: 0.0,
..SimConfig::default()
}));
}
}