1use crate::collider::Contact;
7use crate::math::{Fix128, Vec3Fix};
8
9#[cfg(not(feature = "std"))]
10use alloc::vec;
11#[cfg(not(feature = "std"))]
12use alloc::vec::Vec;
13
14#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct HeightField {
20 pub heights: Vec<Fix128>,
22 pub width: u32,
24 pub depth: u32,
26 pub spacing: Fix128,
28 pub origin: Vec3Fix,
30}
31
32impl HeightField {
33 #[must_use]
35 pub fn new(
36 heights: Vec<Fix128>,
37 width: u32,
38 depth: u32,
39 spacing: Fix128,
40 origin: Vec3Fix,
41 ) -> Self {
42 debug_assert_eq!(heights.len(), (width * depth) as usize);
43 Self {
44 heights,
45 width,
46 depth,
47 spacing,
48 origin,
49 }
50 }
51
52 #[must_use]
54 pub fn flat(width: u32, depth: u32, spacing: Fix128, origin: Vec3Fix, height: Fix128) -> Self {
55 let heights = vec![height; (width * depth) as usize];
56 Self::new(heights, width, depth, spacing, origin)
57 }
58
59 #[inline]
61 #[must_use]
62 pub fn get_height(&self, gx: u32, gz: u32) -> Fix128 {
63 if self.width == 0 || self.depth == 0 {
64 return Fix128::ZERO;
66 }
67 let gx = gx.min(self.width - 1);
68 let gz = gz.min(self.depth - 1);
69 self.heights[(gx + gz * self.width) as usize]
70 }
71
72 #[inline]
74 pub fn set_height(&mut self, gx: u32, gz: u32, height: Fix128) {
75 if gx < self.width && gz < self.depth {
76 self.heights[(gx + gz * self.width) as usize] = height;
77 }
78 }
79
80 fn world_to_grid(&self, world_pos: Vec3Fix) -> (Fix128, Fix128) {
82 let local_x = world_pos.x - self.origin.x;
83 let local_z = world_pos.z - self.origin.z;
84
85 if self.spacing.is_zero() {
86 return (Fix128::ZERO, Fix128::ZERO);
87 }
88
89 (local_x / self.spacing, local_z / self.spacing)
90 }
91
92 #[must_use]
94 pub fn sample_height(&self, world_x: Fix128, world_z: Fix128) -> Fix128 {
95 let local_x = world_x - self.origin.x;
96 let local_z = world_z - self.origin.z;
97
98 if self.spacing.is_zero() {
99 return Fix128::ZERO;
100 }
101
102 let gx_f = local_x / self.spacing;
103 let gz_f = local_z / self.spacing;
104
105 let max_gx = (self.width as i64).saturating_sub(2).max(0);
107 let max_gz = (self.depth as i64).saturating_sub(2).max(0);
108 let gx0 = if gx_f.is_negative() {
109 0u32
110 } else {
111 (gx_f.hi.min(max_gx).max(0)) as u32
112 };
113 let gz0 = if gz_f.is_negative() {
114 0u32
115 } else {
116 (gz_f.hi.min(max_gz).max(0)) as u32
117 };
118
119 let gx1 = gx0 + 1;
120 let gz1 = gz0 + 1;
121
122 let fx = gx_f - Fix128::from_int(gx0 as i64);
124 let fz = gz_f - Fix128::from_int(gz0 as i64);
125
126 let fx = clamp01(fx);
128 let fz = clamp01(fz);
129
130 let h00 = self.get_height(gx0, gz0);
132 let h10 = self.get_height(gx1, gz0);
133 let h01 = self.get_height(gx0, gz1);
134 let h11 = self.get_height(gx1, gz1);
135
136 let one = Fix128::ONE;
138 let h0 = h00 * (one - fx) + h10 * fx;
139 let h1 = h01 * (one - fx) + h11 * fx;
140 h0 * (one - fz) + h1 * fz
141 }
142
143 #[must_use]
145 pub fn sample_normal(&self, world_x: Fix128, world_z: Fix128) -> Vec3Fix {
146 let eps = self.spacing.half();
147 let span = |lo: Fix128, cells: u32, at: Fix128| {
151 let hi = lo + self.spacing * Fix128::from_int(i64::from(cells.saturating_sub(1)));
152 let a = if at - eps < lo { lo } else { at - eps };
153 let b = if at + eps > hi { hi } else { at + eps };
154 (a, b)
155 };
156 let (x0, x1) = span(self.origin.x, self.width, world_x);
157 let (z0, z1) = span(self.origin.z, self.depth, world_z);
158 let slope = |h0: Fix128, h1: Fix128, a: Fix128, b: Fix128| {
159 if b > a {
160 (h1 - h0) / (b - a)
161 } else {
162 Fix128::ZERO
163 }
164 };
165 let sx = slope(
166 self.sample_height(x0, world_z),
167 self.sample_height(x1, world_z),
168 x0,
169 x1,
170 );
171 let sz = slope(
172 self.sample_height(world_x, z0),
173 self.sample_height(world_x, z1),
174 z0,
175 z1,
176 );
177
178 Vec3Fix::new(-sx, Fix128::ONE, -sz).normalize()
180 }
181
182 #[must_use]
190 pub fn collide_sphere(&self, center: Vec3Fix, radius: Fix128) -> Option<Contact> {
191 if self.width == 0 || self.depth == 0 || self.spacing <= Fix128::ZERO {
192 return None;
193 }
194 let (gx_f, gz_f) = self.world_to_grid(center);
195
196 let margin = Fix128::from_int(2);
200 let last = |n: u32| Fix128::from_int(i64::from(n) - 1);
201 if gx_f < -margin || gx_f > last(self.width) + margin {
202 return None;
203 }
204 if gz_f < -margin || gz_f > last(self.depth) + margin {
205 return None;
206 }
207
208 let ground_height = self.sample_height(center.x, center.z);
209 let normal = self.sample_normal(center.x, center.z);
210 let dist = (center.y - ground_height) * normal.y;
212
213 if dist < radius {
214 let depth = radius - dist;
215 let point_on_surface = Vec3Fix::new(center.x, ground_height, center.z);
216
217 Some(Contact {
218 depth,
219 normal,
220 point_a: center - normal * radius,
221 point_b: point_on_surface,
222 })
223 } else {
224 None
225 }
226 }
227
228 #[inline]
230 #[must_use]
231 pub fn signed_distance(&self, point: Vec3Fix) -> Fix128 {
232 let ground_height = self.sample_height(point.x, point.z);
233 point.y - ground_height
234 }
235
236 #[must_use]
238 pub fn aabb(&self) -> crate::collider::AABB {
239 if self.heights.is_empty() || self.spacing <= Fix128::ZERO {
240 return crate::collider::AABB::new(self.origin, self.origin);
244 }
245 let mut min_h = self.heights[0];
246 let mut max_h = self.heights[0];
247 for &h in &self.heights[1..] {
248 if h < min_h {
249 min_h = h;
250 }
251 if h > max_h {
252 max_h = h;
253 }
254 }
255
256 let max_x = self.origin.x + self.spacing * Fix128::from_int((self.width - 1) as i64);
257 let max_z = self.origin.z + self.spacing * Fix128::from_int((self.depth - 1) as i64);
258
259 crate::collider::AABB::new(
260 Vec3Fix::new(self.origin.x, min_h, self.origin.z),
261 Vec3Fix::new(max_x, max_h, max_z),
262 )
263 }
264}
265
266#[inline]
267fn clamp01(v: Fix128) -> Fix128 {
268 if v < Fix128::ZERO {
269 Fix128::ZERO
270 } else if v > Fix128::ONE {
271 Fix128::ONE
272 } else {
273 v
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_flat_heightfield() {
283 let hf = HeightField::flat(10, 10, Fix128::ONE, Vec3Fix::ZERO, Fix128::ZERO);
284 let h = hf.sample_height(Fix128::from_int(5), Fix128::from_int(5));
285 assert!(
286 h.abs() < Fix128::from_ratio(1, 10),
287 "Flat field should have height ~0"
288 );
289 }
290
291 #[test]
292 fn test_heightfield_interpolation() {
293 let mut heights = Vec::new();
295 for z in 0..4u32 {
296 for x in 0..4u32 {
297 let _ = z;
298 heights.push(Fix128::from_int(x as i64));
299 }
300 }
301 let hf = HeightField::new(heights, 4, 4, Fix128::ONE, Vec3Fix::ZERO);
302
303 let h0 = hf.sample_height(Fix128::ZERO, Fix128::ONE);
305 assert!(h0.abs() < Fix128::ONE, "Height at x=0 should be ~0");
306
307 let h2 = hf.sample_height(Fix128::from_int(2), Fix128::ONE);
309 let error = (h2 - Fix128::from_int(2)).abs();
310 assert!(error < Fix128::ONE, "Height at x=2 should be ~2");
311 }
312
313 #[test]
314 fn test_sphere_heightfield_collision() {
315 let hf = HeightField::flat(10, 10, Fix128::ONE, Vec3Fix::ZERO, Fix128::ZERO);
316
317 let contact = hf.collide_sphere(
319 Vec3Fix::new(
320 Fix128::from_int(5),
321 Fix128::from_ratio(1, 2),
322 Fix128::from_int(5),
323 ),
324 Fix128::ONE,
325 );
326
327 assert!(
328 contact.is_some(),
329 "Sphere at y=0.5 with r=1 should penetrate ground at y=0"
330 );
331 let c = contact.unwrap();
332 assert!(c.depth > Fix128::ZERO);
333 }
334
335 #[test]
336 fn test_sphere_above_heightfield() {
337 let hf = HeightField::flat(10, 10, Fix128::ONE, Vec3Fix::ZERO, Fix128::ZERO);
338
339 let contact = hf.collide_sphere(
340 Vec3Fix::new(
341 Fix128::from_int(5),
342 Fix128::from_int(5),
343 Fix128::from_int(5),
344 ),
345 Fix128::ONE,
346 );
347
348 assert!(
349 contact.is_none(),
350 "Sphere at y=5 should not collide with ground"
351 );
352 }
353
354 #[test]
355 fn test_normal_on_flat() {
356 let hf = HeightField::flat(10, 10, Fix128::ONE, Vec3Fix::ZERO, Fix128::ZERO);
357 let normal = hf.sample_normal(Fix128::from_int(5), Fix128::from_int(5));
358
359 assert!(
361 normal.y > Fix128::from_ratio(9, 10),
362 "Normal should point up on flat terrain"
363 );
364 }
365
366 #[test]
367 fn test_signed_distance() {
368 let hf = HeightField::flat(10, 10, Fix128::ONE, Vec3Fix::ZERO, Fix128::ZERO);
369
370 let above = hf.signed_distance(Vec3Fix::from_int(5, 3, 5));
371 assert!(
372 above > Fix128::ZERO,
373 "Point above should have positive distance"
374 );
375
376 let below = hf.signed_distance(Vec3Fix::from_int(5, -2, 5));
377 assert!(
378 below < Fix128::ZERO,
379 "Point below should have negative distance"
380 );
381 }
382
383 #[test]
384 fn test_aabb() {
385 let hf = HeightField::flat(10, 10, Fix128::ONE, Vec3Fix::ZERO, Fix128::from_int(5));
386 let aabb = hf.aabb();
387 assert_eq!(aabb.min.y.hi, 5);
388 assert_eq!(aabb.max.y.hi, 5);
389 }
390
391 #[test]
392 fn set_height_updates_one_cell_and_feeds_bilinear_sampling() {
393 let mut hf = HeightField::flat(4, 3, Fix128::ONE, Vec3Fix::ZERO, Fix128::ZERO);
395 hf.set_height(2, 1, Fix128::from_int(8));
396 for gz in 0..3u32 {
398 for gx in 0..4u32 {
399 let want = if (gx, gz) == (2, 1) {
400 Fix128::from_int(8)
401 } else {
402 Fix128::ZERO
403 };
404 assert_eq!(hf.get_height(gx, gz), want, "({gx}, {gz})");
405 }
406 }
407 assert_eq!(
409 hf.sample_height(Fix128::from_int(2), Fix128::from_int(1)),
410 Fix128::from_int(8)
411 );
412 assert_eq!(
413 hf.sample_height(Fix128::from_ratio(5, 2), Fix128::from_int(1)),
414 Fix128::from_int(4)
415 );
416 assert_eq!(
417 hf.sample_height(Fix128::from_int(2), Fix128::from_ratio(1, 2)),
418 Fix128::from_int(4)
419 );
420 assert_eq!(
421 hf.sample_height(Fix128::from_ratio(3, 2), Fix128::from_ratio(1, 2)),
422 Fix128::from_int(2)
423 );
424 assert_eq!(
426 hf.sample_height(Fix128::from_ratio(1, 2), Fix128::from_ratio(3, 2)),
427 Fix128::ZERO
428 );
429 hf.set_height(2, 1, Fix128::from_int(-3));
431 assert_eq!(hf.get_height(2, 1), Fix128::from_int(-3));
432 hf.set_height(4, 0, Fix128::from_int(99));
434 hf.set_height(0, 3, Fix128::from_int(99));
435 hf.set_height(u32::MAX, u32::MAX, Fix128::from_int(99));
436 let mut sum = Fix128::ZERO;
437 for gz in 0..3u32 {
438 for gx in 0..4u32 {
439 sum = sum + hf.get_height(gx, gz);
440 }
441 }
442 assert_eq!(sum, Fix128::from_int(-3));
443 let aabb = hf.aabb();
445 assert_eq!(aabb.min.y, Fix128::from_int(-3));
446 assert_eq!(aabb.max.y, Fix128::ZERO);
447 }
448}