pub use crate::ecs::camera::commands::*;
pub use crate::ecs::camera::queries::*;
use crate::ecs::camera::components::{compute_pan_orbit_transform, compute_third_person_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, THIRD_PERSON_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().egui_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().egui_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
.core
.query_entities(crate::ecs::world::CAMERA)
.collect();
for entity in camera_entities {
let Some(camera) = world.core.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) {
if let Some(camera_entity) = world.resources.active_camera
&& world
.core
.entity_has_components(camera_entity, PAN_ORBIT_CAMERA)
{
return;
}
look_camera_system(world);
wasd_keyboard_controls_system(world);
#[cfg(feature = "gamepad")]
gamepad_fly_camera_system(world);
touch_fly_camera_system(world);
}
pub fn first_person_camera_look_system(world: &mut World) {
let Some(camera_entity) = world.resources.active_camera else {
return;
};
let mouse_sensitivity = 0.002_f32;
let gamepad_sensitivity = 2.5_f32;
let pitch_limit = 85_f32.to_radians();
let cursor_locked = world.resources.window.handle.as_ref().is_some_and(|_| true);
let raw_delta = world.resources.input.mouse.raw_mouse_delta;
let has_mouse_input = raw_delta.x.abs() > 0.0 || raw_delta.y.abs() > 0.0;
#[cfg(feature = "gamepad")]
let (gamepad_right_stick_x, gamepad_right_stick_y) = {
let deadzone = 0.15_f32;
if let Some(gamepad) = query_active_gamepad(world) {
let raw_x = gamepad.value(gilrs::Axis::RightStickX);
let raw_y = gamepad.value(gilrs::Axis::RightStickY);
let magnitude = (raw_x * raw_x + raw_y * raw_y).sqrt();
if magnitude > deadzone {
let normalized = (magnitude - deadzone) / (1.0 - deadzone);
(
raw_x * normalized / magnitude,
raw_y * normalized / magnitude,
)
} else {
(0.0, 0.0)
}
} else {
(0.0, 0.0)
}
};
#[cfg(not(feature = "gamepad"))]
let (gamepad_right_stick_x, gamepad_right_stick_y) = (0.0_f32, 0.0_f32);
let has_gamepad_input = gamepad_right_stick_x.abs() > 0.0 || gamepad_right_stick_y.abs() > 0.0;
if !has_mouse_input && !has_gamepad_input {
return;
}
let dt = world.resources.window.timing.delta_time;
let delta = if has_gamepad_input {
Vec2::new(
gamepad_right_stick_x * gamepad_sensitivity * dt,
-gamepad_right_stick_y * gamepad_sensitivity * dt,
)
} else if has_mouse_input && cursor_locked {
raw_delta * mouse_sensitivity
} else {
return;
};
let Some(camera_transform) = world.core.get_local_transform_mut(camera_entity) else {
return;
};
let yaw = nalgebra_glm::quat_angle_axis(-delta.x, &Vec3::new(0.0, 1.0, 0.0));
camera_transform.rotation = yaw * camera_transform.rotation;
let forward =
nalgebra_glm::quat_rotate_vec3(&camera_transform.rotation, &Vec3::new(0.0, 0.0, -1.0));
let current_pitch = forward.y.asin();
let new_pitch = current_pitch - delta.y;
if new_pitch.abs() <= pitch_limit {
let pitch = nalgebra_glm::quat_angle_axis(-delta.y, &Vec3::new(1.0, 0.0, 0.0));
camera_transform.rotation *= pitch;
}
mark_local_transform_dirty(world, camera_entity);
}
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.core.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.core.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.core.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.core.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.core.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.core.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.core.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.core.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.core.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.core.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.core.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.core.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
.core
.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.core.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
.core
.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.core.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
.core
.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.core.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
.core
.entity_has_components(camera_entity, PAN_ORBIT_CAMERA)
{
return;
}
let delta_time = world.resources.window.timing.delta_time;
let Some(pan_orbit) = world.core.get_pan_orbit_camera_mut(camera_entity) else {
return;
};
if !pan_orbit.enabled {
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.core.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
.core
.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.core.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.core.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.core.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.core.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.core.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);
}
}
pub fn third_person_camera_system(world: &mut World) {
third_person_mouse_input(world);
third_person_update_transform(world);
}
fn third_person_mouse_input(world: &mut World) {
let Some(camera_entity) = world.resources.active_camera else {
return;
};
if !world
.core
.entity_has_components(camera_entity, THIRD_PERSON_CAMERA)
{
return;
};
if egui_wants_pointer(world) {
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 right_clicked = mouse_state.contains(MouseState::RIGHT_CLICKED);
let scrolled = mouse_state.contains(MouseState::SCROLLED);
let Some(camera) = world.core.get_third_person_camera_mut(camera_entity) else {
return;
};
if right_clicked {
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;
camera.target_yaw -= delta_x * camera.orbit_sensitivity;
camera.target_pitch = (camera.target_pitch + delta_y * camera.orbit_sensitivity)
.clamp(camera.pitch_lower_limit, camera.pitch_upper_limit);
}
if scrolled {
let zoom_delta = -wheel_delta.y * camera.target_distance * 0.2 * camera.zoom_sensitivity;
camera.target_distance =
(camera.target_distance + zoom_delta).clamp(camera.distance_min, camera.distance_max);
}
}
fn third_person_update_transform(world: &mut World) {
let Some(camera_entity) = world.resources.active_camera else {
return;
};
if !world
.core
.entity_has_components(camera_entity, THIRD_PERSON_CAMERA)
{
return;
};
let delta_time = world.resources.window.timing.delta_time;
let (follow_target, height_offset) = {
let Some(camera) = world.core.get_third_person_camera(camera_entity) else {
return;
};
(camera.follow_target, camera.height_offset)
};
let target_position = follow_target
.and_then(|target| world.core.get_local_transform(target))
.map(|transform| transform.translation + Vec3::new(0.0, height_offset, 0.0))
.unwrap_or_else(|| Vec3::new(0.0, height_offset, 0.0));
let (focus, yaw, pitch, distance, _collision_enabled, _collision_radius) = {
let Some(camera) = world.core.get_third_person_camera_mut(camera_entity) else {
return;
};
let orbit_smoothness = camera.orbit_smoothness;
let zoom_smoothness = camera.zoom_smoothness;
let follow_smoothness = camera.follow_smoothness;
camera.yaw = lerp_and_snap_f32(camera.yaw, camera.target_yaw, orbit_smoothness, delta_time);
camera.pitch = lerp_and_snap_f32(
camera.pitch,
camera.target_pitch,
orbit_smoothness,
delta_time,
);
camera.distance = lerp_and_snap_f32(
camera.distance,
camera.target_distance,
zoom_smoothness,
delta_time,
);
camera.current_focus = lerp_and_snap_vec3(
camera.current_focus,
target_position,
follow_smoothness,
delta_time,
);
(
camera.current_focus,
camera.yaw,
camera.pitch,
camera.distance,
camera.collision_enabled,
camera.collision_radius,
)
};
#[cfg(feature = "physics")]
let distance = if _collision_enabled {
let (ideal_position, _) = compute_third_person_transform(focus, yaw, pitch, distance);
let direction = ideal_position - focus;
let ray_length = nalgebra_glm::length(&direction);
if ray_length > 0.001 {
let ray_dir = direction / ray_length;
let ray = rapier3d::prelude::Ray::new(
rapier3d::na::Point3::new(focus.x, focus.y, focus.z),
rapier3d::na::Vector3::new(ray_dir.x, ray_dir.y, ray_dir.z),
);
let query_pipeline = world.resources.physics.query_pipeline();
if let Some((_handle, toi)) = query_pipeline.cast_ray(&ray, ray_length, true) {
let safe_distance = (toi - _collision_radius).max(0.5);
if safe_distance < distance {
safe_distance
} else {
distance
}
} else {
distance
}
} else {
distance
}
} else {
distance
};
let (position, rotation) = compute_third_person_transform(focus, yaw, pitch, distance);
let Some(local_transform) = world.core.get_local_transform_mut(camera_entity) else {
return;
};
local_transform.translation = position;
local_transform.rotation = rotation;
mark_local_transform_dirty(world, camera_entity);
}