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