use crate::util::{Dir, Plane, Projection}; use serde::{Deserialize, Serialize}; use specs::Component; use specs_idvs::IdvStorage; use std::f32::consts::PI; use vek::{Quaternion, Vec2, Vec3}; // Orientation #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(into = "SerdeOri")] #[serde(from = "SerdeOri")] pub struct Ori(Quaternion); impl Default for Ori { /// Returns the default orientation (no rotation; default Dir) fn default() -> Self { Self(Quaternion::identity()) } } impl Ori { pub fn new(quat: Quaternion) -> Self { #[cfg(debug_assert)] { let v4 = quat.into_vec4(); debug_assert!(v4.map(f32::is_finite).reduce_and()); debug_assert!(v4.is_normalized()); } Self(quat) } /// Tries to convert into a Dir and then the appropriate rotation pub fn from_unnormalized_vec(vec: T) -> Option where T: Into>, { Dir::from_unnormalized(vec.into()).map(Self::from) } /// Look direction as a vector (no pedantic normalization performed) pub fn look_vec(self) -> Vec3 { self.to_quat() * *Dir::default() } /// Get the internal quaternion representing the rotation from /// `Dir::default()` to this orientation. /// /// The operation is a cheap copy. pub fn to_quat(self) -> Quaternion { debug_assert!(self.is_normalized()); self.0 } /// Look direction (as a Dir it is pedantically normalized) pub fn look_dir(&self) -> Dir { self.to_quat() * Dir::default() } pub fn up(&self) -> Dir { self.pitched_up(PI / 2.0).look_dir() } pub fn down(&self) -> Dir { self.pitched_down(PI / 2.0).look_dir() } pub fn left(&self) -> Dir { self.yawed_left(PI / 2.0).look_dir() } pub fn right(&self) -> Dir { self.yawed_right(PI / 2.0).look_dir() } pub fn slerp(ori1: Self, ori2: Self, s: f32) -> Self { Self(Quaternion::slerp(ori1.0, ori2.0, s).normalized()) } pub fn slerped_towards(self, ori: Ori, s: f32) -> Self { Self::slerp(self, ori, s) } /// Multiply rotation quaternion by `q` /// (the rotations are in local vector space). /// /// ``` /// use vek::{Quaternion, Vec3}; /// use veloren_common::{comp::Ori, util::Dir}; /// /// let ang = 90_f32.to_radians(); /// let roll_right = Quaternion::rotation_y(ang); /// let pitch_up = Quaternion::rotation_x(ang); /// /// let ori1 = Ori::from(Dir::new(Vec3::unit_x())); /// let ori2 = Ori::default().rotated(roll_right).rotated(pitch_up); /// /// assert!((ori1.look_dir().dot(*ori2.look_dir()) - 1.0).abs() <= std::f32::EPSILON); /// ``` pub fn rotated(self, q: Quaternion) -> Self { Self((self.to_quat() * q.normalized()).normalized()) } /// Premultiply rotation quaternion by `q` /// (the rotations are in global vector space). /// /// ``` /// use vek::{Quaternion, Vec3}; /// use veloren_common::{comp::Ori, util::Dir}; /// /// let ang = 90_f32.to_radians(); /// let roll_right = Quaternion::rotation_y(ang); /// let pitch_up = Quaternion::rotation_x(ang); /// /// let ori1 = Ori::from(Dir::up()); /// let ori2 = Ori::default().prerotated(roll_right).prerotated(pitch_up); /// /// assert!((ori1.look_dir().dot(*ori2.look_dir()) - 1.0).abs() <= std::f32::EPSILON); /// ``` pub fn prerotated(self, q: Quaternion) -> Self { Self((q.normalized() * self.to_quat()).normalized()) } /// Take `global` into this Ori's local vector space /// /// ``` /// use vek::Vec3; /// use veloren_common::{comp::Ori, util::Dir}; /// /// let ang = 90_f32.to_radians(); /// let (fw, left, up) = (Dir::default(), Dir::left(), Dir::up()); /// /// let ori = Ori::default().rolled_left(ang).pitched_up(ang); /// approx::assert_relative_eq!(ori.global_to_local(fw).dot(*-up), 1.0); /// approx::assert_relative_eq!(ori.global_to_local(left).dot(*fw), 1.0); /// let ori = Ori::default().rolled_right(ang).pitched_up(2.0 * ang); /// approx::assert_relative_eq!(ori.global_to_local(up).dot(*left), 1.0); /// ``` pub fn global_to_local(&self, global: T) -> as std::ops::Mul>::Output where Quaternion: std::ops::Mul, { self.to_quat().inverse() * global } /// Take `local` into the global vector space /// /// ``` /// use vek::Vec3; /// use veloren_common::{comp::Ori, util::Dir}; /// /// let ang = 90_f32.to_radians(); /// let (fw, left, up) = (Dir::default(), Dir::left(), Dir::up()); /// /// let ori = Ori::default().rolled_left(ang).pitched_up(ang); /// approx::assert_relative_eq!(ori.local_to_global(fw).dot(*left), 1.0); /// approx::assert_relative_eq!(ori.local_to_global(left).dot(*-up), 1.0); /// let ori = Ori::default().rolled_right(ang).pitched_up(2.0 * ang); /// approx::assert_relative_eq!(ori.local_to_global(up).dot(*left), 1.0); /// ``` pub fn local_to_global(&self, local: T) -> as std::ops::Mul>::Output where Quaternion: std::ops::Mul, { self.to_quat() * local } pub fn to_horizontal(self) -> Self { let fw = self.look_dir(); Dir::from_unnormalized(fw.xy().into()) .or_else(|| { // if look_dir is straight down, pitch up, or if straight up, pitch down Dir::from_unnormalized( if fw.dot(Vec3::unit_z()) < 0.0 { self.up() } else { self.down() } .xy() .into(), ) }) .map(|dir| dir.into()) .expect( "If the horizontal component of a Dir can not be normalized, the horizontal \ component of a Dir perpendicular to it must be", ) } pub fn pitched_up(self, angle_radians: f32) -> Self { self.rotated(Quaternion::rotation_x(angle_radians)) } pub fn pitched_down(self, angle_radians: f32) -> Self { self.rotated(Quaternion::rotation_x(-angle_radians)) } pub fn yawed_left(self, angle_radians: f32) -> Self { self.rotated(Quaternion::rotation_z(angle_radians)) } pub fn yawed_right(self, angle_radians: f32) -> Self { self.rotated(Quaternion::rotation_z(-angle_radians)) } pub fn rolled_left(self, angle_radians: f32) -> Self { self.rotated(Quaternion::rotation_y(-angle_radians)) } pub fn rolled_right(self, angle_radians: f32) -> Self { self.rotated(Quaternion::rotation_y(angle_radians)) } /// Returns a version without sideways tilt (roll) /// /// ``` /// use veloren_common::comp::Ori; /// /// let ang = 45_f32.to_radians(); /// let zenith = vek::Vec3::unit_z(); /// /// let rl = Ori::default().rolled_left(ang); /// assert!((rl.up().angle_between(zenith) - ang).abs() <= std::f32::EPSILON); /// assert!(rl.uprighted().up().angle_between(zenith) <= std::f32::EPSILON); /// /// let pd_rr = Ori::default().pitched_down(ang).rolled_right(ang); /// let pd_upr = pd_rr.uprighted(); /// /// assert!((pd_upr.up().angle_between(zenith) - ang).abs() <= std::f32::EPSILON); /// /// let ang1 = pd_upr.rolled_right(ang).up().angle_between(zenith); /// let ang2 = pd_rr.up().angle_between(zenith); /// assert!((ang1 - ang2).abs() <= std::f32::EPSILON); /// ``` pub fn uprighted(self) -> Self { let fw = self.look_dir(); match Dir::up().projected(&Plane::from(fw)) { Some(dir_p) => { let up = self.up(); let go_right_s = fw.cross(*up).dot(*dir_p).signum(); self.rolled_right(up.angle_between(*dir_p) * go_right_s) }, None => self, } } fn is_normalized(&self) -> bool { self.0.into_vec4().is_normalized() } } impl From for Ori { fn from(dir: Dir) -> Self { let from = Dir::default(); let q = Quaternion::::rotation_from_to_3d(*from, *dir).normalized(); Self(q).uprighted() } } impl From> for Ori { fn from(dir: Vec3) -> Self { let dir = Dir::from_unnormalized(dir).unwrap_or_default(); let from = Dir::default(); let q = Quaternion::::rotation_from_to_3d(*from, *dir).normalized(); Self(q).uprighted() } } impl From> for Ori { fn from(quat: Quaternion) -> Self { Self::new(quat) } } impl From> for Ori { fn from( vek::quaternion::repr_simd::Quaternion { x, y, z, w }: vek::quaternion::repr_simd::Quaternion, ) -> Self { Self::from(Quaternion { x, y, z, w }) } } impl From for Quaternion { fn from(Ori(q): Ori) -> Self { q } } impl From for vek::quaternion::repr_simd::Quaternion { fn from(Ori(Quaternion { x, y, z, w }): Ori) -> Self { vek::quaternion::repr_simd::Quaternion { x, y, z, w } } } impl From for Dir { fn from(ori: Ori) -> Self { ori.look_dir() } } impl From for Vec3 { fn from(ori: Ori) -> Self { ori.look_vec() } } impl From for vek::vec::repr_simd::Vec3 { fn from(ori: Ori) -> Self { vek::vec::repr_simd::Vec3::from(ori.look_vec()) } } impl From for Vec2 { fn from(ori: Ori) -> Self { ori.look_dir().to_horizontal().unwrap_or_default().xy() } } impl From for vek::vec::repr_simd::Vec2 { fn from(ori: Ori) -> Self { vek::vec::repr_simd::Vec2::from(ori.look_vec().xy()) } } // Validate at Deserialization #[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] struct SerdeOri(Quaternion); impl From for Ori { fn from(serde_quat: SerdeOri) -> Self { let quat: Quaternion = serde_quat.0; if quat.into_vec4().map(f32::is_nan).reduce_or() { tracing::warn!( ?quat, "Deserialized rotation quaternion containing NaNs, replacing with default" ); Default::default() } else if !Self(quat).is_normalized() { tracing::warn!( ?quat, "Deserialized unnormalized rotation quaternion (magnitude: {}), replacing with \ default", quat.magnitude() ); Default::default() } else { Self::new(quat) } } } impl From for SerdeOri { fn from(other: Ori) -> SerdeOri { SerdeOri(other.to_quat()) } } impl Component for Ori { type Storage = IdvStorage; } #[cfg(test)] mod tests { use super::*; #[test] fn from_to_dir() { let from_to = |dir: Dir| { let ori = Ori::from(dir); approx::assert_relative_eq!(ori.look_dir().dot(*dir), 1.0); approx::assert_relative_eq!((ori.to_quat() * Dir::default()).dot(*dir), 1.0); }; let angles = 32; for i in 0..angles { let theta = PI * 2. * (i as f32) / (angles as f32); let v = Vec3::unit_y(); let q = Quaternion::rotation_x(theta); from_to(Dir::new(q * v)); let v = Vec3::unit_z(); let q = Quaternion::rotation_y(theta); from_to(Dir::new(q * v)); let v = Vec3::unit_x(); let q = Quaternion::rotation_z(theta); from_to(Dir::new(q * v)); } } #[test] fn dirs() { let ori = Ori::default(); let def = Dir::default(); for dir in &[ori.up(), ori.down(), ori.left(), ori.right()] { approx::assert_relative_eq!(dir.dot(*def), 0.0); } } }