docs/bevy/examples/transforms/align.rs 7.4 K · 227 lines · raw · history

1 //! This example shows how to align the orientations of objects in 3D space along two axes using the `Transform::align` API.
2
3 use bevy::{
4 color::palettes::basic::{GRAY, RED, WHITE},
5 input::mouse::{AccumulatedMouseMotion, MouseButtonInput},
6 math::StableInterpolate,
7 prelude::*,
8 };
9 use chacha20::ChaCha8Rng;
10 use rand::{RngExt, SeedableRng};
11
12 fn 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)]
23 struct 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)]
32 struct RandomAxes(Dir3, Dir3);
33
34 #[derive(Component)]
35 struct Instructions;
36
37 #[derive(Resource)]
38 struct MousePressed(bool);
39
40 #[derive(Resource)]
41 struct SeededRng(ChaCha8Rng);
42
43 // Setup
44
45 fn 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 shadow_maps_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 WorldAssetRoot(
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
122 fn 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
133 fn 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
140 fn 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
159 fn 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
191 fn 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
217 fn 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`.
224 fn 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 }