Skip to content

Commit f89af05

Browse files
JondolftguichaouaRobWaltIQuick143
authored
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 <[email protected]> Co-authored-by: Robert Walter <[email protected]> Co-authored-by: IQuick 143 <[email protected]>
1 parent 9cd3165 commit f89af05

File tree

12 files changed

+797
-80
lines changed

12 files changed

+797
-80
lines changed

crates/bevy_gizmos/src/gizmos.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use bevy_ecs::{
99
system::{Deferred, ReadOnlySystemParam, Res, Resource, SystemBuffer, SystemMeta, SystemParam},
1010
world::{unsafe_world_cell::UnsafeWorldCell, World},
1111
};
12-
use bevy_math::{Dir3, Mat2, Quat, Vec2, Vec3};
12+
use bevy_math::{Dir3, Quat, Rotation2d, Vec2, Vec3};
1313
use bevy_transform::TransformPoint;
1414

1515
use crate::{
@@ -590,11 +590,17 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> {
590590
/// # bevy_ecs::system::assert_is_system(system);
591591
/// ```
592592
#[inline]
593-
pub fn rect_2d(&mut self, position: Vec2, rotation: f32, size: Vec2, color: impl Into<Color>) {
593+
pub fn rect_2d(
594+
&mut self,
595+
position: Vec2,
596+
rotation: impl Into<Rotation2d>,
597+
size: Vec2,
598+
color: impl Into<Color>,
599+
) {
594600
if !self.enabled {
595601
return;
596602
}
597-
let rotation = Mat2::from_angle(rotation);
603+
let rotation: Rotation2d = rotation.into();
598604
let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2);
599605
self.linestrip_2d([tl, tr, br, bl, tl], color);
600606
}

crates/bevy_math/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ keywords = ["bevy"]
1212
glam = { version = "0.25", features = ["bytemuck"] }
1313
thiserror = "1.0"
1414
serde = { version = "1", features = ["derive"], optional = true }
15+
libm = { version = "0.2", optional = true }
1516
approx = { version = "0.5", optional = true }
1617

1718
[dev-dependencies]
@@ -25,7 +26,7 @@ approx = ["dep:approx", "glam/approx"]
2526
mint = ["glam/mint"]
2627
# Enable libm mathematical functions for glam types to ensure consistent outputs
2728
# across platforms at the cost of losing hardware-level optimization using intrinsics
28-
libm = ["glam/libm"]
29+
libm = ["dep:libm", "glam/libm"]
2930
# Enable assertions to check the validity of parameters passed to glam
3031
glam_assert = ["glam/glam-assert"]
3132
# Enable assertions in debug builds to check the validity of parameters passed to glam

crates/bevy_math/src/bounding/bounded2d/mod.rs

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
mod primitive_impls;
22

3-
use glam::Mat2;
4-
53
use super::{BoundingVolume, IntersectsVolume};
6-
use crate::prelude::Vec2;
4+
use crate::prelude::{Mat2, Rotation2d, Vec2};
75

86
/// Computes the geometric center of the given set of points.
97
#[inline(always)]
@@ -21,10 +19,11 @@ fn point_cloud_2d_center(points: &[Vec2]) -> Vec2 {
2119
pub trait Bounded2d {
2220
/// Get an axis-aligned bounding box for the shape with the given translation and rotation.
2321
/// The rotation is in radians, counterclockwise, with 0 meaning no rotation.
24-
fn aabb_2d(&self, translation: Vec2, rotation: f32) -> Aabb2d;
22+
fn aabb_2d(&self, translation: Vec2, rotation: impl Into<Rotation2d>) -> Aabb2d;
2523
/// Get a bounding circle for the shape
2624
/// The rotation is in radians, counterclockwise, with 0 meaning no rotation.
27-
fn bounding_circle(&self, translation: Vec2, rotation: f32) -> BoundingCircle;
25+
fn bounding_circle(&self, translation: Vec2, rotation: impl Into<Rotation2d>)
26+
-> BoundingCircle;
2827
}
2928

3029
/// A 2D axis-aligned bounding box, or bounding rectangle
@@ -55,10 +54,14 @@ impl Aabb2d {
5554
///
5655
/// Panics if the given set of points is empty.
5756
#[inline(always)]
58-
pub fn from_point_cloud(translation: Vec2, rotation: f32, points: &[Vec2]) -> Aabb2d {
57+
pub fn from_point_cloud(
58+
translation: Vec2,
59+
rotation: impl Into<Rotation2d>,
60+
points: &[Vec2],
61+
) -> Aabb2d {
5962
// Transform all points by rotation
60-
let rotation_mat = Mat2::from_angle(rotation);
61-
let mut iter = points.iter().map(|point| rotation_mat * *point);
63+
let rotation: Rotation2d = rotation.into();
64+
let mut iter = points.iter().map(|point| rotation * *point);
6265

6366
let first = iter
6467
.next()
@@ -94,7 +97,7 @@ impl Aabb2d {
9497

9598
impl BoundingVolume for Aabb2d {
9699
type Translation = Vec2;
97-
type Rotation = f32;
100+
type Rotation = Rotation2d;
98101
type HalfSize = Vec2;
99102

100103
#[inline(always)]
@@ -157,7 +160,11 @@ impl BoundingVolume for Aabb2d {
157160
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
158161
/// and consider storing the original AABB and rotating that every time instead.
159162
#[inline(always)]
160-
fn transformed_by(mut self, translation: Self::Translation, rotation: Self::Rotation) -> Self {
163+
fn transformed_by(
164+
mut self,
165+
translation: Self::Translation,
166+
rotation: impl Into<Self::Rotation>,
167+
) -> Self {
161168
self.transform_by(translation, rotation);
162169
self
163170
}
@@ -170,7 +177,11 @@ impl BoundingVolume for Aabb2d {
170177
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
171178
/// and consider storing the original AABB and rotating that every time instead.
172179
#[inline(always)]
173-
fn transform_by(&mut self, translation: Self::Translation, rotation: Self::Rotation) {
180+
fn transform_by(
181+
&mut self,
182+
translation: Self::Translation,
183+
rotation: impl Into<Self::Rotation>,
184+
) {
174185
self.rotate_by(rotation);
175186
self.translate_by(translation);
176187
}
@@ -189,7 +200,7 @@ impl BoundingVolume for Aabb2d {
189200
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
190201
/// and consider storing the original AABB and rotating that every time instead.
191202
#[inline(always)]
192-
fn rotated_by(mut self, rotation: Self::Rotation) -> Self {
203+
fn rotated_by(mut self, rotation: impl Into<Self::Rotation>) -> Self {
193204
self.rotate_by(rotation);
194205
self
195206
}
@@ -202,11 +213,14 @@ impl BoundingVolume for Aabb2d {
202213
/// can cause the AABB to grow indefinitely. Avoid applying multiple rotations to the same AABB,
203214
/// and consider storing the original AABB and rotating that every time instead.
204215
#[inline(always)]
205-
fn rotate_by(&mut self, rotation: Self::Rotation) {
206-
let rot_mat = Mat2::from_angle(rotation);
207-
let abs_rot_mat = Mat2::from_cols(rot_mat.x_axis.abs(), rot_mat.y_axis.abs());
216+
fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
217+
let rotation: Rotation2d = rotation.into();
218+
let abs_rot_mat = Mat2::from_cols(
219+
Vec2::new(rotation.cos, rotation.sin),
220+
Vec2::new(rotation.sin, rotation.cos),
221+
);
208222
let half_size = abs_rot_mat * self.half_size();
209-
*self = Self::new(rot_mat * self.center(), half_size);
223+
*self = Self::new(rotation * self.center(), half_size);
210224
}
211225
}
212226

@@ -431,7 +445,12 @@ impl BoundingCircle {
431445
///
432446
/// The bounding circle is not guaranteed to be the smallest possible.
433447
#[inline(always)]
434-
pub fn from_point_cloud(translation: Vec2, rotation: f32, points: &[Vec2]) -> BoundingCircle {
448+
pub fn from_point_cloud(
449+
translation: Vec2,
450+
rotation: impl Into<Rotation2d>,
451+
points: &[Vec2],
452+
) -> BoundingCircle {
453+
let rotation: Rotation2d = rotation.into();
435454
let center = point_cloud_2d_center(points);
436455
let mut radius_squared = 0.0;
437456

@@ -443,10 +462,7 @@ impl BoundingCircle {
443462
}
444463
}
445464

446-
BoundingCircle::new(
447-
Mat2::from_angle(rotation) * center + translation,
448-
radius_squared.sqrt(),
449-
)
465+
BoundingCircle::new(rotation * center + translation, radius_squared.sqrt())
450466
}
451467

452468
/// Get the radius of the bounding circle
@@ -476,7 +492,7 @@ impl BoundingCircle {
476492

477493
impl BoundingVolume for BoundingCircle {
478494
type Translation = Vec2;
479-
type Rotation = f32;
495+
type Rotation = Rotation2d;
480496
type HalfSize = f32;
481497

482498
#[inline(always)]
@@ -531,13 +547,14 @@ impl BoundingVolume for BoundingCircle {
531547
}
532548

533549
#[inline(always)]
534-
fn translate_by(&mut self, translation: Vec2) {
550+
fn translate_by(&mut self, translation: Self::Translation) {
535551
self.center += translation;
536552
}
537553

538554
#[inline(always)]
539-
fn rotate_by(&mut self, rotation: f32) {
540-
self.center = Mat2::from_angle(rotation) * self.center;
555+
fn rotate_by(&mut self, rotation: impl Into<Self::Rotation>) {
556+
let rotation: Rotation2d = rotation.into();
557+
self.center = rotation * self.center;
541558
}
542559
}
543560

0 commit comments

Comments
 (0)