1use crate::mesh::NavMesh;
14use crate::NavError;
15use glam::{Vec2, Vec3};
16use std::collections::HashMap;
17
18pub const VOXEL: f32 = 1.0;
20
21pub trait Source2d {
23 fn is_solid(&self, cell: [i32; 2]) -> bool;
25}
26
27pub trait Source3d {
30 fn is_solid(&self, cell: [i32; 3]) -> bool;
32 fn surface_top(&self, cell: [i32; 3], wx: f32, wz: f32) -> Option<f32>;
34}
35
36impl<Q: repame_physics::VoxelQuery> Source3d for Q {
37 fn is_solid(&self, cell: [i32; 3]) -> bool {
38 repame_physics::VoxelQuery::is_solid(self, cell)
39 }
40 fn surface_top(&self, cell: [i32; 3], wx: f32, wz: f32) -> Option<f32> {
41 repame_physics::VoxelQuery::surface_top(self, cell, wx, wz)
42 }
43}
44
45#[derive(Clone, Copy, Debug)]
47pub struct Bake2d {
48 pub min: [i32; 2],
50 pub max: [i32; 2],
52 pub cell_size: f32,
54 pub agent_radius: f32,
57}
58
59impl Default for Bake2d {
60 fn default() -> Self {
61 Self {
62 min: [0, 0],
63 max: [0, 0],
64 cell_size: 1.0,
65 agent_radius: 0.4,
66 }
67 }
68}
69
70#[derive(Clone, Copy, Debug)]
73pub struct Bake3d {
74 pub min: [i32; 3],
76 pub max: [i32; 3],
78 pub agent_height: f32,
80 pub agent_radius: f32,
83 pub max_slope_deg: f32,
85}
86
87impl Default for Bake3d {
88 fn default() -> Self {
89 Self {
90 min: [0, 0, 0],
91 max: [0, 0, 0],
92 agent_height: 1.8,
93 agent_radius: 0.4,
94 max_slope_deg: 45.0,
95 }
96 }
97}
98
99pub fn bake_2d<S: Source2d>(source: &S, config: &Bake2d) -> Result<NavMesh<Vec2>, NavError> {
104 if config.max[0] <= config.min[0] || config.max[1] <= config.min[1] {
105 return Err(NavError::EmptyRegion);
106 }
107 if config.cell_size <= 0.0 {
108 return Err(NavError::InvalidConfig);
109 }
110 let radius_cells = (config.agent_radius / config.cell_size).ceil() as i32;
111 let walkable = |x: i32, z: i32| -> bool {
112 if source.is_solid([x, z]) {
113 return false;
114 }
115 for dx in -radius_cells..=radius_cells {
116 for dz in -radius_cells..=radius_cells {
117 if dx * dx + dz * dz <= radius_cells * radius_cells && source.is_solid([x + dx, z + dz])
118 {
119 return false;
120 }
121 }
122 }
123 true
124 };
125
126 let mut vertices: Vec<Vec2> = Vec::new();
127 let mut corners: HashMap<(i32, i32), u32> = HashMap::new();
128 let mut polygons: Vec<Vec<u32>> = Vec::new();
129 let scale = config.cell_size;
130 let vertex = |x: i32, z: i32, vertices: &mut Vec<Vec2>, corners: &mut HashMap<(i32, i32), u32>| {
131 *corners.entry((x, z)).or_insert_with(|| {
132 vertices.push(Vec2::new(x as f32 * scale, z as f32 * scale));
133 (vertices.len() - 1) as u32
134 })
135 };
136
137 for x in config.min[0]..config.max[0] {
138 for z in config.min[1]..config.max[1] {
139 if !walkable(x, z) {
140 continue;
141 }
142 let c0 = vertex(x, z, &mut vertices, &mut corners);
143 let c1 = vertex(x + 1, z, &mut vertices, &mut corners);
144 let c2 = vertex(x + 1, z + 1, &mut vertices, &mut corners);
145 let c3 = vertex(x, z + 1, &mut vertices, &mut corners);
146 polygons.push(vec![c0, c1, c2, c3]);
147 }
148 }
149 if polygons.is_empty() {
150 return Err(NavError::NoWalkableArea);
151 }
152 NavMesh::from_polygons(vertices, polygons)
153}
154
155pub fn bake_3d<S: Source3d>(source: &S, config: &Bake3d) -> Result<NavMesh<Vec3>, NavError> {
157 if config.max[0] <= config.min[0] || config.max[1] <= config.min[1] || config.max[2] <= config.min[2]
158 {
159 return Err(NavError::EmptyRegion);
160 }
161 let radius_cells = (config.agent_radius / VOXEL).ceil() as i32;
162 let margin = radius_cells + 1;
163 let clearance_cells = config.agent_height.ceil() as i32;
164
165 let scan_min = [config.min[0] - margin, config.min[2] - margin];
166 let scan_max = [config.max[0] + margin, config.max[2] + margin];
167 let width = (scan_max[0] - scan_min[0] + 1) as usize;
168 let height = (scan_max[1] - scan_min[1] + 1) as usize;
169
170 let mut ground: Vec<Option<[i32; 3]>> = vec![None; width * height];
171 let index = |x: i32, z: i32| -> usize {
172 ((x - scan_min[0]) as usize) + ((z - scan_min[1]) as usize) * width
173 };
174
175 for x in scan_min[0]..=scan_max[0] {
176 for z in scan_min[1]..=scan_max[1] {
177 let mut found = None;
178 for y in (config.min[1]..config.max[1]).rev() {
179 if !source.is_solid([x, y, z]) {
180 continue;
181 }
182 let mut clear = true;
183 for k in 1..=clearance_cells {
184 if source.is_solid([x, y + k, z]) {
185 clear = false;
186 break;
187 }
188 }
189 if clear {
190 found = Some([x, y, z]);
191 break;
192 }
193 }
194 ground[index(x, z)] = found;
195 }
196 }
197
198 let column = |x: i32, z: i32| -> Option<[i32; 3]> {
199 if x < scan_min[0] || z < scan_min[1] || x > scan_max[0] || z > scan_max[1] {
200 None
201 } else {
202 ground[index(x, z)]
203 }
204 };
205 let corner_height = |x: i32, z: i32| -> Option<f32> {
206 column(x, z)
207 .map(|cell| source.surface_top(cell, x as f32, z as f32).unwrap_or((cell[1] + 1) as f32))
208 };
209 let erode = |x: i32, z: i32| -> bool {
210 if column(x, z).is_none() {
211 return false;
212 }
213 for dx in -radius_cells..=radius_cells {
214 for dz in -radius_cells..=radius_cells {
215 if dx * dx + dz * dz <= radius_cells * radius_cells && column(x + dx, z + dz).is_none() {
216 return false;
217 }
218 }
219 }
220 true
221 };
222
223 let cos_limit = config.max_slope_deg.to_radians().cos();
224 let mut vertices: Vec<Vec3> = Vec::new();
225 let mut corners: HashMap<(i32, i32), u32> = HashMap::new();
226 let mut polygons: Vec<Vec<u32>> = Vec::new();
227
228 for x in config.min[0]..config.max[0] {
229 for z in config.min[2]..config.max[2] {
230 if !erode(x, z) {
231 continue;
232 }
233 let (Some(h00), Some(h10), Some(h11), Some(h01)) = (
234 corner_height(x, z),
235 corner_height(x + 1, z),
236 corner_height(x + 1, z + 1),
237 corner_height(x, z + 1),
238 ) else {
239 continue;
240 };
241 let p00 = Vec3::new(x as f32, h00, z as f32);
242 let p10 = Vec3::new((x + 1) as f32, h10, z as f32);
243 let p11 = Vec3::new((x + 1) as f32, h11, (z + 1) as f32);
244 let p01 = Vec3::new(x as f32, h01, (z + 1) as f32);
245 let normal = (p10 - p00).cross(p11 - p00) + (p11 - p00).cross(p01 - p00);
246 let length = normal.length();
247 if length <= 1e-6 {
248 continue;
249 }
250 if normal.y.abs() / length < cos_limit {
251 continue;
252 }
253 let key = |x: i32, z: i32, vertices: &mut Vec<Vec3>, corners: &mut HashMap<(i32, i32), u32>| {
254 *corners.entry((x, z)).or_insert_with(|| {
255 let height = corner_height(x, z).unwrap_or(0.0);
256 vertices.push(Vec3::new(x as f32, height, z as f32));
257 (vertices.len() - 1) as u32
258 })
259 };
260 let c0 = key(x, z, &mut vertices, &mut corners);
261 let c1 = key(x + 1, z, &mut vertices, &mut corners);
262 let c2 = key(x + 1, z + 1, &mut vertices, &mut corners);
263 let c3 = key(x, z + 1, &mut vertices, &mut corners);
264 polygons.push(vec![c0, c1, c2, c3]);
265 }
266 }
267 if polygons.is_empty() {
268 return Err(NavError::NoWalkableArea);
269 }
270 NavMesh::from_polygons(vertices, polygons)
271}