use std::f32::consts::FRAC_PI_2;
use crate::widgets::{RadioButton, WidgetClickEvent, WidgetClickSender};
use bevy::camera::RenderTarget;
use bevy::{
asset::RenderAssetUsages,
color::palettes::css::GREEN,
input::mouse::AccumulatedMouseMotion,
math::{reflection_matrix, uvec2, vec3},
pbr::{ExtendedMaterial, MaterialExtension},
prelude::*,
render::render_resource::{
AsBindGroup, Extent3d, TextureDimension, TextureFormat, TextureUsages,
},
shader::ShaderRef,
window::{PrimaryWindow, WindowResized},
};
#[path = "../helpers/widgets.rs"]
mod widgets;
#[derive(Resource)]
struct MirrorImage(Handle<Image>);
#[derive(Component)]
struct MirrorCamera;
#[derive(Component)]
struct Mirror;
#[derive(Clone, AsBindGroup, Asset, Reflect)]
struct ScreenSpaceTextureExtension {
#[uniform(100)]
dummy: f32,
}
impl MaterialExtension for ScreenSpaceTextureExtension {
fn fragment_shader() -> ShaderRef {
"shaders/screen_space_texture_material.wgsl".into()
}
}
#[derive(Clone, Copy, PartialEq, Default)]
enum DragAction {
#[default]
MoveCamera,
MoveFox,
}
#[derive(Resource, Default)]
struct AppStatus {
drag_action: DragAction,
}
#[derive(Clone, Copy, Component)]
struct HelpText;
const CAMERA_TARGET: Vec3 = vec3(-25.0, 20.0, 0.0);
const CAMERA_ORBIT_DISTANCE: f32 = 500.0;
const CAMERA_PITCH_SPEED: f32 = 0.003;
const CAMERA_YAW_SPEED: f32 = 0.004;
const CAMERA_PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;
const MIRROR_ROTATION_ANGLE: f32 = -FRAC_PI_2;
const MIRROR_POSITION: Vec3 = vec3(-25.0, 75.0, 0.0);
static FOX_ASSET_PATH: &str = "models/animated/Fox.glb";
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();
}
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>,
) {
let camera_projection = PerspectiveProjection::default();
let camera_transform = spawn_main_camera(&mut commands, &camera_projection);
spawn_light(&mut commands);
spawn_ground_plane(&mut commands, &mut meshes, &mut standard_materials);
spawn_fox(&mut commands, &asset_server);
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_buttons(&mut commands);
spawn_help_text(&mut commands, &app_status);
}
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
}
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),
));
}
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)),
));
}
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
}
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,
invert_culling: true,
..default()
},
RenderTarget::Image(mirror_render_target.clone().into()),
mirror_camera_transform,
Projection::Perspective(mirror_camera_projection),
MirrorCamera,
));
}
fn spawn_fox(commands: &mut Commands, asset_server: &AssetServer) {
commands.spawn((
SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset(FOX_ASSET_PATH))),
Transform::from_xyz(-50.0, 0.0, -100.0),
));
}
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
}
fn spawn_buttons(commands: &mut Commands) {
commands.spawn((
widgets::main_ui_node(),
children![widgets::option_buttons(
"Drag Action",
&[
(DragAction::MoveCamera, "Move Camera"),
(DragAction::MoveFox, "Move Fox"),
],
)],
));
}
fn calculate_mirror_camera_transform_and_projection(
main_camera_transform: &Transform,
main_camera_projection: &PerspectiveProjection,
mirror_transform: &Transform,
) -> (Transform, PerspectiveProjection) {
let mirror_camera_transform = Transform::from_matrix(
Mat4::from_mat3a(reflection_matrix(Vec3::NEG_Z)) * main_camera_transform.to_matrix(),
);
let distance_from_camera_to_mirror = InfinitePlane3d::new(mirror_transform.rotation * Vec3::Y)
.signed_distance(
Isometry3d::IDENTITY,
mirror_transform.translation - main_camera_transform.translation,
);
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();
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)
}
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>,
) {
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());
}
}
fn create_mirror_texture_image(images: &mut Assets<Image>, window_size: UVec2) -> Handle<Image> {
let mirror_image_extent = Extent3d {
width: window_size.x,
height: window_size.y,
depth_or_array_layers: 1,
};
let mut image = Image::new_uninit(
mirror_image_extent,
TextureDimension::D2,
TextureFormat::Bgra8UnormSrgb,
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
);
image.texture_descriptor.usage |=
TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST | TextureUsages::RENDER_ATTACHMENT;
images.add(image)
}
fn move_fox_on_mouse_down(
mut scene_roots_query: Query<&mut Transform, With<SceneRoot>>,
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>,
) {
if app_status.drag_action != DragAction::MoveFox
|| !buttons.pressed(MouseButton::Left)
|| interactions_query
.iter()
.any(|interaction| *interaction != Interaction::None)
{
return;
}
let Some(mouse_position) = windows_query
.iter()
.next()
.and_then(Window::cursor_position)
else {
return;
};
let Some((camera, camera_transform)) = cameras_query.iter().next() else {
return;
};
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;
for mut transform in scene_roots_query.iter_mut() {
transform.translation = transform.translation.with_xz(plane_intersection.xz());
}
}
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;
}
}
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);
}
}
}
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>,
) {
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;
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 {
let (yaw, pitch, _) = main_camera_transform.rotation.to_euler(EulerRot::YXZ);
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);
let target = Vec3::ZERO;
main_camera_transform.translation =
target - main_camera_transform.forward() * CAMERA_ORBIT_DISTANCE;
}
}
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;
};
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());
}
}
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>>,
) {
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();
}
}
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,
));
}
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",
}
)
}
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);
}
}