diff --git a/Cargo.toml b/Cargo.toml index 7434818..541053a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,10 @@ edition = "2021" readme = "README.md" license-file = "LICENSE" +[[bin]] +name = "yahs" +path = "src/main.rs" + [features] default = [ # Default to a native dev build. @@ -17,35 +21,19 @@ dev = [ # library. "bevy/dynamic_linking", "bevy/bevy_dev_tools", + "bevy/sysinfo_plugin", ] dev_native = [ "dev", # Enable system information plugin for native dev builds. "bevy/sysinfo_plugin", - "iyes_perf_ui/sysinfo", ] -config-files = ["ron", "bevy_common_assets", "serde"] -inspect = ["bevy-inspector-egui", "bevy_panorbit_camera/bevy_egui"] [dependencies] -# core dependencies -bevy = "0.14.2" -bevy-trait-query = "0.6.0" -# physics dependencies -avian3d = { version = "0.1.2", features = ["debug-plugin"] } -# ui dependencies -bevy_panorbit_camera = { version = "0.20.0" } -bevy-inspector-egui = { version = "0.27.0", features = ["highlight_changes"], optional = true } -iyes_perf_ui = "0.3.0" -# file io dependencies -bevy_common_assets = { version = "0.11.0", features = ["ron"], optional = true } -ron = { version = "0.8.1", optional = true } -serde = { version = "1.0", features = ["derive"], optional = true } -parry3d = { version = "0.17.2", features = ["parallel"] } - -[[bin]] -name = "yahs" -path = "src/main.rs" +bevy = "0.15.0-rc.3" +avian3d = { git = "https://github.com/Jondolf/avian.git", branch = "bevy-0.15", features = ["debug-plugin"] } +bevy-trait-query = { git = "https://github.com/JoJoJet/bevy-trait-query.git", branch = "bevy-0.15-rc" } +iyes_perf_ui = { git = "https://github.com/JohnathanFL/iyes_perf_ui.git", branch = "main" } # ----------------------------------------------------------------------------- # Some Bevy optimizations @@ -66,7 +54,7 @@ type_complexity = "allow" # Compile with Performance Optimizations: # https://bevyengine.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations -# Enable a small amount of optimization in the dev profile. +# Enable a small amount of optimization in the dev profile for our code. [profile.dev] opt-level = 1 diff --git a/docs/devlog.md b/docs/devlog.md index 4824b0d..58ca661 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -1,5 +1,165 @@ # development log +## 2024-11-26 + +I added a `GasMonitor` to the UI for displaying the gas properties in real time. +With that in place, I can now notice that the balloon's density is reported as +NaN, which is probably why buoyancy is not working. I think I found the bug. The +density was not being updated in the system that updates gas properties from +the atmosphere. Fixed that, but buoyancy is still pegged at 0. Curious, the +buoyant system was querying `With`, maybe something is messed up +with the query because this system is definitely running every frame. A default +`ForceBundle` is supposed to be added to the balloon when it is added to the +world, so I'm curious why the buoyancy component is not being added. The +`Weight` component is being added... Ah, the buoyancy system is also querying +`Volume` components, weight is not. Maybe the query isn't turning up the bodies +because the `Volume` component is not being added? With Bevy 0.15 we can enforce +such a thing with the `#[require(x)]` attribute. Turns out there's no system +that updates the volume because I wanted to calculate it from the balloon's +primitive shape. I'll change buoyancy to query the balloon and get its volume +instead. That fixed the buoyancy system so it runs, but the force results might +not be correct. + +```sh +INFO yahs::simulator::forces: Weight [0, -5.1347504, 0] +INFO yahs::simulator::forces: Buoyancy [0, 3888.388, 0] +INFO yahs::simulator::forces: Drag [NaN, NaN, NaN] +``` + +I traced the drag force NaN to the drag area returning nan from +`balloon.shape.diameter()` at startup before the shape is set up. + +```sh + INFO yahs::simulator::forces::aero: balloon shape: Sphere { radius: NaN } +ERROR yahs::simulator::forces: Drag has NaN magnitude! +ERROR yahs::simulator::forces: Buoyancy has NaN magnitude! + INFO yahs::simulator::forces::aero: balloon shape: Sphere { radius: 4.2564797 } + INFO yahs::simulator::forces::aero: balloon shape: Sphere { radius: 4.2810054 } + INFO yahs::simulator::forces::aero: balloon shape: Sphere { radius: 4.2810054 } +``` + +I enforced that the mesh volumes are updated before the forces are calculated, +but there might be a few frames at the beginning where the Mesh isn't +initialized yet. Maybe it works a little differently because it's an asset? +I noticed that the balloon was being spawned after the ground plane, and scene +setup was not constrained to run in any system set. So the forces probably ran +before the scene was done loading. I added a few conditions: + +- The app starts in the `Loading` state. +- The scene advances to the `Running` state when it is done setting up the scene. +- The physics systems only run when the app is in the `Running` state. + +Weird, something else is going on. The balloon shape is a real number when it is +spawned and before the forces run, but when the forces run it is NaN. + +```sh + INFO bevy_winit::system: Creating new window "🎈" (0v1#4294967296) + INFO yahs::app3d::scene: sphere: Sphere { radius: 0.5 } + INFO bevy_dev_tools::states: yahs::simulator::core::SimState transition: Some(Loading) => Some(Running) + INFO yahs::app3d::scene: sim state: Res(State(Running)) + INFO yahs::app3d::scene: balloon spawned: 19v1 Balloon { material_properties: BalloonMaterial { name: "Latex", max_temperature: 373.0, density: 920.0, emissivity: 0.9, absorptivity: 0.9, thermal_conductivity: 0.13, specific_heat: 2000.0, poissons_ratio: 0.5, elasticity: 10000000.0, max_strain: 0.8, max_stress: 500000.0, thickness: 0.0001 }, shape: Sphere { radius: 0.5 } } Sphere { radius: 0.5 } + INFO yahs::simulator::forces::aero: balloon shape: Sphere { radius: NaN } +ERROR yahs::simulator::forces: Drag has NaN magnitude! +ERROR yahs::simulator::forces: Buoyancy has NaN magnitude! + WARN avian3d::collision::narrow_phase: 18v1#4294967314 (Ground) and 19v1#4294967315 (Balloon) are overlapping at spawn, which can result in explosive behavior. + INFO yahs::app3d::scene: sim state: Res(State(Running)) + INFO yahs::simulator::forces::aero: balloon shape: Sphere { radius: 4.2564797 } + INFO yahs::app3d::scene: sim state: Res(State(Running)) + INFO yahs::simulator::forces::aero: balloon shape: Sphere { radius: 4.2810054 } + INFO yahs::app3d::scene: sim state: Res(State(Running)) + INFO yahs::simulator::forces::aero: balloon shape: Sphere { radius: 4.2810054 } +``` + +I noticed that density is initialized to NaN with the ideal gas and the volume +of the balloon is derived from the ideal gas volume. The volume is calculated +before the force, so that explains where this NaN is coming from. + +Found it. Ideal gas law has pressure in the denominator of the volume equation. +Pressure initializes to zero by default, so the volume is NaN. Changing the +default from zero to the standard atmospheric pressure fixes that issue. + +I think the sim was crashing because it kept defaulting to use way too much +mass in the balloon. I added `with_volume()` to the ideal gas to fix that, so we +can spawn the balloon with a known volume at the default density. + +It works! + +## 2024-11-24 + +I found a Bevy 0.15 branch of +[iyes_perf_ui](https://github.com/IyesGames/iyes_perf_ui/pull/22), yay! + +I spent some time today practicing with Events and Observers by adding debug UI +toggles. Not very productive but practice is practice. + +## 2024-11-23 + +Now that the basic forces are working, I will add look to adding the other +fundamentals of the flight simulation: + +- Ideal gas law, including expansion of gas volume as pressure changes. +- Stats or plots showing the state of the gas and balloon kinematics over time. +- A payload hanging from a tether would be fun too. For this we can lean on the + Avian [chain_3d](https://github.com/Jondolf/avian/blob/main/examples/chain_3d.rs) + example. + +I upgraded Bevy to 0.15.0-rc.3 and it broke the build, especially with regard to +the avian physics plugins. In migrating to the new Bevy version I simplified +some things, like removing the `crate::properties::Mass` component and instead +using Avian's mass properties. There are some complications because after +upgrading bevy it is crashing due to `Mass` not being a component. I guess mass +properties [being refactored](https://github.com/Jondolf/avian/discussions/499). + +The Avian maintainer created a new crate called +[bevy_heavy](https://github.com/Jondolf/bevy_heavy) that contains the new mass +properties tools that allow for directly updating mass properties on primitive +shapes. Then we can use primitive shapes for mass and volume. That simplifies +things by ~~removing mass, volume, and density as separate components~~. Turns +out it is not that simple and having them as separate components is useful and +clean. It is much simpler to write systems that update these components. + +[Migrating to Bevy `0.15.0-rc.3`](https://github.com/bevyengine/bevy-website/tree/main/release-content/0.15/migration-guides) +is proving to be a bit of a challenge. The main feature from it that I want to +use is the new separation of meshes from rendering, since then we can use meshes +for calculations like volume and drag even if we want to use a CLI and don't +want to render anything. This feature also led to some welcome improvements to +Bevy's meshing tools. + +Something about 0.15 seems to have broken bundles. I wonder if the API changed +or if there's some old garbage hanging around that is causing issues. I wiped +the cache and rebuilt the project (`cargo clean && cargo build`). It turns out +the reason that builds are failing is because some of the 3rd party dependencies +I'm using are not compatible with the new Bevy version. I need to go through and +update them or remove them. + +- [x] `avian3d` -> branch `bevy-0.15` +- [ ] ~~`bevy_heavy`~~ remove for now +- [x] `bevy-trait-query` -> branch `bevy-0.15-rc` +- [ ] ~~`bevy_common_assets`~~ remove for now +- [ ] ~~`bevy_panorbit_camera`~~ remove for now +- [ ] ~~`iyes_perf_ui`~~ remove for now +- [ ] ~~`bevy-inspector-egui`~~ remove for now + +I also removed `serde` from the dependencies. I won't be getting to config files +any time soon. + +It's probably better to not use so many 3rd party plugins. Fortunately most of +these are debug tools and not essential to the simulator. + +This demo does a great job using Egui and debug vectors: +[bevy_motion_matching](https://github.com/kahboon0425/bevy_motion_matching) +something to look into later. + +I'm gonna do it. I'm going to make the simulator and the 3D app separate crates. +There are three reasons for this: + +1. The simulator crate can be used as a library in other projects. +2. The 3D app can be built and run independently of the rest of the code. +3. I want faster compile times. + +Nevermind, it complicated things way too much and was distracting. In the future +it might be worthwhile but it's probably best to just use feature flags anyway. + ## 2024-11-18 again I think I was a bit naive to install `bevy-trait-query`. It works for now but in diff --git a/docs/Drag-Force-in-External-Flows-Lesson-2-Handout.pdf b/docs/reference/Drag-Force-in-External-Flows-Lesson-2-Handout.pdf similarity index 100% rename from docs/Drag-Force-in-External-Flows-Lesson-2-Handout.pdf rename to docs/reference/Drag-Force-in-External-Flows-Lesson-2-Handout.pdf diff --git a/docs/Fluids-lecture.pdf b/docs/reference/Fluids-lecture.pdf similarity index 100% rename from docs/Fluids-lecture.pdf rename to docs/reference/Fluids-lecture.pdf diff --git a/docs/Newtonian_Aerodynamics_for_General_Body_Shapes.pdf b/docs/reference/Newtonian_Aerodynamics_for_General_Body_Shapes.pdf similarity index 100% rename from docs/Newtonian_Aerodynamics_for_General_Body_Shapes.pdf rename to docs/reference/Newtonian_Aerodynamics_for_General_Body_Shapes.pdf diff --git a/docs/newtonian_aero.md b/docs/reference/newtonian_aero.md similarity index 100% rename from docs/newtonian_aero.md rename to docs/reference/newtonian_aero.md diff --git a/src/app3d/camera.rs b/src/app3d/camera.rs new file mode 100644 index 0000000..1f8f018 --- /dev/null +++ b/src/app3d/camera.rs @@ -0,0 +1,20 @@ +use bevy::prelude::*; + +// use crate::controls::CameraControls; + +pub struct CameraPlugin; + +impl Plugin for CameraPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, setup); + } +} + +fn setup(mut commands: Commands) { + commands.spawn(( + // Note we're setting the initial position below with yaw, pitch, and radius, hence + // we don't set transform on the camera. + Camera3d::default(), + Transform::from_xyz(0.0, 20., 50.0).looking_at(Vec3::new(0., 20., 0.), Vec3::Y), + )); +} diff --git a/src/controls.rs b/src/app3d/controls.rs similarity index 81% rename from src/controls.rs rename to src/app3d/controls.rs index 68ea9ae..2a0c010 100644 --- a/src/controls.rs +++ b/src/app3d/controls.rs @@ -8,6 +8,7 @@ impl Plugin for ControlsPlugin { } } +#[allow(dead_code)] #[derive(Resource, Default)] pub struct KeyBindingsConfig { pub camera_controls: CameraControls, @@ -25,9 +26,11 @@ pub struct CameraControls { #[derive(Reflect)] pub struct DebugControls { - pub toggle_wireframe: KeyCode, pub toggle_inspector: KeyCode, + pub toggle_wireframe: KeyCode, + pub toggle_physics_debug: KeyCode, pub toggle_perf_ui: KeyCode, + pub toggle_anything_else: KeyCode, } #[derive(Reflect)] @@ -52,9 +55,11 @@ impl Default for CameraControls { impl Default for DebugControls { fn default() -> Self { Self { - toggle_wireframe: KeyCode::KeyW, - toggle_inspector: KeyCode::KeyI, - toggle_perf_ui: KeyCode::KeyP, + toggle_wireframe: KeyCode::F1, + toggle_inspector: KeyCode::F2, + toggle_physics_debug: KeyCode::F3, + toggle_perf_ui: KeyCode::F4, + toggle_anything_else: KeyCode::F5, } } } diff --git a/src/app3d/mod.rs b/src/app3d/mod.rs index 40c0245..08683ca 100644 --- a/src/app3d/mod.rs +++ b/src/app3d/mod.rs @@ -1,6 +1,23 @@ mod scene; +mod camera; mod ui; +pub mod controls; -// Re-export the plugins so they can be added to the app with `app.add_plugins`. -pub use scene::ScenePlugin; -pub use ui::InterfacePlugins; +use scene::ScenePlugin; +use ui::InterfacePlugin; +use controls::ControlsPlugin; +use camera::CameraPlugin; + +use bevy::app::{PluginGroup, PluginGroupBuilder}; + +pub struct App3dPlugins; + +impl PluginGroup for App3dPlugins { + fn build(self) -> PluginGroupBuilder { + PluginGroupBuilder::start::() + .add(InterfacePlugin) + .add(ControlsPlugin) + .add(ScenePlugin) + .add(CameraPlugin) + } +} diff --git a/src/app3d/scene.rs b/src/app3d/scene.rs new file mode 100644 index 0000000..647fe1c --- /dev/null +++ b/src/app3d/scene.rs @@ -0,0 +1,74 @@ +use avian3d::prelude::*; +use bevy::prelude::*; + +use crate::simulator::*; + +pub struct ScenePlugin; + +impl Plugin for ScenePlugin { + fn build(&self, app: &mut App) { + app.add_systems(Startup, (spawn_balloon, simple_scene)); + // app.add_systems(PostStartup, |mut commands: Commands| { + // commands.set_state(SimState::Running); + // }); + } +} + +/// set up a simple 3D scene +fn simple_scene( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let ground_size = Vec3::new(50.0, 0.1, 50.0); + let plane = meshes.add(Plane3d::default().mesh().size(ground_size.x, ground_size.z).subdivisions(10)); + let plane_material = materials.add(StandardMaterial { + base_color: Color::srgb(0.5, 0.5, 0.5), + perceptual_roughness: 0.5, + ..default() + }); + + // ground + commands.spawn(( + Name::new("Ground"), + RigidBody::Static, + Mesh3d(plane.clone()), + MeshMaterial3d(plane_material.clone()), + ColliderConstructor::TrimeshFromMesh, + )); +} + +fn spawn_balloon( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let debug_material = materials.add(StandardMaterial { + base_color: Color::srgba(1.0, 0.0, 0.0, 0.5), + perceptual_roughness: 0.0, + metallic: 1.0, + ..default() + }); + let sphere = Sphere::default(); + let shape = meshes.add(sphere.mesh().ico(5).unwrap()); + let species = GasSpecies::helium(); + commands.spawn(( + Name::new("Balloon"), + SimulatedBody, + BalloonBundle { + balloon: Balloon { + material_properties: BalloonMaterial::default(), + shape: sphere, + }, + gas: IdealGas::new(species).with_mass(Mass::new(0.01)), + }, + RigidBody::Dynamic, + Collider::sphere(sphere.radius), + Transform { + translation: Vec3::new(0.0, 10.0, 0.0), + ..default() + }, + MeshMaterial3d(debug_material), + Mesh3d(shape), + )); +} diff --git a/src/app3d/scene/camera.rs b/src/app3d/scene/camera.rs deleted file mode 100644 index 8372f55..0000000 --- a/src/app3d/scene/camera.rs +++ /dev/null @@ -1,65 +0,0 @@ -use bevy::prelude::*; -use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; -use avian3d::math::TAU; - -use crate::controls::KeyBindingsConfig; - -pub struct CameraPlugin; - -impl Plugin for CameraPlugin { - fn build(&self, app: &mut App) { - app.add_plugins(PanOrbitCameraPlugin); - app.add_systems(Startup, setup); - app.add_systems(Update, toggle_camera_controls_system); - } -} - -fn setup(mut commands: Commands, key_bindings: Res) { - commands.spawn(( - // Note we're setting the initial position below with yaw, pitch, and radius, hence - // we don't set transform on the camera. - Camera3dBundle::default(), - PanOrbitCamera { - // Set focal point (what the camera should look at) - focus: Vec3::new(0.0, 1.0, 0.0), - // Set the starting position, relative to focus (overrides camera's transform). - yaw: Some(TAU / 8.0), - pitch: Some(TAU / 8.0), - radius: Some(5.0), - // Set limits on rotation and zoom - yaw_upper_limit: Some(TAU / 4.0), - yaw_lower_limit: Some(-TAU / 4.0), - pitch_upper_limit: Some(TAU / 3.0), - pitch_lower_limit: Some(-TAU / 3.0), - zoom_upper_limit: Some(100.0), - zoom_lower_limit: 1.0, - // Adjust sensitivity of controls - orbit_sensitivity: 1.5, - pan_sensitivity: 0.5, - zoom_sensitivity: 0.5, - // Allow the camera to go upside down - allow_upside_down: true, - // Change the controls (these match Blender) - button_orbit: key_bindings.camera_controls.button_orbit, - button_pan: key_bindings.camera_controls.button_pan, - modifier_pan: key_bindings.camera_controls.modifier_pan, - // Reverse the zoom direction - reversed_zoom: false, - ..default() - }, - )); -} - -// This is how you can change config at runtime. -// Press 'T' to toggle the camera zoom direction. -fn toggle_camera_controls_system( - key_input: Res>, - key_bindings: Res, - mut pan_orbit_query: Query<&mut PanOrbitCamera>, -) { - if key_input.just_pressed(key_bindings.camera_controls.toggle_zoom_direction) { - for mut pan_orbit in pan_orbit_query.iter_mut() { - pan_orbit.reversed_zoom = !pan_orbit.reversed_zoom; - } - } -} diff --git a/src/app3d/scene/mod.rs b/src/app3d/scene/mod.rs deleted file mode 100644 index cb7c6da..0000000 --- a/src/app3d/scene/mod.rs +++ /dev/null @@ -1,57 +0,0 @@ -mod camera; - -use avian3d::prelude::*; -use bevy::prelude::*; - -pub struct ScenePlugin; - -impl Plugin for ScenePlugin { - fn build(&self, app: &mut App) { - app.add_plugins(camera::CameraPlugin); - app.add_systems(Startup, simple_scene); - } -} - -/// set up a simple 3D scene -fn simple_scene( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - // light - commands.spawn(( - Name::new("Light"), - PointLightBundle { - point_light: PointLight { - shadows_enabled: true, - ..default() - }, - transform: Transform::from_xyz(4.0, 8.0, 4.0), - ..default() - }, - )); - // ground - let ground_size = Vec3::new(4.0, 0.1, 4.0); - commands.spawn(( - Name::new("Ground"), - PbrBundle { - mesh: meshes.add(Cuboid::new(ground_size.x, ground_size.y, ground_size.z)), - material: materials.add(Color::srgba(0.75, 0.75, 0.75, 0.1)), - transform: Transform::from_translation(Vec3::new(0.0, -2.0, 0.0)), - ..default() - }, - RigidBody::Static, - Collider::cuboid(ground_size.x, ground_size.y, ground_size.z), - )); - commands.spawn(( - Name::new("Ceiling"), - PbrBundle { - mesh: meshes.add(Cuboid::new(ground_size.x, ground_size.y, ground_size.z)), - material: materials.add(Color::srgba(0.75, 0.75, 0.75, 0.1)), - transform: Transform::from_translation(Vec3::new(0.0, 3.0, 0.0)), - ..default() - }, - RigidBody::Static, - Collider::cuboid(ground_size.x, ground_size.y, ground_size.z), - )); -} diff --git a/src/app3d/scene/vectors.rs b/src/app3d/scene/vectors.rs deleted file mode 100644 index 8f21a06..0000000 --- a/src/app3d/scene/vectors.rs +++ /dev/null @@ -1,35 +0,0 @@ -use bevy::{prelude::*, render::primitives::Aabb}; -use avian3d::math::PI; -use crate::simulator::forces::Force; - -pub struct ForceVectorPlugin; - -impl Plugin for ForceVectorPlugin { - fn build(&self, app: &mut App) { - app.add_systems(Update, draw_axes); - } -} - -/// System to visualize all force vectors -// fn visualize_forces( -// gizmos: Res, -// query: Query<(&Transform, &dyn Force)>, -// ) { -// for (transform, all_forces) in query.iter() { -// let origin = transform.translation; -// let segments: Vec<(Vec3, Vec3)> = all_forces.iter().map(|force| { -// let force_vector = force.force(); -// (origin, origin + force_vector) -// }).collect(); - -// } -// } - -// This system draws the axes based on the cube's transform, with length based on the size of -// the entity's axis-aligned bounding box (AABB). -fn draw_axes(mut gizmos: Gizmos, query: Query<(&Transform, &Aabb)>) { - for (&transform, &aabb) in &query { - let length = aabb.half_extents.length() * 0.1; - gizmos.axes(transform, length); - } -} diff --git a/src/app3d/ui/core.rs b/src/app3d/ui/core.rs index b1b8d9b..f9ee765 100644 --- a/src/app3d/ui/core.rs +++ b/src/app3d/ui/core.rs @@ -1,20 +1,21 @@ -use bevy::{app::PluginGroupBuilder, prelude::*}; -use iyes_perf_ui::prelude::*; +use bevy::prelude::*; +// use iyes_perf_ui::prelude::*; -use crate::controls::KeyBindingsConfig; +use crate::app3d::controls::KeyBindingsConfig; use crate::simulator::SimState; use super::*; /// A plugin group that includes all interface-related plugins -pub struct InterfacePlugins; - -impl PluginGroup for InterfacePlugins { - fn build(self) -> PluginGroupBuilder { - PluginGroupBuilder::start::() - .add(CoreUiPlugin) - .add(PausePlayPlugin) - .add(monitors::MonitorsPlugin) +pub struct InterfacePlugin; + +impl Plugin for InterfacePlugin { + fn build(&self, app: &mut App) { + app.add_plugins(( + CoreUiPlugin, + PausePlayPlugin, + monitors::MonitorsPlugin, + )); } } @@ -25,7 +26,7 @@ pub struct CoreUiPlugin; impl Plugin for CoreUiPlugin { fn build(&self, app: &mut App) { app.add_plugins(( - PerfUiPlugin, + // PerfUiPlugin, #[cfg(feature = "dev")] dev_tools::plugin, )); @@ -50,7 +51,7 @@ fn toggle_pause( match sim_state.as_ref().get() { SimState::Stopped => next_state.set(SimState::Running), SimState::Running => next_state.set(SimState::Stopped), - _ => () + _ => next_state.set(SimState::Running) } } } diff --git a/src/app3d/ui/dev_tools.rs b/src/app3d/ui/dev_tools.rs index dd4dae6..1243a9f 100644 --- a/src/app3d/ui/dev_tools.rs +++ b/src/app3d/ui/dev_tools.rs @@ -1,9 +1,13 @@ //! Development tools for the game. This plugin is only enabled in dev builds. +use avian3d::debug_render::PhysicsDebugPlugin; +#[cfg(not(target_arch = "wasm32"))] +use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin}; #[allow(unused_imports)] use bevy::{ - // dev_tools::states::log_transitions, + color::palettes::basic::*, + dev_tools::states::log_transitions, diagnostic::{ - FrameTimeDiagnosticsPlugin, EntityCountDiagnosticsPlugin, + EntityCountDiagnosticsPlugin, FrameTimeDiagnosticsPlugin, SystemInformationDiagnosticsPlugin, }, input::common_conditions::input_just_pressed, @@ -11,12 +15,7 @@ use bevy::{ }; use iyes_perf_ui::prelude::*; -#[cfg(not(target_arch = "wasm32"))] -use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin}; - -use avian3d::debug_render::PhysicsDebugPlugin; - -use crate::controls::KeyBindingsConfig; +use crate::{app3d::controls::KeyBindingsConfig, simulator::SimState}; pub(super) fn plugin(app: &mut App) { // Toggle the debug overlay for UI. @@ -26,83 +25,89 @@ pub(super) fn plugin(app: &mut App) { // performance FrameTimeDiagnosticsPlugin, EntityCountDiagnosticsPlugin, - SystemInformationDiagnosticsPlugin, // rendering #[cfg(not(target_arch = "wasm32"))] WireframePlugin, )); - app.add_systems( - Update, - toggle_debug_ui - .before(iyes_perf_ui::PerfUiSet::Setup), - ); + + app.init_resource::(); + app.add_event::(); + app.add_event::(); + app.add_observer(spawn_perf_ui); + app.add_observer(despawn_perf_ui); + + app.add_systems(Update, log_transitions::); // Wireframe doesn't work on WASM #[cfg(not(target_arch = "wasm32"))] - app.add_systems( - Update, - toggle_wireframe, - ); + app.add_systems(Update, toggle_debug_ui); + // #[cfg(feature = "inspect")] + // { + // use bevy_inspector_egui::quick::WorldInspectorPlugin; + // app.add_plugins(WorldInspectorPlugin::new()); + // } +} - #[cfg(feature = "inspect")] - { - use bevy_inspector_egui::quick::WorldInspectorPlugin; - app.add_plugins(WorldInspectorPlugin::new()); - } +#[derive(Debug, Default, Resource)] +struct DebugState { + wireframe: bool, + physics_debug: bool, + perf_ui: bool, } -/// Toggle the debug overlay +#[allow(dead_code)] +#[derive(Component, Default)] +struct DebugUi; + +#[cfg(not(target_arch = "wasm32"))] fn toggle_debug_ui( mut commands: Commands, - q_root: Query>, + mut wireframe_config: ResMut, + mut debug_state: ResMut, key_input: Res>, key_bindings: Res, ) { + if key_input.just_pressed(key_bindings.debug_controls.toggle_wireframe) { + debug_state.wireframe = !debug_state.wireframe; + wireframe_config.global = !wireframe_config.global; + warn!("wireframe: {}", debug_state.wireframe); + } + + if key_input.just_pressed(key_bindings.debug_controls.toggle_physics_debug) { + debug_state.physics_debug = !debug_state.physics_debug; + warn!("physics debug: {} - not implemented", debug_state.physics_debug); + } + if key_input.just_pressed(key_bindings.debug_controls.toggle_perf_ui) { - if let Ok(e) = q_root.get_single() { - // despawn the existing Perf UI - commands.entity(e).despawn_recursive(); + debug_state.perf_ui = !debug_state.perf_ui; + warn!("perf ui: {}", debug_state.perf_ui); + if debug_state.perf_ui { + commands.trigger(SpawnPerfUi); } else { - // create a simple Perf UI with default settings - // and all entries provided by the crate: - commands.spawn(( - PerfUiRoot { - // set a fixed width to make all the bars line up - values_col_width: Some(160.0), - ..Default::default() - }, - // when we have lots of entries, we have to group them - // into tuples, because of Bevy Rust syntax limitations - ( - PerfUiWidgetBar::new(PerfUiEntryFPS::default()), - PerfUiWidgetBar::new(PerfUiEntryFPSWorst::default()), - PerfUiWidgetBar::new(PerfUiEntryFrameTime::default()), - PerfUiWidgetBar::new(PerfUiEntryFrameTimeWorst::default()), - PerfUiWidgetBar::new(PerfUiEntryEntityCount::default()), - ), - ( - PerfUiEntryRunningTime::default(), - PerfUiEntryClock::default(), - ), - ( - PerfUiEntryCursorPosition::default(), - // PerfUiEntryWindowResolution::default(), - // PerfUiEntryWindowScaleFactor::default(), - // PerfUiEntryWindowMode::default(), - // PerfUiEntryWindowPresentMode::default(), - ), - )); + commands.trigger(DespawnPerfUi); } } } -#[cfg(not(target_arch = "wasm32"))] -fn toggle_wireframe( - mut wireframe_config: ResMut, - key_input: Res>, - key_bindings: Res, -) { - if key_input.just_pressed(key_bindings.debug_controls.toggle_wireframe) { - wireframe_config.global = !wireframe_config.global; +#[derive(Event, Default)] +struct SpawnPerfUi; + +fn spawn_perf_ui(_trigger: Trigger, mut commands: Commands) { + info!("spawn_perf_ui"); + warn!("spawning perf ui DOES NOT WORK"); + commands.spawn((DebugUi, + PerfUiRoot::default(), + PerfUiEntryFPS::default(), + PerfUiEntryClock::default(), + )); +} + +#[derive(Event, Default)] +struct DespawnPerfUi; + +fn despawn_perf_ui(_trigger: Trigger, mut commands: Commands, ui: Query, With)>) { + info!("despawn_perf_ui"); + for ui in ui.iter() { + commands.entity(ui).despawn_recursive(); } } diff --git a/src/app3d/ui/mod.rs b/src/app3d/ui/mod.rs index af959cc..a4fcc13 100644 --- a/src/app3d/ui/mod.rs +++ b/src/app3d/ui/mod.rs @@ -4,4 +4,4 @@ mod monitors; #[cfg(feature = "dev")] mod dev_tools; -pub use core::InterfacePlugins; +pub use core::InterfacePlugin; diff --git a/src/app3d/ui/monitors.rs b/src/app3d/ui/monitors.rs index a344b19..0542daa 100644 --- a/src/app3d/ui/monitors.rs +++ b/src/app3d/ui/monitors.rs @@ -1,7 +1,7 @@ //! UI for monitoring the simulation. #![allow(unused_imports)] use bevy::{ - ecs::system::{lifetimeless::SRes, SystemParam}, + ecs::system::{SystemParam, lifetimeless::{SQuery, SRes}}, prelude::*, }; use iyes_perf_ui::{entry::PerfUiEntry, prelude::*, utils::format_pretty_float}; @@ -9,17 +9,19 @@ use iyes_perf_ui::{entry::PerfUiEntry, prelude::*, utils::format_pretty_float}; use crate::simulator::{ forces::{Buoyancy, Drag, Force, Weight}, SimState, SimulatedBody, + ideal_gas::IdealGas, + balloon::Balloon, }; pub struct MonitorsPlugin; impl Plugin for MonitorsPlugin { fn build(&self, app: &mut App) { + app.add_plugins(PerfUiPlugin); app.add_perf_ui_simple_entry::(); app.add_perf_ui_simple_entry::(); + app.add_perf_ui_simple_entry::(); app.add_systems(Startup, spawn_monitors); - app.add_systems(Update, update_force_monitor_values); - app.init_resource::(); } } @@ -31,6 +33,7 @@ fn spawn_monitors(mut commands: Commands) { }, SimStateMonitor::default(), ForceMonitor::default(), + GasMonitor::default(), )); } @@ -58,7 +61,7 @@ impl Default for SimStateMonitor { label: String::new(), display_units: false, threshold_highlight: Some(10.0), - color_gradient: ColorGradient::new_preset_gyr(0.0, 10.0, 100.0).unwrap(), + color_gradient: ColorGradient::new_preset_gyr(0.0, 5.0, 10.0).unwrap(), digits: 7, precision: 0, sort_key: iyes_perf_ui::utils::next_sort_key(), @@ -84,9 +87,9 @@ impl PerfUiEntry for SimStateMonitor { fn format_value(&self, value: &Self::Value) -> String { match value { + SimState::Loading => String::from("Loading"), SimState::Running => String::from("Running"), SimState::Stopped => String::from("Stopped"), - SimState::Anomaly => String::from("ANOMALY") } } @@ -116,7 +119,7 @@ impl PerfUiEntry for SimStateMonitor { match *value { SimState::Running => self.color_gradient.get_color_for_value(0.0), SimState::Stopped => self.color_gradient.get_color_for_value(10.0), - _ => self.color_gradient.get_color_for_value(100.0), + _ => self.color_gradient.get_color_for_value(5.0), } } @@ -128,25 +131,6 @@ impl PerfUiEntry for SimStateMonitor { } } -#[derive(Resource, Reflect, Default)] -struct ForceMonitorResource { - pub weight: Vec3, - pub buoyancy: Vec3, - pub drag: Vec3, -} - -fn update_force_monitor_values( - mut force_resource: ResMut, - forces: Query<(&Weight, &Buoyancy, &Drag), With>, -) { - for (weight, bouyancy, drag) in forces.iter() { - // assume there's only one simulated body for now - force_resource.weight = weight.force(); - force_resource.buoyancy = bouyancy.force(); - force_resource.drag = drag.force(); - } -} - #[derive(Component)] struct ForceMonitor { /// The label text to display, to allow customization @@ -175,7 +159,7 @@ impl Default for ForceMonitor { threshold_highlight: Some(10.0), color_gradient: ColorGradient::new_preset_gyr(0.0, 10.0, 100.0).unwrap(), digits: 5, - precision: 2, + precision: 3, sort_key: iyes_perf_ui::utils::next_sort_key(), } } @@ -183,7 +167,7 @@ impl Default for ForceMonitor { impl PerfUiEntry for ForceMonitor { type Value = (f32, f32, f32); - type SystemParam = SRes; + type SystemParam = SQuery<(&'static Weight, &'static Buoyancy, &'static Drag), With>; fn label(&self) -> &str { if self.label.is_empty() { @@ -208,17 +192,121 @@ impl PerfUiEntry for ForceMonitor { f_b.push_str(" N"); f_d.push_str(" N"); } - format!("Fg {:} Fb {:} Fd {:}", f_g, f_b, f_d) + format!("Gravity: {}\nBuoyancy: {}\nDrag: {}", f_g, f_b, f_d) + } + + fn update_value( + &self, + force_resources: &mut ::Item<'_, '_>, + ) -> Option { + for (weight, buoyancy, drag) in force_resources.iter() { + return Some(( + weight.force().y, + buoyancy.force().y, + drag.force().y, + )) + } + None + } + + // (optional) We should add a width hint, so that the displayed + // strings in the UI can be correctly aligned. + // This value represents the largest length the formatted string + // is expected to have. + fn width_hint(&self) -> usize { + // there is a helper we can use, since we use `format_pretty_float` + let w = iyes_perf_ui::utils::width_hint_pretty_float(self.digits, self.precision); + if self.display_units { + w + 2 + } else { + w + } + } +} + +#[derive(Component)] +struct GasMonitor { + /// The label text to display, to allow customization + pub label: String, + /// Should we display units? + pub display_units: bool, + /// Highlight the value if it goes above this threshold + #[allow(dead_code)] + pub threshold_highlight: Option, + /// Support color gradients! + #[allow(dead_code)] + pub color_gradient: ColorGradient, + /// Width for formatting the string + pub digits: u8, + /// Precision for formatting the string + pub precision: u8, + /// Required to ensure the entry appears in the correct place in the Perf UI + pub sort_key: i32, +} + +impl Default for GasMonitor { + fn default() -> Self { + GasMonitor { + label: String::new(), + display_units: true, + threshold_highlight: Some(10.0), + color_gradient: ColorGradient::new_preset_gyr(0.0, 10.0, 100.0).unwrap(), + digits: 5, + precision: 2, + sort_key: iyes_perf_ui::utils::next_sort_key(), + } + } +} + +impl PerfUiEntry for GasMonitor { + type Value = (f32, f32, f32, f32, f32, String); + type SystemParam = SQuery<(&'static Balloon, &'static IdealGas)>; + + fn label(&self) -> &str { + if self.label.is_empty() { + "Gas" + } else { + &self.label + } + } + + fn sort_key(&self) -> i32 { + self.sort_key + } + + fn format_value(&self, value: &Self::Value) -> String { + // we can use a premade helper function for nice-looking formatting + let mut volume = format_pretty_float(self.digits, self.precision, value.0 as f64); + let mut pressure = format_pretty_float(self.digits, self.precision, value.1 as f64); + let mut temperature = format_pretty_float(self.digits, self.precision, value.2 as f64); + let mut density = format_pretty_float(self.digits, self.precision, value.3 as f64); + let mut mass = format_pretty_float(self.digits, self.precision, value.4 as f64); + let mut species = value.5.clone(); + + // (and append units to it) + if self.display_units { + volume.push_str(" m3"); + pressure.push_str(" kPa"); + temperature.push_str(" K"); + density.push_str(" kg/m3"); + mass.push_str(" kg"); + species.push_str(""); + } + format!("{}\n{}\n{}\n{}\n{}\n{}", species, volume, pressure, temperature, density, mass) } fn update_value( &self, - force_resource: &mut ::Item<'_, '_>, + items: &mut ::Item<'_, '_>, ) -> Option { + let (balloon, gas) = items.get_single().unwrap(); Some(( - force_resource.weight.length(), - force_resource.buoyancy.length(), - force_resource.drag.length(), + balloon.shape.volume(), + gas.pressure.kilopascals(), + gas.temperature.kelvin(), + gas.density.kilograms_per_cubic_meter(), + gas.mass.value(), + gas.species.name.clone(), )) } @@ -230,7 +318,7 @@ impl PerfUiEntry for ForceMonitor { // there is a helper we can use, since we use `format_pretty_float` let w = iyes_perf_ui::utils::width_hint_pretty_float(self.digits, self.precision); if self.display_units { - w + 2 + w + 5 } else { w } diff --git a/src/lib.rs b/src/lib.rs index 80f997b..319ab1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ + mod app3d; -mod controls; mod simulator; use bevy::{asset::AssetMetaCheck, prelude::*}; @@ -9,7 +9,7 @@ pub struct YahsPlugin; impl Plugin for YahsPlugin { fn build(&self, app: &mut App) { // Add Bevy plugins. - app.add_plugins( + app.add_plugins(( DefaultPlugins .set(AssetPlugin { // Wasm builds will check for meta files (that don't exist) @@ -30,18 +30,8 @@ impl Plugin for YahsPlugin { .into(), ..default() }), - ); - - // Add the simulator plugins that don't deal with graphics. - app.add_plugins(( simulator::SimulatorPlugins, - controls::ControlsPlugin, - )); - - // Add the 3D application plugins. - app.add_plugins(( - app3d::ScenePlugin, - app3d::InterfacePlugins, + app3d::App3dPlugins, )); } } diff --git a/src/simulator/atmosphere.rs b/src/simulator/atmosphere.rs index bc65af3..f5fd6e0 100644 --- a/src/simulator/atmosphere.rs +++ b/src/simulator/atmosphere.rs @@ -5,18 +5,35 @@ //! - 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; use bevy::prelude::*; use super::{ ideal_gas::{ideal_gas_density, GasSpecies}, - Density, Position, Pressure, SimState, SimulatedBody, Temperature, + properties::{Density, Pressure, Temperature}, + SimulationUpdateOrder, SimState, SimulatedBody, }; pub struct AtmospherePlugin; impl Plugin for AtmospherePlugin { fn build(&self, app: &mut App) { app.insert_resource(Atmosphere); - app.add_systems(Update, fault_if_out_of_bounds); + app.add_systems( + Update, + pause_on_out_of_bounds.in_set(SimulationUpdateOrder::First), + ); + } +} + +fn pause_on_out_of_bounds( + positions: Query<&Position, With>, + mut state: ResMut>, +) { + for position in positions.iter() { + if position.y < Atmosphere::MIN_ALTITUDE || position.y > Atmosphere::MAX_ALTITUDE { + error!("Atmosphere out of bounds: {}", position.y); + state.set(SimState::Stopped); + } } } @@ -28,24 +45,22 @@ impl Atmosphere { pub const MAX_ALTITUDE: f32 = 84999.0; // small margin to avoid panics pub const MIN_ALTITUDE: f32 = -56.0; // small margin to avoid panics - pub fn out_of_bounds(&self, position: Vec3) -> bool { - match position.y { - y if (y > Atmosphere::MAX_ALTITUDE) => true, - y if (y < Atmosphere::MIN_ALTITUDE) => true, - _ => false, - } - } - /// Temperature (K) of the atmosphere at a position. pub fn temperature(&self, position: Vec3) -> Temperature { // TODO: Look up temperature based on latitude, longitude, not just altitude - coesa_temperature(position.y).unwrap() // we should handle this better + coesa_temperature(position.y).unwrap_or_else(|e| { + error!("Atmosphere temperature out of bounds: {}", e); + Temperature::STANDARD + }) // we should handle this better } /// Pressure (Pa) of the atmosphere at a position. pub fn pressure(&self, position: Vec3) -> Pressure { // TODO: Look up pressure based on latitude, longitude, not just altitude - coesa_pressure(position.y).unwrap() // we should handle this better + coesa_pressure(position.y).unwrap_or_else(|e| { + error!("Atmosphere pressure out of bounds: {}", e); + Pressure::STANDARD + }) // we should handle this better } /// Density (kg/m³) of the atmosphere at a position. @@ -58,24 +73,29 @@ impl Atmosphere { } } -/// If any of the simulated bodies are out of bounds, set the app state to anomaly -/// TODO: we should use an event for this -fn fault_if_out_of_bounds( - atmosphere: Res, - bodies: Query<(Entity, &Position), With>, - mut next_state: ResMut>, -) { - for (_, position) in bodies.iter() { - if atmosphere.out_of_bounds(position.0) { - next_state.set(SimState::Anomaly) - }; +#[derive(Debug)] +enum AtmosphereError { + // v Fancy stuff from thiserror crate + // #[error( + // "Altitude {0} m is outside of the accepted range! Must be {min}-{max} m", + // min = Atmosphere::MIN_ALTITUDE, + // max = Atmosphere::MAX_ALTITUDE + // )] + // OutOfBounds(f32), + #[allow(dead_code)] + OutOfBounds(f32), +} + +impl std::fmt::Display for AtmosphereError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) } } /// Temperature (K) of the atmosphere at a given altitude (m). /// Only valid for altitudes below 85,000 meters. /// Based on the US Standard Atmosphere, 1976. (aka COESA) -fn coesa_temperature(altitude: f32) -> Result { +fn coesa_temperature(altitude: f32) -> Result { if (-57.0..11000.0).contains(&altitude) { Ok(Temperature::from_celsius(15.04 - 0.00649 * altitude)) } else if (11000.0..25000.0).contains(&altitude) { @@ -83,19 +103,16 @@ fn coesa_temperature(altitude: f32) -> Result { } else if (25000.0..85000.0).contains(&altitude) { Ok(Temperature::from_celsius(-131.21 + 0.00299 * altitude)) } else { - Err(format!( - "Altitude {:}m is outside of the accepted range! Must be 0-85000m", - altitude - )) + Err(AtmosphereError::OutOfBounds(altitude)) } } /// Pressure (Pa) of the atmosphere at a given altitude (m). /// Only valid for altitudes below 85,000 meters. /// Based on the US Standard Atmosphere, 1976. (aka COESA) -fn coesa_pressure(altitude: f32) -> Result { +fn coesa_pressure(altitude: f32) -> Result { if (-57.0..11000.0).contains(&altitude) { - Ok(Pressure::from_kilopascal( + Ok(Pressure::from_kilopascals( 101.29 * f32::powf( coesa_temperature(altitude).unwrap_or_default().kelvin() / 288.08, @@ -103,11 +120,11 @@ fn coesa_pressure(altitude: f32) -> Result { ), )) } else if (11000.0..25000.0).contains(&altitude) { - Ok(Pressure::from_kilopascal( + Ok(Pressure::from_kilopascals( 22.65 * f32::exp(1.73 - 0.000157 * altitude), )) } else if (25000.0..85000.0).contains(&altitude) { - Ok(Pressure::from_kilopascal( + Ok(Pressure::from_kilopascals( 2.488 * f32::powf( coesa_temperature(altitude).unwrap_or_default().kelvin() / 216.6, @@ -115,9 +132,6 @@ fn coesa_pressure(altitude: f32) -> Result { ), )) } else { - Err(format!( - "Altitude {:}m is outside of the accepted range! Must be 0-85000m", - altitude - )) + Err(AtmosphereError::OutOfBounds(altitude)) } } diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index b94ab97..095ab65 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -1,37 +1,69 @@ //! Properties, attributes and functions related to the balloon. -use avian3d::prelude::*; +use avian3d::{math::PI, prelude::Position}; use bevy::prelude::*; -#[cfg(feature = "config-files")] -use serde::{Deserialize, Serialize}; use super::{ - SimulatedBody, - ideal_gas::{GasSpecies, IdealGasBundle}, - properties::*, + ideal_gas::IdealGas, properties::sphere_radius_from_volume, SimulatedBody, + SimulationUpdateOrder, Volume, }; pub struct BalloonPlugin; impl Plugin for BalloonPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, spawn_balloon); - // Register types for reflection app.register_type::(); app.register_type::(); + + app.add_systems( + Update, + update_balloon_from_gas.in_set(SimulationUpdateOrder::MeshVolumes), + ); } } +#[derive(Component, Debug, Clone, PartialEq, Reflect)] +pub struct Balloon { + pub material_properties: BalloonMaterial, + pub shape: Sphere, +} + +impl Default for Balloon { + fn default() -> Self { + Balloon { + material_properties: BalloonMaterial::default(), + shape: Sphere::default(), + } + } +} + +impl Balloon { + pub fn volume(&self) -> Volume { + Volume(self.shape.volume()) + } +} + +/// The balloon is the surface of a [`Primitive3d`] that can be stretched +/// radially [`GasSpecies`] based on the pressure of the gas it contains. #[derive(Bundle)] pub struct BalloonBundle { pub balloon: Balloon, - pub gas: IdealGasBundle, - pub pbr: PbrBundle, + pub gas: IdealGas, +} + +impl Default for BalloonBundle { + fn default() -> Self { + let balloon = Balloon::default(); + let volume = balloon.volume(); + BalloonBundle { + balloon: Balloon::default(), + gas: IdealGas::default().with_volume(volume), + } + } } -#[derive(Debug, Clone, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] +#[derive(Component, Debug, Clone, PartialEq, Reflect)] pub struct BalloonMaterial { pub name: String, pub max_temperature: f32, // temperature (K) where the given material fails @@ -44,6 +76,7 @@ pub struct BalloonMaterial { pub elasticity: f32, // Youngs Modulus aka Modulus of Elasticity (Pa) pub max_strain: f32, // elongation at failure (decimal, unitless) 1 = original size pub max_stress: f32, // tangential stress at failure (Pa) + pub thickness: f32, // thickness of the material (m) } impl Default for BalloonMaterial { @@ -60,153 +93,14 @@ impl Default for BalloonMaterial { elasticity: 0.01e9, // Example Young's Modulus in Pa max_strain: 0.8, // Example max strain (unitless) max_stress: 0.5e6, // Example max stress in Pa + thickness: 0.0001, } } } -/// Balloon properties. The balloon always conforms to the surface of a -/// collider. It does not have its own rigid body. -#[derive(Component, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] -pub struct Balloon { - /// Balloon material type - pub skin_material: BalloonMaterial, - /// Thickness of balloon membrane in meters. For use in calculating stress. - pub unstretched_thickness: f32, - /// surface area of balloon without stretch (m²). For use in calculating stress. - pub unstretched_area: f32, -} - -impl Default for Balloon { - fn default() -> Self { - Balloon { - skin_material: BalloonMaterial::default(), - unstretched_thickness: 0.001, - unstretched_area: 4.0 * std::f32::consts::PI, - } +fn update_balloon_from_gas(mut query: Query<(&mut Balloon, &IdealGas)>) { + for (mut balloon, gas) in query.iter_mut() { + let new_radius = sphere_radius_from_volume(gas.volume().m3()); + balloon.shape.radius = new_radius; } } -fn spawn_balloon( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - let radius = 0.3; - commands.spawn(( - Name::new("BalloonBundle"), - SimulatedBody, - BalloonBundle { - balloon: Balloon::default(), - gas: IdealGasBundle::new( - Collider::sphere(radius), - GasSpecies::helium(), - Temperature::STANDARD, - Pressure::STANDARD, - ), - pbr: PbrBundle { - mesh: meshes.add(Sphere::new(radius)), - material: materials.add(Color::srgb_u8(124, 144, 255)), - transform: Transform::from_xyz(0.0, 0.0, 0.0), - ..default() - }, - }, - RigidBody::Dynamic, - )); -} - -// impl Balloon { - -// pub fn gage_pressure(&self, external_pressure: f32) -> f32 { -// self.lift_gas.pressure() - external_pressure -// } - -// fn set_stress(&mut self, external_pressure: f32) { -// // hoop stress (Pa) of thin-walled hollow sphere from internal pressure -// // https://en.wikipedia.org/wiki/Pressure_vessel#Stress_in_thin-walled_pressure_vessels -// // https://pkel015.connect.amazon.auckland.ac.nz/SolidMechanicsBooks/Part_I/BookSM_Part_I/07_ElasticityApplications/07_Elasticity_Applications_03_Presure_Vessels.pdf -// self.stress = -// self.gage_pressure(external_pressure) * self.radius() / (2.0 * self.skin_thickness); -// if self.stress > self.material.max_stress { -// self.burst(format!( -// "Hoop stress ({:?} Pa) exceeded maximum stress ({:?} Pa)", -// self.stress, self.material.max_stress -// )); -// } -// } - -// fn set_strain(&mut self) { -// // strain (%) of thin-walled hollow sphere from internal pressure -// // https://en.wikipedia.org/wiki/Pressure_vessel#Stress_in_thin-walled_pressure_vessels -// // https://pkel015.connect.amazon.auckland.ac.nz/SolidMechanicsBooks/Part_I/BookSM_Part_I/07_ElasticityApplications/07_Elasticity_Applications_03_Presure_Vessels.pdf -// self.strain = self.radius() / self.unstretched_radius; -// if self.strain > self.material.max_strain { -// self.burst(format!( -// "Tangential strain ({:?} %) exceeded maximum strain ({:?} %)", -// self.strain * 100.0, -// self.material.max_strain * 100.0 -// )); -// } -// } - -// pub fn radial_displacement(&self, external_pressure: f32) -> f32 { -// // https://pkel015.connect.amazon.auckland.ac.nz/SolidMechanicsBooks/Part_I/BookSM_Part_I/07_ElasticityApplications/07_Elasticity_Applications_03_Presure_Vessels.pdf -// ((1.0 - self.material.poissons_ratio) / self.material.elasticity) -// * ((self.gage_pressure(external_pressure) * f32::powf(self.radius(), 2.0)) / 2.0 -// * self.skin_thickness) -// } - -// fn rebound(&mut self, radial_displacement: f32) -> f32 { -// // https://physics.stackexchange.com/questions/10372/inflating-a-balloon-expansion-resistance -// self.set_thickness( -// self.unstretched_thickness * f32::powf(self.unstretched_radius / self.radius(), 2.0), -// ); -// 2.0 * self.material.elasticity -// * radial_displacement -// * self.unstretched_thickness -// * self.unstretched_radius -// / f32::powf(self.radius(), 3.0) -// } - -// pub fn stretch(&mut self, external_pressure: f32) { -// // stretch the balloon and/or compress the gas inside. -// // - the gas wants to be at the same pressure as ambient -// // - the balloon will stretch in response to the pressure difference -// // - the balloon will likely not stretch enough to reach equilibrium -// // - the difference between the ideal gas volume and the deformed -// // balloon volume is the new pressure difference -// // - the balloon fails when it starts to plasticly deform, in other -// // words the balloon stretches as long as tangential stress is less -// // than the material's yield stress -// debug!( -// "current gage pressure: {:?}", -// self.gage_pressure(external_pressure) -// ); - -// self.set_stress(external_pressure); -// self.set_strain(); - -// if self.intact { -// let delta_r = self.radial_displacement(external_pressure); -// debug!( -// "radius before stretch: {:?} delta_r: {:?}", -// self.radius(), -// delta_r -// ); -// let internal_pressure = self.rebound(delta_r); -// self.set_pressure(internal_pressure + external_pressure); -// debug!("radius after stretch: {:?}", self.radius()); -// debug!( -// "gage pressure after stretch: {:?}", -// self.gage_pressure(external_pressure) -// ); -// } -// } - -// fn burst(&mut self, reason: String) { -// // Assert new balloon attributes to reflect that it has burst -// self.intact = false; -// self.set_volume(0.0); -// self.lift_gas.set_mass(0.0); -// warn!("The balloon has burst! Reason: {:?}", reason) -// } -// } diff --git a/src/simulator/core.rs b/src/simulator/core.rs new file mode 100644 index 0000000..924c874 --- /dev/null +++ b/src/simulator/core.rs @@ -0,0 +1,72 @@ +use super::*; +use avian3d::prelude::*; +use bevy::{ + app::{PluginGroup, PluginGroupBuilder}, + prelude::*, +}; + +/// A marker component for entities that are simulated. +#[derive(Component, Default)] +pub struct SimulatedBody; + +pub struct SimulatorPlugins; + +impl PluginGroup for SimulatorPlugins { + fn build(self) -> PluginGroupBuilder { + PluginGroupBuilder::start::() + .add(CorePhysicsPlugin) + .add(atmosphere::AtmospherePlugin) + .add(balloon::BalloonPlugin) + } +} + +struct CorePhysicsPlugin; + +impl Plugin for CorePhysicsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins(PhysicsPlugins::default()); + app.add_plugins(( + properties::CorePropertiesPlugin, + ideal_gas::IdealGasPlugin, + forces::ForcesPlugin, + )); + app.init_state::(); + app.add_systems( + OnEnter(SimState::Running), + |mut time: ResMut>| time.as_mut().unpause(), + ); + app.add_systems( + OnExit(SimState::Running), + |mut time: ResMut>| time.as_mut().pause(), + ); + app.configure_sets( + Update, + ( + SimulationUpdateOrder::First, + SimulationUpdateOrder::IdealGas, + SimulationUpdateOrder::MeshVolumes, + SimulationUpdateOrder::Forces, + SimulationUpdateOrder::Last, + ).chain() + .before(PhysicsSet::Prepare) + .run_if(in_state(SimState::Running)), + ); + } +} + +#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)] +pub enum SimState { + #[default] + Loading, + Running, + Stopped, +} + +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub enum SimulationUpdateOrder { + First, + IdealGas, + MeshVolumes, + Forces, + Last, +} diff --git a/src/simulator/forces/aero.rs b/src/simulator/forces/aero.rs index 9c5497f..1301d90 100644 --- a/src/simulator/forces/aero.rs +++ b/src/simulator/forces/aero.rs @@ -1,11 +1,10 @@ //! Forces applied to rigid bodies due to aerodynamic drag. use avian3d::{math::PI, prelude::*}; -use parry3d::shape::{ShapeType, Shape, Ball}; use bevy::prelude::*; use bevy_trait_query::{self, RegisterExt}; -use super::{Atmosphere, Density, ForceUpdateOrder, Force, SimulatedBody}; +use super::{Atmosphere, Balloon, Density, ForceUpdateOrder, Force, SimulatedBody}; pub struct AeroForcesPlugin; @@ -13,6 +12,7 @@ impl Plugin for AeroForcesPlugin { fn build(&self, app: &mut App) { app.register_type::(); app.register_component_as::(); + app.add_systems(Update, update_drag_parameters.in_set(ForceUpdateOrder::Prepare)); } } @@ -68,15 +68,14 @@ impl Force for Drag { fn update_drag_parameters( atmosphere: Res, - mut bodies: Query<(&mut Drag, &Position, &LinearVelocity, &Collider), With>, + mut bodies: Query<(&mut Drag, &Position, &LinearVelocity, &Balloon)>, ) { - for (mut drag, position, velocity, collider) in bodies.iter_mut() { - let bounding_sphere = collider.shape().compute_bounding_sphere(&position.0.into()); + for (mut drag, position, velocity, balloon) in bodies.iter_mut() { drag.update( velocity.0, atmosphere.density(position.0), - projected_spherical_area(bounding_sphere.radius()), - drag_coefficient(&Ball::new(bounding_sphere.radius()), &atmosphere), + PI * balloon.shape.diameter(), + 1.17, // default drag coefficient for a sphere ); } } @@ -91,16 +90,11 @@ pub fn drag(velocity: Vec3, ambient_density: f32, drag_area: f32, drag_coeff: f3 drag_direction * drag_magnitude } -/// Get the projected area (m^2) of a sphere with a given radius (m) -fn projected_spherical_area(radius: f32) -> f32 { - f32::powf(radius, 2.0) * PI -} - -/// Get the drag coefficient for a given shape and ambient conditions. -fn drag_coefficient(shape: &dyn Shape, _atmosphere: &Atmosphere) -> f32 { - match shape.shape_type() { - ShapeType::Ball => 1.17, - ShapeType::Cuboid => 2.05, - _ => 1.0, - } -} +// Get the drag coefficient for a given shape and ambient conditions. +// fn drag_coefficient(shape: &dyn Shape, _atmosphere: &Atmosphere) -> f32 { +// match shape.shape_type() { +// ShapeType::Ball => 1.17, +// ShapeType::Cuboid => 2.05, +// _ => 1.0, +// } +// } diff --git a/src/simulator/forces/body.rs b/src/simulator/forces/body.rs index 7cb0a0d..0050d00 100644 --- a/src/simulator/forces/body.rs +++ b/src/simulator/forces/body.rs @@ -1,10 +1,10 @@ //! Forces applied to rigid bodies due to gravity and buoyancy. -use avian3d::prelude::*; +use avian3d::{math::PI, prelude::*}; use bevy::prelude::*; use bevy_trait_query::{self, RegisterExt}; -use super::{Atmosphere, Density, Force, ForceUpdateOrder, Mass, SimulatedBody, Volume}; +use super::{Atmosphere, Balloon, Density, Force, ForceUpdateOrder, Mass, SimulatedBody, Volume}; use crate::simulator::properties::{EARTH_RADIUS_M, STANDARD_G}; pub struct BodyForcesPlugin; @@ -27,6 +27,7 @@ impl Plugin for BodyForcesPlugin { /// Downward force (N) vector due to gravity as a function of altitude (m) and /// mass (kg). The direction of this force is always world-space down. #[derive(Component, Reflect)] +#[require(Position, Mass)] pub struct Weight { position: Vec3, mass: f32, @@ -73,12 +74,13 @@ fn update_weight_parameters( mut bodies: Query<(&mut Weight, &Position, &Mass), With>, ) { for (mut weight, position, mass) in bodies.iter_mut() { - weight.update(position.0, mass.kg()); + weight.update(position.0, mass.value()); } } /// Upward force (N) vector due to atmosphere displaced by the given gas volume. #[derive(Component, Reflect)] +#[require(Balloon, Position)] pub struct Buoyancy { position: Vec3, displaced_volume: Volume, @@ -115,15 +117,16 @@ impl Force for Buoyancy { /// 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(position: Vec3, displaced_volume: Volume, ambient_density: Density) -> Vec3 { - Vec3::Y * (displaced_volume.cubic_meters() * ambient_density.kg_per_m3() * g(position)) + Vec3::Y * (displaced_volume.m3() * ambient_density.kg_per_m3() * g(position)) } fn update_buoyant_parameters( atmosphere: Res, - mut bodies: Query<(&mut Buoyancy, &Position, &Volume), With>, + mut bodies: Query<(&mut Buoyancy, &Position, &Balloon), With>, ) { - for (mut buoyancy, position, volume) in bodies.iter_mut() { - let density = atmosphere.density(position.0); - buoyancy.update(position.0, *volume, density); + for (mut buoyancy, position, balloon) in bodies.iter_mut() { + let ambient_density = atmosphere.density(position.0); + let displaced_volume = balloon.volume(); + buoyancy.update(position.0, displaced_volume, ambient_density); } } diff --git a/src/simulator/forces/mod.rs b/src/simulator/forces/mod.rs index a5bb436..fbd2f5d 100644 --- a/src/simulator/forces/mod.rs +++ b/src/simulator/forces/mod.rs @@ -8,11 +8,11 @@ use bevy_trait_query; // Re-expert common forces #[allow(unused_imports)] -pub use body::{Weight, Buoyancy}; -#[allow(unused_imports)] pub use aero::Drag; +#[allow(unused_imports)] +pub use body::{Buoyancy, Weight}; -use super::{Atmosphere, Density, Mass, Volume, SimulatedBody}; +use super::{Atmosphere, Balloon, Density, SimulatedBody, SimulationUpdateOrder, SimState, Volume}; pub struct ForcesPlugin; impl Plugin for ForcesPlugin { @@ -25,11 +25,11 @@ impl Plugin for ForcesPlugin { Update, ( ForceUpdateOrder::First, - ForceUpdateOrder::Prepare.after(ForceUpdateOrder::First), - ForceUpdateOrder::Apply - .after(ForceUpdateOrder::Prepare) - .before(PhysicsStepSet::First), - ), + ForceUpdateOrder::Prepare, + ForceUpdateOrder::Apply, + ) + .chain() + .in_set(SimulationUpdateOrder::Forces), ); app.add_systems( Update, @@ -37,7 +37,9 @@ impl Plugin for ForcesPlugin { ); app.add_systems( Update, - update_total_external_force.in_set(ForceUpdateOrder::Apply), + update_total_external_force + .in_set(ForceUpdateOrder::Apply) + .run_if(in_state(SimState::Running)), ); app.add_plugins((aero::AeroForcesPlugin, body::BodyForcesPlugin)); @@ -45,7 +47,7 @@ impl Plugin for ForcesPlugin { } #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] -enum ForceUpdateOrder { +pub enum ForceUpdateOrder { First, Prepare, Apply, @@ -60,7 +62,6 @@ pub struct ForceBundle { drag: aero::Drag, } -/// Add a `ForceBundle` to entities with a `RigidBody` when they are added. fn on_simulated_body_added(mut commands: Commands, query: Query>) { for entity in &query { commands.entity(entity).insert(ForceBundle::default()); @@ -103,7 +104,11 @@ fn update_total_external_force( // Iterate over each force vector component and compute its value. for force in acting_forces.iter() { - net_force += force.force(); + if force.magnitude().is_nan() { + error!("{} has NaN magnitude!", force.name()); + } else { + net_force += force.force(); + } } physics_force_component.set_force(net_force); } diff --git a/src/simulator/ideal_gas.rs b/src/simulator/ideal_gas.rs index 789d3f2..cbb3e1d 100644 --- a/src/simulator/ideal_gas.rs +++ b/src/simulator/ideal_gas.rs @@ -1,12 +1,10 @@ //! Ideal gas equations. #![allow(dead_code)] - use avian3d::prelude::*; use bevy::prelude::*; -#[cfg(feature = "config-files")] -use serde::{Deserialize, Serialize}; -use crate::simulator::properties::{Mass as SimMass, *}; +use super::properties::{AVOGADRO_CONSTANT, BOLTZMANN_CONSTANT}; +use super::{Atmosphere, Density, MolarMass, Pressure, SimulationUpdateOrder, Temperature, Volume}; pub const R: f32 = BOLTZMANN_CONSTANT * AVOGADRO_CONSTANT; // [J/K-mol] Ideal gas constant @@ -15,15 +13,15 @@ pub struct IdealGasPlugin; impl Plugin for IdealGasPlugin { fn build(&self, app: &mut App) { app.register_type::(); - app.add_systems(Update, ( - update_ideal_gas_volume_from_pressure, - update_ideal_gas_density_from_volume, - )); + app.add_systems( + Update, + update_ideal_gas_from_atmosphere.in_set(SimulationUpdateOrder::IdealGas), + ); } } -#[derive(Component, Debug, Clone, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] +/// Molecular species of a gas. +#[derive(Debug, Clone, PartialEq, Reflect)] pub struct GasSpecies { pub name: String, pub abbreviation: String, @@ -55,6 +53,7 @@ impl Default for GasSpecies { } } +#[allow(dead_code)] impl GasSpecies { pub fn new(name: String, abbreviation: String, molar_mass: MolarMass) -> Self { GasSpecies { @@ -65,17 +64,90 @@ impl GasSpecies { } } +/// Properties of an ideal gas. For properties per unit mass, set the mass to 1. +#[derive(Component, Debug, Clone, PartialEq)] +pub struct IdealGas { + pub temperature: Temperature, + pub pressure: Pressure, + pub density: Density, + pub mass: Mass, + pub species: GasSpecies, +} + +impl Default for IdealGas { + fn default() -> Self { + let species = GasSpecies::default(); + let temperature = Temperature::default(); + let pressure = Pressure::default(); + let density = ideal_gas_density(temperature, pressure, &species); + let mass = Mass::new(1.0); + IdealGas { + temperature, + pressure, + density, + species, + mass, + } + } +} + +impl IdealGas { + pub fn new(species: GasSpecies) -> Self { + let temperature = Temperature::default(); + let pressure = Pressure::default(); + let mass = Mass::new(1.0); + let density = ideal_gas_density(temperature, pressure, &species); + IdealGas { + temperature, + pressure, + density, + species, + mass, + } + } + + pub fn with_temperature(mut self, temperature: Temperature) -> Self { + self.temperature = temperature; + self.update_density(); + self + } + + pub fn with_pressure(mut self, pressure: Pressure) -> Self { + self.pressure = pressure; + self.update_density(); + self + } + + pub fn with_mass(mut self, mass: Mass) -> Self { + self.mass = mass; + self + } + + pub fn with_volume(mut self, volume: Volume) -> Self { + self.mass = Mass::new(self.density.kg_per_m3() * volume.m3()); + self + } + + fn update_density(&mut self) { + self.density = ideal_gas_density(self.temperature, self.pressure, &self.species); + } + + pub fn volume(&self) -> Volume { + ideal_gas_volume(self.temperature, self.pressure, self.mass, &self.species) + } +} + /// Volume (m³) of an ideal gas from its temperature (K), pressure (Pa), /// mass (kg) and molar mass (kg/mol). pub fn ideal_gas_volume( temperature: Temperature, pressure: Pressure, - mass: SimMass, + mass: Mass, species: &GasSpecies, ) -> Volume { Volume( - (mass.kilograms() / species.molar_mass.kilograms_per_mole()) * R * temperature.kelvin() - / pressure.pascal(), + (mass.value() / species.molar_mass.kilograms_per_mole()) * R * temperature.kelvin() + / pressure.pascals(), ) } @@ -87,10 +159,11 @@ pub fn ideal_gas_density( species: &GasSpecies, ) -> Density { Density( - (species.molar_mass.kilograms_per_mole() * pressure.pascal()) / (R * temperature.kelvin()), + species.molar_mass.kilograms_per_mole() * pressure.pascals() / (R * temperature.kelvin()), ) } +#[allow(dead_code)] /// Gage pressure (Pa) of an ideal gas. This is the relative pressure compared /// to the ambient pressure. Use `Atmosphere::pressure()` to get ambient /// conditions. @@ -98,64 +171,13 @@ pub fn gage_pressure(pressure: Pressure, ambient_pressure: Pressure) -> Pressure pressure - ambient_pressure } -/// A finite amount of a particular ideal gas -#[derive(Component, Debug)] -pub struct IdealGas; - -#[derive(Bundle, Debug)] -pub struct IdealGasBundle { - pub collider: Collider, - pub species: GasSpecies, - pub temperature: Temperature, - pub pressure: Pressure, - pub volume: Volume, - pub mass: SimMass, -} - -impl IdealGasBundle { - pub fn new( - collider: Collider, - species: GasSpecies, - temperature: Temperature, - pressure: Pressure, - ) -> Self { - let density = ideal_gas_density(temperature, pressure, &species); - let mass_props = collider.mass_properties(density.kg_per_m3()); - let mass = SimMass::from_mass_properties(mass_props); - Self { - collider, - species: species.clone(), - temperature, - pressure, - volume: ideal_gas_volume(temperature, pressure, mass, &species), - mass, - } - } -} - -impl Default for IdealGasBundle { - fn default() -> Self { - IdealGasBundle::new( - Collider::sphere(1.0), - GasSpecies::default(), - Temperature::STANDARD, - Pressure::STANDARD, - ) - } -} - -fn update_ideal_gas_volume_from_pressure( - mut query: Query<(&mut Volume, &Temperature, &Pressure, &SimMass, &GasSpecies), With>, -) { - for (mut volume, temperature, pressure, mass, species) in query.iter_mut() { - *volume = ideal_gas_volume(*temperature, *pressure, *mass, species); - } -} - -fn update_ideal_gas_density_from_volume( - mut query: Query<(&mut Density, &Volume, &SimMass), With>, +fn update_ideal_gas_from_atmosphere( + mut query: Query<(&mut IdealGas, &Position)>, + atmosphere: Res, ) { - for (mut density, volume, mass) in query.iter_mut() { - *density = *mass / *volume; + for (mut gas, position) in query.iter_mut() { + gas.pressure = atmosphere.pressure(position.0); + gas.temperature = atmosphere.temperature(position.0); + gas.update_density(); } } diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index c24cd24..f0e957a 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -1,3 +1,5 @@ +#![allow(unused_imports)] +pub mod core; pub mod atmosphere; pub mod balloon; pub mod forces; @@ -5,59 +7,10 @@ pub mod ideal_gas; pub mod payload; pub mod properties; -use bevy::app::PluginGroupBuilder; -use bevy::prelude::*; -use avian3d::prelude::*; - // Re-export the properties module at the top level. -pub use properties::{Temperature, Pressure, Volume, Density, Mass}; +pub use core::{SimulatorPlugins, SimState, SimulatedBody, SimulationUpdateOrder}; +pub use properties::{Density, Pressure, Temperature, Volume, MolarMass}; pub use atmosphere::Atmosphere; - -/// A marker component for entities that are simulated. -#[derive(Component, Default)] -pub struct SimulatedBody; - - -pub struct SimulatorPlugins; - -impl PluginGroup for SimulatorPlugins { - fn build(self) -> PluginGroupBuilder { - PluginGroupBuilder::start::() - .add(CorePhysicsPlugin) - .add(atmosphere::AtmospherePlugin) - .add(balloon::BalloonPlugin) - } -} - -struct CorePhysicsPlugin; - -impl Plugin for CorePhysicsPlugin { - fn build(&self, app: &mut App) { - app.add_plugins(( - PhysicsPlugins::default(), - properties::CorePropertiesPlugin, - ideal_gas::IdealGasPlugin, - forces::ForcesPlugin, - )); - app.init_state::(); - app.add_systems(Update, pause_physics_time); - } -} - -#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)] -pub enum SimState { - #[default] - Running, - Stopped, - Anomaly, -} - -fn pause_physics_time( - sim_state: Res>, - mut physics_time: ResMut>) { - match sim_state.as_ref().get() { - SimState::Running => physics_time.unpause(), - SimState::Stopped => physics_time.pause(), - SimState::Anomaly => physics_time.pause(), - } -} +pub use forces::{Weight, Buoyancy, Drag}; +pub use balloon::{Balloon, BalloonBundle, BalloonMaterial}; +pub use ideal_gas::{GasSpecies, IdealGas}; diff --git a/src/simulator/properties.rs b/src/simulator/properties.rs index 6615665..bf82cf6 100644 --- a/src/simulator/properties.rs +++ b/src/simulator/properties.rs @@ -1,16 +1,13 @@ //! Basic physical properties. - #![allow(dead_code)] use std::ops::{Add, Div, Mul, Sub}; use avian3d::{ math::{Scalar, PI}, - prelude::{ColliderDensity, ColliderMassProperties, PhysicsSet}, + prelude::Mass, }; use bevy::{prelude::*, reflect::Reflect}; -#[cfg(feature = "config-files")] -use serde::{Deserialize, Serialize}; pub const BOLTZMANN_CONSTANT: f32 = 1.38e-23_f32; // [J/K] pub const AVOGADRO_CONSTANT: f32 = 6.022e+23_f32; // [1/mol] @@ -29,14 +26,10 @@ fn shell_volume(internal_radius: f32, thickness: f32) -> f32 { external_volume - internal_volume } -fn sphere_radius_from_volume(volume: f32) -> f32 { +pub fn sphere_radius_from_volume(volume: f32) -> f32 { f32::powf(volume, 1.0 / 3.0) / (4.0 / 3.0) * PI } -fn sphere_surface_area(radius: f32) -> f32 { - 4.0 * PI * f32::powf(radius, 2.0) -} - pub struct CorePropertiesPlugin; impl Plugin for CorePropertiesPlugin { @@ -45,23 +38,12 @@ impl Plugin for CorePropertiesPlugin { app.register_type::(); app.register_type::(); app.register_type::(); - app.register_type::(); app.register_type::(); - - // Ensure that the Avian density matches our computed mass and density - // before it starts solving physics. - app.add_systems( - Update, - (sync_avian_mass, sync_avian_density) - .chain() - .in_set(PhysicsSet::Prepare), - ); } } /// Temperature (K) -#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] +#[derive(Component,Debug, Clone, Copy, PartialEq, Reflect)] pub struct Temperature(pub Scalar); impl Temperature { @@ -84,6 +66,12 @@ impl Temperature { } } +impl Default for Temperature { + fn default() -> Self { + Temperature::STANDARD + } +} + impl Add for Temperature { type Output = Temperature; @@ -117,8 +105,7 @@ impl Div for Temperature { } /// Pressure (Pa) -#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] +#[derive(Component, Debug, Clone, Copy, PartialEq, Reflect)] pub struct Pressure(pub Scalar); impl Pressure { @@ -128,16 +115,22 @@ impl Pressure { Pressure(pascal) } - pub fn from_kilopascal(kilopascal: f32) -> Self { - Pressure(kilopascal * 1000.0) + pub fn from_kilopascals(kilopascals: f32) -> Self { + Pressure(kilopascals * 1000.0) } - pub fn pascal(&self) -> f32 { + pub fn pascals(&self) -> f32 { self.0 } - pub fn kilopascal(&self) -> f32 { - self.pascal() / 1000.0 + pub fn kilopascals(&self) -> f32 { + self.pascals() / 1000.0 + } +} + +impl Default for Pressure { + fn default() -> Self { + Pressure::STANDARD } } @@ -175,7 +168,6 @@ impl Div for Pressure { /// The volume of a body in cubic meters. #[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct Volume(pub Scalar); impl Volume { @@ -225,14 +217,13 @@ impl Div for Volume { /// Density (kg/m³) #[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct Density(pub Scalar); impl Density { pub const ZERO: Self = Density(0.0); pub fn new(kilograms: Mass, volume: Volume) -> Self { - Density(kilograms.0 / volume.0) + Density(kilograms.value() / volume.cubic_meters()) } pub fn kilograms_per_cubic_meter(&self) -> f32 { @@ -276,81 +267,8 @@ impl Div for Density { } } -fn sync_avian_density(mut densities: Query<(&mut ColliderDensity, &Volume, &Mass)>) { - for (mut density, volume, mass) in densities.iter_mut() { - let our_density = mass.kg() / volume.m3(); - density.0 = our_density; - } -} - -/// Mass (kg) -#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] -pub struct Mass(pub Scalar); - -impl Mass { - pub fn kilograms(&self) -> f32 { - self.0 - } - - pub fn kg(&self) -> f32 { - self.0 - } - - pub fn from_mass_properties(mass_props: ColliderMassProperties) -> Self { - Mass(mass_props.mass.0) - } -} - -impl Add for Mass { - type Output = Mass; - - fn add(self, rhs: Mass) -> Self::Output { - Mass(self.0 + rhs.0) - } -} - -impl Sub for Mass { - type Output = Mass; - - fn sub(self, rhs: Mass) -> Self::Output { - Mass(self.0 - rhs.0) - } -} - -impl Mul for Mass { - type Output = Mass; - - fn mul(self, rhs: Scalar) -> Self::Output { - Mass(self.0 * rhs) - } -} - -impl Div for Mass { - type Output = Density; - - fn div(self, rhs: Volume) -> Self::Output { - Density(self.0 / rhs.0) - } -} - -impl Div for Mass { - type Output = Mass; - - fn div(self, rhs: Scalar) -> Self::Output { - Mass(self.0 / rhs) - } -} - -fn sync_avian_mass(mut bodies: Query<(&mut ColliderMassProperties, &Mass)>) { - for (mut mass_props, mass) in bodies.iter_mut() { - mass_props.mass.0 = mass.0; - } -} - /// Molar mass (kg/mol) of a substance. #[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct MolarMass(pub Scalar); impl MolarMass {