mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-14 23:53:14 +00:00
Refactoring, reorganization and cleanup (#7)
### New `math` module * Traits for error-tolerant floating-point comparisons * Traits for interpolation and easing * Bevy plugin to manage 3D interpolations with ECS components ### `skybox` module refactor * ECS resource to enable external skybox selection/updates * System run conditions for more idiomatic/ECS-friendly control flow ### Camera controller refactor * Renamed from `orbit_cam` to `camera_controller` * Now delegates to the new interpolation plugin for WASD motion smoothing * Simple API for attaching the `CameraController` component to the camera from another module -- this resolves an awkward separation-of-concerns situation where we had to configure a bunch of render-quality settings in the camera controller module ### Scene module refactor * Renamed from `test_scene` to `scene` * This module is now responsible for configuring the skybox * This module is now responsible for spawning the camera and configuring the render settings
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
use std::f32::consts;
|
||||
|
||||
use bevy::{
|
||||
prelude::*,
|
||||
window::{CursorGrabMode, PrimaryWindow},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
input::{Action, CameraMotion, InputAction, Lifecycle},
|
||||
math::{Easing, 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.before(InterpolationSystem),
|
||||
camera_control_physical.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(_)))
|
||||
)
|
||||
}) {
|
||||
#[allow(irrefutable_let_patterns)] // Will eventually be more variants
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
|
||||
#[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;
|
||||
}
|
||||
Reference in New Issue
Block a user