mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-18 07:35:19 +00:00
06751e54f6
This PR implements basic object selection, with help from `bevy_mod_picking` and `bevy_mod_outline`. ### Work Done * Click to select objects in the world * Selected objects become visually highlighted * The selected entity is available globally on the `Res<Selection>` resource * Selected entities can also be queried via the `Selected` marker component * Pressing `F` will "focus" the camera on the selected entity * Pressing `Esc` will clear the active selection ### Known Issues * We're currently using `bevy_mod_picking`'s "Selection" feature, which isn't aware of our own input events. As a consequence, it will select the object under the cursor when releasing `LMB` after orbiting the camera, which is not what we want. This should be pretty easily fixable by dropping `bevy_mod_picking`'s selection feature and reading the object under the cursor in our own click listener that only runs if the LMB event is unhandled. * The skinned mesh is not currently selectable. This is likely fixable by adjusting the logic in `scene::mark_scene_objects`... somehow. The `Gltf` data structure is kind of a nightmare to work with, so this will take some investigation. ### Future Enhancements * For parity with other DCC apps, the "Focus" action should also adjust the camera zoom level to "frame" the selected object in the view. Punting on that for now because I'm not super confident about the math involved. * Group selection with Shift/Ctrl and/or marquee selection would be really nice to have, but probably not critical for now.
341 lines
9.8 KiB
Rust
341 lines
9.8 KiB
Rust
use std::f32::consts;
|
|
|
|
use bevy::{
|
|
prelude::*,
|
|
window::{CursorGrabMode, PrimaryWindow},
|
|
};
|
|
|
|
use crate::{
|
|
input::{Action, CameraMotion, InputAction, Lifecycle, Selection},
|
|
interaction::Selected,
|
|
math::{
|
|
Easing, InterpPosition, InterpVelocity, Interpolation, InterpolationSystem, NearlyEq,
|
|
Velocity,
|
|
},
|
|
};
|
|
|
|
pub struct CameraControllerPlugin;
|
|
|
|
#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
pub struct CameraMotionSystem;
|
|
|
|
impl Plugin for CameraControllerPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
app.add_systems(
|
|
Update,
|
|
(
|
|
camera_control_kinematic,
|
|
(process_physical_inputs, process_focus_inputs).before(InterpolationSystem),
|
|
(camera_control_physical, interp_camera_focus).after(InterpolationSystem),
|
|
)
|
|
.in_set(CameraMotionSystem),
|
|
);
|
|
}
|
|
}
|
|
|
|
#[derive(Component)]
|
|
pub struct CameraController {
|
|
pub focus: Vec3,
|
|
pub radius: f32,
|
|
pub upside_down: bool,
|
|
}
|
|
|
|
impl Default for CameraController {
|
|
fn default() -> Self {
|
|
Self {
|
|
focus: Vec3::ZERO,
|
|
radius: 10.0,
|
|
upside_down: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Bundle)]
|
|
pub struct CameraControllerBundle {
|
|
controller: CameraController,
|
|
velocity: Velocity,
|
|
}
|
|
|
|
impl CameraControllerBundle {
|
|
pub fn new(focus: Vec3, position: Vec3) -> Self {
|
|
Self {
|
|
controller: CameraController {
|
|
focus,
|
|
radius: Vec3::distance(position, focus),
|
|
..default()
|
|
},
|
|
velocity: Velocity(Vec3::ZERO),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn camera_control_kinematic(
|
|
mut q_window: Query<&mut Window, With<PrimaryWindow>>,
|
|
mut input: EventReader<InputAction>,
|
|
mut q_camera: Query<(&mut CameraController, &mut Transform, &Projection)>,
|
|
) {
|
|
if input.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let (mut controller, mut xform, proj) = q_camera.single_mut();
|
|
let mut window = q_window.single_mut();
|
|
|
|
let mut any_ongoing = false;
|
|
let mut any_started = false;
|
|
let mut any_stopped = false;
|
|
|
|
for action in input.iter().filter(|action| {
|
|
!matches!(
|
|
action,
|
|
InputAction(_, Action::Camera(CameraMotion::Translate(_)))
|
|
)
|
|
}) {
|
|
if let InputAction(lifecycle, Action::Camera(motion)) = action {
|
|
use CameraMotion::*;
|
|
|
|
match lifecycle {
|
|
Lifecycle::Start => any_started = true,
|
|
Lifecycle::Ongoing => any_ongoing = true,
|
|
Lifecycle::Stop => any_stopped = true,
|
|
}
|
|
|
|
match motion {
|
|
Pan(delta) => do_pan(proj, &window, *delta, &mut xform, &mut controller),
|
|
Zoom(delta) => do_zoom(&window, *delta, &mut xform, &mut controller),
|
|
Orbit(delta) => do_orbit(&window, *delta, *lifecycle, &mut xform, &mut controller),
|
|
Rotate(delta) => do_rotate(&window, *delta, &mut xform, &mut controller),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
if any_started {
|
|
capture_pointer(&mut window);
|
|
} else if any_stopped && !any_ongoing {
|
|
release_pointer(&mut window);
|
|
}
|
|
}
|
|
|
|
fn process_physical_inputs(
|
|
mut cmd: Commands,
|
|
mut input: EventReader<InputAction>,
|
|
q_camera: Query<(Entity, &Velocity), With<CameraController>>,
|
|
mut q_interp: Query<&mut InterpVelocity>,
|
|
) {
|
|
for action in input.iter() {
|
|
let &InputAction(_, Action::Camera(CameraMotion::Translate(delta))) = action else {
|
|
continue;
|
|
};
|
|
|
|
for (ent, velocity) in q_camera.iter() {
|
|
let target = delta * 0.075;
|
|
|
|
if let Ok(mut interp) = q_interp.get_mut(ent) {
|
|
if interp.target.nearly_eq_within(target, 1e-3) {
|
|
continue;
|
|
}
|
|
|
|
interp.initial = **velocity;
|
|
interp.target = target;
|
|
interp.t = 0.;
|
|
} else {
|
|
cmd.entity(ent).insert(InterpVelocity {
|
|
inner: Interpolation {
|
|
initial: **velocity,
|
|
target,
|
|
easing: Easing::Sine,
|
|
..default()
|
|
},
|
|
accel_zero2max: 0.333,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn process_focus_inputs(
|
|
mut cmd: Commands,
|
|
mut input: EventReader<InputAction>,
|
|
mut q_camera: Query<(Entity, &CameraController, &mut Velocity)>,
|
|
q_interp_vel: Query<&InterpVelocity>,
|
|
mut q_interp_pos: Query<&mut InterpPosition>,
|
|
q_selected_xform: Query<&GlobalTransform, With<Selected>>,
|
|
) {
|
|
if input.iter().any(|action| {
|
|
matches!(
|
|
action,
|
|
InputAction(Lifecycle::Stop, Action::Selection(Selection::Focus))
|
|
)
|
|
}) {
|
|
let Ok(target) = q_selected_xform
|
|
.get_single()
|
|
.map(|xform| xform.translation())
|
|
else {
|
|
return;
|
|
};
|
|
|
|
for (ent, controller, mut velocity) in q_camera.iter_mut() {
|
|
// Zero out any existing camera velocity or interpolation
|
|
**velocity = Vec3::ZERO;
|
|
|
|
if q_interp_vel.get(ent).is_ok() {
|
|
cmd.entity(ent).remove::<InterpVelocity>();
|
|
}
|
|
|
|
let initial = controller.focus;
|
|
let easing = Easing::SineOut;
|
|
let duration = 0.25;
|
|
|
|
if let Ok(mut interp) = q_interp_pos.get_mut(ent) {
|
|
if interp.target.nearly_eq_within(target, 1e-3) {
|
|
continue;
|
|
}
|
|
|
|
interp.initial = initial;
|
|
interp.target = target;
|
|
interp.easing = easing;
|
|
interp.t = 0.;
|
|
interp.duration = duration;
|
|
} else {
|
|
cmd.entity(ent).insert(InterpPosition {
|
|
inner: Interpolation {
|
|
initial,
|
|
target,
|
|
easing,
|
|
..default()
|
|
},
|
|
duration,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn camera_control_physical(
|
|
mut q_camera: Query<(&mut CameraController, &mut Transform, &Velocity)>,
|
|
) {
|
|
for (mut controller, mut xform, velocity) in q_camera.iter_mut() {
|
|
let delta_pos = xform.rotation * (**velocity);
|
|
xform.translation += delta_pos;
|
|
controller.focus += delta_pos;
|
|
}
|
|
}
|
|
|
|
fn interp_camera_focus(
|
|
mut q_camera: Query<(&mut CameraController, &mut Transform, &InterpPosition)>,
|
|
) {
|
|
for (mut controller, mut xform, interp) in q_camera.iter_mut() {
|
|
let target_focus = interp.current();
|
|
let delta_pos = target_focus - controller.focus;
|
|
|
|
controller.focus = target_focus;
|
|
xform.translation += delta_pos;
|
|
}
|
|
}
|
|
|
|
#[inline]
|
|
fn capture_pointer(window: &mut Window) {
|
|
window.cursor.visible = false;
|
|
window.cursor.grab_mode = CursorGrabMode::Confined;
|
|
}
|
|
|
|
#[inline]
|
|
fn release_pointer(window: &mut Window) {
|
|
window.cursor.visible = true;
|
|
window.cursor.grab_mode = CursorGrabMode::None;
|
|
}
|
|
|
|
#[inline]
|
|
fn do_orbit(
|
|
win: &Window,
|
|
delta: Vec2,
|
|
event_lifecycle: Lifecycle,
|
|
xform: &mut Transform,
|
|
controller: &mut CameraController,
|
|
) {
|
|
// Set `upside_down` flag if the event just started or stopped
|
|
if matches!(event_lifecycle, Lifecycle::Start | Lifecycle::Stop) {
|
|
controller.upside_down = xform.up().y <= 0.;
|
|
}
|
|
|
|
// Translate to origin
|
|
xform.translation += xform.forward() * controller.radius;
|
|
|
|
// Calculate rotation
|
|
let delta_x = {
|
|
let delta = delta.x / (win.physical_width() as f32) * consts::PI * 2.0;
|
|
match controller.upside_down {
|
|
true => -delta,
|
|
false => delta,
|
|
}
|
|
};
|
|
let delta_y = delta.y / (win.physical_height() as f32) * consts::PI;
|
|
|
|
let yaw = Quat::from_rotation_y(-delta_x);
|
|
let pitch = Quat::from_rotation_x(-delta_y);
|
|
|
|
// Rotate around global Y and local X
|
|
xform.rotation = yaw * xform.rotation;
|
|
xform.rotation *= pitch;
|
|
|
|
// Translate back to the original position
|
|
xform.translation += xform.back() * controller.radius;
|
|
}
|
|
|
|
#[inline]
|
|
fn do_pan(
|
|
proj: &Projection,
|
|
win: &Window,
|
|
delta: Vec2,
|
|
xform: &mut Transform,
|
|
controller: &mut CameraController,
|
|
) {
|
|
// Normalize the pan distance with respect to the window resolution
|
|
let pan = match proj {
|
|
Projection::Perspective(p) => {
|
|
delta
|
|
* Vec2 {
|
|
x: (p.fov * p.aspect_ratio) / (win.physical_width() as f32),
|
|
y: p.fov / (win.physical_height() as f32),
|
|
}
|
|
}
|
|
_ => delta,
|
|
};
|
|
|
|
// Pan along local XY plane, with the distance scaled by the radius
|
|
let up = xform.up();
|
|
let left = xform.left();
|
|
let delta = (pan.y * up + pan.x * left) * controller.radius;
|
|
|
|
// Update camera translation and focus point
|
|
xform.translation += delta;
|
|
controller.focus += delta;
|
|
}
|
|
|
|
#[inline]
|
|
fn do_zoom(win: &Window, delta: f32, xform: &mut Transform, controller: &mut CameraController) {
|
|
let delta = (delta * 1.5) / (win.physical_height() as f32);
|
|
controller.radius -= controller.radius * delta;
|
|
xform.translation = controller.focus + xform.back() * controller.radius;
|
|
}
|
|
|
|
#[inline]
|
|
fn do_rotate(win: &Window, delta: Vec2, xform: &mut Transform, controller: &mut CameraController) {
|
|
// TODO: Move this somewhere else
|
|
let sensitivity = 0.75;
|
|
|
|
let delta_x = delta.x / (win.physical_width() as f32) * consts::PI * 2.0 * sensitivity;
|
|
let delta_y = delta.y / (win.physical_height() as f32) * consts::PI * sensitivity;
|
|
|
|
let yaw = Quat::from_rotation_y(-delta_x);
|
|
let pitch = Quat::from_rotation_x(-delta_y);
|
|
|
|
// Rotate around global Y and local X
|
|
xform.rotation = yaw * xform.rotation;
|
|
xform.rotation *= pitch;
|
|
|
|
// Update focus point
|
|
controller.focus = xform.translation + xform.forward() * controller.radius;
|
|
}
|