Skip to main content

scenix_math/
spherical.rs

1use crate::{EPSILON, Vec3, acos, atan2, clamp, cos, sin};
2
3/// Spherical coordinates using Y as the polar axis.
4#[derive(Clone, Copy, Debug, Default, PartialEq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
6pub struct Spherical {
7    /// Radius from origin.
8    pub radius: f32,
9    /// Polar angle from positive Y.
10    pub phi: f32,
11    /// Azimuth angle in the XZ plane.
12    pub theta: f32,
13}
14
15impl Spherical {
16    /// Creates spherical coordinates.
17    #[inline]
18    pub const fn new(radius: f32, phi: f32, theta: f32) -> Self {
19        Self { radius, phi, theta }
20    }
21
22    /// Converts from a vector.
23    pub fn from_vec3(value: Vec3) -> Self {
24        let radius = value.length();
25        if radius <= EPSILON {
26            return Self::default();
27        }
28        Self::new(
29            radius,
30            acos(clamp(value.y / radius, -1.0, 1.0)),
31            atan2(value.x, value.z),
32        )
33    }
34
35    /// Converts to a vector.
36    pub fn to_vec3(self) -> Vec3 {
37        let sin_phi = sin(self.phi);
38        Vec3::new(
39            self.radius * sin_phi * sin(self.theta),
40            self.radius * cos(self.phi),
41            self.radius * sin_phi * cos(self.theta),
42        )
43    }
44
45    /// Clamps the polar angle.
46    #[inline]
47    pub fn clamp_phi(mut self, min: f32, max: f32) -> Self {
48        self.phi = clamp(self.phi, min, max);
49        self
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56    use crate::assert_close;
57
58    #[test]
59    fn spherical_round_trips_vec3() {
60        let value = Vec3::new(2.0, 3.0, 4.0);
61        let out = Spherical::from_vec3(value).to_vec3();
62        assert_close(out.x, value.x);
63        assert_close(out.y, value.y);
64        assert_close(out.z, value.z);
65    }
66}