177 lines
5.4 KiB
Rust
177 lines
5.4 KiB
Rust
use voltex_math::Vec3;
|
|
|
|
/// A dynamic obstacle represented as a position and radius.
|
|
#[derive(Debug, Clone)]
|
|
pub struct DynamicObstacle {
|
|
pub position: Vec3,
|
|
pub radius: f32,
|
|
}
|
|
|
|
/// Compute avoidance steering force using velocity obstacle approach.
|
|
///
|
|
/// Projects the agent's velocity forward by `look_ahead` distance and checks
|
|
/// for circle intersections with obstacles. Returns a steering force perpendicular
|
|
/// to the approach direction to avoid the nearest threatening obstacle.
|
|
pub fn avoid_obstacles(
|
|
agent_pos: Vec3,
|
|
agent_vel: Vec3,
|
|
agent_radius: f32,
|
|
obstacles: &[DynamicObstacle],
|
|
look_ahead: f32,
|
|
) -> Vec3 {
|
|
let speed = agent_vel.length();
|
|
if speed < f32::EPSILON {
|
|
return Vec3::ZERO;
|
|
}
|
|
|
|
let forward = agent_vel * (1.0 / speed);
|
|
let mut nearest_t = f32::INFINITY;
|
|
let mut avoidance = Vec3::ZERO;
|
|
|
|
for obs in obstacles {
|
|
let to_obs = obs.position - agent_pos;
|
|
let combined_radius = agent_radius + obs.radius;
|
|
|
|
// Project obstacle center onto the velocity ray
|
|
let proj = to_obs.dot(forward);
|
|
|
|
// Obstacle is behind or too far ahead
|
|
if proj < 0.0 || proj > look_ahead {
|
|
continue;
|
|
}
|
|
|
|
// Lateral distance from the velocity ray to obstacle center (XZ only for ground agents)
|
|
let closest_on_ray = agent_pos + forward * proj;
|
|
let diff = obs.position - closest_on_ray;
|
|
let lateral_dist_sq = diff.x * diff.x + diff.z * diff.z;
|
|
let combined_sq = combined_radius * combined_radius;
|
|
|
|
if lateral_dist_sq >= combined_sq {
|
|
continue; // No collision
|
|
}
|
|
|
|
// This obstacle threatens the agent — check if it's the nearest
|
|
if proj < nearest_t {
|
|
nearest_t = proj;
|
|
|
|
// Avoidance direction: perpendicular to approach, away from obstacle
|
|
// Use XZ plane lateral vector
|
|
let lateral = Vec3::new(diff.x, 0.0, diff.z);
|
|
let lat_len = lateral.length();
|
|
if lat_len > f32::EPSILON {
|
|
// Steer away from obstacle (opposite direction of lateral offset)
|
|
let steer_dir = lateral * (-1.0 / lat_len);
|
|
// Strength inversely proportional to distance (closer = stronger)
|
|
let strength = 1.0 - (proj / look_ahead);
|
|
avoidance = steer_dir * strength * speed;
|
|
} else {
|
|
// Agent heading straight at obstacle center — pick perpendicular
|
|
// Use cross product with Y to get a lateral direction
|
|
let perp = Vec3::new(-forward.z, 0.0, forward.x);
|
|
avoidance = perp * speed;
|
|
}
|
|
}
|
|
}
|
|
|
|
avoidance
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_no_obstacle_zero_force() {
|
|
let force = avoid_obstacles(
|
|
Vec3::new(0.0, 0.0, 0.0),
|
|
Vec3::new(1.0, 0.0, 0.0),
|
|
0.5,
|
|
&[],
|
|
5.0,
|
|
);
|
|
assert!(force.length() < 1e-6, "no obstacles should give zero force");
|
|
}
|
|
|
|
#[test]
|
|
fn test_obstacle_behind_zero_force() {
|
|
let obs = DynamicObstacle {
|
|
position: Vec3::new(-3.0, 0.0, 0.0),
|
|
radius: 1.0,
|
|
};
|
|
let force = avoid_obstacles(
|
|
Vec3::new(0.0, 0.0, 0.0),
|
|
Vec3::new(1.0, 0.0, 0.0),
|
|
0.5,
|
|
&[obs],
|
|
5.0,
|
|
);
|
|
assert!(force.length() < 1e-6, "obstacle behind should give zero force");
|
|
}
|
|
|
|
#[test]
|
|
fn test_obstacle_ahead_lateral_force() {
|
|
let obs = DynamicObstacle {
|
|
position: Vec3::new(3.0, 0.0, 0.5), // slightly to the right
|
|
radius: 1.0,
|
|
};
|
|
let force = avoid_obstacles(
|
|
Vec3::new(0.0, 0.0, 0.0),
|
|
Vec3::new(2.0, 0.0, 0.0), // moving in +X
|
|
0.5,
|
|
&[obs],
|
|
5.0,
|
|
);
|
|
assert!(force.length() > 0.1, "obstacle ahead should give non-zero force");
|
|
// Force should push away from obstacle (obstacle is at +Z, force should be -Z)
|
|
assert!(force.z < 0.0, "force should push away from obstacle (negative Z)");
|
|
}
|
|
|
|
#[test]
|
|
fn test_obstacle_far_away_zero_force() {
|
|
let obs = DynamicObstacle {
|
|
position: Vec3::new(3.0, 0.0, 10.0), // far to the side
|
|
radius: 1.0,
|
|
};
|
|
let force = avoid_obstacles(
|
|
Vec3::new(0.0, 0.0, 0.0),
|
|
Vec3::new(1.0, 0.0, 0.0),
|
|
0.5,
|
|
&[obs],
|
|
5.0,
|
|
);
|
|
assert!(force.length() < 1e-6, "distant obstacle should give zero force");
|
|
}
|
|
|
|
#[test]
|
|
fn test_obstacle_beyond_lookahead_zero_force() {
|
|
let obs = DynamicObstacle {
|
|
position: Vec3::new(10.0, 0.0, 0.0),
|
|
radius: 1.0,
|
|
};
|
|
let force = avoid_obstacles(
|
|
Vec3::new(0.0, 0.0, 0.0),
|
|
Vec3::new(1.0, 0.0, 0.0),
|
|
0.5,
|
|
&[obs],
|
|
5.0, // look_ahead is only 5
|
|
);
|
|
assert!(force.length() < 1e-6, "obstacle beyond lookahead should give zero force");
|
|
}
|
|
|
|
#[test]
|
|
fn test_zero_velocity_zero_force() {
|
|
let obs = DynamicObstacle {
|
|
position: Vec3::new(3.0, 0.0, 0.0),
|
|
radius: 1.0,
|
|
};
|
|
let force = avoid_obstacles(
|
|
Vec3::new(0.0, 0.0, 0.0),
|
|
Vec3::ZERO,
|
|
0.5,
|
|
&[obs],
|
|
5.0,
|
|
);
|
|
assert!(force.length() < 1e-6, "zero velocity should give zero force");
|
|
}
|
|
}
|