#![doc = include_str!("../README.md")]
mod common;
mod linear_programming;
mod obstacles;
mod simulator;
mod visibility_set;
pub use glam::Vec2;
use common::*;
use linear_programming::{solve_linear_program, Line};
use obstacles::get_lines_for_agent_to_obstacle;
pub use obstacles::Obstacle;
pub use simulator::{AgentParameters, Simulator, SimulatorMargin};
pub use visibility_set::VisibilitySet;
#[derive(Clone, PartialEq, Debug)]
pub struct Agent {
pub position: Vec2,
pub velocity: Vec2,
pub radius: f32,
pub max_velocity: f32,
pub avoidance_responsibility: f32,
}
#[derive(Clone, PartialEq, Debug)]
pub struct AvoidanceOptions {
pub obstacle_margin: f32,
pub time_horizon: f32,
pub obstacle_time_horizon: f32,
}
impl Agent {
pub fn compute_avoiding_velocity(
&self,
neighbours: &[&Agent],
obstacles: &[&Obstacle],
preferred_velocity: Vec2,
time_step: f32,
avoidance_options: &AvoidanceOptions,
) -> Vec2 {
assert!(time_step > 0.0, "time_step must be positive, was {}", time_step);
let lines = obstacles
.iter()
.flat_map(|o| {
get_lines_for_agent_to_obstacle(
self,
o,
avoidance_options.obstacle_margin,
avoidance_options.obstacle_time_horizon,
)
})
.chain(neighbours.iter().map(|neighbour| {
self.get_line_for_neighbour(
neighbour,
avoidance_options.time_horizon,
time_step,
)
}))
.collect::<Vec<Line>>();
let obstacle_line_count = lines.len() - neighbours.len();
solve_linear_program(
&lines,
obstacle_line_count,
self.max_velocity,
preferred_velocity,
)
}
fn get_line_for_neighbour(
&self,
neighbour: &Agent,
time_horizon: f32,
time_step: f32,
) -> Line {
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_circle_center = relative_neighbour_position / time_horizon;
let cutoff_circle_center_to_relative_velocity =
relative_agent_velocity - cutoff_circle_center;
let cutoff_circle_center_to_relative_velocity_length_squared =
cutoff_circle_center_to_relative_velocity.length_squared();
let dot = cutoff_circle_center_to_relative_velocity
.dot(relative_neighbour_position);
if dot < 0.0
&& dot * dot
> sum_radius_squared
* cutoff_circle_center_to_relative_velocity_length_squared
{
let cutoff_circle_radius = sum_radius / time_horizon;
vo_normal =
cutoff_circle_center_to_relative_velocity.normalize_or_zero();
relative_velocity_projected_to_vo =
vo_normal * cutoff_circle_radius + cutoff_circle_center;
inside_vo = cutoff_circle_center_to_relative_velocity_length_squared
< cutoff_circle_radius * cutoff_circle_radius;
} else {
let tangent_triangle_leg =
(distance_squared - sum_radius_squared).sqrt();
let tangent_side = determinant(
relative_neighbour_position,
cutoff_circle_center_to_relative_velocity,
)
.signum();
let shadow_direction =
relative_neighbour_position * tangent_triangle_leg * tangent_side
+ relative_neighbour_position.perp() * sum_radius;
let shadow_direction = shadow_direction / distance_squared;
vo_normal = shadow_direction.perp();
relative_velocity_projected_to_vo =
relative_agent_velocity.project_onto_normalized(shadow_direction);
inside_vo = determinant(relative_agent_velocity, shadow_direction)
* tangent_side
>= 0.0;
}
} else {
let cutoff_circle_center = relative_neighbour_position / time_step;
let cutoff_circle_radius = sum_radius / time_step;
vo_normal =
(relative_agent_velocity - cutoff_circle_center).normalize_or_zero();
relative_velocity_projected_to_vo =
vo_normal * cutoff_circle_radius + cutoff_circle_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
};
Line {
point: self.velocity + u * responsibility,
direction: -vo_normal.perp(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
mod get_line_for_neighbour_tests {
use glam::Vec2;
use super::{Agent, Line};
macro_rules! assert_line_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.direction.distance_squared(b.direction) < 1e-5,
"\n left: {:?}\n right: {:?}",
a,
b
);
}};
}
#[test]
fn velocity_projects_on_cutoff_circle() {
let position = Vec2::new(1.0, 2.0);
let radius = 2.0;
let agent = Agent {
position: Vec2::ZERO,
velocity: Vec2::ZERO,
radius: radius - 1.0,
avoidance_responsibility: 1.0,
max_velocity: 0.0,
};
let neighbour = Agent {
position: position,
velocity: Vec2::ZERO,
radius: 1.0,
avoidance_responsibility: 1.0,
max_velocity: 0.0,
};
let actual_line = agent.get_line_for_neighbour(
&neighbour, 1.0, 1.0,
);
assert_line_eq!(
actual_line,
Line {
point: position.normalize() * (position.length() - radius),
direction: position.perp().normalize(),
}
);
}
#[test]
fn velocity_projects_to_shadow() {
let mut agent = Agent {
position: Vec2::ZERO,
velocity: Vec2::new(-1.0, 3.0),
radius: 1.0,
max_velocity: 0.0,
avoidance_responsibility: 1.0,
};
let neighbour = Agent {
position: Vec2::new(2.0, 2.0),
velocity: Vec2::ZERO,
radius: 1.0,
max_velocity: 0.0,
avoidance_responsibility: 1.0,
};
let inside_shadow_line = agent.get_line_for_neighbour(
&neighbour, 1.0, 1.0,
);
assert_line_eq!(
inside_shadow_line,
Line { point: Vec2::new(0.0, 3.0), direction: Vec2::new(0.0, 1.0) }
);
agent.velocity = Vec2::new(10.0, -1.0);
let outside_shadow_line = agent.get_line_for_neighbour(
&neighbour, 1.0, 1.0,
);
assert_line_eq!(
outside_shadow_line,
Line { point: Vec2::new(10.0, -0.5), direction: Vec2::new(-1.0, 0.0) }
);
}
#[test]
fn collision_uses_time_step() {
let agent = Agent {
position: Vec2::ZERO,
velocity: Vec2::new(0.0, 0.0),
radius: 2.0,
max_velocity: 0.0,
avoidance_responsibility: 1.0,
};
let neighbour = Agent {
position: Vec2::new(2.0, 2.0),
velocity: Vec2::ZERO,
radius: 2.0,
max_velocity: 0.0,
avoidance_responsibility: 1.0,
};
let collision_line = agent.get_line_for_neighbour(
&neighbour, 1.0, 0.5,
);
assert_line_eq!(
collision_line,
Line {
point: (Vec2::ONE.normalize() * -8.0 + Vec2::new(4.0, 4.0)) * 0.5,
direction: Vec2::new(-1.0, 1.0).normalize(),
}
);
}
#[test]
fn no_collision_uses_time_horizon() {
let agent = Agent {
position: Vec2::ZERO,
velocity: Vec2::new(0.0, 0.0),
radius: 1.0,
max_velocity: 0.0,
avoidance_responsibility: 1.0,
};
let neighbour = Agent {
position: Vec2::new(2.0, 2.0),
velocity: Vec2::ZERO,
radius: 1.0,
max_velocity: 0.0,
avoidance_responsibility: 1.0,
};
let collision_line = agent.get_line_for_neighbour(
&neighbour, 2.0, 0.5,
);
assert_line_eq!(
collision_line,
Line {
point: -Vec2::ONE.normalize() + Vec2::new(1.0, 1.0),
direction: Vec2::new(-1.0, 1.0).normalize(),
}
);
}
#[test]
fn uses_avoidance_responsibility() {
let agent = Agent {
position: Vec2::ZERO,
velocity: Vec2::new(1.5, 0.0),
radius: 1.0,
avoidance_responsibility: 1.0,
max_velocity: 0.0,
};
let neighbour = Agent {
position: Vec2::new(4.0, 0.0),
velocity: Vec2::ZERO,
radius: 1.0,
avoidance_responsibility: 3.0,
max_velocity: 0.0,
};
let actual_line = agent.get_line_for_neighbour(
&neighbour, 2.0, 0.5,
);
assert_line_eq!(
actual_line,
Line { point: Vec2::new(1.375, 0.0), direction: Vec2::new(0.0, 1.0) }
);
}
#[test]
fn uses_avoidance_responsibility_only_when_inside_vo() {
let agent = Agent {
position: Vec2::ZERO,
velocity: Vec2::new(0.5, 0.0),
radius: 1.0,
avoidance_responsibility: 1.0,
max_velocity: 0.0,
};
let neighbour = Agent {
position: Vec2::new(4.0, 0.0),
velocity: Vec2::ZERO,
radius: 1.0,
avoidance_responsibility: 3.0,
max_velocity: 0.0,
};
let actual_line = agent.get_line_for_neighbour(
&neighbour, 2.0, 0.5,
);
assert_line_eq!(
actual_line,
Line { point: Vec2::new(1.0, 0.0), direction: Vec2::new(0.0, 1.0) }
);
}
}
}