From 6bfedc2346d806d97234b03ebfa3004f8cc1f3d9 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 28 Feb 2024 11:19:14 -0500 Subject: [PATCH 01/10] Added Transform::align and Transform::align_by; example 'align' added to Transforms examples --- Cargo.toml | 11 + .../src/components/transform.rs | 76 ++++++ examples/transforms/align.rs | 246 ++++++++++++++++++ 3 files changed, 333 insertions(+) create mode 100644 examples/transforms/align.rs diff --git a/Cargo.toml b/Cargo.toml index 0a41a17a9350e..955a54c7dad1f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2231,6 +2231,17 @@ description = "Illustrates how to (constantly) rotate an object around an axis" category = "Transforms" wasm = true +[[example]] +name = "align" +path = "examples/transforms/align.rs" +doc-scrape-examples = true + +[package.metadata.example.align] +name = "Alignment" +description = "A demonstration of Transform's axis-alignment feature" +category = "Transforms" +wasm = true + [[example]] name = "scale" path = "examples/transforms/scale.rs" diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 46dcceb68e5cf..a0a9b5564a9c4 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -146,6 +146,29 @@ impl Transform { self } + /// Returns this [`Transform`] with a rotation so that the `handle` vector, reinterpreted in local coordinates, + /// points in the given `direction`, while `weak_handle` points towards `weak_direction`. + /// + /// In some cases a rotation cannot be constructed. Another axis will be picked in those cases: + /// * if `handle` or `direction` is zero, `Vec3::X` takes its place + /// * if `weak_handle` or `weak_direction` is zero, `Vec3::Y` takes its place + /// * if `handle` is parallel with `weak_handle` or `direction` is parallel with `weak_direction`, a rotation is + /// constructed which takes `handle` to `direction` but ignores the weak counterparts (i.e. is otherwise unspecified) + /// + /// See [`Transform::align`] for additional details. + #[inline] + #[must_use] + pub fn aligned_by( + mut self, + handle: Vec3, + direction: Vec3, + weak_handle: Vec3, + weak_direction: Vec3, + ) -> Self { + self.align(handle, direction, weak_handle, weak_direction); + self + } + /// Returns this [`Transform`] with a new translation. #[inline] #[must_use] @@ -367,6 +390,59 @@ impl Transform { self.rotation = Quat::from_mat3(&Mat3::from_cols(right, up, back)); } + /// Rotates this [`Transform`] so that the `handle` vector, reinterpreted in local coordinates, points + /// in the given `direction`, while `weak_handle` points towards `weak_direction`. + /// + /// More precisely, the [`Transform::rotation`] produced will be such that: + /// * applying it to `handle` produces `direction` + /// * applying it to `weak_handle` produces a vector that lies in the half-plane generated by `direction` and + /// `weak_direction` (with positive contribution by `weak_direction`) + /// + /// For example, [`Transform::look_to`] is recovered when `handle` is `Vec3::NEG_Z` (the [`Transform::forward`] direction + /// in the default orientation) and `weak_handle` 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 `handle` or `direction` is zero, `Vec3::X` takes its place + /// * if `weak_handle` or `weak_direction` is zero, `Vec3::Y` takes its place + /// * if `handle` is parallel with `weak_handle` or `direction` is parallel with `weak_direction`, a rotation is + /// constructed which takes `handle` to `direction` but ignores the weak counterparts (i.e. is otherwise unspecified) + #[inline] + pub fn align( + &mut self, + handle: Vec3, + direction: Vec3, + weak_handle: Vec3, + weak_direction: Vec3, + ) { + let handle = handle.try_normalize().unwrap_or(Vec3::X); + let direction = direction.try_normalize().unwrap_or(Vec3::X); + let weak_handle = weak_handle.try_normalize().unwrap_or(Vec3::Y); + let weak_direction = weak_direction.try_normalize().unwrap_or(Vec3::Y); + + // The solution quaternion will be constructed in two steps. + // First, we start with a rotation that takes `handle` to `direction`. + let first_rotation = Quat::from_rotation_arc(handle, direction); + + // Let's follow by rotating about the `direction` axis so that the image of `weak_handle` + // is taken to something that lies in the plane of `direction` and `weak_direction`. Since + // `direction` is fixed by this rotation, the first criterion is still satisfied. + let weak_image = first_rotation * weak_handle; + let weak_image_ortho = weak_image.reject_from_normalized(direction).try_normalize(); + let weak_direction_ortho = weak_direction + .reject_from_normalized(direction) + .try_normalize(); + + // If one of the two weak vectors was parallel to `direction`, then we just do the first part + self.rotation = match (weak_image_ortho, weak_direction_ortho) { + (Some(weak_img_ortho), Some(weak_dir_ortho)) => { + let second_rotation = Quat::from_rotation_arc(weak_img_ortho, weak_dir_ortho); + second_rotation * first_rotation + } + _ => first_rotation, + }; + } + /// Multiplies `self` with `transform` component by component, returning the /// resulting [`Transform`] #[inline] diff --git a/examples/transforms/align.rs b/examples/transforms/align.rs new file mode 100644 index 0000000000000..857879f260f5f --- /dev/null +++ b/examples/transforms/align.rs @@ -0,0 +1,246 @@ +//! This example shows how to use the `Transform::align` API. + +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(); +} + +#[derive(Component, Default)] +struct Cube { + initial_transform: Transform, + target_transform: Transform, + progress: u16, + 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>, + mut materials: ResMut>, +) { + // 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::rgb(0.5, 0.3, 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::rgb(0.5, 0.5, 0.5)), + ..default() + }, + Cube { + initial_transform: Transform::IDENTITY, + target_transform: random_axes_target_alignment(&RandomAxes(first, second)), + ..default() + }, + )); + + commands.spawn(( + TextBundle::from_section( + "Colors:\n\ + R: X axis - the primary axis of the alignment\n\ + G: Y axis - the secondary axis of the alignment\n\ + B: Z axis - action determined by the preceding two\n\ + White: Random direction - primary direction of alignment\n\ + Gray: Random direction - secondary direction of alignment\n\ + Press 'R' to generate random alignment 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 + +fn draw_cube_axes(mut gizmos: Gizmos, query: Query<&Transform, With>) { + 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, Color::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::GREEN); + + // local Z-axis arrow + let z_ends = arrow_ends(cube_transform, Vec3::Z, 1.5); + gizmos.arrow(z_ends.0, z_ends.1, Color::BLUE); +} + +fn draw_random_axes(mut gizmos: Gizmos, query: Query<&RandomAxes>) { + let RandomAxes(v1, v2) = query.single(); + gizmos.arrow(Vec3::ZERO, 1.5 * *v1, Color::WHITE); + gizmos.arrow(Vec3::ZERO, 1.5 * *v2, Color::GRAY); +} + +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; + } +} + +fn handle_keypress( + mut cube: Query<(&mut Cube, &Transform)>, + mut random_axes: Query<&mut RandomAxes>, + mut instructions: Query<&mut Visibility, With>, + keyboard: Res>, +) { + 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; + } + } +} + +fn handle_mouse( + mut button_events: EventReader, + mut motion_events: EventReader, + mut camera: Query<&mut Transform, With>, + mut mouse_pressed: ResMut, +) { + // 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::() * 2. - 1.; + let theta = random::() * 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) +} + +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) +} From ad711d1517f968ef7ee7de380749206a7080350c Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 28 Feb 2024 13:08:29 -0500 Subject: [PATCH 02/10] Updated examples/README.md --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index 7a605b114e0ad..5736a6804599f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -374,6 +374,7 @@ Example | Description Example | Description --- | --- [3D Rotation](../examples/transforms/3d_rotation.rs) | Illustrates how to (constantly) rotate an object around an axis +[Alignment](../examples/transforms/align.rs) | A demonstration of Transform's axis-alignment feature [Scale](../examples/transforms/scale.rs) | Illustrates how to scale an object in each direction [Transform](../examples/transforms/transform.rs) | Shows multiple transformations of objects [Translation](../examples/transforms/translation.rs) | Illustrates how to move an object along an axis From f84d79ad90f83e781c4279f588d079030e174b3c Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 28 Feb 2024 13:29:46 -0500 Subject: [PATCH 03/10] Color -> LegacyColor in example 'align' --- examples/transforms/align.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/transforms/align.rs b/examples/transforms/align.rs index 857879f260f5f..466c70c881525 100644 --- a/examples/transforms/align.rs +++ b/examples/transforms/align.rs @@ -48,7 +48,7 @@ fn setup( 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::rgb(0.5, 0.3, 0.3)), + material: materials.add(LegacyColor::rgb(0.5, 0.3, 0.3)), ..default() }); @@ -71,7 +71,7 @@ fn setup( commands.spawn(( PbrBundle { mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), - material: materials.add(Color::rgb(0.5, 0.5, 0.5)), + material: materials.add(LegacyColor::rgb(0.5, 0.5, 0.5)), ..default() }, Cube { @@ -117,21 +117,21 @@ fn draw_cube_axes(mut gizmos: Gizmos, query: Query<&Transform, With>) { // Local X-axis arrow let x_ends = arrow_ends(cube_transform, Vec3::X, 1.5); - gizmos.arrow(x_ends.0, x_ends.1, Color::RED); + gizmos.arrow(x_ends.0, x_ends.1, LegacyColor::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::GREEN); + gizmos.arrow(y_ends.0, y_ends.1, LegacyColor::GREEN); // local Z-axis arrow let z_ends = arrow_ends(cube_transform, Vec3::Z, 1.5); - gizmos.arrow(z_ends.0, z_ends.1, Color::BLUE); + gizmos.arrow(z_ends.0, z_ends.1, LegacyColor::BLUE); } fn draw_random_axes(mut gizmos: Gizmos, query: Query<&RandomAxes>) { let RandomAxes(v1, v2) = query.single(); - gizmos.arrow(Vec3::ZERO, 1.5 * *v1, Color::WHITE); - gizmos.arrow(Vec3::ZERO, 1.5 * *v2, Color::GRAY); + gizmos.arrow(Vec3::ZERO, 1.5 * *v1, LegacyColor::WHITE); + gizmos.arrow(Vec3::ZERO, 1.5 * *v2, LegacyColor::GRAY); } fn rotate_cube(mut cube: Query<(&mut Cube, &mut Transform)>) { From 2b56fe0287dc5cf547ff0fe6b4a548fb77cab0a0 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 2 Mar 2024 12:22:57 -0500 Subject: [PATCH 04/10] LegacyColor -> Color again; simplified example for clarity --- examples/transforms/align.rs | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/examples/transforms/align.rs b/examples/transforms/align.rs index 466c70c881525..b9e7f22f900db 100644 --- a/examples/transforms/align.rs +++ b/examples/transforms/align.rs @@ -2,6 +2,10 @@ use bevy::input::mouse::{MouseButton, MouseButtonInput, MouseMotion}; use bevy::prelude::*; +use bevy::color::{ + palettes::basic::{RED, WHITE, GRAY}, + Color, +}; use rand::random; use std::f32::consts::PI; @@ -48,7 +52,7 @@ fn setup( commands.spawn(PbrBundle { transform: Transform::from_xyz(0., -2., 0.), mesh: meshes.add(Plane3d::default().mesh().size(100.0, 100.0)), - material: materials.add(LegacyColor::rgb(0.5, 0.3, 0.3)), + material: materials.add(Color::srgb(0.3, 0.5, 0.3)), ..default() }); @@ -71,7 +75,7 @@ fn setup( commands.spawn(( PbrBundle { mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), - material: materials.add(LegacyColor::rgb(0.5, 0.5, 0.5)), + material: materials.add(Color::srgb(0.5, 0.5, 0.5)), ..default() }, Cube { @@ -83,13 +87,11 @@ fn setup( commands.spawn(( TextBundle::from_section( - "Colors:\n\ - R: X axis - the primary axis of the alignment\n\ - G: Y axis - the secondary axis of the alignment\n\ - B: Z axis - action determined by the preceding two\n\ - White: Random direction - primary direction of alignment\n\ - Gray: Random direction - secondary direction of alignment\n\ - Press 'R' to generate random alignment directions.\n\ + "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.", @@ -117,21 +119,17 @@ fn draw_cube_axes(mut gizmos: Gizmos, query: Query<&Transform, With>) { // Local X-axis arrow let x_ends = arrow_ends(cube_transform, Vec3::X, 1.5); - gizmos.arrow(x_ends.0, x_ends.1, LegacyColor::RED); + 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, LegacyColor::GREEN); - - // local Z-axis arrow - let z_ends = arrow_ends(cube_transform, Vec3::Z, 1.5); - gizmos.arrow(z_ends.0, z_ends.1, LegacyColor::BLUE); + gizmos.arrow(y_ends.0, y_ends.1, Color::srgb(0.65, 0., 0.)); } fn draw_random_axes(mut gizmos: Gizmos, query: Query<&RandomAxes>) { let RandomAxes(v1, v2) = query.single(); - gizmos.arrow(Vec3::ZERO, 1.5 * *v1, LegacyColor::WHITE); - gizmos.arrow(Vec3::ZERO, 1.5 * *v2, LegacyColor::GRAY); + gizmos.arrow(Vec3::ZERO, 1.5 * *v1, WHITE); + gizmos.arrow(Vec3::ZERO, 1.5 * *v2, GRAY); } fn rotate_cube(mut cube: Query<(&mut Cube, &mut Transform)>) { From edb835876ce59c66216acdbd304ca81855cca876 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 12 Mar 2024 11:08:15 -0400 Subject: [PATCH 05/10] Improved documentation of alignment API --- .../src/components/transform.rs | 84 ++++++++++++------- examples/transforms/align.rs | 6 +- 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 13961c03329a5..034c63b1eff8a 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -148,6 +148,11 @@ impl Transform { /// Returns this [`Transform`] with a rotation so that the `handle` vector, reinterpreted in local coordinates, /// points in the given `direction`, while `weak_handle` points towards `weak_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 `Transform::aligned_by(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`. + /// /// In some cases a rotation cannot be constructed. Another axis will be picked in those cases: /// * if `handle` or `direction` is zero, `Vec3::X` takes its place /// * if `weak_handle` or `weak_direction` is zero, `Vec3::Y` takes its place @@ -159,12 +164,17 @@ impl Transform { #[must_use] pub fn aligned_by( mut self, - handle: Vec3, - direction: Vec3, - weak_handle: Vec3, - weak_direction: Vec3, + main_axis: Vec3, + main_direction: Vec3, + secondary_axis: Vec3, + secondary_direction: Vec3, ) -> Self { - self.align(handle, direction, weak_handle, weak_direction); + self.align( + main_axis, + main_direction, + secondary_axis, + secondary_direction, + ); self } @@ -389,53 +399,63 @@ impl Transform { self.rotation = Quat::from_mat3(&Mat3::from_cols(right, up, back)); } - /// Rotates this [`Transform`] so that the `handle` vector, reinterpreted in local coordinates, points - /// in the given `direction`, while `weak_handle` points towards `weak_direction`. + /// 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`. + /// + /// For a visual demonstration, see the 'align' example. /// /// More precisely, the [`Transform::rotation`] produced will be such that: - /// * applying it to `handle` produces `direction` - /// * applying it to `weak_handle` produces a vector that lies in the half-plane generated by `direction` and - /// `weak_direction` (with positive contribution by `weak_direction`) + /// * 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`) /// - /// For example, [`Transform::look_to`] is recovered when `handle` is `Vec3::NEG_Z` (the [`Transform::forward`] direction - /// in the default orientation) and `weak_handle` is `Vec3::Y` (the [`Transform::up`] direction in the default + /// [`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 `handle` or `direction` is zero, `Vec3::X` takes its place - /// * if `weak_handle` or `weak_direction` is zero, `Vec3::Y` takes its place - /// * if `handle` is parallel with `weak_handle` or `direction` is parallel with `weak_direction`, a rotation is - /// constructed which takes `handle` to `direction` but ignores the weak counterparts (i.e. is otherwise unspecified) + /// * if `main_axis` or `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` but ignores the secondary counterparts + /// (i.e. is otherwise unspecified) #[inline] pub fn align( &mut self, - handle: Vec3, - direction: Vec3, - weak_handle: Vec3, - weak_direction: Vec3, + main_axis: Vec3, + main_direction: Vec3, + secondary_axis: Vec3, + secondary_direction: Vec3, ) { - let handle = handle.try_normalize().unwrap_or(Vec3::X); - let direction = direction.try_normalize().unwrap_or(Vec3::X); - let weak_handle = weak_handle.try_normalize().unwrap_or(Vec3::Y); - let weak_direction = weak_direction.try_normalize().unwrap_or(Vec3::Y); + let main_axis = main_axis.try_normalize().unwrap_or(Vec3::X); + let main_direction = main_direction.try_normalize().unwrap_or(Vec3::X); + let secondary_axis = secondary_axis.try_normalize().unwrap_or(Vec3::Y); + let secondary_direction = secondary_direction.try_normalize().unwrap_or(Vec3::Y); // The solution quaternion will be constructed in two steps. // First, we start with a rotation that takes `handle` to `direction`. - let first_rotation = Quat::from_rotation_arc(handle, direction); + let first_rotation = Quat::from_rotation_arc(main_axis, main_direction); // Let's follow by rotating about the `direction` axis so that the image of `weak_handle` // is taken to something that lies in the plane of `direction` and `weak_direction`. Since // `direction` is fixed by this rotation, the first criterion is still satisfied. - let weak_image = first_rotation * weak_handle; - let weak_image_ortho = weak_image.reject_from_normalized(direction).try_normalize(); - let weak_direction_ortho = weak_direction - .reject_from_normalized(direction) + let secondary_image = first_rotation * secondary_axis; + let secondary_image_ortho = secondary_image + .reject_from_normalized(main_direction) + .try_normalize(); + let secondary_direction_ortho = secondary_direction + .reject_from_normalized(main_direction) .try_normalize(); // If one of the two weak vectors was parallel to `direction`, then we just do the first part - self.rotation = match (weak_image_ortho, weak_direction_ortho) { - (Some(weak_img_ortho), Some(weak_dir_ortho)) => { - let second_rotation = Quat::from_rotation_arc(weak_img_ortho, weak_dir_ortho); + self.rotation = match (secondary_image_ortho, secondary_direction_ortho) { + (Some(secondary_img_ortho), Some(secondary_dir_ortho)) => { + let second_rotation = + Quat::from_rotation_arc(secondary_img_ortho, secondary_dir_ortho); second_rotation * first_rotation } _ => first_rotation, diff --git a/examples/transforms/align.rs b/examples/transforms/align.rs index b9e7f22f900db..3fab7d2a4b2d0 100644 --- a/examples/transforms/align.rs +++ b/examples/transforms/align.rs @@ -1,11 +1,11 @@ //! This example shows how to use the `Transform::align` API. -use bevy::input::mouse::{MouseButton, MouseButtonInput, MouseMotion}; -use bevy::prelude::*; use bevy::color::{ - palettes::basic::{RED, WHITE, GRAY}, + palettes::basic::{GRAY, RED, WHITE}, Color, }; +use bevy::input::mouse::{MouseButton, MouseButtonInput, MouseMotion}; +use bevy::prelude::*; use rand::random; use std::f32::consts::PI; From 56325a031a5e049ae8f7ef645a4e76dfef7babce Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 12 Mar 2024 12:14:24 -0400 Subject: [PATCH 06/10] More comments in example code --- examples/transforms/align.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples/transforms/align.rs b/examples/transforms/align.rs index 3fab7d2a4b2d0..f0ef75720b30f 100644 --- a/examples/transforms/align.rs +++ b/examples/transforms/align.rs @@ -18,11 +18,19 @@ fn main() { .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, } @@ -85,6 +93,7 @@ fn setup( }, )); + // Instructions for the example commands.spawn(( TextBundle::from_section( "The bright red axis is the primary alignment axis, and it will always be\n\ @@ -114,6 +123,7 @@ fn setup( // Update systems +// Draw the main and secondary axes on the rotating cube fn draw_cube_axes(mut gizmos: Gizmos, query: Query<&Transform, With>) { let cube_transform = query.single(); @@ -126,12 +136,14 @@ fn draw_cube_axes(mut gizmos: Gizmos, query: Query<&Transform, With>) { 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(); @@ -154,6 +166,7 @@ fn rotate_cube(mut cube: Query<(&mut Cube, &mut Transform)>) { } } +// 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>, @@ -190,6 +203,7 @@ fn handle_keypress( } } +// Handle user mouse input for panning the camera around fn handle_mouse( mut button_events: EventReader, mut motion_events: EventReader, @@ -238,6 +252,8 @@ fn build_direction(height: f32, theta: f32) -> Vec3 { 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) From 8b8f7fbe2bc445ffa0a7e2ad4b7c4ca04880c364 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Tue, 12 Mar 2024 12:20:24 -0400 Subject: [PATCH 07/10] Tweak to doc comments --- crates/bevy_transform/src/components/transform.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 034c63b1eff8a..5546e108e2993 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -406,8 +406,6 @@ impl Transform { /// 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`. /// - /// For a visual demonstration, see the 'align' example. - /// /// 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 From c25d7d4d9bea1f34e96c4db75a0e7c041cbfdf16 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 13 Mar 2024 12:21:14 -0400 Subject: [PATCH 08/10] Cleaned up old names from comments, added example code to Transform::align docs --- .../src/components/transform.rs | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 5546e108e2993..2ceded03cbfda 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -421,6 +421,24 @@ impl Transform { /// * 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` but ignores the secondary counterparts /// (i.e. is otherwise unspecified) + /// + /// Example + /// ``` + /// # use bevy_math::Vec3; + /// # use bevy_ecs::prelude::*; + /// # use bevy_transform::components::Transform; + /// # #[derive(Component)] + /// # struct HasFront { + /// # front_vector: Vec3, + /// # } + /// fn face_origin(mut query: Query<(&mut Transform, &HasFront)>) { + /// for (mut transform, front) in &mut query { + /// let origin_facing_vector = -transform.translation; + /// transform.align(front.front_vector, origin_facing_vector, Vec3::Y, Vec3::Y); + /// } + /// } + /// # bevy_ecs::system::assert_is_system(face_origin); + /// ``` #[inline] pub fn align( &mut self, @@ -435,12 +453,12 @@ impl Transform { let secondary_direction = secondary_direction.try_normalize().unwrap_or(Vec3::Y); // The solution quaternion will be constructed in two steps. - // First, we start with a rotation that takes `handle` to `direction`. + // First, we start with a rotation that takes `main_axis` to `main_direction`. let first_rotation = Quat::from_rotation_arc(main_axis, main_direction); - // Let's follow by rotating about the `direction` axis so that the image of `weak_handle` - // is taken to something that lies in the plane of `direction` and `weak_direction`. Since - // `direction` is fixed by this rotation, the first criterion is still satisfied. + // Let's follow by rotating about the `main_direction` axis so that the image of `secondary_axis` + // is taken to something that lies in the plane of `main_direction` and `secondary_direction`. Since + // `main_direction` is fixed by this rotation, the first criterion is still satisfied. let secondary_image = first_rotation * secondary_axis; let secondary_image_ortho = secondary_image .reject_from_normalized(main_direction) @@ -449,7 +467,7 @@ impl Transform { .reject_from_normalized(main_direction) .try_normalize(); - // If one of the two weak vectors was parallel to `direction`, then we just do the first part + // If one of the two weak vectors was parallel to `main_direction`, then we just do the first part self.rotation = match (secondary_image_ortho, secondary_direction_ortho) { (Some(secondary_img_ortho), Some(secondary_dir_ortho)) => { let second_rotation = From b098b6238cda7c5ad6ca2cd8194a2796ac7a7dfb Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 14 Mar 2024 08:50:34 -0400 Subject: [PATCH 09/10] Changed doc-test to demonstrate fallback behavior, clarified fallback behavior in comments, updated example description --- .../src/components/transform.rs | 28 ++++++++----------- examples/transforms/align.rs | 2 +- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 2ceded03cbfda..0ce6cc6a10c0a 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -416,28 +416,24 @@ impl Transform { /// 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 `direction` is zero, `Vec3::X` takes its place + /// * 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` but ignores the secondary counterparts - /// (i.e. is otherwise unspecified) + /// a rotation is constructed which takes `main_axis` to `main_direction` along a great circle, ignoring the secondary + /// counterparts /// /// Example /// ``` - /// # use bevy_math::Vec3; - /// # use bevy_ecs::prelude::*; + /// # use bevy_math::{Vec3, Quat}; /// # use bevy_transform::components::Transform; - /// # #[derive(Component)] - /// # struct HasFront { - /// # front_vector: Vec3, - /// # } - /// fn face_origin(mut query: Query<(&mut Transform, &HasFront)>) { - /// for (mut transform, front) in &mut query { - /// let origin_facing_vector = -transform.translation; - /// transform.align(front.front_vector, origin_facing_vector, Vec3::Y, Vec3::Y); - /// } - /// } - /// # bevy_ecs::system::assert_is_system(face_origin); + /// 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)); /// ``` #[inline] pub fn align( diff --git a/examples/transforms/align.rs b/examples/transforms/align.rs index f0ef75720b30f..1879868da0d6e 100644 --- a/examples/transforms/align.rs +++ b/examples/transforms/align.rs @@ -1,4 +1,4 @@ -//! This example shows how to use the `Transform::align` API. +//! 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}, From 733631cfab372f458e454b7a657c17af845ad281 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 14 Mar 2024 09:29:30 -0400 Subject: [PATCH 10/10] One more doctest example for the road (applying a transformation and comparing output) --- crates/bevy_transform/src/components/transform.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 0ce6cc6a10c0a..ae58c50d850fb 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -426,8 +426,14 @@ impl Transform { /// ``` /// # use bevy_math::{Vec3, Quat}; /// # use bevy_transform::components::Transform; - /// let mut t1 = Transform::IDENTITY; - /// let mut t2 = Transform::IDENTITY; + /// # let mut t1 = Transform::IDENTITY; + /// # let mut t2 = Transform::IDENTITY; + /// t1.align(Vec3::X, Vec3::Y, Vec3::new(1., 1., 0.), Vec3::Z); + /// let main_axis_image = t1.rotation * Vec3::X; + /// let secondary_axis_image = t1.rotation * Vec3::new(1., 1., 0.); + /// assert!(main_axis_image.abs_diff_eq(Vec3::Y, 1e-5)); + /// assert!(secondary_axis_image.abs_diff_eq(Vec3::new(0., 1., 1.), 1e-5)); + /// /// 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);