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            let theta = (z.z / r).acos();
30            let phi = z.y.atan2(z.x);
31            dr = r.powf(self.power - 1.0) * self.power * dr + 1.0;
32            let zr = r.powf(self.power);
33            let t = theta * self.power;
34            let ph = phi * self.power;
35            z = Vec3::new(t.sin() * ph.cos(), t.sin() * ph.sin(), t.cos()) * zr + p;
36        }
37        0.5 * r.ln() * r / dr
38    }
39
40    pub fn ray_march(&self, origin: Vec3, direction: Vec3, params: &RayMarchParams) -> RayMarchResult {
41        let mut t = 0.0f32;
42        for step in 0..params.max_steps {
43            let p = origin + direction * t;
44            let d = self.sdf(p);
45            if d < params.min_dist { return RayMarchResult { distance: t, steps: step, hit: true, position: p }; }
46            if t > params.max_dist { break; }
47            t += d;
48        }
49        RayMarchResult { distance: t, steps: params.max_steps, hit: false, position: origin + direction * t }
50    }
51}
52
53/// Mandelbox SDF.
54pub struct Mandelbox { pub scale: f32, pub max_iter: u32, pub fold_limit: f32 }
55impl Default for Mandelbox { fn default() -> Self { Self { scale: 2.0, max_iter: 15, fold_limit: 1.0 } } }
56impl Mandelbox {
57    pub fn sdf(&self, p: Vec3) -> f32 {
58        let mut z = p;
59        let mut dr = 1.0f32;
60        let fl = self.fold_limit;
61        for _ in 0..self.max_iter {
62            // Box fold
63            z = Vec3::new(
64                if z.x > fl { 2.0 * fl - z.x } else if z.x < -fl { -2.0 * fl - z.x } else { z.x },
65                if z.y > fl { 2.0 * fl - z.y } else if z.y < -fl { -2.0 * fl - z.y } else { z.y },
66                if z.z > fl { 2.0 * fl - z.z } else if z.z < -fl { -2.0 * fl - z.z } else { z.z },
67            );
68            // Sphere fold
69            let r2 = z.length_squared();
70            if r2 < 0.25 { let t = 4.0; z *= t; dr *= t; }
71            else if r2 < 1.0 { let t = 1.0 / r2; z *= t; dr *= t; }
72            z = z * self.scale + p;
73            dr = dr * self.scale.abs() + 1.0;
74        }
75        z.length() / dr.abs()
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    #[test]
83    fn mandelbulb_sdf_at_origin() {
84        let mb = Mandelbulb::default();
85        let d = mb.sdf(Vec3::ZERO);
86        assert!(d < 0.1, "Origin should be near/inside the bulb, got {d}");
87    }
88    #[test]
89    fn mandelbulb_sdf_far_away() {
90        let mb = Mandelbulb::default();
91        let d = mb.sdf(Vec3::new(10.0, 0.0, 0.0));
92        assert!(d > 1.0, "Far point should be outside, got {d}");
93    }
94    #[test]
95    fn mandelbox_evaluates() {
96        let mb = Mandelbox::default();
97        let _d = mb.sdf(Vec3::new(0.5, 0.5, 0.5));
98    }
99    #[test]
100    fn ray_march_hits_mandelbulb() {
101        let mb = Mandelbulb::default();
102        let result = mb.ray_march(Vec3::new(0.0, 0.0, 3.0), Vec3::new(0.0, 0.0, -1.0), &RayMarchParams::default());
103        assert!(result.hit, "Ray toward origin should hit the Mandelbulb");
104    }
105}