Skip to main content

oxiphysics_geometry/signed_distance_field/
scene.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! SDF scene: composable scene of multiple SDF objects with Boolean operations.
5
6use super::combinators::{sdf_smooth_union, sdf_union};
7use super::helpers::{scale3, sub3};
8use super::operators::sdf_normal;
9use super::primitives::{sdf_box, sdf_capsule, sdf_cylinder, sdf_plane, sdf_sphere, sdf_torus};
10use super::ray_marcher::{RayMarchHit, RayMarcher};
11
12// ─────────────────────────────────────────────────────────────────────────────
13// SDF Scene
14// ─────────────────────────────────────────────────────────────────────────────
15
16/// Composable SDF object with a transform and operation.
17#[derive(Debug, Clone)]
18pub struct SdfObject {
19    /// Object name for debugging.
20    pub name: String,
21    /// Translation applied before evaluating SDF.
22    pub translation: [f64; 3],
23    /// Uniform scale factor.
24    pub scale: f64,
25    /// SDF primitive kind.
26    pub kind: SdfKind,
27}
28
29/// Discriminated union of SDF primitive types.
30#[derive(Debug, Clone)]
31pub enum SdfKind {
32    /// Sphere with radius.
33    Sphere(f64),
34    /// Axis-aligned box with half-extents.
35    Box([f64; 3]),
36    /// Capsule from A to B with radius.
37    Capsule([f64; 3], [f64; 3], f64),
38    /// Cylinder with radius and half-height.
39    Cylinder(f64, f64),
40    /// Torus with major and minor radii.
41    Torus(f64, f64),
42    /// Plane with normal and offset.
43    Plane([f64; 3], f64),
44}
45
46impl SdfObject {
47    /// Construct an SDF sphere object.
48    pub fn sphere(name: &str, radius: f64, translation: [f64; 3]) -> Self {
49        Self {
50            name: name.to_string(),
51            translation,
52            scale: 1.0,
53            kind: SdfKind::Sphere(radius),
54        }
55    }
56
57    /// Construct an SDF box object.
58    pub fn box_shape(name: &str, half_extents: [f64; 3], translation: [f64; 3]) -> Self {
59        Self {
60            name: name.to_string(),
61            translation,
62            scale: 1.0,
63            kind: SdfKind::Box(half_extents),
64        }
65    }
66
67    /// Evaluate the SDF at world-space point `p`.
68    pub fn evaluate(&self, p: [f64; 3]) -> f64 {
69        // Transform point to local space
70        let local = scale3(sub3(p, self.translation), 1.0 / self.scale);
71        let raw = match &self.kind {
72            SdfKind::Sphere(r) => sdf_sphere(local, *r),
73            SdfKind::Box(b) => sdf_box(local, *b),
74            SdfKind::Capsule(a, b, r) => sdf_capsule(local, *a, *b, *r),
75            SdfKind::Cylinder(r, h) => sdf_cylinder(local, *r, *h),
76            SdfKind::Torus(r1, r2) => sdf_torus(local, *r1, *r2),
77            SdfKind::Plane(n, d) => sdf_plane(local, *n, *d),
78        };
79        raw * self.scale
80    }
81}
82
83/// An SDF scene composed of multiple objects with Boolean operations.
84#[derive(Debug, Clone, Default)]
85pub struct SdfScene {
86    /// Objects in the scene.
87    pub objects: Vec<SdfObject>,
88    /// Smooth blend radius for scene-level union.
89    pub blend_k: f64,
90}
91
92impl SdfScene {
93    /// Construct an empty scene.
94    pub fn new() -> Self {
95        Self {
96            objects: Vec::new(),
97            blend_k: 0.0,
98        }
99    }
100
101    /// Add an object to the scene.
102    pub fn add(&mut self, obj: SdfObject) {
103        self.objects.push(obj);
104    }
105
106    /// Evaluate the scene SDF (smooth union of all objects) at point `p`.
107    pub fn evaluate(&self, p: [f64; 3]) -> f64 {
108        if self.objects.is_empty() {
109            return f64::MAX;
110        }
111        let mut d = self.objects[0].evaluate(p);
112        for obj in &self.objects[1..] {
113            let di = obj.evaluate(p);
114            d = if self.blend_k > 0.0 {
115                sdf_smooth_union(d, di, self.blend_k)
116            } else {
117                sdf_union(d, di)
118            };
119        }
120        d
121    }
122
123    /// Cast a ray through the scene using sphere tracing.
124    pub fn ray_cast(&self, origin: [f64; 3], dir: [f64; 3]) -> Option<RayMarchHit> {
125        let marcher = RayMarcher::new();
126        marcher.march(&|p| self.evaluate(p), origin, dir)
127    }
128
129    /// Compute gradient (surface normal) at point `p`.
130    pub fn normal(&self, p: [f64; 3]) -> [f64; 3] {
131        sdf_normal(&|q| self.evaluate(q), p, 1e-4)
132    }
133}