use crate::{Ctx, Entity, MeshData, Transform3D, Vec3};
use navmesh_rs::{Limits, Workspace, bake, crowd, detour, recast, tilecache};
use std::sync::atomic::{AtomicU64, Ordering};
mod carve;
static NEXT_REVISION: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Debug, Default)]
pub struct NavSource {
pub triangles: Vec<[Vec3; 3]>,
}
impl NavSource {
#[must_use]
pub fn from_mesh(mesh: &MeshData, transform: Transform3D) -> Self {
let mut triangles = Vec::with_capacity(mesh.indices.len() / 3);
if !mesh.indices.len().is_multiple_of(3) {
return Self {
triangles: vec![[Vec3::NAN; 3]],
};
}
for indices in mesh.indices.as_chunks::<3>().0 {
let mut tri = [Vec3::ZERO; 3];
for (v, &index) in tri.iter_mut().zip(indices) {
let Some(&p) = mesh.positions.get(index as usize) else {
return Self {
triangles: vec![[Vec3::NAN; 3]],
};
};
*v = transform.position + transform.rotation * (p * transform.scale);
}
if transform.scale.element_product() < 0.0 {
tri.swap(1, 2);
}
triangles.push(tri);
}
Self { triangles }
}
}
#[derive(Clone, Copy, Debug)]
pub struct NavBakeSettings {
pub agent_radius: f32,
pub agent_height: f32,
pub max_climb: f32,
pub max_slope: f32,
pub cell_size: f32,
pub cell_height: f32,
pub tile_size: u32,
pub carving: bool,
}
impl Default for NavBakeSettings {
fn default() -> Self {
Self {
agent_radius: 0.3,
agent_height: 1.8,
max_climb: 0.4,
max_slope: 45.0,
cell_size: 0.2,
cell_height: 0.1,
tile_size: 32,
carving: false,
}
}
}
#[derive(Debug)]
pub struct NavBakeError(String);
impl std::fmt::Display for NavBakeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for NavBakeError {}
fn error(value: impl std::fmt::Display) -> NavBakeError {
NavBakeError(value.to_string())
}
const MAX_TILES: usize = 1 << 14;
const TILE_WORKING_BYTES: usize = 64 << 20;
const TILE_WORK: u64 = 100_000_000;
const SETUP_BYTES: usize = 2 << 30;
fn bake_error(e: &bake::BakeError, s: &NavBakeSettings) -> NavBakeError {
use navmesh_rs::Error;
let at = e.tile.map_or_else(String::new, |[x, z]| format!(" in tile ({x}, {z})"));
let advice = format!(
"lower NavBakeSettings::tile_size (now {}) or raise cell_size (now {})",
s.tile_size, s.cell_size
);
match &e.error {
Error::LimitExceeded => error(format!(
"navigation bake limit exceeded{at}: a tile needs more than {} MiB of working memory, \
{} million work units or its output size cap; {advice}",
TILE_WORKING_BYTES >> 20,
TILE_WORK / 1_000_000
)),
Error::FormatLimit(what) => error(format!("navigation bake format limit{at}: {what}; {advice}")),
Error::SizeOverflow => error(format!("navigation bake size overflow{at}; {advice}")),
other => error(format!("navigation bake failed{at}: {other}")),
}
}
struct PolygonBounds {
reference: detour::PolyRef,
min: Vec3,
max: Vec3,
}
pub struct NavMesh3D {
state: Option<NavState>,
}
struct NavState {
mesh: detour::NavMesh,
settings: NavBakeSettings,
polygons: Vec<PolygonBounds>,
obstacles: Vec<NavObstacle3D>,
scratch: Vec<NavObstacle3D>,
revision: u64,
crowd: Option<crowd::Crowd>,
crowd_revision: u64,
crowd_seen: Vec<bool>,
crowd_owners: Vec<Option<Entity>>,
crowd_workspace: Workspace,
cache: Option<tilecache::TileCache>,
carve_handles: Vec<tilecache::ObstacleRef>,
carved: Vec<NavObstacle3D>,
pending: bool,
carve_failed: bool,
}
impl NavMesh3D {
pub fn bake(sources: &[NavSource], settings: NavBakeSettings) -> Result<Self, NavBakeError> {
NavState::bake(sources, settings).map(|state| Self { state: Some(state) })
}
#[must_use]
pub fn sample_position(&self, point: Vec3, max_distance: f32) -> Option<Vec3> {
self.state.as_ref()?.sample_position(point, max_distance)
}
#[must_use]
pub fn find_path(&self, from: Vec3, to: Vec3) -> Option<Vec<Vec3>> {
self.state.as_ref()?.find_path(from, to)
}
pub fn set_obstacles(&mut self, obstacles: &[NavObstacle3D]) {
if let Some(state) = &mut self.state {
state.set_obstacles(obstacles);
}
}
pub fn update_carving(&mut self, budget: usize) -> Result<usize, NavBakeError> {
self.state.as_mut().map_or(Ok(0), |s| s.update_carving(budget))
}
pub fn carving_pending(&self) -> bool {
self.state.as_ref().is_some_and(|s| s.pending || s.carving_changed())
}
}
impl NavState {
fn bake(sources: &[NavSource], settings: NavBakeSettings) -> Result<Self, NavBakeError> {
let s = settings;
if ![
s.agent_radius,
s.agent_height,
s.max_climb,
s.max_slope,
s.cell_size,
s.cell_height,
]
.iter()
.all(|x| x.is_finite())
|| s.agent_radius < 0.0
|| s.agent_height <= 0.0
|| s.max_climb < 0.0
|| !(0.0..90.0).contains(&s.max_slope)
|| s.cell_size <= 0.0
|| s.cell_height <= 0.0
|| !(8..=255).contains(&s.tile_size)
{
return Err(error("invalid navigation bake settings"));
}
let mut verts = Vec::new();
let mut tris = Vec::new();
for source in sources {
for triangle in &source.triangles {
for p in triangle {
if !p.is_finite() {
return Err(error("nonfinite source triangle"));
}
tris.push(i32::try_from(verts.len() / 3).map_err(error)?);
verts.extend(p.to_array());
}
}
}
if tris.is_empty() {
return Err(error("no source triangles"));
}
let mut config = bake::BakeConfig::default();
config.recast.cs = s.cell_size;
config.recast.ch = s.cell_height;
config.recast.walkable_height = (s.agent_height / s.cell_height).ceil() as i32;
config.recast.walkable_radius = (s.agent_radius / s.cell_size).ceil() as i32;
config.recast.walkable_climb = (s.max_climb / s.cell_height).floor() as i32;
config.recast.walkable_slope_angle = s.max_slope;
config.recast.min_region_area = 0;
config.recast.merge_region_area = 0;
config.recast.detail_sample_dist = s.cell_size * 6.0;
config.recast.detail_sample_max_error = s.cell_height;
config.walkable_height = s.agent_height;
config.walkable_radius = s.agent_radius;
config.walkable_climb = s.max_climb;
config.strict_triangulation = true;
let grid = carve::TileGrid::new(&verts, &s)?;
config.recast.tile_size = grid.tile_size as i32;
config.tile_limits.max_bytes = TILE_WORKING_BYTES;
config.tile_limits.max_work = TILE_WORK;
config.tile_limits.max_tile_bytes = grid.max_tile_bytes();
config.max_total_bytes = SETUP_BYTES + grid.count * config.tile_limits.max_tile_bytes;
let (mesh, cache) = if s.carving {
let (mesh, cache) = carve::bake_layers(&verts, &tris, &config, grid.layout())?;
(mesh, Some(cache))
} else {
let baked = bake::tiles(&bake::Input::new(&verts, &tris), &config, &mut Workspace::new())
.map_err(|e| bake_error(&e, &s))?;
(
baked.into_navmesh(Limits::default()).map_err(|e| bake_error(&e, &s))?,
None,
)
};
let mut polygons = Vec::new();
for i in 0..mesh.max_tiles() as usize {
let Some(tile) = mesh.tile(i) else {
continue;
};
let base = mesh.poly_ref_base(i).expect("resident tile has reference");
for (j, poly) in tile.polys.iter().enumerate() {
let mut min = Vec3::splat(f32::INFINITY);
let mut max = Vec3::splat(f32::NEG_INFINITY);
for &v in &poly.verts[..poly.vert_count as usize] {
let at = v as usize * 3;
let p = Vec3::from_slice(&tile.verts[at..at + 3]);
min = min.min(p);
max = max.max(p);
}
polygons.push(PolygonBounds {
reference: base | j as detour::PolyRef,
min,
max,
});
}
}
Ok(Self {
mesh,
settings,
polygons,
obstacles: Vec::new(),
scratch: Vec::new(),
revision: NEXT_REVISION.fetch_add(1, Ordering::Relaxed),
crowd: None,
crowd_revision: 0,
crowd_seen: Vec::new(),
crowd_owners: Vec::new(),
crowd_workspace: Workspace::new(),
cache,
carve_handles: Vec::new(),
carved: Vec::new(),
pending: false,
carve_failed: false,
})
}
fn sample_position(&self, point: Vec3, max_distance: f32) -> Option<Vec3> {
if !point.is_finite() || !max_distance.is_finite() || max_distance < 0.0 {
return None;
}
let query = detour::NavMeshQuery::new(&self.mesh, 1).ok()?;
let (r, p, _) = query
.find_nearest_poly(&point.to_array(), &[max_distance; 3], &detour::DefaultFilter::default())
.ok()?;
let p = Vec3::from_array(p);
(r != 0 && p.distance(point) <= max_distance).then_some(p)
}
fn find_path(&self, from: Vec3, to: Vec3) -> Option<Vec<Vec3>> {
if !from.is_finite() || !to.is_finite() {
return None;
}
let mut query = detour::NavMeshQuery::new(&self.mesh, 8192).ok()?;
let s = self.settings;
let ext = [s.cell_size * 2.0, s.max_climb + s.cell_height * 2.0, s.cell_size * 2.0];
let filter = detour::DefaultFilter::default();
let (a, pa, _) = query.find_nearest_poly(&from.to_array(), &ext, &filter).ok()?;
let (b, pb, _) = query.find_nearest_poly(&to.to_array(), &ext, &filter).ok()?;
if a == 0 || b == 0 {
return None;
}
let mut path = Vec::new();
let status = query.find_path(a, b, &pa, &pb, &filter, &mut path, 8192).ok()?;
if status.has_detail(
detour::Status::PARTIAL_RESULT | detour::Status::OUT_OF_NODES | detour::Status::BUFFER_TOO_SMALL,
) || path.last() != Some(&b)
{
return None;
}
let mut corners = Vec::new();
query.find_straight_path(&pa, &pb, &path, &mut corners, 8192, 0).ok()?;
if corners.last()?.flags & detour::STRAIGHTPATH_END == 0 {
return None;
}
Some(corners.into_iter().map(|p| Vec3::from_array(p.pos)).collect())
}
fn agent_route(&self, from: Vec3, to: Vec3) -> Option<Vec<Vec3>> {
let corners = self.find_path(from, to)?;
let mut query = detour::NavMeshQuery::new(&self.mesh, 64).ok()?;
let filter = detour::DefaultFilter::default();
let (mut poly, mut position, _) = query
.find_nearest_poly(&corners.first()?.to_array(), &[self.settings.cell_size; 3], &filter)
.ok()?;
if poly == 0 {
return None;
}
let mut route = vec![Vec3::from_array(position)];
let mut visited = Vec::with_capacity(64);
for segment in corners.windows(2) {
let count = (segment[0].distance(segment[1]) / self.settings.cell_size)
.ceil()
.max(1.0) as usize;
if count > 1_000_000usize.saturating_sub(route.len()) {
return None;
}
for i in 1..=count {
let target = segment[0].lerp(segment[1], i as f32 / count as f32);
let (mut next, status) = query
.move_along_surface(poly, &position, &target.to_array(), &filter, &mut visited, 64)
.ok()?;
if status.has_detail(detour::Status::BUFFER_TOO_SMALL | detour::Status::OUT_OF_NODES) {
return None;
}
poly = *visited.last()?;
next = query.closest_point_on_poly(poly, &next).ok()?.0;
let point = Vec3::from_array(next);
if (point - target).with_y(0.0).length() > self.settings.cell_size {
return None;
}
route.push(point);
position = next;
}
}
Some(route)
}
fn set_obstacles(&mut self, obstacles: &[NavObstacle3D]) {
if self.obstacles == obstacles {
return;
}
self.obstacles.clear();
self.obstacles.extend_from_slice(obstacles);
self.apply_obstacles();
}
fn apply_obstacles(&mut self) {
let radius = Vec3::new(self.settings.agent_radius, 0.0, self.settings.agent_radius);
for poly in &self.polygons {
let blocked = self.carve_failed
|| self.obstacles.iter().any(|o| {
!(o.carve && self.cache.is_some())
&& (!o.center.is_finite()
|| !o.half_extents.is_finite()
|| o.half_extents.min_element() < 0.0
|| {
let min = o.center - o.half_extents - radius;
let max = o.center + o.half_extents + radius;
poly.max.cmpge(min - Vec3::Y * self.settings.agent_height).all()
&& poly.min.cmple(max).all()
})
});
self.mesh
.set_poly_flags(poly.reference, u16::from(!blocked))
.expect("cached polygon reference");
}
self.revision = NEXT_REVISION.fetch_add(1, Ordering::Relaxed);
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NavObstacle3D {
pub center: Vec3,
pub half_extents: Vec3,
pub carve: bool,
}
impl NavObstacle3D {
#[must_use]
pub fn new(center: Vec3, half_extents: Vec3) -> Self {
Self {
center,
half_extents,
carve: false,
}
}
}
#[derive(Clone, Debug)]
pub struct NavAgent3D {
pub speed: f32,
pub radius: f32,
pub acceleration: f32,
pub destination: Option<Vec3>,
pub avoidance: bool,
pub avoidance_weight: f32,
pub neighbour_distance: f32,
crowd_id: Option<crowd::AgentId>,
crowd_revision: u64,
path: Vec<Vec3>,
corner: usize,
revision: u64,
planned_destination: Option<Vec3>,
expected_position: Vec3,
velocity: f32,
}
impl Default for NavAgent3D {
fn default() -> Self {
Self {
speed: 3.0,
radius: 0.3,
acceleration: 10.0,
destination: None,
avoidance: false,
avoidance_weight: 2.0,
neighbour_distance: 3.6,
crowd_id: None,
crowd_revision: 0,
path: Vec::new(),
corner: 0,
revision: 0,
planned_destination: None,
expected_position: Vec3::NAN,
velocity: 0.0,
}
}
}
pub fn step_navmesh3d(ctx: &mut Ctx) {
let dt = ctx.dt();
if !dt.is_finite() || dt <= 0.0 {
return;
}
let Some(mut nav) = ctx
.world
.get_resource_mut::<NavMesh3D>()
.and_then(|nav| nav.state.take())
else {
return;
};
nav.scratch.clear();
ctx.world.query::<&NavObstacle3D>().for_each(|o| nav.scratch.push(*o));
if nav.scratch != nav.obstacles {
let obstacles = std::mem::take(&mut nav.scratch);
nav.set_obstacles(&obstacles);
nav.scratch = obstacles;
}
if nav.update_carving(1).is_err() {
nav.carve_failed = true;
nav.apply_obstacles();
}
step_crowd(ctx, &mut nav, dt);
ctx.world
.query::<(&mut Transform3D, &mut NavAgent3D)>()
.for_each(|(t, a)| {
if a.avoidance {
return;
}
a.crowd_id = None;
let valid = a.radius.is_finite()
&& a.radius >= 0.0
&& a.radius <= nav.settings.agent_radius
&& a.speed.is_finite()
&& a.speed > 0.0
&& a.acceleration.is_finite()
&& a.acceleration > 0.0
&& a.destination.is_some_and(|p| p.is_finite())
&& t.position.is_finite();
if !valid || a.destination.is_none() {
a.velocity = 0.0;
a.revision = 0;
return;
}
if a.revision != nav.revision || a.destination != a.planned_destination || t.position != a.expected_position
{
a.path.clear();
a.corner = 0;
a.velocity = 0.0;
if let Some(path) = nav.agent_route(t.position, a.destination.unwrap()) {
a.path.extend(path);
}
a.revision = nav.revision;
a.planned_destination = a.destination;
}
a.velocity = (a.velocity + a.acceleration * dt).min(a.speed);
let mut remaining = a.velocity * dt;
while let Some(&target) = a.path.get(a.corner) {
let delta = target - t.position;
let distance = delta.length();
if distance <= remaining {
t.position = target;
remaining -= distance;
a.corner += 1;
} else {
t.position += delta / distance * remaining;
break;
}
}
if a.corner == a.path.len() {
a.velocity = 0.0;
}
a.expected_position = t.position;
});
ctx.world.resource_mut::<NavMesh3D>().state = Some(nav);
}
fn step_crowd(ctx: &mut Ctx, nav: &mut NavState, dt: f32) {
let count = ctx.world.query::<&NavAgent3D>().iter().filter(|a| a.avoidance).count();
if count == 0 && nav.crowd.is_none() {
return;
}
let capacity = nav.crowd.as_ref().map_or(0, crowd::Crowd::max_agents);
if nav.crowd_revision != nav.revision || count.min(16383) > capacity {
let size = count.max(16).next_power_of_two().min(16383);
nav.crowd = crowd::Crowd::new(
&mut nav.crowd_workspace,
&nav.mesh,
size,
(nav.settings.agent_radius * 1.3).max(0.001),
)
.ok();
if let Some(c) = &mut nav.crowd {
c.set_fixes(navmesh_rs::UpstreamFixes::ALL);
}
nav.crowd_seen.resize(size, false);
nav.crowd_owners.clear();
nav.crowd_owners.resize(size, None);
nav.crowd_revision = nav.revision;
ctx.world.query::<&mut NavAgent3D>().for_each(|a| a.crowd_id = None);
}
let Some(c) = &mut nav.crowd else {
return;
};
nav.crowd_seen.fill(false);
ctx.world
.query::<(Entity, &mut Transform3D, &mut NavAgent3D)>()
.for_each(|(entity, t, a)| {
if !a.avoidance {
return;
}
a.revision = 0;
if a.crowd_id
.is_some_and(|id| nav.crowd_owners.get(id.0) != Some(&Some(entity)))
{
a.crowd_id = None;
}
if a.crowd_revision != nav.crowd_revision {
a.crowd_id = None;
}
let valid = t.position.is_finite()
&& a.radius.is_finite()
&& a.radius > 0.0
&& a.radius <= nav.settings.agent_radius
&& a.speed.is_finite()
&& a.speed > 0.0
&& a.acceleration.is_finite()
&& a.acceleration > 0.0
&& a.avoidance_weight.is_finite()
&& a.avoidance_weight >= 0.0
&& a.neighbour_distance.is_finite()
&& a.neighbour_distance > 0.0;
if !valid {
a.crowd_id = None;
return;
}
if t.position != a.expected_position
&& let Some(id) = a.crowd_id.take()
{
let _ = c.remove_agent(id);
}
let mut p = crowd::AgentParams::default();
p.radius = a.radius * 1.3;
p.height = nav.settings.agent_height;
p.max_speed = a.speed;
p.max_acceleration = a.acceleration;
p.collision_query_range = a.neighbour_distance;
p.separation_weight = a.avoidance_weight;
let fresh = a.crowd_id.is_none();
if fresh {
a.crowd_id = c
.add_agent(&mut nav.crowd_workspace, &nav.mesh, t.position.to_array(), p)
.ok();
}
let Some(id) = a.crowd_id else {
return;
};
nav.crowd_seen[id.0] = true;
nav.crowd_owners[id.0] = Some(entity);
let _ = c.update_agent_parameters(id, p);
if fresh || a.destination != a.planned_destination {
let _ = c.reset_move_target(id);
if let Some(to) = a.destination.filter(|to| nav.mesh.max_tiles() > 0 && to.is_finite()) {
let query = detour::NavMeshQuery::new(&nav.mesh, 8192).ok();
if let Some(mut q) = query {
let ext = [
nav.settings.cell_size * 2.0,
nav.settings.max_climb + nav.settings.cell_height * 2.0,
nav.settings.cell_size * 2.0,
];
let filter = detour::DefaultFilter::default();
if let (Ok((from_ref, from, _)), Ok((to_ref, target, _))) = (
q.find_nearest_poly(&t.position.to_array(), &ext, &filter),
q.find_nearest_poly(&to.to_array(), &ext, &filter),
) {
let mut path = Vec::new();
if from_ref != 0
&& to_ref != 0
&& q.find_path(from_ref, to_ref, &from, &target, &filter, &mut path, 8192)
.is_ok_and(|s| {
!s.has_detail(
detour::Status::PARTIAL_RESULT
| detour::Status::OUT_OF_NODES
| detour::Status::BUFFER_TOO_SMALL,
)
})
&& path.last() == Some(&to_ref)
{
let _ = c.request_move_target(&nav.mesh, id, to_ref, target);
}
}
}
}
a.planned_destination = a.destination;
}
a.crowd_revision = nav.crowd_revision;
});
for (i, seen) in nav.crowd_seen.iter().enumerate() {
if !seen && c.agent(crowd::AgentId(i)).is_some() {
let _ = c.remove_agent(crowd::AgentId(i));
nav.crowd_owners[i] = None;
}
}
let steps = (dt / (1.0 / 60.0)).ceil().clamp(1.0, 64.0) as u32;
for _ in 0..steps {
if c.update(&mut nav.crowd_workspace, &nav.mesh, dt / steps as f32)
.is_err()
{
break;
}
}
ctx.world
.query::<(Entity, &mut Transform3D, &mut NavAgent3D)>()
.for_each(|(entity, t, a)| {
if a.avoidance
&& let Some(agent) = a
.crowd_id
.filter(|id| nav.crowd_owners[id.0] == Some(entity))
.and_then(|id| c.agent(id))
{
t.position = Vec3::from_array(agent.npos);
a.expected_position = t.position;
}
});
}