From 582ddfc3cdf51e72d1429501abe5e9bd795d4058 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ludvig=20B=C3=B6klin?= Date: Tue, 9 Feb 2021 13:28:51 +0100 Subject: [PATCH] Ori: add tests, rename to_vec() => look_vec(); Dir: add methods, normalize on rot --- Cargo.lock | 1 + common/Cargo.toml | 1 + common/src/comp/ori.rs | 167 ++++++++++++++++++++++++++------ common/src/util/dir.rs | 40 ++++++-- nix/rustPkgs.nix | 2 +- voxygen/src/scene/figure/mod.rs | 2 +- voxygen/src/scene/particle.rs | 4 +- 7 files changed, 179 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aaa8deb05c..78fd763331 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6055,6 +6055,7 @@ dependencies = [ name = "veloren-common" version = "0.8.0" dependencies = [ + "approx 0.4.0", "arraygen", "assets_manager", "criterion", diff --git a/common/Cargo.toml b/common/Cargo.toml index fdd42a15f7..90964627b0 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -13,6 +13,7 @@ bin_csv = ["csv", "structopt"] default = ["simd"] [dependencies] +approx = "0.4.0" arraygen = "0.1.13" crossbeam-utils = "0.8.1" crossbeam-channel = "0.5" diff --git a/common/src/comp/ori.rs b/common/src/comp/ori.rs index 74aff18e30..735a785e0e 100644 --- a/common/src/comp/ori.rs +++ b/common/src/comp/ori.rs @@ -18,8 +18,12 @@ impl Default for Ori { impl Ori { pub fn new(quat: Quaternion) -> Self { - debug_assert!(quat.into_vec4().map(f32::is_finite).reduce_and()); - debug_assert!(quat.into_vec4().is_normalized()); + #[cfg(debug_assert)] + { + let v4 = quat.into_vec4(); + debug_assert!(v4.map(f32::is_finite).reduce_and()); + debug_assert!(v4.is_normalized()); + } Self(quat) } @@ -31,11 +35,16 @@ impl Ori { Dir::from_unnormalized(vec.into()).map(Self::from) } - pub fn to_vec(self) -> Vec3 { *self.look_dir() } + /// Look direction as a vector (no pedantic normalization performed) + pub fn look_vec(self) -> Vec3 { self.to_quat() * *Dir::default() } - pub fn to_quat(self) -> Quaternion { self.0 } + pub fn to_quat(self) -> Quaternion { + debug_assert!(self.is_normalized()); + self.0 + } - pub fn look_dir(&self) -> Dir { Dir::new(self.0 * *Dir::default()) } + /// 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() } @@ -53,26 +62,86 @@ impl Ori { /// Multiply rotation quaternion by `q` /// (the rotations are in local vector space). - pub fn rotated(self, q: Quaternion) -> Self { Self((self.0 * q).normalized()) } + /// + /// ``` + /// 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). - pub fn prerotated(self, q: Quaternion) -> Self { Self((q * self.0).normalized()) } + /// + /// ``` + /// 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.0.inverse() * global + 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.0 * local + self.to_quat() * local } pub fn pitched_up(self, angle_radians: f32) -> Self { @@ -101,7 +170,6 @@ impl Ori { /// Returns a version without sideways tilt (roll) /// - /// # Examples /// ``` /// use veloren_common::comp::Ori; /// @@ -109,21 +177,21 @@ impl Ori { /// 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); + /// 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); + /// 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); + /// assert!((ang1 - ang2).abs() <= std::f32::EPSILON); /// ``` pub fn uprighted(self) -> Self { let fw = self.look_dir(); - match Dir::new(Vec3::unit_z()).projected(&Plane::from(fw)) { + 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(); @@ -138,17 +206,20 @@ impl Ori { impl From for Ori { fn from(dir: Dir) -> Self { - let from = *Dir::default(); - Self::from(Quaternion::::rotation_from_to_3d(from, *dir)).uprighted() + let from = Dir::default(); + let q = Quaternion::::rotation_from_to_3d(*from, *dir).normalized(); + + #[cfg(debug_assertions)] + { + approx::assert_relative_eq!((q * from).dot(*dir), 1.0); + } + + Self(q).uprighted() } } -impl From for Quaternion { - fn from(Ori(q): Ori) -> Self { q } -} - impl From> for Ori { - fn from(quat: Quaternion) -> Self { Self(quat.normalized()) } + fn from(quat: Quaternion) -> Self { Self::new(quat) } } impl From> for Ori { @@ -159,6 +230,10 @@ impl From> for Ori { } } +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 } @@ -170,19 +245,19 @@ impl From for Dir { } impl From for Vec3 { - fn from(ori: Ori) -> Self { *ori.look_dir() } + 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_dir()) } + 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().xy() } + fn from(ori: Ori) -> Self { ori.look_vec().xy() } } impl From for vek::vec::repr_simd::Vec2 { - fn from(ori: Ori) -> Self { vek::vec::repr_simd::Vec2::from(ori.look_dir().xy()) } + fn from(ori: Ori) -> Self { vek::vec::repr_simd::Vec2::from(ori.look_vec().xy()) } } // Validate at Deserialization @@ -212,9 +287,47 @@ impl From for Ori { } } impl Into for Ori { - fn into(self) -> SerdeOri { SerdeOri(self.0) } + fn into(self) -> SerdeOri { SerdeOri(self.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 vec![ori.up(), ori.down(), ori.left(), ori.right()] { + approx::assert_relative_eq!(dir.dot(*def), 0.0); + } + } +} diff --git a/common/src/util/dir.rs b/common/src/util/dir.rs index 9f76484479..70ca92ad37 100644 --- a/common/src/util/dir.rs +++ b/common/src/util/dir.rs @@ -11,7 +11,7 @@ use vek::*; #[serde(from = "SerdeDir")] pub struct Dir(Vec3); impl Default for Dir { - fn default() -> Self { Self(Vec3::unit_y()) } + fn default() -> Self { Self::forward() } } // Validate at Deserialization @@ -82,12 +82,38 @@ impl Dir { Self(slerp_normalized(from.0, to.0, factor)) } + pub fn slerped_to(self, to: Self, factor: f32) -> Self { + Self(slerp_normalized(self.0, to.0, factor)) + } + /// Note: this uses `from` if `to` is unnormalizable pub fn slerp_to_vec3(from: Self, to: Vec3, factor: f32) -> Self { Self(slerp_to_unnormalized(from.0, to, factor).unwrap_or_else(|e| e)) } + pub fn rotation_between(&self, to: Self) -> Quaternion { + Quaternion::::rotation_from_to_3d(self.0, to.0) + } + + pub fn rotation(&self) -> Quaternion { Self::default().rotation_between(*self) } + pub fn is_valid(&self) -> bool { !self.0.map(f32::is_nan).reduce_or() && self.is_normalized() } + + pub fn up() -> Self { Dir::new(Vec3::::unit_z()) } + + pub fn down() -> Self { -Dir::new(Vec3::::unit_z()) } + + pub fn left() -> Self { -Dir::new(Vec3::::unit_x()) } + + pub fn right() -> Self { Dir::new(Vec3::::unit_x()) } + + pub fn forward() -> Self { Dir::new(Vec3::::unit_y()) } + + pub fn back() -> Self { -Dir::new(Vec3::::unit_y()) } + + pub fn vec(&self) -> &Vec3 { &self.0 } + + pub fn to_vec(self) -> Vec3 { self.0 } } impl std::ops::Deref for Dir { @@ -100,12 +126,6 @@ impl From for Vec3 { fn from(dir: Dir) -> Self { *dir } } -impl std::ops::Mul for Quaternion { - type Output = Dir; - - fn mul(self, dir: Dir) -> Self::Output { Dir::new(self * *dir) } -} - impl Projection for Dir { type Output = Option; @@ -123,6 +143,12 @@ impl Projection for Vec3 { } } +impl std::ops::Mul for Quaternion { + type Output = Dir; + + fn mul(self, dir: Dir) -> Self::Output { Dir((self * *dir).normalized()) } +} + impl std::ops::Neg for Dir { type Output = Dir; diff --git a/nix/rustPkgs.nix b/nix/rustPkgs.nix index e4f9106c7a..a96730488b 100644 --- a/nix/rustPkgs.nix +++ b/nix/rustPkgs.nix @@ -6,7 +6,7 @@ let channel = mozPkgs.rustChannelOf { rustToolchain = ../rust-toolchain; - sha256 = "sha256-kDtMqYvrTbahqYHYFQOWyvT0+F5o4UVcqkMZt0c43kc="; + sha256 = "sha256-9wp6afVeZqCOEgXxYQiryYeF07kW5IHh3fQaOKF2oRI="; }; in channel // { diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 31049c3f7d..e7c537114f 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -584,7 +584,7 @@ impl FigureMgr { .map(|i| { ( (anim::vek::Vec3::from(i.pos),), - anim::vek::Vec3::from(i.ori.to_vec()), + anim::vek::Vec3::from(i.ori.look_vec()), ) }) .unwrap_or(( diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 06416e8781..2f09844996 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -375,7 +375,7 @@ impl ParticleMgr { .join() { if let CharacterState::BasicBeam(b) = character_state { - let particle_ori = b.particle_ori.unwrap_or_else(|| ori.to_vec()); + let particle_ori = b.particle_ori.unwrap_or_else(|| ori.look_vec()); if b.stage_section == StageSection::Cast { if b.static_data.base_hps > 0 { // Emit a light when using healing @@ -586,7 +586,7 @@ impl ParticleMgr { let radians = shockwave.properties.angle.to_radians(); - let ori_vec = ori.to_vec(); + let ori_vec = ori.look_vec(); let theta = ori_vec.y.atan2(ori_vec.x); let dtheta = radians / distance;