Skip to main content

oxiphysics_geometry/signed_distance_field/
ray_marcher.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Sphere-tracing ray marcher for SDF scenes.
5
6use super::helpers::{add3, normalize3, scale3};
7use super::operators::sdf_normal;
8
9// ─────────────────────────────────────────────────────────────────────────────
10// Ray Marching (sphere tracing)
11// ─────────────────────────────────────────────────────────────────────────────
12
13/// Result of a ray-march intersection test.
14#[derive(Debug, Clone)]
15pub struct RayMarchHit {
16    /// Distance along the ray \[same units as SDF\].
17    pub t: f64,
18    /// Hit point.
19    pub point: [f64; 3],
20    /// Outward surface normal at the hit point.
21    pub normal: [f64; 3],
22    /// SDF value at the hit point (should be near zero).
23    pub sdf_value: f64,
24    /// Number of iterations used.
25    pub steps: usize,
26}
27
28/// Sphere-tracing ray marcher for SDF scenes.
29#[derive(Debug, Clone)]
30pub struct RayMarcher {
31    /// Maximum ray-march distance.
32    pub t_max: f64,
33    /// Surface hit tolerance.
34    pub tolerance: f64,
35    /// Maximum sphere-tracing iterations.
36    pub max_steps: usize,
37    /// Step scale factor (< 1 for over-relaxation safety).
38    pub step_scale: f64,
39}
40
41impl RayMarcher {
42    /// Construct a ray marcher with default parameters.
43    pub fn new() -> Self {
44        Self {
45            t_max: 100.0,
46            tolerance: 1e-4,
47            max_steps: 256,
48            step_scale: 0.95,
49        }
50    }
51
52    /// Construct a ray marcher with custom parameters.
53    pub fn with_params(t_max: f64, tolerance: f64, max_steps: usize, step_scale: f64) -> Self {
54        Self {
55            t_max,
56            tolerance,
57            max_steps,
58            step_scale,
59        }
60    }
61
62    /// March a ray from `origin` in direction `dir` (should be unit length)
63    /// through the SDF `f`.
64    ///
65    /// Returns `Some(RayMarchHit)` if a surface is found, `None` otherwise.
66    pub fn march<F>(&self, f: &F, origin: [f64; 3], dir: [f64; 3]) -> Option<RayMarchHit>
67    where
68        F: Fn([f64; 3]) -> f64,
69    {
70        let d = normalize3(dir);
71        let mut t = 0.0;
72        for step in 0..self.max_steps {
73            let p = add3(origin, scale3(d, t));
74            let sdf = f(p);
75            if sdf.abs() < self.tolerance {
76                let normal = sdf_normal(f, p, 1e-4);
77                return Some(RayMarchHit {
78                    t,
79                    point: p,
80                    normal,
81                    sdf_value: sdf,
82                    steps: step + 1,
83                });
84            }
85            if t > self.t_max {
86                break;
87            }
88            t += sdf.abs() * self.step_scale;
89        }
90        None
91    }
92
93    /// Cast a shadow ray: returns true if the path from `p` toward `light_dir`
94    /// is unobstructed within distance `max_dist`.
95    pub fn shadow<F>(&self, f: &F, p: [f64; 3], light_dir: [f64; 3], max_dist: f64) -> f64
96    where
97        F: Fn([f64; 3]) -> f64,
98    {
99        let d = normalize3(light_dir);
100        let mut t = 0.01; // offset to avoid self-intersection
101        let mut shadow = 1.0_f64;
102        for _ in 0..self.max_steps {
103            if t >= max_dist {
104                break;
105            }
106            let q = add3(p, scale3(d, t));
107            let h = f(q);
108            if h < self.tolerance {
109                return 0.0;
110            }
111            shadow = shadow.min(8.0 * h / t);
112            t += h;
113        }
114        shadow.clamp(0.0, 1.0)
115    }
116
117    /// Compute ambient occlusion at surface point `p` with normal `n`.
118    pub fn ambient_occlusion<F>(&self, f: &F, p: [f64; 3], n: [f64; 3], step: f64) -> f64
119    where
120        F: Fn([f64; 3]) -> f64,
121    {
122        let mut occ = 0.0;
123        let mut scale = 1.0;
124        for i in 0..5 {
125            let dist = (i + 1) as f64 * step;
126            let q = add3(p, scale3(n, dist));
127            occ += scale * (dist - f(q));
128            scale *= 0.5;
129        }
130        1.0 - occ.clamp(0.0, 1.0)
131    }
132}
133
134impl Default for RayMarcher {
135    fn default() -> Self {
136        Self::new()
137    }
138}