Skip to main content

proof_engine/fractal/
fractal3d.rs

1//! 3D fractals — Mandelbulb, Mandelbox via ray marching on signed distance fields.
2
3use glam::Vec3;
4
5#[derive(Debug, Clone)]
6pub struct RayMarchParams {
7    pub max_steps: u32,
8    pub max_dist: f32,
9    pub min_dist: f32,
10    pub power: f32,
11}
12impl Default for RayMarchParams {
13    fn default() -> Self { Self { max_steps: 128, max_dist: 100.0, min_dist: 0.001, power: 8.0 } }
14}
15
16pub struct RayMarchResult { pub distance: f32, pub steps: u32, pub hit: bool, pub position: Vec3 }
17
18/// Mandelbulb SDF.
19pub struct Mandelbulb { pub power: f32, pub max_iter: u32 }
20impl Default for Mandelbulb { fn default() -> Self { Self { power: 8.0, max_iter: 15 } } }
21impl Mandelbulb {
22    pub fn sdf(&self, p: Vec3) -> f32 {
23        let mut z = p;
24        let mut dr = 1.0f32;
25        let mut r = 0.0f32;
26        for _ in 0..self.max_iter {
27            r = z.length();
28            if r > 2.0 { break; }
29            if r < 1e-6 {
30                // z^n is 0 here; the angle formulas below divide by r and
31                // produced NaN for any orbit that hits the origin (p = 0).
32                z = p;
33                dr = 1.0;
34                continue;
35            }
36            let theta = (z.z / r).acos();
37            let phi = z.y.atan2(z.x);
38            dr = r.powf(self.power - 1.0) * self.power * dr + 1.0;
39            let zr = r.powf(self.power);
40            let t = theta * self.power;
41            let ph = phi * self.power;
42            z = Vec3::new(t.sin() * ph.cos(), t.sin() * ph.sin(), t.cos()) * zr + p;
43        }
44        if r < 1e-6 {
45            return 0.0; // the orbit stayed at the origin: inside the set
46        }
47        0.5 * r.ln() * r / dr
48    }
49
50    pub fn ray_march(&self, origin: Vec3, direction: Vec3, params: &RayMarchParams) -> RayMarchResult {
51        let mut t = 0.0f32;
52        for step in 0..params.max_steps {
53            let p = origin + direction * t;
54            let d = self.sdf(p);
55            if d < params.min_dist { return RayMarchResult { distance: t, steps: step, hit: true, position: p }; }
56            if t > params.max_dist { break; }
57            t += d;
58        }
59        RayMarchResult { distance: t, steps: params.max_steps, hit: false, position: origin + direction * t }
60    }
61}
62
63/// Mandelbox SDF.
64pub struct Mandelbox { pub scale: f32, pub max_iter: u32, pub fold_limit: f32 }
65impl Default for Mandelbox { fn default() -> Self { Self { scale: 2.0, max_iter: 15, fold_limit: 1.0 } } }
66impl Mandelbox {
67    pub fn sdf(&self, p: Vec3) -> f32 {
68        let mut z = p;
69        let mut dr = 1.0f32;
70        let fl = self.fold_limit;
71        for _ in 0..self.max_iter {
72            // Box fold
73            z = Vec3::new(
74                if z.x > fl { 2.0 * fl - z.x } else if z.x < -fl { -2.0 * fl - z.x } else { z.x },
75                if z.y > fl { 2.0 * fl - z.y } else if z.y < -fl { -2.0 * fl - z.y } else { z.y },
76                if z.z > fl { 2.0 * fl - z.z } else if z.z < -fl { -2.0 * fl - z.z } else { z.z },
77            );
78            // Sphere fold
79            let r2 = z.length_squared();
80            if r2 < 0.25 { let t = 4.0; z *= t; dr *= t; }
81            else if r2 < 1.0 { let t = 1.0 / r2; z *= t; dr *= t; }
82            z = z * self.scale + p;
83            dr = dr * self.scale.abs() + 1.0;
84        }
85        z.length() / dr.abs()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    #[test]
93    fn mandelbulb_sdf_at_origin() {
94        let mb = Mandelbulb::default();
95        let d = mb.sdf(Vec3::ZERO);
96        assert!(d < 0.1, "Origin should be near/inside the bulb, got {d}");
97    }
98    #[test]
99    fn mandelbulb_sdf_far_away() {
100        let mb = Mandelbulb::default();
101        let d = mb.sdf(Vec3::new(10.0, 0.0, 0.0));
102        assert!(d > 1.0, "Far point should be outside, got {d}");
103    }
104    #[test]
105    fn mandelbox_evaluates() {
106        let mb = Mandelbox::default();
107        let _d = mb.sdf(Vec3::new(0.5, 0.5, 0.5));
108    }
109    #[test]
110    fn ray_march_hits_mandelbulb() {
111        let mb = Mandelbulb::default();
112        let result = mb.ray_march(Vec3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), &RayMarchParams::default());
113        assert!(result.hit, "Ray toward origin should hit the Mandelbulb");
114    }
115}