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