use glam::Vec3;
#[derive(Clone, Debug)]
pub struct Plane {
pub point: Vec3,
pub normal: Vec3,
}
impl Plane {
pub fn signed_distance_to_plane(&self, point: Vec3) -> f32 {
(point - self.point).dot(self.normal)
}
}
pub fn solve_linear_program(
constraints: &[Plane],
radius: f32,
preferred_value: Vec3,
) -> Vec3 {
match solve_linear_program_3d(
constraints,
radius,
&OptimalValue::Point(preferred_value),
) {
LinearProgram3DResult::Feasible(optimal_value) => optimal_value,
LinearProgram3DResult::Infeasible {
index_of_failed_line,
partial_value,
} => solve_linear_program_4d(
constraints,
radius,
index_of_failed_line,
partial_value,
),
}
}
#[derive(Clone, Debug)]
struct Line {
point: Vec3,
direction: Vec3,
}
const RVO_EPSILON: f32 = 0.00001;
enum OptimalValue {
Point(Vec3),
Direction(Vec3),
}
fn solve_linear_program_along_line(
line: &Line,
radius: f32,
constraints: &[Plane],
optimal_value: &OptimalValue,
) -> Result<Vec3, ()> {
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_dot = line.direction.dot(constraint.normal);
let numerator = constraint.signed_distance_to_plane(line.point);
if direction_dot * direction_dot <= RVO_EPSILON {
if numerator < -RVO_EPSILON {
return Err(());
}
continue;
}
let t = -numerator / direction_dot;
if direction_dot >= 0.0 {
t_left = t_left.max(t);
} else {
t_right = t_right.min(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)
}
fn solve_linear_program_along_plane(
plane: &Plane,
radius: f32,
constraints: &[Plane],
optimal_value: &OptimalValue,
) -> Result<Vec3, ()> {
let plane_distance_from_origin = plane.point.dot(plane.normal);
let plane_distance_squared_from_origin =
plane_distance_from_origin * plane_distance_from_origin;
let radius_squared = radius * radius;
if plane_distance_squared_from_origin > radius_squared {
return Err(());
}
let squared_radius_in_valid_plane =
radius_squared - plane_distance_squared_from_origin;
let valid_plane_center = plane_distance_from_origin * plane.normal;
let mut best_value = match optimal_value {
&OptimalValue::Direction(direction) => {
let projected_optimal_direction_in_plane =
direction - direction.dot(plane.normal) * plane.normal;
let squared_length_of_projection = projected_optimal_direction_in_plane
.dot(projected_optimal_direction_in_plane);
if squared_length_of_projection <= RVO_EPSILON {
valid_plane_center
} else {
valid_plane_center
+ (squared_radius_in_valid_plane / squared_length_of_projection)
.sqrt()
* projected_optimal_direction_in_plane
}
}
&OptimalValue::Point(point) => {
let distance_from_plane = plane.signed_distance_to_plane(point);
let projected_point = point - distance_from_plane * plane.normal;
if projected_point.dot(projected_point) > radius_squared {
let relative_point = projected_point - valid_plane_center;
let squared_distance_from_center = relative_point.dot(relative_point);
valid_plane_center
+ (squared_radius_in_valid_plane / squared_distance_from_center)
.sqrt()
* relative_point
} else {
projected_point
}
}
};
for (index, constraint) in constraints.iter().enumerate() {
let pen = constraint.signed_distance_to_plane(best_value);
if pen >= 0.0 {
continue;
}
let cross = constraint.normal.cross(plane.normal);
if cross.dot(cross) <= RVO_EPSILON {
return Err(());
}
let cross = cross.normalize();
let line_normal = cross.cross(plane.normal);
let line = Line {
direction: cross,
point: plane.point
- constraint.signed_distance_to_plane(plane.point)
/ line_normal.dot(constraint.normal)
* line_normal,
};
let Ok(new_value) = solve_linear_program_along_line(
&line,
radius,
&constraints[0..index],
optimal_value,
) else {
return Err(());
};
best_value = new_value;
}
Ok(best_value)
}
#[derive(PartialEq, Debug)]
enum LinearProgram3DResult {
Feasible(Vec3),
Infeasible {
index_of_failed_line: usize,
partial_value: Vec3,
},
}
fn solve_linear_program_3d(
constraints: &[Plane],
radius: f32,
optimal_value: &OptimalValue,
) -> LinearProgram3DResult {
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 constraint.signed_distance_to_plane(best_value) > 0.0 {
continue;
}
match solve_linear_program_along_plane(
constraint,
radius,
&constraints[0..index],
optimal_value,
) {
Ok(new_value) => best_value = new_value,
Err(()) => {
return LinearProgram3DResult::Infeasible {
index_of_failed_line: index,
partial_value: best_value,
}
}
}
}
LinearProgram3DResult::Feasible(best_value)
}
fn solve_linear_program_4d(
constraints: &[Plane],
radius: f32,
index_of_failed_plane: usize,
partial_value: Vec3,
) -> Vec3 {
let mut penetration = 0.0;
let mut best_value = partial_value;
for (index, constraint) in
constraints[index_of_failed_plane..].iter().enumerate()
{
if -constraint.signed_distance_to_plane(best_value) <= penetration {
continue;
}
let index = index + index_of_failed_plane;
let mut penetration_constraints = Vec::with_capacity(index);
for previous_constraint in &constraints[0..index] {
let cross = previous_constraint.normal.cross(constraint.normal);
let new_plane_point;
if cross.dot(cross) <= RVO_EPSILON {
if constraint.normal.dot(previous_constraint.normal) > 0.0 {
continue;
}
new_plane_point = (constraint.point + previous_constraint.point) * 0.5;
} else {
let line_normal = cross.cross(constraint.normal);
new_plane_point = constraint.point
- previous_constraint.signed_distance_to_plane(constraint.point)
/ line_normal.dot(previous_constraint.normal)
* line_normal;
}
penetration_constraints.push(Plane {
point: new_plane_point,
normal: (previous_constraint.normal - constraint.normal).normalize(),
});
}
if let LinearProgram3DResult::Feasible(result) = solve_linear_program_3d(
&penetration_constraints,
radius,
&OptimalValue::Direction(constraint.normal),
) {
best_value = result;
penetration = -constraint.signed_distance_to_plane(best_value);
}
}
best_value
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! assert_vec3_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::Vec3;
use crate::linear_programming::Plane;
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: Vec3::new(0.0, 1.0, 0.0),
point: Vec3::new(0.5, 0.0, 0.0),
};
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Point(Vec3::new(5.0, 0.25, 0.0)),
),
Ok(Vec3::new(0.5, 0.25, 0.0))
);
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Point(Vec3::new(5.0, 2.0, 0.0)),
),
Ok(Vec3::new(0.5, circle_height_at_half, 0.0))
);
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Point(Vec3::new(5.0, -100.0, 0.0)),
),
Ok(Vec3::new(0.5, -circle_height_at_half, 0.0))
);
}
#[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: Vec3::new(0.0, 0.0, 1.0),
point: Vec3::new(0.5, 0.0, 0.0),
};
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Direction(Vec3::new(1.0, 0.0, 0.5).normalize()),
),
Ok(Vec3::new(0.5, 0.0, circle_height_at_half))
);
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Direction(Vec3::new(1.0, 0.0, -0.5).normalize()),
),
Ok(Vec3::new(0.5, 0.0, -circle_height_at_half))
);
}
#[test]
fn constraints_remove_valid_values() {
let valid_line = Line {
direction: Vec3::new(0.0, 0.0, 1.0),
point: Vec3::new(0.0, 0.5, 0.0),
};
let constraints = [
Plane {
normal: Vec3::new(0.0, 0.0, -1.0),
point: Vec3::new(0.0, -100.0, 0.5),
},
Plane {
normal: Vec3::new(0.0, -1.0, 1.0).normalize(),
point: Vec3::new(0.0, 0.25, -1.0),
},
];
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
&constraints,
&OptimalValue::Point(Vec3::new(0.0, -5.0, 0.25)),
),
Ok(Vec3::new(0.0, 0.5, 0.25))
);
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
&constraints,
&OptimalValue::Point(Vec3::new(0.0, -5.0, 1.0)),
),
Ok(Vec3::new(0.0, 0.5, 0.5))
);
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
&constraints,
&OptimalValue::Point(Vec3::new(0.0, -5.0, -1.0)),
),
Ok(Vec3::new(0.0, 0.5, -0.75))
);
}
#[test]
fn constraints_are_infeasible() {
let valid_line = Line {
direction: Vec3::new(0.0, 1.0, 0.0),
point: Vec3::new(0.5, 0.0, 0.0),
};
let constraints = [
Plane {
normal: Vec3::new(0.0, 1.0, 0.0),
point: Vec3::new(-100.0, 0.5, 0.0),
},
Plane {
normal: Vec3::new(0.0, -1.0, 0.0),
point: Vec3::new(-100.0, -0.5, 0.0),
},
];
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
&constraints,
&OptimalValue::Point(Vec3::ZERO),
),
Err(())
);
}
#[test]
fn valid_line_outside_sphere() {
let valid_line = Line {
direction: Vec3::new(0.0, 1.0, 0.0),
point: Vec3::new(2.0, 0.0, 0.0),
};
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
Default::default(),
&OptimalValue::Point(Vec3::ZERO),
),
Err(())
);
}
#[test]
fn constraint_parallel_to_line() {
let valid_line =
Line { direction: Vec3::new(1.0, 0.0, 0.0), point: Vec3::ZERO };
let feasible_constraint = [Plane {
point: Vec3::new(0.0, -0.1, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
}];
assert_vec3_near!(
solve_linear_program_along_line(
&valid_line,
1.0,
&feasible_constraint,
&OptimalValue::Direction(Vec3::X)
)
.unwrap(),
Vec3::new(1.0, 0.0, 0.0)
);
let infeasible_constraint = [Plane {
point: Vec3::new(0.0, 0.1, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
}];
assert_eq!(
solve_linear_program_along_line(
&valid_line,
1.0,
&infeasible_constraint,
&OptimalValue::Direction(Vec3::X)
),
Err(())
);
}
}
mod solve_linear_program_along_plane_tests {
use glam::Vec3;
use crate::linear_programming::{
solve_linear_program_along_plane, OptimalValue,
};
use super::Plane;
#[test]
fn uses_projected_optimal_point() {
let value_plane = Plane {
point: Vec3::new(2.0, 2.0, 2.0),
normal: Vec3::new(1.0, 0.0, 1.0).normalize(),
};
assert_vec3_near!(
solve_linear_program_along_plane(
&value_plane,
10.0,
&[],
&OptimalValue::Point(Vec3::new(0.0, 1.0, 0.0)),
)
.unwrap(),
Vec3::new(2.0, 1.0, 2.0)
);
assert_vec3_near!(
solve_linear_program_along_plane(
&value_plane,
10.0,
&[],
&OptimalValue::Point(Vec3::new(0.0, 15.0, 0.0)),
)
.unwrap(),
Vec3::new(2.0, 92f32.sqrt(), 2.0)
);
}
#[test]
fn uses_projected_optimal_direction() {
let value_plane = Plane {
point: Vec3::new(-10.0, 10.0, 14.0),
normal: Vec3::new(1.0, 0.0, 1.0).normalize(),
};
assert_vec3_near!(
solve_linear_program_along_plane(
&value_plane,
10.0,
&[],
&OptimalValue::Direction(Vec3::new(0.0, 1.0, 0.0)),
)
.unwrap(),
Vec3::new(2.0, 92f32.sqrt(), 2.0)
);
assert_vec3_near!(
solve_linear_program_along_plane(
&value_plane,
10.0,
&[],
&OptimalValue::Direction(Vec3::new(1.0, 0.0, 1.0).normalize()),
)
.unwrap(),
Vec3::new(2.0, 0.0, 2.0)
);
}
#[test]
fn satisfies_constraints() {
let value_plane =
Plane { point: Vec3::ZERO, normal: Vec3::new(0.0, 0.0, 1.0) };
let constraints = [
Plane {
point: Vec3::new(0.0, -0.1, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
},
Plane {
point: Vec3::new(0.0, 0.5, 0.0),
normal: Vec3::new(-1.0, -1.0, 0.0),
},
];
assert_vec3_near!(
solve_linear_program_along_plane(
&value_plane,
1.0,
&constraints,
&OptimalValue::Direction(Vec3::new(1.0, 0.0, 0.0))
)
.unwrap(),
Vec3::new(0.6, -0.1, 0.0)
);
}
#[test]
fn constraints_are_infeasible() {
let value_plane =
Plane { point: Vec3::ZERO, normal: Vec3::new(0.0, 0.0, 1.0) };
let constraints = [
Plane {
point: Vec3::new(0.0, -0.1, 0.0),
normal: Vec3::new(0.0, -1.0, 0.0),
},
Plane {
point: Vec3::new(0.0, 0.1, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
},
];
assert_eq!(
solve_linear_program_along_plane(
&value_plane,
1.0,
&constraints,
&OptimalValue::Point(Vec3::ZERO)
),
Err(())
);
}
#[test]
fn value_plane_and_constraint_are_parallel() {
let value_plane =
Plane { point: Vec3::ZERO, normal: Vec3::new(0.0, 0.0, 1.0) };
let constraints = [Plane {
point: Vec3::new(0.0, 0.0, 0.1),
normal: Vec3::new(0.0, 0.0, 1.0),
}];
assert_eq!(
solve_linear_program_along_plane(
&value_plane,
1.0,
&constraints,
&OptimalValue::Point(Vec3::ZERO)
),
Err(())
);
}
#[test]
fn value_plane_outside_radius() {
let value_plane = Plane {
point: Vec3::new(0.0, 0.0, 1.1),
normal: Vec3::new(0.0, 0.0, 1.0),
};
assert_eq!(
solve_linear_program_along_plane(
&value_plane,
1.0,
&[],
&OptimalValue::Point(Vec3::ZERO)
),
Err(())
);
}
}
mod solve_linear_program_3d_tests {
use glam::Vec3;
use crate::linear_programming::{
solve_linear_program_3d, LinearProgram3DResult, OptimalValue, Plane,
};
fn unwrap_feasible(result: LinearProgram3DResult) -> Vec3 {
match result {
LinearProgram3DResult::Feasible(result) => result,
other => panic!("expected feasible, got {:?}", other),
}
}
fn unwrap_infeasible(result: LinearProgram3DResult) -> (usize, Vec3) {
match result {
LinearProgram3DResult::Infeasible {
index_of_failed_line,
partial_value,
} => (index_of_failed_line, partial_value),
other => panic!("expected infeasible, got {:?}", other),
}
}
#[test]
fn uses_projected_optimal_point() {
let one_over_root_2 = 1.0f32 / 2.0f32.sqrt();
assert_vec3_near!(
unwrap_feasible(solve_linear_program_3d(
Default::default(),
1.0,
&OptimalValue::Point(Vec3::new(0.5, 0.25, 0.0)),
)),
Vec3::new(0.5, 0.25, 0.0)
);
assert_vec3_near!(
unwrap_feasible(solve_linear_program_3d(
Default::default(),
1.0,
&OptimalValue::Point(Vec3::new(1.0, 1.0, 0.0)),
)),
Vec3::new(one_over_root_2, one_over_root_2, 0.0)
);
}
#[test]
fn uses_optimal_direction() {
let one_over_root_2 = 1.0f32 / 2.0f32.sqrt();
assert_vec3_near!(
unwrap_feasible(solve_linear_program_3d(
Default::default(),
3.0,
&OptimalValue::Direction(Vec3::new(
0.0,
one_over_root_2,
one_over_root_2
)),
)),
Vec3::new(0.0, one_over_root_2 * 3.0, one_over_root_2 * 3.0)
);
assert_vec3_near!(
unwrap_feasible(solve_linear_program_3d(
Default::default(),
5.0,
&OptimalValue::Direction(Vec3::new(
0.0,
one_over_root_2,
-one_over_root_2
)),
)),
Vec3::new(0.0, one_over_root_2 * 5.0, one_over_root_2 * -5.0)
);
}
#[test]
fn projects_to_constraint() {
let constraints = [Plane {
point: Vec3::new(0.5, 0.5, 0.5),
normal: Vec3::ONE.normalize(),
}];
assert_vec3_near!(
unwrap_feasible(solve_linear_program_3d(
&constraints,
1.0,
&OptimalValue::Point(Vec3::ZERO),
)),
Vec3::new(0.5, 0.5, 0.5)
);
}
#[test]
fn constraints_are_infeasible() {
let constraints = [
Plane {
point: Vec3::new(0.5, 0.0, 0.0),
normal: Vec3::new(-1.0, 0.0, 0.0),
},
Plane {
point: Vec3::new(0.0, 0.1, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
},
Plane {
point: Vec3::new(0.0, -0.1, 0.0),
normal: Vec3::new(0.0, -1.0, 0.0),
},
];
let (index_of_failed_line, partial_value) =
unwrap_infeasible(solve_linear_program_3d(
&constraints,
1.0,
&OptimalValue::Point(Vec3::ZERO),
));
assert_eq!(index_of_failed_line, 2);
assert_vec3_near!(partial_value, Vec3::new(0.0, 0.1, 0.0));
}
}
mod solve_linear_program_4d_tests {
use glam::Vec3;
use super::{solve_linear_program_4d, Plane};
#[test]
fn finds_least_penetrating_value() {
let constraints = [
Plane {
point: Vec3::new(0.0, 1.0, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
},
Plane {
point: Vec3::new(0.0, 1.0, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
},
Plane {
point: Vec3::new(1.0, 0.0, 0.0),
normal: Vec3::new(1.0, 0.0, 0.0),
},
Plane {
point: Vec3::new(-2.0, -2.0, 0.0),
normal: Vec3::new(-1.0, -1.0, 0.0).normalize(),
},
];
assert_vec3_near!(
solve_linear_program_4d(
&constraints,
10.0,
3,
Vec3::new(1.0, 1.0, 0.0)
),
Vec3::new(-0.75736, -0.75736, 9.94248)
);
}
}
mod solve_linear_program_tests {
use glam::Vec3;
use super::{solve_linear_program, Plane};
#[test]
fn finds_valid_value_when_feasible() {
let constraints = [
Plane {
point: Vec3::new(0.0, 1.0, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
},
Plane {
point: Vec3::new(1.0, 0.0, 0.0),
normal: Vec3::new(1.0, 0.0, 0.0),
},
Plane {
point: Vec3::new(0.0, 0.0, 1.0),
normal: Vec3::new(0.0, 0.0, 1.0),
},
];
assert_vec3_near!(
solve_linear_program(
&constraints,
10.0,
Vec3::ZERO,
),
Vec3::new(1.0, 1.0, 1.0)
);
}
#[test]
fn finds_least_penetrating_value_when_infeasible() {
let constraints = [
Plane {
point: Vec3::new(0.0, 1.0, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
},
Plane {
point: Vec3::new(0.0, 1.0, 0.0),
normal: Vec3::new(0.0, 1.0, 0.0),
},
Plane {
point: Vec3::new(1.0, 0.0, 0.0),
normal: Vec3::new(1.0, 0.0, 0.0),
},
Plane {
point: Vec3::new(-2.0, -2.0, 0.0),
normal: Vec3::new(-1.0, -1.0, 0.0).normalize(),
},
];
assert_vec3_near!(
solve_linear_program(
&constraints,
10.0,
Vec3::ZERO,
),
Vec3::new(-0.75736, -0.75736, 9.94248)
);
}
}
}