#![allow(missing_docs)]
#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::f64::consts::PI;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum BuoyantShape {
Sphere { radius: f64 },
Box { half_extents: [f64; 3] },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuoyantBody {
pub id: u32,
pub position: [f64; 3],
pub velocity: [f64; 3],
pub angular_velocity: [f64; 3],
pub mass: f64,
pub shape: BuoyantShape,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FluidVolume {
pub id: u32,
pub min: [f64; 3],
pub max: [f64; 3],
pub density: f64,
pub surface_y: f64,
pub linear_drag: f64,
pub angular_drag: f64,
}
impl FluidVolume {
pub fn new(min: [f64; 3], max: [f64; 3], density: f64) -> Self {
let surface_y = max[1];
Self {
id: 0,
min,
max,
density,
surface_y,
linear_drag: 0.5,
angular_drag: 0.1,
}
}
pub fn contains_point(&self, p: [f64; 3]) -> bool {
p[0] >= self.min[0]
&& p[0] <= self.max[0]
&& p[1] >= self.min[1]
&& p[1] <= self.max[1]
&& p[2] >= self.min[2]
&& p[2] <= self.max[2]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuoyancyForce {
pub force: [f64; 3],
pub torque: [f64; 3],
pub submerged_fraction: f64,
}
#[inline]
fn sphere_cap_volume(r: f64, h: f64) -> f64 {
let h = h.clamp(0.0, 2.0 * r);
PI * h * h * (3.0 * r - h) / 3.0
}
#[inline]
fn sphere_volume(r: f64) -> f64 {
4.0 / 3.0 * PI * r * r * r
}
fn sphere_submerged_fraction(cy: f64, r: f64, surface_y: f64) -> f64 {
let bottom = cy - r;
let top = cy + r;
if top <= surface_y {
return 1.0;
}
if bottom >= surface_y {
return 0.0;
}
let h = surface_y - bottom;
let v_sub = sphere_cap_volume(r, h);
let v_total = sphere_volume(r);
(v_sub / v_total).clamp(0.0, 1.0)
}
fn box_submerged_fraction(cy: f64, hy: f64, surface_y: f64) -> f64 {
let bottom = cy - hy;
let top = cy + hy;
if top <= surface_y {
return 1.0;
}
if bottom >= surface_y {
return 0.0;
}
let submerged_height = (surface_y - bottom).max(0.0);
(submerged_height / (2.0 * hy)).clamp(0.0, 1.0)
}
fn displaced_volume(body: &BuoyantBody, fluid: &FluidVolume) -> (f64, f64) {
match &body.shape {
BuoyantShape::Sphere { radius } => {
let r = *radius;
let total = sphere_volume(r);
let frac = sphere_submerged_fraction(body.position[1], r, fluid.surface_y);
(total, frac)
}
BuoyantShape::Box { half_extents } => {
let [hx, hy, hz] = *half_extents;
let total = 8.0 * hx * hy * hz;
let frac = box_submerged_fraction(body.position[1], hy, fluid.surface_y);
(total, frac)
}
}
}
pub fn compute_buoyancy(
body: &BuoyantBody,
fluid: &FluidVolume,
gravity: f64,
) -> Option<BuoyancyForce> {
let p = body.position;
if p[0] < fluid.min[0] || p[0] > fluid.max[0] || p[2] < fluid.min[2] || p[2] > fluid.max[2] {
return None;
}
let (total_vol, frac) = displaced_volume(body, fluid);
if frac <= 0.0 {
return None;
}
let v_sub = total_vol * frac;
let f_buoy = fluid.density * v_sub * gravity;
let [vx, vy, vz] = body.velocity;
let c = fluid.linear_drag;
let force = [-c * vx, f_buoy - c * vy, -c * vz];
let [wx, wy, wz] = body.angular_velocity;
let ca = fluid.angular_drag;
let torque = [-ca * wx, -ca * wy, -ca * wz];
Some(BuoyancyForce {
force,
torque,
submerged_fraction: frac,
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuoyancyWorld {
fluids: Vec<FluidVolume>,
next_id: u32,
pub gravity: f64,
}
impl BuoyancyWorld {
pub fn new(gravity: f64) -> Self {
Self {
fluids: Vec::new(),
next_id: 0,
gravity,
}
}
pub fn add_fluid(&mut self, min: [f64; 3], max: [f64; 3], density: f64, surface_y: f64) -> u32 {
let id = self.next_id;
self.next_id += 1;
self.fluids.push(FluidVolume {
id,
min,
max,
density,
surface_y,
linear_drag: 0.5,
angular_drag: 0.1,
});
id
}
#[allow(clippy::too_many_arguments)]
pub fn add_fluid_with_drag(
&mut self,
min: [f64; 3],
max: [f64; 3],
density: f64,
surface_y: f64,
linear_drag: f64,
angular_drag: f64,
) -> u32 {
let id = self.next_id;
self.next_id += 1;
self.fluids.push(FluidVolume {
id,
min,
max,
density,
surface_y,
linear_drag,
angular_drag,
});
id
}
pub fn remove_fluid(&mut self, id: u32) {
self.fluids.retain(|f| f.id != id);
}
pub fn fluid(&self, id: u32) -> Option<&FluidVolume> {
self.fluids.iter().find(|f| f.id == id)
}
pub fn fluid_mut(&mut self, id: u32) -> Option<&mut FluidVolume> {
self.fluids.iter_mut().find(|f| f.id == id)
}
pub fn fluids_at_point(&self, p: [f64; 3]) -> Vec<&FluidVolume> {
self.fluids.iter().filter(|f| f.contains_point(p)).collect()
}
pub fn fluid_count(&self) -> usize {
self.fluids.len()
}
pub fn fluids(&self) -> impl Iterator<Item = &FluidVolume> {
self.fluids.iter()
}
pub fn apply(&self, bodies: &[BuoyantBody]) -> Vec<(u32, BuoyancyForce)> {
let g = self.gravity;
let mut out = Vec::new();
for body in bodies {
let mut combined = BuoyancyForce {
force: [0.0; 3],
torque: [0.0; 3],
submerged_fraction: 0.0,
};
let mut hit = false;
for fluid in &self.fluids {
if let Some(bf) = compute_buoyancy(body, fluid, g) {
combined.force[0] += bf.force[0];
combined.force[1] += bf.force[1];
combined.force[2] += bf.force[2];
combined.torque[0] += bf.torque[0];
combined.torque[1] += bf.torque[1];
combined.torque[2] += bf.torque[2];
combined.submerged_fraction =
combined.submerged_fraction.max(bf.submerged_fraction);
hit = true;
}
}
if hit {
out.push((body.id, combined));
}
}
out
}
pub fn apply_single(&self, body: &BuoyantBody) -> Option<BuoyancyForce> {
self.apply(std::slice::from_ref(body))
.into_iter()
.next()
.map(|(_, bf)| bf)
}
pub fn is_floating(&self, body: &BuoyantBody, threshold: f64) -> bool {
if let Some(bf) = self.apply_single(body) {
bf.submerged_fraction > 0.0 && bf.submerged_fraction < threshold
} else {
false
}
}
}