pub use crate::ecs::camera::commands::*;
pub use crate::ecs::camera::queries::*;
use crate::ecs::camera::components::compute_pan_orbit_transform;
#[cfg(feature = "gamepad")]
use crate::ecs::input::queries::query_active_gamepad;
use crate::ecs::transform::commands::mark_local_transform_dirty;
use crate::ecs::world::{
PAN_ORBIT_CAMERA, Vec2, Vec3, World,
components::{Camera, Projection, Smoothing},
resources::MouseState,
};
#[cfg(feature = "egui")]
fn egui_wants_keyboard(world: &World) -> bool {
world
.resources
.user_interface
.state
.as_ref()
.is_some_and(|gui_state| gui_state.egui_ctx().wants_keyboard_input())
}
#[cfg(not(feature = "egui"))]
fn egui_wants_keyboard(_world: &World) -> bool {
false
}
#[cfg(feature = "egui")]
fn egui_wants_pointer(world: &World) -> bool {
world
.resources
.user_interface
.state
.as_ref()
.is_some_and(|gui_state| gui_state.egui_ctx().wants_pointer_input())
}
#[cfg(not(feature = "egui"))]
fn egui_wants_pointer(_world: &World) -> bool {
false
}
pub fn update_camera_aspect_ratios(world: &mut World) {
let _span = tracing::info_span!("camera_aspect").entered();
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::WindowExtWebSys;
if let Some(window_handle) = &world.resources.window.handle
&& let Some(canvas) = window_handle.canvas()
{
world.resources.window.cached_viewport_size =
Some((canvas.client_width() as u32, canvas.client_height() as u32));
}
}
let Some((width, height)) = world.resources.window.cached_viewport_size else {
return;
};
let aspect_ratio = width as f32 / height.max(1) as f32;
let camera_entities: Vec<_> = world.query_entities(crate::ecs::world::CAMERA).collect();
for entity in camera_entities {
let Some(camera) = world.get_camera_mut(entity) else {
continue;
};
if let Projection::Perspective(perspective) = &mut camera.projection {
perspective.aspect_ratio = Some(aspect_ratio);
}
}
}
pub fn fly_camera_system(world: &mut World) {
look_camera_system(world);
wasd_keyboard_controls_system(world);
#[cfg(feature = "gamepad")]
gamepad_fly_camera_system(world);
touch_fly_camera_system(world);
}
fn look_camera_system(world: &mut World) {
use std::sync::atomic::{AtomicBool, Ordering};
static CAMERA_HAS_CURSOR: AtomicBool = AtomicBool::new(false);
let Some(camera_entity) = world.resources.active_camera else {
return;
};
let delta_time = world.resources.window.timing.delta_time;
let (right, up) = {
let Some(local_transform) = world.get_local_transform(camera_entity) else {
return;
};
(local_transform.right_vector(), local_transform.up_vector())
};
let right_clicked = world
.resources
.input
.mouse
.state
.contains(MouseState::RIGHT_CLICKED);
if right_clicked {
let already_grabbed = CAMERA_HAS_CURSOR.load(Ordering::Relaxed);
if !already_grabbed {
if let Some(window_handle) = &world.resources.window.handle {
if window_handle
.set_cursor_grab(winit::window::CursorGrabMode::Locked)
.is_err()
{
let _ = window_handle.set_cursor_grab(winit::window::CursorGrabMode::Confined);
}
window_handle.set_cursor_visible(false);
}
CAMERA_HAS_CURSOR.store(true, Ordering::Relaxed);
}
let raw_delta = world.resources.input.mouse.raw_mouse_delta;
let Some(camera) = world.get_camera_mut(camera_entity) else {
return;
};
let Some(smoothing) = camera.smoothing.as_mut() else {
return;
};
let smoothing_factor = if smoothing.mouse_smoothness > 0.0 {
1.0 - smoothing.mouse_smoothness.powi(7).powf(delta_time)
} else {
1.0
};
smoothing.smoothed_mouse_delta = smoothing.smoothed_mouse_delta * (1.0 - smoothing_factor)
+ raw_delta * smoothing_factor;
let pixels_to_radians = (std::f32::consts::PI / 1000.0) * smoothing.mouse_dpi_scale;
let mut delta =
smoothing.smoothed_mouse_delta * smoothing.mouse_sensitivity * pixels_to_radians;
delta.x *= -1.0;
delta.y *= -1.0;
let Some(local_transform) = world.get_local_transform_mut(camera_entity) else {
return;
};
let yaw = nalgebra_glm::quat_angle_axis(delta.x, &Vec3::y());
local_transform.rotation = yaw * local_transform.rotation;
let forward = local_transform.forward_vector();
let current_pitch = forward.y.asin();
let new_pitch = current_pitch + delta.y;
if new_pitch.abs() <= 89_f32.to_radians() {
let pitch = nalgebra_glm::quat_angle_axis(delta.y, &Vec3::x());
local_transform.rotation *= pitch;
}
mark_local_transform_dirty(world, camera_entity);
} else {
if CAMERA_HAS_CURSOR.load(Ordering::Relaxed) {
if let Some(window_handle) = &world.resources.window.handle {
let _ = window_handle.set_cursor_grab(winit::window::CursorGrabMode::None);
window_handle.set_cursor_visible(true);
}
CAMERA_HAS_CURSOR.store(false, Ordering::Relaxed);
}
if let Some(Camera {
smoothing:
Some(Smoothing {
smoothed_mouse_delta,
mouse_smoothness,
..
}),
..
}) = world.get_camera_mut(camera_entity)
{
let decay_smoothness = (*mouse_smoothness * 0.5).max(0.01);
let smoothing_factor = 1.0 - decay_smoothness.powi(7).powf(delta_time);
*smoothed_mouse_delta =
*smoothed_mouse_delta * (1.0 - smoothing_factor) + Vec2::zeros() * smoothing_factor;
}
}
if world
.resources
.input
.mouse
.state
.contains(MouseState::MIDDLE_CLICKED)
{
let mut delta =
world.resources.input.mouse.position_delta * world.resources.window.timing.delta_time;
delta.x *= -1.0;
delta.y *= -1.0;
let Some(local_transform) = world.get_local_transform_mut(camera_entity) else {
return;
};
let translation_right = right * delta.x;
let translation_up = up * delta.y;
local_transform.translation += translation_right;
local_transform.translation += translation_up;
let changed = translation_right.magnitude() > 0.0 || translation_up.magnitude() > 0.0;
if changed {
mark_local_transform_dirty(world, camera_entity);
}
}
}
fn wasd_keyboard_controls_system(world: &mut World) {
if egui_wants_keyboard(world) {
return;
}
let Some(camera_entity) = world.resources.active_camera else {
return;
};
let delta_time = world.resources.window.timing.delta_time;
let (
left_key_pressed,
right_key_pressed,
forward_key_pressed,
backward_key_pressed,
up_key_pressed,
shift_pressed,
) = {
let keyboard = &world.resources.input.keyboard;
(
keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyA),
keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyD),
keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyW),
keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyS),
keyboard.is_key_pressed(winit::keyboard::KeyCode::Space),
keyboard.is_key_pressed(winit::keyboard::KeyCode::ShiftLeft)
|| keyboard.is_key_pressed(winit::keyboard::KeyCode::ShiftRight),
)
};
let base_speed = if shift_pressed { 60.0 } else { 20.0 };
let mut target_movement = Vec3::zeros();
if forward_key_pressed {
target_movement.z += 1.0;
}
if backward_key_pressed {
target_movement.z -= 1.0;
}
if left_key_pressed {
target_movement.x -= 1.0;
}
if right_key_pressed {
target_movement.x += 1.0;
}
if up_key_pressed {
target_movement.y += 1.0;
}
if target_movement.magnitude() > 0.0 {
target_movement = target_movement.normalize();
}
let Some(camera) = world.get_camera_mut(camera_entity) else {
return;
};
let Some(smoothing) = camera.smoothing.as_mut() else {
return;
};
let smoothing_factor = if smoothing.keyboard_smoothness > 0.0 {
1.0 - smoothing.keyboard_smoothness.powi(7).powf(delta_time)
} else {
1.0
};
smoothing.smoothed_movement =
smoothing.smoothed_movement * (1.0 - smoothing_factor) + target_movement * smoothing_factor;
let movement = smoothing.smoothed_movement;
let Some(local_transform) = world.get_local_transform_mut(camera_entity) else {
return;
};
let forward = local_transform.forward_vector();
let right = local_transform.right_vector();
let up = local_transform.up_vector();
let forward_translation = forward * movement.z * base_speed * delta_time;
let right_translation = right * movement.x * base_speed * delta_time;
let up_translation = up * movement.y * base_speed * delta_time;
local_transform.translation += forward_translation;
local_transform.translation += right_translation;
local_transform.translation += up_translation;
let changed = forward_translation.magnitude() > 0.0
|| right_translation.magnitude() > 0.0
|| up_translation.magnitude() > 0.0;
if changed {
mark_local_transform_dirty(world, camera_entity);
}
}
#[cfg(feature = "gamepad")]
fn apply_deadzone(value: f32, deadzone: f32) -> f32 {
if value.abs() > deadzone {
let sign = value.signum();
let compensated = (value.abs() - deadzone) / (1.0 - deadzone);
sign * compensated
} else {
0.0
}
}
#[cfg(feature = "gamepad")]
fn gamepad_fly_camera_system(world: &mut World) {
if world.resources.input.gamepad.gilrs.is_none() {
return;
}
let Some(camera_entity) = world.resources.active_camera else {
return;
};
let delta_time = world.resources.window.timing.delta_time;
let base_speed = 20.0;
let (
left_trigger2_pressed,
right_trigger2_pressed,
left_stick_x_axis_data,
left_stick_y_axis_data,
right_stick_x_axis_data,
right_stick_y_axis_data,
select_pressed,
) = {
let Some(gamepad) = query_active_gamepad(world) else {
return;
};
(
gamepad.is_pressed(gilrs::Button::LeftTrigger2),
gamepad.is_pressed(gilrs::Button::RightTrigger2),
gamepad.axis_data(gilrs::Axis::LeftStickX).cloned(),
gamepad.axis_data(gilrs::Axis::LeftStickY).cloned(),
gamepad.axis_data(gilrs::Axis::RightStickX).cloned(),
gamepad.axis_data(gilrs::Axis::RightStickY).cloned(),
gamepad.is_pressed(gilrs::Button::Select),
)
};
if select_pressed {
world.resources.window.should_exit = true;
}
let Some(camera) = world.get_camera_mut(camera_entity) else {
return;
};
let Some(smoothing) = camera.smoothing.as_mut() else {
return;
};
let deadzone = smoothing.gamepad_deadzone;
let mut target_rotation = Vec2::zeros();
if let Some(axis_data) = right_stick_x_axis_data {
target_rotation.x = apply_deadzone(axis_data.value(), deadzone);
}
if let Some(axis_data) = right_stick_y_axis_data {
target_rotation.y = apply_deadzone(axis_data.value(), deadzone);
}
let smoothing_factor = if smoothing.gamepad_smoothness > 0.0 {
1.0 - smoothing.gamepad_smoothness.powi(7).powf(delta_time)
} else {
1.0
};
smoothing.smoothed_gamepad_input = smoothing.smoothed_gamepad_input * (1.0 - smoothing_factor)
+ target_rotation * smoothing_factor;
let rotation_speed = smoothing.smoothed_gamepad_input * smoothing.gamepad_sensitivity * 2.0;
let Some(transform) = world.get_local_transform_mut(camera_entity) else {
return;
};
let forward = transform.forward_vector();
let right = transform.right_vector();
let up = transform.up_vector();
let mut changed = false;
#[cfg(target_arch = "wasm32")]
let (up_trigger_pressed, down_trigger_pressed) =
(left_trigger2_pressed, right_trigger2_pressed);
#[cfg(not(target_arch = "wasm32"))]
let (up_trigger_pressed, down_trigger_pressed) =
(right_trigger2_pressed, left_trigger2_pressed);
if up_trigger_pressed {
let translation = up * base_speed * delta_time;
changed |= translation.magnitude() > 0.0;
transform.translation += translation;
}
if down_trigger_pressed {
let translation = up * base_speed * delta_time;
changed |= translation.magnitude() > 0.0;
transform.translation -= translation;
}
if let Some(axis_data) = left_stick_x_axis_data {
let compensated_value = apply_deadzone(axis_data.value(), deadzone);
let translation = right * compensated_value * base_speed * delta_time;
changed |= translation.magnitude() > 0.0;
transform.translation += translation;
}
if let Some(axis_data) = left_stick_y_axis_data {
let compensated_value = apply_deadzone(axis_data.value(), deadzone);
let translation = forward * compensated_value * base_speed * delta_time;
changed |= translation.magnitude() > 0.0;
transform.translation += translation;
}
if rotation_speed.x.abs() > 0.001 {
let yaw = nalgebra_glm::quat_angle_axis(-(rotation_speed.x * delta_time), &Vec3::y());
transform.rotation = yaw * transform.rotation;
changed = true;
}
if rotation_speed.y.abs() > 0.001 {
let forward = transform.forward_vector();
let current_pitch = forward.y.asin();
let new_pitch = current_pitch + rotation_speed.y * delta_time;
if new_pitch.abs() <= 89_f32.to_radians() {
let pitch = nalgebra_glm::quat_angle_axis(rotation_speed.y * delta_time, &Vec3::x());
transform.rotation *= pitch;
}
changed = true;
}
if changed {
mark_local_transform_dirty(world, camera_entity);
}
}
fn touch_fly_camera_system(world: &mut World) {
use crate::ecs::input::resources::TouchGesture;
let Some(camera_entity) = world.resources.active_camera else {
return;
};
if egui_wants_pointer(world) {
return;
}
let gesture = world.resources.input.touch.gesture;
let delta_time = world.resources.window.timing.delta_time;
let base_speed = 20.0;
match gesture {
TouchGesture::SingleDrag { delta } => {
let pixels_to_radians = std::f32::consts::PI / 500.0;
let rotation_delta = delta * pixels_to_radians;
let Some(local_transform) = world.get_local_transform_mut(camera_entity) else {
return;
};
let yaw = nalgebra_glm::quat_angle_axis(-rotation_delta.x, &Vec3::y());
local_transform.rotation = yaw * local_transform.rotation;
let forward = local_transform.forward_vector();
let current_pitch = forward.y.asin();
let new_pitch = current_pitch - rotation_delta.y;
if new_pitch.abs() <= 89_f32.to_radians() {
let pitch = nalgebra_glm::quat_angle_axis(-rotation_delta.y, &Vec3::x());
local_transform.rotation *= pitch;
}
mark_local_transform_dirty(world, camera_entity);
}
TouchGesture::TwoFingerDrag { delta } => {
let Some(local_transform) = world.get_local_transform_mut(camera_entity) else {
return;
};
let right = local_transform.right_vector();
let up = local_transform.up_vector();
let pan_scale = delta_time * base_speed * 0.1;
let translation_right = right * -delta.x * pan_scale;
let translation_up = up * delta.y * pan_scale;
local_transform.translation += translation_right;
local_transform.translation += translation_up;
let changed = translation_right.magnitude() > 0.0 || translation_up.magnitude() > 0.0;
if changed {
mark_local_transform_dirty(world, camera_entity);
}
}
TouchGesture::Pinch { delta, .. } => {
let Some(local_transform) = world.get_local_transform_mut(camera_entity) else {
return;
};
let forward = local_transform.forward_vector();
let move_speed = delta * delta_time * base_speed * 0.05;
local_transform.translation += forward * move_speed;
if move_speed.abs() > 0.0 {
mark_local_transform_dirty(world, camera_entity);
}
}
TouchGesture::None => {}
}
}
pub fn pan_orbit_camera_system(world: &mut World) {
pan_orbit_mouse_input(world);
#[cfg(feature = "gamepad")]
pan_orbit_gamepad_input(world);
pan_orbit_touch_input(world);
pan_orbit_update_transform(world);
}
fn pan_orbit_mouse_input(world: &mut World) {
use crate::ecs::camera::components::{PanOrbitButton, PanOrbitModifier};
let Some(camera_entity) = world.resources.active_camera else {
return;
};
if !world.entity_has_components(camera_entity, PAN_ORBIT_CAMERA) {
return;
}
let mouse_in_viewport =
if let Some(viewport_rect) = &world.resources.window.active_viewport_rect {
let mouse_pos = world.resources.input.mouse.position;
viewport_rect.contains(mouse_pos)
} else {
true
};
if !mouse_in_viewport {
return;
}
if egui_wants_pointer(world) && world.resources.window.active_viewport_rect.is_none() {
return;
}
if world.resources.user_interface.hud_wants_pointer {
return;
}
let mouse_state = world.resources.input.mouse.state;
let position_delta = world.resources.input.mouse.position_delta;
let wheel_delta = world.resources.input.mouse.wheel_delta;
let window_size = if let Some(window_handle) = &world.resources.window.handle {
let size = window_handle.inner_size();
Vec2::new(size.width as f32, size.height as f32)
} else {
Vec2::new(1920.0, 1080.0)
};
let left_clicked = mouse_state.contains(MouseState::LEFT_CLICKED);
let right_clicked = mouse_state.contains(MouseState::RIGHT_CLICKED);
let middle_clicked = mouse_state.contains(MouseState::MIDDLE_CLICKED);
let scrolled = mouse_state.contains(MouseState::SCROLLED);
let (shift_pressed, ctrl_pressed, alt_pressed) = {
let keyboard = &world.resources.input.keyboard;
(
keyboard.is_key_pressed(winit::keyboard::KeyCode::ShiftLeft)
|| keyboard.is_key_pressed(winit::keyboard::KeyCode::ShiftRight),
keyboard.is_key_pressed(winit::keyboard::KeyCode::ControlLeft)
|| keyboard.is_key_pressed(winit::keyboard::KeyCode::ControlRight),
keyboard.is_key_pressed(winit::keyboard::KeyCode::AltLeft)
|| keyboard.is_key_pressed(winit::keyboard::KeyCode::AltRight),
)
};
let Some(pan_orbit) = world.get_pan_orbit_camera_mut(camera_entity) else {
return;
};
if !pan_orbit.enabled {
return;
}
let is_button_pressed = |button: PanOrbitButton| -> bool {
match button {
PanOrbitButton::Left => left_clicked,
PanOrbitButton::Right => right_clicked,
PanOrbitButton::Middle => middle_clicked,
}
};
let is_modifier_satisfied = |modifier: Option<PanOrbitModifier>| -> bool {
match modifier {
None => true,
Some(PanOrbitModifier::Shift) => shift_pressed,
Some(PanOrbitModifier::Control) => ctrl_pressed,
Some(PanOrbitModifier::Alt) => alt_pressed,
}
};
let orbit_modifier_active = is_modifier_satisfied(pan_orbit.modifier_orbit);
let pan_modifier_active = is_modifier_satisfied(pan_orbit.modifier_pan);
let orbit_button_pressed = is_button_pressed(pan_orbit.button_orbit);
let pan_button_pressed = is_button_pressed(pan_orbit.button_pan);
let wants_zoom_drag = ctrl_pressed && middle_clicked;
let wants_orbit = orbit_modifier_active
&& orbit_button_pressed
&& !wants_zoom_drag
&& !(pan_orbit.modifier_pan.is_some() && pan_modifier_active);
let wants_pan = pan_modifier_active
&& pan_button_pressed
&& !wants_zoom_drag
&& !(pan_orbit.modifier_orbit.is_some() && orbit_modifier_active);
if wants_zoom_drag {
let zoom_delta =
-position_delta.y * pan_orbit.target_radius * 0.01 * pan_orbit.zoom_sensitivity;
pan_orbit.target_radius += zoom_delta;
pan_orbit.target_radius = pan_orbit.target_radius.max(pan_orbit.zoom_lower_limit);
if let Some(upper) = pan_orbit.zoom_upper_limit {
pan_orbit.target_radius = pan_orbit.target_radius.min(upper);
}
}
if wants_orbit {
let delta_x = (position_delta.x / window_size.x) * std::f32::consts::PI * 2.0;
let delta_y = (position_delta.y / window_size.y) * std::f32::consts::PI;
let delta_x = if pan_orbit.is_upside_down {
-delta_x
} else {
delta_x
};
pan_orbit.target_yaw -= delta_x * pan_orbit.orbit_sensitivity;
pan_orbit.target_pitch += delta_y * pan_orbit.orbit_sensitivity;
if pan_orbit.allow_upside_down {
pan_orbit.target_pitch %= std::f32::consts::PI * 2.0;
} else {
pan_orbit.target_pitch = pan_orbit
.target_pitch
.clamp(pan_orbit.pitch_lower_limit, pan_orbit.pitch_upper_limit);
}
}
if wants_pan {
let (_, rotation) = compute_pan_orbit_transform(
pan_orbit.target_focus,
pan_orbit.target_yaw,
pan_orbit.target_pitch,
pan_orbit.target_radius,
);
let right = nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::x());
let up = nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::y());
let pan_scale = pan_orbit.target_radius * pan_orbit.pan_sensitivity * 0.002;
let pan_right = right * -position_delta.x * pan_scale;
let pan_up = up * position_delta.y * pan_scale;
pan_orbit.target_focus += pan_right + pan_up;
}
if scrolled {
let zoom_delta =
-wheel_delta.y * pan_orbit.target_radius * 0.2 * pan_orbit.zoom_sensitivity;
pan_orbit.target_radius += zoom_delta;
pan_orbit.target_radius = pan_orbit.target_radius.max(pan_orbit.zoom_lower_limit);
if let Some(upper) = pan_orbit.zoom_upper_limit {
pan_orbit.target_radius = pan_orbit.target_radius.min(upper);
}
}
if !orbit_button_pressed {
pan_orbit.is_upside_down = pan_orbit.target_pitch.abs() > std::f32::consts::FRAC_PI_2;
}
}
#[cfg(feature = "gamepad")]
fn pan_orbit_gamepad_input(world: &mut World) {
if world.resources.input.gamepad.gilrs.is_none() {
return;
}
let Some(camera_entity) = world.resources.active_camera else {
return;
};
if !world.entity_has_components(camera_entity, PAN_ORBIT_CAMERA) {
return;
}
let delta_time = world.resources.window.timing.delta_time;
let (left_stick_x, left_stick_y, right_stick_x, right_stick_y, left_trigger, right_trigger) = {
let Some(gamepad) = query_active_gamepad(world) else {
return;
};
(
gamepad
.axis_data(gilrs::Axis::LeftStickX)
.map(|a| a.value())
.unwrap_or(0.0),
gamepad
.axis_data(gilrs::Axis::LeftStickY)
.map(|a| a.value())
.unwrap_or(0.0),
gamepad
.axis_data(gilrs::Axis::RightStickX)
.map(|a| a.value())
.unwrap_or(0.0),
gamepad
.axis_data(gilrs::Axis::RightStickY)
.map(|a| a.value())
.unwrap_or(0.0),
if gamepad.is_pressed(gilrs::Button::LeftTrigger2) {
1.0
} else {
0.0
},
if gamepad.is_pressed(gilrs::Button::RightTrigger2) {
1.0
} else {
0.0
},
)
};
let Some(pan_orbit) = world.get_pan_orbit_camera_mut(camera_entity) else {
return;
};
if !pan_orbit.enabled {
return;
}
let deadzone = pan_orbit.gamepad_deadzone;
let target_orbit = Vec2::new(
apply_deadzone(right_stick_x, deadzone),
apply_deadzone(right_stick_y, deadzone),
);
let target_pan = Vec2::new(
apply_deadzone(left_stick_x, deadzone),
apply_deadzone(left_stick_y, deadzone),
);
let smoothing_factor = if pan_orbit.gamepad_smoothness > 0.0 {
1.0 - pan_orbit.gamepad_smoothness.powi(7).powf(delta_time)
} else {
1.0
};
pan_orbit.smoothed_gamepad_orbit = pan_orbit.smoothed_gamepad_orbit * (1.0 - smoothing_factor)
+ target_orbit * smoothing_factor;
pan_orbit.smoothed_gamepad_pan =
pan_orbit.smoothed_gamepad_pan * (1.0 - smoothing_factor) + target_pan * smoothing_factor;
let orbit_input = pan_orbit.smoothed_gamepad_orbit;
let pan_input = pan_orbit.smoothed_gamepad_pan;
if orbit_input.magnitude() > 0.001 {
pan_orbit.target_yaw -= orbit_input.x * pan_orbit.gamepad_orbit_sensitivity * delta_time;
pan_orbit.target_pitch += orbit_input.y * pan_orbit.gamepad_orbit_sensitivity * delta_time;
pan_orbit.target_pitch = pan_orbit
.target_pitch
.clamp(pan_orbit.pitch_lower_limit, pan_orbit.pitch_upper_limit);
}
if pan_input.magnitude() > 0.001 {
let (_, rotation) = compute_pan_orbit_transform(
pan_orbit.target_focus,
pan_orbit.target_yaw,
pan_orbit.target_pitch,
pan_orbit.target_radius,
);
let right = nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::x());
let forward_flat = {
let forward = nalgebra_glm::quat_rotate_vec3(&rotation, &-Vec3::z());
Vec3::new(forward.x, 0.0, forward.z).normalize()
};
let pan_scale =
pan_orbit.target_radius * pan_orbit.gamepad_pan_sensitivity * delta_time * 0.1;
pan_orbit.target_focus += right * pan_input.x * pan_scale;
pan_orbit.target_focus += forward_flat * pan_input.y * pan_scale;
}
let zoom_input: f32 = right_trigger - left_trigger;
if zoom_input.abs() > 0.1 {
let zoom_delta = zoom_input
* pan_orbit.target_radius
* pan_orbit.gamepad_zoom_sensitivity
* delta_time
* 0.1;
pan_orbit.target_radius += zoom_delta;
pan_orbit.target_radius = pan_orbit.target_radius.max(pan_orbit.zoom_lower_limit);
if let Some(upper) = pan_orbit.zoom_upper_limit {
pan_orbit.target_radius = pan_orbit.target_radius.min(upper);
}
}
}
fn pan_orbit_touch_input(world: &mut World) {
use crate::ecs::input::resources::TouchGesture;
let Some(camera_entity) = world.resources.active_camera else {
return;
};
if !world.entity_has_components(camera_entity, PAN_ORBIT_CAMERA) {
return;
}
if egui_wants_pointer(world) {
return;
}
let gesture = world.resources.input.touch.gesture;
let window_size = if let Some(window_handle) = &world.resources.window.handle {
let size = window_handle.inner_size();
Vec2::new(size.width as f32, size.height as f32)
} else {
Vec2::new(1920.0, 1080.0)
};
let Some(pan_orbit) = world.get_pan_orbit_camera_mut(camera_entity) else {
return;
};
if !pan_orbit.enabled {
return;
}
match gesture {
TouchGesture::SingleDrag { delta } => {
let delta_x = delta.x / window_size.x * std::f32::consts::PI * 2.0;
let delta_y = delta.y / window_size.y * std::f32::consts::PI;
pan_orbit.target_yaw -= delta_x * pan_orbit.orbit_sensitivity;
pan_orbit.target_pitch += delta_y * pan_orbit.orbit_sensitivity;
pan_orbit.target_pitch = pan_orbit
.target_pitch
.clamp(pan_orbit.pitch_lower_limit, pan_orbit.pitch_upper_limit);
}
TouchGesture::TwoFingerDrag { delta } => {
let (_, rotation) = compute_pan_orbit_transform(
pan_orbit.target_focus,
pan_orbit.target_yaw,
pan_orbit.target_pitch,
pan_orbit.target_radius,
);
let right = nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::x());
let up = nalgebra_glm::quat_rotate_vec3(&rotation, &Vec3::y());
let pan_scale = pan_orbit.target_radius * pan_orbit.pan_sensitivity * 0.002;
let pan_right = right * -delta.x * pan_scale;
let pan_up = up * delta.y * pan_scale;
pan_orbit.target_focus += pan_right + pan_up;
}
TouchGesture::Pinch { delta, .. } => {
let zoom_delta = -delta * pan_orbit.target_radius * 0.005 * pan_orbit.zoom_sensitivity;
pan_orbit.target_radius += zoom_delta;
pan_orbit.target_radius = pan_orbit.target_radius.max(pan_orbit.zoom_lower_limit);
if let Some(upper) = pan_orbit.zoom_upper_limit {
pan_orbit.target_radius = pan_orbit.target_radius.min(upper);
}
}
TouchGesture::None => {}
}
}
fn pan_orbit_update_transform(world: &mut World) {
let Some(camera_entity) = world.resources.active_camera else {
return;
};
if !world.entity_has_components(camera_entity, PAN_ORBIT_CAMERA) {
return;
}
let delta_time = world.resources.window.timing.delta_time;
let Some(pan_orbit) = world.get_pan_orbit_camera_mut(camera_entity) else {
return;
};
if !pan_orbit.enabled {
return;
}
let needs_update = pan_orbit.yaw != pan_orbit.target_yaw
|| pan_orbit.pitch != pan_orbit.target_pitch
|| pan_orbit.radius != pan_orbit.target_radius
|| pan_orbit.focus != pan_orbit.target_focus;
if !needs_update {
return;
}
let orbit_smoothness = pan_orbit.orbit_smoothness;
let pan_smoothness = pan_orbit.pan_smoothness;
let zoom_smoothness = pan_orbit.zoom_smoothness;
pan_orbit.yaw = lerp_and_snap_f32(
pan_orbit.yaw,
pan_orbit.target_yaw,
orbit_smoothness,
delta_time,
);
pan_orbit.pitch = lerp_and_snap_f32(
pan_orbit.pitch,
pan_orbit.target_pitch,
orbit_smoothness,
delta_time,
);
pan_orbit.radius = lerp_and_snap_f32(
pan_orbit.radius,
pan_orbit.target_radius,
zoom_smoothness,
delta_time,
);
pan_orbit.focus = lerp_and_snap_vec3(
pan_orbit.focus,
pan_orbit.target_focus,
pan_smoothness,
delta_time,
);
let yaw = pan_orbit.yaw;
let pitch = pan_orbit.pitch;
let radius = pan_orbit.radius;
let focus = pan_orbit.focus;
let (position, rotation) = compute_pan_orbit_transform(focus, yaw, pitch, radius);
let Some(local_transform) = world.get_local_transform_mut(camera_entity) else {
return;
};
local_transform.rotation = rotation;
local_transform.translation = position;
mark_local_transform_dirty(world, camera_entity);
}
const SNAP_EPSILON: f32 = 0.001;
fn lerp_and_snap_f32(from: f32, to: f32, smoothness: f32, delta_time: f32) -> f32 {
let t = smoothness.powi(7);
let result = from + (to - from) * (1.0 - t.powf(delta_time));
if smoothness < 1.0 && (result - to).abs() < SNAP_EPSILON {
to
} else {
result
}
}
fn lerp_and_snap_vec3(from: Vec3, to: Vec3, smoothness: f32, delta_time: f32) -> Vec3 {
let t = smoothness.powi(7);
let result = from + (to - from) * (1.0 - t.powf(delta_time));
if smoothness < 1.0 && (result - to).magnitude() < SNAP_EPSILON {
to
} else {
result
}
}
pub fn ortho_camera_system(world: &mut World) {
let Some(camera_entity) = world.resources.active_camera else {
return;
};
let is_ortho = world
.get_camera(camera_entity)
.is_some_and(|camera| matches!(camera.projection, Projection::Orthographic(_)));
if !is_ortho {
return;
}
if egui_wants_keyboard(world) {
return;
}
let delta_time = world.resources.window.timing.delta_time;
if let Some(window_handle) = &world.resources.window.handle {
let size = window_handle.inner_size();
let width = size.width as f32;
let height = size.height as f32;
if let Some(camera) = world.get_camera_mut(camera_entity)
&& let Projection::Orthographic(ref mut ortho) = camera.projection
{
ortho.y_mag = ortho.x_mag * (height / width);
}
}
let (x_mag, viewport_width) = {
let camera = world.get_camera(camera_entity);
let x_mag = camera
.and_then(|camera| match camera.projection {
Projection::Orthographic(ortho) => Some(ortho.x_mag),
_ => None,
})
.unwrap_or(960.0);
let viewport_width = world
.resources
.window
.handle
.as_ref()
.map(|handle| handle.inner_size().width as f32)
.unwrap_or(1920.0);
(x_mag, viewport_width)
};
let zoom = viewport_width / (2.0 * x_mag);
let keyboard = &world.resources.input.keyboard;
let left = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyA)
|| keyboard.is_key_pressed(winit::keyboard::KeyCode::ArrowLeft);
let right = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyD)
|| keyboard.is_key_pressed(winit::keyboard::KeyCode::ArrowRight);
let up = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyW)
|| keyboard.is_key_pressed(winit::keyboard::KeyCode::ArrowUp);
let down = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyS)
|| keyboard.is_key_pressed(winit::keyboard::KeyCode::ArrowDown);
let rotate_left = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyQ);
let rotate_right = keyboard.is_key_pressed(winit::keyboard::KeyCode::KeyE);
let wheel_delta = world.resources.input.mouse.wheel_delta.y;
let pan_speed = 400.0 / zoom;
let mut movement = Vec2::new(0.0, 0.0);
if left {
movement.x -= pan_speed * delta_time;
}
if right {
movement.x += pan_speed * delta_time;
}
if up {
movement.y += pan_speed * delta_time;
}
if down {
movement.y -= pan_speed * delta_time;
}
let mut changed = false;
if (movement.x != 0.0 || movement.y != 0.0)
&& let Some(transform) = world.get_local_transform_mut(camera_entity)
{
let right_vec = transform.right_vector();
let up_vec = transform.up_vector();
transform.translation += right_vec * movement.x + up_vec * movement.y;
changed = true;
}
if rotate_left || rotate_right {
let rotation_speed = 2.0 * delta_time;
if let Some(transform) = world.get_local_transform_mut(camera_entity) {
if rotate_left {
let rotation = nalgebra_glm::quat_angle_axis(-rotation_speed, &Vec3::z());
transform.rotation = rotation * transform.rotation;
}
if rotate_right {
let rotation = nalgebra_glm::quat_angle_axis(rotation_speed, &Vec3::z());
transform.rotation = rotation * transform.rotation;
}
changed = true;
}
}
if wheel_delta != 0.0 {
if egui_wants_pointer(world) {
return;
}
if let Some(camera) = world.get_camera_mut(camera_entity)
&& let Projection::Orthographic(ref mut ortho) = camera.projection
{
let zoom_factor = 1.0 / (1.0 + wheel_delta * 0.1);
ortho.x_mag *= zoom_factor;
let min_x_mag = viewport_width / (2.0 * 10.0);
let max_x_mag = viewport_width / (2.0 * 0.1);
ortho.x_mag = ortho.x_mag.clamp(min_x_mag, max_x_mag);
changed = true;
}
}
if changed {
mark_local_transform_dirty(world, camera_entity);
}
}