Skip to main content

mirror/
mirror.rs

1//! Demonstrates how to create a mirror with a second camera.
2
3use 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/// A resource that stores a handle to the image that contains the rendered
27/// mirror world.
28#[derive(Resource)]
29struct MirrorImage(Handle<Image>);
30
31/// A marker component for the camera that renders the mirror world.
32#[derive(Component)]
33struct MirrorCamera;
34
35/// A marker component for the mirror mesh itself.
36#[derive(Component)]
37struct 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)]
52struct ScreenSpaceTextureExtension {
53    /// An unused value that we have just to satisfy [`ExtendedMaterial`]
54    /// requirements.
55    #[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/// 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)]
68enum 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)]
80struct 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)]
88struct HelpText;
89
90/// The coordinates that the camera looks at.
91const CAMERA_TARGET: Vec3 = vec3(-25.0, 20.0, 0.0);
92/// The camera stays this distance in meters from the camera target.
93const 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.
96const 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.
99const CAMERA_YAW_SPEED: f32 = 0.004;
100// Limiting pitch stops some unexpected rotation past 90° up or down.
101const 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.
106const MIRROR_ROTATION_ANGLE: f32 = -FRAC_PI_2;
107const MIRROR_POSITION: Vec3 = vec3(-25.0, 75.0, 0.0);
108
109/// The path to the animated fox model.
110static FOX_ASSET_PATH: &str = "models/animated/Fox.glb";
111
112/// The app entry point.
113fn 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.
143fn 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).
189fn 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.
208fn 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.
219fn 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.
234fn 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.
247fn 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`].
282fn 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.
290fn 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.
323fn 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.
341fn 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.
388fn 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.
423fn 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.
433fn 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.
482fn 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`.
497fn 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.
546fn 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.
583fn 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.
613fn 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.
627fn 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.
639fn 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}