From 7cf8854f611404edc00439a95b09f13f5013e73f Mon Sep 17 00:00:00 2001 From: Meyer Zinn Date: Mon, 1 May 2023 19:09:08 -0500 Subject: [PATCH 1/4] wip physics --- src/main.rs | 2 +- src/voxel/voxel.rs | 4 + src/voxel/world/mod.rs | 4 +- src/voxel/world/physics.rs | 226 +++++++++++++++++++++++++++++++++++++ src/voxel/world/player.rs | 49 +++++--- 5 files changed, 270 insertions(+), 15 deletions(-) create mode 100644 src/voxel/world/physics.rs diff --git a/src/main.rs b/src/main.rs index 6877de0..a001539 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,7 +30,7 @@ fn setup(mut cmds: Commands) { transform: Transform::from_xyz(2.0, 160.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y), ..Default::default() }) - .insert(voxel::player::PlayerController::default()) + .insert(voxel::player::PlayerBundle::default()) .insert(Fxaa::default()) .insert(bevy_atmosphere::plugin::AtmosphereCamera::default()); diff --git a/src/voxel/voxel.rs b/src/voxel/voxel.rs index 5dae8f6..83ad229 100644 --- a/src/voxel/voxel.rs +++ b/src/voxel/voxel.rs @@ -5,6 +5,10 @@ pub struct Voxel(pub u8); impl Voxel { pub const EMPTY_VOXEL: Self = Self(0); + + pub fn collidable(self) -> bool { + !(self == Self::EMPTY_VOXEL) + } } impl Default for Voxel { diff --git a/src/voxel/world/mod.rs b/src/voxel/world/mod.rs index f36df45..af03103 100644 --- a/src/voxel/world/mod.rs +++ b/src/voxel/world/mod.rs @@ -8,6 +8,7 @@ use super::{storage::ChunkMap, terraingen, Voxel}; /// Systems for dynamically loading / unloading regions (aka chunks) of the world according to camera position. mod chunks; +pub mod physics; pub use chunks::{ ChunkCommandQueue, ChunkEntities, ChunkLoadRadius, CurrentLocalPlayerChunk, DirtyChunks, }; @@ -36,7 +37,8 @@ impl Plugin for VoxelWorldPlugin { .add_plugin(chunks_anim::ChunkAppearanceAnimatorPlugin) .add_plugin(bevy_atmosphere::plugin::AtmospherePlugin) .add_plugin(player::VoxelWorldPlayerControllerPlugin) - .add_plugin(sky::InteractiveSkyboxPlugin); + .add_plugin(sky::InteractiveSkyboxPlugin) + .add_plugin(physics::VoxelWorldPhysicsPlugin); } } diff --git a/src/voxel/world/physics.rs b/src/voxel/world/physics.rs new file mode 100644 index 0000000..e9af305 --- /dev/null +++ b/src/voxel/world/physics.rs @@ -0,0 +1,226 @@ +//! Voxel collision detection and resolution. +//! +//! Based on Based on https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/ . + +use bevy::math::Vec3A; +use bevy::prelude::{ + Component, CoreSet, Deref, DerefMut, Entity, IVec3, IntoSystemConfig, Plugin, Query, Res, + SystemSet, Transform, Vec3, +}; +use bevy::render::primitives::Aabb; +use itertools::iproduct; +use std::cmp::Ordering; +use std::f32::{INFINITY, NEG_INFINITY}; + +use crate::voxel::storage::ChunkMap; +use crate::voxel::Voxel; + +use super::ChunkShape; + +#[derive(Component, Deref, DerefMut, Default)] +pub struct Velocity(pub Vec3A); + +#[derive(Component, Deref, DerefMut, Default)] +pub struct Acceleration(pub Vec3A); + +#[derive(Component, Deref, DerefMut)] +pub struct Drag(pub f32); + +/// Threshold to consider floats equal. +const EPSILON: f32 = 1e-5; +const SIM_TIME: f32 = 1.0 / 20.0; + +#[derive(Component)] +/// Marker component for entities that can collide with voxels. +pub struct Collider { + pub half_extents: Vec3A, +} + +#[derive(Debug)] +struct Collision { + normal: Vec3A, + time: f32, +} + +#[cfg(test)] +impl PartialEq for Collision { + fn eq(&self, other: &Self) -> bool { + (self.time - other.time).abs() <= EPSILON + && (self.normal - other.normal) + .abs() + .cmple(Vec3A::splat(EPSILON)) + .all() + } +} + +// Returns the time of collision (in [0, 1)) and normal if the given [Aabb] collides with a voxel at the given position. +fn swept_voxel_collision(b1: Aabb, displacement: Vec3A, b2: Aabb) -> Option { + // find the distance between the objects on the near and far sides for each axis + let is_displacement_positive = displacement.cmpgt(Vec3A::ZERO); + + let inv_entry = Vec3A::select( + is_displacement_positive, + b2.min() - b1.max(), + b2.max() - b1.min(), + ); + let inv_exit = Vec3A::select( + is_displacement_positive, + b2.max() - b1.min(), + b2.min() - b1.max(), + ); + + let is_displacement_nonzero: [bool; 3] = displacement.abs().cmpgt(Vec3A::splat(EPSILON)).into(); + let (mut entry, mut exit) = (Vec3A::splat(NEG_INFINITY), Vec3A::splat(INFINITY)); + for axis in 0..3 { + if is_displacement_nonzero[axis] { + entry[axis] = inv_entry[axis] / displacement[axis]; + exit[axis] = inv_exit[axis] / displacement[axis]; + } + } + + let entry_time = entry.max_element(); // @todo: why is this not min/max (flipped)?? + let exit_time = exit.min_element(); + + if entry_time > exit_time || entry.cmplt(Vec3A::ZERO).all() || entry.cmpge(Vec3A::ONE).any() { + // no collision in [0, 1) + None + } else { + // we collided! need to determine the normal vector based on which axis collided first. + let comp: [bool; 3] = entry.cmpeq(Vec3A::splat(entry_time)).into(); + let axis_idx = comp.into_iter().position(|x| x).unwrap(); + let axis = Vec3A::AXES[axis_idx]; + if entry[axis_idx] > 0.0 { + Some(Collision { + time: entry_time, + normal: -axis, + }) + } else { + Some(Collision { + time: entry_time, + normal: axis, + }) + } + } +} + +fn voxel_aabb(voxel: IVec3) -> Aabb { + let center = voxel.as_vec3a() + Vec3A::splat(0.5); + Aabb { + center, + half_extents: Vec3A::splat(0.5), + } +} + +fn step( + mut colliders: Query<( + Entity, + &mut Transform, + &mut Velocity, + Option<&Acceleration>, + Option<&Collider>, + Option<&Drag>, + )>, + voxels: Res>, +) { + for (_entity, mut transform, mut velocity, acceleration, collider, drag) in &mut colliders { + if let Some(acceleration) = acceleration { + **velocity += **acceleration * SIM_TIME; + } + + let mut displacement = **velocity * SIM_TIME; + if let Some(&Collider { half_extents }) = collider { + let aabb = Aabb { + center: transform.translation.into(), + half_extents, + }; + + let start = (Vec3A::min(aabb.min(), aabb.min() + displacement).floor()).as_ivec3() + + IVec3::NEG_Y; + let end = Vec3A::max(aabb.max(), aabb.max() + displacement) + .floor() + .as_ivec3(); + + assert!(start.cmple(end).all()); + + if let Some(collision) = iproduct!(start.x..end.x, start.y..end.y, start.z..end.z) + .map(|(x, y, z)| IVec3::new(x, y, z)) + .filter(|voxel| voxels.voxel_at(*voxel).is_some_and(Voxel::collidable)) + .map(|voxel| swept_voxel_collision(aabb, displacement, voxel_aabb(voxel))) + .flatten() + .min_by(|a, b| { + if a.time < b.time { + Ordering::Less + } else { + Ordering::Greater + } + }) + { + println!("resolving collision!"); + + // clip the displacement to avoid overlap + displacement = **velocity * collision.time; + + let previous_velocity = **velocity; + // cancel velocity in the normal direction + **velocity -= Vec3A::dot(previous_velocity, collision.normal) * collision.normal; + } + } + transform.translation += Vec3::from(displacement); + if let Some(drag) = drag { + **velocity *= **drag; + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, SystemSet)] +pub struct PhysicsSet; + +pub struct VoxelWorldPhysicsPlugin; + +impl Plugin for VoxelWorldPhysicsPlugin { + fn build(&self, app: &mut bevy::prelude::App) { + app.add_system(step.in_set(PhysicsSet).in_base_set(CoreSet::Update)); + } +} + +#[cfg(test)] +mod test { + use super::{swept_voxel_collision, voxel_aabb, Collision}; + use bevy::{math::Vec3A, prelude::IVec3, render::primitives::Aabb}; + + #[test] + fn test_swept_collisions() { + let player = Aabb { + center: Vec3A::new(0., 0.9, 0.0), // player is 1.8m tall, standing at the origin with feet touching the xz plane. + half_extents: Vec3A::new(0.25, 0.9, 0.25), // player is 0.5m wide and 1.8m tall + }; + + // no collision with a voxel centered at (1.5, 0.5, 1.5) + assert!( + swept_voxel_collision(player, Vec3A::ZERO, voxel_aabb(IVec3::new(1, 0, 1))).is_none() + ); + // no collision with a voxel centered at (1.5, 0.5, 0.5) + assert!( + swept_voxel_collision(player, Vec3A::ZERO, voxel_aabb(IVec3::new(1, 0, 0))).is_none() + ); + // player is now moving in the +x direction, and should collide: + assert_eq!( + swept_voxel_collision(player, Vec3A::X, voxel_aabb(IVec3::new(1, 0, 0))).unwrap(), + Collision { + time: 0.75, + normal: Vec3A::NEG_X + } + ); + assert!( + swept_voxel_collision(player, Vec3A::Y, voxel_aabb(IVec3::new(0, -1, 0))).is_none() + ); + assert!(swept_voxel_collision(player, Vec3A::Y, voxel_aabb(IVec3::new(1, 0, 0))).is_none()); + assert_eq!( + swept_voxel_collision(player, Vec3A::Y, voxel_aabb(IVec3::new(0, 2, 0))).unwrap(), + Collision { + time: 0.2, + normal: Vec3A::NEG_Y + } + ); + } +} diff --git a/src/voxel/world/player.rs b/src/voxel/world/player.rs index 0628f9e..39e2ca7 100644 --- a/src/voxel/world/player.rs +++ b/src/voxel/world/player.rs @@ -1,13 +1,38 @@ -use bevy::{input::mouse::MouseMotion, prelude::*, window::CursorGrabMode}; +use bevy::{input::mouse::MouseMotion, math::Vec3A, prelude::*, window::CursorGrabMode}; use bevy_egui::EguiContexts; use std::f32::consts::FRAC_PI_2; use crate::debug::DebugUISet; +use super::physics::{Acceleration, Collider, Drag, PhysicsSet, Velocity}; + // Reusing the player controller impl for now. pub const DEFAULT_CAMERA_SENS: f32 = 0.005; +#[derive(Bundle)] +pub struct PlayerBundle { + pub controller: PlayerController, + pub velocity: Velocity, + pub acceleration: Acceleration, + pub collider: Collider, + pub drag: Drag, +} + +impl Default for PlayerBundle { + fn default() -> Self { + Self { + controller: Default::default(), + velocity: Default::default(), + acceleration: Default::default(), + collider: Collider { + half_extents: Vec3A::new(0.25, 0.9, 0.25), + }, + drag: Drag(0.99), + } + } +} + #[derive(Default, Component)] pub struct PlayerController { yaw: f32, @@ -55,11 +80,11 @@ pub fn handle_player_mouse_move( pub fn handle_player_input( mut egui: EguiContexts, - mut query: Query<(&mut PlayerController, &mut Transform)>, + mut query: Query<(&mut PlayerController, &Transform, &mut Acceleration)>, keys: Res>, btns: Res>, ) { - let (mut controller, mut transform) = query.single_mut(); + let (mut controller, transform, mut acceleration) = query.single_mut(); // cursor grabbing // @todo: this should prevent cursor grabbing when the user is interacting with a debug UI. Why doesn't this work? @@ -76,7 +101,7 @@ pub fn handle_player_input( let forward = transform.rotation.mul_vec3(Vec3::Z).normalize() * Vec3::new(1.0, 0., 1.0); let right = transform.rotation.mul_vec3(Vec3::X).normalize(); - let mut acceleration = 1.0f32; + let mut speed = 1.0f32; if keys.pressed(KeyCode::W) { direction.z -= 1.0; @@ -103,17 +128,14 @@ pub fn handle_player_input( } if keys.pressed(KeyCode::LControl) { - acceleration *= 8.0; - } - - if direction == Vec3::ZERO { - return; + speed *= 8.0; } - // hardcoding 0.10 as a factor for now to not go zoomin across the world. - transform.translation += direction.x * right * acceleration - + direction.z * forward * acceleration - + direction.y * Vec3::Y * acceleration; + // // hardcoding 0.10 as a factor for now to not go zoomin across the world. + // transform.translation += direction.x * right * acceleration + // + direction.z * forward * acceleration + // + direction.y * Vec3::Y * acceleration; + **acceleration = (speed * direction.x * right + direction.z * forward * speed + direction.y * Vec3::Y * speed).into(); } #[derive(Hash, Copy, Clone, PartialEq, Eq, Debug, SystemSet)] @@ -128,6 +150,7 @@ impl Plugin for VoxelWorldPlayerControllerPlugin { (handle_player_input, handle_player_mouse_move) .chain() .in_base_set(CoreSet::Update) + .before(PhysicsSet) .after(DebugUISet::Display), ); } From 513cb3d3d31677b5b9791164b83838f7b7e43789 Mon Sep 17 00:00:00 2001 From: Meyer Zinn Date: Mon, 1 May 2023 19:38:30 -0500 Subject: [PATCH 2/4] still buggy? --- src/voxel/world/physics.rs | 37 +++++++++++++++++++++++++++---------- src/voxel/world/player.rs | 17 +++++++++++++---- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/voxel/world/physics.rs b/src/voxel/world/physics.rs index e9af305..3c8f811 100644 --- a/src/voxel/world/physics.rs +++ b/src/voxel/world/physics.rs @@ -8,7 +8,7 @@ use bevy::prelude::{ SystemSet, Transform, Vec3, }; use bevy::render::primitives::Aabb; -use itertools::iproduct; +use itertools::{iproduct, Itertools}; use std::cmp::Ordering; use std::f32::{INFINITY, NEG_INFINITY}; @@ -33,7 +33,7 @@ const SIM_TIME: f32 = 1.0 / 20.0; #[derive(Component)] /// Marker component for entities that can collide with voxels. pub struct Collider { - pub half_extents: Vec3A, + pub aabb: Aabb, } #[derive(Debug)] @@ -128,24 +128,41 @@ fn step( } let mut displacement = **velocity * SIM_TIME; - if let Some(&Collider { half_extents }) = collider { + if let Some(&Collider { aabb }) = collider { let aabb = Aabb { - center: transform.translation.into(), - half_extents, + center: Vec3A::from(transform.translation) + aabb.center, + ..aabb }; - let start = (Vec3A::min(aabb.min(), aabb.min() + displacement).floor()).as_ivec3() + IVec3::NEG_Y; let end = Vec3A::max(aabb.max(), aabb.max() + displacement) .floor() .as_ivec3(); + println!( + "pos: {}, start: {}, end: {}", + aabb.center, start, end + ); + assert!(start.cmple(end).all()); - if let Some(collision) = iproduct!(start.x..end.x, start.y..end.y, start.z..end.z) + let all_voxels = iproduct!(start.x..end.x, start.y..end.y, start.z..end.z) .map(|(x, y, z)| IVec3::new(x, y, z)) - .filter(|voxel| voxels.voxel_at(*voxel).is_some_and(Voxel::collidable)) - .map(|voxel| swept_voxel_collision(aabb, displacement, voxel_aabb(voxel))) + .collect_vec(); + + let interesting_voxels = all_voxels + .iter() + .filter(|voxel| voxels.voxel_at(**voxel).is_some_and(Voxel::collidable)) + .collect_vec(); + + // println!( + // "pos: {}, voxels: {:?}, interesting voxels: {:?}", + // transform.translation, all_voxels, interesting_voxels + // ); + + if let Some(collision) = interesting_voxels + .into_iter() + .map(|voxel| swept_voxel_collision(aabb, displacement, voxel_aabb(*voxel))) .flatten() .min_by(|a, b| { if a.time < b.time { @@ -158,7 +175,7 @@ fn step( println!("resolving collision!"); // clip the displacement to avoid overlap - displacement = **velocity * collision.time; + displacement = **velocity * collision.time * SIM_TIME; let previous_velocity = **velocity; // cancel velocity in the normal direction diff --git a/src/voxel/world/player.rs b/src/voxel/world/player.rs index 39e2ca7..96b0588 100644 --- a/src/voxel/world/player.rs +++ b/src/voxel/world/player.rs @@ -1,4 +1,7 @@ -use bevy::{input::mouse::MouseMotion, math::Vec3A, prelude::*, window::CursorGrabMode}; +use bevy::{ + input::mouse::MouseMotion, math::Vec3A, prelude::*, render::primitives::Aabb, + window::CursorGrabMode, +}; use bevy_egui::EguiContexts; use std::f32::consts::FRAC_PI_2; @@ -26,9 +29,12 @@ impl Default for PlayerBundle { velocity: Default::default(), acceleration: Default::default(), collider: Collider { - half_extents: Vec3A::new(0.25, 0.9, 0.25), + aabb: Aabb { + center: Vec3A::new(0.0, -0.7, 0.0), // the collision center is ~0.6m below the eyes, which are ~0.2m below the top of the collision box + half_extents: Vec3A::new(0.4, 0.9, 0.4), + }, }, - drag: Drag(0.99), + drag: Drag(0.98), } } } @@ -135,7 +141,10 @@ pub fn handle_player_input( // transform.translation += direction.x * right * acceleration // + direction.z * forward * acceleration // + direction.y * Vec3::Y * acceleration; - **acceleration = (speed * direction.x * right + direction.z * forward * speed + direction.y * Vec3::Y * speed).into(); + **acceleration = (speed * direction.x * right + + direction.z * forward * speed + + direction.y * Vec3::Y * speed) + .into(); } #[derive(Hash, Copy, Clone, PartialEq, Eq, Debug, SystemSet)] From 117424be70826e85f84a6f28bbb5547efd89fca6 Mon Sep 17 00:00:00 2001 From: Meyer Zinn Date: Mon, 1 May 2023 19:50:02 -0500 Subject: [PATCH 3/4] sliding doesn't work? --- src/voxel/world/physics.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/voxel/world/physics.rs b/src/voxel/world/physics.rs index 3c8f811..19cd67c 100644 --- a/src/voxel/world/physics.rs +++ b/src/voxel/world/physics.rs @@ -139,14 +139,11 @@ fn step( .floor() .as_ivec3(); - println!( - "pos: {}, start: {}, end: {}", - aabb.center, start, end - ); + // println!("pos: {}, start: {}, end: {}", aabb.center, start, end); assert!(start.cmple(end).all()); - let all_voxels = iproduct!(start.x..end.x, start.y..end.y, start.z..end.z) + let all_voxels = iproduct!(start.x..=end.x, start.y..=end.y, start.z..=end.z) .map(|(x, y, z)| IVec3::new(x, y, z)) .collect_vec(); @@ -156,8 +153,8 @@ fn step( .collect_vec(); // println!( - // "pos: {}, voxels: {:?}, interesting voxels: {:?}", - // transform.translation, all_voxels, interesting_voxels + // "voxels: {:?}, interesting voxels: {:?}", + // all_voxels, interesting_voxels // ); if let Some(collision) = interesting_voxels From 982b94caf7bcb8f063914ba7791fca5fc9fe3e7f Mon Sep 17 00:00:00 2001 From: Meyer Zinn Date: Mon, 1 May 2023 19:52:57 -0500 Subject: [PATCH 4/4] satisfy clippy --- src/voxel/mod.rs | 10 +++++----- src/voxel/world/physics.rs | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/voxel/mod.rs b/src/voxel/mod.rs index 60302c5..b2f5533 100644 --- a/src/voxel/mod.rs +++ b/src/voxel/mod.rs @@ -1,17 +1,17 @@ -///! Storage primitives for storing voxel data +/// Storage primitives for storing voxel data pub mod storage; -///! Utils for managing a voxel world. +/// Utils for managing a voxel world. mod world; pub use world::*; -///! Terrain generator. +/// Terrain generator. pub mod terraingen; -///! Systems and utilities for rendering voxels. +/// Systems and utilities for rendering voxels. pub mod render; -///! Systems for defining voxel materials with physical properties. +/// Systems for defining voxel materials with physical properties. pub mod material; /// rust ports of signed distance field functions for use in world generation. diff --git a/src/voxel/world/physics.rs b/src/voxel/world/physics.rs index 19cd67c..9d0913f 100644 --- a/src/voxel/world/physics.rs +++ b/src/voxel/world/physics.rs @@ -159,8 +159,7 @@ fn step( if let Some(collision) = interesting_voxels .into_iter() - .map(|voxel| swept_voxel_collision(aabb, displacement, voxel_aabb(*voxel))) - .flatten() + .flat_map(|voxel| swept_voxel_collision(aabb, displacement, voxel_aabb(*voxel))) .min_by(|a, b| { if a.time < b.time { Ordering::Less