docs/bevy/examples/3d/mirror.rs 22.7 K · 643 lines · raw · history

1 //! Demonstrates how to create a mirror with a second camera.
2
3 use std::f32::consts::FRAC_PI_2;
4
5 use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
6 use bevy::camera::RenderTarget;
7 use bevy::{
8 color::palettes::css::GREEN,
9 feathers::{
10 controls::FeathersRadio, dark_theme::create_dark_theme, theme::UiTheme, FeathersPlugins,
11 },
12 input::mouse::AccumulatedMouseMotion,
13 math::{reflection_matrix, uvec2, vec3},
14 pbr::{ExtendedMaterial, MaterialExtension},
15 picking::hover::Hovered,
16 prelude::*,
17 render::render_resource::{AsBindGroup, TextureFormat},
18 shader::ShaderRef,
19 ui_widgets::{radio_self_update, ValueChange},
20 window::{PrimaryWindow, WindowResized},
21 };
22
23 #[path = "../helpers/radio.rs"]
24 mod radio;
25
26 /// A resource that stores a handle to the image that contains the rendered
27 /// mirror world.
28 #[derive(Resource)]
29 struct MirrorImage(Handle<Image>);
30
31 /// A marker component for the camera that renders the mirror world.
32 #[derive(Component)]
33 struct MirrorCamera;
34
35 /// A marker component for the mirror mesh itself.
36 #[derive(Component)]
37 struct Mirror;
38
39 /// The dummy material extension that we use for the mirror surface.
40 ///
41 /// This shader samples its emissive texture at the screen space position of
42 /// each fragment rather than at the UVs. Effectively, this uses a PBR shader as
43 /// a mask that copies a portion of the emissive texture to the screen, all in
44 /// screen space.
45 ///
46 /// We use [`ExtendedMaterial`], as that's the easiest way to implement custom
47 /// shaders that modify the built-in [`StandardMaterial`]. We don't require any
48 /// extra data to be passed to the shader beyond the [`StandardMaterial`] PBR
49 /// fields, but currently Bevy requires at least one field to be present in the
50 /// extended material, so we simply have an unused field.
51 #[derive(Clone, AsBindGroup, Asset, Reflect)]
52 struct ScreenSpaceTextureExtension {
53 /// An unused value that we have just to satisfy [`ExtendedMaterial`]
54 /// requirements.
55 #[uniform(100)]
56 dummy: f32,
57 }
58
59 impl MaterialExtension for ScreenSpaceTextureExtension {
60 fn fragment_shader() -> ShaderRef {
61 "shaders/screen_space_texture_material.wesl".into()
62 }
63 }
64
65 /// The action that will be performed when the user drags the mouse: either
66 /// moving the camera or moving the rigged model.
67 #[derive(Clone, Copy, PartialEq, Default)]
68 enum DragAction {
69 /// Dragging will move the camera.
70 #[default]
71 MoveCamera,
72 /// Dragging will move the animated fox.
73 MoveFox,
74 }
75
76 /// The settings that the user has currently chosen.
77 ///
78 /// Currently, this just consists of the [`DragAction`].
79 #[derive(Resource, Default)]
80 struct AppStatus {
81 /// The action that will be performed when the user drags the mouse: either
82 /// moving the camera or moving the rigged model.
83 drag_action: DragAction,
84 }
85
86 /// A marker component for the help text at the top of the screen.
87 #[derive(Clone, Copy, Component)]
88 struct HelpText;
89
90 /// The coordinates that the camera looks at.
91 const CAMERA_TARGET: Vec3 = vec3(-25.0, 20.0, 0.0);
92 /// The camera stays this distance in meters from the camera target.
93 const CAMERA_ORBIT_DISTANCE: f32 = 500.0;
94 /// The speed at which the user can move the camera vertically, in radians per
95 /// mouse input unit.
96 const CAMERA_PITCH_SPEED: f32 = 0.003;
97 /// The speed at which the user can move the camera horizontally, in radians per
98 /// mouse input unit.
99 const CAMERA_YAW_SPEED: f32 = 0.004;
100 // Limiting pitch stops some unexpected rotation past 90° up or down.
101 const CAMERA_PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;
102
103 /// The angle that the mirror faces.
104 ///
105 /// The mirror is rotated across the X axis in this many radians.
106 const MIRROR_ROTATION_ANGLE: f32 = -FRAC_PI_2;
107 const MIRROR_POSITION: Vec3 = vec3(-25.0, 75.0, 0.0);
108
109 /// The path to the animated fox model.
110 static FOX_ASSET_PATH: &str = "models/animated/Fox.glb";
111
112 /// The app entry point.
113 fn main() {
114 App::new()
115 .add_plugins(DefaultPlugins.set(WindowPlugin {
116 primary_window: Some(Window {
117 title: "Bevy Mirror Example".into(),
118 ..default()
119 }),
120 ..default()
121 }))
122 .add_plugins(MaterialPlugin::<
123 ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>,
124 >::default())
125 .add_plugins(FeathersPlugins)
126 .insert_resource(UiTheme(create_dark_theme()))
127 .init_resource::<AppStatus>()
128 .add_systems(Startup, setup)
129 .add_systems(Update, handle_window_resize_messages)
130 .add_systems(Update, (move_camera_on_mouse_down, move_fox_on_mouse_down))
131 .add_observer(handle_drag_action_change)
132 .add_observer(radio_self_update)
133 .add_systems(
134 Update,
135 update_mirror_camera_on_main_camera_transform_change.after(move_camera_on_mouse_down),
136 )
137 .add_systems(Update, play_fox_animation)
138 .add_systems(Update, update_help_text)
139 .run();
140 }
141
142 /// A startup system that spawns the scene and sets up the mirror render target.
143 fn setup(
144 mut commands: Commands,
145 windows_query: Query<&Window>,
146 asset_server: Res<AssetServer>,
147 mut meshes: ResMut<Assets<Mesh>>,
148 mut standard_materials: ResMut<Assets<StandardMaterial>>,
149 mut screen_space_texture_materials: ResMut<
150 Assets<ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>>,
151 >,
152 mut images: ResMut<Assets<Image>>,
153 app_status: Res<AppStatus>,
154 ) {
155 // Spawn the main camera.
156 let camera_projection = PerspectiveProjection::default();
157 let camera_transform = spawn_main_camera(&mut commands, &camera_projection);
158
159 // Spawn the light.
160 spawn_light(&mut commands);
161
162 // Spawn the objects reflected in the mirror.
163 spawn_ground_plane(&mut commands, &mut meshes, &mut standard_materials);
164 spawn_fox(&mut commands, &asset_server);
165
166 // Spawn the mirror and associated camera.
167 let mirror_render_target_image =
168 create_mirror_texture_resource(&mut commands, &windows_query, &mut images);
169 let mirror_transform = spawn_mirror(
170 &mut commands,
171 &mut meshes,
172 &mut screen_space_texture_materials,
173 mirror_render_target_image.clone(),
174 );
175 spawn_mirror_camera(
176 &mut commands,
177 &camera_transform,
178 &camera_projection,
179 &mirror_transform,
180 mirror_render_target_image,
181 );
182
183 // Spawn the UI.
184 spawn_buttons(&mut commands);
185 spawn_help_text(&mut commands, &app_status);
186 }
187
188 /// Spawns the main camera (not the mirror camera).
189 fn spawn_main_camera(
190 commands: &mut Commands,
191 camera_projection: &PerspectiveProjection,
192 ) -> Transform {
193 let camera_transform = Transform::from_translation(
194 vec3(-2.0, 1.0, -2.0).normalize_or_zero() * CAMERA_ORBIT_DISTANCE,
195 )
196 .looking_at(CAMERA_TARGET, Vec3::Y);
197
198 commands.spawn((
199 Camera3d::default(),
200 camera_transform,
201 Projection::Perspective(camera_projection.clone()),
202 ));
203
204 camera_transform
205 }
206
207 /// Spawns a directional light to illuminate the scene.
208 fn spawn_light(commands: &mut Commands) {
209 commands.spawn((
210 DirectionalLight {
211 illuminance: 5000.0,
212 ..default()
213 },
214 Transform::from_xyz(-85.0, 16.0, -200.0).looking_at(vec3(-50.0, 0.0, 100.0), Vec3::Y),
215 ));
216 }
217
218 /// Spawns the circular ground plane object.
219 fn spawn_ground_plane(
220 commands: &mut Commands,
221 meshes: &mut Assets<Mesh>,
222 standard_materials: &mut Assets<StandardMaterial>,
223 ) {
224 commands.spawn((
225 Mesh3d(meshes.add(Circle::new(200.0))),
226 MeshMaterial3d(standard_materials.add(Color::from(GREEN))),
227 Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2))
228 .with_translation(vec3(-25.0, 0.0, 0.0)),
229 ));
230 }
231
232 /// Creates the initial image that the mirror camera will render the mirror
233 /// world to.
234 fn create_mirror_texture_resource(
235 commands: &mut Commands,
236 windows_query: &Query<&Window>,
237 images: &mut Assets<Image>,
238 ) -> Handle<Image> {
239 let window = windows_query.iter().next().expect("No window found");
240 let window_size = uvec2(window.physical_width(), window.physical_height());
241 let image = create_mirror_texture_image(images, window_size);
242 commands.insert_resource(MirrorImage(image.clone()));
243 image
244 }
245
246 /// Spawns the camera that renders the mirror world.
247 fn spawn_mirror_camera(
248 commands: &mut Commands,
249 camera_transform: &Transform,
250 camera_projection: &PerspectiveProjection,
251 mirror_transform: &Transform,
252 mirror_render_target: Handle<Image>,
253 ) {
254 let (mirror_camera_transform, mirror_camera_projection) =
255 calculate_mirror_camera_transform_and_projection(
256 camera_transform,
257 camera_projection,
258 mirror_transform,
259 );
260
261 commands.spawn((
262 Camera3d::default(),
263 Camera {
264 order: -1,
265 // Reflecting the model across the mirror will flip the winding of
266 // all the polygons. Therefore, in order to properly backface cull,
267 // we need to turn on `invert_culling`.
268 invert_culling: true,
269 ..default()
270 },
271 RenderTarget::Image(mirror_render_target.clone().into()),
272 mirror_camera_transform,
273 Projection::Perspective(mirror_camera_projection),
274 MirrorCamera,
275 ));
276 }
277
278 /// Spawns the animated fox.
279 ///
280 /// Note that this doesn't play the animation; that's handled in
281 /// [`play_fox_animation`].
282 fn spawn_fox(commands: &mut Commands, asset_server: &AssetServer) {
283 commands.spawn((
284 WorldAssetRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_ASSET_PATH))),
285 Transform::from_xyz(-50.0, 0.0, -100.0),
286 ));
287 }
288
289 /// Spawns the mirror plane mesh and returns its transform.
290 fn spawn_mirror(
291 commands: &mut Commands,
292 meshes: &mut Assets<Mesh>,
293 screen_space_texture_materials: &mut Assets<
294 ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>,
295 >,
296 mirror_render_target: Handle<Image>,
297 ) -> Transform {
298 let mirror_transform = Transform::from_scale(vec3(300.0, 1.0, 150.0))
299 .with_rotation(Quat::from_rotation_x(MIRROR_ROTATION_ANGLE))
300 .with_translation(MIRROR_POSITION);
301
302 commands.spawn((
303 Mesh3d(meshes.add(Plane3d::default().mesh().size(1.0, 1.0))),
304 MeshMaterial3d(screen_space_texture_materials.add(ExtendedMaterial {
305 base: StandardMaterial {
306 base_color: Color::BLACK,
307 emissive: Color::WHITE.into(),
308 emissive_texture: Some(mirror_render_target),
309 perceptual_roughness: 0.0,
310 metallic: 1.0,
311 ..default()
312 },
313 extension: ScreenSpaceTextureExtension { dummy: 0.0 },
314 })),
315 mirror_transform,
316 Mirror,
317 ));
318
319 mirror_transform
320 }
321
322 /// Spawns the buttons at the bottom of the screen.
323 fn spawn_buttons(commands: &mut Commands) {
324 // Spawn the radio buttons that allow the user to select an object to
325 // control.
326 commands.spawn_scene(bsn! {
327 @main_ui_node_scene()
328 Children [@feathers_option_buttons(
329 "Drag Action",
330 &[
331 (DragAction::MoveCamera, "Move Camera"),
332 (DragAction::MoveFox, "Move Fox"),
333 ],
334 0,
335 )]
336 });
337 }
338
339 /// Given the transform and projection of the main camera, returns an
340 /// appropriate transform and projection for the mirror camera.
341 fn calculate_mirror_camera_transform_and_projection(
342 main_camera_transform: &Transform,
343 main_camera_projection: &PerspectiveProjection,
344 mirror_transform: &Transform,
345 ) -> (Transform, PerspectiveProjection) {
346 // Calculate the reflection matrix (a.k.a. Householder matrix) that will
347 // reflect the scene across the mirror plane.
348 //
349 // Note that you must calculate this in *matrix* form and only *afterward*
350 // convert to a `Transform` instead of composing `Transform`s. This is
351 // because the reflection matrix has non-uniform scale, and composing
352 // transforms can't always handle composition of matrices with non-uniform
353 // scales.
354 let mirror_camera_transform = Transform::from_matrix(
355 Mat4::from_mat3a(reflection_matrix(Vec3::NEG_Z)) * main_camera_transform.to_matrix(),
356 );
357
358 // Compute the distance from the camera to the mirror plane. This will be
359 // used to calculate the distance to the near clip plane for the mirror
360 // world.
361 let distance_from_camera_to_mirror = InfinitePlane3d::new(mirror_transform.rotation * Vec3::Y)
362 .signed_distance(
363 Isometry3d::IDENTITY,
364 mirror_transform.translation - main_camera_transform.translation,
365 );
366
367 // Compute the normal of the mirror plane in view space.
368 let view_from_world = main_camera_transform.compute_affine().matrix3.inverse();
369 let mirror_projection_plane_normal =
370 (view_from_world * (mirror_transform.rotation * Vec3::NEG_Y)).normalize();
371
372 // Compute the final projection. It should match the main camera projection,
373 // except that `near` and `near_normal` should be set to the updated near
374 // plane and near normal plane as above.
375 let mirror_camera_projection = PerspectiveProjection {
376 near_clip_plane: mirror_projection_plane_normal.extend(distance_from_camera_to_mirror),
377 ..*main_camera_projection
378 };
379
380 (mirror_camera_transform, mirror_camera_projection)
381 }
382
383 /// A system that resizes the render target image when the user resizes the window.
384 ///
385 /// Since the image that stores the rendered mirror world has the same physical
386 /// size as the window, we need to reallocate it and reattach it to the mirror
387 /// material whenever the window size changes.
388 fn handle_window_resize_messages(
389 windows_query: Query<&Window>,
390 mut mirror_cameras_query: Query<&mut RenderTarget, With<MirrorCamera>>,
391 mut images: ResMut<Assets<Image>>,
392 mut mirror_image: ResMut<MirrorImage>,
393 mut screen_space_texture_materials: ResMut<
394 Assets<ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>>,
395 >,
396 mut resize_messages: MessageReader<WindowResized>,
397 ) {
398 // We run at most once, regardless of the number of window resize messages
399 // there were this frame.
400 let Some(resize_message) = resize_messages.read().next() else {
401 return;
402 };
403 let Ok(window) = windows_query.get(resize_message.window) else {
404 return;
405 };
406
407 let window_size = uvec2(window.physical_width(), window.physical_height());
408 let image = create_mirror_texture_image(&mut images, window_size);
409 images.remove(mirror_image.0.id());
410
411 mirror_image.0 = image.clone();
412
413 for mut target in mirror_cameras_query.iter_mut() {
414 *target = image.clone().into();
415 }
416
417 for (_, material) in screen_space_texture_materials.iter_mut() {
418 material.base.emissive_texture = Some(image.clone());
419 }
420 }
421
422 /// Creates the image that will be used to store the reflected scene.
423 fn create_mirror_texture_image(images: &mut Assets<Image>, window_size: UVec2) -> Handle<Image> {
424 images.add(Image::new_target_texture(
425 window_size.x,
426 window_size.y,
427 TextureFormat::Bgra8UnormSrgb,
428 None,
429 ))
430 }
431
432 // Moves the fox when the user moves the mouse with the left button down.
433 fn move_fox_on_mouse_down(
434 mut scene_roots_query: Query<&mut Transform, With<WorldAssetRoot>>,
435 windows_query: Query<&Window, With<PrimaryWindow>>,
436 cameras_query: Query<(&Camera, &GlobalTransform)>,
437 radio_hovered_query: Query<&Hovered, With<FeathersRadio>>,
438 buttons: Res<ButtonInput<MouseButton>>,
439 app_status: Res<AppStatus>,
440 ) {
441 // Only process the mouse motion if the left mouse button is pressed, the
442 // mouse action is set to move the fox, and the pointer isn't over a UI
443 // widget.
444 if app_status.drag_action != DragAction::MoveFox
445 || !buttons.pressed(MouseButton::Left)
446 || radio_hovered_query.iter().any(Hovered::get)
447 {
448 return;
449 }
450
451 // Find out where the user clicked the mouse.
452 let Some(mouse_position) = windows_query
453 .iter()
454 .next()
455 .and_then(Window::cursor_position)
456 else {
457 return;
458 };
459
460 // Grab the camera.
461 let Some((camera, camera_transform)) = cameras_query.iter().next() else {
462 return;
463 };
464
465 // Figure out where the user clicked on the plane.
466 let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else {
467 return;
468 };
469 let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) else {
470 return;
471 };
472 let plane_intersection = ray.origin + ray.direction.normalize() * ray_distance;
473
474 // Move the fox.
475 for mut transform in scene_roots_query.iter_mut() {
476 transform.translation = transform.translation.with_xz(plane_intersection.xz());
477 }
478 }
479
480 /// An observer that changes the drag action when the user clicks on one of the
481 /// radio buttons.
482 fn handle_drag_action_change(
483 event: On<ValueChange<Entity>>,
484 new_value_query: Query<&RadioButtonOptionValue<DragAction>>,
485 mut app_status: ResMut<AppStatus>,
486 ) {
487 let Ok(RadioButtonOptionValue(drag_action)) = new_value_query.get(event.value) else {
488 return;
489 };
490
491 app_status.drag_action = *drag_action;
492 }
493
494 /// A system that processes user mouse actions that move the camera.
495 ///
496 /// This is mostly copied from `examples/camera/camera_orbit.rs`.
497 fn move_camera_on_mouse_down(
498 mut main_cameras_query: Query<&mut Transform, (With<Camera>, Without<MirrorCamera>)>,
499 radio_hovered_query: Query<&Hovered, With<FeathersRadio>>,
500 mouse_buttons: Res<ButtonInput<MouseButton>>,
501 mouse_motion: Res<AccumulatedMouseMotion>,
502 app_status: Res<AppStatus>,
503 ) {
504 // Only process the mouse motion if the left mouse button is pressed, the
505 // mouse action is set to move the fox, and the pointer isn't over a UI
506 // widget.
507 if app_status.drag_action != DragAction::MoveCamera
508 || !mouse_buttons.pressed(MouseButton::Left)
509 || radio_hovered_query.iter().any(Hovered::get)
510 {
511 return;
512 }
513
514 let delta = mouse_motion.delta;
515
516 // Mouse motion is one of the few inputs that should not be multiplied by delta time,
517 // as we are already receiving the full movement since the last frame was rendered. Multiplying
518 // by delta time here would make the movement slower that it should be.
519 let delta_pitch = delta.y * CAMERA_PITCH_SPEED;
520 let delta_yaw = delta.x * CAMERA_YAW_SPEED;
521
522 for mut main_camera_transform in &mut main_cameras_query {
523 // Obtain the existing pitch and yaw values from the transform.
524 let (yaw, pitch, _) = main_camera_transform.rotation.to_euler(EulerRot::YXZ);
525
526 // Establish the new yaw and pitch, preventing the pitch value from exceeding our limits.
527 let pitch = (pitch + delta_pitch).clamp(-CAMERA_PITCH_LIMIT, CAMERA_PITCH_LIMIT);
528 let yaw = yaw + delta_yaw;
529 main_camera_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.0);
530
531 // Adjust the translation to maintain the correct orientation toward the orbit target.
532 // In our example it's a static target, but this could easily be customized.
533 let target = Vec3::ZERO;
534 main_camera_transform.translation =
535 target - main_camera_transform.forward() * CAMERA_ORBIT_DISTANCE;
536 }
537 }
538
539 /// Updates the position, rotation, and projection of the mirror camera when the
540 /// main camera is moved.
541 ///
542 /// When the main camera is moved, the mirror camera must be moved to match it.
543 /// The *projection* on the mirror camera must also be altered, because the
544 /// projection takes the view-space rotation of and distance to the mirror into
545 /// account.
546 fn update_mirror_camera_on_main_camera_transform_change(
547 main_cameras_query: Query<
548 (&Transform, &Projection),
549 (Changed<Transform>, With<Camera>, Without<MirrorCamera>),
550 >,
551 mut mirror_cameras_query: Query<
552 (&mut Transform, &mut Projection),
553 (With<Camera>, With<MirrorCamera>, Without<Mirror>),
554 >,
555 mirrors_query: Query<&Transform, (Without<MirrorCamera>, With<Mirror>)>,
556 ) {
557 let Some((main_camera_transform, Projection::Perspective(main_camera_projection))) =
558 main_cameras_query.iter().next()
559 else {
560 return;
561 };
562
563 let Some(mirror_transform) = mirrors_query.iter().next() else {
564 return;
565 };
566
567 // Here we need the transforms of both the camera and the mirror in order to
568 // properly calculate the new projection.
569 let (new_mirror_camera_transform, new_mirror_camera_projection) =
570 calculate_mirror_camera_transform_and_projection(
571 main_camera_transform,
572 main_camera_projection,
573 mirror_transform,
574 );
575
576 for (mut mirror_camera_transform, mut mirror_camera_projection) in &mut mirror_cameras_query {
577 *mirror_camera_transform = new_mirror_camera_transform;
578 *mirror_camera_projection = Projection::Perspective(new_mirror_camera_projection.clone());
579 }
580 }
581
582 /// Plays the initial animation on the fox model.
583 fn play_fox_animation(
584 mut commands: Commands,
585 mut animation_players_query: Query<
586 (Entity, &mut AnimationPlayer),
587 Without<AnimationGraphHandle>,
588 >,
589 asset_server: Res<AssetServer>,
590 mut animation_graphs: ResMut<Assets<AnimationGraph>>,
591 ) {
592 // Only pick up animation players that don't already have an animation graph
593 // handle.
594 // This ensures that we only start playing the animation once.
595 if animation_players_query.is_empty() {
596 return;
597 }
598
599 let fox_animation = asset_server.load(GltfAssetLabel::Animation(0).from_asset(FOX_ASSET_PATH));
600 let (fox_animation_graph, fox_animation_node) =
601 AnimationGraph::from_clip(fox_animation.clone());
602 let fox_animation_graph = animation_graphs.add(fox_animation_graph);
603
604 for (entity, mut animation_player) in animation_players_query.iter_mut() {
605 commands
606 .entity(entity)
607 .insert(AnimationGraphHandle(fox_animation_graph.clone()));
608 animation_player.play(fox_animation_node).repeat();
609 }
610 }
611
612 /// Spawns the help text at the top of the screen.
613 fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
614 commands.spawn((
615 Text::new(create_help_string(app_status)),
616 Node {
617 position_type: PositionType::Absolute,
618 top: px(12),
619 left: px(12),
620 ..default()
621 },
622 HelpText,
623 ));
624 }
625
626 /// Creates the help string at the top left of the screen.
627 fn create_help_string(app_status: &AppStatus) -> String {
628 format!(
629 "Click and drag to move the {}",
630 match app_status.drag_action {
631 DragAction::MoveCamera => "camera",
632 DragAction::MoveFox => "fox",
633 }
634 )
635 }
636
637 /// Updates the help text in the top left of the screen to reflect the current
638 /// drag mode.
639 fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
640 for mut text in &mut help_text {
641 text.0 = create_help_string(&app_status);
642 }
643 }