mirror of
https://github.com/bevyengine/bevy.git
synced 2026-06-30 07:45:50 -04:00
3f401d1507
# Objective Fix #19101. (Kinda, this is not the exact solution proposed in that issue, but it still shortens most of the examples) ## Solution Most of the images created in examples are for use in render targets. `Image::new_target_texture` is made for exactly that and significantly shortens the image creation process. ## Testing I tested all the examples I changed and they seem to work fine. --- btw for some reason most of the examples use `TextureFormat::Bgra8UnormSrgb` while the documentation for `Image::new_target_texture` recommends `TextureFormat::Rgba8UnormSrgb` for SDR images. What's up with that? --------- Co-authored-by: Carter Anderson <mcanders1@gmail.com>
665 lines
23 KiB
Rust
665 lines
23 KiB
Rust
//! Demonstrates how to create a mirror with a second camera.
|
|
|
|
use std::f32::consts::FRAC_PI_2;
|
|
|
|
use crate::widgets::{RadioButton, WidgetClickEvent, WidgetClickSender};
|
|
use bevy::camera::RenderTarget;
|
|
use bevy::{
|
|
color::palettes::css::GREEN,
|
|
input::mouse::AccumulatedMouseMotion,
|
|
math::{reflection_matrix, uvec2, vec3},
|
|
pbr::{ExtendedMaterial, MaterialExtension},
|
|
prelude::*,
|
|
render::render_resource::{AsBindGroup, TextureFormat},
|
|
shader::ShaderRef,
|
|
window::{PrimaryWindow, WindowResized},
|
|
};
|
|
|
|
#[path = "../helpers/widgets.rs"]
|
|
mod widgets;
|
|
|
|
/// A resource that stores a handle to the image that contains the rendered
|
|
/// mirror world.
|
|
#[derive(Resource)]
|
|
struct MirrorImage(Handle<Image>);
|
|
|
|
/// 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.wgsl".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<StandardMaterial, ScreenSpaceTextureExtension>,
|
|
>::default())
|
|
.init_resource::<AppStatus>()
|
|
.add_message::<WidgetClickEvent<DragAction>>()
|
|
.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_systems(Update, widgets::handle_ui_interactions::<DragAction>)
|
|
.add_systems(
|
|
Update,
|
|
(handle_mouse_action_change, update_radio_buttons)
|
|
.after(widgets::handle_ui_interactions::<DragAction>),
|
|
)
|
|
.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<AssetServer>,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut standard_materials: ResMut<Assets<StandardMaterial>>,
|
|
mut screen_space_texture_materials: ResMut<
|
|
Assets<ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>>,
|
|
>,
|
|
mut images: ResMut<Assets<Image>>,
|
|
app_status: Res<AppStatus>,
|
|
) {
|
|
// 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<Mesh>,
|
|
standard_materials: &mut Assets<StandardMaterial>,
|
|
) {
|
|
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<Image>,
|
|
) -> Handle<Image> {
|
|
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<Image>,
|
|
) {
|
|
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<Mesh>,
|
|
screen_space_texture_materials: &mut Assets<
|
|
ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>,
|
|
>,
|
|
mirror_render_target: Handle<Image>,
|
|
) -> 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((
|
|
widgets::main_ui_node(),
|
|
children![widgets::option_buttons(
|
|
"Drag Action",
|
|
&[
|
|
(DragAction::MoveCamera, "Move Camera"),
|
|
(DragAction::MoveFox, "Move Fox"),
|
|
],
|
|
)],
|
|
));
|
|
}
|
|
|
|
/// 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<MirrorCamera>>,
|
|
mut images: ResMut<Assets<Image>>,
|
|
mut mirror_image: ResMut<MirrorImage>,
|
|
mut screen_space_texture_materials: ResMut<
|
|
Assets<ExtendedMaterial<StandardMaterial, ScreenSpaceTextureExtension>>,
|
|
>,
|
|
mut resize_messages: MessageReader<WindowResized>,
|
|
) {
|
|
// 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<Image>, window_size: UVec2) -> Handle<Image> {
|
|
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<WorldAssetRoot>>,
|
|
windows_query: Query<&Window, With<PrimaryWindow>>,
|
|
cameras_query: Query<(&Camera, &GlobalTransform)>,
|
|
interactions_query: Query<&Interaction, With<RadioButton>>,
|
|
buttons: Res<ButtonInput<MouseButton>>,
|
|
app_status: Res<AppStatus>,
|
|
) {
|
|
// 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)
|
|
|| interactions_query
|
|
.iter()
|
|
.any(|interaction| *interaction != Interaction::None)
|
|
{
|
|
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());
|
|
}
|
|
}
|
|
|
|
/// A system that changes the drag action when the user clicks on one of the
|
|
/// radio buttons.
|
|
fn handle_mouse_action_change(
|
|
mut app_status: ResMut<AppStatus>,
|
|
mut messages: MessageReader<WidgetClickEvent<DragAction>>,
|
|
) {
|
|
for message in messages.read() {
|
|
app_status.drag_action = **message;
|
|
}
|
|
}
|
|
|
|
/// A system that updates the radio buttons at the bottom of the screen to
|
|
/// reflect the current drag action.
|
|
fn update_radio_buttons(
|
|
mut widgets_query: Query<(
|
|
Entity,
|
|
Option<&mut BackgroundColor>,
|
|
Has<Text>,
|
|
&WidgetClickSender<DragAction>,
|
|
)>,
|
|
app_status: Res<AppStatus>,
|
|
mut text_ui_writer: TextUiWriter,
|
|
) {
|
|
for (entity, maybe_bg_color, has_text, sender) in &mut widgets_query {
|
|
let selected = app_status.drag_action == **sender;
|
|
if let Some(mut bg_color) = maybe_bg_color {
|
|
widgets::update_ui_radio_button(&mut bg_color, selected);
|
|
}
|
|
if has_text {
|
|
widgets::update_ui_radio_button_text(entity, &mut text_ui_writer, selected);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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<Camera>, Without<MirrorCamera>)>,
|
|
interactions_query: Query<&Interaction, With<RadioButton>>,
|
|
mouse_buttons: Res<ButtonInput<MouseButton>>,
|
|
mouse_motion: Res<AccumulatedMouseMotion>,
|
|
app_status: Res<AppStatus>,
|
|
) {
|
|
// 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)
|
|
|| interactions_query
|
|
.iter()
|
|
.any(|interaction| *interaction != Interaction::None)
|
|
{
|
|
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<Transform>, With<Camera>, Without<MirrorCamera>),
|
|
>,
|
|
mut mirror_cameras_query: Query<
|
|
(&mut Transform, &mut Projection),
|
|
(With<Camera>, With<MirrorCamera>, Without<Mirror>),
|
|
>,
|
|
mirrors_query: Query<&Transform, (Without<MirrorCamera>, With<Mirror>)>,
|
|
) {
|
|
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<AnimationGraphHandle>,
|
|
>,
|
|
asset_server: Res<AssetServer>,
|
|
mut animation_graphs: ResMut<Assets<AnimationGraph>>,
|
|
) {
|
|
// 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<HelpText>>, app_status: Res<AppStatus>) {
|
|
for mut text in &mut help_text {
|
|
text.0 = create_help_string(&app_status);
|
|
}
|
|
}
|