use glam::{UVec2, Vec2};
use rayon::prelude::*;
#[derive(Debug, Clone, PartialEq)]
pub struct NavGrid {
size: UVec2,
cell_size: f32,
pub origin: Vec2,
costs: Vec<u8>,
}
impl NavGrid {
pub const WALL: u8 = 0;
#[track_caller]
#[must_use]
pub fn new(width: u32, height: u32, cell_size: f32) -> Self {
assert!(
width > 0 && height > 0 && (width as u64) * (height as u64) <= 1 << 28,
"NavGrid::new: {width} x {height} cells (each side at least 1, at most 2^28 cells)"
);
assert!(
cell_size > 0.0 && cell_size.is_finite(),
"NavGrid::new: cell_size must be positive, got {cell_size}"
);
Self {
size: UVec2::new(width, height),
cell_size,
origin: Vec2::ZERO,
costs: vec![1; width as usize * height as usize],
}
}
#[must_use]
pub fn size(&self) -> UVec2 {
self.size
}
#[must_use]
pub fn cell_size(&self) -> f32 {
self.cell_size
}
pub fn set_cost(&mut self, x: u32, y: u32, cost: u8) {
if x < self.size.x && y < self.size.y {
self.costs[(y * self.size.x + x) as usize] = cost;
}
}
#[must_use]
pub fn cost(&self, x: u32, y: u32) -> Option<u8> {
(x < self.size.x && y < self.size.y).then(|| self.costs[(y * self.size.x + x) as usize])
}
pub fn costs_mut(&mut self) -> &mut [u8] {
&mut self.costs
}
#[must_use]
pub fn walkable(&self, world: Vec2) -> bool {
self.cell_at(world)
.is_some_and(|c| self.costs[self.index(c)] != Self::WALL)
}
#[must_use]
pub fn cell_at(&self, world: Vec2) -> Option<UVec2> {
let c = ((world - self.origin) / self.cell_size).floor();
(c.x >= 0.0 && c.y >= 0.0 && c.x < self.size.x as f32 && c.y < self.size.y as f32)
.then(|| UVec2::new(c.x as u32, c.y as u32))
}
#[must_use]
pub fn center_of(&self, cell: UVec2) -> Vec2 {
self.origin + (cell.as_vec2() + 0.5) * self.cell_size
}
fn index(&self, cell: UVec2) -> usize {
(cell.y * self.size.x + cell.x) as usize
}
}
const STEPS: [(i32, i32); 8] = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)];
const S: f32 = std::f32::consts::FRAC_1_SQRT_2;
const DIRS: [Vec2; 9] = [
Vec2::new(1.0, 0.0),
Vec2::new(S, S),
Vec2::new(0.0, 1.0),
Vec2::new(-S, S),
Vec2::new(-1.0, 0.0),
Vec2::new(-S, -S),
Vec2::new(0.0, -1.0),
Vec2::new(S, -S),
Vec2::ZERO,
];
const NONE: u8 = 8;
const STRAIGHT: u32 = 10;
const DIAGONAL: u32 = 14;
const UNREACHED: u32 = u32::MAX;
#[derive(Debug, Clone, Default)]
pub struct FlowField {
size: UVec2,
cell_size: f32,
origin: Vec2,
stride: usize,
costs: Vec<u8>,
dist: Vec<u32>,
dir: Vec<u8>,
buckets: Vec<Vec<u32>>,
}
impl FlowField {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn compute(&mut self, nav: &NavGrid, goals: &[Vec2]) {
let (w, h) = (nav.size.x as usize, nav.size.y as usize);
let stride = w + 2;
let n = stride * (h + 2);
self.size = nav.size;
self.cell_size = nav.cell_size;
self.origin = nav.origin;
self.stride = stride;
self.costs.clear();
self.costs.resize(n, NavGrid::WALL);
for (row, src) in self.costs[stride..].chunks_mut(stride).zip(nav.costs.chunks(w)) {
row[1..=w].copy_from_slice(src);
}
self.dist.clear();
self.dist.resize(n, UNREACHED);
self.dir.clear();
self.dir.resize(n, NONE);
let s = stride as isize;
let off: [isize; 8] = STEPS.map(|(dx, dy)| dy as isize * s + dx as isize);
let sides: [(isize, isize); 8] = STEPS.map(|(dx, dy)| (dx as isize, dy as isize * s));
let ring = ((DIAGONAL * 255 + 1) as usize).next_power_of_two();
let mask = ring - 1;
if self.buckets.len() != ring {
self.buckets = (0..ring).map(|_| Vec::new()).collect();
}
for b in &mut self.buckets {
b.clear();
}
let mut pending = 0usize;
for &g in goals {
if let Some(c) = nav.cell_at(g) {
let i = (c.y as usize + 1) * stride + c.x as usize + 1;
if self.costs[i] != NavGrid::WALL && self.dist[i] != 0 {
self.dist[i] = 0;
self.buckets[0].push(i as u32);
pending += 1;
}
}
}
let costs = &self.costs;
let dist = &mut self.dist;
let mut d = 0u32;
while pending > 0 {
let slot = d as usize & mask;
while let Some(i) = self.buckets[slot].pop() {
pending -= 1;
let i = i as usize;
if dist[i] != d {
continue; }
for k in 0..8 {
let j = (i as isize + off[k]) as usize;
let cost = costs[j] as u32;
if cost == 0 {
continue;
}
let step = if k % 2 == 1 {
let (sx, sy) = sides[k];
if costs[(i as isize + sx) as usize] == 0 || costs[(i as isize + sy) as usize] == 0 {
continue;
}
DIAGONAL
} else {
STRAIGHT
};
let nd = d + step * cost;
if nd < dist[j] {
dist[j] = nd;
self.buckets[nd as usize & mask].push(j as u32);
pending += 1;
}
}
}
d += 1;
}
let dist = &self.dist;
self.dir[stride..n - stride]
.par_chunks_mut(stride)
.enumerate()
.for_each(|(y, row)| {
let base = (y + 1) * stride;
for (x, out) in row.iter_mut().enumerate().take(w + 1).skip(1) {
let i = base + x;
let here = dist[i];
if here == 0 || here == UNREACHED {
continue;
}
let mut best = (here, NONE);
for k in 0..8 {
if k % 2 == 1 {
let (sx, sy) = sides[k];
if costs[(i as isize + sx) as usize] == 0 || costs[(i as isize + sy) as usize] == 0 {
continue;
}
}
let there = dist[(i as isize + off[k]) as usize];
if there < best.0 {
best = (there, k as u8);
}
}
*out = best.1;
}
});
}
#[doc(hidden)]
#[must_use]
pub fn raw_view(&self) -> RawFlowView<'_> {
RawFlowView {
dir: &self.dir,
directions: &DIRS,
size: self.size,
stride: self.stride,
origin: self.origin,
cell_size: self.cell_size,
}
}
fn cell(&self, world: Vec2) -> Option<usize> {
if self.dir.is_empty() {
return None;
}
let c = ((world - self.origin) / self.cell_size).floor();
(c.x >= 0.0 && c.y >= 0.0 && c.x < self.size.x as f32 && c.y < self.size.y as f32)
.then(|| (c.y as usize + 1) * self.stride + c.x as usize + 1)
}
#[inline]
#[must_use]
pub fn direction_at(&self, world: Vec2) -> Vec2 {
self.cell(world).map_or(Vec2::ZERO, |i| DIRS[self.dir[i] as usize])
}
#[inline]
#[must_use]
pub fn distance_at(&self, world: Vec2) -> Option<f32> {
let d = self.dist[self.cell(world)?];
(d != UNREACHED).then(|| d as f32 / STRAIGHT as f32 * self.cell_size)
}
}
#[doc(hidden)]
pub struct RawFlowView<'a> {
pub dir: &'a [u8],
pub directions: &'static [Vec2; 9],
pub size: UVec2,
pub stride: usize,
pub origin: Vec2,
pub cell_size: f32,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagonal_steps_cost_about_root_two() {
let nav = NavGrid::new(11, 11, 1.0);
let mut f = FlowField::new();
f.compute(&nav, &[vec(0.5, 0.5)]);
let at = |x: usize, y: usize| (y + 1) * f.stride + x + 1;
assert_eq!(f.dist[at(10, 10)], 10 * DIAGONAL);
assert_eq!(f.dist[at(10, 0)], 10 * STRAIGHT);
assert_eq!(f.dir[at(1, 1)], 5, "(1, 1) points north-west, at the goal");
}
#[test]
fn diagonals_never_cut_corners() {
let mut nav = NavGrid::new(2, 2, 1.0);
nav.set_cost(1, 0, NavGrid::WALL);
nav.set_cost(0, 0, NavGrid::WALL);
let mut f = FlowField::new();
f.compute(&nav, &[vec(0.5, 1.5)]);
let at = (1 + 1) * f.stride + 1 + 1;
assert_eq!(f.dir[at], 4);
assert_eq!(f.dist[at], STRAIGHT);
}
#[test]
fn recomputing_reuses_memory() {
let nav = NavGrid::new(64, 64, 1.0);
let mut f = FlowField::new();
f.compute(&nav, &[vec(1.0, 1.0)]);
let ptrs = (f.dist.as_ptr(), f.dir.as_ptr(), f.buckets[0].capacity());
f.compute(&nav, &[vec(60.0, 60.0)]);
assert_eq!((f.dist.as_ptr(), f.dir.as_ptr()), (ptrs.0, ptrs.1));
assert!(f.buckets[0].capacity() >= ptrs.2);
}
fn vec(x: f32, y: f32) -> Vec2 {
Vec2::new(x, y)
}
}