Skip to content
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

Alignment API for Transforms #12187

Merged
merged 13 commits into from
Mar 14, 2024
Merged

Conversation

mweatherley
Copy link
Contributor

@mweatherley mweatherley commented Feb 28, 2024

Objective

Solution

  • We introduce Transform::align, which allows a rotation to be specified by four pieces of alignment data, as explained by the documentation:
/// Rotates this [`Transform`] so that the `main_axis` vector, reinterpreted in local coordinates, points
/// in the given `main_direction`, while `secondary_axis` points towards `secondary_direction`.
///
/// For example, if a spaceship model has its nose pointing in the X-direction in its own local coordinates
/// and its dorsal fin pointing in the Y-direction, then `align(Vec3::X, v, Vec3::Y, w)` will make the spaceship's
/// nose point in the direction of `v`, while the dorsal fin does its best to point in the direction `w`.
///
/// More precisely, the [`Transform::rotation`] produced will be such that:
/// * applying it to `main_axis` results in `main_direction`
/// * applying it to `secondary_axis` produces a vector that lies in the half-plane generated by `main_direction` and
/// `secondary_direction` (with positive contribution by `secondary_direction`)
///
/// [`Transform::look_to`] is recovered, for instance, when `main_axis` is `Vec3::NEG_Z` (the [`Transform::forward`]
/// direction in the default orientation) and `secondary_axis` is `Vec3::Y` (the [`Transform::up`] direction in the default
/// orientation). (Failure cases may differ somewhat.)
///
/// In some cases a rotation cannot be constructed. Another axis will be picked in those cases:
/// * if `main_axis` or `main_direction` is zero, `Vec3::X` takes its place
/// * if `secondary_axis` or `secondary_direction` is zero, `Vec3::Y` takes its place
/// * if `main_axis` is parallel with `secondary_axis` or `main_direction` is parallel with `secondary_direction`,
/// a rotation is constructed which takes `main_axis` to `main_direction` along a great circle, ignoring the secondary
/// counterparts
/// 
/// Example
/// ```
/// # use bevy_math::{Vec3, Quat};
/// # use bevy_transform::components::Transform;
/// let mut t1 = Transform::IDENTITY;
/// let mut t2 = Transform::IDENTITY;
/// t1.align(Vec3::ZERO, Vec3::Z, Vec3::ZERO, Vec3::X);
/// t2.align(Vec3::X, Vec3::Z, Vec3::Y, Vec3::X);
/// assert_eq!(t1.rotation, t2.rotation);
/// 
/// t1.align(Vec3::X, Vec3::Z, Vec3::X, Vec3::Y);
/// assert_eq!(t1.rotation, Quat::from_rotation_arc(Vec3::X, Vec3::Z));
/// ```
pub fn align(
    &mut self,
    main_axis: Vec3,
    main_direction: Vec3,
    secondary_axis: Vec3,
    secondary_direction: Vec3,
) { //... }
  • We introduce Transform::aligned_by, the returning-Self version of align:
pub fn aligned_by(
    mut self,
    main_axis: Vec3,
    main_direction: Vec3,
    secondary_axis: Vec3,
    secondary_direction: Vec3,
) -> Self { //... }
  • We introduce an example (examples/transforms/align.rs) that shows the usage of this API. It is likely to be mathier than most other Transform APIs, so when run, the example demonstrates what the API does in space:
Screenshot 2024-03-12 at 11 01 19 AM

Changelog

  • Added methods align, aligned_by to Transform.
  • Added transforms/align.rs to examples.

Discussion

On the form of align

The original issue linked above suggests an API similar to that of the existing Transform::look_to method:

pub fn align_to(&mut self, direction: Vec3, up: Vec3) { //... }

Not allowing an input axis of some sort that is to be aligned with direction would not really solve the problem in the issue, since the user could easily be in a scenario where they have to compose with another rotation on their own (undesirable). This leads to something like:

pub fn align_to(&mut self, axis: Vec3, direction: Vec3, up: Vec3) { //... }

However, this still has two problems:

  • If the vector that the user wants to align is parallel to the Y-axis, then the API basically does not work (we cannot fully specify a rotation)
  • More generally, it does not give the user the freedom to specify which direction is to be treated as the local "up" direction, so it fails as a general alignment API

Specifying both leads us to the present situation, with two local axis inputs (main_axis and secondary_axis) and two target directions (main_direction and secondary_direction). This might seem a little cumbersome for general use, but for the time being I stand by the decision not to expand further without prompting from users. I'll expand on this below.

Additional APIs?

Presently, this PR introduces only align and aligned_by. Other potentially useful bundles of API surface arrange into a few different categories:

  1. Inferring direction from position, a la Transform::look_at, which might look something like this:
pub fn align_at(&mut self, axis: Vec3, target: Vec3, up: Vec3) {
    self.align(axis, target - self.translation, Vec3::Y, up);
}

(This is simple but still runs into issues when the user wants to point the local Y-axis somewhere.)

  1. Filling in some data for the user for common use-cases; e.g.:
pub fn align_x(&mut self, direction: Vec3, up: Vec3) {
    self.align(Vec3::X, direction, Vec3::Y, up);
}

(Here, use of the up vector doesn't lose any generality, but it might be less convenient to specify than something else. This does naturally leave open the question of what align_y would look like if we provided it.)

Morally speaking, I do think that the up business is more pertinent when the intention is to work with cameras, which the look_at and look_to APIs seem to cover pretty well. If that's the case, then I'm not sure what the ideal shape for these API functions would be, since it seems like a lot of input would have to be baked into the function definitions. For some cases, this might not be the end of the world:

pub fn align_x_z(&mut self, direction: Vec3, weak_direction: Vec3) {
    self.align(Vec3::X, direction, Vec3::Z, weak_direction);
}

(However, this is not symmetrical in x and z, so you'd still need six API functions just to support the standard positive coordinate axes, and if you support negative axes then things really start to balloon.)

The reasons that these are not actually produced in this PR are as follows:

  1. Without prompting from actual users in the wild, it is unknown to me whether these additional APIs would actually see a lot of use. Extending these to our users in the future would be trivial if we see there is a demand for something specific from the above-mentioned categories.
  2. As discussed above, there are so many permutations of these that could be provided that trying to do so looks like it risks unduly ballooning the API surface for this feature.
  3. Finally, and most importantly, creating these helper functions in user-space is trivial, since they all just involve specializing align to particular inputs; e.g.:
fn align_ship(ship_transform: &mut Transform, nose_direction: Vec3, dorsal_direction: Vec3) {
    ship_transform.align(Ship::NOSE, nose_direction, Ship::DORSAL, dorsal_direction);
}

With that in mind, I would prefer instead to focus on making the documentation and examples for a thin API as clear as possible, so that users can get a grip on the tool and specialize it for their own needs when they feel the desire to do so.

Dir3?

As in the case of Transform::look_to and Transform::look_at, the inputs to this function are, morally speaking, directions rather than vectors (actually, if we're being pedantic, the input is really really a pair of orthonormal frames), so it's worth asking whether we should really be using Dir3 as inputs instead of Vec3. I opted for Vec3 for the following reasons:

  1. Specifying a Dir3 in user-space is just more annoying than providing a Vec3. Even in the most basic cases (e.g. providing a vector literal), you still have to do error handling or call an unsafe unwrap in your function invocations.
  2. The existing API mentioned above uses Vec3, so we are just adhering to the same thing.

Of course, the use of Vec3 has its own downsides; it can be argued that the replacement of zero-vectors with fixed ones (which we do in Transform::align as well as Transform::look_to) more-or-less amounts to failing silently.

Future steps

The question of additional APIs was addressed above. For me, the main thing here to handle more immediately is actually just upstreaming this API (or something similar and slightly mathier) to glam::Quat. The reason that this would be desirable for users is that this API currently only works with Transforms even though all it's actually doing is specifying a rotation. Upstreaming to glam::Quat, properly done, could buy a lot basically for free, since a number of Transform methods take a rotation as an input. Using these together would require a little bit of mathematical savvy, but it opens up some good things (e.g. Transform::rotate_around).

Copy link
Contributor

The generated examples/README.md is out of sync with the example metadata in Cargo.toml or the example readme template. Please run cargo run -p build-templated-pages -- update examples to update it, and commit the file change.

@TrialDragon TrialDragon added C-Feature A new feature, making something new possible A-Transform Translations, rotations and scales labels Feb 28, 2024
@MiniaczQ
Copy link
Contributor

MiniaczQ commented Mar 5, 2024

Little bit niche, but definitely useful API.

The explanation can be a bit rough to users, the handle and weak are not my first choice.
I'd leverage the (transform) local and (world) global as well as direction and up that are used in other methods:
local_direction local_up global_direction global_up

Since this is a very 'generic' function, which will be exposed under wrappers, it can use a more explicit name like align_local_to_global.

As for the bloat with various axis combinations, perhaps we can use an enum, similar to EulerRot, which would contain all the main axis (and their negatives) combinations (should be 24?).

AlignAxes {
  XY,
  NXY,
  NXNY,
  XNY,
  ...
}

@mweatherley mweatherley marked this pull request as ready for review March 12, 2024 16:22
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.

I like this, nice work! Do you think you could also add a doc-test for align?

Whats the plan for moving this up into glam?

@mweatherley
Copy link
Contributor Author

mweatherley commented Mar 13, 2024

I like this, nice work! Do you think you could also add a doc-test for align?

Done!

Whats the plan for moving this up into glam?

Great question. Mathematically speaking, the process that we're doing here really consists of two steps:

  1. An orthonormal frame is determined by a nondegenerate ordered pair of vectors (i.e. spanning a plane) by orthogonalizing the second with respect to the first then taking their cross product to give the third vector (with a couple normalizations in there).
  2. Given a pair of orthonormal frames, we find a quaternion that transports one to the other.

Algebraically, number 2 is basically trivial (at least given what's already in glam) — it should amount to calling Quat::from_mat3 a couple times, inverting one of them, and then multiplying the resulting quaternions.

So I think the trick will be finding the right balance in terms of the kinds of things that "belong" in glam; for instance, might it be reasonable for the library to provide a function that constructs a single quaternion from a nondegenerate pair of vectors? Maybe! As I see it, step (1) is where a lot of the ergonomics are, but step (2) is maybe the more natural one mathematically, but I think that slicing it up this way is at least vaguely how I imagine it.

More than anything, I'm concerned about the tension between:

  • Breaking it up making it harder to use
  • Not breaking it up packaging a lot of things implicitly together for a "basic" library

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.

Nice work!

I'd prefer if the doc test asserted the invariant presented in the doc for some standard case, but I'm going to approve as is.

  • applying it to main_axis results in main_direction
  • applying it to secondary_axis produces a vector that lies in the half-plane generated by main_direction and secondary_direction (with positive contribution by secondary_direction)

Please do link the glam PR here if you end up making one. I think implementing it on the transform is a good improvement on it's own.

@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 13, 2024
Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

One doc nit, but otherwise I'm pleased by this. Thanks for motivating this so well, and the really clear internals!

@alice-i-cecile alice-i-cecile 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 13, 2024
@mweatherley
Copy link
Contributor Author

I'd prefer if the doc test asserted the invariant presented in the doc for some standard case, but I'm going to approve as is.

Cool! I changed the doctest so that the examples are more like a standard library function — demonstrating some inputs and outputs along with the fallback behavior.

Please do link the glam PR here if you end up making one. I think implementing it on the transform is a good improvement on it's own.

Will do!

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Mar 14, 2024
Merged via the queue into bevyengine:main with commit 325f0fd Mar 14, 2024
29 checks passed
@mweatherley mweatherley deleted the coord-alignment branch March 14, 2024 15:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Transform Translations, rotations and scales 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.

API for rotation alignment
5 participants