diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index 1bd91741a5ed7..8a84093c635a2 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -9,7 +9,7 @@ use bevy_ecs::{ system::{Deferred, ReadOnlySystemParam, Res, Resource, SystemBuffer, SystemMeta, SystemParam}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; -use bevy_math::{Dir3, Mat2, Quat, Vec2, Vec3}; +use bevy_math::{Dir3, Quat, Rotation2d, Vec2, Vec3}; use bevy_transform::TransformPoint; use crate::{ @@ -590,11 +590,17 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # bevy_ecs::system::assert_is_system(system); /// ``` #[inline] - pub fn rect_2d(&mut self, position: Vec2, rotation: f32, size: Vec2, color: impl Into) { + pub fn rect_2d( + &mut self, + position: Vec2, + rotation: impl Into, + size: Vec2, + color: impl Into, + ) { if !self.enabled { return; } - let rotation = Mat2::from_angle(rotation); + let rotation: Rotation2d = rotation.into(); let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2); self.linestrip_2d([tl, tr, br, bl, tl], color); } diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 777214c1b2f94..128e8529d3d2c 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -12,6 +12,7 @@ keywords = ["bevy"] glam = { version = "0.25", features = ["bytemuck"] } thiserror = "1.0" serde = { version = "1", features = ["derive"], optional = true } +libm = { version = "0.2", optional = true } approx = { version = "0.5", optional = true } [dev-dependencies] @@ -25,7 +26,7 @@ approx = ["dep:approx", "glam/approx"] mint = ["glam/mint"] # Enable libm mathematical functions for glam types to ensure consistent outputs # across platforms at the cost of losing hardware-level optimization using intrinsics -libm = ["glam/libm"] +libm = ["dep:libm", "glam/libm"] # Enable assertions to check the validity of parameters passed to glam glam_assert = ["glam/glam-assert"] # Enable assertions in debug builds to check the validity of parameters passed to glam diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index 26ccd39b94652..86d5a8109a425 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -1,9 +1,7 @@ mod primitive_impls; -use glam::Mat2; - use super::{BoundingVolume, IntersectsVolume}; -use crate::prelude::Vec2; +use crate::prelude::{Mat2, Rotation2d, Vec2}; /// Computes the geometric center of the given set of points. #[inline(always)] @@ -21,10 +19,11 @@ fn point_cloud_2d_center(points: &[Vec2]) -> Vec2 { pub trait Bounded2d { /// Get an axis-aligned bounding box for the shape with the given translation and rotation. /// The rotation is in radians, counterclockwise, with 0 meaning no rotation. - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d; + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d; /// Get a bounding circle for the shape /// The rotation is in radians, counterclockwise, with 0 meaning no rotation. - fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle; + fn bounding_circle(&self, translation: Vec2, rotation: impl Into) + -> BoundingCircle; } /// A 2D axis-aligned bounding box, or bounding rectangle @@ -55,10 +54,14 @@ impl Aabb2d { /// /// Panics if the given set of points is empty. #[inline(always)] - pub fn from_point_cloud(translation: Vec2, rotation: f32, points: &[Vec2]) -> Aabb2d { + pub fn from_point_cloud( + translation: Vec2, + rotation: impl Into, + points: &[Vec2], + ) -> Aabb2d { // Transform all points by rotation - let rotation_mat = Mat2::from_angle(rotation); - let mut iter = points.iter().map(|point| rotation_mat * *point); + let rotation: Rotation2d = rotation.into(); + let mut iter = points.iter().map(|point| rotation * *point); let first = iter .next() @@ -94,7 +97,7 @@ impl Aabb2d { impl BoundingVolume for Aabb2d { type Translation = Vec2; - type Rotation = f32; + type Rotation = Rotation2d; type HalfSize = Vec2; #[inline(always)] @@ -157,7 +160,11 @@ impl BoundingVolume for Aabb2d { /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, /// and consider storing the original AABB and rotating that every time instead. #[inline(always)] - fn transformed_by(mut self, translation: Self::Translation, rotation: Self::Rotation) -> Self { + fn transformed_by( + mut self, + translation: Self::Translation, + rotation: impl Into, + ) -> Self { self.transform_by(translation, rotation); self } @@ -170,7 +177,11 @@ impl BoundingVolume for Aabb2d { /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, /// and consider storing the original AABB and rotating that every time instead. #[inline(always)] - fn transform_by(&mut self, translation: Self::Translation, rotation: Self::Rotation) { + fn transform_by( + &mut self, + translation: Self::Translation, + rotation: impl Into, + ) { self.rotate_by(rotation); self.translate_by(translation); } @@ -189,7 +200,7 @@ impl BoundingVolume for Aabb2d { /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, /// and consider storing the original AABB and rotating that every time instead. #[inline(always)] - fn rotated_by(mut self, rotation: Self::Rotation) -> Self { + fn rotated_by(mut self, rotation: impl Into) -> Self { self.rotate_by(rotation); self } @@ -202,11 +213,14 @@ impl BoundingVolume for Aabb2d { /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, /// and consider storing the original AABB and rotating that every time instead. #[inline(always)] - fn rotate_by(&mut self, rotation: Self::Rotation) { - let rot_mat = Mat2::from_angle(rotation); - let abs_rot_mat = Mat2::from_cols(rot_mat.x_axis.abs(), rot_mat.y_axis.abs()); + fn rotate_by(&mut self, rotation: impl Into) { + let rotation: Rotation2d = rotation.into(); + let abs_rot_mat = Mat2::from_cols( + Vec2::new(rotation.cos, rotation.sin), + Vec2::new(rotation.sin, rotation.cos), + ); let half_size = abs_rot_mat * self.half_size(); - *self = Self::new(rot_mat * self.center(), half_size); + *self = Self::new(rotation * self.center(), half_size); } } @@ -431,7 +445,12 @@ impl BoundingCircle { /// /// The bounding circle is not guaranteed to be the smallest possible. #[inline(always)] - pub fn from_point_cloud(translation: Vec2, rotation: f32, points: &[Vec2]) -> BoundingCircle { + pub fn from_point_cloud( + translation: Vec2, + rotation: impl Into, + points: &[Vec2], + ) -> BoundingCircle { + let rotation: Rotation2d = rotation.into(); let center = point_cloud_2d_center(points); let mut radius_squared = 0.0; @@ -443,10 +462,7 @@ impl BoundingCircle { } } - BoundingCircle::new( - Mat2::from_angle(rotation) * center + translation, - radius_squared.sqrt(), - ) + BoundingCircle::new(rotation * center + translation, radius_squared.sqrt()) } /// Get the radius of the bounding circle @@ -476,7 +492,7 @@ impl BoundingCircle { impl BoundingVolume for BoundingCircle { type Translation = Vec2; - type Rotation = f32; + type Rotation = Rotation2d; type HalfSize = f32; #[inline(always)] @@ -531,13 +547,14 @@ impl BoundingVolume for BoundingCircle { } #[inline(always)] - fn translate_by(&mut self, translation: Vec2) { + fn translate_by(&mut self, translation: Self::Translation) { self.center += translation; } #[inline(always)] - fn rotate_by(&mut self, rotation: f32) { - self.center = Mat2::from_angle(rotation) * self.center; + fn rotate_by(&mut self, rotation: impl Into) { + let rotation: Rotation2d = rotation.into(); + self.center = rotation * self.center; } } diff --git a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs index e42db83e10a8b..4cfa6c008004e 100644 --- a/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded2d/primitive_impls.rs @@ -5,23 +5,29 @@ use crate::{ BoxedPolygon, BoxedPolyline2d, Capsule2d, Circle, Ellipse, Line2d, Plane2d, Polygon, Polyline2d, Rectangle, RegularPolygon, Segment2d, Triangle2d, }, - Dir2, Mat2, Vec2, + Dir2, Mat2, Rotation2d, Vec2, }; use super::{Aabb2d, Bounded2d, BoundingCircle}; impl Bounded2d for Circle { - fn aabb_2d(&self, translation: Vec2, _rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, _rotation: impl Into) -> Aabb2d { Aabb2d::new(translation, Vec2::splat(self.radius)) } - fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + _rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::new(translation, self.radius) } } impl Bounded2d for Ellipse { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { + let rotation: Rotation2d = rotation.into(); + // V = (hh * cos(beta), hh * sin(beta)) // #####*##### // ### | ### @@ -50,14 +56,19 @@ impl Bounded2d for Ellipse { Aabb2d::new(translation, half_size) } - fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + _rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::new(translation, self.semi_major()) } } impl Bounded2d for Plane2d { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { - let normal = Mat2::from_angle(rotation) * *self.normal; + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { + let rotation: Rotation2d = rotation.into(); + let normal = rotation * *self.normal; let facing_x = normal == Vec2::X || normal == Vec2::NEG_X; let facing_y = normal == Vec2::Y || normal == Vec2::NEG_Y; @@ -70,14 +81,19 @@ impl Bounded2d for Plane2d { Aabb2d::new(translation, half_size) } - fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + _rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::new(translation, f32::MAX / 2.0) } } impl Bounded2d for Line2d { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { - let direction = Mat2::from_angle(rotation) * *self.direction; + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { + let rotation: Rotation2d = rotation.into(); + let direction = rotation * *self.direction; // Dividing `f32::MAX` by 2.0 is helpful so that we can do operations // like growing or shrinking the AABB without breaking things. @@ -89,49 +105,66 @@ impl Bounded2d for Line2d { Aabb2d::new(translation, half_size) } - fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + _rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::new(translation, f32::MAX / 2.0) } } impl Bounded2d for Segment2d { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { // Rotate the segment by `rotation` - let direction = Mat2::from_angle(rotation) * *self.direction; + let rotation: Rotation2d = rotation.into(); + let direction = rotation * *self.direction; let half_size = (self.half_length * direction).abs(); Aabb2d::new(translation, half_size) } - fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + _rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::new(translation, self.half_length) } } impl Bounded2d for Polyline2d { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { Aabb2d::from_point_cloud(translation, rotation, &self.vertices) } - fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) } } impl Bounded2d for BoxedPolyline2d { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { Aabb2d::from_point_cloud(translation, rotation, &self.vertices) } - fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) } } impl Bounded2d for Triangle2d { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { - let rotation_mat = Mat2::from_angle(rotation); - let [a, b, c] = self.vertices.map(|vtx| rotation_mat * vtx); + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { + let rotation: Rotation2d = rotation.into(); + let [a, b, c] = self.vertices.map(|vtx| rotation * vtx); let min = Vec2::new(a.x.min(b.x).min(c.x), a.y.min(b.y).min(c.y)); let max = Vec2::new(a.x.max(b.x).max(c.x), a.y.max(b.y).max(c.y)); @@ -142,8 +175,12 @@ impl Bounded2d for Triangle2d { } } - fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { - let rotation_mat = Mat2::from_angle(rotation); + fn bounding_circle( + &self, + translation: Vec2, + rotation: impl Into, + ) -> BoundingCircle { + let rotation: Rotation2d = rotation.into(); let [a, b, c] = self.vertices; // The points of the segment opposite to the obtuse or right angle if one exists @@ -164,17 +201,19 @@ impl Bounded2d for Triangle2d { // The triangle is obtuse or right, so the minimum bounding circle's diameter is equal to the longest side. // We can compute the minimum bounding circle from the line segment of the longest side. let (segment, center) = Segment2d::from_points(point1, point2); - segment.bounding_circle(rotation_mat * center + translation, rotation) + segment.bounding_circle(rotation * center + translation, rotation) } else { // The triangle is acute, so the smallest bounding circle is the circumcircle. let (Circle { radius }, circumcenter) = self.circumcircle(); - BoundingCircle::new(rotation_mat * circumcenter + translation, radius) + BoundingCircle::new(rotation * circumcenter + translation, radius) } } } impl Bounded2d for Rectangle { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { + let rotation: Rotation2d = rotation.into(); + // Compute the AABB of the rotated rectangle by transforming the half-extents // by an absolute rotation matrix. let (sin, cos) = rotation.sin_cos(); @@ -184,38 +223,52 @@ impl Bounded2d for Rectangle { Aabb2d::new(translation, half_size) } - fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + _rotation: impl Into, + ) -> BoundingCircle { let radius = self.half_size.length(); BoundingCircle::new(translation, radius) } } impl Bounded2d for Polygon { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { Aabb2d::from_point_cloud(translation, rotation, &self.vertices) } - fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) } } impl Bounded2d for BoxedPolygon { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { Aabb2d::from_point_cloud(translation, rotation, &self.vertices) } - fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::from_point_cloud(translation, rotation, &self.vertices) } } impl Bounded2d for RegularPolygon { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { + let rotation: Rotation2d = rotation.into(); + let mut min = Vec2::ZERO; let mut max = Vec2::ZERO; - for vertex in self.vertices(rotation) { + for vertex in self.vertices(rotation.as_radians()) { min = min.min(vertex); max = max.max(vertex); } @@ -226,17 +279,23 @@ impl Bounded2d for RegularPolygon { } } - fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + _rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::new(translation, self.circumcircle.radius) } } impl Bounded2d for Capsule2d { - fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d { + fn aabb_2d(&self, translation: Vec2, rotation: impl Into) -> Aabb2d { + let rotation: Rotation2d = rotation.into(); + // Get the line segment between the hemicircles of the rotated capsule let segment = Segment2d { // Multiplying a normalized vector (Vec2::Y) with a rotation returns a normalized vector. - direction: Dir2::new_unchecked(Mat2::from_angle(rotation) * Vec2::Y), + direction: rotation * Dir2::Y, half_length: self.half_length, }; let (a, b) = (segment.point1(), segment.point2()); @@ -251,7 +310,11 @@ impl Bounded2d for Capsule2d { } } - fn bounding_circle(&self, translation: Vec2, _rotation: f32) -> BoundingCircle { + fn bounding_circle( + &self, + translation: Vec2, + _rotation: impl Into, + ) -> BoundingCircle { BoundingCircle::new(translation, self.radius + self.half_length) } } diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index 53597d2c4bb64..17a3d134a4505 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -155,7 +155,11 @@ impl BoundingVolume for Aabb3d { /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, /// and consider storing the original AABB and rotating that every time instead. #[inline(always)] - fn transformed_by(mut self, translation: Self::Translation, rotation: Self::Rotation) -> Self { + fn transformed_by( + mut self, + translation: Self::Translation, + rotation: impl Into, + ) -> Self { self.transform_by(translation, rotation); self } @@ -168,7 +172,11 @@ impl BoundingVolume for Aabb3d { /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, /// and consider storing the original AABB and rotating that every time instead. #[inline(always)] - fn transform_by(&mut self, translation: Self::Translation, rotation: Self::Rotation) { + fn transform_by( + &mut self, + translation: Self::Translation, + rotation: impl Into, + ) { self.rotate_by(rotation); self.translate_by(translation); } @@ -187,7 +195,7 @@ impl BoundingVolume for Aabb3d { /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, /// and consider storing the original AABB and rotating that every time instead. #[inline(always)] - fn rotated_by(mut self, rotation: Self::Rotation) -> Self { + fn rotated_by(mut self, rotation: impl Into) -> Self { self.rotate_by(rotation); self } @@ -200,8 +208,8 @@ impl BoundingVolume for Aabb3d { /// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB, /// and consider storing the original AABB and rotating that every time instead. #[inline(always)] - fn rotate_by(&mut self, rotation: Self::Rotation) { - let rot_mat = Mat3::from_quat(rotation); + fn rotate_by(&mut self, rotation: impl Into) { + let rot_mat = Mat3::from_quat(rotation.into()); let abs_rot_mat = Mat3::from_cols( rot_mat.x_axis.abs(), rot_mat.y_axis.abs(), @@ -542,12 +550,13 @@ impl BoundingVolume for BoundingSphere { } #[inline(always)] - fn translate_by(&mut self, translation: Vec3) { + fn translate_by(&mut self, translation: Self::Translation) { self.center += translation; } #[inline(always)] - fn rotate_by(&mut self, rotation: Quat) { + fn rotate_by(&mut self, rotation: impl Into) { + let rotation: Quat = rotation.into(); self.center = rotation * self.center; } } diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs index c5f2722db9fd0..48363ee896ea4 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -150,7 +150,7 @@ impl Bounded3d for Capsule3d { // Get the line segment between the hemispheres of the rotated capsule let segment = Segment3d { // Multiplying a normalized vector (Vec3::Y) with a rotation returns a normalized vector. - direction: Dir3::new_unchecked(rotation * Vec3::Y), + direction: rotation * Dir3::Y, half_length: self.half_length, }; let (a, b) = (segment.point1(), segment.point2()); diff --git a/crates/bevy_math/src/bounding/mod.rs b/crates/bevy_math/src/bounding/mod.rs index 3fcda1d7069ee..2dd2f369756cb 100644 --- a/crates/bevy_math/src/bounding/mod.rs +++ b/crates/bevy_math/src/bounding/mod.rs @@ -49,13 +49,21 @@ pub trait BoundingVolume: Sized { fn shrink(&self, amount: Self::HalfSize) -> Self; /// Transforms the bounding volume by first rotating it around the origin and then applying a translation. - fn transformed_by(mut self, translation: Self::Translation, rotation: Self::Rotation) -> Self { + fn transformed_by( + mut self, + translation: Self::Translation, + rotation: impl Into, + ) -> Self { self.transform_by(translation, rotation); self } /// Transforms the bounding volume by first rotating it around the origin and then applying a translation. - fn transform_by(&mut self, translation: Self::Translation, rotation: Self::Rotation) { + fn transform_by( + &mut self, + translation: Self::Translation, + rotation: impl Into, + ) { self.rotate_by(rotation); self.translate_by(translation); } @@ -73,7 +81,7 @@ pub trait BoundingVolume: Sized { /// /// The result is a combination of the original volume and the rotated volume, /// so it is guaranteed to be either the same size or larger than the original. - fn rotated_by(mut self, rotation: Self::Rotation) -> Self { + fn rotated_by(mut self, rotation: impl Into) -> Self { self.rotate_by(rotation); self } @@ -82,7 +90,7 @@ pub trait BoundingVolume: Sized { /// /// The result is a combination of the original volume and the rotated volume, /// so it is guaranteed to be either the same size or larger than the original. - fn rotate_by(&mut self, rotation: Self::Rotation); + fn rotate_by(&mut self, rotation: impl Into); } /// A trait that generalizes intersection tests against a volume. diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index 05a600a4f876f..c98736960c840 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -1,6 +1,6 @@ use crate::{ primitives::{Primitive2d, Primitive3d}, - Quat, Vec2, Vec3, Vec3A, + Quat, Rotation2d, Vec2, Vec3, Vec3A, }; /// An error indicating that a direction is invalid. @@ -174,6 +174,23 @@ impl std::ops::Mul for f32 { } } +impl std::ops::Mul for Rotation2d { + type Output = Dir2; + + /// Rotates the [`Dir2`] using a [`Rotation2d`]. + fn mul(self, direction: Dir2) -> Self::Output { + let rotated = self * *direction; + + #[cfg(debug_assertions)] + assert_is_normalized( + "`Dir2` is denormalized after rotation.", + rotated.length_squared(), + ); + + Dir2(rotated) + } +} + #[cfg(feature = "approx")] impl approx::AbsDiffEq for Dir2 { type Epsilon = f32; diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index aba4c96753c51..604a299ab282c 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -13,12 +13,14 @@ mod direction; pub mod primitives; mod ray; mod rects; +mod rotation2d; pub use affine3::*; pub use aspect_ratio::AspectRatio; pub use direction::*; pub use ray::{Ray2d, Ray3d}; pub use rects::*; +pub use rotation2d::Rotation2d; /// The `bevy_math` prelude. pub mod prelude { @@ -32,7 +34,7 @@ pub mod prelude { direction::{Dir2, Dir3, Dir3A}, primitives::*, BVec2, BVec3, BVec4, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, - Quat, Ray2d, Ray3d, Rect, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, + Quat, Ray2d, Ray3d, Rect, Rotation2d, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, }; } diff --git a/crates/bevy_math/src/rotation2d.rs b/crates/bevy_math/src/rotation2d.rs new file mode 100644 index 0000000000000..7291e57c19233 --- /dev/null +++ b/crates/bevy_math/src/rotation2d.rs @@ -0,0 +1,580 @@ +use glam::FloatExt; + +use crate::prelude::{Mat2, Vec2}; + +/// A counterclockwise 2D rotation in radians. +/// +/// The rotation angle is wrapped to be within the `(-pi, pi]` range. +/// +/// # Example +/// +/// ``` +/// # use approx::assert_relative_eq; +/// # use bevy_math::{Rotation2d, Vec2}; +/// use std::f32::consts::PI; +/// +/// // Create rotations from radians or degrees +/// let rotation1 = Rotation2d::radians(PI / 2.0); +/// let rotation2 = Rotation2d::degrees(45.0); +/// +/// // Get the angle back as radians or degrees +/// assert_eq!(rotation1.as_degrees(), 90.0); +/// assert_eq!(rotation2.as_radians(), PI / 4.0); +/// +/// // "Add" rotations together using `*` +/// assert_relative_eq!(rotation1 * rotation2, Rotation2d::degrees(135.0)); +/// +/// // Rotate vectors +/// assert_relative_eq!(rotation1 * Vec2::X, Vec2::Y); +/// ``` +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct Rotation2d { + /// The cosine of the rotation angle in radians. + /// + /// This is the real part of the unit complex number representing the rotation. + pub cos: f32, + /// The sine of the rotation angle in radians. + /// + /// This is the imaginary part of the unit complex number representing the rotation. + pub sin: f32, +} + +impl Default for Rotation2d { + fn default() -> Self { + Self::IDENTITY + } +} + +impl Rotation2d { + /// No rotation. + pub const IDENTITY: Self = Self { cos: 1.0, sin: 0.0 }; + + /// A rotation of π radians. + pub const PI: Self = Self { + cos: -1.0, + sin: 0.0, + }; + + /// A counterclockwise rotation of π/2 radians. + pub const FRAC_PI_2: Self = Self { cos: 0.0, sin: 1.0 }; + + /// A counterclockwise rotation of π/3 radians. + pub const FRAC_PI_3: Self = Self { + cos: 0.5, + sin: 0.866_025_4, + }; + + /// A counterclockwise rotation of π/4 radians. + pub const FRAC_PI_4: Self = Self { + cos: std::f32::consts::FRAC_1_SQRT_2, + sin: std::f32::consts::FRAC_1_SQRT_2, + }; + + /// A counterclockwise rotation of π/6 radians. + pub const FRAC_PI_6: Self = Self { + cos: 0.866_025_4, + sin: 0.5, + }; + + /// A counterclockwise rotation of π/8 radians. + pub const FRAC_PI_8: Self = Self { + cos: 0.923_879_5, + sin: 0.382_683_43, + }; + + /// Creates a [`Rotation2d`] from a counterclockwise angle in radians. + #[inline] + pub fn radians(radians: f32) -> Self { + #[cfg(feature = "libm")] + let (sin, cos) = ( + libm::sin(radians as f64) as f32, + libm::cos(radians as f64) as f32, + ); + #[cfg(not(feature = "libm"))] + let (sin, cos) = radians.sin_cos(); + + Self::from_sin_cos(sin, cos) + } + + /// Creates a [`Rotation2d`] from a counterclockwise angle in degrees. + #[inline] + pub fn degrees(degrees: f32) -> Self { + Self::radians(degrees.to_radians()) + } + + /// Creates a [`Rotation2d`] from the sine and cosine of an angle in radians. + /// + /// The rotation is only valid if `sin * sin + cos * cos == 1.0`. + /// + /// # Panics + /// + /// Panics if `sin * sin + cos * cos != 1.0` when the `glam_assert` feature is enabled. + #[inline] + pub fn from_sin_cos(sin: f32, cos: f32) -> Self { + let rotation = Self { sin, cos }; + debug_assert!( + rotation.is_normalized(), + "the given sine and cosine produce an invalid rotation" + ); + rotation + } + + /// Returns the rotation in radians in the `(-pi, pi]` range. + #[inline] + pub fn as_radians(self) -> f32 { + #[cfg(feature = "libm")] + { + libm::atan2(self.sin as f64, self.cos as f64) as f32 + } + #[cfg(not(feature = "libm"))] + { + f32::atan2(self.sin, self.cos) + } + } + + /// Returns the rotation in degrees in the `(-180, 180]` range. + #[inline] + pub fn as_degrees(self) -> f32 { + self.as_radians().to_degrees() + } + + /// Returns the sine and cosine of the rotation angle in radians. + #[inline] + pub const fn sin_cos(self) -> (f32, f32) { + (self.sin, self.cos) + } + + /// Computes the length or norm of the complex number used to represent the rotation. + /// + /// The length is typically expected to be `1.0`. Unexpectedly denormalized rotations + /// can be a result of incorrect construction or floating point error caused by + /// successive operations. + #[inline] + #[doc(alias = "norm")] + pub fn length(self) -> f32 { + Vec2::new(self.sin, self.cos).length() + } + + /// Computes the squared length or norm of the complex number used to represent the rotation. + /// + /// This is generally faster than [`Rotation2d::length()`], as it avoids a square + /// root operation. + /// + /// The length is typically expected to be `1.0`. Unexpectedly denormalized rotations + /// can be a result of incorrect construction or floating point error caused by + /// successive operations. + #[inline] + #[doc(alias = "norm2")] + pub fn length_squared(self) -> f32 { + Vec2::new(self.sin, self.cos).length_squared() + } + + /// Computes `1.0 / self.length()`. + /// + /// For valid results, `self` must _not_ have a length of zero. + #[inline] + pub fn length_recip(self) -> f32 { + Vec2::new(self.sin, self.cos).length_recip() + } + + /// Returns `self` with a length of `1.0` if possible, and `None` otherwise. + /// + /// `None` will be returned if the sine and cosine of `self` are both zero (or very close to zero), + /// or if either of them is NaN or infinite. + /// + /// Note that [`Rotation2d`] should typically already be normalized by design. + /// Manual normalization is only needed when successive operations result in + /// accumulated floating point error, or if the rotation was constructed + /// with invalid values. + #[inline] + pub fn try_normalize(self) -> Option { + let recip = self.length_recip(); + if recip.is_finite() && recip > 0.0 { + Some(Self::from_sin_cos(self.sin * recip, self.cos * recip)) + } else { + None + } + } + + /// Returns `self` with a length of `1.0`. + /// + /// Note that [`Rotation2d`] should typically already be normalized by design. + /// Manual normalization is only needed when successive operations result in + /// accumulated floating point error, or if the rotation was constructed + /// with invalid values. + /// + /// # Panics + /// + /// Panics if `self` has a length of zero, NaN, or infinity when debug assertions are enabled. + #[inline] + pub fn normalize(self) -> Self { + let length_recip = self.length_recip(); + Self::from_sin_cos(self.sin * length_recip, self.cos * length_recip) + } + + /// Returns `true` if the rotation is neither infinite nor NaN. + #[inline] + pub fn is_finite(self) -> bool { + self.sin.is_finite() && self.cos.is_finite() + } + + /// Returns `true` if the rotation is NaN. + #[inline] + pub fn is_nan(self) -> bool { + self.sin.is_nan() || self.cos.is_nan() + } + + /// Returns whether `self` has a length of `1.0` or not. + /// + /// Uses a precision threshold of approximately `1e-4`. + #[inline] + pub fn is_normalized(self) -> bool { + // The allowed length is 1 +/- 1e-4, so the largest allowed + // squared length is (1 + 1e-4)^2 = 1.00020001, which makes + // the threshold for the squared length approximately 2e-4. + (self.length_squared() - 1.0).abs() <= 2e-4 + } + + /// Returns `true` if the rotation is near [`Rotation2d::IDENTITY`]. + #[inline] + pub fn is_near_identity(self) -> bool { + // Same as `Quat::is_near_identity`, but using sine and cosine + let threshold_angle_sin = 0.000_049_692_047; // let threshold_angle = 0.002_847_144_6; + self.cos > 0.0 && self.sin.abs() < threshold_angle_sin + } + + /// Returns the angle in radians needed to make `self` and `other` coincide. + #[inline] + pub fn angle_between(self, other: Self) -> f32 { + (other * self.inverse()).as_radians() + } + + /// Returns the inverse of the rotation. This is also the conjugate + /// of the unit complex number representing the rotation. + #[inline] + #[must_use] + #[doc(alias = "conjugate")] + pub fn inverse(self) -> Self { + Self { + cos: self.cos, + sin: -self.sin, + } + } + + /// Performs a linear interpolation between `self` and `rhs` based on + /// the value `s`, and normalizes the rotation afterwards. + /// + /// When `s == 0.0`, the result will be equal to `self`. + /// When `s == 1.0`, the result will be equal to `rhs`. + /// + /// This is slightly more efficient than [`slerp`](Self::slerp), and produces a similar result + /// when the difference between the two rotations is small. At larger differences, + /// the result resembles a kind of ease-in-out effect. + /// + /// If you would like the angular velocity to remain constant, consider using [`slerp`](Self::slerp) instead. + /// + /// # Details + /// + /// `nlerp` corresponds to computing an angle for a point at position `s` on a line drawn + /// between the endpoints of the arc formed by `self` and `rhs` on a unit circle, + /// and normalizing the result afterwards. + /// + /// Note that if the angles are opposite like 0 and π, the line will pass through the origin, + /// and the resulting angle will always be either `self` or `rhs` depending on `s`. + /// If `s` happens to be `0.5` in this case, a valid rotation cannot be computed, and `self` + /// will be returned as a fallback. + /// + /// # Example + /// + /// ``` + /// # use bevy_math::Rotation2d; + /// # + /// let rot1 = Rotation2d::IDENTITY; + /// let rot2 = Rotation2d::degrees(135.0); + /// + /// let result1 = rot1.nlerp(rot2, 1.0 / 3.0); + /// assert_eq!(result1.as_degrees(), 28.675055); + /// + /// let result2 = rot1.nlerp(rot2, 0.5); + /// assert_eq!(result2.as_degrees(), 67.5); + /// ``` + #[inline] + pub fn nlerp(self, end: Self, s: f32) -> Self { + Self { + sin: self.sin.lerp(end.sin, s), + cos: self.cos.lerp(end.cos, s), + } + .try_normalize() + // Fall back to the start rotation. + // This can happen when `self` and `end` are opposite angles and `s == 0.5`, + // because the resulting rotation would be zero, which cannot be normalized. + .unwrap_or(self) + } + + /// Performs a spherical linear interpolation between `self` and `end` + /// based on the value `s`. + /// + /// This corresponds to interpolating between the two angles at a constant angular velocity. + /// + /// When `s == 0.0`, the result will be equal to `self`. + /// When `s == 1.0`, the result will be equal to `rhs`. + /// + /// If you would like the rotation to have a kind of ease-in-out effect, consider + /// using the slightly more efficient [`nlerp`](Self::nlerp) instead. + /// + /// # Example + /// + /// ``` + /// # use bevy_math::Rotation2d; + /// # + /// let rot1 = Rotation2d::IDENTITY; + /// let rot2 = Rotation2d::degrees(135.0); + /// + /// let result1 = rot1.slerp(rot2, 1.0 / 3.0); + /// assert_eq!(result1.as_degrees(), 45.0); + /// + /// let result2 = rot1.slerp(rot2, 0.5); + /// assert_eq!(result2.as_degrees(), 67.5); + /// ``` + #[inline] + pub fn slerp(self, end: Self, s: f32) -> Self { + self * Self::radians(self.angle_between(end) * s) + } +} + +impl From for Rotation2d { + /// Creates a [`Rotation2d`] from a counterclockwise angle in radians. + fn from(rotation: f32) -> Self { + Self::radians(rotation) + } +} + +impl From for Mat2 { + /// Creates a [`Mat2`] rotation matrix from a [`Rotation2d`]. + fn from(rot: Rotation2d) -> Self { + Mat2::from_cols_array(&[rot.cos, -rot.sin, rot.sin, rot.cos]) + } +} + +impl std::ops::Mul for Rotation2d { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self { + cos: self.cos * rhs.cos - self.sin * rhs.sin, + sin: self.sin * rhs.cos + self.cos * rhs.sin, + } + } +} + +impl std::ops::MulAssign for Rotation2d { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl std::ops::Mul for Rotation2d { + type Output = Vec2; + + /// Rotates a [`Vec2`] by a [`Rotation2d`]. + fn mul(self, rhs: Vec2) -> Self::Output { + Vec2::new( + rhs.x * self.cos - rhs.y * self.sin, + rhs.x * self.sin + rhs.y * self.cos, + ) + } +} + +#[cfg(feature = "approx")] +impl approx::AbsDiffEq for Rotation2d { + type Epsilon = f32; + fn default_epsilon() -> f32 { + f32::EPSILON + } + fn abs_diff_eq(&self, other: &Self, epsilon: f32) -> bool { + self.cos.abs_diff_eq(&other.cos, epsilon) && self.sin.abs_diff_eq(&other.sin, epsilon) + } +} + +#[cfg(feature = "approx")] +impl approx::RelativeEq for Rotation2d { + fn default_max_relative() -> f32 { + f32::EPSILON + } + fn relative_eq(&self, other: &Self, epsilon: f32, max_relative: f32) -> bool { + self.cos.relative_eq(&other.cos, epsilon, max_relative) + && self.sin.relative_eq(&other.sin, epsilon, max_relative) + } +} + +#[cfg(feature = "approx")] +impl approx::UlpsEq for Rotation2d { + fn default_max_ulps() -> u32 { + 4 + } + fn ulps_eq(&self, other: &Self, epsilon: f32, max_ulps: u32) -> bool { + self.cos.ulps_eq(&other.cos, epsilon, max_ulps) + && self.sin.ulps_eq(&other.sin, epsilon, max_ulps) + } +} + +#[cfg(test)] +mod tests { + use approx::assert_relative_eq; + + use crate::{Dir2, Rotation2d, Vec2}; + + #[test] + fn creation() { + let rotation1 = Rotation2d::radians(std::f32::consts::FRAC_PI_2); + let rotation2 = Rotation2d::degrees(90.0); + let rotation3 = Rotation2d::from_sin_cos(1.0, 0.0); + + // All three rotations should be equal + assert_relative_eq!(rotation1.sin, rotation2.sin); + assert_relative_eq!(rotation1.cos, rotation2.cos); + assert_relative_eq!(rotation1.sin, rotation3.sin); + assert_relative_eq!(rotation1.cos, rotation3.cos); + + // The rotation should be 90 degrees + assert_relative_eq!(rotation1.as_radians(), std::f32::consts::FRAC_PI_2); + assert_relative_eq!(rotation1.as_degrees(), 90.0); + } + + #[test] + fn rotate() { + let rotation = Rotation2d::degrees(90.0); + + assert_relative_eq!(rotation * Vec2::X, Vec2::Y); + assert_relative_eq!(rotation * Dir2::Y, Dir2::NEG_X); + } + + #[test] + fn add() { + let rotation1 = Rotation2d::degrees(90.0); + let rotation2 = Rotation2d::degrees(180.0); + + // 90 deg + 180 deg becomes -90 deg after it wraps around to be within the ]-180, 180] range + assert_eq!((rotation1 * rotation2).as_degrees(), -90.0); + } + + #[test] + fn subtract() { + let rotation1 = Rotation2d::degrees(90.0); + let rotation2 = Rotation2d::degrees(45.0); + + assert_relative_eq!((rotation1 * rotation2.inverse()).as_degrees(), 45.0); + + // This should be equivalent to the above + assert_relative_eq!( + rotation2.angle_between(rotation1), + std::f32::consts::FRAC_PI_4 + ); + } + + #[test] + fn length() { + let rotation = Rotation2d { + sin: 10.0, + cos: 5.0, + }; + + assert_eq!(rotation.length_squared(), 125.0); + assert_eq!(rotation.length(), 11.18034); + assert!((rotation.normalize().length() - 1.0).abs() < 10e-7); + } + + #[test] + fn is_near_identity() { + assert!(!Rotation2d::radians(0.1).is_near_identity()); + assert!(!Rotation2d::radians(-0.1).is_near_identity()); + assert!(Rotation2d::radians(0.00001).is_near_identity()); + assert!(Rotation2d::radians(-0.00001).is_near_identity()); + assert!(Rotation2d::radians(0.0).is_near_identity()); + } + + #[test] + fn normalize() { + let rotation = Rotation2d { + sin: 10.0, + cos: 5.0, + }; + let normalized_rotation = rotation.normalize(); + + assert_eq!(normalized_rotation.sin, 0.89442724); + assert_eq!(normalized_rotation.cos, 0.44721362); + + assert!(!rotation.is_normalized()); + assert!(normalized_rotation.is_normalized()); + } + + #[test] + fn try_normalize() { + // Valid + assert!(Rotation2d { + sin: 10.0, + cos: 5.0, + } + .try_normalize() + .is_some()); + + // NaN + assert!(Rotation2d { + sin: f32::NAN, + cos: 5.0, + } + .try_normalize() + .is_none()); + + // Zero + assert!(Rotation2d { sin: 0.0, cos: 0.0 }.try_normalize().is_none()); + + // Non-finite + assert!(Rotation2d { + sin: f32::INFINITY, + cos: 5.0, + } + .try_normalize() + .is_none()); + } + + #[test] + fn nlerp() { + let rot1 = Rotation2d::IDENTITY; + let rot2 = Rotation2d::degrees(135.0); + + assert_eq!(rot1.nlerp(rot2, 1.0 / 3.0).as_degrees(), 28.675055); + assert!(rot1.nlerp(rot2, 0.0).is_near_identity()); + assert_eq!(rot1.nlerp(rot2, 0.5).as_degrees(), 67.5); + assert_eq!(rot1.nlerp(rot2, 1.0).as_degrees(), 135.0); + + let rot1 = Rotation2d::IDENTITY; + let rot2 = Rotation2d::from_sin_cos(0.0, -1.0); + + assert!(rot1.nlerp(rot2, 1.0 / 3.0).is_near_identity()); + assert!(rot1.nlerp(rot2, 0.0).is_near_identity()); + // At 0.5, there is no valid rotation, so the fallback is the original angle. + assert_eq!(rot1.nlerp(rot2, 0.5).as_degrees(), 0.0); + assert_eq!(rot1.nlerp(rot2, 1.0).as_degrees().abs(), 180.0); + } + + #[test] + fn slerp() { + let rot1 = Rotation2d::IDENTITY; + let rot2 = Rotation2d::degrees(135.0); + + assert_eq!(rot1.slerp(rot2, 1.0 / 3.0).as_degrees(), 45.0); + assert!(rot1.slerp(rot2, 0.0).is_near_identity()); + assert_eq!(rot1.slerp(rot2, 0.5).as_degrees(), 67.5); + assert_eq!(rot1.slerp(rot2, 1.0).as_degrees(), 135.0); + + let rot1 = Rotation2d::IDENTITY; + let rot2 = Rotation2d::from_sin_cos(0.0, -1.0); + + assert!((rot1.slerp(rot2, 1.0 / 3.0).as_degrees() - 60.0).abs() < 10e-6); + assert!(rot1.slerp(rot2, 0.0).is_near_identity()); + assert_eq!(rot1.slerp(rot2, 0.5).as_degrees(), 90.0); + assert_eq!(rot1.slerp(rot2, 1.0).as_degrees().abs(), 180.0); + } +} diff --git a/crates/bevy_reflect/src/impls/math/rotation2d.rs b/crates/bevy_reflect/src/impls/math/rotation2d.rs new file mode 100644 index 0000000000000..4082e1bff5dee --- /dev/null +++ b/crates/bevy_reflect/src/impls/math/rotation2d.rs @@ -0,0 +1,13 @@ +use crate as bevy_reflect; +use crate::{ReflectDeserialize, ReflectSerialize}; +use bevy_math::Rotation2d; +use bevy_reflect_derive::impl_reflect; + +impl_reflect!( + #[reflect(Debug, PartialEq, Serialize, Deserialize)] + #[type_path = "bevy_math"] + struct Rotation2d { + cos: f32, + sin: f32, + } +); diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index ca9d181b001b7..cf4a181cbfc79 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -493,6 +493,7 @@ mod impls { mod primitives2d; mod primitives3d; mod rect; + mod rotation2d; } #[cfg(feature = "petgraph")] mod petgraph;