diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a58569..aff2582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ Notable changes to this project will be documented in this file. -## [unreleased] +## [0.3.0](https://github.com/philiplinden/buoy/compare/v0.2.0..v0.3.0) - 2025-07-28 ### ๐Ÿงช Experimental @@ -18,74 +18,4 @@ Notable changes to this project will be documented in this file. - Remove big_space options (#39) ([`b03eff2`](https://github.com/philiplinden/buoy/commit/b03eff2fb9175f28a235db4cfe9b6da1b0aeb828)) -## [0.2.0](https://github.com/philiplinden/buoy/compare/v0.1.0..v0.2.0) - 2025-06-27 - -### ๐Ÿš€ Features - -- Mesh tools and a simple scene (#37) ([`d9d7bc2`](https://github.com/philiplinden/buoy/commit/d9d7bc29ad77b49017b29eb6dc673a638bddac7a)) -- Add egui side panel (#35) ([`1c5dff4`](https://github.com/philiplinden/buoy/commit/1c5dff46f16cf93c01641ddbb4625dc69aabc544)) - -### โ™ป๏ธ Refactor - -- Merge buoy-common with buoy-runtime (#38) ([`9e8fd7e`](https://github.com/philiplinden/buoy/commit/9e8fd7e4f29aae8f5b18840d4225965704e0b581)) -- Run headless by default (#36) ([`012f179`](https://github.com/philiplinden/buoy/commit/012f179b78804c3cd393d2d4d304e9e55befb2f9)) -- Buoy-core -> buoy-physics, add buoy-common, grid space is optional (#34) ([`a8e770e`](https://github.com/philiplinden/buoy/commit/a8e770e58c88a950f74c46a53fc069474c044607)) -- Remove python experiments (#33) ([`089042b`](https://github.com/philiplinden/buoy/commit/089042b574e39ba263ec528affa14a5448073341)) - -### ๐Ÿ› Bug Fixes - -- Disable mesh utils for now and focus on headless ([`db8ac1f`](https://github.com/philiplinden/buoy/commit/db8ac1f90a3a683c5d5f7902302ff1f66faa871a)) - -### โš™๏ธ Repository - -- Refresh the changelog on pushes to main ([`26a2e6a`](https://github.com/philiplinden/buoy/commit/26a2e6a3f26df28b7d622901302cc12f579b98dc)) -- Package the bevy-ui binary for releases ([`064e94c`](https://github.com/philiplinden/buoy/commit/064e94cd37ee1a161564b2b5c04f80533033742d)) -- Include unreleased changes in log ([`c9fa447`](https://github.com/philiplinden/buoy/commit/c9fa4478d781a42c0e25d37669a5157b390f4195)) - -## [0.1.0] - 2025-03-02 - -### ๐Ÿ“š Documentation - -- Update readme ([`5685be7`](https://github.com/philiplinden/buoy/commit/5685be7df46da343be3b831e7e5c15e024227a1c)) -- More resources ([`696266b`](https://github.com/philiplinden/buoy/commit/696266b175e90f3e36d5de8494ef2f7977d41b43)) - -### โ™ป๏ธ Refactor - -- Disallow dead code in most places, add placeholders for payload ([`edb98d3`](https://github.com/philiplinden/buoy/commit/edb98d3a6aff3c3633de027930e3406e3fdd3bb0)) - -### ๐ŸŽจ UI & Styling - -- Improved plots ([`6d1bba1`](https://github.com/philiplinden/buoy/commit/6d1bba1862d9be682693fe6932148195a8db3ea7)) -- Switch to egui, add plots (#15) ([`bdc6113`](https://github.com/philiplinden/buoy/commit/bdc6113a3d77993f56df5796358879371002e8f9)) -- Deconflict debug text and console screen space ([`837a626`](https://github.com/philiplinden/buoy/commit/837a626c0a4781d22150d1fe3ea12e71519d5729)) -- Add gizmos, flatten app module (#3) ([`119dade`](https://github.com/philiplinden/buoy/commit/119dade9ffb889d48ddcccb3381958bff7eae594)) -- Add a gas monitor ([`3de7b41`](https://github.com/philiplinden/buoy/commit/3de7b4167d26622d516bd4cf9a11c5c584d724b0)) -- Some debug ui practice ([`c80aa64`](https://github.com/philiplinden/buoy/commit/c80aa64d2bbd4cc2c1932889863bada2f8a1d0c2)) -- Sim state monitoring, play/pause ([`88ce33b`](https://github.com/philiplinden/buoy/commit/88ce33b46f65d1e592057e51128b995d65eb34eb)) -- Drop egui, add iyes_perf_ui ([`8cf8096`](https://github.com/philiplinden/buoy/commit/8cf8096625dd3ea4cfe98125a4f8d1d148eb5fbb)) - -### ๐Ÿ› Bug Fixes - -- Fix up physics states ([`b311fe4`](https://github.com/philiplinden/buoy/commit/b311fe4d9e38f67e2c4fa5ee20b268da6797afe1)) - -### ๐Ÿงช Experimental - -- Scaffold things in python for prototyping (#25) ([`45f8e98`](https://github.com/philiplinden/buoy/commit/45f8e98b571bff0a2bf8aac607f6745b0cc2743c)) -- Smooth out artifacts ([`11ee325`](https://github.com/philiplinden/buoy/commit/11ee325c5d2f418f33b6db4e483ed94fb9f58709)) -- Experimenting with plots and big space ([`8a57fb7`](https://github.com/philiplinden/buoy/commit/8a57fb7f7f6ce7151209f9f8f577bf3d248b7277)) -- Experimenting with meshes and modules ([`2d75f55`](https://github.com/philiplinden/buoy/commit/2d75f5528cecac68c87851a3f5e1a4388741c1e1)) -- Forces are stable! (#14) ([`5e0dbef`](https://github.com/philiplinden/buoy/commit/5e0dbefea4e4adf7eda26d73c3f7f9eb711020ad)) - -### โš™๏ธ Repository - -- Yet another reorg - more submodules ([`2ac04de`](https://github.com/philiplinden/buoy/commit/2ac04de27df4b41f1223042e15e2c5a817b1bcb3)) -- Skip deploys ([`2db5450`](https://github.com/philiplinden/buoy/commit/2db5450b77d736bbd545dab3b71a3681782d62f9)) -- I guess docs need sound? ([`eb141a6`](https://github.com/philiplinden/buoy/commit/eb141a6a446042670815ea3cfb9a5598b7f5e036)) -- Replace cache step with the one from leafwing ([`99642d9`](https://github.com/philiplinden/buoy/commit/99642d9cd84ff8eda7d29904e2fc7c22c3fd8f08)) -- Add bevy deps ([`56ed0f3`](https://github.com/philiplinden/buoy/commit/56ed0f3d6c41e6f2b114d4cacfda22bd3e0f2a0a)) - - -## New Contributors โค๏ธ - -* @philiplinden made their first contribution diff --git a/Cargo.toml b/Cargo.toml index 89af0ed..33d21d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" # Important for Bevy members = [ "crates/buoy-physics", - "crates/buoy-ui", + # "crates/buoy-ui", "crates/buoy-runtime", ] @@ -36,7 +36,6 @@ avian3d = { version = "0.3.1", default-features = false, features = [ # "collider-from-mesh", ] } uom = "0.37.0" -bevy_common_assets = { version = "0.13", features = ["ron"] } serde = { version = "1.0", features = ["derive"] } [workspace.metadata.bevy_cli] diff --git a/crates/buoy-physics/Cargo.toml b/crates/buoy-physics/Cargo.toml index d5d62be..3065c9d 100644 --- a/crates/buoy-physics/Cargo.toml +++ b/crates/buoy-physics/Cargo.toml @@ -1,26 +1,16 @@ [package] name = "buoy-physics" -version = "0.3.0" -description = "Physics for buoyancy simulation" +version = "0.4.0" +description = "Physics for buoyancy simulation (no bevy dependency)" edition = { workspace = true } authors = { workspace = true } license = { workspace = true } [dependencies] -bevy = { workspace = true } -avian3d = { workspace = true } uom = { workspace = true } serde = { workspace = true } +glam = "0.30.5" +log = "0.4.27" [features] -default = [ - "dev", -] -dev = [ - "bevy/dynamic_linking", - "bevy/bevy_debug_stepping", - "avian3d/bevy_diagnostic", -] - - -# [package.metadata.mutually_exclusive_features] +default = [] diff --git a/crates/buoy-physics/src/atmosphere.rs b/crates/buoy-physics/src/atmosphere.rs index 5ba0e8b..55bcd08 100644 --- a/crates/buoy-physics/src/atmosphere.rs +++ b/crates/buoy-physics/src/atmosphere.rs @@ -5,25 +5,20 @@ //! - https://www.translatorscafe.com/unit-converter/en-US/calculator/altitude //! - https://www.grc.nasa.gov/WWW/K-12/airplane/atmosmet.html -use avian3d::prelude::{Position, RigidBody}; -use bevy::prelude::*; use uom::si::{ f32::*, thermodynamic_temperature::{degree_celsius, kelvin}, pressure::kilopascal, }; +use glam::Vec3; +use log::error; use crate::{ ideal_gas::{ideal_gas_density, GasSpecies}, constants::{STANDARD_TEMPERATURE, STANDARD_PRESSURE}, }; -pub(crate) fn plugin(app: &mut App) { - app.insert_resource(Atmosphere); -} - /// US Standard Atmosphere, 1976 -#[derive(Resource)] pub struct Atmosphere; impl Atmosphere { diff --git a/crates/buoy-physics/src/force.rs b/crates/buoy-physics/src/force.rs new file mode 100644 index 0000000..cf6e74a --- /dev/null +++ b/crates/buoy-physics/src/force.rs @@ -0,0 +1,50 @@ +use glam::Vec3; +use uom::si::{ + acceleration::meter_per_second_squared, area::square_meter, f32::*, length::meter, + mass::kilogram, mass_density::kilogram_per_cubic_meter, ratio::ratio, volume::cubic_meter, + velocity::meter_per_second, +}; + +use crate::constants::{EARTH_RADIUS_M, STANDARD_GRAVITY}; + +/// Force (N) due to drag as a solid body moves through a fluid. +pub fn drag( + velocity: Vec3, + ambient_density: MassDensity, + drag_area: Area, + drag_coefficient: f32, +) -> Vec3 { + let velocity_magnitude: f32 = velocity.length(); + if velocity_magnitude < f32::EPSILON { + return Vec3::ZERO; + } + + -0.5 * drag_coefficient + * ambient_density.get::() + * drag_area.get::() + * velocity_magnitude * velocity_magnitude + * velocity / velocity_magnitude +} + +/// Upward force (N) vector due to atmosphere displaced by the given gas volume. +/// The direction of this force is always world-space up (it opposes gravity). +pub fn buoyancy( + gravity_acceleration: Acceleration, + displaced_volume: Volume, + ambient_density: MassDensity, +) -> Vec3 { + Vec3::Y + * (displaced_volume.get::() + * ambient_density.get::() + * gravity_acceleration.get::()) +} + +/// Fraction of standard gravity at an altitude (m) above mean sea level. +pub fn local_gravity(altitude: Length) -> Acceleration { + *EARTH_RADIUS_M / (*EARTH_RADIUS_M + altitude) * *STANDARD_GRAVITY +} + +#[allow(dead_code)] +pub fn weight(mass: uom::si::f32::Mass, gravity: Acceleration) -> Vec3 { + Vec3::NEG_Y * mass.get::() * gravity.get::() +} diff --git a/crates/buoy-physics/src/forces.rs b/crates/buoy-physics/src/forces.rs deleted file mode 100644 index a7de01c..0000000 --- a/crates/buoy-physics/src/forces.rs +++ /dev/null @@ -1,92 +0,0 @@ -use avian3d::{math::Scalar, prelude::*}; -use bevy::prelude::*; -use uom::si::{ - acceleration::meter_per_second_squared, area::square_meter, f32::*, length::meter, - mass::kilogram, mass_density::kilogram_per_cubic_meter, ratio::ratio, volume::cubic_meter, - velocity::meter_per_second, -}; - -use crate::atmosphere::Atmosphere; -use crate::constants::{EARTH_RADIUS_M, STANDARD_GRAVITY}; -use crate::geometry::{projected_area, collider_volume}; - -pub(crate) fn plugin(app: &mut App) { - app.insert_resource(Gravity(Vec3::ZERO)); - app.add_systems(FixedUpdate, (net_force).in_set(PhysicsStepSet::First)); -} - -fn net_force( - mut query: Query<( - &mut ExternalForce, - &Position, - &ComputedMass, - &Collider, - &LinearVelocity, - &DragCoefficient, - )>, - atmosphere: Res, -) { - for (mut external_force, position, computed_mass, collider, velocity, drag_coefficient) in - query.iter_mut() - { - let mass = uom::si::f32::Mass::new::(computed_mass.value()); - let drag_area = - uom::si::f32::Area::new::(projected_area(collider, velocity.0)); - let gravity = local_gravity(uom::si::f32::Length::new::(position.y)); - let weight = weight(mass, gravity); - let buoyancy = buoyancy(gravity, uom::si::f32::Volume::new::(collider_volume(collider)), atmosphere.density(position.0)); - let drag = drag( - velocity.0, - atmosphere.density(position.0), - drag_area, - drag_coefficient.0, - ); - let net_force = weight + buoyancy + drag; - - external_force.apply_force(net_force); - } -} - -/// Force (N) due to drag as a solid body moves through a fluid. -pub fn drag( - velocity: Vec3, - ambient_density: MassDensity, - drag_area: Area, - drag_coefficient: Scalar, -) -> Vec3 { - let velocity_magnitude = velocity.length(); - if velocity_magnitude < f32::EPSILON { - return Vec3::ZERO; - } - - -0.5 * drag_coefficient - * ambient_density.get::() - * drag_area.get::() - * velocity_magnitude * velocity_magnitude - * velocity / velocity_magnitude -} - -/// Upward force (N) vector due to atmosphere displaced by the given gas volume. -/// The direction of this force is always world-space up (it opposes gravity). -pub fn buoyancy( - gravity_acceleration: Acceleration, - displaced_volume: Volume, - ambient_density: MassDensity, -) -> Vec3 { - Vec3::Y - * (displaced_volume.get::() - * ambient_density.get::() - * gravity_acceleration.get::()) -} - -/// Fraction of standard gravity at an altitude (m) above mean sea level. -pub fn local_gravity(altitude: Length) -> Acceleration { - *EARTH_RADIUS_M / (*EARTH_RADIUS_M + altitude) * *STANDARD_GRAVITY -} - -fn weight(mass: uom::si::f32::Mass, gravity: Acceleration) -> Vec3 { - Vec3::NEG_Y * mass.get::() * gravity.get::() -} - -#[derive(Component, Default)] -pub struct DragCoefficient(pub Scalar); diff --git a/crates/buoy-physics/src/geometry.rs b/crates/buoy-physics/src/geometry.rs index 28b4839..4f69f47 100644 --- a/crates/buoy-physics/src/geometry.rs +++ b/crates/buoy-physics/src/geometry.rs @@ -1,11 +1,24 @@ -#![allow(dead_code)] - +use glam::{Vec2, Vec3}; use crate::constants::PI; -use avian3d::{ - parry::shape::{ShapeType, SharedShape}, - prelude::*, -}; -use bevy::math::{Quat, Vec2, Vec3}; + +/// Generate the 8 corners of a cuboid from its half-size +#[allow(dead_code)] +pub fn generate_cuboid_corners(half_size: Vec3) -> Vec { + let x = half_size.x; + let y = half_size.y; + let z = half_size.z; + + vec![ + Vec3::new(-x, -y, -z), // 0: bottom-back-left + Vec3::new(x, -y, -z), // 1: bottom-back-right + Vec3::new(x, y, -z), // 2: top-back-right + Vec3::new(-x, y, -z), // 3: top-back-left + Vec3::new(-x, -y, z), // 4: bottom-front-left + Vec3::new(x, -y, z), // 5: bottom-front-right + Vec3::new(x, y, z), // 6: top-front-right + Vec3::new(-x, y, z), // 7: top-front-left + ] +} pub fn sphere_volume(radius: f32) -> f32 { (4.0 / 3.0) * PI * f32::powf(radius, 3.0) @@ -26,6 +39,61 @@ pub fn sphere_surface_area(radius: f32) -> f32 { 4.0 * PI * f32::powf(radius, 2.0) } +// Volume calculation functions for different shapes +pub fn cuboid_volume(half_size: Vec3) -> f32 { + 8.0 * half_size.x * half_size.y * half_size.z +} + +pub fn cylinder_volume(radius: f32, height: f32) -> f32 { + PI * radius * radius * height +} + +pub fn capsule_volume(radius: f32, half_height: f32) -> f32 { + let cylinder_vol = cylinder_volume(radius, 2.0 * half_height); + let sphere_vol = sphere_volume(radius); + cylinder_vol + sphere_vol +} + +/// Calculate the volume of a convex polyhedron using the divergence theorem +/// V = (1/3) * sum(face_area * face_normal โ‹… face_centroid) +pub fn convex_polyhedron_volume(points: &[Vec3], faces: &[Vec]) -> f32 { + let mut volume = 0.0; + + for face_indices in faces { + if face_indices.len() < 3 { + continue; + } + + // Calculate face centroid + let mut centroid = Vec3::ZERO; + for &idx in face_indices { + centroid += points[idx]; + } + centroid /= face_indices.len() as f32; + + // Calculate face normal using first three vertices + let v0 = points[face_indices[0]]; + let v1 = points[face_indices[1]]; + let v2 = points[face_indices[2]]; + let normal = (v1 - v0).cross(v2 - v0).normalize(); + + // Calculate face area + let mut face_area = 0.0; + for i in 0..face_indices.len() { + let p1 = points[face_indices[i]]; + let p2 = points[face_indices[(i + 1) % face_indices.len()]]; + face_area += p1.cross(p2).length(); + } + face_area *= 0.5; + + // Add contribution to volume + volume += face_area * normal.dot(centroid); + } + + volume / 3.0 +} + + pub fn projected_area_of_sphere(radius: f32, _direction: Vec3) -> f32 { PI * radius * radius // ฯ€rยฒ } @@ -56,51 +124,6 @@ pub fn projected_area_of_cylinder(radius: f32, height: f32, direction: Vec3) -> cylindrical_area + end_caps_area } -pub fn projected_area_of_convex_hull(points: &[Vec3], direction: Vec3) -> f32 { - // Project vertices onto plane perpendicular to direction - let projected_points = project_vertices_onto_plane(points, direction); - - // Calculate area of the projected polygon - polygon_area(&projected_points) -} - -/// Generate the 8 corners of a cuboid from its half-size -fn generate_cuboid_corners(half_size: Vec3) -> Vec { - let x = half_size.x; - let y = half_size.y; - let z = half_size.z; - - vec![ - Vec3::new(-x, -y, -z), // 0: bottom-back-left - Vec3::new(x, -y, -z), // 1: bottom-back-right - Vec3::new(x, y, -z), // 2: top-back-right - Vec3::new(-x, y, -z), // 3: top-back-left - Vec3::new(-x, -y, z), // 4: bottom-front-left - Vec3::new(x, -y, z), // 5: bottom-front-right - Vec3::new(x, y, z), // 6: top-front-right - Vec3::new(-x, y, z), // 7: top-front-left - ] -} - -/// Feature-based projected area calculation using Parry-style silhouette method -/// For each face: projected_area += face_area ร— |face_normal โ‹… dir| -fn feature_based_projected_area(_shape: &SharedShape, _direction: Vec3) -> Option { - // TODO: Implement feature-based calculation using shape topology - // This would use the shape's faces, edges, and vertices directly - // For now, return None to fall back to AABB projection - None -} - -/// AABB projected area fallback -/// Use the projected area of the collider's AABB in the direction -fn aabb_projected_area(collider: &Collider, direction: Vec3) -> f32 { - // Get the AABB of the collider - let aabb = collider.aabb(Vec3::ZERO, Quat::from_rotation_arc(Vec3::Z, direction)); - let half_size = aabb_half_extents(&aabb); - let corners = generate_cuboid_corners(half_size); - projected_area_of_convex_hull(&corners, direction) -} - /// Projected area for capsule shape pub fn projected_area_of_capsule(radius: f32, half_height: f32, direction: Vec3) -> f32 { let nx = direction.x.abs(); @@ -116,80 +139,6 @@ pub fn projected_area_of_capsule(radius: f32, half_height: f32, direction: Vec3) cylindrical_area + caps_area } -/// Calculate the projected area of a collider in the direction of motion. -/// Used for aerodynamic drag force calculations: F_drag = 0.5 * rho * v^2 * C_d * A_projected -pub fn projected_area(collider: &Collider, direction: Vec3) -> f32 { - let normalized_direction = direction.normalize(); - let shape = collider.shape(); - let shape_type = shape.shape_type(); - - match shape_type { - // 1. Sphere โ€“ Use analytical projection: ฯ€ * rยฒ - ShapeType::Ball => { - if let Some(radius) = shape.as_ball() { - projected_area_of_sphere(radius.radius, normalized_direction) - } else { - aabb_projected_area(collider, normalized_direction) - } - } - - // 2. Cuboid โ€“ Project all 8 corners, form 2D convex hull, return area - ShapeType::Cuboid => { - if let Some(cuboid) = shape.as_cuboid() { - let corners = generate_cuboid_corners(cuboid.half_extents.into()); - projected_area_of_convex_hull(&corners, normalized_direction) - } else { - aabb_projected_area(collider, normalized_direction) - } - } - - // 3. Cylinder โ€“ Use analytical formula - ShapeType::Cylinder => { - if let Some(cylinder) = shape.as_cylinder() { - projected_area_of_cylinder( - cylinder.radius, - cylinder.half_height * 2.0, - normalized_direction, - ) - } else { - aabb_projected_area(collider, normalized_direction) - } - } - - // 4. Capsule โ€“ Use analytical formula - ShapeType::Capsule => { - if let Some(capsule) = shape.as_capsule() { - projected_area_of_capsule(capsule.radius, capsule.half_height(), normalized_direction) - } else { - aabb_projected_area(collider, normalized_direction) - } - } - - // 5. ConvexHull โ€“ Use Parry-style silhouette method - ShapeType::ConvexPolyhedron => { - if let Some(area) = feature_based_projected_area(shape, normalized_direction) { - area - } else { - // Fall back to AABB projection - aabb_projected_area(collider, normalized_direction) - } - } - - // 6. Other convex shapes โ€“ Try feature-based first - _ if shape.is_convex() => { - if let Some(area) = feature_based_projected_area(shape, normalized_direction) { - area - } else { - // Fall back to AABB projection - aabb_projected_area(collider, normalized_direction) - } - } - - // 7. Non-convex shapes โ€“ Use AABB fallback - _ => aabb_projected_area(collider, normalized_direction), - } -} - pub fn project_vertices_onto_plane(points: &[Vec3], normal: Vec3) -> Vec { let normalized_normal = normal.normalize(); @@ -224,114 +173,3 @@ pub fn polygon_area(points: &[Vec2]) -> f32 { } 0.5 * area.abs() } -// Volume calculation functions for different shapes - -pub fn cuboid_volume(half_size: Vec3) -> f32 { - 8.0 * half_size.x * half_size.y * half_size.z -} - -pub fn cylinder_volume(radius: f32, height: f32) -> f32 { - PI * radius * radius * height -} - -pub fn capsule_volume(radius: f32, half_height: f32) -> f32 { - let cylinder_vol = cylinder_volume(radius, 2.0 * half_height); - let sphere_vol = sphere_volume(radius); - cylinder_vol + sphere_vol -} - -/// Calculate the volume of a convex polyhedron using the divergence theorem -/// V = (1/3) * sum(face_area * face_normal โ‹… face_centroid) -pub fn convex_polyhedron_volume(points: &[Vec3], faces: &[Vec]) -> f32 { - let mut volume = 0.0; - - for face_indices in faces { - if face_indices.len() < 3 { - continue; - } - - // Calculate face centroid - let mut centroid = Vec3::ZERO; - for &idx in face_indices { - centroid += points[idx]; - } - centroid /= face_indices.len() as f32; - - // Calculate face normal using first three vertices - let v0 = points[face_indices[0]]; - let v1 = points[face_indices[1]]; - let v2 = points[face_indices[2]]; - let normal = (v1 - v0).cross(v2 - v0).normalize(); - - // Calculate face area - let mut face_area = 0.0; - for i in 0..face_indices.len() { - let p1 = points[face_indices[i]]; - let p2 = points[face_indices[(i + 1) % face_indices.len()]]; - face_area += p1.cross(p2).length(); - } - face_area *= 0.5; - - // Add contribution to volume - volume += face_area * normal.dot(centroid); - } - - volume / 3.0 -} - -/// Calculate the volume of a collider -pub fn collider_volume(collider: &Collider) -> f32 { - let shape = collider.shape(); - let shape_type = shape.shape_type(); - - match shape_type { - ShapeType::Ball => { - if let Some(ball) = shape.as_ball() { - sphere_volume(ball.radius) - } else { - 0.0 - } - } - - ShapeType::Cuboid => { - if let Some(cuboid) = shape.as_cuboid() { - cuboid_volume(cuboid.half_extents.into()) - } else { - 0.0 - } - } - - ShapeType::Cylinder => { - if let Some(cylinder) = shape.as_cylinder() { - cylinder_volume(cylinder.radius, cylinder.half_height * 2.0) - } else { - 0.0 - } - } - - ShapeType::Capsule => { - if let Some(capsule) = shape.as_capsule() { - capsule_volume(capsule.radius, capsule.half_height()) - } else { - 0.0 - } - } - - _ => { - // For other shapes, use AABB volume as fallback - let aabb = collider.aabb(Vec3::ZERO, Quat::IDENTITY); - aabb_volume(&aabb) * 0.5 // Conservative estimate - } - } -} - -/// Get the half-extents of an AABB (half of the size) -pub fn aabb_half_extents(aabb: &ColliderAabb) -> Vec3 { - aabb.size() * 0.5 -} - -/// Calculate the volume of an AABB -pub fn aabb_volume(aabb: &ColliderAabb) -> f32 { - let size = aabb.size(); - size.x * size.y * size.z -} diff --git a/crates/buoy-physics/src/ideal_gas.rs b/crates/buoy-physics/src/ideal_gas.rs index 707c317..446df23 100644 --- a/crates/buoy-physics/src/ideal_gas.rs +++ b/crates/buoy-physics/src/ideal_gas.rs @@ -1,11 +1,6 @@ //! Ideal gas equations. -#![allow(dead_code)] use std::ops::{Div, Mul}; - -use avian3d::math::Scalar; -use bevy::{asset::Asset, prelude::*, reflect::TypePath}; -use serde::Deserialize; use uom::si::{ f32::{ ThermodynamicTemperature, Pressure, Mass, Volume, @@ -24,10 +19,6 @@ use crate::{ geometry::sphere_volume, }; -pub(crate) fn plugin(_app: &mut App) { - // nothing yet -} - /// Volume (mยณ) of an ideal gas from its temperature (K), pressure (Pa), /// mass (kg) and molar mass (kg/mol). pub fn ideal_gas_volume( @@ -50,7 +41,7 @@ pub fn ideal_gas_density( } /// Molecular species of a gas. -#[derive(Component, Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct GasSpecies { pub name: String, pub abbreviation: String, @@ -98,31 +89,8 @@ impl Default for GasSpecies { } } -#[derive(Deserialize, Debug, Asset, TypePath, Clone)] -pub struct GasSpeciesConfig { - pub name: String, - pub abbreviation: String, - pub molar_mass: f32, // [kg/mol] -} - -impl GasSpeciesConfig { - pub fn to_species(&self) -> GasSpecies { - GasSpecies { - name: self.name.clone(), - abbreviation: self.abbreviation.clone(), - molar_mass: MolarMass::new::(self.molar_mass), - } - } -} - -#[derive(Deserialize, Debug, Asset, TypePath, Clone)] -pub struct GasPropertiesConfig { - pub gases: Vec, - // materials: Vec, // can be added later -} - /// Properties of an ideal gas per unit mass. -#[derive(Component, Default, Debug, Clone, PartialEq)] +#[derive(Default, Debug, Clone, PartialEq)] pub struct IdealGas { pub species: GasSpecies, pub mass: Mass, diff --git a/crates/buoy-physics/src/lib.rs b/crates/buoy-physics/src/lib.rs index e0ed8df..593baec 100644 --- a/crates/buoy-physics/src/lib.rs +++ b/crates/buoy-physics/src/lib.rs @@ -3,29 +3,5 @@ pub mod atmosphere; pub mod constants; pub mod geometry; pub mod ideal_gas; -pub mod forces; -pub mod prelude { - pub use crate::{ - atmosphere::Atmosphere, - ideal_gas::{GasSpecies, IdealGas}, - }; -} - -use avian3d::prelude::{PhysicsInterpolationPlugin, PhysicsPlugins, PhysicsSet}; -use bevy::{ - app::{PluginGroup, PluginGroupBuilder}, - prelude::*, -}; - -pub struct BuoyPhysicsPlugin; - -impl Plugin for BuoyPhysicsPlugin { - fn build(&self, app: &mut App) { - app.add_plugins(( - PhysicsPlugins::default(), - atmosphere::plugin, - ideal_gas::plugin, - forces::plugin, - )); - } -} +pub mod force; +pub mod material_properties; diff --git a/crates/buoy-physics/src/material_properties.rs b/crates/buoy-physics/src/material_properties.rs index fd52860..e1fe804 100644 --- a/crates/buoy-physics/src/material_properties.rs +++ b/crates/buoy-physics/src/material_properties.rs @@ -1,6 +1,6 @@ /// The skin is the material that composes the outer surface of the balloon. /// TODO: Implement multiple material types, such as latex, polyurethane, etc. -#[derive(Debug, Clone, Reflect)] +#[derive(Debug, Clone)] pub struct Skin { // temperature (K) where the given material fails pub max_temperature: f32, diff --git a/crates/buoy-runtime/Cargo.toml b/crates/buoy-runtime/Cargo.toml index f6eeac7..17e6c32 100644 --- a/crates/buoy-runtime/Cargo.toml +++ b/crates/buoy-runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "buoy-runtime" -version = "0.3.0" +version = "0.4.0" description = "Runtime for the Buoy simulator" edition = { workspace = true } authors = { workspace = true } @@ -16,7 +16,6 @@ buoy-physics = { path = "../buoy-physics" } bevy = { workspace = true } avian3d = { workspace = true } uom = { workspace = true } -bevy_common_assets = { workspace = true } serde = { workspace = true } bevy_repl = { version = "0.3.0", features = ["derive"] } diff --git a/crates/buoy-runtime/src/collider.rs b/crates/buoy-runtime/src/collider.rs new file mode 100644 index 0000000..7193cd8 --- /dev/null +++ b/crates/buoy-runtime/src/collider.rs @@ -0,0 +1,166 @@ +#![allow(dead_code)] + +use crate::constants::PI; +use avian3d::{ + parry::shape::{ShapeType, SharedShape}, + prelude::*, +}; +use bevy::math::{Quat, Vec2, Vec3}; + +pub fn projected_area_of_convex_hull(points: &[Vec3], direction: Vec3) -> f32 { + // Project vertices onto plane perpendicular to direction + let projected_points = project_vertices_onto_plane(points, direction); + + // Calculate area of the projected polygon + polygon_area(&projected_points) +} + +/// Feature-based projected area calculation using Parry-style silhouette method +/// For each face: projected_area += face_area ร— |face_normal โ‹… dir| +fn feature_based_projected_area(_shape: &SharedShape, _direction: Vec3) -> Option { + // TODO: Implement feature-based calculation using shape topology + // This would use the shape's faces, edges, and vertices directly + // For now, return None to fall back to AABB projection + None +} + +/// AABB projected area fallback +/// Use the projected area of the collider's AABB in the direction +fn aabb_projected_area(collider: &Collider, direction: Vec3) -> f32 { + // Get the AABB of the collider + let aabb = collider.aabb(Vec3::ZERO, Quat::from_rotation_arc(Vec3::Z, direction)); + let half_size = aabb_half_extents(&aabb); + let corners = generate_cuboid_corners(half_size); + projected_area_of_convex_hull(&corners, direction) +} + +/// Calculate the projected area of a collider in the direction of motion. +/// Used for aerodynamic drag force calculations: F_drag = 0.5 * rho * v^2 * C_d * A_projected +pub fn projected_area(collider: &Collider, direction: Vec3) -> f32 { + let normalized_direction = direction.normalize(); + let shape = collider.shape(); + let shape_type = shape.shape_type(); + + match shape_type { + // 1. Sphere โ€“ Use analytical projection: ฯ€ * rยฒ + ShapeType::Ball => { + if let Some(radius) = shape.as_ball() { + projected_area_of_sphere(radius.radius, normalized_direction) + } else { + aabb_projected_area(collider, normalized_direction) + } + } + + // 2. Cuboid โ€“ Project all 8 corners, form 2D convex hull, return area + ShapeType::Cuboid => { + if let Some(cuboid) = shape.as_cuboid() { + let corners = generate_cuboid_corners(cuboid.half_extents.into()); + projected_area_of_convex_hull(&corners, normalized_direction) + } else { + aabb_projected_area(collider, normalized_direction) + } + } + + // 3. Cylinder โ€“ Use analytical formula + ShapeType::Cylinder => { + if let Some(cylinder) = shape.as_cylinder() { + projected_area_of_cylinder( + cylinder.radius, + cylinder.half_height * 2.0, + normalized_direction, + ) + } else { + aabb_projected_area(collider, normalized_direction) + } + } + + // 4. Capsule โ€“ Use analytical formula + ShapeType::Capsule => { + if let Some(capsule) = shape.as_capsule() { + projected_area_of_capsule(capsule.radius, capsule.half_height(), normalized_direction) + } else { + aabb_projected_area(collider, normalized_direction) + } + } + + // 5. ConvexHull โ€“ Use Parry-style silhouette method + ShapeType::ConvexPolyhedron => { + if let Some(area) = feature_based_projected_area(shape, normalized_direction) { + area + } else { + // Fall back to AABB projection + aabb_projected_area(collider, normalized_direction) + } + } + + // 6. Other convex shapes โ€“ Try feature-based first + _ if shape.is_convex() => { + if let Some(area) = feature_based_projected_area(shape, normalized_direction) { + area + } else { + // Fall back to AABB projection + aabb_projected_area(collider, normalized_direction) + } + } + + // 7. Non-convex shapes โ€“ Use AABB fallback + _ => aabb_projected_area(collider, normalized_direction), + } +} + +/// Calculate the volume of a collider +pub fn collider_volume(collider: &Collider) -> f32 { + let shape = collider.shape(); + let shape_type = shape.shape_type(); + + match shape_type { + ShapeType::Ball => { + if let Some(ball) = shape.as_ball() { + sphere_volume(ball.radius) + } else { + 0.0 + } + } + + ShapeType::Cuboid => { + if let Some(cuboid) = shape.as_cuboid() { + cuboid_volume(cuboid.half_extents.into()) + } else { + 0.0 + } + } + + ShapeType::Cylinder => { + if let Some(cylinder) = shape.as_cylinder() { + cylinder_volume(cylinder.radius, cylinder.half_height * 2.0) + } else { + 0.0 + } + } + + ShapeType::Capsule => { + if let Some(capsule) = shape.as_capsule() { + capsule_volume(capsule.radius, capsule.half_height()) + } else { + 0.0 + } + } + + _ => { + // For other shapes, use AABB volume as fallback + let aabb = collider.aabb(Vec3::ZERO, Quat::IDENTITY); + aabb_volume(&aabb) * 0.5 // Conservative estimate + } + } +} + +/// Get the half-extents of an AABB (half of the size) +pub fn aabb_half_extents(aabb: &ColliderAabb) -> Vec3 { + aabb.size() * 0.5 +} + +/// Calculate the volume of an AABB +pub fn aabb_volume(aabb: &ColliderAabb) -> f32 { + let size = aabb.size(); + size.x * size.y * size.z +} diff --git a/crates/buoy-runtime/src/config.rs b/crates/buoy-runtime/src/config.rs index e69de29..bb4d9ae 100644 --- a/crates/buoy-runtime/src/config.rs +++ b/crates/buoy-runtime/src/config.rs @@ -0,0 +1,23 @@ + +#[derive(Deserialize, Debug, Asset, TypePath, Clone)] +pub struct GasSpeciesConfig { + pub name: String, + pub abbreviation: String, + pub molar_mass: f32, // [kg/mol] +} + +impl GasSpeciesConfig { + pub fn to_species(&self) -> GasSpecies { + GasSpecies { + name: self.name.clone(), + abbreviation: self.abbreviation.clone(), + molar_mass: MolarMass::new::(self.molar_mass), + } + } +} + +#[derive(Deserialize, Debug, Asset, TypePath, Clone)] +pub struct GasPropertiesConfig { + pub gases: Vec, + // materials: Vec, // can be added later +} diff --git a/crates/buoy-runtime/src/forces.rs b/crates/buoy-runtime/src/forces.rs new file mode 100644 index 0000000..a6380d5 --- /dev/null +++ b/crates/buoy-runtime/src/forces.rs @@ -0,0 +1,44 @@ +use uom::si::{ + acceleration::meter_per_second_squared, area::square_meter, f32::*, length::meter, + mass::kilogram, mass_density::kilogram_per_cubic_meter, ratio::ratio, volume::cubic_meter, + velocity::meter_per_second, +}; + +use crate::atmosphere::Atmosphere; +use crate::constants::{EARTH_RADIUS_M, STANDARD_GRAVITY}; +use crate::geometry::{projected_area, collider_volume}; + +fn net_force( + mut query: Query<( + &mut ExternalForce, + &Position, + &ComputedMass, + &Collider, + &LinearVelocity, + &DragCoefficient, + )>, + atmosphere: Res, +) { + for (mut external_force, position, computed_mass, collider, velocity, drag_coefficient) in + query.iter_mut() + { + let mass = uom::si::f32::Mass::new::(computed_mass.value()); + let drag_area = + uom::si::f32::Area::new::(projected_area(collider, velocity.0)); + let gravity = local_gravity(uom::si::f32::Length::new::(position.y)); + let weight = weight(mass, gravity); + let buoyancy = buoyancy(gravity, uom::si::f32::Volume::new::(collider_volume(collider)), atmosphere.density(position.0)); + let drag = drag( + velocity.0, + atmosphere.density(position.0), + drag_area, + drag_coefficient.0, + ); + let net_force = weight + buoyancy + drag; + + external_force.apply_force(net_force); + } +} + +#[derive(Component, Default)] +pub struct DragCoefficient(pub Scalar); diff --git a/crates/buoy-runtime/src/format.rs b/crates/buoy-runtime/src/format.rs index d91a715..f71e74f 100644 --- a/crates/buoy-runtime/src/format.rs +++ b/crates/buoy-runtime/src/format.rs @@ -11,7 +11,7 @@ impl Plugin for PrettyPrintPlugin { } } -#[derive(Component, Debug, Reflect)] +#[derive(Debug, Reflect)] pub struct UomQuantity { value: f32, unit: String, diff --git a/crates/buoy-runtime/src/lib.rs b/crates/buoy-runtime/src/lib.rs index 84903cb..3d5fa0e 100644 --- a/crates/buoy-runtime/src/lib.rs +++ b/crates/buoy-runtime/src/lib.rs @@ -1,9 +1,11 @@ pub mod format; -pub mod sequencing; -pub mod objects; -pub mod config; +// pub mod sequencing; +// pub mod objects; +// pub mod config; +// pub mod collider; use bevy::{app::PluginGroupBuilder, prelude::*}; +use bevy_repl::prelude::*; /// A custom flavor of Bevy's DefaultPlugins that includes common plugins used by Buoy. pub struct BuoyDefaultPlugins; @@ -12,9 +14,8 @@ impl PluginGroup for BuoyDefaultPlugins { fn build(self) -> PluginGroupBuilder { PluginGroupBuilder::start::() .add(RuntimePlugin) - .add(sequencing::plugin) .add(format::PrettyPrintPlugin) - .add_group(bevy_repl::prelude::ReplPlugins) + .add_group(ReplPlugins) } } @@ -33,3 +34,16 @@ pub enum RuntimeState { Running, Faulted, } + +// pub struct BuoyPhysicsPlugin; + +// impl Plugin for BuoyPhysicsPlugin { +// fn build(&self, app: &mut App) { +// app.add_plugins(( +// PhysicsPlugins::default(), +// buoy_physics::atmosphere::plugin, +// buoy_physics::ideal_gas::plugin, +// buoy_physics::forces::plugin, +// )); +// } +// } diff --git a/crates/buoy-physics/src/mesh_utils.rs b/crates/buoy-runtime/src/mesh_utils.rs similarity index 100% rename from crates/buoy-physics/src/mesh_utils.rs rename to crates/buoy-runtime/src/mesh_utils.rs diff --git a/crates/buoy-runtime/src/sequencing.rs b/crates/buoy-runtime/src/sequencing.rs index b61f63c..b2b02b2 100644 --- a/crates/buoy-runtime/src/sequencing.rs +++ b/crates/buoy-runtime/src/sequencing.rs @@ -8,7 +8,6 @@ //! //! 1. **Input** - User input, events, and command parsing //! 2. **Physics** - Physics simulation and calculations -//! 3. **Console** - REPL command execution and output //! 4. **Render** - Scene rendering and visual updates //! 5. **Ui** - UI rendering and interaction processing //! @@ -19,9 +18,6 @@ //! //! // Add a system to a specific pipeline stage //! app.add_systems(Update, my_system.in_set(SimulationPipeline::Physics)); -//! -//! // Or use the helper function -//! SimulationPipeline::add_system_to_stage(&mut app, SimulationPipeline::Input, input_system); //! ``` use avian3d::prelude::*; diff --git a/crates/buoy-ui/Cargo.toml b/crates/buoy-ui/Cargo.toml index 2ffd098..78d746a 100644 --- a/crates/buoy-ui/Cargo.toml +++ b/crates/buoy-ui/Cargo.toml @@ -30,7 +30,6 @@ bevy = { workspace = true, features = [ ] } avian3d = { workspace = true, optional = true, features = ["bevy_picking"] } bevy_egui = { version = "0.35.0" } -bevy_common_assets = { workspace = true } bevy-inspector-egui = { version = "0.32.0", optional = true, features = [ "highlight_changes", ] } @@ -38,7 +37,6 @@ bevy-inspector-egui = { version = "0.32.0", optional = true, features = [ [features] default = ["dev"] dev = [ - "buoy-physics/dev", "buoy-runtime/dev", "bevy/dynamic_linking", "avian3d/diagnostic_ui",