diff --git a/oxide_physics/Cargo.toml b/oxide_physics/Cargo.toml new file mode 100644 index 0000000..db4f4f2 --- /dev/null +++ b/oxide_physics/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "oxide_physics" +version = "0.0.1" +authors = [ + "Chris Ohk ", + "Changseo Jang ", + "Yongwook Choi ", + "Chaneun Yeo ", + "Seokwon Moon ", + "Oxide Engine" +] +edition = "2021" +description = "Physics library for Oxide" + +repository = "https://github.com/OxideEngine/Oxide" + +license = "MIT" + +[lib] +name = "oxide_physics" +path = "src/lib.rs" + +[dependencies] +oxide_math = {path = "../oxide_math", version = "0.0.1"} +num-traits = "0.2" +generational-arena = "0.2" \ No newline at end of file diff --git a/oxide_physics/src/aabb.rs b/oxide_physics/src/aabb.rs new file mode 100644 index 0000000..6da969d --- /dev/null +++ b/oxide_physics/src/aabb.rs @@ -0,0 +1,165 @@ +use crate::collide_broad_phase::{BoundingVolume, HasBoundingVolume}; +use oxide_math::commons::vector3::Vector3; + +#[derive(Debug, PartialEq)] +pub struct AABB { + pub mins: Vector3, + pub maxs: Vector3, +} + +pub fn aabb(shape: &S, tv: Vector3) -> AABB +where + S: HasBoundingVolume, +{ + shape.bounding_volume(tv) +} + +pub fn local_aabb(shape: &S) -> AABB +where + S: HasBoundingVolume, +{ + shape.local_bounding_volume() +} + +impl AABB { + pub fn new(mins: Vector3, maxs: Vector3) -> AABB { + AABB { mins, maxs } + } + + pub fn mins(&self) -> Vector3 { + Vector3 { + x: self.mins.x, + y: self.mins.y, + z: self.mins.z, + } + } + + pub fn maxs(&self) -> Vector3 { + Vector3 { + x: self.maxs.x, + y: self.maxs.y, + z: self.maxs.z, + } + } +} + +impl BoundingVolume for AABB { + // check if the bounding volume 'bv' intersects with self + fn intersects(&self, other: &AABB) -> bool { + self.mins.x <= other.maxs.x + && self.mins.y <= other.maxs.y + && self.mins.z <= other.maxs.z + && self.maxs.x >= other.mins.x + && self.maxs.y >= other.mins.y + && self.maxs.z >= other.mins.z + } + + // check if self contains the 'bv' + fn contains(&self, other: &AABB) -> bool { + self.mins.x <= other.mins.x + && self.mins.y <= other.mins.y + && self.mins.z <= other.mins.z + && self.maxs.x >= other.maxs.x + && self.maxs.y >= other.maxs.y + && self.maxs.z >= other.maxs.z + } + + // merge this bounding volume with the other 'bv' + fn merged(&self, other: &AABB) -> AABB { + AABB { + mins: Vector3 { + x: self.mins.x.min(other.mins.x), + y: self.mins.y.min(other.mins.y), + z: self.mins.z.min(other.mins.z), + }, + maxs: Vector3 { + x: self.maxs.x.max(other.maxs.x), + y: self.maxs.y.max(other.maxs.y), + z: self.maxs.z.max(other.maxs.z), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::shape::Ball; + + #[test] + fn test_intersects() { + let ball = Ball::new(1.0).unwrap(); + let ball_aabb0 = ball.local_bounding_volume(); + let ball_aabb1 = ball.bounding_volume(Vector3 { + x: 0.5, + y: 0.5, + z: 0.5, + }); + let ball_aabb2 = ball.bounding_volume(Vector3 { + x: 1.5, + y: 1.5, + z: 1.5, + }); + let ball_aabb3 = ball.bounding_volume(Vector3 { + x: 3.0, + y: 1.0, + z: 1.0, + }); + assert_eq!(true, ball_aabb0.intersects(&ball_aabb1)); + assert_eq!(true, ball_aabb0.intersects(&ball_aabb2)); + assert_eq!(false, ball_aabb0.intersects(&ball_aabb3)); + } + + #[test] + fn test_contains() { + let ball = Ball::new(1.0).unwrap(); + let bigball = Ball::new(3.0).unwrap(); + let ball_aabb0 = ball.local_bounding_volume(); + let ball_aabb1 = ball.bounding_volume(Vector3 { + x: 0.5, + y: 0.5, + z: 0.5, + }); + let ball_aabb2 = ball.bounding_volume(Vector3 { + x: 1.5, + y: 1.5, + z: 1.5, + }); + let ball_aabb3 = ball.bounding_volume(Vector3 { + x: 3.0, + y: 1.0, + z: 1.0, + }); + let bigball_aabb = bigball.local_bounding_volume(); + assert_eq!(false, ball_aabb0.contains(&ball_aabb1)); + assert_eq!(true, bigball_aabb.contains(&ball_aabb2)); + assert_eq!(false, bigball_aabb.contains(&ball_aabb3)); + } + + #[test] + fn test_merged() { + let ball = Ball::new(1.0).unwrap(); + let bigball = Ball::new(3.0).unwrap(); + let ball_aabb = ball.bounding_volume(Vector3 { + x: 3.0, + y: 1.0, + z: 1.0, + }); + let bigball_aabb = bigball.local_bounding_volume(); + assert_eq!( + ball_aabb.merged(&bigball_aabb), + AABB { + mins: Vector3 { + x: -3.0, + y: -3.0, + z: -3.0 + }, + maxs: Vector3 { + x: 4.0, + y: 3.0, + z: 3.0 + }, + } + ); + } +} diff --git a/oxide_physics/src/aabb_ball.rs b/oxide_physics/src/aabb_ball.rs new file mode 100644 index 0000000..3afd68d --- /dev/null +++ b/oxide_physics/src/aabb_ball.rs @@ -0,0 +1,91 @@ +use crate::aabb::AABB; +use crate::collide_broad_phase::HasBoundingVolume; +use crate::shape::Ball; +use oxide_math::commons::vector3::Vector3; + +pub fn ball_aabb(center: Vector3, radius: f32) -> AABB { + AABB::new( + Vector3 { + x: center.x - radius, + y: center.y - radius, + z: center.z - radius, + }, + Vector3 { + x: center.x + radius, + y: center.y + radius, + z: center.z + radius, + }, + ) +} + +pub fn local_ball_aabb(radius: f32) -> AABB { + ball_aabb( + Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }, + radius, + ) +} + +impl HasBoundingVolume for Ball { + fn bounding_volume(&self, tv: Vector3) -> AABB { + ball_aabb(tv, self.radius()) + } + + fn local_bounding_volume(&self) -> AABB { + local_ball_aabb(self.radius()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ball_aabb() { + let ball = Ball::new(4.0).unwrap(); + let ball_aabb = ball.bounding_volume(Vector3 { + x: 3.0, + y: 4.0, + z: 5.0, + }); + assert_eq!( + ball_aabb, + AABB { + mins: Vector3 { + x: -1.0, + y: 0.0, + z: 1.0 + }, + maxs: Vector3 { + x: 7.0, + y: 8.0, + z: 9.0 + }, + } + ); + } + + #[test] + fn test_local_aabb() { + let ball = Ball::new(4.0).unwrap(); + let ball_aabb = ball.local_bounding_volume(); + assert_eq!( + ball_aabb, + AABB { + mins: Vector3 { + x: -4.0, + y: -4.0, + z: -4.0 + }, + maxs: Vector3 { + x: 4.0, + y: 4.0, + z: 4.0 + }, + } + ); + } +} diff --git a/oxide_physics/src/aabb_cuboid.rs b/oxide_physics/src/aabb_cuboid.rs new file mode 100644 index 0000000..6dd5022 --- /dev/null +++ b/oxide_physics/src/aabb_cuboid.rs @@ -0,0 +1,84 @@ +use crate::aabb::AABB; +use crate::collide_broad_phase::HasBoundingVolume; +use crate::shape::Cuboid; +use oxide_math::commons::vector3::Vector3; + +impl HasBoundingVolume for Cuboid { + fn bounding_volume(&self, tv: Vector3) -> AABB { + let tv2 = Vector3 { + x: tv.x, + y: tv.y, + z: tv.z, + }; + AABB::new(self.mins() + tv, self.maxs() + tv2) + } + + fn local_bounding_volume(&self) -> AABB { + self.bounding_volume(Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cuboid_aabb() { + let cuboid = Cuboid::new(Vector3 { + x: 3.0, + y: 4.0, + z: 5.0, + }) + .unwrap(); + let cuboid_aabb = cuboid.bounding_volume(Vector3 { + x: 3.0, + y: 4.0, + z: 5.0, + }); + assert_eq!( + cuboid_aabb, + AABB { + mins: Vector3 { + x: 0.0, + y: 0.0, + z: 0.0 + }, + maxs: Vector3 { + x: 6.0, + y: 8.0, + z: 10.0 + }, + } + ); + } + + #[test] + fn test_local_aabb() { + let cuboid = Cuboid::new(Vector3 { + x: 3.0, + y: 4.0, + z: 5.0, + }) + .unwrap(); + let cuboid_aabb = cuboid.local_bounding_volume(); + assert_eq!( + cuboid_aabb, + AABB { + mins: Vector3 { + x: -3.0, + y: -4.0, + z: -5.0 + }, + maxs: Vector3 { + x: 3.0, + y: 4.0, + z: 5.0 + }, + } + ); + } +} diff --git a/oxide_physics/src/collide_broad_phase.rs b/oxide_physics/src/collide_broad_phase.rs new file mode 100644 index 0000000..4b6805f --- /dev/null +++ b/oxide_physics/src/collide_broad_phase.rs @@ -0,0 +1,26 @@ +use oxide_math::commons::vector3::Vector3; + +pub trait BoundingVolume { + // check if the bounding volume 'bv' intersects with self + fn intersects(&self, bv: &Self) -> bool; + + // check if self contains the 'bv' + fn contains(&self, bv: &Self) -> bool; + + // merge this bounding volume with the other 'bv' + fn merged(&self, bv: &Self) -> Self; +} + +pub trait HasBoundingVolume { + // TBD: rotation by 4x4 matrix + // bounding volume of 'self' translated by 'tv' + fn bounding_volume(&self, tv: Vector3) -> BV; + + fn local_bounding_volume(&self) -> BV { + self.bounding_volume(Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }) + } +} diff --git a/oxide_physics/src/lib.rs b/oxide_physics/src/lib.rs new file mode 100644 index 0000000..37c23ab --- /dev/null +++ b/oxide_physics/src/lib.rs @@ -0,0 +1,9 @@ +pub mod particle; +pub mod pfgen; + +pub mod shape; + +pub mod aabb; +mod aabb_ball; +mod aabb_cuboid; +pub mod collide_broad_phase; diff --git a/oxide_physics/src/particle.rs b/oxide_physics/src/particle.rs new file mode 100644 index 0000000..0a1d0a5 --- /dev/null +++ b/oxide_physics/src/particle.rs @@ -0,0 +1,363 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +use num_traits::pow; +use oxide_math::commons::vector::*; +use oxide_math::commons::vector3::Vector3; + +extern crate generational_arena; +use generational_arena::Arena; + +pub struct Particle { + inverse_mass: f32, + damping: f32, + position: Vector3, + pub velocity: Vector3, + force_accum: Vector3, + acceleration: Vector3, +} + +impl Particle { + // default mass is set to 1.0 + fn new(position: Vector3) -> Particle { + Particle { + inverse_mass: 1.0, + damping: 1.0, + position: Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }, + velocity: position, + force_accum: Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }, + acceleration: Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }, + } + } + + // returns integrated velocity + fn integrate(&mut self, duration: f32) -> Result { + // not to integrate things with infinite mass + if self.inverse_mass <= 0.0f32 { + return Ok(Vector3 { + x: self.velocity.x, + y: self.velocity.y, + z: self.velocity.z, + }); + } + if duration <= 0.0 { + return Err("Cannot integrate with zero duration"); + } + + // update linear position + self.position.x += self.velocity.scale(duration).x; + self.position.y += self.velocity.scale(duration).y; + self.position.z += self.velocity.scale(duration).z; + + // work out the acceleration from the force + let delta = self.force_accum.scale(self.inverse_mass); + let resulting_acc = Vector3 { + x: self.acceleration.x + delta.x, + y: self.acceleration.y + delta.y, + z: self.acceleration.z + delta.z, + }; + + // update linear velocity from the acceleration + self.velocity.x += resulting_acc.scale(duration).x; + self.velocity.y += resulting_acc.scale(duration).y; + self.velocity.z += resulting_acc.scale(duration).z; + + // impose drag + self.velocity = self.velocity.scale(pow(self.damping, duration as usize)); + + Particle::clear_accumulator(self); + + Ok(Vector3 { + x: self.velocity.x, + y: self.velocity.y, + z: self.velocity.z, + }) + } + + // Returns inverse of mass + fn set_mass(&mut self, mass: f32) -> Result { + if mass == 0.0f32 { + return Err("Cannot set zero mass"); + } + self.inverse_mass = (1.0f32) / mass; + Ok(self.inverse_mass) + } + + // Returns mass of the particle + pub fn get_mass(&self) -> f32 { + if self.inverse_mass == 0.0f32 { + f32::MAX + } else { + 1.0f32 / self.inverse_mass + } + } + + // Returns the velocity of the particle + pub fn get_velocity(&self) -> Vector3 { + Vector3 { + x: self.velocity.x, + y: self.velocity.y, + z: self.velocity.z, + } + } + + pub fn has_finite_mass(&self) -> bool { + self.inverse_mass > 0.0f32 + } + + fn clear_accumulator(&mut self) { + self.force_accum = Vector3 { + x: 0.0f32, + y: 0.0f32, + z: 0.0f32, + }; + } + + pub fn add_force(&mut self, force: &Vector3) { + self.force_accum = Vector3 { + x: self.force_accum.x + force.x, + y: self.force_accum.y + force.y, + z: self.force_accum.z + force.z, + }; + } +} + +// The default particle set containing all the particles added to the world +// Uses arena to avoid ABA problem +pub struct DefaultParticleSet { + particles: Arena>, + removed: Vec, +} + +impl DefaultParticleSet { + // Creates an empty set + pub fn new() -> Self { + DefaultParticleSet { + particles: Arena::new(), + removed: Vec::new(), + } + } + + // Adds a particle to this set + pub fn insert(&mut self, particle: Particle) -> DefaultParticleHandle { + self.particles.insert(Box::new(particle)) + } + + // Removes a particle from this set + pub fn remove(&mut self, particle_handle: DefaultParticleHandle) -> Option> { + let result = self.particles.remove(particle_handle)?; + self.removed.push(particle_handle); + Some(result) + } +} + +impl Default for DefaultParticleSet { + fn default() -> Self { + Self::new() + } +} + +pub type DefaultParticleHandle = generational_arena::Index; + +#[cfg(test)] +mod tests { + use oxide_math::components::velocity::Velocity; + + use super::*; + + #[test] + fn test_set_mass() { + let mut p = Particle::new(Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }); + if let Err(msg) = p.set_mass(5.0) { + panic!("{}", msg); + } + assert_eq!(0.2, p.inverse_mass); + } + + #[test] + #[should_panic] + fn test_bad_set_mass() { + let mut p = Particle::new(Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }); + if let Err(msg) = p.set_mass(0.0) { + panic!("{}", msg); + } + } + + #[test] + fn test_get_mass() { + let mut p = Particle::new(Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }); + if let Err(msg) = p.set_mass(5.0) { + panic!("{}", msg); + } + assert_eq!(5.0, p.get_mass()); + } + + #[test] + fn test_get_velocity() { + let mut p = Particle::new(Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }); + if let Err(msg) = p.set_mass(5.0) { + panic!("{}", msg); + } + let f = Vector3 { + x: 10.0, + y: 0.0, + z: 0.0, + }; + p.add_force(&f); + p.integrate(1.0).unwrap(); + assert_eq!( + Vector3 { + x: 2.0, + y: 0.0, + z: 0.0 + }, + p.get_velocity() + ); + } + + fn test_has_finite_mass() { + let mut p = Particle::new(Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }); + assert_eq!(true, p.has_finite_mass()); + let mut q = Particle { + inverse_mass: 0.0, + damping: 1.0, + position: Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }, + velocity: Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }, + force_accum: Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }, + acceleration: Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }, + }; + assert_eq!(false, q.has_finite_mass()); + } + + #[test] + fn test_particle1() { + let mut p = Particle::new(Vector3 { + x: 0.0, + y: 0.0, + z: 0.0, + }); + assert_eq!( + p.position, + Vector3 { + x: 0.0, + y: 0.0, + z: 0.0 + } + ); + assert_eq!( + p.get_velocity(), + Vector3 { + x: 0.0, + y: 0.0, + z: 0.0 + } + ); + assert_eq!(p.get_mass(), 1.0); + let f = Vector3 { + x: 2.0, + y: 0.0, + z: 0.0, + }; + p.add_force(&f); + p.integrate(1.0).unwrap(); + assert_eq!( + Vector3 { + x: 2.0, + y: 0.0, + z: 0.0 + }, + p.velocity + ); + assert_eq!( + Vector3 { + x: 0.0, + y: 0.0, + z: 0.0 + }, + p.position + ); + p.add_force(&f); + p.integrate(1.0).unwrap(); + assert_eq!( + Vector3 { + x: 4.0, + y: 0.0, + z: 0.0 + }, + p.velocity + ); + assert_eq!( + Vector3 { + x: 2.0, + y: 0.0, + z: 0.0 + }, + p.position + ); + p.integrate(1.0).unwrap(); + assert_eq!( + Vector3 { + x: 4.0, + y: 0.0, + z: 0.0 + }, + p.velocity + ); + assert_eq!( + Vector3 { + x: 6.0, + y: 0.0, + z: 0.0 + }, + p.position + ); + } +} diff --git a/oxide_physics/src/pfgen.rs b/oxide_physics/src/pfgen.rs new file mode 100644 index 0000000..28bff4f --- /dev/null +++ b/oxide_physics/src/pfgen.rs @@ -0,0 +1,101 @@ +#![allow(dead_code)] +#![allow(unused_variables)] + +use crate::particle::*; +use oxide_math::commons::vector::*; +use oxide_math::commons::vector3::Vector3; +use std::vec::Vec; + +extern crate generational_arena; + +/* + * force generator trait + */ +pub trait ParticleForceGenerator { + /* + * interface to calculate and update the force + * to the given particle + */ + fn update_force(&self, particle: &mut Particle, duration: f32); +} + +/* + * Registry for (Particle, Force Generator) pairs + * Holds all particles and associated force generators + */ +struct ParticleForceRegistration { + particle: DefaultParticleHandle, + fg: DefaultForceGeneratorHandle, +} + +pub type DefaultForceGeneratorHandle = generational_arena::Index; + +pub struct ParticleForceRegistry { + registrations: Vec, +} + +impl ParticleForceRegistry { + pub fn add( + &self, + particle: DefaultParticleHandle, + fg: DefaultForceGeneratorHandle, + ) -> Result { + todo!(); + } + + pub fn remove( + &self, + particle: DefaultParticleHandle, + fg: DefaultForceGeneratorHandle, + ) -> Result { + todo!(); + } + + pub fn clear(&self) -> Result { + todo!(); + } + + pub fn update_forces(&self, duration: f32) { + // for i in self.registrations.iter_mut() { + // i.fg.update_force(i.particle, duration); + // } + todo!(); + } +} + +/* + * Gravity Force Generator + */ +struct ParticleGravity { + gravity: Vector3, +} + +impl ParticleForceGenerator for ParticleGravity { + fn update_force(&self, particle: &mut Particle, duration: f32) { + if particle.has_finite_mass() { + particle.add_force(&self.gravity.scale(particle.get_mass())); + } + } +} + +/* + * Drag Force Generator + */ +struct ParticleDrag { + k1: f32, + k2: f32, +} + +impl ParticleForceGenerator for ParticleDrag { + fn update_force(&self, particle: &mut Particle, duration: f32) { + let force: &Vector3 = &particle.velocity; + + // Calculate the total drag coefficient + let drag_coeff: f32 = + self.k1 * force.get_length() + self.k2 * force.get_length() * force.get_length(); + + // Calculate the final force and apply it + let final_force = force.normalize().scale(-drag_coeff); + particle.add_force(&final_force); + } +} diff --git a/oxide_physics/src/shape.rs b/oxide_physics/src/shape.rs new file mode 100644 index 0000000..fc0c149 --- /dev/null +++ b/oxide_physics/src/shape.rs @@ -0,0 +1,111 @@ +use crate::aabb::AABB; +use oxide_math::commons::vector3::Vector3; + +pub trait Shape { + fn bounding_volume(&self) -> AABB; + fn local_bounding_volume(&self, tv: Vector3) -> AABB; +} + +#[derive(Debug, PartialEq)] +pub struct Ball { + radius: f32, +} + +#[derive(Debug, PartialEq)] +// x, y, and z are the half-extent of the cuboid +pub struct Cuboid { + x: f32, + y: f32, + z: f32, +} + +impl Ball { + pub fn new(radius: f32) -> Result { + if radius <= 0.0 { + Err("A ball radius must be positive.") + } else { + Ok(Ball { radius }) + } + } + + pub fn radius(&self) -> f32 { + self.radius + } +} + +impl Cuboid { + // x, y, and z are the half-extent of the cuboid + pub fn new(position: Vector3) -> Result { + if position.x == 0.0 || position.y == 0.0 || position.z == 0.0 { + Err("Cuboid's edge must be bigger than 0") + } else { + Ok(Cuboid { + x: position.x.abs(), + y: position.y.abs(), + z: position.z.abs(), + }) + } + } + + pub fn mins(&self) -> Vector3 { + Vector3 { + x: -self.x, + y: -self.y, + z: -self.z, + } + } + + pub fn maxs(&self) -> Vector3 { + Vector3 { + x: self.x, + y: self.y, + z: self.z, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_ball() { + let ball = Ball::new(4.0).unwrap(); + assert_eq!(Ball { radius: 4.0 }, ball); + assert_eq!(4.0, ball.radius()); + } + + #[test] + fn test_cuboid() { + let cuboid = Cuboid::new(Vector3 { + x: 2.0, + y: -4.0, + z: 6.0, + }) + .unwrap(); + assert_eq!( + Cuboid { + x: 2.0, + y: 4.0, + z: 6.0, + }, + cuboid + ); + assert_eq!( + Vector3 { + x: -2.0, + y: -4.0, + z: -6.0 + }, + cuboid.mins() + ); + assert_eq!( + Vector3 { + x: 2.0, + y: 4.0, + z: 6.0 + }, + cuboid.maxs() + ); + } +}