Skip to main content

oxiphysics_gpu/sdf_compute/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4use rayon::prelude::*;
5
6/// Boolean CSG operations combining two [`SdfShape`] primitives.
7#[derive(Debug, Clone)]
8pub enum SdfCombine {
9    /// Union: min(d_a, d_b).
10    Union(SdfShape, SdfShape),
11    /// Intersection: max(d_a, d_b).
12    Intersection(SdfShape, SdfShape),
13    /// Subtraction of b from a: max(d_a, -d_b).
14    Subtraction(SdfShape, SdfShape),
15    /// Smooth union with blending factor k.
16    SmoothUnion(SdfShape, SdfShape, f64),
17}
18impl SdfCombine {
19    /// Signed distance from point `p` to this combined shape.
20    pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
21        match self {
22            SdfCombine::Union(a, b) => a.signed_distance(p).min(b.signed_distance(p)),
23            SdfCombine::Intersection(a, b) => a.signed_distance(p).max(b.signed_distance(p)),
24            SdfCombine::Subtraction(a, b) => a.signed_distance(p).max(-b.signed_distance(p)),
25            SdfCombine::SmoothUnion(a, b, k) => {
26                let da = a.signed_distance(p);
27                let db = b.signed_distance(p);
28                let h = (0.5 + 0.5 * (db - da) / k).clamp(0.0, 1.0);
29                db * (1.0 - h) + da * h - k * h * (1.0 - h)
30            }
31        }
32    }
33}
34/// An analytic signed distance shape primitive.
35#[derive(Debug, Clone)]
36pub enum SdfShape {
37    /// A sphere with a given centre and radius.
38    Sphere {
39        /// Centre of the sphere.
40        center: [f64; 3],
41        /// Radius.
42        r: f64,
43    },
44    /// An axis-aligned box specified by centre and half-extents.
45    Box3 {
46        /// Centre of the box.
47        center: [f64; 3],
48        /// Half-extents in each axis.
49        half: [f64; 3],
50    },
51    /// A capsule (cylinder with hemispherical caps) between two end-points.
52    Capsule {
53        /// First end-point.
54        a: [f64; 3],
55        /// Second end-point.
56        b: [f64; 3],
57        /// Radius of the capsule.
58        r: f64,
59    },
60}
61impl SdfShape {
62    /// Signed distance from point `p` to this shape.
63    ///
64    /// Negative values are inside the shape.
65    pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
66        match self {
67            SdfShape::Sphere { center, r } => {
68                let dx = p[0] - center[0];
69                let dy = p[1] - center[1];
70                let dz = p[2] - center[2];
71                (dx * dx + dy * dy + dz * dz).sqrt() - r
72            }
73            SdfShape::Box3 { center, half } => {
74                let qx = (p[0] - center[0]).abs() - half[0];
75                let qy = (p[1] - center[1]).abs() - half[1];
76                let qz = (p[2] - center[2]).abs() - half[2];
77                let ext = (qx.max(0.0).powi(2) + qy.max(0.0).powi(2) + qz.max(0.0).powi(2)).sqrt();
78                let interior = qx.max(qy).max(qz).min(0.0);
79                ext + interior
80            }
81            SdfShape::Capsule { a, b, r } => {
82                let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
83                let ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
84                let ab_len2 = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
85                let t = if ab_len2 < 1e-12 {
86                    0.0
87                } else {
88                    ((ap[0] * ab[0] + ap[1] * ab[1] + ap[2] * ab[2]) / ab_len2).clamp(0.0, 1.0)
89                };
90                let closest = [a[0] + t * ab[0], a[1] + t * ab[1], a[2] + t * ab[2]];
91                let dx = p[0] - closest[0];
92                let dy = p[1] - closest[1];
93                let dz = p[2] - closest[2];
94                (dx * dx + dy * dy + dz * dz).sqrt() - r
95            }
96        }
97    }
98}
99/// A 3-D SDF grid filled analytically via `generate_sdf_grid`.
100///
101/// Separate from the existing [`SdfGrid`] to keep naming unambiguous.
102#[derive(Debug, Clone)]
103pub struct GpuSdfGrid {
104    /// Flat storage: index = `ix * ny * nz + iy * nz + iz`.
105    pub data: Vec<f64>,
106    /// Number of cells in x.
107    pub nx: usize,
108    /// Number of cells in y.
109    pub ny: usize,
110    /// Number of cells in z.
111    pub nz: usize,
112    /// World-space origin of the (0,0,0) corner.
113    pub origin: [f64; 3],
114    /// Uniform cell spacing.
115    pub cell_size: f64,
116}
117impl GpuSdfGrid {
118    /// Create a new grid filled with zeros.
119    pub fn new(nx: usize, ny: usize, nz: usize, origin: [f64; 3], cell_size: f64) -> Self {
120        Self {
121            data: vec![0.0_f64; nx * ny * nz],
122            nx,
123            ny,
124            nz,
125            origin,
126            cell_size,
127        }
128    }
129    /// Flat index for cell `(ix, iy, iz)`.
130    #[inline]
131    pub fn index(&self, ix: usize, iy: usize, iz: usize) -> usize {
132        ix * self.ny * self.nz + iy * self.nz + iz
133    }
134    /// Read the value at cell `(ix, iy, iz)`.
135    #[inline]
136    pub fn get(&self, ix: usize, iy: usize, iz: usize) -> f64 {
137        self.data[self.index(ix, iy, iz)]
138    }
139    /// World-space centre of cell `(ix, iy, iz)`.
140    #[inline]
141    pub fn cell_center(&self, ix: usize, iy: usize, iz: usize) -> [f64; 3] {
142        [
143            self.origin[0] + (ix as f64 + 0.5) * self.cell_size,
144            self.origin[1] + (iy as f64 + 0.5) * self.cell_size,
145            self.origin[2] + (iz as f64 + 0.5) * self.cell_size,
146        ]
147    }
148    /// Trilinear interpolation of the SDF at world-space point `p`.
149    ///
150    /// Points outside the grid are clamped to the nearest grid value.
151    pub fn sample_trilinear(&self, p: [f64; 3]) -> f64 {
152        let fx = (p[0] - self.origin[0]) / self.cell_size - 0.5;
153        let fy = (p[1] - self.origin[1]) / self.cell_size - 0.5;
154        let fz = (p[2] - self.origin[2]) / self.cell_size - 0.5;
155        let ix = fx.floor().clamp(0.0, (self.nx - 1) as f64) as usize;
156        let iy = fy.floor().clamp(0.0, (self.ny - 1) as f64) as usize;
157        let iz = fz.floor().clamp(0.0, (self.nz - 1) as f64) as usize;
158        let tx = (fx - ix as f64).clamp(0.0, 1.0);
159        let ty = (fy - iy as f64).clamp(0.0, 1.0);
160        let tz = (fz - iz as f64).clamp(0.0, 1.0);
161        let nx1 = (ix + 1).min(self.nx - 1);
162        let ny1 = (iy + 1).min(self.ny - 1);
163        let nz1 = (iz + 1).min(self.nz - 1);
164        let c000 = self.get(ix, iy, iz);
165        let c100 = self.get(nx1, iy, iz);
166        let c010 = self.get(ix, ny1, iz);
167        let c110 = self.get(nx1, ny1, iz);
168        let c001 = self.get(ix, iy, nz1);
169        let c101 = self.get(nx1, iy, nz1);
170        let c011 = self.get(ix, ny1, nz1);
171        let c111 = self.get(nx1, ny1, nz1);
172        let c00 = c000 * (1.0 - tx) + c100 * tx;
173        let c10 = c010 * (1.0 - tx) + c110 * tx;
174        let c01 = c001 * (1.0 - tx) + c101 * tx;
175        let c11 = c011 * (1.0 - tx) + c111 * tx;
176        let c0 = c00 * (1.0 - ty) + c10 * ty;
177        let c1 = c01 * (1.0 - ty) + c11 * ty;
178        c0 * (1.0 - tz) + c1 * tz
179    }
180    /// Estimate the SDF gradient at `p` using central finite differences.
181    ///
182    /// The step size is `cell_size * 0.5`.
183    pub fn gradient_at(&self, p: [f64; 3]) -> [f64; 3] {
184        let h = self.cell_size * 0.5;
185        let gx = (self.sample_trilinear([p[0] + h, p[1], p[2]])
186            - self.sample_trilinear([p[0] - h, p[1], p[2]]))
187            / (2.0 * h);
188        let gy = (self.sample_trilinear([p[0], p[1] + h, p[2]])
189            - self.sample_trilinear([p[0], p[1] - h, p[2]]))
190            / (2.0 * h);
191        let gz = (self.sample_trilinear([p[0], p[1], p[2] + h])
192            - self.sample_trilinear([p[0], p[1], p[2] - h]))
193            / (2.0 * h);
194        [gx, gy, gz]
195    }
196}
197/// Sphere tracing (ray marching) result.
198pub struct SphereTraceResult {
199    /// Whether the ray hit the surface.
200    pub hit: bool,
201    /// Position of the hit point (if hit).
202    pub position: [f64; 3],
203    /// Distance traveled along the ray.
204    pub t: f64,
205    /// Number of iterations used.
206    pub iterations: usize,
207}
208/// A 3-D signed distance field stored on a uniform Cartesian grid.
209pub struct SdfGrid {
210    /// Number of cells in the x direction.
211    pub nx: usize,
212    /// Number of cells in the y direction.
213    pub ny: usize,
214    /// Number of cells in the z direction.
215    pub nz: usize,
216    /// Uniform cell spacing (same in all directions).
217    pub dx: f64,
218    /// World-space coordinate of the (0,0,0) grid corner.
219    pub origin: [f64; 3],
220    /// Flat storage: index = `i*ny*nz + j*nz + k`.
221    pub values: Vec<f64>,
222}
223impl SdfGrid {
224    /// Create a new grid filled with `f64::MAX`.
225    pub fn new(nx: usize, ny: usize, nz: usize, dx: f64, origin: [f64; 3]) -> Self {
226        let n = nx * ny * nz;
227        Self {
228            nx,
229            ny,
230            nz,
231            dx,
232            origin,
233            values: vec![f64::MAX; n],
234        }
235    }
236    /// Flat index for cell `(i, j, k)`.
237    #[inline]
238    pub fn index(&self, i: usize, j: usize, k: usize) -> usize {
239        i * self.ny * self.nz + j * self.nz + k
240    }
241    /// World-space centre of cell `(i, j, k)`.
242    #[inline]
243    pub fn world_pos(&self, i: usize, j: usize, k: usize) -> [f64; 3] {
244        [
245            self.origin[0] + (i as f64 + 0.5) * self.dx,
246            self.origin[1] + (j as f64 + 0.5) * self.dx,
247            self.origin[2] + (k as f64 + 0.5) * self.dx,
248        ]
249    }
250    /// Read the SDF value at `(i, j, k)`.
251    #[inline]
252    pub fn get(&self, i: usize, j: usize, k: usize) -> f64 {
253        self.values[self.index(i, j, k)]
254    }
255    /// Write the SDF value at `(i, j, k)`.
256    #[inline]
257    pub fn set(&mut self, i: usize, j: usize, k: usize, v: f64) {
258        let idx = self.index(i, j, k);
259        self.values[idx] = v;
260    }
261    /// Fill the grid with the signed distance to a sphere.
262    pub fn compute_sphere_sdf(&mut self, center: [f64; 3], radius: f64) {
263        let _nx = self.nx;
264        let ny = self.ny;
265        let nz = self.nz;
266        let dx = self.dx;
267        let origin = self.origin;
268        self.values.par_iter_mut().enumerate().for_each(|(idx, v)| {
269            let i = idx / (ny * nz);
270            let j = (idx / nz) % ny;
271            let k = idx % nz;
272            let px = origin[0] + (i as f64 + 0.5) * dx;
273            let py = origin[1] + (j as f64 + 0.5) * dx;
274            let pz = origin[2] + (k as f64 + 0.5) * dx;
275            let dist =
276                ((px - center[0]).powi(2) + (py - center[1]).powi(2) + (pz - center[2]).powi(2))
277                    .sqrt();
278            *v = dist - radius;
279        });
280    }
281    /// Fill the grid with the signed distance to an axis-aligned box.
282    pub fn compute_box_sdf(&mut self, box_center: [f64; 3], half_extents: [f64; 3]) {
283        let _nx = self.nx;
284        let ny = self.ny;
285        let nz = self.nz;
286        let dx = self.dx;
287        let origin = self.origin;
288        self.values.par_iter_mut().enumerate().for_each(|(idx, v)| {
289            let i = idx / (ny * nz);
290            let j = (idx / nz) % ny;
291            let k = idx % nz;
292            let px = origin[0] + (i as f64 + 0.5) * dx - box_center[0];
293            let py = origin[1] + (j as f64 + 0.5) * dx - box_center[1];
294            let pz = origin[2] + (k as f64 + 0.5) * dx - box_center[2];
295            let qx = px.abs() - half_extents[0];
296            let qy = py.abs() - half_extents[1];
297            let qz = pz.abs() - half_extents[2];
298            let ext = (qx.max(0.0).powi(2) + qy.max(0.0).powi(2) + qz.max(0.0).powi(2)).sqrt();
299            let interior = qx.max(qy).max(qz).min(0.0);
300            *v = ext + interior;
301        });
302    }
303    /// Fill the grid with the signed distance to an infinite cylinder
304    /// aligned along the z-axis.
305    pub fn compute_cylinder_sdf(&mut self, center: [f64; 2], radius: f64, half_height: f64) {
306        let ny = self.ny;
307        let nz = self.nz;
308        let dx = self.dx;
309        let origin = self.origin;
310        self.values.par_iter_mut().enumerate().for_each(|(idx, v)| {
311            let i = idx / (ny * nz);
312            let j = (idx / nz) % ny;
313            let k = idx % nz;
314            let px = origin[0] + (i as f64 + 0.5) * dx - center[0];
315            let py = origin[1] + (j as f64 + 0.5) * dx - center[1];
316            let pz = origin[2] + (k as f64 + 0.5) * dx;
317            let r = (px * px + py * py).sqrt();
318            let d_radial = r - radius;
319            let d_axial = pz.abs() - half_height;
320            let ext = (d_radial.max(0.0).powi(2) + d_axial.max(0.0).powi(2)).sqrt();
321            let interior = d_radial.max(d_axial).min(0.0);
322            *v = ext + interior;
323        });
324    }
325    /// Fill the grid with the signed distance to a torus
326    /// centred at the origin in the xz-plane.
327    pub fn compute_torus_sdf(&mut self, center: [f64; 3], major_radius: f64, minor_radius: f64) {
328        let ny = self.ny;
329        let nz = self.nz;
330        let dx = self.dx;
331        let origin = self.origin;
332        self.values.par_iter_mut().enumerate().for_each(|(idx, v)| {
333            let i = idx / (ny * nz);
334            let j = (idx / nz) % ny;
335            let k = idx % nz;
336            let px = origin[0] + (i as f64 + 0.5) * dx - center[0];
337            let py = origin[1] + (j as f64 + 0.5) * dx - center[1];
338            let pz = origin[2] + (k as f64 + 0.5) * dx - center[2];
339            let q_x = (px * px + pz * pz).sqrt() - major_radius;
340            let dist = (q_x * q_x + py * py).sqrt() - minor_radius;
341            *v = dist;
342        });
343    }
344    /// Estimate the gradient of the SDF at `(i, j, k)` using central differences.
345    pub fn gradient_at(&self, i: usize, j: usize, k: usize) -> [f64; 3] {
346        let two_dx = 2.0 * self.dx;
347        let gx = if i == 0 {
348            (self.get(i + 1, j, k) - self.get(i, j, k)) / self.dx
349        } else if i + 1 == self.nx {
350            (self.get(i, j, k) - self.get(i - 1, j, k)) / self.dx
351        } else {
352            (self.get(i + 1, j, k) - self.get(i - 1, j, k)) / two_dx
353        };
354        let gy = if j == 0 {
355            (self.get(i, j + 1, k) - self.get(i, j, k)) / self.dx
356        } else if j + 1 == self.ny {
357            (self.get(i, j, k) - self.get(i, j - 1, k)) / self.dx
358        } else {
359            (self.get(i, j + 1, k) - self.get(i, j - 1, k)) / two_dx
360        };
361        let gz = if k == 0 {
362            (self.get(i, j, k + 1) - self.get(i, j, k)) / self.dx
363        } else if k + 1 == self.nz {
364            (self.get(i, j, k) - self.get(i, j, k - 1)) / self.dx
365        } else {
366            (self.get(i, j, k + 1) - self.get(i, j, k - 1)) / two_dx
367        };
368        [gx, gy, gz]
369    }
370    /// Total number of cells in the grid.
371    #[inline]
372    pub fn total_cells(&self) -> usize {
373        self.nx * self.ny * self.nz
374    }
375    /// Trilinear interpolation of the SDF at an arbitrary world-space point.
376    ///
377    /// Returns `None` if the point is outside the grid.
378    pub fn sample(&self, pos: [f64; 3]) -> Option<f64> {
379        let fx = (pos[0] - self.origin[0]) / self.dx - 0.5;
380        let fy = (pos[1] - self.origin[1]) / self.dx - 0.5;
381        let fz = (pos[2] - self.origin[2]) / self.dx - 0.5;
382        if fx < 0.0 || fy < 0.0 || fz < 0.0 {
383            return None;
384        }
385        let ix = fx as usize;
386        let iy = fy as usize;
387        let iz = fz as usize;
388        if ix + 1 >= self.nx || iy + 1 >= self.ny || iz + 1 >= self.nz {
389            return None;
390        }
391        let tx = fx - ix as f64;
392        let ty = fy - iy as f64;
393        let tz = fz - iz as f64;
394        let c000 = self.get(ix, iy, iz);
395        let c100 = self.get(ix + 1, iy, iz);
396        let c010 = self.get(ix, iy + 1, iz);
397        let c110 = self.get(ix + 1, iy + 1, iz);
398        let c001 = self.get(ix, iy, iz + 1);
399        let c101 = self.get(ix + 1, iy, iz + 1);
400        let c011 = self.get(ix, iy + 1, iz + 1);
401        let c111 = self.get(ix + 1, iy + 1, iz + 1);
402        let c00 = c000 * (1.0 - tx) + c100 * tx;
403        let c10 = c010 * (1.0 - tx) + c110 * tx;
404        let c01 = c001 * (1.0 - tx) + c101 * tx;
405        let c11 = c011 * (1.0 - tx) + c111 * tx;
406        let c0 = c00 * (1.0 - ty) + c10 * ty;
407        let c1 = c01 * (1.0 - ty) + c11 * ty;
408        Some(c0 * (1.0 - tz) + c1 * tz)
409    }
410    /// Compute the gradient at an arbitrary point using trilinear interpolation
411    /// of gradient values.
412    pub fn gradient_at_point(&self, pos: [f64; 3]) -> Option<[f64; 3]> {
413        let fx = (pos[0] - self.origin[0]) / self.dx - 0.5;
414        let fy = (pos[1] - self.origin[1]) / self.dx - 0.5;
415        let fz = (pos[2] - self.origin[2]) / self.dx - 0.5;
416        if fx < 0.0 || fy < 0.0 || fz < 0.0 {
417            return None;
418        }
419        let ix = fx as usize;
420        let iy = fy as usize;
421        let iz = fz as usize;
422        if ix + 1 >= self.nx || iy + 1 >= self.ny || iz + 1 >= self.nz {
423            return None;
424        }
425        let eps = self.dx * 0.5;
426        let gx = (self.sample([pos[0] + eps, pos[1], pos[2]]).unwrap_or(0.0)
427            - self.sample([pos[0] - eps, pos[1], pos[2]]).unwrap_or(0.0))
428            / (2.0 * eps);
429        let gy = (self.sample([pos[0], pos[1] + eps, pos[2]]).unwrap_or(0.0)
430            - self.sample([pos[0], pos[1] - eps, pos[2]]).unwrap_or(0.0))
431            / (2.0 * eps);
432        let gz = (self.sample([pos[0], pos[1], pos[2] + eps]).unwrap_or(0.0)
433            - self.sample([pos[0], pos[1], pos[2] - eps]).unwrap_or(0.0))
434            / (2.0 * eps);
435        Some([gx, gy, gz])
436    }
437}
438/// A triangle in 3-D space.
439#[derive(Debug, Clone)]
440pub struct Triangle {
441    /// Three vertex positions.
442    pub v: [[f64; 3]; 3],
443}
444/// Query result for closest point / normal / distance.
445#[derive(Debug, Clone)]
446pub struct DistanceQuery {
447    /// Signed distance at the query point.
448    pub distance: f64,
449    /// Normal direction at the query point (normalised gradient).
450    pub normal: [f64; 3],
451    /// Closest point on the surface (approximate, from gradient ray march).
452    pub closest_point: [f64; 3],
453    /// Whether the query point is inside the surface.
454    pub is_inside: bool,
455}