saft_sdf/
sdf.rs

1#![allow(missing_docs, clippy::manual_clamp)]
2
3use super::Material;
4use super::SignedDistance;
5use macaw::*;
6#[cfg(target_arch = "spirv")]
7use num_traits::Float;
8
9#[inline]
10fn square_vec3(v: Vec3) -> f32 {
11    v.dot(v)
12}
13
14#[inline]
15fn hypot(v: Vec2) -> f32 {
16    #[cfg(target_arch = "spirv")]
17    let k = v.length();
18    #[cfg(not(target_arch = "spirv"))]
19    let k = v.x.hypot(v.y);
20    k
21}
22
23#[inline]
24pub fn sd_plane<T: SignedDistance>(pos: Vec3, plane: Vec4) -> T {
25    T::new_with_distance(Material::default(), pos.dot(plane.truncate()) + plane.w)
26}
27
28#[inline]
29pub fn sd_sphere<T: SignedDistance>(pos: Vec3, center: Vec3, radius: f32) -> T {
30    T::new_with_distance(Material::default(), (pos - center).length() - radius)
31}
32
33#[inline]
34pub fn sd_rounded_box<T: SignedDistance>(pos: Vec3, half_size: Vec3, rounding_radius: f32) -> T {
35    let q = pos.abs() - half_size + Vec3::splat(rounding_radius);
36    let dist = q.max(Vec3::splat(0.0)).length() + q.x.max(q.y.max(q.z)).min(0.0) - rounding_radius;
37    T::new_with_distance(Material::default(), dist)
38}
39
40#[inline]
41pub fn sd_torus<T: SignedDistance>(pos: Vec3, big_r: f32, small_r: f32) -> T {
42    let q = Vec2::new(hypot(pos.xz()) - big_r, pos.y);
43    let dist = q.length() - small_r;
44    T::new_with_distance(Material::default(), dist)
45}
46
47#[inline]
48pub fn sd_torus_sector<T: SignedDistance>(
49    mut pos: Vec3,
50    big_r: f32,
51    small_r: f32,
52    sin_cos_half_angle: (f32, f32),
53) -> T {
54    pos.x = pos.x.abs();
55    let k = if sin_cos_half_angle.1 * pos.x > sin_cos_half_angle.0 * pos.z {
56        pos.x * sin_cos_half_angle.0 + pos.z * sin_cos_half_angle.1
57    } else {
58        hypot(pos.xz())
59    };
60    let dist = (pos.dot(pos) + big_r.powi(2) - 2.0 * big_r * k)
61        .max(0.0)
62        .sqrt()
63        - small_r;
64    T::new_with_distance(Material::default(), dist)
65}
66
67#[inline]
68pub fn sd_biconvex_lens<T: SignedDistance>(
69    pos: Vec3,
70    lower_sagitta: f32,
71    upper_sagitta: f32,
72    chord: f32,
73) -> T {
74    let chord_radius = chord / 2.0;
75    let lower_radius = (chord_radius.powi(2) + lower_sagitta.powi(2)) / (2.0 * lower_sagitta);
76    let upper_radius = (chord_radius.powi(2) + upper_sagitta.powi(2)) / (2.0 * upper_sagitta);
77    let lower_center = Vec3::new(0.0, lower_radius - lower_sagitta, 0.0);
78    let upper_center = Vec3::new(0.0, -(upper_radius - upper_sagitta), 0.0);
79    sd_op_intersect(
80        sd_sphere(pos, lower_center, lower_radius),
81        sd_sphere(pos, upper_center, upper_radius),
82    )
83}
84
85#[inline]
86pub fn sd_capsule<T: SignedDistance>(pos: Vec3, points: &[Vec3; 2], radius: f32) -> T {
87    let pa = pos - points[0];
88    let ba = points[1] - points[0];
89    let h = (pa.dot(ba) / ba.dot(ba)).clamp(0.0, 1.0);
90    let distance = (pa - ba * h).length() - radius;
91    T::new_with_distance(Material::default(), distance)
92}
93
94#[inline]
95pub fn sd_rounded_cylinder_f(
96    pos: Vec3,
97    cylinder_radius: f32,
98    half_height: f32,
99    rounding_radius: f32,
100) -> f32 {
101    let d = Vec2::new(
102        hypot(pos.xz()) - cylinder_radius + rounding_radius,
103        pos.y.abs() - half_height + rounding_radius,
104    );
105    d.x.max(d.y).min(0.0) + d.max(Vec2::ZERO).length() - rounding_radius
106}
107
108#[inline]
109pub fn sd_rounded_cylinder<T: SignedDistance>(
110    pos: Vec3,
111    cylinder_radius: f32,
112    half_height: f32,
113    rounding_radius: f32,
114) -> T {
115    T::new_with_distance(
116        Material::default(),
117        sd_rounded_cylinder_f(pos, cylinder_radius, half_height, rounding_radius),
118    )
119}
120
121#[allow(clippy::many_single_char_names)]
122#[inline]
123pub fn sd_tapered_capsule_f(pos: Vec3, p: &[Vec3; 2], r: [f32; 2]) -> f32 {
124    // Straight from https://www.iquilezles.org/www/articles/distfunctions/distfunctions.htm
125
126    // sampling independent computations (only depend on shape)
127    let ba = p[1] - p[0];
128    let l2 = ba.dot(ba);
129    let rr = r[0] - r[1];
130    let a2 = l2 - rr * rr;
131    let il2 = 1.0 / l2;
132
133    // sampling dependant computations
134    let pa = pos - p[0];
135    let y = pa.dot(ba);
136    let z = y - l2;
137    let x2 = square_vec3(pa * l2 - ba * y);
138    let y2 = y * y * l2;
139    let z2 = z * z * l2;
140
141    // single square root!
142    let k = rr.signum() * rr * rr * x2;
143    if z.signum() * a2 * z2 > k {
144        (x2 + z2).sqrt() * il2 - r[1]
145    } else if y.signum() * a2 * y2 < k {
146        (x2 + y2).sqrt() * il2 - r[0]
147    } else {
148        (y * rr + (x2 * a2 * il2).sqrt()) * il2 - r[0]
149    }
150}
151
152#[inline]
153pub fn sd_tapered_capsule<T: SignedDistance>(pos: Vec3, p: &[Vec3; 2], r: [f32; 2]) -> T {
154    T::new_with_distance(Material::default(), sd_tapered_capsule_f(pos, p, r))
155}
156
157/// Base at origin, with height `h` along positive Y.
158#[allow(clippy::many_single_char_names)]
159#[inline]
160pub fn sd_cone_f(p: Vec3, r: f32, h: f32) -> f32 {
161    let q = vec2(r, h);
162    let w = vec2(hypot(p.xz()), h - p.y);
163    let a = w - q * (w.dot(q) / q.dot(q)).clamp(0.0, 1.0);
164    let b = w - vec2(r * (w.x / r).clamp(0.0, 1.0), h);
165    let d = a.dot(a).min(b.dot(b));
166    let s = (w.x * h - w.y * r).max(w.y - h);
167    d.sqrt() * s.signum()
168}
169
170#[inline]
171pub fn sd_cone<T: SignedDistance>(pos: Vec3, r: f32, h: f32) -> T {
172    T::new_with_distance(Material::default(), sd_cone_f(pos, r, h))
173}
174
175#[inline]
176pub fn sd_material<T: SignedDistance>(sd: T, material: Material) -> T {
177    T::new_with_distance(material, sd.distance())
178}
179
180#[inline]
181pub fn sd_op_union<T: SignedDistance>(d1: T, d2: T) -> T {
182    if d1.distance() < d2.distance() {
183        d1
184    } else {
185        d2
186    }
187}
188
189#[inline]
190pub fn sd_op_subtract<T: SignedDistance>(d1: T, d2: T) -> T {
191    let neg_distance1 = -d1.distance();
192    let distance2 = d2.distance();
193    if neg_distance1 > distance2 {
194        d1.copy_with_distance(neg_distance1)
195    } else {
196        d2
197    }
198}
199
200#[inline]
201pub fn sd_op_intersect<T: SignedDistance>(d1: T, d2: T) -> T {
202    if d1.distance() > d2.distance() {
203        d1
204    } else {
205        d2
206    }
207}
208
209#[inline]
210pub fn sd_op_union_smooth<T: SignedDistance>(d1: T, d2: T, size: f32) -> T {
211    let h = 0.5 + 0.5 * (d2.distance() - d1.distance()) / size;
212    let h = h.clamp(0.0, 1.0);
213
214    let new_d = d2.lerp(&d1, h);
215
216    let distance = new_d.distance() - size * h * (1.0 - h);
217
218    new_d.copy_with_distance(distance)
219}
220
221#[inline]
222pub fn sd_op_subtract_smooth<T: SignedDistance>(d1: T, d2: T, size: f32) -> T {
223    let h = 0.5 - 0.5 * (d2.distance() + d1.distance()) / size;
224    let h = h.clamp(0.0, 1.0);
225
226    let d1 = d1.copy_with_distance(-d1.distance());
227
228    let new_d = d2.lerp(&d1, h);
229
230    let distance = (size * h) * (1.0 - h) + new_d.distance();
231
232    new_d.copy_with_distance(distance)
233}
234
235#[inline]
236pub fn sd_op_intersect_smooth<T: SignedDistance>(d1: T, d2: T, size: f32) -> T {
237    let h = 0.5 - 0.5 * (d2.distance() - d1.distance()) / size;
238    let h = h.clamp(0.0, 1.0);
239
240    let new_d = d2.lerp(&d1, h);
241
242    let distance = (size * h) * (1.0 - h) + new_d.distance();
243
244    new_d.copy_with_distance(distance)
245}