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;
}
(constraint.point + previous_constraint.point) * 0.5
} else {
let line_normal = cross.cross(constraint.normal);
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)]
#[path = "linear_programming_test.rs"]
mod test;