Skip to content

Add Rotation2d #11658

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 38 commits into from
Mar 11, 2024
Merged

Add Rotation2d #11658

merged 38 commits into from
Mar 11, 2024

Conversation

Jondolf
Copy link
Contributor

@Jondolf Jondolf commented Feb 1, 2024

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 f32s instead of four), so I chose that. This is like Nalgebra's UnitComplex type, which can be used for the Rotation2 type.

Implementation

Add a Rotation2d type represented as a unit complex number:

/// 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:

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 (Add Transform2d component #8268)
  • More gizmos, maybe meshes... everything in 2D that uses rotation

@Jondolf Jondolf added A-Math Fundamental domain-agnostic mathematical operations C-Feature A new feature, making something new possible labels Feb 1, 2024
/// ```
#[inline]
pub fn lerp(self, end: Self, s: f32) -> Self {
Self::from_sin_cos(self.sin.lerp(end.sin, s), self.cos.lerp(end.cos, s))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm about 99% sure this results in invalid rotations most of the time.
To make this work, one needs to normalise such that sin^2 + cos^2 = 1 (effectively meaning that the vector (sin, cos) has unit length).

To imagine this using complex numbers, every rotation is an unit complex number cos + i * sin and for it to be valid, it has to lie on the unit circle. Lerping moves us between two points on the unit circle along a line. However it is easy to see that this line passes through the inside of the circle, rather than along the surface.

This brings me to another point:
When we lerp two opposite (180° apart) transformations, the midpoint of the lerp is the point (0; 0) which I don't know how to map to any rotation (and normalisation of it would cause a division by 0).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added methods for normalizing and getting the length/norm of the complex number stored in Rotation2d, and changed this lerp to normalize the result.

For the case of opposite angles and s == 0.5, I made it just return the original angle for now (this is documented). I'm not sure if this is the best approach; we could also

  • Just return NaN/infinity for the rotation
  • Return the midpoint between the angles (on which side though?)
  • Panic
  • Something else?

Copy link
Contributor

@NthTensor NthTensor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me now that lerp is fixed.

@Jondolf
Copy link
Contributor Author

Jondolf commented Mar 7, 2024

Addressed the review suggestions. I also updated the 2D bounding volume transformations to use Rotation2d, as discussed here: #11681 (comment)

@Jondolf Jondolf added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Mar 8, 2024
@alice-i-cecile alice-i-cecile added the M-Needs-Release-Note Work that should be called out in the blog due to impact label Mar 11, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Mar 11, 2024
Merged via the queue into bevyengine:main with commit f89af05 Mar 11, 2024
@Jondolf Jondolf deleted the rotation2d branch March 11, 2024 19:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Math Fundamental domain-agnostic mathematical operations C-Feature A new feature, making something new possible M-Needs-Release-Note Work that should be called out in the blog due to impact S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants