pub mod crowd;
pub use crowd::{Crowd, CrowdMember, CrowdParams};
use bevy_math::Vec2;
const EPSILON: f32 = 1.0e-5;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
pub struct Agent {
pub pos: Vec2,
pub vel: Vec2,
pub radius: f32,
pub avoids: bool,
}
impl Default for Agent {
fn default() -> Self {
Self { pos: Vec2::ZERO, vel: Vec2::ZERO, radius: 0.0, avoids: false }
}
}
#[derive(Debug, Clone, Copy)]
struct Line {
point: Vec2,
direction: Vec2,
}
#[derive(Debug, Default)]
pub struct OrcaScratch {
lines: Vec<Line>,
proj_lines: Vec<Line>,
}
#[inline]
fn det(a: Vec2, b: Vec2) -> f32 {
a.x * b.y - a.y * b.x
}
pub fn new_velocity(
agent: &Agent,
pref_vel: Vec2,
neighbors: &[Agent],
walls: &[(Vec2, f32)],
time_horizon: f32,
dt: f32,
max_speed: f32,
) -> Vec2 {
new_velocity_in(
&mut OrcaScratch::default(),
agent,
pref_vel,
neighbors,
walls,
time_horizon,
dt,
max_speed,
)
}
#[allow(clippy::too_many_arguments)]
pub fn new_velocity_in(
scratch: &mut OrcaScratch,
agent: &Agent,
pref_vel: Vec2,
neighbors: &[Agent],
walls: &[(Vec2, f32)],
time_horizon: f32,
dt: f32,
max_speed: f32,
) -> Vec2 {
if !max_speed.is_finite() || max_speed <= 0.0 {
return Vec2::ZERO;
}
if !pref_vel.is_finite() {
return Vec2::ZERO;
}
if !dt.is_finite() || dt <= 0.0 || !time_horizon.is_finite() || time_horizon <= 0.0 {
return pref_vel.clamp_length_max(max_speed);
}
let inv_time_horizon = 1.0 / time_horizon;
let inv_dt = 1.0 / dt;
let OrcaScratch { lines, proj_lines } = scratch;
lines.clear();
lines.reserve(walls.len() + neighbors.len());
for &(b, approach) in walls {
lines.push(Line { point: approach * b, direction: Vec2::new(-b.y, b.x) });
}
let num_obstacle = lines.len();
for other in neighbors {
let relative_position = other.pos - agent.pos;
let relative_velocity = agent.vel - other.vel;
let dist_sq = relative_position.length_squared();
let combined_radius = agent.radius + other.radius;
let combined_radius_sq = combined_radius * combined_radius;
let u: Vec2;
let direction: Vec2;
if dist_sq > combined_radius_sq {
let w = relative_velocity - inv_time_horizon * relative_position;
let w_length_sq = w.length_squared();
let dot1 = w.dot(relative_position);
if dot1 < 0.0 && dot1 * dot1 > combined_radius_sq * w_length_sq {
let w_length = w_length_sq.sqrt();
let unit_w = if w_length > EPSILON { w / w_length } else { Vec2::ZERO };
direction = Vec2::new(unit_w.y, -unit_w.x);
u = (combined_radius * inv_time_horizon - w_length) * unit_w;
} else {
let leg = (dist_sq - combined_radius_sq).max(0.0).sqrt();
if det(relative_position, w) > 0.0 {
direction = Vec2::new(
relative_position.x * leg - relative_position.y * combined_radius,
relative_position.x * combined_radius + relative_position.y * leg,
) / dist_sq;
} else {
direction = -Vec2::new(
relative_position.x * leg + relative_position.y * combined_radius,
-relative_position.x * combined_radius + relative_position.y * leg,
) / dist_sq;
}
let dot2 = relative_velocity.dot(direction);
u = dot2 * direction - relative_velocity;
}
} else {
let w = relative_velocity - inv_dt * relative_position;
let w_length = w.length();
let unit_w = if w_length > EPSILON { w / w_length } else { Vec2::ZERO };
direction = Vec2::new(unit_w.y, -unit_w.x);
u = (combined_radius * inv_dt - w_length) * unit_w;
}
let responsibility = if other.avoids { 0.5 } else { 1.0 };
lines.push(Line { point: agent.vel + responsibility * u, direction });
}
let mut result = pref_vel;
let fail = linear_program2(lines, max_speed, pref_vel, false, &mut result);
if fail < lines.len() {
linear_program3(lines, num_obstacle, fail, max_speed, proj_lines, &mut result);
}
result
}
fn linear_program1(
lines: &[Line],
line_no: usize,
radius: f32,
opt_velocity: Vec2,
direction_opt: bool,
result: &mut Vec2,
) -> bool {
let line = lines[line_no];
let dot_product = line.point.dot(line.direction);
let discriminant = dot_product * dot_product + radius * radius - line.point.length_squared();
if discriminant < 0.0 {
return false;
}
let sqrt_discriminant = discriminant.sqrt();
let mut t_left = -dot_product - sqrt_discriminant;
let mut t_right = -dot_product + sqrt_discriminant;
for other_line in lines.iter().take(line_no) {
let denominator = det(line.direction, other_line.direction);
let numerator = det(other_line.direction, line.point - other_line.point);
if denominator.abs() <= EPSILON {
if numerator < 0.0 {
return false;
}
continue;
}
let t = numerator / denominator;
if denominator >= 0.0 {
t_right = t_right.min(t);
} else {
t_left = t_left.max(t);
}
if t_left > t_right {
return false;
}
}
if direction_opt {
if opt_velocity.dot(line.direction) > 0.0 {
*result = line.point + t_right * line.direction;
} else {
*result = line.point + t_left * line.direction;
}
} else {
let t = line.direction.dot(opt_velocity - line.point);
if t < t_left {
*result = line.point + t_left * line.direction;
} else if t > t_right {
*result = line.point + t_right * line.direction;
} else {
*result = line.point + t * line.direction;
}
}
true
}
fn linear_program2(
lines: &[Line],
radius: f32,
opt_velocity: Vec2,
direction_opt: bool,
result: &mut Vec2,
) -> usize {
if direction_opt {
*result = opt_velocity * radius;
} else if opt_velocity.length_squared() > radius * radius {
*result = opt_velocity.normalize_or_zero() * radius;
} else {
*result = opt_velocity;
}
for i in 0..lines.len() {
if det(lines[i].direction, lines[i].point - *result) > 0.0 {
let temp = *result;
if !linear_program1(lines, i, radius, opt_velocity, direction_opt, result) {
*result = temp;
return i;
}
}
}
lines.len()
}
fn linear_program3(
lines: &[Line],
num_obstacle: usize,
begin_line: usize,
radius: f32,
proj_lines: &mut Vec<Line>,
result: &mut Vec2,
) {
let mut distance = 0.0_f32;
for i in begin_line.max(num_obstacle)..lines.len() {
if det(lines[i].direction, lines[i].point - *result) > distance {
proj_lines.clear();
proj_lines.extend_from_slice(&lines[0..num_obstacle]);
for j in num_obstacle..i {
let determinant = det(lines[i].direction, lines[j].direction);
let point = if determinant.abs() <= EPSILON {
if lines[i].direction.dot(lines[j].direction) > 0.0 {
continue;
}
0.5 * (lines[i].point + lines[j].point)
} else {
lines[i].point
+ (det(lines[j].direction, lines[i].point - lines[j].point) / determinant)
* lines[i].direction
};
let direction = (lines[j].direction - lines[i].direction).normalize_or_zero();
proj_lines.push(Line { point, direction });
}
let temp = *result;
let opt = Vec2::new(-lines[i].direction.y, lines[i].direction.x);
if linear_program2(proj_lines.as_slice(), radius, opt, true, result) < proj_lines.len() {
*result = temp;
}
distance = det(lines[i].direction, lines[i].point - *result);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn agent(pos: Vec2, vel: Vec2) -> Agent {
Agent { pos, vel, radius: 0.5, avoids: true }
}
fn hash_f32(x: u32) -> f32 {
let mut h = x.wrapping_mul(747_796_405).wrapping_add(2_891_336_453);
h = ((h >> ((h >> 28).wrapping_add(4))) ^ h).wrapping_mul(277_803_737);
h = (h >> 22) ^ h;
(h as f32) / (u32::MAX as f32)
}
#[test]
fn free_agent_keeps_its_preferred_velocity() {
let a = agent(Vec2::ZERO, Vec2::ZERO);
let pref = Vec2::new(1.0, 0.0);
let v = new_velocity(&a, pref, &[], &[], 2.0, 1.0 / 60.0, 2.0);
assert!((v - pref).length() < 1.0e-4, "expected {pref}, got {v}");
}
#[test]
fn output_is_clamped_to_max_speed() {
let a = agent(Vec2::ZERO, Vec2::ZERO);
let max_speed = 1.5;
let v = new_velocity(&a, Vec2::new(10.0, 0.0), &[], &[], 2.0, 1.0 / 60.0, max_speed);
assert!(v.length() <= max_speed + 1.0e-4, "speed {} exceeds max {max_speed}", v.length());
assert!((v.length() - max_speed).abs() < 1.0e-3, "should saturate the disc, got {}", v.length());
}
#[test]
fn head_on_pair_deflects_instead_of_colliding() {
let a = agent(Vec2::new(-1.0, 0.0), Vec2::new(1.0, 0.0));
let b = agent(Vec2::new(1.0, 0.0), Vec2::new(-1.0, 0.0));
let pref = Vec2::new(1.0, 0.0);
let max_speed = 1.5;
let v = new_velocity(&a, pref, &[b], &[], 2.0, 1.0 / 60.0, max_speed);
assert!(v.length() <= max_speed + 1.0e-4);
assert!((v - pref).length() > 1.0e-3, "expected avoidance, but velocity stayed at pref {pref}");
}
#[test]
fn speed_bound_holds_across_many_configurations() {
let max_speed = 2.0;
for i in 0..40u32 {
let hx = hash_f32(i.wrapping_mul(0x9E37_79B1)) * 4.0 - 2.0;
let hy = hash_f32(i.wrapping_mul(0x85EB_CA6B).wrapping_add(1)) * 4.0 - 2.0;
let a = agent(Vec2::ZERO, Vec2::new(1.0, 0.0));
let b = agent(Vec2::new(hx, hy), Vec2::new(-0.5, 0.2));
let v = new_velocity(&a, Vec2::new(1.5, 0.5), &[b], &[], 2.0, 1.0 / 60.0, max_speed);
assert!(v.is_finite(), "non-finite velocity for config {i}: {v}");
assert!(v.length() <= max_speed + 1.0e-4, "config {i}: speed {} > max {max_speed}", v.length());
}
}
#[test]
fn deterministic_for_identical_input() {
let a = agent(Vec2::new(-1.0, 0.0), Vec2::new(1.0, 0.0));
let b = agent(Vec2::new(1.0, 0.3), Vec2::new(-1.0, 0.0));
let call = || new_velocity(&a, Vec2::new(1.0, 0.0), &[b], &[], 2.0, 1.0 / 60.0, 1.5);
assert_eq!(call(), call());
}
#[test]
fn degenerate_dt_still_returns_a_finite_velocity() {
let a = agent(Vec2::new(-1.0, 0.0), Vec2::new(1.0, 0.0));
let b = agent(Vec2::new(1.0, 0.0), Vec2::new(-1.0, 0.0));
let pref = Vec2::new(1.0, 0.5);
let max_speed = 1.5;
for dt in [0.0_f32, -1.0, f32::NAN, f32::INFINITY] {
let v = new_velocity(&a, pref, &[b], &[], 2.0, dt, max_speed);
assert!(v.is_finite(), "dt {dt} gave a non-finite velocity {v}");
assert!(v.length() <= max_speed + 1.0e-4, "dt {dt}: speed {} > max {max_speed}", v.length());
}
}
#[test]
fn degenerate_time_horizon_still_returns_a_finite_velocity() {
let a = agent(Vec2::new(-1.0, 0.0), Vec2::new(1.0, 0.0));
let b = agent(Vec2::new(1.0, 0.0), Vec2::new(-1.0, 0.0));
let pref = Vec2::new(1.0, 0.5);
let max_speed = 1.5;
for h in [0.0_f32, -1.0, f32::NAN, f32::INFINITY] {
let v = new_velocity(&a, pref, &[b], &[], h, 1.0 / 60.0, max_speed);
assert!(v.is_finite(), "time_horizon {h} gave a non-finite velocity {v}");
assert!(v.length() <= max_speed + 1.0e-4, "h {h}: speed {} > max {max_speed}", v.length());
}
}
#[test]
fn degenerate_scalars_return_zero() {
let a = agent(Vec2::ZERO, Vec2::ZERO);
for max_speed in [0.0_f32, -1.0, f32::NAN, f32::INFINITY] {
let v = new_velocity(&a, Vec2::new(1.0, 0.0), &[], &[], 2.0, 1.0 / 60.0, max_speed);
assert_eq!(v, Vec2::ZERO, "max_speed {max_speed} should give the zero velocity");
}
for pref in [Vec2::new(f32::NAN, 0.0), Vec2::new(0.0, f32::INFINITY), Vec2::NAN] {
let v = new_velocity(&a, pref, &[], &[], 2.0, 1.0 / 60.0, 2.0);
assert_eq!(v, Vec2::ZERO, "pref {pref} should give the zero velocity");
}
}
#[test]
fn scratch_matches_allocating_path_bit_for_bit() {
let max_speed = 2.0;
let mut scratch = OrcaScratch::default();
let walls = [(Vec2::new(1.0, 0.0), 0.4_f32)];
for i in 0..40u32 {
let hx = hash_f32(i.wrapping_mul(0x9E37_79B1)) * 4.0 - 2.0;
let hy = hash_f32(i.wrapping_mul(0x85EB_CA6B).wrapping_add(1)) * 4.0 - 2.0;
let a = agent(Vec2::ZERO, Vec2::new(1.0, 0.0));
let b = agent(Vec2::new(hx, hy), Vec2::new(-0.5, 0.2));
let pref = Vec2::new(1.5, 0.5);
let plain = new_velocity(&a, pref, &[b], &walls, 2.0, 1.0 / 60.0, max_speed);
let reused = new_velocity_in(&mut scratch, &a, pref, &[b], &walls, 2.0, 1.0 / 60.0, max_speed);
assert_eq!(plain.x.to_bits(), reused.x.to_bits(), "config {i}: x differs");
assert_eq!(plain.y.to_bits(), reused.y.to_bits(), "config {i}: y differs");
}
}
}