Skip to main content

oxiphysics_gpu/sdf_compute/
functions_2.rs

1//! Auto-generated module
2//!
3//! πŸ€– Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#[allow(unused_imports)]
6use super::functions::*;
7use rayon::prelude::*;
8
9use super::types::{DistanceQuery, GpuSdfGrid, SdfGrid, SdfShape};
10
11/// Bilateral filter for SDF: preserves sharp features while smoothing noise.
12///
13/// Uses a spatial Gaussian kernel weighted by the range (SDF value) difference.
14pub 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}
57/// Query the distance field at a point and compute surface normal + closest point.
58pub 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}
79/// Batch query multiple points and return DistanceQuery results.
80pub 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}
89/// Find the zero-crossing (surface) along a 1-D ray by bisection.
90///
91/// Returns the parameter `t` such that `grid.sample(origin + t * direction) β‰ˆ 0`,
92/// or `None` if no crossing found in `[t_min, t_max]`.
93pub 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}
129/// Compute the projected area of a surface onto the xy-plane.
130///
131/// Counts grid cells with SDF < 0 in the bottom z-slice.
132pub 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}
378/// Generate a [`GpuSdfGrid`] by evaluating `shape` at every cell centre.
379///
380/// * `origin`  β€” world-space corner of the grid.
381/// * `extent`  β€” total size in each dimension.
382/// * `res`     β€” number of cells in each dimension.
383pub 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}
402/// Simplified surface extraction from a [`GpuSdfGrid`].
403///
404/// For each cell of the grid, the function checks whether any of the 12 cube
405/// edges cross the iso-surface.  For each crossing edge, it linearly
406/// interpolates the crossing point and emits a degenerate triangle
407/// (three copies of the crossing point) as a placeholder.  This is *not* a
408/// full marching-cubes implementation β€” it counts crossings and returns one
409/// "triangle" per crossing β€” but it exercises the grid access pattern and
410/// confirms that crossings are correctly detected.
411///
412/// Returns a `Vec<[[f64;3\];3]>` of triangles (three vertices each).
413pub 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}