#![doc = include_str!("../README.md")]
mod linear_programming;
mod simulator;
use std::borrow::Cow;
use crate::linear_programming::{solve_linear_program, Plane};
pub use glam::Vec3;
pub use simulator::{AgentParameters, Simulator, SimulatorMargin};
#[derive(Clone, PartialEq, Debug)]
pub struct Agent {
pub position: Vec3,
pub velocity: Vec3,
pub radius: f32,
pub avoidance_responsibility: f32,
}
#[derive(Clone, PartialEq, Debug)]
pub struct AvoidanceOptions {
pub time_horizon: f32,
}
impl Agent {
pub fn compute_avoiding_velocity(
&self,
neighbours: &[Cow<'_, Agent>],
preferred_velocity: Vec3,
max_speed: f32,
time_step: f32,
avoidance_options: &AvoidanceOptions,
) -> Vec3 {
assert!(time_step > 0.0, "time_step must be positive, was {}", time_step);
let planes = neighbours
.iter()
.map(|neighbour| {
self.get_plane_for_neighbour(
neighbour,
avoidance_options.time_horizon,
time_step,
)
})
.collect::<Vec<Plane>>();
solve_linear_program(&planes, max_speed, preferred_velocity)
}
fn get_plane_for_neighbour(
&self,
neighbour: &Agent,
time_horizon: f32,
time_step: f32,
) -> Plane {
let relative_neighbour_position = neighbour.position - self.position;
let relative_agent_velocity = self.velocity - neighbour.velocity;
let distance_squared = relative_neighbour_position.length_squared();
let sum_radius = self.radius + neighbour.radius;
let sum_radius_squared = sum_radius * sum_radius;
let vo_normal;
let relative_velocity_projected_to_vo;
let inside_vo;
if distance_squared > sum_radius_squared {
let cutoff_sphere_center = relative_neighbour_position / time_horizon;
let cutoff_sphere_center_to_relative_velocity =
relative_agent_velocity - cutoff_sphere_center;
let cutoff_sphere_center_to_relative_velocity_length_squared =
cutoff_sphere_center_to_relative_velocity.length_squared();
let dot = cutoff_sphere_center_to_relative_velocity
.dot(relative_neighbour_position);
if dot < 0.0
&& dot * dot
> sum_radius_squared
* cutoff_sphere_center_to_relative_velocity_length_squared
{
let cutoff_sphere_radius = sum_radius / time_horizon;
vo_normal =
cutoff_sphere_center_to_relative_velocity.normalize_or_zero();
relative_velocity_projected_to_vo =
vo_normal * cutoff_sphere_radius + cutoff_sphere_center;
inside_vo = cutoff_sphere_center_to_relative_velocity_length_squared
< cutoff_sphere_radius * cutoff_sphere_radius;
} else {
let tangent_ring_triangle_leg_squared =
distance_squared - sum_radius_squared;
let squared_distance_between_rays = relative_neighbour_position
.cross(relative_agent_velocity)
.length_squared();
let a = relative_neighbour_position.length_squared();
let b = relative_neighbour_position.dot(relative_agent_velocity);
let c = relative_agent_velocity.length_squared()
- squared_distance_between_rays / tangent_ring_triangle_leg_squared;
let t = (-b - (b * b - a * c).sqrt()) / a;
vo_normal = (relative_agent_velocity + t * relative_neighbour_position)
.normalize_or_zero();
let distance_to_plane = Plane { normal: vo_normal, point: Vec3::ZERO }
.signed_distance_to_plane(relative_agent_velocity);
inside_vo = distance_to_plane < 0.0;
relative_velocity_projected_to_vo =
relative_agent_velocity - distance_to_plane * vo_normal;
}
} else {
let cutoff_sphere_center = relative_neighbour_position / time_step;
let cutoff_sphere_radius = sum_radius / time_step;
vo_normal =
(relative_agent_velocity - cutoff_sphere_center).normalize_or_zero();
relative_velocity_projected_to_vo =
vo_normal * cutoff_sphere_radius + cutoff_sphere_center;
inside_vo = true;
}
let u = relative_velocity_projected_to_vo - relative_agent_velocity;
let responsibility = if inside_vo {
self.avoidance_responsibility
/ (self.avoidance_responsibility + neighbour.avoidance_responsibility)
} else {
1.0
};
Plane { point: self.velocity + u * responsibility, normal: vo_normal }
}
}
#[cfg(test)]
mod tests {
use super::*;
mod get_plane_for_neighbour_tests {
use glam::Vec3;
use crate::{tests::Plane, Agent};
macro_rules! assert_plane_eq {
($a: expr, $b: expr) => {{
let a = $a;
let b = $b;
assert!(
a.point.distance_squared(b.point) < 1e-5,
"\n left: {:?}\n right: {:?}",
a,
b
);
assert!(
a.normal.distance_squared(b.normal) < 1e-5,
"\n left: {:?}\n right: {:?}",
a,
b
);
}};
}
#[test]
fn velocity_projects_on_cutoff_sphere() {
let position = Vec3::new(1.0, 2.0, 0.0);
let radius = 2.0;
let agent = Agent {
position: Vec3::ZERO,
velocity: Vec3::ZERO,
radius: radius - 1.0,
avoidance_responsibility: 1.0,
};
let neighbour = Agent {
position: position,
velocity: Vec3::ZERO,
radius: 1.0,
avoidance_responsibility: 1.0,
};
let actual_plane = agent.get_plane_for_neighbour(
&neighbour, 1.0, 1.0,
);
assert_plane_eq!(
actual_plane,
Plane {
point: position.normalize() * (position.length() - radius),
normal: -position.normalize(),
}
);
}
#[test]
fn velocity_projects_to_shadow() {
let mut agent = Agent {
position: Vec3::ZERO,
velocity: Vec3::new(1.0, 3.0, 0.0),
radius: 1.0,
avoidance_responsibility: 1.0,
};
let neighbour = Agent {
position: Vec3::new(2.0, 2.0, 0.0),
velocity: Vec3::ZERO,
radius: 1.0,
avoidance_responsibility: 1.0,
};
let inside_shadow_plane = agent.get_plane_for_neighbour(
&neighbour, 1.0, 1.0,
);
assert_plane_eq!(
inside_shadow_plane,
Plane {
point: Vec3::new(0.5, 3.0, 0.0),
normal: Vec3::new(-1.0, 0.0, 0.0),
}
);
agent.velocity = Vec3::new(10.0, -1.0, 0.0);
let outside_shadow_plane = agent.get_plane_for_neighbour(
&neighbour, 1.0, 1.0,
);
assert_plane_eq!(
outside_shadow_plane,
Plane {
point: Vec3::new(10.0, 0.0, 0.0),
normal: Vec3::new(0.0, -1.0, 0.0),
}
);
}
#[test]
fn collision_uses_time_step() {
let agent = Agent {
position: Vec3::ZERO,
velocity: Vec3::ZERO,
radius: 2.0,
avoidance_responsibility: 1.0,
};
let neighbour = Agent {
position: Vec3::new(2.0, 2.0, 0.0),
velocity: Vec3::ZERO,
radius: 2.0,
avoidance_responsibility: 1.0,
};
let collision_plane = agent.get_plane_for_neighbour(
&neighbour, 1.0, 0.5,
);
assert_plane_eq!(
collision_plane,
Plane {
point: (Vec3::new(1.0, 1.0, 0.0).normalize() * -8.0
+ Vec3::new(4.0, 4.0, 0.0))
* 0.5,
normal: Vec3::new(-1.0, -1.0, 0.0).normalize(),
}
);
}
#[test]
fn no_collision_uses_time_horizon() {
let agent = Agent {
position: Vec3::ZERO,
velocity: Vec3::ZERO,
radius: 1.0,
avoidance_responsibility: 1.0,
};
let neighbour = Agent {
position: Vec3::new(2.0, 2.0, 0.0),
velocity: Vec3::ZERO,
radius: 1.0,
avoidance_responsibility: 1.0,
};
let collision_plane = agent.get_plane_for_neighbour(
&neighbour, 2.0, 0.5,
);
assert_plane_eq!(
collision_plane,
Plane {
point: -Vec3::new(1.0, 1.0, 0.0).normalize()
+ Vec3::new(1.0, 1.0, 0.0),
normal: Vec3::new(-1.0, -1.0, 0.0).normalize(),
}
);
}
#[test]
fn uses_avoidance_responsibility() {
let agent = Agent {
position: Vec3::ZERO,
velocity: Vec3::new(1.5, 0.0, 0.0),
radius: 1.0,
avoidance_responsibility: 1.0,
};
let neighbour = Agent {
position: Vec3::new(4.0, 0.0, 0.0),
velocity: Vec3::ZERO,
radius: 1.0,
avoidance_responsibility: 3.0,
};
let actual_plane = agent.get_plane_for_neighbour(
&neighbour, 2.0, 0.5,
);
assert_plane_eq!(
actual_plane,
Plane {
point: Vec3::new(1.375, 0.0, 0.0),
normal: Vec3::new(-1.0, 0.0, 0.0),
}
);
}
#[test]
fn uses_avoidance_responsibility_only_when_inside_vo() {
let agent = Agent {
position: Vec3::ZERO,
velocity: Vec3::new(0.5, 0.0, 0.0),
radius: 1.0,
avoidance_responsibility: 1.0,
};
let neighbour = Agent {
position: Vec3::new(4.0, 0.0, 0.0),
velocity: Vec3::ZERO,
radius: 1.0,
avoidance_responsibility: 3.0,
};
let actual_plane = agent.get_plane_for_neighbour(
&neighbour, 2.0, 0.5,
);
assert_plane_eq!(
actual_plane,
Plane {
point: Vec3::new(1.0, 0.0, 0.0),
normal: Vec3::new(-1.0, 0.0, 0.0),
}
);
}
}
}