1use std::f32::consts::FRAC_PI_2;
4
5use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue};
6use bevy::camera::RenderTarget;
7use 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"]
24mod radio;
25
26#[derive(Resource)]
29struct MirrorImage(Handle<Image>);
30
31#[derive(Component)]
33struct MirrorCamera;
34
35#[derive(Component)]
37struct Mirror;
38
39#[derive(Clone, AsBindGroup, Asset, Reflect)]
52struct ScreenSpaceTextureExtension {
53 #[uniform(100)]
56 dummy: f32,
57}
58
59impl MaterialExtension for ScreenSpaceTextureExtension {
60 fn fragment_shader() -> ShaderRef {
61 "shaders/screen_space_texture_material.wesl".into()
62 }
63}
64
65#[derive(Clone, Copy, PartialEq, Default)]
68enum DragAction {
69 #[default]
71 MoveCamera,
72 MoveFox,
74}
75
76#[derive(Resource, Default)]
80struct AppStatus {
81 drag_action: DragAction,
84}
85
86#[derive(Clone, Copy, Component)]
88struct HelpText;
89
90const CAMERA_TARGET: Vec3 = vec3(-25.0, 20.0, 0.0);
92const CAMERA_ORBIT_DISTANCE: f32 = 500.0;
94const CAMERA_PITCH_SPEED: f32 = 0.003;
97const CAMERA_YAW_SPEED: f32 = 0.004;
100const CAMERA_PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;
102
103const MIRROR_ROTATION_ANGLE: f32 = -FRAC_PI_2;
107const MIRROR_POSITION: Vec3 = vec3(-25.0, 75.0, 0.0);
108
109static FOX_ASSET_PATH: &str = "models/animated/Fox.glb";
111
112fn 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
142fn 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 let camera_projection = PerspectiveProjection::default();
157 let camera_transform = spawn_main_camera(&mut commands, &camera_projection);
158
159 spawn_light(&mut commands);
161
162 spawn_ground_plane(&mut commands, &mut meshes, &mut standard_materials);
164 spawn_fox(&mut commands, &asset_server);
165
166 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_buttons(&mut commands);
185 spawn_help_text(&mut commands, &app_status);
186}
187
188fn 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
207fn 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
218fn 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
232fn 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
246fn 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 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
278fn 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
289fn 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
322fn spawn_buttons(commands: &mut Commands) {
324 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
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 images.add(Image::new_target_texture(
425 window_size.x,
426 window_size.y,
427 TextureFormat::Bgra8UnormSrgb,
428 None,
429 ))
430}
431
432fn 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 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 let Some(mouse_position) = windows_query
453 .iter()
454 .next()
455 .and_then(Window::cursor_position)
456 else {
457 return;
458 };
459
460 let Some((camera, camera_transform)) = cameras_query.iter().next() else {
462 return;
463 };
464
465 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 for mut transform in scene_roots_query.iter_mut() {
476 transform.translation = transform.translation.with_xz(plane_intersection.xz());
477 }
478}
479
480fn 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
494fn 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 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 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 let (yaw, pitch, _) = main_camera_transform.rotation.to_euler(EulerRot::YXZ);
525
526 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 let target = Vec3::ZERO;
534 main_camera_transform.translation =
535 target - main_camera_transform.forward() * CAMERA_ORBIT_DISTANCE;
536 }
537}
538
539fn 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 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
582fn 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 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
612fn 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
626fn 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
637fn 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}