1#[allow(unused_imports)]
6use super::functions::*;
7use rayon::prelude::*;
8
9use super::types::{DistanceQuery, GpuSdfGrid, SdfGrid, SdfShape};
10
11pub fn sdf_bilateral_filter(grid: &SdfGrid, sigma_s: f64, sigma_r: f64) -> SdfGrid {
15 let nx = grid.nx;
16 let ny = grid.ny;
17 let nz = grid.nz;
18 let mut out = SdfGrid::new(nx, ny, nz, grid.dx, grid.origin);
19 let s2 = 2.0 * sigma_s * sigma_s;
20 let r2 = 2.0 * sigma_r * sigma_r;
21 for i in 0..nx {
22 for j in 0..ny {
23 for k in 0..nz {
24 let v0 = grid.get(i, j, k);
25 let mut acc = 0.0;
26 let mut wt = 0.0;
27 for di in -1i32..=1 {
28 for dj in -1i32..=1 {
29 for dk in -1i32..=1 {
30 let ni = i as i32 + di;
31 let nj = j as i32 + dj;
32 let nk = k as i32 + dk;
33 if ni >= 0
34 && ni < nx as i32
35 && nj >= 0
36 && nj < ny as i32
37 && nk >= 0
38 && nk < nz as i32
39 {
40 let vn = grid.get(ni as usize, nj as usize, nk as usize);
41 let dist2 = (di * di + dj * dj + dk * dk) as f64;
42 let w_s = (-dist2 / s2).exp();
43 let w_r = (-(v0 - vn) * (v0 - vn) / r2).exp();
44 let w = w_s * w_r;
45 acc += w * vn;
46 wt += w;
47 }
48 }
49 }
50 }
51 out.set(i, j, k, if wt > 1e-15 { acc / wt } else { v0 });
52 }
53 }
54 }
55 out
56}
57pub fn query_distance_field(grid: &SdfGrid, pos: [f64; 3]) -> Option<DistanceQuery> {
59 let dist = grid.sample(pos)?;
60 let grad = grid.gradient_at_point(pos).unwrap_or([0.0; 3]);
61 let grad_len = (grad[0] * grad[0] + grad[1] * grad[1] + grad[2] * grad[2]).sqrt();
62 let normal = if grad_len > 1e-15 {
63 [grad[0] / grad_len, grad[1] / grad_len, grad[2] / grad_len]
64 } else {
65 [0.0, 0.0, 1.0]
66 };
67 let closest_point = [
68 pos[0] - dist * normal[0],
69 pos[1] - dist * normal[1],
70 pos[2] - dist * normal[2],
71 ];
72 Some(DistanceQuery {
73 distance: dist,
74 normal,
75 closest_point,
76 is_inside: dist < 0.0,
77 })
78}
79pub fn query_distance_field_batch(
81 grid: &SdfGrid,
82 points: &[[f64; 3]],
83) -> Vec<Option<DistanceQuery>> {
84 points
85 .par_iter()
86 .map(|&p| query_distance_field(grid, p))
87 .collect()
88}
89pub fn find_zero_crossing(
94 grid: &SdfGrid,
95 origin: [f64; 3],
96 direction: [f64; 3],
97 t_min: f64,
98 t_max: f64,
99 n_bisect: usize,
100) -> Option<f64> {
101 let sample_at = |t: f64| -> Option<f64> {
102 let p = [
103 origin[0] + t * direction[0],
104 origin[1] + t * direction[1],
105 origin[2] + t * direction[2],
106 ];
107 grid.sample(p)
108 };
109 let v_min = sample_at(t_min)?;
110 let v_max = sample_at(t_max)?;
111 if v_min * v_max > 0.0 {
112 return None;
113 }
114 let mut lo = t_min;
115 let mut hi = t_max;
116 let mut v_lo = v_min;
117 for _ in 0..n_bisect {
118 let mid = (lo + hi) * 0.5;
119 let v_mid = sample_at(mid)?;
120 if v_lo * v_mid <= 0.0 {
121 hi = mid;
122 } else {
123 lo = mid;
124 v_lo = v_mid;
125 }
126 }
127 Some((lo + hi) * 0.5)
128}
129pub fn projected_area_xy(grid: &SdfGrid) -> f64 {
133 let ny = grid.ny;
134 let nz = grid.nz;
135 let nx = grid.nx;
136 let mut count = 0usize;
137 for i in 0..nx {
138 for j in 0..ny {
139 let occupied = (0..nz).any(|k| grid.get(i, j, k) < 0.0);
140 if occupied {
141 count += 1;
142 }
143 }
144 }
145 count as f64 * grid.dx * grid.dx
146}
147#[cfg(test)]
148mod tests_new_sdf {
149 use super::*;
150
151 fn sphere_grid(n: usize, dx: f64, radius: f64) -> SdfGrid {
152 let center = [(n as f64 * 0.5) * dx; 3];
153 let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
154 g.compute_sphere_sdf(center, radius);
155 g
156 }
157 #[test]
158 fn test_marching_cubes_sphere_produces_triangles() {
159 let g = sphere_grid(15, 0.1, 0.5);
160 let tris = marching_cubes(&g, 0.0);
161 assert!(
162 !tris.is_empty(),
163 "marching cubes on sphere should produce triangles"
164 );
165 }
166 #[test]
167 fn test_marching_cubes_all_positive_no_triangles() {
168 let mut g = SdfGrid::new(5, 5, 5, 0.1, [0.0; 3]);
169 g.values.iter_mut().for_each(|v| *v = 1.0);
170 let tris = marching_cubes(&g, 0.0);
171 assert!(tris.is_empty(), "no triangles when all SDF > 0");
172 }
173 #[test]
174 fn test_marching_cubes_triangle_count_increases_with_resolution() {
175 let g_lo = sphere_grid(8, 0.2, 0.5);
176 let g_hi = sphere_grid(20, 0.1, 0.5);
177 let n_lo = mesh_triangle_count(&g_lo, 0.0);
178 let n_hi = mesh_triangle_count(&g_hi, 0.0);
179 assert!(
180 n_hi >= n_lo,
181 "finer grid should produce at least as many triangles: lo={n_lo}, hi={n_hi}"
182 );
183 }
184 #[test]
185 fn test_marching_cubes_small_grid() {
186 let mut g = SdfGrid::new(2, 2, 2, 0.5, [0.0; 3]);
187 g.set(0, 0, 0, -0.1);
188 g.values.iter_mut().skip(1).for_each(|v| *v = 0.5);
189 let tris = marching_cubes(&g, 0.0);
190 let _ = tris;
191 }
192 #[test]
193 fn test_gaussian_blur_preserves_size() {
194 let g = sphere_grid(10, 0.1, 0.4);
195 let blurred = sdf_gaussian_blur(&g, 1.0);
196 assert_eq!(blurred.nx, g.nx);
197 assert_eq!(blurred.ny, g.ny);
198 assert_eq!(blurred.nz, g.nz);
199 }
200 #[test]
201 fn test_gaussian_blur_reduces_extremes() {
202 let g = sphere_grid(15, 0.1, 0.5);
203 let (lo_before, hi_before) = g
204 .values
205 .iter()
206 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
207 (lo.min(v), hi.max(v))
208 });
209 let blurred = sdf_gaussian_blur(&g, 1.5);
210 let (lo_after, hi_after) = blurred
211 .values
212 .iter()
213 .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), &v| {
214 (lo.min(v), hi.max(v))
215 });
216 assert!(
217 lo_after >= lo_before - 1e-6,
218 "blur should raise minimum: {lo_before} β {lo_after}"
219 );
220 assert!(
221 hi_after <= hi_before + 1e-6,
222 "blur should lower maximum: {hi_before} β {hi_after}"
223 );
224 }
225 #[test]
226 fn test_laplacian_sharpen_size() {
227 let g = sphere_grid(8, 0.1, 0.3);
228 let sharp = sdf_laplacian_sharpen(&g, 0.001);
229 assert_eq!(sharp.values.len(), g.values.len());
230 }
231 #[test]
232 fn test_sdf_dilate_expands() {
233 let g = sphere_grid(15, 0.1, 0.3);
234 let dilated = sdf_dilate(&g, 0.1);
235 let count_orig = g.values.iter().filter(|&&v| v < 0.0).count();
236 let count_dil = dilated.values.iter().filter(|&&v| v < 0.0).count();
237 assert!(count_dil >= count_orig, "dilation should expand interior");
238 }
239 #[test]
240 fn test_sdf_erode_shrinks() {
241 let g = sphere_grid(15, 0.1, 0.3);
242 let eroded = sdf_erode(&g, 0.05);
243 let count_orig = g.values.iter().filter(|&&v| v < 0.0).count();
244 let count_er = eroded.values.iter().filter(|&&v| v < 0.0).count();
245 assert!(count_er <= count_orig, "erosion should shrink interior");
246 }
247 #[test]
248 fn test_sdf_open_leq_original() {
249 let g = sphere_grid(15, 0.1, 0.3);
250 let opened = sdf_open(&g, 0.05);
251 let count_orig = g.values.iter().filter(|&&v| v < 0.0).count();
252 let count_open = opened.values.iter().filter(|&&v| v < 0.0).count();
253 assert!(
254 count_open <= count_orig + 5,
255 "open should not significantly expand"
256 );
257 }
258 #[test]
259 fn test_sdf_close_geq_original() {
260 let g = sphere_grid(15, 0.1, 0.3);
261 let closed = sdf_close(&g, 0.05);
262 let count_orig = g.values.iter().filter(|&&v| v < 0.0).count();
263 let count_close = closed.values.iter().filter(|&&v| v < 0.0).count();
264 assert!(
265 count_close >= count_orig - 5,
266 "close should not significantly shrink"
267 );
268 }
269 #[test]
270 fn test_sdf_offset_surface() {
271 let g = sphere_grid(15, 0.1, 0.3);
272 let offset = sdf_offset_surface(&g, 0.05);
273 for (&orig, &off) in g.values.iter().zip(offset.values.iter()) {
274 assert!((off - (orig - 0.05)).abs() < 1e-12);
275 }
276 }
277 #[test]
278 fn test_laplacian_smooth_preserves_size() {
279 let g = sphere_grid(8, 0.1, 0.3);
280 let smoothed = sdf_laplacian_smooth(&g, 3, 0.01);
281 assert_eq!(smoothed.values.len(), g.values.len());
282 }
283 #[test]
284 fn test_mean_curvature_smooth_size() {
285 let g = sphere_grid(8, 0.1, 0.3);
286 let smoothed = sdf_mean_curvature_smooth(&g, 0.001);
287 assert_eq!(smoothed.values.len(), g.values.len());
288 }
289 #[test]
290 fn test_bilateral_filter_size() {
291 let g = sphere_grid(8, 0.1, 0.3);
292 let filtered = sdf_bilateral_filter(&g, 1.5, 0.1);
293 assert_eq!(filtered.values.len(), g.values.len());
294 }
295 #[test]
296 fn test_bilateral_filter_preserves_sign() {
297 let n = 15usize;
298 let dx = 0.1;
299 let center = [(n as f64 * 0.5) * dx; 3];
300 let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
301 g.compute_sphere_sdf(center, 0.5);
302 let c = n / 2;
303 assert!(g.get(c, c, c) < 0.0, "center should be inside");
304 let filtered = sdf_bilateral_filter(&g, 1.0, 0.2);
305 assert!(
306 filtered.get(c, c, c) < 0.0,
307 "center should remain inside after filter"
308 );
309 }
310 #[test]
311 fn test_query_distance_field_inside() {
312 let n = 21usize;
313 let dx = 0.1;
314 let center = [(n as f64 * 0.5) * dx; 3];
315 let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
316 g.compute_sphere_sdf(center, 0.5);
317 let q = query_distance_field(&g, center).expect("should return query");
318 assert!(q.is_inside, "center should be inside");
319 assert!(q.distance < 0.0, "distance at center should be negative");
320 }
321 #[test]
322 fn test_query_distance_field_outside() {
323 let n = 21usize;
324 let dx = 0.1;
325 let center = [(n as f64 * 0.5) * dx; 3];
326 let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
327 g.compute_sphere_sdf(center, 0.3);
328 let far = [center[0] + 0.8, center[1], center[2]];
329 if let Some(q) = query_distance_field(&g, far)
330 && q.distance.is_finite()
331 {
332 assert!(!q.is_inside, "far point should be outside");
333 }
334 }
335 #[test]
336 fn test_query_batch() {
337 let g = sphere_grid(15, 0.1, 0.4);
338 let center = [(15_f64 * 0.5) * 0.1; 3];
339 let pts = vec![center, [0.0, 0.0, 0.0]];
340 let results = query_distance_field_batch(&g, &pts);
341 assert_eq!(results.len(), 2);
342 }
343 #[test]
344 fn test_find_zero_crossing() {
345 let n = 31usize;
346 let dx = 0.05;
347 let center = [(n as f64 * 0.5) * dx; 3];
348 let radius = 0.4;
349 let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
350 g.compute_sphere_sdf(center, radius);
351 let origin = [0.1, center[1], center[2]];
352 let direction = [1.0, 0.0, 0.0];
353 let t = find_zero_crossing(&g, origin, direction, 0.0, 1.0, 20);
354 assert!(t.is_some(), "should find zero crossing");
355 let t_val = t.unwrap();
356 let expected_t = center[0] - radius - origin[0];
357 assert!(
358 (t_val - expected_t).abs() < 0.1,
359 "t_val={t_val}, expectedβ{expected_t}"
360 );
361 }
362 #[test]
363 fn test_projected_area_sphere() {
364 let n = 21usize;
365 let dx = 0.1;
366 let center = [(n as f64 * 0.5) * dx; 3];
367 let radius = 0.4;
368 let mut g = SdfGrid::new(n, n, n, dx, [0.0; 3]);
369 g.compute_sphere_sdf(center, radius);
370 let area = projected_area_xy(&g);
371 let expected_area = std::f64::consts::PI * radius * radius;
372 assert!(
373 (area - expected_area).abs() / expected_area < 0.3,
374 "projected area {area} vs expected {expected_area}"
375 );
376 }
377}
378pub fn generate_sdf_grid(
384 shape: &SdfShape,
385 origin: [f64; 3],
386 extent: [f64; 3],
387 res: usize,
388) -> GpuSdfGrid {
389 let cell_size = extent[0] / res as f64;
390 let mut grid = GpuSdfGrid::new(res, res, res, origin, cell_size);
391 for ix in 0..res {
392 for iy in 0..res {
393 for iz in 0..res {
394 let p = grid.cell_center(ix, iy, iz);
395 let idx = grid.index(ix, iy, iz);
396 grid.data[idx] = shape.signed_distance(p);
397 }
398 }
399 }
400 grid
401}
402pub fn march_surface(grid: &GpuSdfGrid, iso: f64) -> Vec<[[f64; 3]; 3]> {
414 let mut triangles: Vec<[[f64; 3]; 3]> = Vec::new();
415 if grid.nx < 2 || grid.ny < 2 || grid.nz < 2 {
416 return triangles;
417 }
418 for ix in 0..grid.nx - 1 {
419 for iy in 0..grid.ny - 1 {
420 for iz in 0..grid.nz - 1 {
421 let corners: [[usize; 3]; 8] = [
422 [ix, iy, iz],
423 [ix + 1, iy, iz],
424 [ix + 1, iy + 1, iz],
425 [ix, iy + 1, iz],
426 [ix, iy, iz + 1],
427 [ix + 1, iy, iz + 1],
428 [ix + 1, iy + 1, iz + 1],
429 [ix, iy + 1, iz + 1],
430 ];
431 let edges: [[usize; 2]; 12] = [
432 [0, 1],
433 [1, 2],
434 [2, 3],
435 [3, 0],
436 [4, 5],
437 [5, 6],
438 [6, 7],
439 [7, 4],
440 [0, 4],
441 [1, 5],
442 [2, 6],
443 [3, 7],
444 ];
445 for edge in &edges {
446 let [ia, ib] = *edge;
447 let ca = corners[ia];
448 let cb = corners[ib];
449 let da = grid.get(ca[0], ca[1], ca[2]);
450 let db = grid.get(cb[0], cb[1], cb[2]);
451 if (da < iso) != (db < iso) {
452 let t = if (db - da).abs() > 1e-12 {
453 (iso - da) / (db - da)
454 } else {
455 0.5
456 };
457 let pa = grid.cell_center(ca[0], ca[1], ca[2]);
458 let pb = grid.cell_center(cb[0], cb[1], cb[2]);
459 let pt = [
460 pa[0] + t * (pb[0] - pa[0]),
461 pa[1] + t * (pb[1] - pa[1]),
462 pa[2] + t * (pb[2] - pa[2]),
463 ];
464 triangles.push([pt, pt, pt]);
465 }
466 }
467 }
468 }
469 }
470 triangles
471}
472#[cfg(test)]
473mod gpu_sdf_tests {
474
475 use crate::sdf_compute::SdfCombine;
476 use crate::sdf_compute::SdfShape;
477 use crate::sdf_compute::generate_sdf_grid;
478 use crate::sdf_compute::march_surface;
479 #[test]
480 fn test_sphere_sdf_at_center() {
481 let r = 1.5;
482 let center = [0.0, 0.0, 0.0];
483 let shape = SdfShape::Sphere { center, r };
484 let d = shape.signed_distance(center);
485 assert!(
486 (d - (-r)).abs() < 1e-12,
487 "SDF at center should be -r, got {d}"
488 );
489 }
490 #[test]
491 fn test_sphere_sdf_outside() {
492 let r = 1.0;
493 let shape = SdfShape::Sphere {
494 center: [0.0; 3],
495 r,
496 };
497 let d = shape.signed_distance([3.0, 0.0, 0.0]);
498 assert!((d - 2.0).abs() < 1e-12, "SDF outside sphere, got {d}");
499 }
500 #[test]
501 fn test_box_sdf_outside() {
502 let shape = SdfShape::Box3 {
503 center: [0.0; 3],
504 half: [1.0, 1.0, 1.0],
505 };
506 let d = shape.signed_distance([3.0, 0.0, 0.0]);
507 assert!(d > 0.0, "SDF outside box should be positive, got {d}");
508 }
509 #[test]
510 fn test_smooth_union_between_two_spheres() {
511 let sa = SdfShape::Sphere {
512 center: [-0.5, 0.0, 0.0],
513 r: 1.0,
514 };
515 let sb = SdfShape::Sphere {
516 center: [0.5, 0.0, 0.0],
517 r: 1.0,
518 };
519 let combo = SdfCombine::SmoothUnion(sa, sb, 0.5);
520 let d = combo.signed_distance([0.0, 0.0, 0.0]);
521 assert!(d < 0.0, "smooth union midpoint should be inside, got {d}");
522 }
523 #[test]
524 fn test_hard_union_and_intersection() {
525 let sa = SdfShape::Sphere {
526 center: [0.0; 3],
527 r: 2.0,
528 };
529 let sb = SdfShape::Sphere {
530 center: [0.0; 3],
531 r: 1.0,
532 };
533 let union = SdfCombine::Union(sa.clone(), sb.clone());
534 let inter = SdfCombine::Intersection(sa, sb);
535 let p = [0.0, 0.0, 0.0];
536 assert!((union.signed_distance(p) - (-2.0)).abs() < 1e-12);
537 assert!((inter.signed_distance(p) - (-1.0)).abs() < 1e-12);
538 }
539 #[test]
540 fn test_generate_sdf_grid_sphere_center() {
541 let r = 0.4;
542 let shape = SdfShape::Sphere {
543 center: [0.5, 0.5, 0.5],
544 r,
545 };
546 let grid = generate_sdf_grid(&shape, [0.0; 3], [1.0, 1.0, 1.0], 11);
547 let mid = 5;
548 let d = grid.get(mid, mid, mid);
549 assert!(
550 (d - (-r)).abs() < 0.1,
551 "grid at sphere centre β -r, got {d}"
552 );
553 }
554 #[test]
555 fn test_grid_gradient_points_away_from_sphere() {
556 let r = 0.3;
557 let center = [0.5, 0.5, 0.5];
558 let shape = SdfShape::Sphere { center, r };
559 let grid = generate_sdf_grid(&shape, [0.0; 3], [1.0, 1.0, 1.0], 21);
560 let p = [center[0] + r + 0.05, center[1], center[2]];
561 let grad = grid.gradient_at(p);
562 assert!(
563 grad[0] > 0.0,
564 "gradient x should be positive, got {:?}",
565 grad
566 );
567 }
568 #[test]
569 fn test_march_surface_finds_crossings() {
570 let r = 0.3;
571 let shape = SdfShape::Sphere {
572 center: [0.5, 0.5, 0.5],
573 r,
574 };
575 let grid = generate_sdf_grid(&shape, [0.0; 3], [1.0, 1.0, 1.0], 11);
576 let tris = march_surface(&grid, 0.0);
577 assert!(
578 !tris.is_empty(),
579 "marching cubes should find iso-surface crossings for a sphere"
580 );
581 }
582 #[test]
583 fn test_capsule_sdf() {
584 let shape = SdfShape::Capsule {
585 a: [0.0, 0.0, 0.0],
586 b: [1.0, 0.0, 0.0],
587 r: 0.5,
588 };
589 let d = shape.signed_distance([0.5, 0.0, 0.0]);
590 assert!(d < 0.0, "midpoint inside capsule, got {d}");
591 let d_far = shape.signed_distance([5.0, 0.0, 0.0]);
592 assert!(d_far > 0.0, "far point outside capsule, got {d_far}");
593 }
594}