Last active
August 13, 2023 14:57
-
-
Save bg5sbk/9acd5f15e6b30104242607328ddf337d to your computer and use it in GitHub Desktop.
Rapier example for Bevy with Pan + Orbit Camera
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// bevy = "0.11.0" | |
// bevy_rapier3d = { version = "0.22.0", features = [ "simd-stable", "debug-render-3d" ] } | |
// Pan + Orbit camera code: https://bevy-cheatbook.github.io/cookbook/pan-orbit-camera.html | |
// Use the right mouse button to rotate, middle button to pan, scroll wheel to move inwards/outwards. | |
use bevy::prelude::*; | |
use bevy_rapier3d::prelude::*; | |
use bevy::input::mouse::{MouseWheel,MouseMotion}; | |
use bevy::render::camera::Projection; | |
fn main() { | |
App::new() | |
.add_plugins(DefaultPlugins) | |
.add_plugins(RapierPhysicsPlugin::<NoUserData>::default()) | |
.add_plugins(RapierDebugRenderPlugin::default()) | |
.add_systems(Startup, setup_graphics) | |
.add_systems(Startup, setup_physics) | |
.add_systems(Update, pan_orbit_camera) | |
.run(); | |
} | |
fn setup_graphics(mut commands: Commands) { | |
let translation = Vec3::new(-2.0, 2.5, 5.0); | |
let radius = translation.length(); | |
// Add a camera so we can see the debug-render. | |
commands.spawn(( | |
Camera3dBundle { | |
transform: Transform::from_translation(translation) | |
.looking_at(Vec3::ZERO, Vec3::Y), | |
..Default::default() | |
}, | |
PanOrbitCamera { | |
radius, | |
..Default::default() | |
} | |
)); | |
} | |
fn setup_physics(mut commands: Commands) { | |
/* Create the ground. */ | |
commands | |
.spawn(Collider::cuboid(100.0, 0.1, 100.0)) | |
.insert(TransformBundle::from(Transform::from_xyz(0.0, -2.0, 0.0))); | |
/* Create the bouncing ball. */ | |
commands | |
.spawn(RigidBody::Dynamic) | |
.insert(Collider::ball(0.5)) | |
.insert(Restitution::coefficient(0.7)) | |
.insert(TransformBundle::from(Transform::from_xyz(0.0, 4.0, 0.0))); | |
} | |
/* | |
fn print_ball_altitude(positions: Query<&Transform, With<RigidBody>>) { | |
for transform in positions.iter() { | |
println!("Ball altitude: {}", transform.translation.y); | |
} | |
} | |
*/ | |
// ANCHOR: example | |
/// Tags an entity as capable of panning and orbiting. | |
#[derive(Component)] | |
struct PanOrbitCamera { | |
/// The "focus point" to orbit around. It is automatically updated when panning the camera | |
pub focus: Vec3, | |
pub radius: f32, | |
pub upside_down: bool, | |
} | |
impl Default for PanOrbitCamera { | |
fn default() -> Self { | |
PanOrbitCamera { | |
focus: Vec3::ZERO, | |
radius: 5.0, | |
upside_down: false, | |
} | |
} | |
} | |
/// Pan the camera with middle mouse click, zoom with scroll wheel, orbit with right mouse click. | |
fn pan_orbit_camera( | |
mut windows: Query<&mut Window>, | |
mut ev_motion: EventReader<MouseMotion>, | |
mut ev_scroll: EventReader<MouseWheel>, | |
input_mouse: Res<Input<MouseButton>>, | |
mut query: Query<(&mut PanOrbitCamera, &mut Transform, &Projection)>, | |
) { | |
// change input mapping for orbit and panning here | |
let orbit_button = MouseButton::Right; | |
let pan_button = MouseButton::Middle; | |
let mut pan = Vec2::ZERO; | |
let mut rotation_move = Vec2::ZERO; | |
let mut scroll = 0.0; | |
let mut orbit_button_changed = false; | |
if input_mouse.pressed(orbit_button) { | |
for ev in ev_motion.iter() { | |
rotation_move += ev.delta; | |
} | |
} else if input_mouse.pressed(pan_button) { | |
// Pan only if we're not rotating at the moment | |
for ev in ev_motion.iter() { | |
pan += ev.delta; | |
} | |
} | |
for ev in ev_scroll.iter() { | |
scroll += ev.y; | |
} | |
if input_mouse.just_released(orbit_button) || input_mouse.just_pressed(orbit_button) { | |
orbit_button_changed = true; | |
} | |
for (mut pan_orbit, mut transform, projection) in query.iter_mut() { | |
if orbit_button_changed { | |
// only check for upside down when orbiting started or ended this frame | |
// if the camera is "upside" down, panning horizontally would be inverted, so invert the input to make it correct | |
let up = transform.rotation * Vec3::Y; | |
pan_orbit.upside_down = up.y <= 0.0; | |
} | |
let mut any = false; | |
if rotation_move.length_squared() > 0.0 { | |
any = true; | |
let window = get_primary_window_size(&mut windows.single_mut()); | |
let delta_x = { | |
let delta = rotation_move.x / window.x * std::f32::consts::PI * 2.0; | |
if pan_orbit.upside_down { -delta } else { delta } | |
}; | |
let delta_y = rotation_move.y / window.y * std::f32::consts::PI; | |
let yaw = Quat::from_rotation_y(-delta_x); | |
let pitch = Quat::from_rotation_x(-delta_y); | |
transform.rotation = yaw * transform.rotation; // rotate around global y axis | |
transform.rotation = transform.rotation * pitch; // rotate around local x axis | |
} else if pan.length_squared() > 0.0 { | |
any = true; | |
// make panning distance independent of resolution and FOV, | |
let window = get_primary_window_size(&mut windows.single_mut()); | |
if let Projection::Perspective(projection) = projection { | |
pan *= Vec2::new(projection.fov * projection.aspect_ratio, projection.fov) / window; | |
} | |
// translate by local axes | |
let right = transform.rotation * Vec3::X * -pan.x; | |
let up = transform.rotation * Vec3::Y * pan.y; | |
// make panning proportional to distance away from focus point | |
let translation = (right + up) * pan_orbit.radius; | |
pan_orbit.focus += translation; | |
} else if scroll.abs() > 0.0 { | |
any = true; | |
pan_orbit.radius -= scroll * pan_orbit.radius * 0.2; | |
// dont allow zoom to reach zero or you get stuck | |
pan_orbit.radius = f32::max(pan_orbit.radius, 0.05); | |
} | |
if any { | |
// emulating parent/child to make the yaw/y-axis rotation behave like a turntable | |
// parent = x and y rotation | |
// child = z-offset | |
let rot_matrix = Mat3::from_quat(transform.rotation); | |
transform.translation = pan_orbit.focus + rot_matrix.mul_vec3(Vec3::new(0.0, 0.0, pan_orbit.radius)); | |
} | |
} | |
// consume any remaining events, so they don't pile up if we don't need them | |
// (and also to avoid Bevy warning us about not checking events every frame update) | |
ev_motion.clear(); | |
} | |
fn get_primary_window_size(windows: &mut Window) -> Vec2 { | |
let window = Vec2::new(windows.width() as f32, windows.height() as f32); | |
window | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment