mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 20:53:53 +00:00
325f0fd982
# Objective - Closes #11793 - Introduces a general API for aligning local coordinates of Transforms with given vectors. ## Solution - We introduce `Transform::align`, which allows a rotation to be specified by four pieces of alignment data, as explained by the documentation: ````rust /// 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`: ````rust 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: <img width="1440" alt="Screenshot 2024-03-12 at 11 01 19 AM" src="https://github.com/bevyengine/bevy/assets/2975848/884b3cc3-cbd9-48ae-8f8c-49a677c59dfe"> --- ## 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: ````rust 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: ````rust 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: ````rust 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.) 2. Filling in some data for the user for common use-cases; e.g.: ````rust 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: ````rust 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.: ````rust 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 `Transform`s 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`).
260 lines
8.1 KiB
Rust
260 lines
8.1 KiB
Rust
//! This example shows how to align the orientations of objects in 3D space along two axes using the `Transform::align` API.
|
|
|
|
use bevy::color::{
|
|
palettes::basic::{GRAY, RED, WHITE},
|
|
Color,
|
|
};
|
|
use bevy::input::mouse::{MouseButton, MouseButtonInput, MouseMotion};
|
|
use bevy::prelude::*;
|
|
use rand::random;
|
|
use std::f32::consts::PI;
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.add_systems(Startup, setup)
|
|
.add_systems(Update, (draw_cube_axes, draw_random_axes))
|
|
.add_systems(Update, (handle_keypress, handle_mouse, rotate_cube).chain())
|
|
.run();
|
|
}
|
|
|
|
/// This struct stores metadata for a single rotational move of the cube
|
|
#[derive(Component, Default)]
|
|
struct Cube {
|
|
/// The initial transform of the cube move, the starting point of interpolation
|
|
initial_transform: Transform,
|
|
|
|
/// The target transform of the cube move, the endpoint of interpolation
|
|
target_transform: Transform,
|
|
|
|
/// The progress of the cube move in percentage points
|
|
progress: u16,
|
|
|
|
/// Whether the cube is currently in motion; allows motion to be paused
|
|
in_motion: bool,
|
|
}
|
|
|
|
#[derive(Component)]
|
|
struct RandomAxes(Vec3, Vec3);
|
|
|
|
#[derive(Component)]
|
|
struct Instructions;
|
|
|
|
#[derive(Resource)]
|
|
struct MousePressed(bool);
|
|
|
|
// Setup
|
|
|
|
fn setup(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
// A camera looking at the origin
|
|
commands.spawn(Camera3dBundle {
|
|
transform: Transform::from_xyz(3., 2.5, 4.).looking_at(Vec3::ZERO, Vec3::Y),
|
|
..default()
|
|
});
|
|
|
|
// A plane that we can sit on top of
|
|
commands.spawn(PbrBundle {
|
|
transform: Transform::from_xyz(0., -2., 0.),
|
|
mesh: meshes.add(Plane3d::default().mesh().size(100.0, 100.0)),
|
|
material: materials.add(Color::srgb(0.3, 0.5, 0.3)),
|
|
..default()
|
|
});
|
|
|
|
// A light source
|
|
commands.spawn(PointLightBundle {
|
|
point_light: PointLight {
|
|
shadows_enabled: true,
|
|
..default()
|
|
},
|
|
transform: Transform::from_xyz(4.0, 7.0, -4.0),
|
|
..default()
|
|
});
|
|
|
|
// Initialize random axes
|
|
let first = random_direction();
|
|
let second = random_direction();
|
|
commands.spawn(RandomAxes(first, second));
|
|
|
|
// Finally, our cube that is going to rotate
|
|
commands.spawn((
|
|
PbrBundle {
|
|
mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)),
|
|
material: materials.add(Color::srgb(0.5, 0.5, 0.5)),
|
|
..default()
|
|
},
|
|
Cube {
|
|
initial_transform: Transform::IDENTITY,
|
|
target_transform: random_axes_target_alignment(&RandomAxes(first, second)),
|
|
..default()
|
|
},
|
|
));
|
|
|
|
// Instructions for the example
|
|
commands.spawn((
|
|
TextBundle::from_section(
|
|
"The bright red axis is the primary alignment axis, and it will always be\n\
|
|
made to coincide with the primary target direction (white) exactly.\n\
|
|
The fainter red axis is the secondary alignment axis, and it is made to\n\
|
|
line up with the secondary target direction (gray) as closely as possible.\n\
|
|
Press 'R' to generate random target directions.\n\
|
|
Press 'T' to align the cube to those directions.\n\
|
|
Click and drag the mouse to rotate the camera.\n\
|
|
Press 'H' to hide/show these instructions.",
|
|
TextStyle {
|
|
font_size: 20.,
|
|
..default()
|
|
},
|
|
)
|
|
.with_style(Style {
|
|
position_type: PositionType::Absolute,
|
|
top: Val::Px(12.0),
|
|
left: Val::Px(12.0),
|
|
..default()
|
|
}),
|
|
Instructions,
|
|
));
|
|
|
|
commands.insert_resource(MousePressed(false));
|
|
}
|
|
|
|
// Update systems
|
|
|
|
// Draw the main and secondary axes on the rotating cube
|
|
fn draw_cube_axes(mut gizmos: Gizmos, query: Query<&Transform, With<Cube>>) {
|
|
let cube_transform = query.single();
|
|
|
|
// Local X-axis arrow
|
|
let x_ends = arrow_ends(cube_transform, Vec3::X, 1.5);
|
|
gizmos.arrow(x_ends.0, x_ends.1, RED);
|
|
|
|
// local Y-axis arrow
|
|
let y_ends = arrow_ends(cube_transform, Vec3::Y, 1.5);
|
|
gizmos.arrow(y_ends.0, y_ends.1, Color::srgb(0.65, 0., 0.));
|
|
}
|
|
|
|
// Draw the randomly generated axes
|
|
fn draw_random_axes(mut gizmos: Gizmos, query: Query<&RandomAxes>) {
|
|
let RandomAxes(v1, v2) = query.single();
|
|
gizmos.arrow(Vec3::ZERO, 1.5 * *v1, WHITE);
|
|
gizmos.arrow(Vec3::ZERO, 1.5 * *v2, GRAY);
|
|
}
|
|
|
|
// Actually update the cube's transform according to its initial source and target
|
|
fn rotate_cube(mut cube: Query<(&mut Cube, &mut Transform)>) {
|
|
let (mut cube, mut cube_transform) = cube.single_mut();
|
|
|
|
if !cube.in_motion {
|
|
return;
|
|
}
|
|
|
|
let start = cube.initial_transform.rotation;
|
|
let end = cube.target_transform.rotation;
|
|
|
|
let p: f32 = cube.progress.into();
|
|
let t = p / 100.;
|
|
|
|
*cube_transform = Transform::from_rotation(start.slerp(end, t));
|
|
|
|
if cube.progress == 100 {
|
|
cube.in_motion = false;
|
|
} else {
|
|
cube.progress += 1;
|
|
}
|
|
}
|
|
|
|
// Handle user inputs from the keyboard for dynamically altering the scenario
|
|
fn handle_keypress(
|
|
mut cube: Query<(&mut Cube, &Transform)>,
|
|
mut random_axes: Query<&mut RandomAxes>,
|
|
mut instructions: Query<&mut Visibility, With<Instructions>>,
|
|
keyboard: Res<ButtonInput<KeyCode>>,
|
|
) {
|
|
let (mut cube, cube_transform) = cube.single_mut();
|
|
let mut random_axes = random_axes.single_mut();
|
|
|
|
if keyboard.just_pressed(KeyCode::KeyR) {
|
|
// Randomize the target axes
|
|
let first = random_direction();
|
|
let second = random_direction();
|
|
*random_axes = RandomAxes(first, second);
|
|
|
|
// Stop the cube and set it up to transform from its present orientation to the new one
|
|
cube.in_motion = false;
|
|
cube.initial_transform = *cube_transform;
|
|
cube.target_transform = random_axes_target_alignment(&random_axes);
|
|
cube.progress = 0;
|
|
}
|
|
|
|
if keyboard.just_pressed(KeyCode::KeyT) {
|
|
cube.in_motion ^= true;
|
|
}
|
|
|
|
if keyboard.just_pressed(KeyCode::KeyH) {
|
|
let mut instructions_viz = instructions.single_mut();
|
|
if *instructions_viz == Visibility::Hidden {
|
|
*instructions_viz = Visibility::Visible;
|
|
} else {
|
|
*instructions_viz = Visibility::Hidden;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle user mouse input for panning the camera around
|
|
fn handle_mouse(
|
|
mut button_events: EventReader<MouseButtonInput>,
|
|
mut motion_events: EventReader<MouseMotion>,
|
|
mut camera: Query<&mut Transform, With<Camera>>,
|
|
mut mouse_pressed: ResMut<MousePressed>,
|
|
) {
|
|
// Store left-pressed state in the MousePressed resource
|
|
for button_event in button_events.read() {
|
|
if button_event.button != MouseButton::Left {
|
|
continue;
|
|
}
|
|
*mouse_pressed = MousePressed(button_event.state.is_pressed());
|
|
}
|
|
|
|
// If the mouse is not pressed, just ignore motion events
|
|
if !mouse_pressed.0 {
|
|
return;
|
|
}
|
|
let displacement = motion_events
|
|
.read()
|
|
.fold(0., |acc, mouse_motion| acc + mouse_motion.delta.x);
|
|
let mut camera_transform = camera.single_mut();
|
|
camera_transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(-displacement / 75.));
|
|
}
|
|
|
|
// Helper functions (i.e. non-system functions)
|
|
|
|
fn arrow_ends(transform: &Transform, axis: Vec3, length: f32) -> (Vec3, Vec3) {
|
|
let local_vector = length * (transform.rotation * axis);
|
|
(transform.translation, transform.translation + local_vector)
|
|
}
|
|
|
|
fn random_direction() -> Vec3 {
|
|
let height = random::<f32>() * 2. - 1.;
|
|
let theta = random::<f32>() * 2. * PI;
|
|
|
|
build_direction(height, theta)
|
|
}
|
|
|
|
fn build_direction(height: f32, theta: f32) -> Vec3 {
|
|
let z = height;
|
|
let m = f32::acos(z).sin();
|
|
let x = theta.cos() * m;
|
|
let y = theta.sin() * m;
|
|
|
|
Vec3::new(x, y, z)
|
|
}
|
|
|
|
// This is where `Transform::align` is actually used!
|
|
// Note that the choice of `Vec3::X` and `Vec3::Y` here matches the use of those in `draw_cube_axes`.
|
|
fn random_axes_target_alignment(random_axes: &RandomAxes) -> Transform {
|
|
let RandomAxes(first, second) = random_axes;
|
|
Transform::IDENTITY.aligned_by(Vec3::X, *first, Vec3::Y, *second)
|
|
}
|