align/
align.rs

1//! This example shows how to align the orientations of objects in 3D space along two axes using the `Transform::align` API.
2
3use bevy::{
4    color::palettes::basic::{GRAY, RED, WHITE},
5    input::mouse::{AccumulatedMouseMotion, MouseButtonInput},
6    math::StableInterpolate,
7    prelude::*,
8};
9use rand::{Rng, SeedableRng};
10use rand_chacha::ChaCha8Rng;
11
12fn main() {
13    App::new()
14        .add_plugins(DefaultPlugins)
15        .add_systems(Startup, setup)
16        .add_systems(Update, (draw_ship_axes, draw_random_axes))
17        .add_systems(Update, (handle_keypress, handle_mouse, rotate_ship).chain())
18        .run();
19}
20
21/// This struct stores metadata for a single rotational move of the ship
22#[derive(Component, Default)]
23struct Ship {
24    /// The target transform of the ship move, the endpoint of interpolation
25    target_transform: Transform,
26
27    /// Whether the ship is currently in motion; allows motion to be paused
28    in_motion: bool,
29}
30
31#[derive(Component)]
32struct RandomAxes(Dir3, Dir3);
33
34#[derive(Component)]
35struct Instructions;
36
37#[derive(Resource)]
38struct MousePressed(bool);
39
40#[derive(Resource)]
41struct SeededRng(ChaCha8Rng);
42
43// Setup
44
45fn setup(
46    mut commands: Commands,
47    mut meshes: ResMut<Assets<Mesh>>,
48    mut materials: ResMut<Assets<StandardMaterial>>,
49    asset_server: Res<AssetServer>,
50) {
51    // We're seeding the PRNG here to make this example deterministic for testing purposes.
52    // This isn't strictly required in practical use unless you need your app to be deterministic.
53    let mut seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
54
55    // A camera looking at the origin
56    commands.spawn((
57        Camera3d::default(),
58        Transform::from_xyz(3., 2.5, 4.).looking_at(Vec3::ZERO, Vec3::Y),
59    ));
60
61    // A plane that we can sit on top of
62    commands.spawn((
63        Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))),
64        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
65        Transform::from_xyz(0., -2., 0.),
66    ));
67
68    // A light source
69    commands.spawn((
70        PointLight {
71            shadows_enabled: true,
72            ..default()
73        },
74        Transform::from_xyz(4.0, 7.0, -4.0),
75    ));
76
77    // Initialize random axes
78    let first = seeded_rng.random();
79    let second = seeded_rng.random();
80    commands.spawn(RandomAxes(first, second));
81
82    // Finally, our ship that is going to rotate
83    commands.spawn((
84        SceneRoot(
85            asset_server
86                .load(GltfAssetLabel::Scene(0).from_asset("models/ship/craft_speederD.gltf")),
87        ),
88        Ship {
89            target_transform: random_axes_target_alignment(&RandomAxes(first, second)),
90            ..default()
91        },
92    ));
93
94    // Instructions for the example
95    commands.spawn((
96        Text::new(
97            "The bright red axis is the primary alignment axis, and it will always be\n\
98            made to coincide with the primary target direction (white) exactly.\n\
99            The fainter red axis is the secondary alignment axis, and it is made to\n\
100            line up with the secondary target direction (gray) as closely as possible.\n\
101            Press 'R' to generate random target directions.\n\
102            Press 'T' to align the ship to those directions.\n\
103            Click and drag the mouse to rotate the camera.\n\
104            Press 'H' to hide/show these instructions.",
105        ),
106        Node {
107            position_type: PositionType::Absolute,
108            top: px(12),
109            left: px(12),
110            ..default()
111        },
112        Instructions,
113    ));
114
115    commands.insert_resource(MousePressed(false));
116    commands.insert_resource(SeededRng(seeded_rng));
117}
118
119// Update systems
120
121// Draw the main and secondary axes on the rotating ship
122fn draw_ship_axes(mut gizmos: Gizmos, ship_transform: Single<&Transform, With<Ship>>) {
123    // Local Z-axis arrow, negative direction
124    let z_ends = arrow_ends(*ship_transform, Vec3::NEG_Z, 1.5);
125    gizmos.arrow(z_ends.0, z_ends.1, RED);
126
127    // local X-axis arrow
128    let x_ends = arrow_ends(*ship_transform, Vec3::X, 1.5);
129    gizmos.arrow(x_ends.0, x_ends.1, Color::srgb(0.65, 0., 0.));
130}
131
132// Draw the randomly generated axes
133fn draw_random_axes(mut gizmos: Gizmos, random_axes: Single<&RandomAxes>) {
134    let RandomAxes(v1, v2) = *random_axes;
135    gizmos.arrow(Vec3::ZERO, 1.5 * *v1, WHITE);
136    gizmos.arrow(Vec3::ZERO, 1.5 * *v2, GRAY);
137}
138
139// Actually update the ship's transform according to its initial source and target
140fn rotate_ship(ship: Single<(&mut Ship, &mut Transform)>, time: Res<Time>) {
141    let (mut ship, mut ship_transform) = ship.into_inner();
142
143    if !ship.in_motion {
144        return;
145    }
146
147    let target_rotation = ship.target_transform.rotation;
148
149    ship_transform
150        .rotation
151        .smooth_nudge(&target_rotation, 3.0, time.delta_secs());
152
153    if ship_transform.rotation.angle_between(target_rotation) <= f32::EPSILON {
154        ship.in_motion = false;
155    }
156}
157
158// Handle user inputs from the keyboard for dynamically altering the scenario
159fn handle_keypress(
160    mut ship: Single<&mut Ship>,
161    mut random_axes: Single<&mut RandomAxes>,
162    mut instructions_viz: Single<&mut Visibility, With<Instructions>>,
163    keyboard: Res<ButtonInput<KeyCode>>,
164    mut seeded_rng: ResMut<SeededRng>,
165) {
166    if keyboard.just_pressed(KeyCode::KeyR) {
167        // Randomize the target axes
168        let first = seeded_rng.0.random();
169        let second = seeded_rng.0.random();
170        **random_axes = RandomAxes(first, second);
171
172        // Stop the ship and set it up to transform from its present orientation to the new one
173        ship.in_motion = false;
174        ship.target_transform = random_axes_target_alignment(&random_axes);
175    }
176
177    if keyboard.just_pressed(KeyCode::KeyT) {
178        ship.in_motion ^= true;
179    }
180
181    if keyboard.just_pressed(KeyCode::KeyH) {
182        if *instructions_viz.as_ref() == Visibility::Hidden {
183            **instructions_viz = Visibility::Visible;
184        } else {
185            **instructions_viz = Visibility::Hidden;
186        }
187    }
188}
189
190// Handle user mouse input for panning the camera around
191fn handle_mouse(
192    accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
193    mut mouse_button_inputs: MessageReader<MouseButtonInput>,
194    mut camera_transform: Single<&mut Transform, With<Camera>>,
195    mut mouse_pressed: ResMut<MousePressed>,
196) {
197    // Store left-pressed state in the MousePressed resource
198    for mouse_button_input in mouse_button_inputs.read() {
199        if mouse_button_input.button != MouseButton::Left {
200            continue;
201        }
202        *mouse_pressed = MousePressed(mouse_button_input.state.is_pressed());
203    }
204
205    // If the mouse is not pressed, just ignore motion events
206    if !mouse_pressed.0 {
207        return;
208    }
209    if accumulated_mouse_motion.delta != Vec2::ZERO {
210        let displacement = accumulated_mouse_motion.delta.x;
211        camera_transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(-displacement / 75.));
212    }
213}
214
215// Helper functions (i.e. non-system functions)
216
217fn arrow_ends(transform: &Transform, axis: Vec3, length: f32) -> (Vec3, Vec3) {
218    let local_vector = length * (transform.rotation * axis);
219    (transform.translation, transform.translation + local_vector)
220}
221
222// This is where `Transform::align` is actually used!
223// Note that the choice of `Vec3::X` and `Vec3::Y` here matches the use of those in `draw_ship_axes`.
224fn random_axes_target_alignment(random_axes: &RandomAxes) -> Transform {
225    let RandomAxes(first, second) = random_axes;
226    Transform::IDENTITY.aligned_by(Vec3::NEG_Z, *first, Vec3::X, *second)
227}