1use std::f32::consts::FRAC_PI_2;
4
5use crate::widgets::{RadioButton, WidgetClickEvent, WidgetClickSender};
6use bevy::camera::RenderTarget;
7use bevy::{
8 asset::RenderAssetUsages,
9 color::palettes::css::GREEN,
10 input::mouse::AccumulatedMouseMotion,
11 math::{reflection_matrix, uvec2, vec3},
12 pbr::{ExtendedMaterial, MaterialExtension},
13 prelude::*,
14 render::render_resource::{
15 AsBindGroup, Extent3d, TextureDimension, TextureFormat, TextureUsages,
16 },
17 shader::ShaderRef,
18 window::{PrimaryWindow, WindowResized},
19};
20
21#[path = "../helpers/widgets.rs"]
22mod widgets;
23
24#[derive(Resource)]
27struct MirrorImage(Handle<Image>);
28
29#[derive(Component)]
31struct MirrorCamera;
32
33#[derive(Component)]
35struct Mirror;
36
37#[derive(Clone, AsBindGroup, Asset, Reflect)]
50struct ScreenSpaceTextureExtension {
51 #[uniform(100)]
54 dummy: f32,
55}
56
57impl MaterialExtension for ScreenSpaceTextureExtension {
58 fn fragment_shader() -> ShaderRef {
59 "shaders/screen_space_texture_material.wgsl".into()
60 }
61}
62
63#[derive(Clone, Copy, PartialEq, Default)]
66enum DragAction {
67 #[default]
69 MoveCamera,
70 MoveFox,
72}
73
74#[derive(Resource, Default)]
78struct AppStatus {
79 drag_action: DragAction,
82}
83
84#[derive(Clone, Copy, Component)]
86struct HelpText;
87
88const CAMERA_TARGET: Vec3 = vec3(-25.0, 20.0, 0.0);
90const CAMERA_ORBIT_DISTANCE: f32 = 500.0;
92const CAMERA_PITCH_SPEED: f32 = 0.003;
95const CAMERA_YAW_SPEED: f32 = 0.004;
98const CAMERA_PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;
100
101const MIRROR_ROTATION_ANGLE: f32 = -FRAC_PI_2;
105const MIRROR_POSITION: Vec3 = vec3(-25.0, 75.0, 0.0);
106
107static FOX_ASSET_PATH: &str = "models/animated/Fox.glb";
109
110fn main() {
112 App::new()
113 .add_plugins(DefaultPlugins.set(WindowPlugin {
114 primary_window: Some(Window {
115 title: "Bevy Mirror Example".into(),
116 ..default()
117 }),
118 ..default()
119 }))
120 .add_plugins(MaterialPlugin::<
121 ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>,
122 >::default())
123 .init_resource::<AppStatus>()
124 .add_message::<WidgetClickEvent<DragAction>>()
125 .add_systems(Startup, setup)
126 .add_systems(Update, handle_window_resize_messages)
127 .add_systems(Update, (move_camera_on_mouse_down, move_fox_on_mouse_down))
128 .add_systems(Update, widgets::handle_ui_interactions::<DragAction>)
129 .add_systems(
130 Update,
131 (handle_mouse_action_change, update_radio_buttons)
132 .after(widgets::handle_ui_interactions::<DragAction>),
133 )
134 .add_systems(
135 Update,
136 update_mirror_camera_on_main_camera_transform_change.after(move_camera_on_mouse_down),
137 )
138 .add_systems(Update, play_fox_animation)
139 .add_systems(Update, update_help_text)
140 .run();
141}
142
143fn setup(
145 mut commands: Commands,
146 windows_query: Query<&Window>,
147 asset_server: Res<AssetServer>,
148 mut meshes: ResMut<Assets<Mesh>>,
149 mut standard_materials: ResMut<Assets<StandardMaterial>>,
150 mut screen_space_texture_materials: ResMut<
151 Assets<ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>>,
152 >,
153 mut images: ResMut<Assets<Image>>,
154 app_status: Res<AppStatus>,
155) {
156 let camera_projection = PerspectiveProjection::default();
158 let camera_transform = spawn_main_camera(&mut commands, &camera_projection);
159
160 spawn_light(&mut commands);
162
163 spawn_ground_plane(&mut commands, &mut meshes, &mut standard_materials);
165 spawn_fox(&mut commands, &asset_server);
166
167 let mirror_render_target_image =
169 create_mirror_texture_resource(&mut commands, &windows_query, &mut images);
170 let mirror_transform = spawn_mirror(
171 &mut commands,
172 &mut meshes,
173 &mut screen_space_texture_materials,
174 mirror_render_target_image.clone(),
175 );
176 spawn_mirror_camera(
177 &mut commands,
178 &camera_transform,
179 &camera_projection,
180 &mirror_transform,
181 mirror_render_target_image,
182 );
183
184 spawn_buttons(&mut commands);
186 spawn_help_text(&mut commands, &app_status);
187}
188
189fn spawn_main_camera(
191 commands: &mut Commands,
192 camera_projection: &PerspectiveProjection,
193) -> Transform {
194 let camera_transform = Transform::from_translation(
195 vec3(-2.0, 1.0, -2.0).normalize_or_zero() * CAMERA_ORBIT_DISTANCE,
196 )
197 .looking_at(CAMERA_TARGET, Vec3::Y);
198
199 commands.spawn((
200 Camera3d::default(),
201 camera_transform,
202 Projection::Perspective(camera_projection.clone()),
203 ));
204
205 camera_transform
206}
207
208fn spawn_light(commands: &mut Commands) {
210 commands.spawn((
211 DirectionalLight {
212 illuminance: 5000.0,
213 ..default()
214 },
215 Transform::from_xyz(-85.0, 16.0, -200.0).looking_at(vec3(-50.0, 0.0, 100.0), Vec3::Y),
216 ));
217}
218
219fn spawn_ground_plane(
221 commands: &mut Commands,
222 meshes: &mut Assets<Mesh>,
223 standard_materials: &mut Assets<StandardMaterial>,
224) {
225 commands.spawn((
226 Mesh3d(meshes.add(Circle::new(200.0))),
227 MeshMaterial3d(standard_materials.add(Color::from(GREEN))),
228 Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2))
229 .with_translation(vec3(-25.0, 0.0, 0.0)),
230 ));
231}
232
233fn create_mirror_texture_resource(
236 commands: &mut Commands,
237 windows_query: &Query<&Window>,
238 images: &mut Assets<Image>,
239) -> Handle<Image> {
240 let window = windows_query.iter().next().expect("No window found");
241 let window_size = uvec2(window.physical_width(), window.physical_height());
242 let image = create_mirror_texture_image(images, window_size);
243 commands.insert_resource(MirrorImage(image.clone()));
244 image
245}
246
247fn spawn_mirror_camera(
249 commands: &mut Commands,
250 camera_transform: &Transform,
251 camera_projection: &PerspectiveProjection,
252 mirror_transform: &Transform,
253 mirror_render_target: Handle<Image>,
254) {
255 let (mirror_camera_transform, mirror_camera_projection) =
256 calculate_mirror_camera_transform_and_projection(
257 camera_transform,
258 camera_projection,
259 mirror_transform,
260 );
261
262 commands.spawn((
263 Camera3d::default(),
264 Camera {
265 order: -1,
266 invert_culling: true,
270 ..default()
271 },
272 RenderTarget::Image(mirror_render_target.clone().into()),
273 mirror_camera_transform,
274 Projection::Perspective(mirror_camera_projection),
275 MirrorCamera,
276 ));
277}
278
279fn spawn_fox(commands: &mut Commands, asset_server: &AssetServer) {
284 commands.spawn((
285 WorldAssetRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_ASSET_PATH))),
286 Transform::from_xyz(-50.0, 0.0, -100.0),
287 ));
288}
289
290fn spawn_mirror(
292 commands: &mut Commands,
293 meshes: &mut Assets<Mesh>,
294 screen_space_texture_materials: &mut Assets<
295 ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>,
296 >,
297 mirror_render_target: Handle<Image>,
298) -> Transform {
299 let mirror_transform = Transform::from_scale(vec3(300.0, 1.0, 150.0))
300 .with_rotation(Quat::from_rotation_x(MIRROR_ROTATION_ANGLE))
301 .with_translation(MIRROR_POSITION);
302
303 commands.spawn((
304 Mesh3d(meshes.add(Plane3d::default().mesh().size(1.0, 1.0))),
305 MeshMaterial3d(screen_space_texture_materials.add(ExtendedMaterial {
306 base: StandardMaterial {
307 base_color: Color::BLACK,
308 emissive: Color::WHITE.into(),
309 emissive_texture: Some(mirror_render_target),
310 perceptual_roughness: 0.0,
311 metallic: 1.0,
312 ..default()
313 },
314 extension: ScreenSpaceTextureExtension { dummy: 0.0 },
315 })),
316 mirror_transform,
317 Mirror,
318 ));
319
320 mirror_transform
321}
322
323fn spawn_buttons(commands: &mut Commands) {
325 commands.spawn((
328 widgets::main_ui_node(),
329 children![widgets::option_buttons(
330 "Drag Action",
331 &[
332 (DragAction::MoveCamera, "Move Camera"),
333 (DragAction::MoveFox, "Move Fox"),
334 ],
335 )],
336 ));
337}
338
339fn calculate_mirror_camera_transform_and_projection(
342 main_camera_transform: &Transform,
343 main_camera_projection: &PerspectiveProjection,
344 mirror_transform: &Transform,
345) -> (Transform, PerspectiveProjection) {
346 let mirror_camera_transform = Transform::from_matrix(
355 Mat4::from_mat3a(reflection_matrix(Vec3::NEG_Z)) * main_camera_transform.to_matrix(),
356 );
357
358 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 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 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
383fn 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 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
422fn create_mirror_texture_image(images: &mut Assets<Image>, window_size: UVec2) -> Handle<Image> {
424 let mirror_image_extent = Extent3d {
425 width: window_size.x,
426 height: window_size.y,
427 depth_or_array_layers: 1,
428 };
429
430 let mut image = Image::new_uninit(
431 mirror_image_extent,
432 TextureDimension::D2,
433 TextureFormat::Bgra8UnormSrgb,
434 RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
435 );
436 image.texture_descriptor.usage |=
437 TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT;
438
439 images.add(image)
440}
441
442fn move_fox_on_mouse_down(
444 mut scene_roots_query: Query<&mut Transform, With<WorldAssetRoot>>,
445 windows_query: Query<&Window, With<PrimaryWindow>>,
446 cameras_query: Query<(&Camera, &GlobalTransform)>,
447 interactions_query: Query<&Interaction, With<RadioButton>>,
448 buttons: Res<ButtonInput<MouseButton>>,
449 app_status: Res<AppStatus>,
450) {
451 if app_status.drag_action != DragAction::MoveFox
455 || !buttons.pressed(MouseButton::Left)
456 || interactions_query
457 .iter()
458 .any(|interaction| *interaction != Interaction::None)
459 {
460 return;
461 }
462
463 let Some(mouse_position) = windows_query
465 .iter()
466 .next()
467 .and_then(Window::cursor_position)
468 else {
469 return;
470 };
471
472 let Some((camera, camera_transform)) = cameras_query.iter().next() else {
474 return;
475 };
476
477 let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else {
479 return;
480 };
481 let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) else {
482 return;
483 };
484 let plane_intersection = ray.origin + ray.direction.normalize() * ray_distance;
485
486 for mut transform in scene_roots_query.iter_mut() {
488 transform.translation = transform.translation.with_xz(plane_intersection.xz());
489 }
490}
491
492fn handle_mouse_action_change(
495 mut app_status: ResMut<AppStatus>,
496 mut messages: MessageReader<WidgetClickEvent<DragAction>>,
497) {
498 for message in messages.read() {
499 app_status.drag_action = **message;
500 }
501}
502
503fn update_radio_buttons(
506 mut widgets_query: Query<(
507 Entity,
508 Option<&mut BackgroundColor>,
509 Has<Text>,
510 &WidgetClickSender<DragAction>,
511 )>,
512 app_status: Res<AppStatus>,
513 mut text_ui_writer: TextUiWriter,
514) {
515 for (entity, maybe_bg_color, has_text, sender) in &mut widgets_query {
516 let selected = app_status.drag_action == **sender;
517 if let Some(mut bg_color) = maybe_bg_color {
518 widgets::update_ui_radio_button(&mut bg_color, selected);
519 }
520 if has_text {
521 widgets::update_ui_radio_button_text(entity, &mut text_ui_writer, selected);
522 }
523 }
524}
525
526fn move_camera_on_mouse_down(
530 mut main_cameras_query: Query<&mut Transform, (With<Camera>, Without<MirrorCamera>)>,
531 interactions_query: Query<&Interaction, With<RadioButton>>,
532 mouse_buttons: Res<ButtonInput<MouseButton>>,
533 mouse_motion: Res<AccumulatedMouseMotion>,
534 app_status: Res<AppStatus>,
535) {
536 if app_status.drag_action != DragAction::MoveCamera
540 || !mouse_buttons.pressed(MouseButton::Left)
541 || interactions_query
542 .iter()
543 .any(|interaction| *interaction != Interaction::None)
544 {
545 return;
546 }
547
548 let delta = mouse_motion.delta;
549
550 let delta_pitch = delta.y * CAMERA_PITCH_SPEED;
554 let delta_yaw = delta.x * CAMERA_YAW_SPEED;
555
556 for mut main_camera_transform in &mut main_cameras_query {
557 let (yaw, pitch, _) = main_camera_transform.rotation.to_euler(EulerRot::YXZ);
559
560 let pitch = (pitch + delta_pitch).clamp(-CAMERA_PITCH_LIMIT, CAMERA_PITCH_LIMIT);
562 let yaw = yaw + delta_yaw;
563 main_camera_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.0);
564
565 let target = Vec3::ZERO;
568 main_camera_transform.translation =
569 target - main_camera_transform.forward() * CAMERA_ORBIT_DISTANCE;
570 }
571}
572
573fn update_mirror_camera_on_main_camera_transform_change(
581 main_cameras_query: Query<
582 (&Transform, &Projection),
583 (Changed<Transform>, With<Camera>, Without<MirrorCamera>),
584 >,
585 mut mirror_cameras_query: Query<
586 (&mut Transform, &mut Projection),
587 (With<Camera>, With<MirrorCamera>, Without<Mirror>),
588 >,
589 mirrors_query: Query<&Transform, (Without<MirrorCamera>, With<Mirror>)>,
590) {
591 let Some((main_camera_transform, Projection::Perspective(main_camera_projection))) =
592 main_cameras_query.iter().next()
593 else {
594 return;
595 };
596
597 let Some(mirror_transform) = mirrors_query.iter().next() else {
598 return;
599 };
600
601 let (new_mirror_camera_transform, new_mirror_camera_projection) =
604 calculate_mirror_camera_transform_and_projection(
605 main_camera_transform,
606 main_camera_projection,
607 mirror_transform,
608 );
609
610 for (mut mirror_camera_transform, mut mirror_camera_projection) in &mut mirror_cameras_query {
611 *mirror_camera_transform = new_mirror_camera_transform;
612 *mirror_camera_projection = Projection::Perspective(new_mirror_camera_projection.clone());
613 }
614}
615
616fn play_fox_animation(
618 mut commands: Commands,
619 mut animation_players_query: Query<
620 (Entity, &mut AnimationPlayer),
621 Without<AnimationGraphHandle>,
622 >,
623 asset_server: Res<AssetServer>,
624 mut animation_graphs: ResMut<Assets<AnimationGraph>>,
625) {
626 if animation_players_query.is_empty() {
630 return;
631 }
632
633 let fox_animation = asset_server.load(GltfAssetLabel::Animation(0).from_asset(FOX_ASSET_PATH));
634 let (fox_animation_graph, fox_animation_node) =
635 AnimationGraph::from_clip(fox_animation.clone());
636 let fox_animation_graph = animation_graphs.add(fox_animation_graph);
637
638 for (entity, mut animation_player) in animation_players_query.iter_mut() {
639 commands
640 .entity(entity)
641 .insert(AnimationGraphHandle(fox_animation_graph.clone()));
642 animation_player.play(fox_animation_node).repeat();
643 }
644}
645
646fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) {
648 commands.spawn((
649 Text::new(create_help_string(app_status)),
650 Node {
651 position_type: PositionType::Absolute,
652 top: px(12),
653 left: px(12),
654 ..default()
655 },
656 HelpText,
657 ));
658}
659
660fn create_help_string(app_status: &AppStatus) -> String {
662 format!(
663 "Click and drag to move the {}",
664 match app_status.drag_action {
665 DragAction::MoveCamera => "camera",
666 DragAction::MoveFox => "fox",
667 }
668 )
669}
670
671fn update_help_text(mut help_text: Query<&mut Text, With<HelpText>>, app_status: Res<AppStatus>) {
674 for mut text in &mut help_text {
675 text.0 = create_help_string(&app_status);
676 }
677}