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