//! Demonstrates how to create a mirror with a second camera. use std::f32::consts::FRAC_PI_2; use crate::radio::{feathers_option_buttons, main_ui_node_scene, RadioButtonOptionValue}; use bevy::camera::RenderTarget; use bevy::{ color::palettes::css::GREEN, feathers::{ controls::FeathersRadio, dark_theme::create_dark_theme, theme::UiTheme, FeathersPlugins, }, input::mouse::AccumulatedMouseMotion, math::{reflection_matrix, uvec2, vec3}, pbr::{ExtendedMaterial, MaterialExtension}, picking::hover::Hovered, prelude::*, render::render_resource::{AsBindGroup, TextureFormat}, shader::ShaderRef, ui_widgets::{radio_self_update, ValueChange}, window::{PrimaryWindow, WindowResized}, }; #[path = "../helpers/radio.rs"] mod radio; /// A resource that stores a handle to the image that contains the rendered /// mirror world. #[derive(Resource)] struct MirrorImage(Handle); /// A marker component for the camera that renders the mirror world. #[derive(Component)] struct MirrorCamera; /// A marker component for the mirror mesh itself. #[derive(Component)] struct Mirror; /// The dummy material extension that we use for the mirror surface. /// /// This shader samples its emissive texture at the screen space position of /// each fragment rather than at the UVs. Effectively, this uses a PBR shader as /// a mask that copies a portion of the emissive texture to the screen, all in /// screen space. /// /// We use [`ExtendedMaterial`], as that's the easiest way to implement custom /// shaders that modify the built-in [`StandardMaterial`]. We don't require any /// extra data to be passed to the shader beyond the [`StandardMaterial`] PBR /// fields, but currently Bevy requires at least one field to be present in the /// extended material, so we simply have an unused field. #[derive(Clone, AsBindGroup, Asset, Reflect)] struct ScreenSpaceTextureExtension { /// An unused value that we have just to satisfy [`ExtendedMaterial`] /// requirements. #[uniform(100)] dummy: f32, } impl MaterialExtension for ScreenSpaceTextureExtension { fn fragment_shader() -> ShaderRef { "shaders/screen_space_texture_material.wesl".into() } } /// The action that will be performed when the user drags the mouse: either /// moving the camera or moving the rigged model. #[derive(Clone, Copy, PartialEq, Default)] enum DragAction { /// Dragging will move the camera. #[default] MoveCamera, /// Dragging will move the animated fox. MoveFox, } /// The settings that the user has currently chosen. /// /// Currently, this just consists of the [`DragAction`]. #[derive(Resource, Default)] struct AppStatus { /// The action that will be performed when the user drags the mouse: either /// moving the camera or moving the rigged model. drag_action: DragAction, } /// A marker component for the help text at the top of the screen. #[derive(Clone, Copy, Component)] struct HelpText; /// The coordinates that the camera looks at. const CAMERA_TARGET: Vec3 = vec3(-25.0, 20.0, 0.0); /// The camera stays this distance in meters from the camera target. const CAMERA_ORBIT_DISTANCE: f32 = 500.0; /// The speed at which the user can move the camera vertically, in radians per /// mouse input unit. const CAMERA_PITCH_SPEED: f32 = 0.003; /// The speed at which the user can move the camera horizontally, in radians per /// mouse input unit. const CAMERA_YAW_SPEED: f32 = 0.004; // Limiting pitch stops some unexpected rotation past 90° up or down. const CAMERA_PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01; /// The angle that the mirror faces. /// /// The mirror is rotated across the X axis in this many radians. const MIRROR_ROTATION_ANGLE: f32 = -FRAC_PI_2; const MIRROR_POSITION: Vec3 = vec3(-25.0, 75.0, 0.0); /// The path to the animated fox model. static FOX_ASSET_PATH: &str = "models/animated/Fox.glb"; /// The app entry point. fn main() { App::new() .add_plugins(DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { title: "Bevy Mirror Example".into(), ..default() }), ..default() })) .add_plugins(MaterialPlugin::< ExtendedMaterial, >::default()) .add_plugins(FeathersPlugins) .insert_resource(UiTheme(create_dark_theme())) .init_resource::() .add_systems(Startup, setup) .add_systems(Update, handle_window_resize_messages) .add_systems(Update, (move_camera_on_mouse_down, move_fox_on_mouse_down)) .add_observer(handle_drag_action_change) .add_observer(radio_self_update) .add_systems( Update, update_mirror_camera_on_main_camera_transform_change.after(move_camera_on_mouse_down), ) .add_systems(Update, play_fox_animation) .add_systems(Update, update_help_text) .run(); } /// A startup system that spawns the scene and sets up the mirror render target. fn setup( mut commands: Commands, windows_query: Query<&Window>, asset_server: Res, mut meshes: ResMut>, mut standard_materials: ResMut>, mut screen_space_texture_materials: ResMut< Assets>, >, mut images: ResMut>, app_status: Res, ) { // Spawn the main camera. let camera_projection = PerspectiveProjection::default(); let camera_transform = spawn_main_camera(&mut commands, &camera_projection); // Spawn the light. spawn_light(&mut commands); // Spawn the objects reflected in the mirror. spawn_ground_plane(&mut commands, &mut meshes, &mut standard_materials); spawn_fox(&mut commands, &asset_server); // Spawn the mirror and associated camera. let mirror_render_target_image = create_mirror_texture_resource(&mut commands, &windows_query, &mut images); let mirror_transform = spawn_mirror( &mut commands, &mut meshes, &mut screen_space_texture_materials, mirror_render_target_image.clone(), ); spawn_mirror_camera( &mut commands, &camera_transform, &camera_projection, &mirror_transform, mirror_render_target_image, ); // Spawn the UI. spawn_buttons(&mut commands); spawn_help_text(&mut commands, &app_status); } /// Spawns the main camera (not the mirror camera). fn spawn_main_camera( commands: &mut Commands, camera_projection: &PerspectiveProjection, ) -> Transform { let camera_transform = Transform::from_translation( vec3(-2.0, 1.0, -2.0).normalize_or_zero() * CAMERA_ORBIT_DISTANCE, ) .looking_at(CAMERA_TARGET, Vec3::Y); commands.spawn(( Camera3d::default(), camera_transform, Projection::Perspective(camera_projection.clone()), )); camera_transform } /// Spawns a directional light to illuminate the scene. fn spawn_light(commands: &mut Commands) { commands.spawn(( DirectionalLight { illuminance: 5000.0, ..default() }, Transform::from_xyz(-85.0, 16.0, -200.0).looking_at(vec3(-50.0, 0.0, 100.0), Vec3::Y), )); } /// Spawns the circular ground plane object. fn spawn_ground_plane( commands: &mut Commands, meshes: &mut Assets, standard_materials: &mut Assets, ) { commands.spawn(( Mesh3d(meshes.add(Circle::new(200.0))), MeshMaterial3d(standard_materials.add(Color::from(GREEN))), Transform::from_rotation(Quat::from_rotation_x(-FRAC_PI_2)) .with_translation(vec3(-25.0, 0.0, 0.0)), )); } /// Creates the initial image that the mirror camera will render the mirror /// world to. fn create_mirror_texture_resource( commands: &mut Commands, windows_query: &Query<&Window>, images: &mut Assets, ) -> Handle { let window = windows_query.iter().next().expect("No window found"); let window_size = uvec2(window.physical_width(), window.physical_height()); let image = create_mirror_texture_image(images, window_size); commands.insert_resource(MirrorImage(image.clone())); image } /// Spawns the camera that renders the mirror world. fn spawn_mirror_camera( commands: &mut Commands, camera_transform: &Transform, camera_projection: &PerspectiveProjection, mirror_transform: &Transform, mirror_render_target: Handle, ) { let (mirror_camera_transform, mirror_camera_projection) = calculate_mirror_camera_transform_and_projection( camera_transform, camera_projection, mirror_transform, ); commands.spawn(( Camera3d::default(), Camera { order: -1, // Reflecting the model across the mirror will flip the winding of // all the polygons. Therefore, in order to properly backface cull, // we need to turn on `invert_culling`. invert_culling: true, ..default() }, RenderTarget::Image(mirror_render_target.clone().into()), mirror_camera_transform, Projection::Perspective(mirror_camera_projection), MirrorCamera, )); } /// Spawns the animated fox. /// /// Note that this doesn't play the animation; that's handled in /// [`play_fox_animation`]. fn spawn_fox(commands: &mut Commands, asset_server: &AssetServer) { commands.spawn(( WorldAssetRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_ASSET_PATH))), Transform::from_xyz(-50.0, 0.0, -100.0), )); } /// Spawns the mirror plane mesh and returns its transform. fn spawn_mirror( commands: &mut Commands, meshes: &mut Assets, screen_space_texture_materials: &mut Assets< ExtendedMaterial, >, mirror_render_target: Handle, ) -> Transform { let mirror_transform = Transform::from_scale(vec3(300.0, 1.0, 150.0)) .with_rotation(Quat::from_rotation_x(MIRROR_ROTATION_ANGLE)) .with_translation(MIRROR_POSITION); commands.spawn(( Mesh3d(meshes.add(Plane3d::default().mesh().size(1.0, 1.0))), MeshMaterial3d(screen_space_texture_materials.add(ExtendedMaterial { base: StandardMaterial { base_color: Color::BLACK, emissive: Color::WHITE.into(), emissive_texture: Some(mirror_render_target), perceptual_roughness: 0.0, metallic: 1.0, ..default() }, extension: ScreenSpaceTextureExtension { dummy: 0.0 }, })), mirror_transform, Mirror, )); mirror_transform } /// Spawns the buttons at the bottom of the screen. fn spawn_buttons(commands: &mut Commands) { // Spawn the radio buttons that allow the user to select an object to // control. commands.spawn_scene(bsn! { @main_ui_node_scene() Children [@feathers_option_buttons( "Drag Action", &[ (DragAction::MoveCamera, "Move Camera"), (DragAction::MoveFox, "Move Fox"), ], 0, )] }); } /// Given the transform and projection of the main camera, returns an /// appropriate transform and projection for the mirror camera. fn calculate_mirror_camera_transform_and_projection( main_camera_transform: &Transform, main_camera_projection: &PerspectiveProjection, mirror_transform: &Transform, ) -> (Transform, PerspectiveProjection) { // Calculate the reflection matrix (a.k.a. Householder matrix) that will // reflect the scene across the mirror plane. // // Note that you must calculate this in *matrix* form and only *afterward* // convert to a `Transform` instead of composing `Transform`s. This is // because the reflection matrix has non-uniform scale, and composing // transforms can't always handle composition of matrices with non-uniform // scales. let mirror_camera_transform = Transform::from_matrix( Mat4::from_mat3a(reflection_matrix(Vec3::NEG_Z)) * main_camera_transform.to_matrix(), ); // Compute the distance from the camera to the mirror plane. This will be // used to calculate the distance to the near clip plane for the mirror // world. let distance_from_camera_to_mirror = InfinitePlane3d::new(mirror_transform.rotation * Vec3::Y) .signed_distance( Isometry3d::IDENTITY, mirror_transform.translation - main_camera_transform.translation, ); // Compute the normal of the mirror plane in view space. let view_from_world = main_camera_transform.compute_affine().matrix3.inverse(); let mirror_projection_plane_normal = (view_from_world * (mirror_transform.rotation * Vec3::NEG_Y)).normalize(); // Compute the final projection. It should match the main camera projection, // except that `near` and `near_normal` should be set to the updated near // plane and near normal plane as above. let mirror_camera_projection = PerspectiveProjection { near_clip_plane: mirror_projection_plane_normal.extend(distance_from_camera_to_mirror), ..*main_camera_projection }; (mirror_camera_transform, mirror_camera_projection) } /// A system that resizes the render target image when the user resizes the window. /// /// Since the image that stores the rendered mirror world has the same physical /// size as the window, we need to reallocate it and reattach it to the mirror /// material whenever the window size changes. fn handle_window_resize_messages( windows_query: Query<&Window>, mut mirror_cameras_query: Query<&mut RenderTarget, With>, mut images: ResMut>, mut mirror_image: ResMut, mut screen_space_texture_materials: ResMut< Assets>, >, mut resize_messages: MessageReader, ) { // We run at most once, regardless of the number of window resize messages // there were this frame. let Some(resize_message) = resize_messages.read().next() else { return; }; let Ok(window) = windows_query.get(resize_message.window) else { return; }; let window_size = uvec2(window.physical_width(), window.physical_height()); let image = create_mirror_texture_image(&mut images, window_size); images.remove(mirror_image.0.id()); mirror_image.0 = image.clone(); for mut target in mirror_cameras_query.iter_mut() { *target = image.clone().into(); } for (_, material) in screen_space_texture_materials.iter_mut() { material.base.emissive_texture = Some(image.clone()); } } /// Creates the image that will be used to store the reflected scene. fn create_mirror_texture_image(images: &mut Assets, window_size: UVec2) -> Handle { images.add(Image::new_target_texture( window_size.x, window_size.y, TextureFormat::Bgra8UnormSrgb, None, )) } // Moves the fox when the user moves the mouse with the left button down. fn move_fox_on_mouse_down( mut scene_roots_query: Query<&mut Transform, With>, windows_query: Query<&Window, With>, cameras_query: Query<(&Camera, &GlobalTransform)>, radio_hovered_query: Query<&Hovered, With>, buttons: Res>, app_status: Res, ) { // Only process the mouse motion if the left mouse button is pressed, the // mouse action is set to move the fox, and the pointer isn't over a UI // widget. if app_status.drag_action != DragAction::MoveFox || !buttons.pressed(MouseButton::Left) || radio_hovered_query.iter().any(Hovered::get) { return; } // Find out where the user clicked the mouse. let Some(mouse_position) = windows_query .iter() .next() .and_then(Window::cursor_position) else { return; }; // Grab the camera. let Some((camera, camera_transform)) = cameras_query.iter().next() else { return; }; // Figure out where the user clicked on the plane. let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else { return; }; let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) else { return; }; let plane_intersection = ray.origin + ray.direction.normalize() * ray_distance; // Move the fox. for mut transform in scene_roots_query.iter_mut() { transform.translation = transform.translation.with_xz(plane_intersection.xz()); } } /// An observer that changes the drag action when the user clicks on one of the /// radio buttons. fn handle_drag_action_change( event: On>, new_value_query: Query<&RadioButtonOptionValue>, mut app_status: ResMut, ) { let Ok(RadioButtonOptionValue(drag_action)) = new_value_query.get(event.value) else { return; }; app_status.drag_action = *drag_action; } /// A system that processes user mouse actions that move the camera. /// /// This is mostly copied from `examples/camera/camera_orbit.rs`. fn move_camera_on_mouse_down( mut main_cameras_query: Query<&mut Transform, (With, Without)>, radio_hovered_query: Query<&Hovered, With>, mouse_buttons: Res>, mouse_motion: Res, app_status: Res, ) { // Only process the mouse motion if the left mouse button is pressed, the // mouse action is set to move the fox, and the pointer isn't over a UI // widget. if app_status.drag_action != DragAction::MoveCamera || !mouse_buttons.pressed(MouseButton::Left) || radio_hovered_query.iter().any(Hovered::get) { return; } let delta = mouse_motion.delta; // Mouse motion is one of the few inputs that should not be multiplied by delta time, // as we are already receiving the full movement since the last frame was rendered. Multiplying // by delta time here would make the movement slower that it should be. let delta_pitch = delta.y * CAMERA_PITCH_SPEED; let delta_yaw = delta.x * CAMERA_YAW_SPEED; for mut main_camera_transform in &mut main_cameras_query { // Obtain the existing pitch and yaw values from the transform. let (yaw, pitch, _) = main_camera_transform.rotation.to_euler(EulerRot::YXZ); // Establish the new yaw and pitch, preventing the pitch value from exceeding our limits. let pitch = (pitch + delta_pitch).clamp(-CAMERA_PITCH_LIMIT, CAMERA_PITCH_LIMIT); let yaw = yaw + delta_yaw; main_camera_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.0); // Adjust the translation to maintain the correct orientation toward the orbit target. // In our example it's a static target, but this could easily be customized. let target = Vec3::ZERO; main_camera_transform.translation = target - main_camera_transform.forward() * CAMERA_ORBIT_DISTANCE; } } /// Updates the position, rotation, and projection of the mirror camera when the /// main camera is moved. /// /// When the main camera is moved, the mirror camera must be moved to match it. /// The *projection* on the mirror camera must also be altered, because the /// projection takes the view-space rotation of and distance to the mirror into /// account. fn update_mirror_camera_on_main_camera_transform_change( main_cameras_query: Query< (&Transform, &Projection), (Changed, With, Without), >, mut mirror_cameras_query: Query< (&mut Transform, &mut Projection), (With, With, Without), >, mirrors_query: Query<&Transform, (Without, With)>, ) { let Some((main_camera_transform, Projection::Perspective(main_camera_projection))) = main_cameras_query.iter().next() else { return; }; let Some(mirror_transform) = mirrors_query.iter().next() else { return; }; // Here we need the transforms of both the camera and the mirror in order to // properly calculate the new projection. let (new_mirror_camera_transform, new_mirror_camera_projection) = calculate_mirror_camera_transform_and_projection( main_camera_transform, main_camera_projection, mirror_transform, ); for (mut mirror_camera_transform, mut mirror_camera_projection) in &mut mirror_cameras_query { *mirror_camera_transform = new_mirror_camera_transform; *mirror_camera_projection = Projection::Perspective(new_mirror_camera_projection.clone()); } } /// Plays the initial animation on the fox model. fn play_fox_animation( mut commands: Commands, mut animation_players_query: Query< (Entity, &mut AnimationPlayer), Without, >, asset_server: Res, mut animation_graphs: ResMut>, ) { // Only pick up animation players that don't already have an animation graph // handle. // This ensures that we only start playing the animation once. if animation_players_query.is_empty() { return; } let fox_animation = asset_server.load(GltfAssetLabel::Animation(0).from_asset(FOX_ASSET_PATH)); let (fox_animation_graph, fox_animation_node) = AnimationGraph::from_clip(fox_animation.clone()); let fox_animation_graph = animation_graphs.add(fox_animation_graph); for (entity, mut animation_player) in animation_players_query.iter_mut() { commands .entity(entity) .insert(AnimationGraphHandle(fox_animation_graph.clone())); animation_player.play(fox_animation_node).repeat(); } } /// Spawns the help text at the top of the screen. fn spawn_help_text(commands: &mut Commands, app_status: &AppStatus) { commands.spawn(( Text::new(create_help_string(app_status)), Node { position_type: PositionType::Absolute, top: px(12), left: px(12), ..default() }, HelpText, )); } /// Creates the help string at the top left of the screen. fn create_help_string(app_status: &AppStatus) -> String { format!( "Click and drag to move the {}", match app_status.drag_action { DragAction::MoveCamera => "camera", DragAction::MoveFox => "fox", } ) } /// Updates the help text in the top left of the screen to reflect the current /// drag mode. fn update_help_text(mut help_text: Query<&mut Text, With>, app_status: Res) { for mut text in &mut help_text { text.0 = create_help_string(&app_status); } }