use crate::determinant;
use glam::Vec2;
#[derive(Clone, Debug)]
pub struct Line {
pub point: Vec2,
pub direction: Vec2,
}
pub fn solve_linear_program(
constraints: &[Line],
rigid_constraint_count: usize,
radius: f32,
preferred_value: Vec2,
) -> Vec2 {
match solve_linear_program_2d(
constraints,
radius,
&OptimalValue::Point(preferred_value),
) {
LinearProgram2DResult::Feasible(optimal_value) => optimal_value,
LinearProgram2DResult::Infeasible {
index_of_failed_line,
partial_value,
} => solve_linear_program_3d(
constraints,
rigid_constraint_count,
radius,
index_of_failed_line,
partial_value,
),
}
}
const RVO_EPSILON: f32 = 0.00001;
enum OptimalValue {
Point(Vec2),
Direction(Vec2),
}
fn solve_linear_program_along_line(
line: &Line,
radius: f32,
constraints: &[Line],
optimal_value: &OptimalValue,
) -> Result<Vec2, ()> {
let line_dot_product = line.point.dot(line.direction);
let discriminant = line_dot_product * line_dot_product + radius * radius
- line.point.dot(line.point);
if discriminant < 0.0 {
return Err(());
}
let discriminant = discriminant.sqrt();
let mut t_left = -line_dot_product - discriminant;
let mut t_right = -line_dot_product + discriminant;
for constraint in constraints {
let direction_determinant =
determinant(line.direction, constraint.direction);
let numerator =
determinant(constraint.direction, line.point - constraint.point);
if direction_determinant.abs() <= RVO_EPSILON {
if numerator < 0.0 {
return Err(());
}
continue;
}
let t = numerator / direction_determinant;
if direction_determinant >= 0.0 {
t_right = t_right.min(t);
} else {
t_left = t_left.max(t);
}
if t_left > t_right {
return Err(());
}
}
let t = match optimal_value {
&OptimalValue::Direction(direction) => {
if direction.dot(line.direction) > 0.0 {
t_right
} else {
t_left
}
}
&OptimalValue::Point(point) => {
let t = line.direction.dot(point - line.point);
t.clamp(t_left, t_right)
}
};
Ok(line.point + t * line.direction)
}
#[derive(PartialEq, Debug)]
enum LinearProgram2DResult {
Feasible(Vec2),
Infeasible {
index_of_failed_line: usize,
partial_value: Vec2,
},
}
fn solve_linear_program_2d(
constraints: &[Line],
radius: f32,
optimal_value: &OptimalValue,
) -> LinearProgram2DResult {
let mut best_value = match optimal_value {
&OptimalValue::Direction(direction) => direction * radius,
&OptimalValue::Point(point) if point.length_squared() > radius * radius => {
point.normalize() * radius
}
&OptimalValue::Point(point) => point,
};
for (index, constraint) in constraints.iter().enumerate() {
if determinant(constraint.direction, best_value - constraint.point) > 0.0 {
continue;
}
match solve_linear_program_along_line(
constraint,
radius,
&constraints[0..index],
optimal_value,
) {
Ok(new_value) => best_value = new_value,
Err(()) => {
return LinearProgram2DResult::Infeasible {
index_of_failed_line: index,
partial_value: best_value,
}
}
}
}
LinearProgram2DResult::Feasible(best_value)
}
fn solve_linear_program_3d(
constraints: &[Line],
rigid_constraint_count: usize,
radius: f32,
index_of_failed_line: usize,
partial_value: Vec2,
) -> Vec2 {
debug_assert!(rigid_constraint_count <= index_of_failed_line);
let mut penetration = 0.0;
let mut best_value = partial_value;
for (index, constraint) in
constraints[index_of_failed_line..].iter().enumerate()
{
if determinant(constraint.direction, constraint.point - best_value)
<= penetration
{
continue;
}
let index = index + index_of_failed_line;
let mut penetration_constraints = Vec::with_capacity(index);
penetration_constraints
.extend_from_slice(&constraints[0..rigid_constraint_count]);
for previous_constraint in &constraints[rigid_constraint_count..index] {
let intersection_determinant =
determinant(constraint.direction, previous_constraint.direction);
let intersection_point;
if intersection_determinant.abs() <= RVO_EPSILON {
if constraint.direction.dot(previous_constraint.direction) > 0.0 {
continue;
}
intersection_point =
(constraint.point + previous_constraint.point) * 0.5;
} else {
let intersection_t = determinant(
previous_constraint.direction,
constraint.point - previous_constraint.point,
) / intersection_determinant;
intersection_point =
constraint.point + intersection_t * constraint.direction;
};
penetration_constraints.push(Line {
direction: (previous_constraint.direction - constraint.direction)
.normalize(),
point: intersection_point,
});
}
if let LinearProgram2DResult::Feasible(result) = solve_linear_program_2d(
&penetration_constraints,
radius,
&OptimalValue::Direction(constraint.direction.perp()),
) {
best_value = result;
penetration =
determinant(constraint.direction, constraint.point - best_value);
}
}
best_value
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_vec2_near {
($a: expr, $b: expr) => {{
let a = $a;
let b = $b;
assert!(
(a - b).length_squared() < super::RVO_EPSILON,
"\n left: {}\n right: {}",
a,
b
);
}};
}
mod solve_linear_program_along_line_tests {
use glam::Vec2;
use super::{solve_linear_program_along_line, Line, OptimalValue};
#[test]
fn projects_optimal_point_with_no_constraints() {
let circle_height_at_half = (1.0f32 - 0.5 * 0.5).sqrt();
let valid_line =
Line { direction: Vec2::new(0.0, 1.0), point: Vec2::new(0.5, 0.0) };
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Point(Vec2::new(5.0, 0.25)),
),
Ok(Vec2::new(0.5, 0.25))
);
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Point(Vec2::new(5.0, 2.0)),
),
Ok(Vec2::new(0.5, circle_height_at_half))
);
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Point(Vec2::new(5.0, -100.0)),
),
Ok(Vec2::new(0.5, -circle_height_at_half))
);
}
#[test]
fn projects_optimal_direction_with_no_constraints() {
let circle_height_at_half = (1.0f32 - 0.5 * 0.5).sqrt();
let valid_line =
Line { direction: Vec2::new(0.0, 1.0), point: Vec2::new(0.5, 0.0) };
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Direction(Vec2::new(1.0, 0.5).normalize()),
),
Ok(Vec2::new(0.5, circle_height_at_half))
);
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Direction(Vec2::new(1.0, -0.5).normalize()),
),
Ok(Vec2::new(0.5, -circle_height_at_half))
);
}
#[test]
fn constraints_remove_valid_values() {
let valid_line =
Line { direction: Vec2::new(0.0, 1.0), point: Vec2::new(0.5, 0.0) };
let constraints = [
Line { direction: Vec2::new(-1.0, 0.0), point: Vec2::new(-100.0, 0.5) },
Line {
direction: Vec2::new(1.0, 1.0).normalize(),
point: Vec2::new(0.25, -1.0),
},
];
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
&constraints,
&OptimalValue::Point(Vec2::new(-5.0, 0.25)),
),
Ok(Vec2::new(0.5, 0.25))
);
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
&constraints,
&OptimalValue::Point(Vec2::new(-5.0, 1.0)),
),
Ok(Vec2::new(0.5, 0.5))
);
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
&constraints,
&OptimalValue::Point(Vec2::new(-5.0, -1.0)),
),
Ok(Vec2::new(0.5, -0.75))
);
}
#[test]
fn constraints_are_infeasible() {
let valid_line =
Line { direction: Vec2::new(0.0, 1.0), point: Vec2::new(0.5, 0.0) };
let constraints = [
Line { direction: Vec2::new(1.0, 0.0), point: Vec2::new(-100.0, 0.5) },
Line {
direction: Vec2::new(-1.0, 0.0),
point: Vec2::new(-100.0, -0.5),
},
];
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
&constraints,
&OptimalValue::Point(Vec2::ZERO),
),
Err(())
);
}
#[test]
fn valid_line_outside_circle() {
let valid_line =
Line { direction: Vec2::new(0.0, 1.0), point: Vec2::new(2.0, 0.0) };
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Point(Vec2::ZERO),
),
Err(())
);
}
}
mod solve_linear_program_2d_tests {
use glam::Vec2;
use crate::linear_programming::LinearProgram2DResult;
use super::{solve_linear_program_2d, Line, OptimalValue};
#[test]
fn uses_projected_optimal_point() {
let one_over_root_2 = 1.0f32 / 2.0f32.sqrt();
assert_eq!(
solve_linear_program_2d(
Default::default(),
1.0,
&OptimalValue::Point(Vec2::new(0.5, 0.25)),
),
LinearProgram2DResult::Feasible(Vec2::new(0.5, 0.25))
);
assert_eq!(
solve_linear_program_2d(
Default::default(),
1.0,
&OptimalValue::Point(Vec2::new(1.0, 1.0)),
),
LinearProgram2DResult::Feasible(Vec2::new(
one_over_root_2,
one_over_root_2
))
);
}
#[test]
fn uses_optimal_direction() {
let one_over_root_2 = 1.0f32 / 2.0f32.sqrt();
assert_eq!(
solve_linear_program_2d(
Default::default(),
3.0,
&OptimalValue::Direction(Vec2::new(one_over_root_2, one_over_root_2)),
),
LinearProgram2DResult::Feasible(Vec2::new(
one_over_root_2 * 3.0,
one_over_root_2 * 3.0
))
);
assert_eq!(
solve_linear_program_2d(
Default::default(),
5.0,
&OptimalValue::Direction(Vec2::new(
one_over_root_2,
-one_over_root_2
)),
),
LinearProgram2DResult::Feasible(Vec2::new(
one_over_root_2 * 5.0,
one_over_root_2 * -5.0
))
);
}
#[test]
fn satisfies_constraints() {
let one_over_root_2 = 1.0f32 / 2.0f32.sqrt();
let constraints = [
Line { direction: Vec2::new(0.0, 1.0), point: Vec2::new(0.5, 0.0) },
Line { direction: Vec2::new(1.0, 0.0), point: Vec2::new(-0.5, -0.25) },
];
assert_eq!(
solve_linear_program_2d(
&constraints,
1.0,
&OptimalValue::Point(Vec2::new(-0.1, 0.3))
),
LinearProgram2DResult::Feasible(Vec2::new(-0.1, 0.3))
);
assert_eq!(
solve_linear_program_2d(
&constraints,
1.0,
&OptimalValue::Point(Vec2::new(-2.0, 2.0))
),
LinearProgram2DResult::Feasible(Vec2::new(
-one_over_root_2,
one_over_root_2
))
);
assert_eq!(
solve_linear_program_2d(
&constraints,
1.0,
&OptimalValue::Point(Vec2::new(2.0, 0.5))
),
LinearProgram2DResult::Feasible(Vec2::new(0.5, 0.5))
);
assert_eq!(
solve_linear_program_2d(
&constraints,
1.0,
&OptimalValue::Point(Vec2::new(0.0, -0.5))
),
LinearProgram2DResult::Feasible(Vec2::new(0.0, -0.25))
);
assert_eq!(
solve_linear_program_2d(
&constraints,
1.0,
&OptimalValue::Point(Vec2::new(1.0, -0.5))
),
LinearProgram2DResult::Feasible(Vec2::new(0.5, -0.25))
);
}
#[test]
fn constraints_are_infeasible() {
let constraints = [
Line { direction: Vec2 { x: 0.0, y: 1.0 }, point: Vec2::ZERO },
Line { direction: Vec2 { x: 1.0, y: 0.0 }, point: Vec2::ZERO },
Line {
direction: Vec2 { x: -1.0, y: -1.0 }.normalize(),
point: Vec2::new(0.1, -0.1),
},
];
assert_eq!(
solve_linear_program_2d(
&constraints,
1.0,
&OptimalValue::Point(Vec2::ONE)
),
LinearProgram2DResult::Infeasible {
index_of_failed_line: 2,
partial_value: Vec2::new(0.0, 1.0)
}
)
}
#[test]
fn optimal_direction_uses_constraint_lines() {
let circle_height_at_half = (1.0f32 - 0.5 * 0.5).sqrt();
let constraints = [
Line { direction: Vec2 { x: 0.0, y: 1.0 }, point: Vec2::new(0.5, 0.0) },
Line {
direction: Vec2 { x: 1.0, y: 0.0 },
point: Vec2::new(-0.5, -0.3),
},
Line {
direction: Vec2 { x: -1.0, y: -1.0 }.normalize(),
point: Vec2::new(-0.6, 0.6),
},
];
assert_eq!(
solve_linear_program_2d(
&constraints,
1.0,
&OptimalValue::Direction(Vec2::new(1.0, -1.0).normalize())
),
LinearProgram2DResult::Feasible(Vec2::new(0.5, -0.3))
);
assert_eq!(
solve_linear_program_2d(
&constraints,
1.0,
&OptimalValue::Direction(Vec2::new(1.0, 0.5).normalize())
),
LinearProgram2DResult::Feasible(Vec2::new(0.5, circle_height_at_half))
);
assert_eq!(
solve_linear_program_2d(
&constraints,
1.0,
&OptimalValue::Direction(Vec2::new(0.0, 1.0))
),
LinearProgram2DResult::Feasible(Vec2::new(0.0, 1.0))
);
}
}
mod solve_linear_program_3d_tests {
use glam::Vec2;
use std::f32::consts::PI;
use super::{solve_linear_program_3d, Line};
#[test]
fn minimally_penetrates_constraints() {
let constraints = [
Line { direction: Vec2::new(1.0, 0.0), point: Vec2::new(-100.0, 0.0) },
Line { direction: Vec2::new(0.0, -1.0), point: Vec2::new(0.0, 0.0) },
Line {
direction: Vec2::new(-1.0, 1.0).normalize(),
point: Vec2::new(0.0, -1.0),
},
];
let root_2 = 2.0f32.sqrt();
assert_vec2_near!(
solve_linear_program_3d(
&constraints,
0,
2.0,
0,
Vec2::new(0.0, 0.0)
),
Vec2::new(-1.0, -1.0).normalize() * (root_2 / (2.0 + root_2))
);
}
#[test]
fn rigid_constraints_never_relaxed() {
let constraints = [
Line { direction: Vec2::new(1.0, 0.0), point: Vec2::new(-100.0, 0.0) },
Line { direction: Vec2::new(0.0, -1.0), point: Vec2::new(0.0, 0.0) },
Line {
direction: Vec2::new(-1.0, 1.0).normalize(),
point: Vec2::new(0.0, -1.0),
},
];
assert_vec2_near!(
solve_linear_program_3d(
&constraints,
2,
2.0,
2,
Vec2::new(0.0, 0.0)
),
Vec2::new(0.0, 0.0)
);
assert_vec2_near!(
solve_linear_program_3d(
&constraints,
1,
2.0,
1,
Vec2::new(0.0, 0.0)
),
Vec2::new(-(PI / 8.0).tan(), 0.0)
);
}
}
mod solve_linear_program_tests {
use glam::Vec2;
use std::f32::consts::PI;
use super::{solve_linear_program, Line};
#[test]
fn uses_projected_optimal_point() {
let one_over_root_2 = 1.0f32 / 2.0f32.sqrt();
assert_eq!(
solve_linear_program(
Default::default(),
0,
1.0,
Vec2::new(0.5, 0.25)
),
Vec2::new(0.5, 0.25)
);
assert_eq!(
solve_linear_program(
Default::default(),
0,
1.0,
Vec2::new(1.0, 1.0)
),
Vec2::new(one_over_root_2, one_over_root_2)
);
}
#[test]
fn satisfies_constraints() {
let one_over_root_2 = 1.0f32 / 2.0f32.sqrt();
let constraints = [
Line { direction: Vec2::new(0.0, 1.0), point: Vec2::new(0.5, 0.0) },
Line { direction: Vec2::new(1.0, 0.0), point: Vec2::new(-0.5, -0.25) },
];
assert_eq!(
solve_linear_program(
&constraints,
0,
1.0,
Vec2::new(-0.1, 0.3)
),
Vec2::new(-0.1, 0.3)
);
assert_eq!(
solve_linear_program(
&constraints,
0,
1.0,
Vec2::new(-2.0, 2.0)
),
Vec2::new(-one_over_root_2, one_over_root_2)
);
assert_eq!(
solve_linear_program(
&constraints,
0,
1.0,
Vec2::new(2.0, 0.5)
),
Vec2::new(0.5, 0.5)
);
assert_eq!(
solve_linear_program(
&constraints,
0,
1.0,
Vec2::new(0.0, -0.5)
),
Vec2::new(0.0, -0.25)
);
assert_eq!(
solve_linear_program(
&constraints,
0,
1.0,
Vec2::new(1.0, -0.5)
),
Vec2::new(0.5, -0.25)
);
}
#[test]
fn infeasible_program_minimally_penetrates_constraints() {
let constraints = [
Line { direction: Vec2::new(1.0, 0.0), point: Vec2::new(-100.0, 0.0) },
Line { direction: Vec2::new(0.0, -1.0), point: Vec2::new(0.0, 0.0) },
Line {
direction: Vec2::new(-1.0, 1.0).normalize(),
point: Vec2::new(0.0, -1.0),
},
];
let root_2 = 2.0f32.sqrt();
assert_vec2_near!(
solve_linear_program(
&constraints,
0,
2.0,
Vec2::new(1.0, 1.0)
),
Vec2::new(-1.0, -1.0).normalize() * (root_2 / (2.0 + root_2))
);
}
#[test]
fn rigid_constraints_never_relaxed_when_infeasible() {
let constraints = [
Line { direction: Vec2::new(1.0, 0.0), point: Vec2::new(-100.0, 0.0) },
Line { direction: Vec2::new(0.0, -1.0), point: Vec2::new(0.0, 0.0) },
Line {
direction: Vec2::new(-1.0, 1.0).normalize(),
point: Vec2::new(0.0, -1.0),
},
];
assert_vec2_near!(
solve_linear_program(
&constraints,
2,
2.0,
Vec2::new(0.0, 0.0)
),
Vec2::new(0.0, 0.0)
);
assert_vec2_near!(
solve_linear_program(
&constraints,
1,
2.0,
Vec2::new(0.0, 0.0)
),
Vec2::new(-(PI / 8.0).tan(), 0.0)
);
}
}
}