Skip to main content

oxiphysics_geometry/implicit_geometry/
functions.rs

1//! Auto-generated module
2//!
3//! ๐Ÿค– Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6use super::types::{
7    ContactPoint, IsoMeshResult, RayMarchResult, SdfCollisionResult, SdfGrid, Triangle,
8};
9
10#[inline]
11pub(super) fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
12    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
13}
14#[inline]
15pub(super) fn len(v: [f64; 3]) -> f64 {
16    dot(v, v).sqrt()
17}
18#[inline]
19pub(super) fn norm(v: [f64; 3]) -> [f64; 3] {
20    let l = len(v).max(1e-300);
21    [v[0] / l, v[1] / l, v[2] / l]
22}
23#[inline]
24pub(super) fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
25    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
26}
27#[inline]
28pub(super) fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
29    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
30}
31#[inline]
32pub(super) fn scale(v: [f64; 3], s: f64) -> [f64; 3] {
33    [v[0] * s, v[1] * s, v[2] * s]
34}
35#[inline]
36pub(super) fn clamp01(v: f64) -> f64 {
37    v.clamp(0.0, 1.0)
38}
39#[inline]
40pub(super) fn clamp_f(v: f64, lo: f64, hi: f64) -> f64 {
41    v.clamp(lo, hi)
42}
43#[inline]
44pub(super) fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
45    [
46        a[1] * b[2] - a[2] * b[1],
47        a[2] * b[0] - a[0] * b[2],
48        a[0] * b[1] - a[1] * b[0],
49    ]
50}
51#[inline]
52pub(super) fn len2(v: [f64; 2]) -> f64 {
53    (v[0] * v[0] + v[1] * v[1]).sqrt()
54}
55#[inline]
56pub(super) fn dot2(a: [f64; 2], b: [f64; 2]) -> f64 {
57    a[0] * b[0] + a[1] * b[1]
58}
59/// Signed distance function trait.
60///
61/// Implementors return the signed distance from point `p` to the surface:
62/// negative inside, zero on surface, positive outside.
63pub trait Sdf: Send + Sync {
64    /// Evaluate the signed distance at `p`.
65    fn dist(&self, p: [f64; 3]) -> f64;
66}
67/// Polynomial smooth-union kernel (Quilez).
68///
69/// Returns a value โ‰ค min(a, b) with a smooth blend of width `k`.
70pub fn sdf_smooth_union(a: f64, b: f64, k: f64) -> f64 {
71    if k < 1e-15 {
72        return a.min(b);
73    }
74    let h = (0.5 + 0.5 * (b - a) / k).clamp(0.0, 1.0);
75    a * h + b * (1.0 - h) - k * h * (1.0 - h)
76}
77/// Polynomial smooth-intersection kernel.
78pub fn sdf_smooth_intersection(a: f64, b: f64, k: f64) -> f64 {
79    if k < 1e-15 {
80        return a.max(b);
81    }
82    let h = (0.5 - 0.5 * (b - a) / k).clamp(0.0, 1.0);
83    a * (1.0 - h) + b * h + k * h * (1.0 - h)
84}
85/// Polynomial smooth-difference kernel (subtract `b` from `a`).
86pub fn sdf_smooth_difference(a: f64, b: f64, k: f64) -> f64 {
87    sdf_smooth_intersection(a, -b, k)
88}
89/// Estimate the gradient of an SDF at `p` via central finite differences.
90///
91/// Returns the unnormalized gradient vector (norm โ‰ˆ 1 for exact SDFs).
92pub fn sdf_gradient<S: Sdf>(sdf: &S, p: [f64; 3], eps: f64) -> [f64; 3] {
93    let dx = sdf.dist([p[0] + eps, p[1], p[2]]) - sdf.dist([p[0] - eps, p[1], p[2]]);
94    let dy = sdf.dist([p[0], p[1] + eps, p[2]]) - sdf.dist([p[0], p[1] - eps, p[2]]);
95    let dz = sdf.dist([p[0], p[1], p[2] + eps]) - sdf.dist([p[0], p[1], p[2] - eps]);
96    [dx / (2.0 * eps), dy / (2.0 * eps), dz / (2.0 * eps)]
97}
98/// Compute the unit outward surface normal at `p` by finite-difference gradient.
99pub fn sdf_normal<S: Sdf>(sdf: &S, p: [f64; 3], eps: f64) -> [f64; 3] {
100    norm(sdf_gradient(sdf, p, eps))
101}
102/// Mean curvature of an SDF at `p` estimated from the Laplacian of the SDF.
103///
104/// H โ‰ˆ โˆ’(1/2) ยท โˆ‡ยฒd / |โˆ‡d|  (signed; negative = concave toward outside).
105pub fn sdf_mean_curvature<S: Sdf>(sdf: &S, p: [f64; 3], eps: f64) -> f64 {
106    let d = sdf.dist(p);
107    let dxp = sdf.dist([p[0] + eps, p[1], p[2]]);
108    let dxm = sdf.dist([p[0] - eps, p[1], p[2]]);
109    let dyp = sdf.dist([p[0], p[1] + eps, p[2]]);
110    let dym = sdf.dist([p[0], p[1] - eps, p[2]]);
111    let dzp = sdf.dist([p[0], p[1], p[2] + eps]);
112    let dzm = sdf.dist([p[0], p[1], p[2] - eps]);
113    let laplacian = (dxp + dxm + dyp + dym + dzp + dzm - 6.0 * d) / (eps * eps);
114    let g = sdf_gradient(sdf, p, eps);
115    let g_len = len(g).max(1e-30);
116    -0.5 * laplacian / g_len
117}
118/// March a ray defined by `origin + t * direction` through an SDF.
119///
120/// * `max_steps` โ€“ step budget.
121/// * `max_t`     โ€“ maximum travel distance.
122/// * `surf_eps`  โ€“ surface-hit threshold.
123///
124/// Uses the sphere-tracing algorithm (Hart 1996).
125pub fn ray_march<S: Sdf>(
126    sdf: &S,
127    origin: [f64; 3],
128    direction: [f64; 3],
129    max_steps: u32,
130    max_t: f64,
131    surf_eps: f64,
132) -> RayMarchResult {
133    let dir = norm(direction);
134    let mut t = 0.0_f64;
135    for step in 0..max_steps {
136        let p = add(origin, scale(dir, t));
137        let d = sdf.dist(p);
138        if d.abs() < surf_eps {
139            return RayMarchResult {
140                hit: true,
141                t,
142                point: p,
143                steps: step + 1,
144            };
145        }
146        t += d.abs().max(surf_eps * 0.1);
147        if t > max_t {
148            break;
149        }
150    }
151    RayMarchResult {
152        hit: false,
153        t,
154        point: add(origin, scale(dir, t)),
155        steps: max_steps,
156    }
157}
158/// Ray march with over-relaxation for faster convergence.
159///
160/// Uses the relaxed sphere tracing of Keinert et al. (2014) with parameter `omega`.
161/// A safe value is `omega = 1.2`.
162pub fn ray_march_relaxed<S: Sdf>(
163    sdf: &S,
164    origin: [f64; 3],
165    direction: [f64; 3],
166    max_steps: u32,
167    max_t: f64,
168    surf_eps: f64,
169    omega: f64,
170) -> RayMarchResult {
171    let dir = norm(direction);
172    let mut t = 0.0_f64;
173    let mut prev_d = f64::INFINITY;
174    for step in 0..max_steps {
175        let p = add(origin, scale(dir, t));
176        let d = sdf.dist(p);
177        if d.abs() < surf_eps {
178            return RayMarchResult {
179                hit: true,
180                t,
181                point: p,
182                steps: step + 1,
183            };
184        }
185        let step_length = if omega * d.abs() <= d.abs() + prev_d.abs() {
186            omega * d.abs()
187        } else {
188            d.abs()
189        };
190        prev_d = d;
191        t += step_length.max(surf_eps * 0.1);
192        if t > max_t {
193            break;
194        }
195    }
196    RayMarchResult {
197        hit: false,
198        t,
199        point: add(origin, scale(dir, t)),
200        steps: max_steps,
201    }
202}
203pub(super) const MC_EDGE_TABLE: [u16; 256] = [
204    0x000, 0xfff, 0x00f, 0xff0, 0x0f0, 0xf0f, 0x0ff, 0xf00, 0xf00, 0x0ff, 0xf0f, 0x0f0, 0xff0,
205    0x00f, 0xfff, 0x000, 0x00f, 0xff0, 0x000, 0xfff, 0x0ff, 0xf00, 0x0f0, 0xf0f, 0xff0, 0x00f,
206    0xfff, 0x000, 0xf0f, 0x0f0, 0xf00, 0x0ff, 0x0f0, 0xf0f, 0x0ff, 0xf00, 0x000, 0xfff, 0x00f,
207    0xff0, 0xf0f, 0x0f0, 0xf00, 0x0ff, 0xfff, 0x000, 0xff0, 0x00f, 0x0ff, 0xf00, 0x0f0, 0xf0f,
208    0x00f, 0xff0, 0x000, 0xfff, 0xf00, 0x0ff, 0xf0f, 0x0f0, 0xff0, 0x00f, 0xfff, 0x000, 0xf00,
209    0x0ff, 0xf0f, 0x0f0, 0xff0, 0x00f, 0xfff, 0x000, 0x000, 0xfff, 0x00f, 0xff0, 0x0f0, 0xf0f,
210    0x0ff, 0xf00, 0xff0, 0x00f, 0xfff, 0x000, 0xf0f, 0x0f0, 0xf00, 0x0ff, 0x00f, 0xff0, 0x000,
211    0xfff, 0x0ff, 0xf00, 0x0f0, 0xf0f, 0x0ff, 0xf00, 0x0f0, 0xf0f, 0x00f, 0xff0, 0x000, 0xfff,
212    0xf0f, 0x0f0, 0xf00, 0x0ff, 0xfff, 0x000, 0xff0, 0x00f, 0x0f0, 0xf0f, 0x0ff, 0xf00, 0x000,
213    0xfff, 0x00f, 0xff0, 0xf00, 0x0ff, 0xf0f, 0x0f0, 0xff0, 0x00f, 0xfff, 0x000, 0x000, 0xfff,
214    0x00f, 0xff0, 0x0f0, 0xf0f, 0x0ff, 0xf00, 0xf00, 0x0ff, 0xf0f, 0x0f0, 0xff0, 0x00f, 0xfff,
215    0x000, 0x00f, 0xff0, 0x000, 0xfff, 0x0ff, 0xf00, 0x0f0, 0xf0f, 0xff0, 0x00f, 0xfff, 0x000,
216    0xf0f, 0x0f0, 0xf00, 0x0ff, 0x0f0, 0xf0f, 0x0ff, 0xf00, 0x000, 0xfff, 0x00f, 0xff0, 0xf0f,
217    0x0f0, 0xf00, 0x0ff, 0xfff, 0x000, 0xff0, 0x00f, 0x0ff, 0xf00, 0x0f0, 0xf0f, 0x00f, 0xff0,
218    0x000, 0xfff, 0xf00, 0x0ff, 0xf0f, 0x0f0, 0xff0, 0x00f, 0xfff, 0x000, 0xf00, 0x0ff, 0xf0f,
219    0x0f0, 0xff0, 0x00f, 0xfff, 0x000, 0x000, 0xfff, 0x00f, 0xff0, 0x0f0, 0xf0f, 0x0ff, 0xf00,
220    0xff0, 0x00f, 0xfff, 0x000, 0xf0f, 0x0f0, 0xf00, 0x0ff, 0x00f, 0xff0, 0x000, 0xfff, 0x0ff,
221    0xf00, 0x0f0, 0xf0f, 0x0ff, 0xf00, 0x0f0, 0xf0f, 0x00f, 0xff0, 0x000, 0xfff, 0xf0f, 0x0f0,
222    0xf00, 0x0ff, 0xfff, 0x000, 0xff0, 0x00f, 0x0f0, 0xf0f, 0x0ff, 0xf00, 0x000, 0xfff, 0x00f,
223    0xff0, 0xf00, 0x0ff, 0xf0f, 0x0f0, 0xff0, 0x00f, 0xfff, 0x000,
224];
225/// Helper: linearly interpolate between two cube vertices on an edge.
226pub(super) fn interp_edge(p0: [f64; 3], d0: f64, p1: [f64; 3], d1: f64) -> [f64; 3] {
227    if (d1 - d0).abs() < 1e-12 {
228        return p0;
229    }
230    let t = d0 / (d0 - d1);
231    [
232        p0[0] + t * (p1[0] - p0[0]),
233        p0[1] + t * (p1[1] - p0[1]),
234        p0[2] + t * (p1[2] - p0[2]),
235    ]
236}
237/// Extract the zero isosurface from an [`SdfGrid`] using Marching Cubes.
238///
239/// Returns an [`IsoMeshResult`] containing all output triangles.
240/// This is an independent implementation distinct from the one in `implicit`.
241pub fn extract_isosurface(grid: &SdfGrid) -> IsoMeshResult {
242    let mut result = IsoMeshResult::default();
243    if grid.nx < 2 || grid.ny < 2 || grid.nz < 2 {
244        return result;
245    }
246    for iz in 0..grid.nz - 1 {
247        for iy in 0..grid.ny - 1 {
248            for ix in 0..grid.nx - 1 {
249                let corners: [[usize; 3]; 8] = [
250                    [ix, iy, iz],
251                    [ix + 1, iy, iz],
252                    [ix + 1, iy + 1, iz],
253                    [ix, iy + 1, iz],
254                    [ix, iy, iz + 1],
255                    [ix + 1, iy, iz + 1],
256                    [ix + 1, iy + 1, iz + 1],
257                    [ix, iy + 1, iz + 1],
258                ];
259                let vals: [f64; 8] =
260                    std::array::from_fn(|i| grid.get(corners[i][0], corners[i][1], corners[i][2]));
261                let pts: [[f64; 3]; 8] = std::array::from_fn(|i| {
262                    grid.cell_center(corners[i][0], corners[i][1], corners[i][2])
263                });
264                let mut cube_idx: usize = 0;
265                for i in 0..8 {
266                    if vals[i] < 0.0 {
267                        cube_idx |= 1 << i;
268                    }
269                }
270                if MC_EDGE_TABLE[cube_idx] == 0 {
271                    continue;
272                }
273                let edge_mask = MC_EDGE_TABLE[cube_idx];
274                let mut verts: [[f64; 3]; 12] = [[0.0; 3]; 12];
275                let edges = [
276                    (0, 1),
277                    (1, 2),
278                    (2, 3),
279                    (3, 0),
280                    (4, 5),
281                    (5, 6),
282                    (6, 7),
283                    (7, 4),
284                    (0, 4),
285                    (1, 5),
286                    (2, 6),
287                    (3, 7),
288                ];
289                for (e, &(i, j)) in edges.iter().enumerate() {
290                    if edge_mask & (1 << e) != 0 {
291                        verts[e] = interp_edge(pts[i], vals[i], pts[j], vals[j]);
292                    }
293                }
294                let active: Vec<usize> = (0..12).filter(|&e| edge_mask & (1 << e) != 0).collect();
295                if active.len() >= 3 {
296                    for i in 1..active.len() - 1 {
297                        let tri = Triangle {
298                            v0: verts[active[0]],
299                            v1: verts[active[i]],
300                            v2: verts[active[i + 1]],
301                        };
302                        result.triangles.push(tri);
303                        result.vertex_count += 3;
304                    }
305                }
306            }
307        }
308    }
309    result
310}
311/// Test whether point `p` is inside the SDF and return contact data.
312///
313/// Returns `Some(result)` if the point is inside (`dist < 0`), else `None`.
314pub fn sdf_point_query<S: Sdf>(sdf: &S, p: [f64; 3], eps: f64) -> Option<SdfCollisionResult> {
315    let d = sdf.dist(p);
316    if d >= 0.0 {
317        return None;
318    }
319    let n = sdf_normal(sdf, p, eps);
320    let contact = add(p, scale(n, -d));
321    Some(SdfCollisionResult {
322        depth: -d,
323        normal: n,
324        contact_point: contact,
325    })
326}
327/// Simple swept-sphere vs SDF collision proxy.
328///
329/// Move sphere of `radius` along `velocity * dt` from `center`.
330/// Returns the safe time-of-impact in \[0, dt\] and contact data if hit.
331pub fn sdf_sphere_sweep<S: Sdf>(
332    sdf: &S,
333    center: [f64; 3],
334    radius: f64,
335    velocity: [f64; 3],
336    dt: f64,
337    eps: f64,
338) -> Option<(f64, SdfCollisionResult)> {
339    let speed = len(velocity);
340    if speed < 1e-15 {
341        return None;
342    }
343    let dir = scale(velocity, 1.0 / speed);
344    let max_t = speed * dt;
345    let result = ray_march_with_offset(sdf, center, dir, radius, 128, max_t, eps);
346    if result.hit {
347        let toi = result.t / speed;
348        let n = sdf_normal(sdf, result.point, eps);
349        Some((
350            toi,
351            SdfCollisionResult {
352                depth: 0.0,
353                normal: n,
354                contact_point: result.point,
355            },
356        ))
357    } else {
358        None
359    }
360}
361pub(super) fn ray_march_with_offset<S: Sdf>(
362    sdf: &S,
363    origin: [f64; 3],
364    dir: [f64; 3],
365    offset: f64,
366    max_steps: u32,
367    max_t: f64,
368    eps: f64,
369) -> RayMarchResult {
370    let mut t = 0.0_f64;
371    for step in 0..max_steps {
372        let p = add(origin, scale(dir, t));
373        let d = sdf.dist(p) - offset;
374        if d.abs() < eps {
375            return RayMarchResult {
376                hit: true,
377                t,
378                point: p,
379                steps: step + 1,
380            };
381        }
382        t += d.abs().max(eps * 0.1);
383        if t > max_t {
384            break;
385        }
386    }
387    RayMarchResult {
388        hit: false,
389        t,
390        point: add(origin, scale(dir, t)),
391        steps: max_steps,
392    }
393}
394/// A simple hash-based value-noise function for SDF displacement.
395///
396/// Produces values in \[โˆ’1, 1\] from integer lattice positions.
397pub(super) fn value_noise_lattice(ix: i32, iy: i32, iz: i32) -> f64 {
398    let n = ix
399        .wrapping_mul(1619)
400        .wrapping_add(iy.wrapping_mul(31337))
401        .wrapping_add(iz.wrapping_mul(6971))
402        .wrapping_add(1376312589i32);
403    let n = n ^ (n << 13);
404    let n = n.wrapping_mul(
405        n.wrapping_mul(n.wrapping_mul(15731).wrapping_add(789221))
406            .wrapping_add(1376312589),
407    );
408    (n & 0x7fffffff) as f64 / 1073741824.0 - 1.0
409}
410/// Smooth step (fade) for noise interpolation.
411#[inline]
412pub(super) fn fade(t: f64) -> f64 {
413    t * t * t * (t * (t * 6.0 - 15.0) + 10.0)
414}
415/// Sample trilinear value noise at continuous position `p`.
416pub(super) fn value_noise(p: [f64; 3]) -> f64 {
417    let ix = p[0].floor() as i32;
418    let iy = p[1].floor() as i32;
419    let iz = p[2].floor() as i32;
420    let fx = fade(p[0] - ix as f64);
421    let fy = fade(p[1] - iy as f64);
422    let fz = fade(p[2] - iz as f64);
423    let v000 = value_noise_lattice(ix, iy, iz);
424    let v100 = value_noise_lattice(ix + 1, iy, iz);
425    let v010 = value_noise_lattice(ix, iy + 1, iz);
426    let v110 = value_noise_lattice(ix + 1, iy + 1, iz);
427    let v001 = value_noise_lattice(ix, iy, iz + 1);
428    let v101 = value_noise_lattice(ix + 1, iy, iz + 1);
429    let v011 = value_noise_lattice(ix, iy + 1, iz + 1);
430    let v111 = value_noise_lattice(ix + 1, iy + 1, iz + 1);
431    let c00 = v000 * (1.0 - fx) + v100 * fx;
432    let c10 = v010 * (1.0 - fx) + v110 * fx;
433    let c01 = v001 * (1.0 - fx) + v101 * fx;
434    let c11 = v011 * (1.0 - fx) + v111 * fx;
435    let c0 = c00 * (1.0 - fy) + c10 * fy;
436    let c1 = c01 * (1.0 - fy) + c11 * fy;
437    c0 * (1.0 - fz) + c1 * fz
438}
439/// Fractal (octaved) value noise: fBm with `octaves` levels.
440pub fn fbm_noise(p: [f64; 3], octaves: u32, lacunarity: f64, gain: f64) -> f64 {
441    let mut value = 0.0_f64;
442    let mut amplitude = 0.5_f64;
443    let mut frequency = 1.0_f64;
444    for _ in 0..octaves {
445        value += amplitude * value_noise(scale(p, frequency));
446        amplitude *= gain;
447        frequency *= lacunarity;
448    }
449    value
450}
451/// Project point `p` to the nearest surface point of the SDF using
452/// gradient descent on the absolute distance.
453///
454/// Returns the approximate surface point and the final signed distance.
455pub fn sdf_closest_point<S: Sdf>(sdf: &S, p: [f64; 3], eps: f64, max_iter: u32) -> ([f64; 3], f64) {
456    let mut q = p;
457    for _ in 0..max_iter {
458        let d = sdf.dist(q);
459        if d.abs() < eps {
460            break;
461        }
462        let g = sdf_gradient(sdf, q, eps);
463        let step = d;
464        q = sub(q, scale(g, step));
465    }
466    let d_final = sdf.dist(q);
467    (q, d_final)
468}
469/// Estimate ambient occlusion at surface point `p` with normal `n` from an SDF.
470///
471/// Uses the analytic AO formula of Quilez (2016): sums SDF samples along the
472/// normal direction weighted by 1/distance.
473pub fn sdf_ambient_occlusion<S: Sdf>(
474    sdf: &S,
475    p: [f64; 3],
476    n: [f64; 3],
477    num_samples: u32,
478    max_dist: f64,
479) -> f64 {
480    let mut occ = 0.0_f64;
481    let mut scale_factor = 1.0_f64;
482    for i in 1..=num_samples {
483        let h = i as f64 / num_samples as f64 * max_dist;
484        let sample_pt = add(p, scale(n, h));
485        let d = sdf.dist(sample_pt);
486        occ += (h - d) * scale_factor;
487        scale_factor *= 0.5;
488    }
489    (1.0 - 2.0 * occ / max_dist).clamp(0.0, 1.0)
490}
491/// Estimate a soft-shadow factor for a point `p` toward a light at `light_pos`.
492///
493/// Returns a value in \[0, 1\]: 0 = fully shadowed, 1 = fully lit.
494/// Uses Quilez's soft-shadow ray-march formula with `k` controlling penumbra sharpness.
495pub fn sdf_soft_shadow<S: Sdf>(sdf: &S, p: [f64; 3], light_pos: [f64; 3], k: f64, eps: f64) -> f64 {
496    let to_light = sub(light_pos, p);
497    let dist_to_light = len(to_light);
498    let dir = scale(to_light, 1.0 / dist_to_light);
499    let mut t = 2.0 * eps;
500    let mut shadow = 1.0_f64;
501    let max_steps = 64u32;
502    for _ in 0..max_steps {
503        if t >= dist_to_light {
504            break;
505        }
506        let q = add(p, scale(dir, t));
507        let d = sdf.dist(q);
508        if d < eps {
509            return 0.0;
510        }
511        shadow = shadow.min(k * d / t);
512        t += d;
513    }
514    shadow.clamp(0.0, 1.0)
515}
516/// Generate a contact manifold between a convex set of sample points and an SDF.
517///
518/// `points` are candidate contact points in world space.
519/// Returns all contacts with `dist < tolerance`.
520pub fn sdf_contact_manifold<S: Sdf>(
521    sdf: &S,
522    points: &[[f64; 3]],
523    tolerance: f64,
524    eps: f64,
525) -> Vec<ContactPoint> {
526    let mut contacts = Vec::new();
527    for &p in points {
528        let d = sdf.dist(p);
529        if d < tolerance {
530            let n = sdf_normal(sdf, p, eps);
531            contacts.push(ContactPoint {
532                position: p,
533                normal: n,
534                depth: (-d).max(0.0),
535            });
536        }
537    }
538    contacts
539}
540/// Count the number of grid cells that straddle the zero isosurface.
541pub fn count_surface_cells(grid: &SdfGrid) -> usize {
542    let mut count = 0usize;
543    for iz in 0..grid.nz.saturating_sub(1) {
544        for iy in 0..grid.ny.saturating_sub(1) {
545            for ix in 0..grid.nx.saturating_sub(1) {
546                let signs: [bool; 8] = [
547                    grid.get(ix, iy, iz) < 0.0,
548                    grid.get(ix + 1, iy, iz) < 0.0,
549                    grid.get(ix + 1, iy + 1, iz) < 0.0,
550                    grid.get(ix, iy + 1, iz) < 0.0,
551                    grid.get(ix, iy, iz + 1) < 0.0,
552                    grid.get(ix + 1, iy, iz + 1) < 0.0,
553                    grid.get(ix + 1, iy + 1, iz + 1) < 0.0,
554                    grid.get(ix, iy + 1, iz + 1) < 0.0,
555                ];
556                let all_in = signs.iter().all(|&s| s);
557                let all_out = signs.iter().all(|&s| !s);
558                if !all_in && !all_out {
559                    count += 1;
560                }
561            }
562        }
563    }
564    count
565}
566#[cfg(test)]
567mod tests {
568    use super::*;
569    use crate::implicit_geometry::BoolOp;
570    use crate::implicit_geometry::SdfBend;
571    use crate::implicit_geometry::SdfBoundedProxy;
572    use crate::implicit_geometry::SdfEllipsoid;
573    use crate::implicit_geometry::SdfExtrude;
574    use crate::implicit_geometry::SdfGyroid;
575    use crate::implicit_geometry::SdfHexagonalPrism;
576    use crate::implicit_geometry::SdfLineSegment;
577    use crate::implicit_geometry::SdfNode;
578    use crate::implicit_geometry::SdfNoiseDisplace;
579    use crate::implicit_geometry::SdfRepeat;
580    use crate::implicit_geometry::SdfRevolution;
581    use crate::implicit_geometry::SdfScale;
582    use crate::implicit_geometry::SdfShell;
583    use crate::implicit_geometry::SdfTranslate;
584    use crate::implicit_geometry::SdfTwist;
585    use crate::implicit_geometry::types::SdfBox;
586    use crate::implicit_geometry::types::SdfDifference;
587    use crate::implicit_geometry::types::SdfIntersection;
588    use crate::implicit_geometry::types::SdfOffset;
589    use crate::implicit_geometry::types::SdfPlane;
590    use crate::implicit_geometry::types::SdfSphere;
591    use crate::implicit_geometry::types::SdfTorus;
592    use crate::implicit_geometry::types::SdfUnion;
593    fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
594        (a - b).abs() < tol
595    }
596    #[test]
597    fn test_sphere_center_is_minus_radius() {
598        let s = SdfSphere::new(2.0);
599        approx_eq(s.dist([0.0, 0.0, 0.0]), -2.0, 1e-12);
600    }
601    #[test]
602    fn test_sphere_on_surface() {
603        let s = SdfSphere::new(1.0);
604        assert!(approx_eq(s.dist([1.0, 0.0, 0.0]), 0.0, 1e-12));
605    }
606    #[test]
607    fn test_sphere_outside() {
608        let s = SdfSphere::new(1.0);
609        assert!(s.dist([2.0, 0.0, 0.0]) > 0.0);
610    }
611    #[test]
612    fn test_plane_on_surface() {
613        let p = SdfPlane::new([0.0, 1.0, 0.0], 0.0);
614        assert!(approx_eq(p.dist([1.0, 0.0, 5.0]), 0.0, 1e-12));
615    }
616    #[test]
617    fn test_plane_above() {
618        let p = SdfPlane::new([0.0, 1.0, 0.0], 0.0);
619        assert!(p.dist([0.0, 1.0, 0.0]) > 0.0);
620    }
621    #[test]
622    fn test_plane_below() {
623        let p = SdfPlane::new([0.0, 1.0, 0.0], 0.0);
624        assert!(p.dist([0.0, -1.0, 0.0]) < 0.0);
625    }
626    #[test]
627    fn test_ellipsoid_inside() {
628        let e = SdfEllipsoid::new(2.0, 1.0, 1.5);
629        assert!(e.dist([0.0, 0.0, 0.0]) < 0.0);
630    }
631    #[test]
632    fn test_ellipsoid_surface_x() {
633        let e = SdfEllipsoid::new(2.0, 1.0, 1.5);
634        let d = e.dist([2.0, 0.0, 0.0]);
635        assert!(approx_eq(d, 0.0, 1e-6), "d={d}");
636    }
637    #[test]
638    fn test_box_inside() {
639        let b = SdfBox::new(1.0, 1.0, 1.0);
640        assert!(b.dist([0.0, 0.0, 0.0]) < 0.0);
641    }
642    #[test]
643    fn test_box_corner() {
644        let b = SdfBox::new(1.0, 1.0, 1.0);
645        let d = b.dist([1.0, 1.0, 1.0]);
646        assert!(approx_eq(d, 0.0, 1e-12));
647    }
648    #[test]
649    fn test_box_outside_face() {
650        let b = SdfBox::new(1.0, 2.0, 1.0);
651        let d = b.dist([2.0, 0.0, 0.0]);
652        assert!(approx_eq(d, 1.0, 1e-12));
653    }
654    #[test]
655    fn test_torus_tube_surface() {
656        let t = SdfTorus::new(3.0, 1.0);
657        let d = t.dist([4.0, 0.0, 0.0]);
658        assert!(approx_eq(d, 0.0, 1e-12), "d={d}");
659    }
660    #[test]
661    fn test_torus_inside_tube() {
662        let t = SdfTorus::new(3.0, 1.0);
663        assert!(t.dist([3.0, 0.0, 0.0]) < 0.0);
664    }
665    #[test]
666    fn test_line_segment_midpoint() {
667        let s = SdfLineSegment::new([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], 0.5);
668        let d = s.dist([1.0, 0.0, 0.0]);
669        assert!(approx_eq(d, -0.5, 1e-12));
670    }
671    #[test]
672    fn test_line_segment_endpoint() {
673        let s = SdfLineSegment::new([0.0, 0.0, 0.0], [2.0, 0.0, 0.0], 0.0);
674        assert!(approx_eq(s.dist([0.0, 0.0, 0.0]), 0.0, 1e-12));
675    }
676    #[test]
677    fn test_union_inside_either() {
678        let a = SdfSphere::new(1.0);
679        let b = SdfTranslate::new(SdfSphere::new(1.0), [3.0, 0.0, 0.0]);
680        let u = SdfUnion::new(a, b);
681        assert!(u.dist([0.0, 0.0, 0.0]) < 0.0);
682        assert!(u.dist([3.0, 0.0, 0.0]) < 0.0);
683    }
684    #[test]
685    fn test_intersection_inside_both() {
686        let a = SdfSphere::new(2.0);
687        let b = SdfBox::new(1.0, 1.0, 1.0);
688        let i = SdfIntersection::new(a, b);
689        assert!(i.dist([0.0, 0.0, 0.0]) < 0.0);
690        assert!(i.dist([3.0, 0.0, 0.0]) > 0.0);
691    }
692    #[test]
693    fn test_difference_removes_inner() {
694        let outer = SdfSphere::new(2.0);
695        let inner = SdfSphere::new(1.0);
696        let d = SdfDifference::new(outer, inner);
697        assert!(d.dist([0.0, 0.0, 0.0]) > 0.0);
698        assert!(d.dist([1.5, 0.0, 0.0]) < 0.0);
699    }
700    #[test]
701    fn test_smooth_union_le_min() {
702        let a = 0.5_f64;
703        let b = 0.3_f64;
704        let su = sdf_smooth_union(a, b, 0.2);
705        assert!(su <= a.min(b) + 1e-12);
706    }
707    #[test]
708    fn test_smooth_union_zero_k() {
709        let a = 0.5_f64;
710        let b = 0.3_f64;
711        assert!(approx_eq(sdf_smooth_union(a, b, 0.0), a.min(b), 1e-12));
712    }
713    #[test]
714    fn test_smooth_intersection_in_range() {
715        let a = 0.5_f64;
716        let b = 0.3_f64;
717        let k = 0.2_f64;
718        let si = sdf_smooth_intersection(a, b, k);
719        assert!(si >= a.min(b) - k, "si={si} below lower bound");
720        assert!(si <= a.max(b) + k, "si={si} above upper bound");
721        assert!(approx_eq(
722            sdf_smooth_intersection(a, b, 0.0),
723            a.max(b),
724            1e-12
725        ));
726    }
727    #[test]
728    fn test_sphere_gradient_unit_length() {
729        let s = SdfSphere::new(1.0);
730        let g = sdf_gradient(&s, [0.6, 0.5, 0.3], 1e-5);
731        let l = len(g);
732        assert!(approx_eq(l, 1.0, 0.01), "gradient length={l}");
733    }
734    #[test]
735    fn test_sphere_normal_points_outward() {
736        let s = SdfSphere::new(1.0);
737        let p = [1.0, 0.0, 0.0];
738        let n = sdf_normal(&s, p, 1e-5);
739        assert!(n[0] > 0.9);
740    }
741    #[test]
742    fn test_offset_grows_sphere() {
743        let s = SdfSphere::new(1.0);
744        let grown = SdfOffset::new(s, 0.5);
745        assert!(grown.dist([1.0, 0.0, 0.0]) < 0.0);
746        let d = grown.dist([1.5, 0.0, 0.0]);
747        assert!(approx_eq(d, 0.0, 1e-12));
748    }
749    #[test]
750    fn test_shell_makes_thin_surface() {
751        let s = SdfSphere::new(1.0);
752        let shell = SdfShell::new(s, 0.1);
753        let d = shell.dist([1.0, 0.0, 0.0]);
754        assert!(approx_eq(d, -0.05, 1e-12), "d={d}");
755    }
756    #[test]
757    fn test_translate_moves_sphere() {
758        let s = SdfTranslate::new(SdfSphere::new(1.0), [5.0, 0.0, 0.0]);
759        assert!(s.dist([5.0, 0.0, 0.0]) < 0.0);
760        assert!(s.dist([0.0, 0.0, 0.0]) > 0.0);
761    }
762    #[test]
763    fn test_scale_doubles_sphere() {
764        let s = SdfScale::new(SdfSphere::new(1.0), 2.0);
765        let d = s.dist([2.0, 0.0, 0.0]);
766        assert!(approx_eq(d, 0.0, 1e-12));
767    }
768    #[test]
769    fn test_ray_march_hits_sphere() {
770        let s = SdfSphere::new(1.0);
771        let res = ray_march(&s, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 128, 20.0, 1e-4);
772        assert!(res.hit, "ray should hit sphere");
773        assert!(approx_eq(res.t, 4.0, 0.01), "t={}", res.t);
774    }
775    #[test]
776    fn test_ray_march_misses_sphere() {
777        let s = SdfSphere::new(1.0);
778        let res = ray_march(&s, [-5.0, 0.0, 0.0], [-1.0, 0.0, 0.0], 128, 20.0, 1e-4);
779        assert!(!res.hit);
780    }
781    #[test]
782    fn test_ray_march_relaxed_hits() {
783        let s = SdfSphere::new(1.0);
784        let res = ray_march_relaxed(&s, [-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 512, 20.0, 1e-4, 1.0);
785        assert!(res.hit, "relaxed ray march should hit sphere");
786    }
787    #[test]
788    fn test_grid_from_sphere_sdf() {
789        let s = SdfSphere::new(1.0);
790        let g = SdfGrid::from_sdf(&s, 10, 10, 10, [-2.0, -2.0, -2.0], 0.4);
791        assert!(g.get(5, 5, 5) < 0.0);
792    }
793    #[test]
794    fn test_grid_dilate_erode_inverse() {
795        let s = SdfSphere::new(1.0);
796        let mut g = SdfGrid::from_sdf(&s, 6, 6, 6, [-1.5, -1.5, -1.5], 0.5);
797        let v_orig = g.get(3, 3, 3);
798        g.dilate(0.3);
799        g.erode(0.3);
800        assert!(approx_eq(g.get(3, 3, 3), v_orig, 1e-12));
801    }
802    #[test]
803    fn test_grid_interpolate_center() {
804        let s = SdfSphere::new(1.0);
805        let g = SdfGrid::from_sdf(&s, 20, 20, 20, [-2.0, -2.0, -2.0], 0.2);
806        let interp = g.interpolate([0.0, 0.0, 0.0]);
807        assert!(interp < -0.5, "interp={interp}");
808    }
809    #[test]
810    fn test_extract_isosurface_has_triangles() {
811        let s = SdfSphere::new(1.0);
812        let g = SdfGrid::from_sdf(&s, 10, 10, 10, [-2.0, -2.0, -2.0], 0.4);
813        let mesh = extract_isosurface(&g);
814        assert!(
815            !mesh.triangles.is_empty(),
816            "isosurface should produce triangles"
817        );
818    }
819    #[test]
820    fn test_count_surface_cells() {
821        let s = SdfSphere::new(1.0);
822        let g = SdfGrid::from_sdf(&s, 10, 10, 10, [-2.0, -2.0, -2.0], 0.4);
823        let cnt = count_surface_cells(&g);
824        assert!(cnt > 0, "should have surface cells");
825    }
826    #[test]
827    fn test_point_query_inside() {
828        let s = SdfSphere::new(2.0);
829        let res = sdf_point_query(&s, [0.0, 0.0, 0.0], 1e-5);
830        assert!(res.is_some());
831        assert!(res.unwrap().depth > 0.0);
832    }
833    #[test]
834    fn test_point_query_outside() {
835        let s = SdfSphere::new(1.0);
836        let res = sdf_point_query(&s, [5.0, 0.0, 0.0], 1e-5);
837        assert!(res.is_none());
838    }
839    #[test]
840    fn test_contact_manifold_sphere() {
841        let s = SdfSphere::new(2.0);
842        let pts: Vec<[f64; 3]> = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [3.0, 0.0, 0.0]];
843        let contacts = sdf_contact_manifold(&s, &pts, 0.0, 1e-5);
844        assert_eq!(contacts.len(), 2, "{} contacts", contacts.len());
845    }
846    #[test]
847    fn test_noise_displace_varies() {
848        let s = SdfSphere::new(1.0);
849        let noisy = SdfNoiseDisplace::new(s, 3.0, 0.2, 4);
850        let d1 = noisy.dist([1.0, 0.0, 0.0]);
851        let d2 = noisy.dist([0.0, 1.0, 0.0]);
852        let _ = (d1, d2);
853    }
854    #[test]
855    fn test_fbm_noise_bounded() {
856        for i in 0..20 {
857            let v = fbm_noise(
858                [i as f64 * 0.3, i as f64 * 0.7, i as f64 * 0.5],
859                4,
860                2.0,
861                0.5,
862            );
863            assert!(v.abs() <= 2.0, "fbm out of range: {v}");
864        }
865    }
866    #[test]
867    fn test_sdf_node_union() {
868        let a = SdfNode::leaf(SdfSphere::new(1.0));
869        let b = SdfNode::leaf(SdfTranslate::new(SdfSphere::new(1.0), [4.0, 0.0, 0.0]));
870        let u = SdfNode::combine(BoolOp::Union, a, b, 0.0);
871        assert!(u.eval([0.0, 0.0, 0.0]) < 0.0);
872        assert!(u.eval([4.0, 0.0, 0.0]) < 0.0);
873    }
874    #[test]
875    fn test_sdf_node_smooth_union() {
876        let a = SdfNode::leaf(SdfSphere::new(1.0));
877        let b = SdfNode::leaf(SdfTranslate::new(SdfSphere::new(1.0), [1.5, 0.0, 0.0]));
878        let su = SdfNode::combine(BoolOp::SmoothUnion, a, b, 0.5);
879        let d = su.eval([0.75, 0.0, 0.0]);
880        assert!(d < 0.0, "d={d}");
881    }
882    #[test]
883    fn test_twist_identity_at_zero_strength() {
884        let s = SdfBox::new(1.0, 1.0, 1.0);
885        let t = SdfTwist::new(SdfBox::new(1.0, 1.0, 1.0), 0.0);
886        let p = [0.5, 0.3, 0.2];
887        assert!(approx_eq(s.dist(p), t.dist(p), 1e-12));
888    }
889    #[test]
890    fn test_bend_identity_at_zero_strength() {
891        let s = SdfBox::new(1.0, 1.0, 1.0);
892        let b = SdfBend::new(SdfBox::new(1.0, 1.0, 1.0), 0.0);
893        let p = [0.5, 0.3, 0.2];
894        assert!(approx_eq(s.dist(p), b.dist(p), 1e-12));
895    }
896    #[test]
897    fn test_repeat_periodicity() {
898        let s = SdfRepeat::new(SdfSphere::new(0.4), 2.0, 2.0, 2.0);
899        let d0 = s.dist([0.0, 0.0, 0.0]);
900        let d1 = s.dist([2.0, 0.0, 0.0]);
901        assert!(approx_eq(d0, d1, 1e-10), "d0={d0} d1={d1}");
902    }
903    #[test]
904    fn test_gyroid_surface_exists() {
905        let g = SdfGyroid::new(1.0, 0.05);
906        let d0 = g.dist([0.0, 0.0, 0.0]);
907        let d1 = g.dist([0.25, 0.0, 0.0]);
908        let _ = (d0, d1);
909        assert!(d0.is_finite());
910        assert!(d1.is_finite());
911    }
912    #[test]
913    fn test_bounded_proxy_outside_bounds() {
914        let inner = SdfSphere::new(1.0);
915        let proxy = SdfBoundedProxy::new(inner, [0.0, 0.0, 0.0], 1.5);
916        let d = proxy.dist([10.0, 0.0, 0.0]);
917        assert!(d > 0.0);
918        assert!(approx_eq(d, 10.0 - 1.5, 0.01));
919    }
920    #[test]
921    fn test_sphere_mean_curvature() {
922        let s = SdfSphere::new(2.0);
923        let p = [2.0, 0.0, 0.0];
924        let h = sdf_mean_curvature(&s, p, 1e-4);
925        assert!(h.is_finite(), "curvature should be finite: {h}");
926    }
927    #[test]
928    fn test_revolution_sphere_equivalent() {
929        let sphere_profile = |r: f64, y: f64| (r * r + y * y).sqrt() - 1.0;
930        let rev = SdfRevolution::new(sphere_profile);
931        let s = SdfSphere::new(1.0);
932        let p = [0.6, 0.5, 0.3];
933        assert!(approx_eq(rev.dist(p), s.dist(p), 1e-10));
934    }
935    #[test]
936    fn test_extrude_circle_gives_cylinder() {
937        let circle_profile = |xz: [f64; 2]| (xz[0] * xz[0] + xz[1] * xz[1]).sqrt() - 1.0;
938        let cyl = SdfExtrude::new(circle_profile, 2.0);
939        assert!(cyl.dist([0.0, 0.0, 0.0]) < 0.0);
940        assert!(cyl.dist([2.0, 0.0, 0.0]) > 0.0);
941        assert!(cyl.dist([0.0, 3.0, 0.0]) > 0.0);
942    }
943    #[test]
944    fn test_hex_prism_center_inside() {
945        let h = SdfHexagonalPrism::new(1.0, 1.0);
946        assert!(h.dist([0.0, 0.0, 0.0]) < 0.0);
947    }
948    #[test]
949    fn test_closest_point_sphere() {
950        let s = SdfSphere::new(1.0);
951        let (_q, d) = sdf_closest_point(&s, [0.5, 0.0, 0.0], 1e-5, 50);
952        assert!(d.abs() < 1e-3, "final dist={d}");
953    }
954    #[test]
955    fn test_ao_near_plane_unoccluded() {
956        let s = SdfSphere::new(1.0);
957        let n = [0.0, 1.0, 0.0];
958        let p = [0.0, 1.0, 0.0];
959        let ao = sdf_ambient_occlusion(&s, p, n, 5, 1.0);
960        assert!(ao.is_finite());
961        assert!((0.0..=1.0).contains(&ao));
962    }
963    #[test]
964    fn test_soft_shadow_unobstructed() {
965        let s = SdfSphere::new(0.5);
966        let shadow = sdf_soft_shadow(&s, [-3.0, 0.0, 0.0], [3.0, 0.0, 0.0], 8.0, 1e-4);
967        assert!(shadow < 1.0, "shadow={shadow}");
968    }
969}