Add Rotation2d (#11658)

# Objective

Rotating vectors is a very common task. It is required for a variety of
things both within Bevy itself and in many third party plugins, for
example all over physics and collision detection, and for things like
Bevy's bounding volumes and several gizmo implementations.

For 3D, we can do this using a `Quat`, but for 2D, we do not have a
clear and efficient option. `Mat2` can be used for rotating vectors if
created using `Mat2::from_angle`, but this is not obvious to many users,
it doesn't have many rotation helpers, and the type does not give any
guarantees that it represents a valid rotation.

We should have a proper type for 2D rotations. In addition to allowing
for potential optimization, it would allow us to have a consistent and
explicitly documented representation used throughout the engine, i.e.
counterclockwise and in radians.

## Representation

The mathematical formula for rotating a 2D vector is the following:

```
new_x = x * cos - y * sin
new_y = x * sin + y * cos
```

Here, `sin` and `cos` are the sine and cosine of the rotation angle.
Computing these every time when a vector needs to be rotated can be
expensive, so the rotation shouldn't be just an `f32` angle. Instead, it
is often more efficient to represent the rotation using the sine and
cosine of the angle instead of storing the angle itself. This can be
freely passed around and reused without unnecessary computations.

The two options are either a 2x2 rotation matrix or a unit complex
number where the cosine is the real part and the sine is the imaginary
part. These are equivalent for the most part, but the unit complex
representation is a bit more memory efficient (two `f32`s instead of
four), so I chose that. This is like Nalgebra's
[`UnitComplex`](https://docs.rs/nalgebra/latest/nalgebra/geometry/type.UnitComplex.html)
type, which can be used for the
[`Rotation2`](https://docs.rs/nalgebra/latest/nalgebra/geometry/type.Rotation2.html)
type.

## Implementation

Add a `Rotation2d` type represented as a unit complex number:

```rust
/// A counterclockwise 2D rotation in radians.
///
/// The rotation angle is wrapped to be within the `]-pi, pi]` range.
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,
}
```

Using it is similar to using `Quat`, but in 2D:

```rust
let rotation = Rotation2d::radians(PI / 2.0);

// Rotate vector (also works on Direction2d!)
assert_eq!(rotation * Vec2::X, Vec2::Y);

// Get angle as degrees
assert_eq!(rotation.as_degrees(), 90.0);

// Getting sin and cos is free
let (sin, cos) = rotation.sin_cos();

// "Subtract" rotations
let rotation2 = Rotation2d::FRAC_PI_4; // there are constants!
let diff = rotation * rotation2.inverse();
assert_eq!(diff.as_radians(), PI / 4.0);

// This is equivalent to the above
assert_eq!(rotation2.angle_between(rotation), PI / 4.0);

// Lerp
let rotation1 = Rotation2d::IDENTITY;
let rotation2 = Rotation2d::FRAC_PI_2;
let result = rotation1.lerp(rotation2, 0.5);
assert_eq!(result.as_radians(), std::f32::consts::FRAC_PI_4);

// Slerp
let rotation1 = Rotation2d::FRAC_PI_4);
let rotation2 = Rotation2d::degrees(-180.0); // we can use degrees too!
let result = rotation1.slerp(rotation2, 1.0 / 3.0);
assert_eq!(result.as_radians(), std::f32::consts::FRAC_PI_2);
```

There's also a `From<f32>` implementation for `Rotation2d`, which means
that methods can still accept radians as floats if the argument uses
`impl Into<Rotation2d>`. This means that adding `Rotation2d` shouldn't
even be a breaking change.

---

## Changelog

- Added `Rotation2d`
- Bounding volume methods now take an `impl Into<Rotation2d>`
- Gizmo methods with rotation now take an `impl Into<Rotation2d>`

## Future use cases

- Collision detection (a type like this is quite essential considering
how common vector rotations are)
- `Transform` helpers (e.g. return a 2D rotation about the Z axis from a
`Transform`)
- The rotation used for `Transform2d` (#8268)
- More gizmos, maybe meshes... everything in 2D that uses rotation

---------

Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com>
Co-authored-by: Robert Walter <robwalter96@gmail.com>
Co-authored-by: IQuick 143 <IQuick143cz@gmail.com>
This commit is contained in:
Joona Aalto 2024-03-11 21:11:57 +02:00 committed by GitHub
parent 9cd3165105
commit f89af0567b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 797 additions and 80 deletions

View file

@ -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<Color>) {
pub fn rect_2d(
&mut self,
position: Vec2,
rotation: impl Into<Rotation2d>,
size: Vec2,
color: impl Into<Color>,
) {
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);
}

View file

@ -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

View file

@ -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<Rotation2d>) -> 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<Rotation2d>)
-> 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<Rotation2d>,
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::Rotation>,
) -> 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::Rotation>,
) {
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::Rotation>) -> 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<Self::Rotation>) {
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<Rotation2d>,
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<Self::Rotation>) {
let rotation: Rotation2d = rotation.into();
self.center = rotation * self.center;
}
}

View file

@ -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<Rotation2d>) -> 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<Rotation2d>,
) -> 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<Rotation2d>) -> 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<Rotation2d>,
) -> 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<Rotation2d>) -> 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<Rotation2d>,
) -> 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<Rotation2d>) -> 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<Rotation2d>,
) -> 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<Rotation2d>) -> 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<Rotation2d>,
) -> BoundingCircle {
BoundingCircle::new(translation, self.half_length)
}
}
impl<const N: usize> Bounded2d for Polyline2d<N> {
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> 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<Rotation2d>,
) -> 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<Rotation2d>) -> 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<Rotation2d>,
) -> 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<Rotation2d>) -> 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<Rotation2d>,
) -> 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<Rotation2d>) -> 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<Rotation2d>,
) -> BoundingCircle {
let radius = self.half_size.length();
BoundingCircle::new(translation, radius)
}
}
impl<const N: usize> Bounded2d for Polygon<N> {
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d {
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> 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<Rotation2d>,
) -> 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<Rotation2d>) -> 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<Rotation2d>,
) -> 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<Rotation2d>) -> 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<Rotation2d>,
) -> 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<Rotation2d>) -> 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<Rotation2d>,
) -> BoundingCircle {
BoundingCircle::new(translation, self.radius + self.half_length)
}
}

View file

@ -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::Rotation>,
) -> 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::Rotation>,
) {
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::Rotation>) -> 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<Self::Rotation>) {
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<Self::Rotation>) {
let rotation: Quat = rotation.into();
self.center = rotation * self.center;
}
}

View file

@ -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());

View file

@ -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::Rotation>,
) -> 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::Rotation>,
) {
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::Rotation>) -> 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<Self::Rotation>);
}
/// A trait that generalizes intersection tests against a volume.

View file

@ -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<Dir2> for f32 {
}
}
impl std::ops::Mul<Dir2> 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;

View file

@ -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,
};
}

View file

@ -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<Self> {
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<f32> for Rotation2d {
/// Creates a [`Rotation2d`] from a counterclockwise angle in radians.
fn from(rotation: f32) -> Self {
Self::radians(rotation)
}
}
impl From<Rotation2d> 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<Vec2> 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);
}
}

View file

@ -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,
}
);

View file

@ -493,6 +493,7 @@ mod impls {
mod primitives2d;
mod primitives3d;
mod rect;
mod rotation2d;
}
#[cfg(feature = "petgraph")]
mod petgraph;