use crate::mesh3d::Vec3;
use crate::frustum_culling::BoundingSphere;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RigidBody3D {
pub position: Vec3,
pub velocity: Vec3,
pub acceleration: Vec3,
pub mass: f32,
pub drag: f32,
pub angular_velocity: Vec3,
pub use_gravity: bool,
pub is_kinematic: bool,
pub is_static: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Collider3D {
pub shape: ColliderShape,
pub is_trigger: bool,
pub friction: f32,
pub bounciness: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ColliderShape {
Sphere { radius: f32 },
Box { size: Vec3 },
Capsule { radius: f32, height: f32 },
}
#[derive(Debug, Clone)]
pub struct Collision3D {
pub point: Vec3,
pub normal: Vec3,
pub penetration: f32,
pub other_id: usize,
}
pub struct Physics3D {
pub gravity: Vec3,
pub bodies: Vec<RigidBody3D>,
pub colliders: Vec<Collider3D>,
}
impl RigidBody3D {
pub fn new(position: Vec3) -> Self {
Self {
position,
velocity: Vec3::zero(),
acceleration: Vec3::zero(),
mass: 1.0,
drag: 0.98,
angular_velocity: Vec3::zero(),
use_gravity: true,
is_kinematic: false,
is_static: false,
}
}
pub fn dynamic(position: Vec3, mass: f32) -> Self {
Self {
position,
mass,
use_gravity: true,
is_kinematic: false,
is_static: false,
..Self::new(position)
}
}
pub fn kinematic(position: Vec3) -> Self {
Self {
position,
is_kinematic: true,
use_gravity: false,
..Self::new(position)
}
}
pub fn static_body(position: Vec3) -> Self {
Self {
position,
is_static: true,
use_gravity: false,
..Self::new(position)
}
}
pub fn apply_force(&mut self, force: Vec3) {
if !self.is_static && !self.is_kinematic {
self.acceleration = self.acceleration + force / self.mass;
}
}
pub fn apply_impulse(&mut self, impulse: Vec3) {
if !self.is_static && !self.is_kinematic {
self.velocity = self.velocity + impulse / self.mass;
}
}
pub fn update(&mut self, dt: f32, gravity: Vec3) {
if self.is_static {
return;
}
if !self.is_kinematic {
if self.use_gravity {
self.acceleration = self.acceleration + gravity;
}
self.velocity = self.velocity + self.acceleration * dt;
self.velocity = self.velocity * self.drag;
self.acceleration = Vec3::zero();
}
self.position = self.position + self.velocity * dt;
}
}
impl Collider3D {
pub fn sphere(radius: f32) -> Self {
Self {
shape: ColliderShape::Sphere { radius },
is_trigger: false,
friction: 0.5,
bounciness: 0.3,
}
}
pub fn box_collider(size: Vec3) -> Self {
Self {
shape: ColliderShape::Box { size },
is_trigger: false,
friction: 0.5,
bounciness: 0.3,
}
}
pub fn capsule(radius: f32, height: f32) -> Self {
Self {
shape: ColliderShape::Capsule { radius, height },
is_trigger: false,
friction: 0.5,
bounciness: 0.3,
}
}
pub fn as_trigger(mut self) -> Self {
self.is_trigger = true;
self
}
pub fn get_bounding_sphere(&self, position: Vec3) -> BoundingSphere {
match &self.shape {
ColliderShape::Sphere { radius } => BoundingSphere::new(position, *radius),
ColliderShape::Box { size } => {
let radius = (size.x * size.x + size.y * size.y + size.z * size.z).sqrt() / 2.0;
BoundingSphere::new(position, radius)
}
ColliderShape::Capsule { radius, height } => {
let total_radius = radius + height / 2.0;
BoundingSphere::new(position, total_radius)
}
}
}
pub fn check_collision(&self, pos1: Vec3, other: &Collider3D, pos2: Vec3) -> Option<Collision3D> {
match (&self.shape, &other.shape) {
(ColliderShape::Sphere { radius: r1 }, ColliderShape::Sphere { radius: r2 }) => {
Self::sphere_sphere_collision(pos1, *r1, pos2, *r2)
}
(ColliderShape::Box { size: s1 }, ColliderShape::Box { size: s2 }) => {
Self::box_box_collision(pos1, *s1, pos2, *s2)
}
_ => None, }
}
fn sphere_sphere_collision(pos1: Vec3, r1: f32, pos2: Vec3, r2: f32) -> Option<Collision3D> {
let delta = pos2 - pos1;
let distance = delta.length();
let min_distance = r1 + r2;
if distance < min_distance {
let normal = if distance > 0.0001 {
delta / distance
} else {
Vec3::new(0.0, 1.0, 0.0)
};
Some(Collision3D {
point: pos1 + normal * r1,
normal,
penetration: min_distance - distance,
other_id: 0,
})
} else {
None
}
}
fn box_box_collision(pos1: Vec3, size1: Vec3, pos2: Vec3, size2: Vec3) -> Option<Collision3D> {
let half1 = size1 / 2.0;
let half2 = size2 / 2.0;
let min1 = pos1 - half1;
let max1 = pos1 + half1;
let min2 = pos2 - half2;
let max2 = pos2 + half2;
if max1.x > min2.x && min1.x < max2.x
&& max1.y > min2.y && min1.y < max2.y
&& max1.z > min2.z && min1.z < max2.z
{
let delta = pos2 - pos1;
let overlap_x = (half1.x + half2.x) - delta.x.abs();
let overlap_y = (half1.y + half2.y) - delta.y.abs();
let overlap_z = (half1.z + half2.z) - delta.z.abs();
let (normal, penetration) = if overlap_x < overlap_y && overlap_x < overlap_z {
(Vec3::new(delta.x.signum(), 0.0, 0.0), overlap_x)
} else if overlap_y < overlap_z {
(Vec3::new(0.0, delta.y.signum(), 0.0), overlap_y)
} else {
(Vec3::new(0.0, 0.0, delta.z.signum()), overlap_z)
};
Some(Collision3D {
point: pos1 + normal * (half1.x.max(half1.y).max(half1.z)),
normal,
penetration,
other_id: 0,
})
} else {
None
}
}
}
impl Physics3D {
pub fn new() -> Self {
Self {
gravity: Vec3::new(0.0, -9.81, 0.0),
bodies: Vec::new(),
colliders: Vec::new(),
}
}
pub fn with_gravity(gravity: Vec3) -> Self {
Self {
gravity,
bodies: Vec::new(),
colliders: Vec::new(),
}
}
pub fn add_body(&mut self, body: RigidBody3D, collider: Collider3D) -> usize {
let id = self.bodies.len();
self.bodies.push(body);
self.colliders.push(collider);
id
}
pub fn update(&mut self, dt: f32) {
for body in &mut self.bodies {
body.update(dt, self.gravity);
}
self.resolve_collisions();
}
fn resolve_collisions(&mut self) {
let body_count = self.bodies.len();
for i in 0..body_count {
for j in (i + 1)..body_count {
let pos1 = self.bodies[i].position;
let pos2 = self.bodies[j].position;
if let Some(mut collision) = self.colliders[i].check_collision(
pos1,
&self.colliders[j],
pos2,
) {
collision.other_id = j;
if !self.colliders[i].is_trigger && !self.colliders[j].is_trigger {
self.resolve_collision_pair(i, j, &collision);
}
}
}
}
}
fn resolve_collision_pair(&mut self, i: usize, j: usize, collision: &Collision3D) {
let body1_static = self.bodies[i].is_static;
let body2_static = self.bodies[j].is_static;
if body1_static && body2_static {
return;
}
let separation = collision.normal * collision.penetration;
if !body1_static && !body2_static {
let total_mass = self.bodies[i].mass + self.bodies[j].mass;
let ratio1 = self.bodies[j].mass / total_mass;
let ratio2 = self.bodies[i].mass / total_mass;
self.bodies[i].position = self.bodies[i].position - separation * ratio1;
self.bodies[j].position = self.bodies[j].position + separation * ratio2;
} else if !body1_static {
self.bodies[i].position = self.bodies[i].position - separation;
} else {
self.bodies[j].position = self.bodies[j].position + separation;
}
let bounciness = (self.colliders[i].bounciness + self.colliders[j].bounciness) / 2.0;
if !body1_static {
let vel_along_normal = self.bodies[i].velocity.dot(&collision.normal);
if vel_along_normal < 0.0 {
self.bodies[i].velocity = self.bodies[i].velocity
- collision.normal * vel_along_normal * (1.0 + bounciness);
}
}
if !body2_static {
let vel_along_normal = self.bodies[j].velocity.dot(&collision.normal);
if vel_along_normal > 0.0 {
self.bodies[j].velocity = self.bodies[j].velocity
- collision.normal * vel_along_normal * (1.0 + bounciness);
}
}
}
pub fn raycast(&self, origin: Vec3, direction: Vec3, max_distance: f32) -> Option<RaycastHit> {
let mut closest_hit: Option<RaycastHit> = None;
let mut closest_distance = max_distance;
for (i, (body, collider)) in self.bodies.iter().zip(self.colliders.iter()).enumerate() {
if let Some(hit) = self.raycast_collider(origin, direction, body.position, collider) {
let distance = origin.distance(&hit.point);
if distance < closest_distance {
closest_distance = distance;
closest_hit = Some(RaycastHit {
point: hit.point,
normal: hit.normal,
distance,
body_id: i,
});
}
}
}
closest_hit
}
fn raycast_collider(
&self,
origin: Vec3,
direction: Vec3,
position: Vec3,
collider: &Collider3D,
) -> Option<RaycastHit> {
match &collider.shape {
ColliderShape::Sphere { radius } => {
Self::raycast_sphere(origin, direction, position, *radius)
}
_ => None, }
}
fn raycast_sphere(origin: Vec3, direction: Vec3, center: Vec3, radius: f32) -> Option<RaycastHit> {
let oc = origin - center;
let a = direction.dot(&direction);
let b = 2.0 * oc.dot(&direction);
let c = oc.dot(&oc) - radius * radius;
let discriminant = b * b - 4.0 * a * c;
if discriminant < 0.0 {
return None;
}
let t = (-b - discriminant.sqrt()) / (2.0 * a);
if t < 0.0 {
return None;
}
let point = origin + direction * t;
let normal = (point - center).normalize();
Some(RaycastHit {
point,
normal,
distance: t,
body_id: 0,
})
}
}
#[derive(Debug, Clone)]
pub struct RaycastHit {
pub point: Vec3,
pub normal: Vec3,
pub distance: f32,
pub body_id: usize,
}
impl Default for Physics3D {
fn default() -> Self {
Self::new()
}
}