From 75497b9202281a6a3babdd9eeca051195da715c3 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Sat, 23 Nov 2024 12:05:35 -0500 Subject: [PATCH 01/10] checkpoint: 2024-11-23 morning --- docs/devlog.md | 11 +++++++++++ src/simulator/balloon.rs | 2 +- src/simulator/ideal_gas.rs | 3 ++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/devlog.md b/docs/devlog.md index 4824b0d..e2c8160 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -1,5 +1,16 @@ # development log +## 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. + ## 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/src/simulator/balloon.rs b/src/simulator/balloon.rs index b94ab97..dfb0de3 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -93,7 +93,7 @@ fn spawn_balloon( ) { let radius = 0.3; commands.spawn(( - Name::new("BalloonBundle"), + Name::new("Balloon"), SimulatedBody, BalloonBundle { balloon: Balloon::default(), diff --git a/src/simulator/ideal_gas.rs b/src/simulator/ideal_gas.rs index 789d3f2..02f3984 100644 --- a/src/simulator/ideal_gas.rs +++ b/src/simulator/ideal_gas.rs @@ -1,5 +1,4 @@ //! Ideal gas equations. -#![allow(dead_code)] use avian3d::prelude::*; use bevy::prelude::*; @@ -55,6 +54,7 @@ impl Default for GasSpecies { } } +#[allow(dead_code)] impl GasSpecies { pub fn new(name: String, abbreviation: String, molar_mass: MolarMass) -> Self { GasSpecies { @@ -91,6 +91,7 @@ pub fn ideal_gas_density( ) } +#[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. From e7a6f27d8cc6c452ca069bd31592a707fa785e1f Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Sat, 23 Nov 2024 15:49:35 -0500 Subject: [PATCH 02/10] checkpoint: 2024-11-23 afternoon --- Cargo.toml | 4 +- docs/devlog.md | 18 ++++++ src/simulator/atmosphere.rs | 25 ++++---- src/simulator/balloon.rs | 125 ++++++------------------------------ src/simulator/ideal_gas.rs | 91 ++++++-------------------- src/simulator/mod.rs | 25 ++++---- src/simulator/properties.rs | 95 +-------------------------- 7 files changed, 86 insertions(+), 297 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7434818..169ff45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,8 @@ inspect = ["bevy-inspector-egui", "bevy_panorbit_camera/bevy_egui"] [dependencies] # core dependencies -bevy = "0.14.2" +bevy = "0.15.0-rc.3" +bevy_heavy = { git = "https://github.com/Jondolf/bevy_heavy.git" } bevy-trait-query = "0.6.0" # physics dependencies avian3d = { version = "0.1.2", features = ["debug-plugin"] } @@ -42,6 +43,7 @@ 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"] } +thiserror = "2.0.3" [[bin]] name = "yahs" diff --git a/docs/devlog.md b/docs/devlog.md index e2c8160..cf978b8 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -11,6 +11,24 @@ fundamentals of the flight simulation: 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. + ## 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/src/simulator/atmosphere.rs b/src/simulator/atmosphere.rs index bc65af3..c09dfd3 100644 --- a/src/simulator/atmosphere.rs +++ b/src/simulator/atmosphere.rs @@ -6,10 +6,13 @@ //! - https://www.grc.nasa.gov/WWW/K-12/airplane/atmosmet.html use bevy::prelude::*; +use avian3d::prelude::Position; +use thiserror; use super::{ ideal_gas::{ideal_gas_density, GasSpecies}, - Density, Position, Pressure, SimState, SimulatedBody, Temperature, + properties::{Density, Pressure, Temperature}, + SimState, SimulatedBody, }; pub struct AtmospherePlugin; @@ -58,6 +61,12 @@ impl Atmosphere { } } +#[derive(Debug, thiserror::Error)] +enum AtmosphereError { + #[error("Altitude {0} m is outside of the accepted range! Must be {min}-{max} m", min = 0., max = 80_000.)] + OutOfBounds(f32) +} + /// 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( @@ -75,7 +84,7 @@ fn fault_if_out_of_bounds( /// 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,17 +92,14 @@ 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( 101.29 @@ -115,9 +121,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 dfb0de3..6539177 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -1,7 +1,7 @@ //! Properties, attributes and functions related to the balloon. -use avian3d::prelude::*; -use bevy::prelude::*; +use avian3d::{math::PI, prelude::*}; +use bevy::{prelude::*, math::primitives::Sphere}; #[cfg(feature = "config-files")] use serde::{Deserialize, Serialize}; @@ -27,7 +27,6 @@ impl Plugin for BalloonPlugin { pub struct BalloonBundle { pub balloon: Balloon, pub gas: IdealGasBundle, - pub pbr: PbrBundle, } #[derive(Debug, Clone, PartialEq, Reflect)] @@ -82,131 +81,43 @@ impl Default for Balloon { Balloon { skin_material: BalloonMaterial::default(), unstretched_thickness: 0.001, - unstretched_area: 4.0 * std::f32::consts::PI, + unstretched_area: 4.0 * PI, } } } + fn spawn_balloon( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, ) { let radius = 0.3; - commands.spawn(( + let shape = Sphere::new(radius); + commands.spawn( Name::new("Balloon"), SimulatedBody, BalloonBundle { balloon: Balloon::default(), gas: IdealGasBundle::new( - Collider::sphere(radius), + shape, 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() - }, + mesh: meshes.add(sphere), }, 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) +// The balloon is stretched around the volume of the gas inside of it. +// This system updates the balloon's volume and radius based on the gas +// pressure. +// fn update_balloon_mesh( +// asset_server: Res, +// mut query: Query<(&mut Mesh3d, &mut Shape, &Volume), With>, +// ) { +// for (mut mesh, mut shape, volume) in query.iter_mut() { +// shape.radius = sphere_radius_from_volume(*volume); // } // } diff --git a/src/simulator/ideal_gas.rs b/src/simulator/ideal_gas.rs index 02f3984..4d1bc82 100644 --- a/src/simulator/ideal_gas.rs +++ b/src/simulator/ideal_gas.rs @@ -5,7 +5,7 @@ use bevy::prelude::*; #[cfg(feature = "config-files")] use serde::{Deserialize, Serialize}; -use crate::simulator::properties::{Mass as SimMass, *}; +use crate::simulator::properties::*; pub const R: f32 = BOLTZMANN_CONSTANT * AVOGADRO_CONSTANT; // [J/K-mol] Ideal gas constant @@ -14,13 +14,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_volume_from_pressure, + // // update_ideal_gas_density_from_volume, + // )); } } +/// Molecular species of a gas. +/// TODO: load species from a file #[derive(Component, Debug, Clone, PartialEq, Reflect)] #[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct GasSpecies { @@ -65,16 +67,25 @@ impl GasSpecies { } } +/// A finite amount of a particular ideal gas. +#[derive(Component, Debug)] +pub struct IdealGas { + pub temperature: Temperature, + pub pressure: Pressure, + pub mass: Mass, +} + /// 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() + (mass.0 / species.molar_mass.kilograms_per_mole()) + * R * temperature.kelvin() / pressure.pascal(), ) } @@ -86,9 +97,7 @@ pub fn ideal_gas_density( pressure: Pressure, species: &GasSpecies, ) -> Density { - Density( - (species.molar_mass.kilograms_per_mole() * pressure.pascal()) / (R * temperature.kelvin()), - ) + Density(species.molar_mass.kilograms_per_mole() * pressure.pascal() / (R * temperature.kelvin())) } #[allow(dead_code)] @@ -98,65 +107,3 @@ pub fn ideal_gas_density( 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>, -) { - for (mut density, volume, mass) in query.iter_mut() { - *density = *mass / *volume; - } -} diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index c24cd24..26afa74 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -5,19 +5,19 @@ pub mod ideal_gas; pub mod payload; pub mod properties; +use avian3d::prelude::*; 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 atmosphere::Atmosphere; +#[allow(unused-imports)] +pub use properties::{Density, Pressure, Temperature, Volume}; /// A marker component for entities that are simulated. #[derive(Component, Default)] pub struct SimulatedBody; - pub struct SimulatorPlugins; impl PluginGroup for SimulatorPlugins { @@ -40,7 +40,14 @@ impl Plugin for CorePhysicsPlugin { forces::ForcesPlugin, )); app.init_state::(); - app.add_systems(Update, pause_physics_time); + 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(), + ); } } @@ -51,13 +58,3 @@ pub enum SimState { 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(), - } -} diff --git a/src/simulator/properties.rs b/src/simulator/properties.rs index 6615665..db93cc3 100644 --- a/src/simulator/properties.rs +++ b/src/simulator/properties.rs @@ -1,12 +1,9 @@ //! 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")] @@ -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,22 +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)] +#[derive(Component,Debug, Default, Clone, Copy, PartialEq, Reflect)] #[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct Temperature(pub Scalar); @@ -276,78 +259,6 @@ 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))] From e08cf94005c020d4979dd95f899e0ea93530a191 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Sat, 23 Nov 2024 20:10:48 -0500 Subject: [PATCH 03/10] bevy: get working on bevy 0.15.0-rc.3 --- Cargo.toml | 28 ++------- docs/devlog.md | 41 +++++++++++++- src/{ => app3d}/controls.rs | 0 src/app3d/mod.rs | 2 + src/app3d/scene/camera.rs | 53 ++--------------- src/app3d/scene/mod.rs | 67 +++++++++++++++------- src/app3d/ui/core.rs | 8 +-- src/app3d/ui/dev_tools.rs | 107 +++++++++++++++++------------------ src/app3d/ui/mod.rs | 2 +- src/lib.rs | 10 +++- src/simulator/atmosphere.rs | 11 ++-- src/simulator/balloon.rs | 51 ++--------------- src/simulator/core.rs | 51 +++++++++++++++++ src/simulator/forces/aero.rs | 21 +++---- src/simulator/forces/body.rs | 6 +- src/simulator/forces/mod.rs | 57 +++++++++---------- src/simulator/ideal_gas.rs | 22 ++++--- src/simulator/mod.rs | 58 +++---------------- src/simulator/properties.rs | 19 ++++--- 19 files changed, 295 insertions(+), 319 deletions(-) rename src/{ => app3d}/controls.rs (100%) create mode 100644 src/simulator/core.rs diff --git a/Cargo.toml b/Cargo.toml index 169ff45..fb1e572 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. @@ -22,33 +26,13 @@ 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.15.0-rc.3" -bevy_heavy = { git = "https://github.com/Jondolf/bevy_heavy.git" } -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"] } +avian3d = { git = "https://github.com/Jondolf/avian.git", branch = "bevy-0.15", features = ["debug-plugin"] } thiserror = "2.0.3" -[[bin]] -name = "yahs" -path = "src/main.rs" - # ----------------------------------------------------------------------------- # Some Bevy optimizations # ----------------------------------------------------------------------------- @@ -68,7 +52,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 cf978b8..fba4b1d 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -27,7 +27,46 @@ 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. +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 +- [ ] ~~`bevy-trait-query`~~ remove for now +- [ ] ~~`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 diff --git a/src/controls.rs b/src/app3d/controls.rs similarity index 100% rename from src/controls.rs rename to src/app3d/controls.rs diff --git a/src/app3d/mod.rs b/src/app3d/mod.rs index 40c0245..a5b24ff 100644 --- a/src/app3d/mod.rs +++ b/src/app3d/mod.rs @@ -1,6 +1,8 @@ mod scene; 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; +pub use controls::ControlsPlugin; diff --git a/src/app3d/scene/camera.rs b/src/app3d/scene/camera.rs index 8372f55..5f07884 100644 --- a/src/app3d/scene/camera.rs +++ b/src/app3d/scene/camera.rs @@ -1,65 +1,20 @@ use bevy::prelude::*; -use bevy_panorbit_camera::{PanOrbitCamera, PanOrbitCameraPlugin}; -use avian3d::math::TAU; -use crate::controls::KeyBindingsConfig; +// use crate::controls::CameraControls; 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) { +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. - 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() - }, + Camera3d::default(), + Transform::from_xyz(0.0, 20., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), )); } - -// 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 index cb7c6da..730bbcf 100644 --- a/src/app3d/scene/mod.rs +++ b/src/app3d/scene/mod.rs @@ -3,12 +3,14 @@ mod camera; use avian3d::prelude::*; use bevy::prelude::*; +use crate::simulator::*; + pub struct ScenePlugin; impl Plugin for ScenePlugin { fn build(&self, app: &mut App) { app.add_plugins(camera::CameraPlugin); - app.add_systems(Startup, simple_scene); + app.add_systems(Startup, (simple_scene, spawn_balloon)); } } @@ -18,40 +20,65 @@ fn simple_scene( 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::default()); + // light commands.spawn(( Name::new("Light"), - PointLightBundle { - point_light: PointLight { - shadows_enabled: true, - ..default() - }, - transform: Transform::from_xyz(4.0, 8.0, 4.0), + PointLight { + intensity: 1500.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), + Mesh3d(plane.clone()), + MeshMaterial3d(plane_material.clone()), )); + + // ceiling 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), + Mesh3d(plane.clone()), + MeshMaterial3d(plane_material.clone()), + )); +} + +pub fn spawn_balloon( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let debug_material = materials.add(StandardMaterial { + base_color: Color::srgb(1.0, 0.0, 0.0), + ..default() + }); + let shape = meshes.add(Sphere::default().mesh().ico(5).unwrap()); + commands.spawn(( + Name::new("Balloon"), + SimulatedBody, + BalloonBundle { + balloon: Balloon::default(), + gas: IdealGasBundle { + species: GasSpecies::helium(), + gas: IdealGas { + temperature: Temperature::STANDARD, + pressure: Pressure::STANDARD, + mass: Mass::new(1.0), + }, + }, + }, + RigidBody::Dynamic, + Mesh3d(shape), + MeshMaterial3d(debug_material), )); + } diff --git a/src/app3d/ui/core.rs b/src/app3d/ui/core.rs index b1b8d9b..84f7f3f 100644 --- a/src/app3d/ui/core.rs +++ b/src/app3d/ui/core.rs @@ -1,7 +1,7 @@ use bevy::{app::PluginGroupBuilder, prelude::*}; -use iyes_perf_ui::prelude::*; +// use iyes_perf_ui::prelude::*; -use crate::controls::KeyBindingsConfig; +use crate::app3d::controls::KeyBindingsConfig; use crate::simulator::SimState; use super::*; @@ -14,7 +14,7 @@ impl PluginGroup for InterfacePlugins { PluginGroupBuilder::start::() .add(CoreUiPlugin) .add(PausePlayPlugin) - .add(monitors::MonitorsPlugin) + // .add(monitors::MonitorsPlugin) } } @@ -25,7 +25,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, )); diff --git a/src/app3d/ui/dev_tools.rs b/src/app3d/ui/dev_tools.rs index dd4dae6..04fc5aa 100644 --- a/src/app3d/ui/dev_tools.rs +++ b/src/app3d/ui/dev_tools.rs @@ -9,14 +9,14 @@ use bevy::{ input::common_conditions::input_just_pressed, prelude::*, }; -use iyes_perf_ui::prelude::*; +// 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; pub(super) fn plugin(app: &mut App) { // Toggle the debug overlay for UI. @@ -31,11 +31,6 @@ pub(super) fn plugin(app: &mut App) { #[cfg(not(target_arch = "wasm32"))] WireframePlugin, )); - app.add_systems( - Update, - toggle_debug_ui - .before(iyes_perf_ui::PerfUiSet::Setup), - ); // Wireframe doesn't work on WASM #[cfg(not(target_arch = "wasm32"))] @@ -44,57 +39,57 @@ pub(super) fn plugin(app: &mut App) { toggle_wireframe, ); - #[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()); + // } } -/// Toggle the debug overlay -fn toggle_debug_ui( - mut commands: Commands, - q_root: Query>, - key_input: Res>, - key_bindings: Res, -) { - 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(); - } 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(), - ), - )); - } - } -} +// /// Toggle the debug overlay +// fn toggle_debug_ui( +// mut commands: Commands, +// q_root: Query>, +// key_input: Res>, +// key_bindings: Res, +// ) { +// 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(); +// } 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(), +// ), +// )); +// } +// } +// } #[cfg(not(target_arch = "wasm32"))] fn toggle_wireframe( diff --git a/src/app3d/ui/mod.rs b/src/app3d/ui/mod.rs index af959cc..78f1cb0 100644 --- a/src/app3d/ui/mod.rs +++ b/src/app3d/ui/mod.rs @@ -1,5 +1,5 @@ mod core; -mod monitors; +// mod monitors; #[cfg(feature = "dev")] mod dev_tools; diff --git a/src/lib.rs b/src/lib.rs index 80f997b..6bc958e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,15 @@ + mod app3d; -mod controls; mod simulator; use bevy::{asset::AssetMetaCheck, prelude::*}; +/// Re-export for convenience +pub use crate::app3d::{ + {ScenePlugin, InterfacePlugins}, + controls::{KeyBindingsConfig, CameraControls, DebugControls, TimeControls} +}; + pub struct YahsPlugin; impl Plugin for YahsPlugin { @@ -35,13 +41,13 @@ impl Plugin for YahsPlugin { // 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::ControlsPlugin, )); } } diff --git a/src/simulator/atmosphere.rs b/src/simulator/atmosphere.rs index c09dfd3..9cc2352 100644 --- a/src/simulator/atmosphere.rs +++ b/src/simulator/atmosphere.rs @@ -5,9 +5,8 @@ //! - https://www.translatorscafe.com/unit-converter/en-US/calculator/altitude //! - https://www.grc.nasa.gov/WWW/K-12/airplane/atmosmet.html -use bevy::prelude::*; use avian3d::prelude::Position; -use thiserror; +use bevy::prelude::*; use super::{ ideal_gas::{ideal_gas_density, GasSpecies}, @@ -63,8 +62,12 @@ impl Atmosphere { #[derive(Debug, thiserror::Error)] enum AtmosphereError { - #[error("Altitude {0} m is outside of the accepted range! Must be {min}-{max} m", min = 0., max = 80_000.)] - OutOfBounds(f32) + #[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), } /// If any of the simulated bodies are out of bounds, set the app state to anomaly diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index 6539177..a789541 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -1,13 +1,11 @@ //! Properties, attributes and functions related to the balloon. use avian3d::{math::PI, prelude::*}; -use bevy::{prelude::*, math::primitives::Sphere}; -#[cfg(feature = "config-files")] -use serde::{Deserialize, Serialize}; +use bevy::prelude::*; use super::{ SimulatedBody, - ideal_gas::{GasSpecies, IdealGasBundle}, + ideal_gas::{GasSpecies, IdealGasBundle, IdealGas}, properties::*, }; @@ -15,8 +13,6 @@ 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::(); @@ -30,7 +26,6 @@ pub struct BalloonBundle { } #[derive(Debug, Clone, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct BalloonMaterial { pub name: String, pub max_temperature: f32, // temperature (K) where the given material fails @@ -63,10 +58,10 @@ impl Default for BalloonMaterial { } } -/// Balloon properties. The balloon always conforms to the surface of a -/// collider. It does not have its own rigid body. +/// Balloon properties. The balloon is the surface of a [`BoundingVolume`] that can +/// be stretched around a [`GasSpecies`] based on the pressure inside. #[derive(Component, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] +#[require(BoundingVolume)] pub struct Balloon { /// Balloon material type pub skin_material: BalloonMaterial, @@ -85,39 +80,3 @@ impl Default for Balloon { } } } - -fn spawn_balloon( - mut commands: Commands, - mut meshes: ResMut>, - mut materials: ResMut>, -) { - let radius = 0.3; - let shape = Sphere::new(radius); - commands.spawn( - Name::new("Balloon"), - SimulatedBody, - BalloonBundle { - balloon: Balloon::default(), - gas: IdealGasBundle::new( - shape, - GasSpecies::helium(), - Temperature::STANDARD, - Pressure::STANDARD, - ), - mesh: meshes.add(sphere), - }, - RigidBody::Dynamic, - ); -} - -// The balloon is stretched around the volume of the gas inside of it. -// This system updates the balloon's volume and radius based on the gas -// pressure. -// fn update_balloon_mesh( -// asset_server: Res, -// mut query: Query<(&mut Mesh3d, &mut Shape, &Volume), With>, -// ) { -// for (mut mesh, mut shape, volume) in query.iter_mut() { -// shape.radius = sphere_radius_from_volume(*volume); -// } -// } diff --git a/src/simulator/core.rs b/src/simulator/core.rs new file mode 100644 index 0000000..b45c309 --- /dev/null +++ b/src/simulator/core.rs @@ -0,0 +1,51 @@ +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(), + ); + } +} + +#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)] +pub enum SimState { + #[default] + Running, + Stopped, + Anomaly, +} diff --git a/src/simulator/forces/aero.rs b/src/simulator/forces/aero.rs index 9c5497f..a4b3759 100644 --- a/src/simulator/forces/aero.rs +++ b/src/simulator/forces/aero.rs @@ -1,9 +1,7 @@ //! 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}; @@ -12,7 +10,6 @@ pub struct AeroForcesPlugin; 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)); } } @@ -76,7 +73,7 @@ fn update_drag_parameters( velocity.0, atmosphere.density(position.0), projected_spherical_area(bounding_sphere.radius()), - drag_coefficient(&Ball::new(bounding_sphere.radius()), &atmosphere), + 1.17, // default drag coefficient for a sphere ); } } @@ -96,11 +93,11 @@ 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..a2c63ce 100644 --- a/src/simulator/forces/body.rs +++ b/src/simulator/forces/body.rs @@ -2,7 +2,6 @@ use avian3d::prelude::*; use bevy::prelude::*; -use bevy_trait_query::{self, RegisterExt}; use super::{Atmosphere, Density, Force, ForceUpdateOrder, Mass, SimulatedBody, Volume}; use crate::simulator::properties::{EARTH_RADIUS_M, STANDARD_G}; @@ -14,9 +13,6 @@ impl Plugin for BodyForcesPlugin { app.register_type::(); app.register_type::(); - app.register_component_as::(); - app.register_component_as::(); - app.add_systems( Update, (update_weight_parameters, update_buoyant_parameters).in_set(ForceUpdateOrder::Prepare), @@ -73,7 +69,7 @@ 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()); } } diff --git a/src/simulator/forces/mod.rs b/src/simulator/forces/mod.rs index a5bb436..b72c431 100644 --- a/src/simulator/forces/mod.rs +++ b/src/simulator/forces/mod.rs @@ -4,7 +4,6 @@ pub mod body; use avian3d::prelude::*; use bevy::prelude::*; -use bevy_trait_query; // Re-expert common forces #[allow(unused_imports)] @@ -12,7 +11,7 @@ pub use body::{Weight, Buoyancy}; #[allow(unused_imports)] pub use aero::Drag; -use super::{Atmosphere, Density, Mass, Volume, SimulatedBody}; +use super::{Atmosphere, Density, Volume, SimulatedBody}; pub struct ForcesPlugin; impl Plugin for ForcesPlugin { @@ -35,10 +34,10 @@ impl Plugin for ForcesPlugin { Update, on_simulated_body_added.in_set(ForceUpdateOrder::First), ); - app.add_systems( - Update, - update_total_external_force.in_set(ForceUpdateOrder::Apply), - ); + // app.add_systems( + // Update, + // update_total_external_force.in_set(ForceUpdateOrder::Apply), + // ); app.add_plugins((aero::AeroForcesPlugin, body::BodyForcesPlugin)); } @@ -60,7 +59,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()); @@ -71,7 +69,6 @@ fn on_simulated_body_added(mut commands: Commands, query: Query String { String::from("Force") @@ -86,26 +83,26 @@ pub trait Force { fn point_of_application(&self) -> Vec3; } -/// Set the `ExternalForce` to the sum of all forces in the `Forces` collection. -/// This effectively applies all the calculated force vectors to the physics -/// rigid body without regard to where the forces came from. -/// -/// TODO: preserve the position of the total force vector and apply it at that -/// point instead of the center of mass. -fn update_total_external_force( - mut body_forces: Query<(&mut ExternalForce, &dyn Force, &RigidBody), With>, -) { - // Iterate over each entity that has force vector components. - for (mut physics_force_component, acting_forces, rigid_body) in body_forces.iter_mut() { - // Forces only act on dynamic bodies. Don't bother with other kinds. - if rigid_body.is_dynamic() { - let mut net_force = Vec3::ZERO; // reset the net force to zero +// Set the `ExternalForce` to the sum of all forces in the `Forces` collection. +// This effectively applies all the calculated force vectors to the physics +// rigid body without regard to where the forces came from. +// +// TODO: preserve the position of the total force vector and apply it at that +// point instead of the center of mass. +// fn update_total_external_force( +// mut body_forces: Query<(&mut ExternalForce, &dyn Force, &RigidBody), With>, +// ) { +// // Iterate over each entity that has force vector components. +// for (mut physics_force_component, acting_forces, rigid_body) in body_forces.iter_mut() { +// // Forces only act on dynamic bodies. Don't bother with other kinds. +// if rigid_body.is_dynamic() { +// let mut net_force = Vec3::ZERO; // reset the net force to zero - // Iterate over each force vector component and compute its value. - for force in acting_forces.iter() { - net_force += force.force(); - } - physics_force_component.set_force(net_force); - } - } -} +// // Iterate over each force vector component and compute its value. +// for force in acting_forces.iter() { +// 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 4d1bc82..8710ac5 100644 --- a/src/simulator/ideal_gas.rs +++ b/src/simulator/ideal_gas.rs @@ -2,10 +2,9 @@ use avian3d::prelude::*; use bevy::prelude::*; -#[cfg(feature = "config-files")] -use serde::{Deserialize, Serialize}; -use crate::simulator::properties::*; +use super::{Pressure, Temperature, Volume, Density, MolarMass}; +use super::properties::{AVOGADRO_CONSTANT, BOLTZMANN_CONSTANT}; pub const R: f32 = BOLTZMANN_CONSTANT * AVOGADRO_CONSTANT; // [J/K-mol] Ideal gas constant @@ -15,16 +14,21 @@ 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_pressure_from_volume, // // update_ideal_gas_density_from_volume, // )); } } +#[derive(Bundle)] +pub struct IdealGasBundle { + pub gas: IdealGas, + pub species: GasSpecies, +} + /// Molecular species of a gas. /// TODO: load species from a file #[derive(Component, Debug, Clone, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct GasSpecies { pub name: String, pub abbreviation: String, @@ -67,8 +71,10 @@ impl GasSpecies { } } -/// A finite amount of a particular ideal gas. -#[derive(Component, Debug)] +/// A finite amount of an ideal gas. Mass properties depend on the +/// [`GasSpecies`]. A gas will expand to fill its [`BoundingVolume`]. +#[derive(Component, Debug, Clone, PartialEq)] +#[require(GasSpecies)] pub struct IdealGas { pub temperature: Temperature, pub pressure: Pressure, @@ -84,7 +90,7 @@ pub fn ideal_gas_volume( species: &GasSpecies, ) -> Volume { Volume( - (mass.0 / species.molar_mass.kilograms_per_mole()) + (mass.value() / species.molar_mass.kilograms_per_mole()) * R * temperature.kelvin() / pressure.pascal(), ) diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index 26afa74..96be228 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,56 +7,10 @@ pub mod ideal_gas; pub mod payload; pub mod properties; -use avian3d::prelude::*; -use bevy::app::PluginGroupBuilder; -use bevy::prelude::*; - // Re-export the properties module at the top level. +pub use core::{SimulatorPlugins, SimState, SimulatedBody}; pub use atmosphere::Atmosphere; -#[allow(unused-imports)] -pub use properties::{Density, Pressure, Temperature, Volume}; - -/// 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( - OnEnter(SimState::Running), - |mut time: ResMut>| time.as_mut().unpause(), - ); - app.add_systems( - OnExit(SimState::Running), - |mut time: ResMut>| time.as_mut().pause(), - ); - } -} - -#[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)] -pub enum SimState { - #[default] - Running, - Stopped, - Anomaly, -} +pub use properties::{Density, Pressure, Temperature, Volume, MolarMass}; +pub use balloon::{Balloon, BalloonBundle}; +pub use forces::{Weight, Buoyancy, Drag}; +pub use ideal_gas::{GasSpecies, IdealGas, IdealGasBundle}; diff --git a/src/simulator/properties.rs b/src/simulator/properties.rs index db93cc3..8c66362 100644 --- a/src/simulator/properties.rs +++ b/src/simulator/properties.rs @@ -1,4 +1,6 @@ //! Basic physical properties. +#![allow(dead_code)] + use std::ops::{Add, Div, Mul, Sub}; use avian3d::{ @@ -6,8 +8,6 @@ use avian3d::{ 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] @@ -44,7 +44,6 @@ impl Plugin for CorePropertiesPlugin { /// Temperature (K) #[derive(Component,Debug, Default, Clone, Copy, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct Temperature(pub Scalar); impl Temperature { @@ -101,7 +100,6 @@ impl Div for Temperature { /// Pressure (Pa) #[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] -#[cfg_attr(feature = "config-files", derive(Serialize, Deserialize))] pub struct Pressure(pub Scalar); impl Pressure { @@ -158,7 +156,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 { @@ -208,14 +205,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 { @@ -261,7 +257,6 @@ impl Div for Density { /// 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 { @@ -285,3 +280,11 @@ impl Div for MolarMass { MolarMass(self.0 / rhs) } } + +/// A 3d volume. +/// TODO: Use this as the basis for the balloon mesh, volume and drag +/// calculations, and the physics collider. +#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] +pub struct BoundingVolume { + pub radius: f32, +} From f6a49a9009c6bc78016ed78755b9903fddb66489 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Sat, 23 Nov 2024 20:20:30 -0500 Subject: [PATCH 04/10] bevy: reintroduce bevy trait query on 0.15 branch --- Cargo.toml | 1 + docs/devlog.md | 2 +- src/lib.rs | 1 - src/simulator/balloon.rs | 2 +- src/simulator/forces/aero.rs | 3 ++ src/simulator/forces/body.rs | 4 +++ src/simulator/forces/mod.rs | 54 +++++++++++++++++++----------------- 7 files changed, 38 insertions(+), 29 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb1e572..40372b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ dev_native = [ [dependencies] 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" } thiserror = "2.0.3" # ----------------------------------------------------------------------------- diff --git a/docs/devlog.md b/docs/devlog.md index fba4b1d..d44700a 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -42,7 +42,7 @@ update them or remove them. - [x] `avian3d` -> branch `bevy-0.15` - [ ] ~~`bevy_heavy`~~ remove for now -- [ ] ~~`bevy-trait-query`~~ 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 diff --git a/src/lib.rs b/src/lib.rs index 6bc958e..6755329 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,6 @@ use bevy::{asset::AssetMetaCheck, prelude::*}; /// Re-export for convenience pub use crate::app3d::{ {ScenePlugin, InterfacePlugins}, - controls::{KeyBindingsConfig, CameraControls, DebugControls, TimeControls} }; pub struct YahsPlugin; diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index a789541..6a5406f 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -1,6 +1,6 @@ //! Properties, attributes and functions related to the balloon. -use avian3d::{math::PI, prelude::*}; +use avian3d::math::PI; use bevy::prelude::*; use super::{ diff --git a/src/simulator/forces/aero.rs b/src/simulator/forces/aero.rs index a4b3759..4e0c5ca 100644 --- a/src/simulator/forces/aero.rs +++ b/src/simulator/forces/aero.rs @@ -2,6 +2,7 @@ use avian3d::{math::PI, prelude::*}; use bevy::prelude::*; +use bevy_trait_query::{self, RegisterExt}; use super::{Atmosphere, Density, ForceUpdateOrder, Force, SimulatedBody}; @@ -10,6 +11,8 @@ pub struct AeroForcesPlugin; 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)); } } diff --git a/src/simulator/forces/body.rs b/src/simulator/forces/body.rs index a2c63ce..9aec62d 100644 --- a/src/simulator/forces/body.rs +++ b/src/simulator/forces/body.rs @@ -2,6 +2,7 @@ use avian3d::prelude::*; use bevy::prelude::*; +use bevy_trait_query::{self, RegisterExt}; use super::{Atmosphere, Density, Force, ForceUpdateOrder, Mass, SimulatedBody, Volume}; use crate::simulator::properties::{EARTH_RADIUS_M, STANDARD_G}; @@ -13,6 +14,9 @@ impl Plugin for BodyForcesPlugin { app.register_type::(); app.register_type::(); + app.register_component_as::(); + app.register_component_as::(); + app.add_systems( Update, (update_weight_parameters, update_buoyant_parameters).in_set(ForceUpdateOrder::Prepare), diff --git a/src/simulator/forces/mod.rs b/src/simulator/forces/mod.rs index b72c431..1343f48 100644 --- a/src/simulator/forces/mod.rs +++ b/src/simulator/forces/mod.rs @@ -4,6 +4,7 @@ pub mod body; use avian3d::prelude::*; use bevy::prelude::*; +use bevy_trait_query; // Re-expert common forces #[allow(unused_imports)] @@ -34,10 +35,10 @@ impl Plugin for ForcesPlugin { Update, on_simulated_body_added.in_set(ForceUpdateOrder::First), ); - // app.add_systems( - // Update, - // update_total_external_force.in_set(ForceUpdateOrder::Apply), - // ); + app.add_systems( + Update, + update_total_external_force.in_set(ForceUpdateOrder::Apply), + ); app.add_plugins((aero::AeroForcesPlugin, body::BodyForcesPlugin)); } @@ -69,6 +70,7 @@ fn on_simulated_body_added(mut commands: Commands, query: Query String { String::from("Force") @@ -83,26 +85,26 @@ pub trait Force { fn point_of_application(&self) -> Vec3; } -// Set the `ExternalForce` to the sum of all forces in the `Forces` collection. -// This effectively applies all the calculated force vectors to the physics -// rigid body without regard to where the forces came from. -// -// TODO: preserve the position of the total force vector and apply it at that -// point instead of the center of mass. -// fn update_total_external_force( -// mut body_forces: Query<(&mut ExternalForce, &dyn Force, &RigidBody), With>, -// ) { -// // Iterate over each entity that has force vector components. -// for (mut physics_force_component, acting_forces, rigid_body) in body_forces.iter_mut() { -// // Forces only act on dynamic bodies. Don't bother with other kinds. -// if rigid_body.is_dynamic() { -// let mut net_force = Vec3::ZERO; // reset the net force to zero +/// Set the `ExternalForce` to the sum of all forces in the `Forces` collection. +/// This effectively applies all the calculated force vectors to the physics +/// rigid body without regard to where the forces came from. +/// +/// TODO: preserve the position of the total force vector and apply it at that +/// point instead of the center of mass. +fn update_total_external_force( + mut body_forces: Query<(&mut ExternalForce, &dyn Force, &RigidBody), With>, +) { + // Iterate over each entity that has force vector components. + for (mut physics_force_component, acting_forces, rigid_body) in body_forces.iter_mut() { + // Forces only act on dynamic bodies. Don't bother with other kinds. + if rigid_body.is_dynamic() { + let mut net_force = Vec3::ZERO; // reset the net force to zero -// // Iterate over each force vector component and compute its value. -// for force in acting_forces.iter() { -// net_force += force.force(); -// } -// physics_force_component.set_force(net_force); -// } -// } -// } + // Iterate over each force vector component and compute its value. + for force in acting_forces.iter() { + net_force += force.force(); + } + physics_force_component.set_force(net_force); + } + } +} From d3c2f201f1b20cf485cf2132b52df7709576413d Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Sat, 23 Nov 2024 23:09:07 -0500 Subject: [PATCH 05/10] balloon: nice --- src/app3d/{scene => }/camera.rs | 2 +- src/app3d/controls.rs | 1 + src/app3d/mod.rs | 23 ++++-- src/app3d/{scene/mod.rs => scene.rs} | 47 ++++++------ src/app3d/scene/vectors.rs | 35 --------- src/app3d/ui/core.rs | 19 ++--- src/app3d/ui/mod.rs | 2 +- src/lib.rs | 19 +---- src/simulator/balloon.rs | 49 ++++++------- src/simulator/core.rs | 20 ++++++ src/simulator/forces/mod.rs | 18 ++--- src/simulator/ideal_gas.rs | 102 +++++++++++++++++++++------ src/simulator/mod.rs | 8 +-- src/simulator/properties.rs | 8 --- 14 files changed, 194 insertions(+), 159 deletions(-) rename src/app3d/{scene => }/camera.rs (83%) rename src/app3d/{scene/mod.rs => scene.rs} (68%) delete mode 100644 src/app3d/scene/vectors.rs diff --git a/src/app3d/scene/camera.rs b/src/app3d/camera.rs similarity index 83% rename from src/app3d/scene/camera.rs rename to src/app3d/camera.rs index 5f07884..1f8f018 100644 --- a/src/app3d/scene/camera.rs +++ b/src/app3d/camera.rs @@ -15,6 +15,6 @@ fn setup(mut commands: Commands) { // 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., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), + Transform::from_xyz(0.0, 20., 50.0).looking_at(Vec3::new(0., 20., 0.), Vec3::Y), )); } diff --git a/src/app3d/controls.rs b/src/app3d/controls.rs index 68ea9ae..ea3c58c 100644 --- a/src/app3d/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, diff --git a/src/app3d/mod.rs b/src/app3d/mod.rs index a5b24ff..08683ca 100644 --- a/src/app3d/mod.rs +++ b/src/app3d/mod.rs @@ -1,8 +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; -pub use controls::ControlsPlugin; +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/mod.rs b/src/app3d/scene.rs similarity index 68% rename from src/app3d/scene/mod.rs rename to src/app3d/scene.rs index 730bbcf..6ec140a 100644 --- a/src/app3d/scene/mod.rs +++ b/src/app3d/scene.rs @@ -1,5 +1,3 @@ -mod camera; - use avian3d::prelude::*; use bevy::prelude::*; @@ -9,7 +7,6 @@ pub struct ScenePlugin; impl Plugin for ScenePlugin { fn build(&self, app: &mut App) { - app.add_plugins(camera::CameraPlugin); app.add_systems(Startup, (simple_scene, spawn_balloon)); } } @@ -22,16 +19,11 @@ fn simple_scene( ) { 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::default()); - - // light - commands.spawn(( - Name::new("Light"), - PointLight { - intensity: 1500.0, - ..default() - }, - )); + let plane_material = materials.add(StandardMaterial { + base_color: Color::srgb(0.5, 0.5, 0.5), + perceptual_roughness: 0.5, + ..default() + }); // ground commands.spawn(( @@ -49,6 +41,10 @@ fn simple_scene( Collider::cuboid(ground_size.x, ground_size.y, ground_size.z), Mesh3d(plane.clone()), MeshMaterial3d(plane_material.clone()), + Transform { + translation: Vec3::new(0.0, 40.0, 0.0), + ..default() + }, )); } @@ -58,27 +54,28 @@ pub fn spawn_balloon( mut materials: ResMut>, ) { let debug_material = materials.add(StandardMaterial { - base_color: Color::srgb(1.0, 0.0, 0.0), + base_color: Color::srgba(1.0, 0.0, 0.0, 0.5), + perceptual_roughness: 0.0, + metallic: 1.0, ..default() }); let shape = meshes.add(Sphere::default().mesh().ico(5).unwrap()); + let species = GasSpecies::helium(); commands.spawn(( Name::new("Balloon"), SimulatedBody, + Balloon, BalloonBundle { - balloon: Balloon::default(), - gas: IdealGasBundle { - species: GasSpecies::helium(), - gas: IdealGas { - temperature: Temperature::STANDARD, - pressure: Pressure::STANDARD, - mass: Mass::new(1.0), - }, - }, + material_properties: BalloonMaterial::default(), + mesh: Mesh3d(shape), + material: MeshMaterial3d(debug_material), + gas: IdealGas::new(species), }, RigidBody::Dynamic, - Mesh3d(shape), - MeshMaterial3d(debug_material), + Transform { + translation: Vec3::new(0.0, 10.0, 0.0), + ..default() + }, )); } 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 84f7f3f..4a12033 100644 --- a/src/app3d/ui/core.rs +++ b/src/app3d/ui/core.rs @@ -1,4 +1,4 @@ -use bevy::{app::PluginGroupBuilder, prelude::*}; +use bevy::prelude::*; // use iyes_perf_ui::prelude::*; use crate::app3d::controls::KeyBindingsConfig; @@ -7,14 +7,15 @@ 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, + )); } } diff --git a/src/app3d/ui/mod.rs b/src/app3d/ui/mod.rs index 78f1cb0..d2b208f 100644 --- a/src/app3d/ui/mod.rs +++ b/src/app3d/ui/mod.rs @@ -4,4 +4,4 @@ mod core; #[cfg(feature = "dev")] mod dev_tools; -pub use core::InterfacePlugins; +pub use core::InterfacePlugin; diff --git a/src/lib.rs b/src/lib.rs index 6755329..319ab1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,17 +4,12 @@ mod simulator; use bevy::{asset::AssetMetaCheck, prelude::*}; -/// Re-export for convenience -pub use crate::app3d::{ - {ScenePlugin, InterfacePlugins}, -}; - 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) @@ -35,18 +30,8 @@ impl Plugin for YahsPlugin { .into(), ..default() }), - ); - - // Add the simulator plugins that don't deal with graphics. - app.add_plugins(( simulator::SimulatorPlugins, - )); - - // Add the 3D application plugins. - app.add_plugins(( - app3d::ScenePlugin, - app3d::InterfacePlugins, - app3d::ControlsPlugin, + app3d::App3dPlugins, )); } } diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index 6a5406f..7e3b49a 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -1,12 +1,12 @@ //! Properties, attributes and functions related to the balloon. -use avian3d::math::PI; +use avian3d::{math::PI, prelude::Position}; use bevy::prelude::*; use super::{ SimulatedBody, - ideal_gas::{GasSpecies, IdealGasBundle, IdealGas}, - properties::*, + SimulationUpdateOrder, + ideal_gas::IdealGas, }; pub struct BalloonPlugin; @@ -16,16 +16,28 @@ impl Plugin for BalloonPlugin { // 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; + +/// 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 material_properties: BalloonMaterial, + pub gas: IdealGas, + pub mesh: Mesh3d, + pub material: MeshMaterial3d, } -#[derive(Debug, Clone, PartialEq, Reflect)] +#[derive(Component, Debug, Clone, PartialEq, Reflect)] pub struct BalloonMaterial { pub name: String, pub max_temperature: f32, // temperature (K) where the given material fails @@ -38,6 +50,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 { @@ -54,29 +67,13 @@ 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 is the surface of a [`BoundingVolume`] that can -/// be stretched around a [`GasSpecies`] based on the pressure inside. -#[derive(Component, Reflect)] -#[require(BoundingVolume)] -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 * PI, - } +fn update_balloon_from_gas(mut query: Query<(&mut Mesh3d, &IdealGas)>) { + for (mut mesh, gas) in query.iter_mut() { + // mesh.volume = gas.volume(); } } diff --git a/src/simulator/core.rs b/src/simulator/core.rs index b45c309..0c85f5a 100644 --- a/src/simulator/core.rs +++ b/src/simulator/core.rs @@ -39,6 +39,17 @@ impl Plugin for CorePhysicsPlugin { 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), + ); } } @@ -49,3 +60,12 @@ pub enum SimState { Stopped, Anomaly, } + +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub enum SimulationUpdateOrder { + First, + IdealGas, + MeshVolumes, + Forces, + Last, +} diff --git a/src/simulator/forces/mod.rs b/src/simulator/forces/mod.rs index 1343f48..df25625 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, Volume, SimulatedBody}; +use super::{Atmosphere, Density, SimulatedBody, SimulationUpdateOrder, 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, @@ -45,7 +45,7 @@ impl Plugin for ForcesPlugin { } #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] -enum ForceUpdateOrder { +pub enum ForceUpdateOrder { First, Prepare, Apply, diff --git a/src/simulator/ideal_gas.rs b/src/simulator/ideal_gas.rs index 8710ac5..8bb4276 100644 --- a/src/simulator/ideal_gas.rs +++ b/src/simulator/ideal_gas.rs @@ -1,10 +1,10 @@ //! Ideal gas equations. - +#![allow(dead_code)] use avian3d::prelude::*; use bevy::prelude::*; -use super::{Pressure, Temperature, Volume, Density, MolarMass}; 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 @@ -13,22 +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_pressure_from_volume, - // // update_ideal_gas_density_from_volume, - // )); + app.add_systems( + Update, + update_ideal_gas_from_atmosphere.in_set(SimulationUpdateOrder::IdealGas), + ); } } -#[derive(Bundle)] -pub struct IdealGasBundle { - pub gas: IdealGas, - pub species: GasSpecies, -} - /// Molecular species of a gas. -/// TODO: load species from a file -#[derive(Component, Debug, Clone, PartialEq, Reflect)] +#[derive(Debug, Clone, PartialEq, Reflect)] pub struct GasSpecies { pub name: String, pub abbreviation: String, @@ -71,14 +64,72 @@ impl GasSpecies { } } -/// A finite amount of an ideal gas. Mass properties depend on the -/// [`GasSpecies`]. A gas will expand to fill its [`BoundingVolume`]. +/// Properties of an ideal gas. For properties per unit mass, set the mass to 1. #[derive(Component, Debug, Clone, PartialEq)] -#[require(GasSpecies)] 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 + } + + 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), @@ -90,8 +141,7 @@ pub fn ideal_gas_volume( species: &GasSpecies, ) -> Volume { Volume( - (mass.value() / species.molar_mass.kilograms_per_mole()) - * R * temperature.kelvin() + (mass.value() / species.molar_mass.kilograms_per_mole()) * R * temperature.kelvin() / pressure.pascal(), ) } @@ -103,7 +153,9 @@ pub fn ideal_gas_density( pressure: Pressure, species: &GasSpecies, ) -> Density { - Density(species.molar_mass.kilograms_per_mole() * pressure.pascal() / (R * temperature.kelvin())) + Density( + species.molar_mass.kilograms_per_mole() * pressure.pascal() / (R * temperature.kelvin()), + ) } #[allow(dead_code)] @@ -113,3 +165,13 @@ pub fn ideal_gas_density( pub fn gage_pressure(pressure: Pressure, ambient_pressure: Pressure) -> Pressure { pressure - ambient_pressure } + +fn update_ideal_gas_from_atmosphere( + mut query: Query<(&mut IdealGas, &Position)>, + atmosphere: Res, +) { + for (mut gas, position) in query.iter_mut() { + gas.pressure = atmosphere.pressure(position.0); + gas.temperature = atmosphere.temperature(position.0); + } +} diff --git a/src/simulator/mod.rs b/src/simulator/mod.rs index 96be228..f0e957a 100644 --- a/src/simulator/mod.rs +++ b/src/simulator/mod.rs @@ -8,9 +8,9 @@ pub mod payload; pub mod properties; // Re-export the properties module at the top level. -pub use core::{SimulatorPlugins, SimState, SimulatedBody}; -pub use atmosphere::Atmosphere; +pub use core::{SimulatorPlugins, SimState, SimulatedBody, SimulationUpdateOrder}; pub use properties::{Density, Pressure, Temperature, Volume, MolarMass}; -pub use balloon::{Balloon, BalloonBundle}; +pub use atmosphere::Atmosphere; pub use forces::{Weight, Buoyancy, Drag}; -pub use ideal_gas::{GasSpecies, IdealGas, IdealGasBundle}; +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 8c66362..36f926b 100644 --- a/src/simulator/properties.rs +++ b/src/simulator/properties.rs @@ -280,11 +280,3 @@ impl Div for MolarMass { MolarMass(self.0 / rhs) } } - -/// A 3d volume. -/// TODO: Use this as the basis for the balloon mesh, volume and drag -/// calculations, and the physics collider. -#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] -pub struct BoundingVolume { - pub radius: f32, -} From 2dd349da8ef56cb5036808c0f6a17f24b0ea1b76 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Sat, 23 Nov 2024 23:18:04 -0500 Subject: [PATCH 06/10] checkpoint --- src/simulator/balloon.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index 7e3b49a..392a4cf 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -7,6 +7,7 @@ use super::{ SimulatedBody, SimulationUpdateOrder, ideal_gas::IdealGas, + properties::sphere_radius_from_volume, }; pub struct BalloonPlugin; @@ -74,6 +75,7 @@ impl Default for BalloonMaterial { fn update_balloon_from_gas(mut query: Query<(&mut Mesh3d, &IdealGas)>) { for (mut mesh, gas) in query.iter_mut() { - // mesh.volume = gas.volume(); + // let new_radius = sphere_radius_from_volume(gas.volume().m3()); + // mesh.0.scale = Vec3::new(new_radius, new_radius, new_radius); } } From c80aa64d2bbd4cc2c1932889863bada2f8a1d0c2 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Sun, 24 Nov 2024 19:14:59 -0500 Subject: [PATCH 07/10] ui: some debug ui practice --- Cargo.toml | 3 +- docs/devlog.md | 8 ++ ...rce-in-External-Flows-Lesson-2-Handout.pdf | Bin docs/{ => reference}/Fluids-lecture.pdf | Bin ...n_Aerodynamics_for_General_Body_Shapes.pdf | Bin docs/{ => reference}/newtonian_aero.md | 0 src/app3d/controls.rs | 12 +- src/app3d/scene.rs | 19 +-- src/app3d/ui/core.rs | 2 +- src/app3d/ui/dev_tools.rs | 126 ++++++++++-------- src/app3d/ui/mod.rs | 2 +- src/app3d/ui/monitors.rs | 1 + src/simulator/atmosphere.rs | 20 +-- src/simulator/balloon.rs | 1 - 14 files changed, 102 insertions(+), 92 deletions(-) rename docs/{ => reference}/Drag-Force-in-External-Flows-Lesson-2-Handout.pdf (100%) rename docs/{ => reference}/Fluids-lecture.pdf (100%) rename docs/{ => reference}/Newtonian_Aerodynamics_for_General_Body_Shapes.pdf (100%) rename docs/{ => reference}/newtonian_aero.md (100%) diff --git a/Cargo.toml b/Cargo.toml index 40372b2..541053a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ dev = [ # library. "bevy/dynamic_linking", "bevy/bevy_dev_tools", + "bevy/sysinfo_plugin", ] dev_native = [ "dev", @@ -32,7 +33,7 @@ dev_native = [ 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" } -thiserror = "2.0.3" +iyes_perf_ui = { git = "https://github.com/JohnathanFL/iyes_perf_ui.git", branch = "main" } # ----------------------------------------------------------------------------- # Some Bevy optimizations diff --git a/docs/devlog.md b/docs/devlog.md index d44700a..cd63d5c 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -1,5 +1,13 @@ # development log +## 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 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/controls.rs b/src/app3d/controls.rs index ea3c58c..2a0c010 100644 --- a/src/app3d/controls.rs +++ b/src/app3d/controls.rs @@ -26,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)] @@ -53,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/scene.rs b/src/app3d/scene.rs index 6ec140a..26cc17c 100644 --- a/src/app3d/scene.rs +++ b/src/app3d/scene.rs @@ -29,22 +29,9 @@ fn simple_scene( commands.spawn(( Name::new("Ground"), RigidBody::Static, - Collider::cuboid(ground_size.x, ground_size.y, ground_size.z), Mesh3d(plane.clone()), MeshMaterial3d(plane_material.clone()), - )); - - // ceiling - commands.spawn(( - Name::new("Ceiling"), - RigidBody::Static, - Collider::cuboid(ground_size.x, ground_size.y, ground_size.z), - Mesh3d(plane.clone()), - MeshMaterial3d(plane_material.clone()), - Transform { - translation: Vec3::new(0.0, 40.0, 0.0), - ..default() - }, + ColliderConstructor::TrimeshFromMesh, )); } @@ -68,14 +55,14 @@ pub fn spawn_balloon( BalloonBundle { material_properties: BalloonMaterial::default(), mesh: Mesh3d(shape), - material: MeshMaterial3d(debug_material), gas: IdealGas::new(species), }, RigidBody::Dynamic, + ColliderConstructor::TrimeshFromMesh, Transform { translation: Vec3::new(0.0, 10.0, 0.0), ..default() }, + MeshMaterial3d(debug_material), )); - } diff --git a/src/app3d/ui/core.rs b/src/app3d/ui/core.rs index 4a12033..65ad6de 100644 --- a/src/app3d/ui/core.rs +++ b/src/app3d/ui/core.rs @@ -14,7 +14,7 @@ impl Plugin for InterfacePlugin { app.add_plugins(( CoreUiPlugin, PausePlayPlugin, - // monitors::MonitorsPlugin, + monitors::MonitorsPlugin, )); } } diff --git a/src/app3d/ui/dev_tools.rs b/src/app3d/ui/dev_tools.rs index 04fc5aa..b681084 100644 --- a/src/app3d/ui/dev_tools.rs +++ b/src/app3d/ui/dev_tools.rs @@ -1,20 +1,19 @@ //! 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, prelude::*, }; -// use iyes_perf_ui::prelude::*; - -#[cfg(not(target_arch = "wasm32"))] -use bevy::pbr::wireframe::{WireframeConfig, WireframePlugin}; - -use avian3d::debug_render::PhysicsDebugPlugin; +use iyes_perf_ui::{prelude::*, entries::PerfUiBundle}; use crate::app3d::controls::KeyBindingsConfig; @@ -26,19 +25,20 @@ pub(super) fn plugin(app: &mut App) { // performance FrameTimeDiagnosticsPlugin, EntityCountDiagnosticsPlugin, - SystemInformationDiagnosticsPlugin, // rendering #[cfg(not(target_arch = "wasm32"))] WireframePlugin, )); + app.init_resource::(); + app.add_event::(); + app.add_event::(); + app.add_observer(spawn_perf_ui); + app.add_observer(despawn_perf_ui); + // 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; @@ -46,58 +46,66 @@ pub(super) fn plugin(app: &mut App) { // } } -// /// Toggle the debug overlay -// fn toggle_debug_ui( -// mut commands: Commands, -// q_root: Query>, -// key_input: Res>, -// key_bindings: Res, -// ) { -// 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(); -// } 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(), -// ), -// )); -// } -// } -// } +#[derive(Debug, Default, Resource)] +struct DebugState { + wireframe: bool, + physics_debug: bool, + perf_ui: bool, +} + +#[allow(dead_code)] +#[derive(Component, Default)] +struct DebugUi; #[cfg(not(target_arch = "wasm32"))] -fn toggle_wireframe( +fn toggle_debug_ui( + mut commands: Commands, 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) { + debug_state.perf_ui = !debug_state.perf_ui; + warn!("perf ui: {}", debug_state.perf_ui); + if debug_state.perf_ui { + commands.trigger(SpawnPerfUi); + } else { + commands.trigger(DespawnPerfUi); + } + } +} + +#[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 d2b208f..a4fcc13 100644 --- a/src/app3d/ui/mod.rs +++ b/src/app3d/ui/mod.rs @@ -1,5 +1,5 @@ mod core; -// mod monitors; +mod monitors; #[cfg(feature = "dev")] mod dev_tools; diff --git a/src/app3d/ui/monitors.rs b/src/app3d/ui/monitors.rs index a344b19..3e45446 100644 --- a/src/app3d/ui/monitors.rs +++ b/src/app3d/ui/monitors.rs @@ -20,6 +20,7 @@ impl Plugin for MonitorsPlugin { app.add_systems(Startup, spawn_monitors); app.add_systems(Update, update_force_monitor_values); app.init_resource::(); + app.add_plugins(PerfUiPlugin); } } diff --git a/src/simulator/atmosphere.rs b/src/simulator/atmosphere.rs index 9cc2352..fcdde9a 100644 --- a/src/simulator/atmosphere.rs +++ b/src/simulator/atmosphere.rs @@ -60,14 +60,16 @@ impl Atmosphere { } } -#[derive(Debug, thiserror::Error)] +#[derive(Debug)] enum AtmosphereError { - #[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), + // 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), + OutOfBounds, } /// If any of the simulated bodies are out of bounds, set the app state to anomaly @@ -95,7 +97,7 @@ 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(AtmosphereError::OutOfBounds(altitude)) + Err(AtmosphereError::OutOfBounds) } } @@ -124,6 +126,6 @@ fn coesa_pressure(altitude: f32) -> Result { ), )) } else { - Err(AtmosphereError::OutOfBounds(altitude)) + Err(AtmosphereError::OutOfBounds) } } diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index 392a4cf..fc659a6 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -35,7 +35,6 @@ pub struct BalloonBundle { pub material_properties: BalloonMaterial, pub gas: IdealGas, pub mesh: Mesh3d, - pub material: MeshMaterial3d, } #[derive(Component, Debug, Clone, PartialEq, Reflect)] From 8467b394b25c3bd91db16d824fcd6fb097420aa4 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Sun, 24 Nov 2024 20:44:23 -0500 Subject: [PATCH 08/10] checkpoint: trying to debug volume change over time --- src/app3d/scene.rs | 13 +++-- src/app3d/ui/monitors.rs | 120 +++++++++++++++++++++++++++++++++++++-- src/simulator/balloon.rs | 18 +++--- 3 files changed, 135 insertions(+), 16 deletions(-) diff --git a/src/app3d/scene.rs b/src/app3d/scene.rs index 26cc17c..7a07c43 100644 --- a/src/app3d/scene.rs +++ b/src/app3d/scene.rs @@ -46,23 +46,26 @@ pub fn spawn_balloon( metallic: 1.0, ..default() }); - let shape = meshes.add(Sphere::default().mesh().ico(5).unwrap()); + let sphere = Sphere::default(); + let shape = meshes.add(sphere.mesh().ico(5).unwrap()); let species = GasSpecies::helium(); commands.spawn(( Name::new("Balloon"), SimulatedBody, - Balloon, BalloonBundle { - material_properties: BalloonMaterial::default(), - mesh: Mesh3d(shape), + balloon: Balloon { + material_properties: BalloonMaterial::default(), + shape: sphere, + }, gas: IdealGas::new(species), }, RigidBody::Dynamic, - ColliderConstructor::TrimeshFromMesh, + 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/ui/monitors.rs b/src/app3d/ui/monitors.rs index 3e45446..88ffecb 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::lifetimeless::{SQuery, SRes, SystemParam}, prelude::*, }; use iyes_perf_ui::{entry::PerfUiEntry, prelude::*, utils::format_pretty_float}; @@ -9,18 +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, }; 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_systems(Startup, spawn_monitors); app.add_systems(Update, update_force_monitor_values); app.init_resource::(); - app.add_plugins(PerfUiPlugin); } } @@ -184,7 +185,7 @@ impl Default for ForceMonitor { impl PerfUiEntry for ForceMonitor { type Value = (f32, f32, f32); - type SystemParam = SRes; + type SystemParam = SRes; // FIXME: use &(dyn Force + 'static) instead fn label(&self) -> &str { if self.label.is_empty() { @@ -209,7 +210,7 @@ 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: {:} Buoyancy: {:} Drag: {:}", f_g, f_b, f_d) } fn update_value( @@ -237,3 +238,114 @@ impl PerfUiEntry for ForceMonitor { } } } + +#[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, + + pub volume: f32, + pub pressure: f32, + pub temperature: f32, + pub density: f32, + pub mass: f32, + pub species: String, +} + +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(), + volume: 0.0, + pressure: 0.0, + temperature: 0.0, + density: 0.0, + mass: 0.0, + species: String::new(), + } + } +} + +impl PerfUiEntry for GasMonitor { + type Value = (f32, f32, f32, f32, f32, String); + type SystemParam = SQuery<&'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); + + // (and append units to it) + if self.display_units { + volume.push_str(" m³"); + pressure.push_str(" Pa"); + temperature.push_str(" K"); + density.push_str(" kg/m³"); + mass.push_str(" kg"); + } + format!("Volume: {:} Pressure: {:} Temperature: {:} Density: {:} Mass: {:} Species: {:}", volume, pressure, temperature, density, mass, self.species) + } + + fn update_value( + &self, + gas: &mut ::Item<'_, '_>, + ) -> Option { + Some(( + gas.volume().m3(), + gas.pressure().pascal(), + gas.temperature().kelvin(), + gas.density().kilograms_per_cubic_meter(), + gas.mass().kilograms(), + gas.species().name(), + )) + } + + // (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 + 5 + } else { + w + } + } +} diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index fc659a6..000fe62 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -26,15 +26,17 @@ impl Plugin for BalloonPlugin { } #[derive(Component, Debug, Clone, PartialEq, Reflect)] -pub struct Balloon; +pub struct Balloon { + pub material_properties: BalloonMaterial, + pub shape: Sphere, +} /// 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 material_properties: BalloonMaterial, + pub balloon: Balloon, pub gas: IdealGas, - pub mesh: Mesh3d, } #[derive(Component, Debug, Clone, PartialEq, Reflect)] @@ -72,9 +74,11 @@ impl Default for BalloonMaterial { } } -fn update_balloon_from_gas(mut query: Query<(&mut Mesh3d, &IdealGas)>) { - for (mut mesh, gas) in query.iter_mut() { - // let new_radius = sphere_radius_from_volume(gas.volume().m3()); - // mesh.0.scale = Vec3::new(new_radius, new_radius, new_radius); +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; } } From 3de7b4167d26622d516bd4cf9a11c5c584d724b0 Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Tue, 26 Nov 2024 23:27:15 -0500 Subject: [PATCH 09/10] ui: add a gas monitor --- docs/devlog.md | 4 ++++ src/app3d/ui/dev_tools.rs | 2 +- src/app3d/ui/monitors.rs | 44 +++++++++++++++---------------------- src/simulator/atmosphere.rs | 6 ++--- src/simulator/ideal_gas.rs | 4 ++-- src/simulator/properties.rs | 10 ++++----- 6 files changed, 33 insertions(+), 37 deletions(-) diff --git a/docs/devlog.md b/docs/devlog.md index cd63d5c..ec15902 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -1,5 +1,9 @@ # development log +## 2024-11-26 + +- Added a `GasMonitor` to the UI for displaying the gas properties in real time. + ## 2024-11-24 I found a Bevy 0.15 branch of diff --git a/src/app3d/ui/dev_tools.rs b/src/app3d/ui/dev_tools.rs index b681084..0bee825 100644 --- a/src/app3d/ui/dev_tools.rs +++ b/src/app3d/ui/dev_tools.rs @@ -13,7 +13,7 @@ use bevy::{ input::common_conditions::input_just_pressed, prelude::*, }; -use iyes_perf_ui::{prelude::*, entries::PerfUiBundle}; +use iyes_perf_ui::prelude::*; use crate::app3d::controls::KeyBindingsConfig; diff --git a/src/app3d/ui/monitors.rs b/src/app3d/ui/monitors.rs index 88ffecb..1d7c39b 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::{SQuery, SRes, SystemParam}, + ecs::system::{SystemParam, lifetimeless::{SQuery, SRes}}, prelude::*, }; use iyes_perf_ui::{entry::PerfUiEntry, prelude::*, utils::format_pretty_float}; @@ -19,6 +19,7 @@ impl Plugin for MonitorsPlugin { 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::(); @@ -33,6 +34,7 @@ fn spawn_monitors(mut commands: Commands) { }, SimStateMonitor::default(), ForceMonitor::default(), + GasMonitor::default(), )); } @@ -210,7 +212,7 @@ impl PerfUiEntry for ForceMonitor { f_b.push_str(" N"); f_d.push_str(" N"); } - format!("Gravity: {:} Buoyancy: {:} Drag: {:}", f_g, f_b, f_d) + format!("Gravity: {}\nBuoyancy: {}\nDrag: {}", f_g, f_b, f_d) } fn update_value( @@ -257,13 +259,6 @@ struct GasMonitor { pub precision: u8, /// Required to ensure the entry appears in the correct place in the Perf UI pub sort_key: i32, - - pub volume: f32, - pub pressure: f32, - pub temperature: f32, - pub density: f32, - pub mass: f32, - pub species: String, } impl Default for GasMonitor { @@ -276,12 +271,6 @@ impl Default for GasMonitor { digits: 5, precision: 2, sort_key: iyes_perf_ui::utils::next_sort_key(), - volume: 0.0, - pressure: 0.0, - temperature: 0.0, - density: 0.0, - mass: 0.0, - species: String::new(), } } } @@ -309,29 +298,32 @@ impl PerfUiEntry for GasMonitor { 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(" m³"); - pressure.push_str(" Pa"); + volume.push_str(" m3"); + pressure.push_str(" kPa"); temperature.push_str(" K"); - density.push_str(" kg/m³"); + density.push_str(" kg/m3"); mass.push_str(" kg"); + species.push_str(""); } - format!("Volume: {:} Pressure: {:} Temperature: {:} Density: {:} Mass: {:} Species: {:}", volume, pressure, temperature, density, mass, self.species) + format!("{}\n{}\n{}\n{}\n{}\n{}", volume, pressure, temperature, density, mass, species) } fn update_value( &self, gas: &mut ::Item<'_, '_>, ) -> Option { + let instance = gas.get_single().unwrap(); Some(( - gas.volume().m3(), - gas.pressure().pascal(), - gas.temperature().kelvin(), - gas.density().kilograms_per_cubic_meter(), - gas.mass().kilograms(), - gas.species().name(), + instance.volume().m3(), + instance.pressure.kilopascals(), + instance.temperature.kelvin(), + instance.density.kilograms_per_cubic_meter(), + instance.mass.value(), + instance.species.name.clone(), )) } diff --git a/src/simulator/atmosphere.rs b/src/simulator/atmosphere.rs index fcdde9a..ad94bb9 100644 --- a/src/simulator/atmosphere.rs +++ b/src/simulator/atmosphere.rs @@ -106,7 +106,7 @@ fn coesa_temperature(altitude: f32) -> Result { /// Based on the US Standard Atmosphere, 1976. (aka COESA) 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, @@ -114,11 +114,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, diff --git a/src/simulator/ideal_gas.rs b/src/simulator/ideal_gas.rs index 8bb4276..00764ef 100644 --- a/src/simulator/ideal_gas.rs +++ b/src/simulator/ideal_gas.rs @@ -142,7 +142,7 @@ pub fn ideal_gas_volume( ) -> Volume { Volume( (mass.value() / species.molar_mass.kilograms_per_mole()) * R * temperature.kelvin() - / pressure.pascal(), + / pressure.pascals(), ) } @@ -154,7 +154,7 @@ 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()), ) } diff --git a/src/simulator/properties.rs b/src/simulator/properties.rs index 36f926b..a01c421 100644 --- a/src/simulator/properties.rs +++ b/src/simulator/properties.rs @@ -109,16 +109,16 @@ 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 } } From a28439931b8c89c5d0ad7ace4db89fb541e6df4e Mon Sep 17 00:00:00 2001 From: Philip Linden Date: Wed, 27 Nov 2024 01:33:19 -0500 Subject: [PATCH 10/10] physics: fix drag, volume, density, and buoyancy --- docs/devlog.md | 82 +++++++++++++++++++++++++++++++++++- src/app3d/scene.rs | 9 ++-- src/app3d/ui/core.rs | 2 +- src/app3d/ui/dev_tools.rs | 4 +- src/app3d/ui/monitors.rs | 67 +++++++++++------------------ src/simulator/atmosphere.rs | 58 +++++++++++++------------ src/simulator/balloon.rs | 38 +++++++++++++---- src/simulator/core.rs | 5 ++- src/simulator/forces/aero.rs | 14 ++---- src/simulator/forces/body.rs | 17 +++++--- src/simulator/forces/mod.rs | 12 ++++-- src/simulator/ideal_gas.rs | 6 +++ src/simulator/properties.rs | 16 ++++++- 13 files changed, 224 insertions(+), 106 deletions(-) diff --git a/docs/devlog.md b/docs/devlog.md index ec15902..58ca661 100644 --- a/docs/devlog.md +++ b/docs/devlog.md @@ -2,7 +2,87 @@ ## 2024-11-26 -- Added a `GasMonitor` to the UI for displaying the gas properties in real time. +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 diff --git a/src/app3d/scene.rs b/src/app3d/scene.rs index 7a07c43..647fe1c 100644 --- a/src/app3d/scene.rs +++ b/src/app3d/scene.rs @@ -7,7 +7,10 @@ pub struct ScenePlugin; impl Plugin for ScenePlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, (simple_scene, spawn_balloon)); + app.add_systems(Startup, (spawn_balloon, simple_scene)); + // app.add_systems(PostStartup, |mut commands: Commands| { + // commands.set_state(SimState::Running); + // }); } } @@ -35,7 +38,7 @@ fn simple_scene( )); } -pub fn spawn_balloon( +fn spawn_balloon( mut commands: Commands, mut meshes: ResMut>, mut materials: ResMut>, @@ -57,7 +60,7 @@ pub fn spawn_balloon( material_properties: BalloonMaterial::default(), shape: sphere, }, - gas: IdealGas::new(species), + gas: IdealGas::new(species).with_mass(Mass::new(0.01)), }, RigidBody::Dynamic, Collider::sphere(sphere.radius), diff --git a/src/app3d/ui/core.rs b/src/app3d/ui/core.rs index 65ad6de..f9ee765 100644 --- a/src/app3d/ui/core.rs +++ b/src/app3d/ui/core.rs @@ -51,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 0bee825..1243a9f 100644 --- a/src/app3d/ui/dev_tools.rs +++ b/src/app3d/ui/dev_tools.rs @@ -15,7 +15,7 @@ use bevy::{ }; use iyes_perf_ui::prelude::*; -use crate::app3d::controls::KeyBindingsConfig; +use crate::{app3d::controls::KeyBindingsConfig, simulator::SimState}; pub(super) fn plugin(app: &mut App) { // Toggle the debug overlay for UI. @@ -36,6 +36,8 @@ pub(super) fn plugin(app: &mut App) { 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_debug_ui); diff --git a/src/app3d/ui/monitors.rs b/src/app3d/ui/monitors.rs index 1d7c39b..0542daa 100644 --- a/src/app3d/ui/monitors.rs +++ b/src/app3d/ui/monitors.rs @@ -10,6 +10,7 @@ use crate::simulator::{ forces::{Buoyancy, Drag, Force, Weight}, SimState, SimulatedBody, ideal_gas::IdealGas, + balloon::Balloon, }; pub struct MonitorsPlugin; @@ -21,8 +22,6 @@ impl Plugin for MonitorsPlugin { 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::(); } } @@ -62,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(), @@ -88,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") } } @@ -120,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), } } @@ -132,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 @@ -179,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(), } } @@ -187,7 +167,7 @@ impl Default for ForceMonitor { impl PerfUiEntry for ForceMonitor { type Value = (f32, f32, f32); - type SystemParam = SRes; // FIXME: use &(dyn Force + 'static) instead + type SystemParam = SQuery<(&'static Weight, &'static Buoyancy, &'static Drag), With>; fn label(&self) -> &str { if self.label.is_empty() { @@ -217,13 +197,16 @@ impl PerfUiEntry for ForceMonitor { fn update_value( &self, - force_resource: &mut ::Item<'_, '_>, + force_resources: &mut ::Item<'_, '_>, ) -> Option { - Some(( - force_resource.weight.length(), - force_resource.buoyancy.length(), - force_resource.drag.length(), - )) + 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 @@ -277,7 +260,7 @@ impl Default for GasMonitor { impl PerfUiEntry for GasMonitor { type Value = (f32, f32, f32, f32, f32, String); - type SystemParam = SQuery<&'static IdealGas>; + type SystemParam = SQuery<(&'static Balloon, &'static IdealGas)>; fn label(&self) -> &str { if self.label.is_empty() { @@ -309,21 +292,21 @@ impl PerfUiEntry for GasMonitor { mass.push_str(" kg"); species.push_str(""); } - format!("{}\n{}\n{}\n{}\n{}\n{}", volume, pressure, temperature, density, mass, species) + format!("{}\n{}\n{}\n{}\n{}\n{}", species, volume, pressure, temperature, density, mass) } fn update_value( &self, - gas: &mut ::Item<'_, '_>, + items: &mut ::Item<'_, '_>, ) -> Option { - let instance = gas.get_single().unwrap(); + let (balloon, gas) = items.get_single().unwrap(); Some(( - instance.volume().m3(), - instance.pressure.kilopascals(), - instance.temperature.kelvin(), - instance.density.kilograms_per_cubic_meter(), - instance.mass.value(), - instance.species.name.clone(), + balloon.shape.volume(), + gas.pressure.kilopascals(), + gas.temperature.kelvin(), + gas.density.kilograms_per_cubic_meter(), + gas.mass.value(), + gas.species.name.clone(), )) } diff --git a/src/simulator/atmosphere.rs b/src/simulator/atmosphere.rs index ad94bb9..f5fd6e0 100644 --- a/src/simulator/atmosphere.rs +++ b/src/simulator/atmosphere.rs @@ -11,14 +11,29 @@ use bevy::prelude::*; use super::{ ideal_gas::{ideal_gas_density, GasSpecies}, properties::{Density, Pressure, Temperature}, - SimState, SimulatedBody, + 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); + } } } @@ -30,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. @@ -69,20 +82,13 @@ enum AtmosphereError { // max = Atmosphere::MAX_ALTITUDE // )] // OutOfBounds(f32), - OutOfBounds, + #[allow(dead_code)] + OutOfBounds(f32), } -/// 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) - }; +impl std::fmt::Display for AtmosphereError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self) } } @@ -97,7 +103,7 @@ 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(AtmosphereError::OutOfBounds) + Err(AtmosphereError::OutOfBounds(altitude)) } } @@ -126,6 +132,6 @@ fn coesa_pressure(altitude: f32) -> Result { ), )) } else { - Err(AtmosphereError::OutOfBounds) + Err(AtmosphereError::OutOfBounds(altitude)) } } diff --git a/src/simulator/balloon.rs b/src/simulator/balloon.rs index 000fe62..095ab65 100644 --- a/src/simulator/balloon.rs +++ b/src/simulator/balloon.rs @@ -4,10 +4,8 @@ use avian3d::{math::PI, prelude::Position}; use bevy::prelude::*; use super::{ - SimulatedBody, - SimulationUpdateOrder, - ideal_gas::IdealGas, - properties::sphere_radius_from_volume, + ideal_gas::IdealGas, properties::sphere_radius_from_volume, SimulatedBody, + SimulationUpdateOrder, Volume, }; pub struct BalloonPlugin; @@ -31,6 +29,21 @@ pub struct Balloon { 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)] @@ -39,6 +52,17 @@ pub struct BalloonBundle { 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(Component, Debug, Clone, PartialEq, Reflect)] pub struct BalloonMaterial { pub name: String, @@ -52,7 +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) + pub thickness: f32, // thickness of the material (m) } impl Default for BalloonMaterial { @@ -74,9 +98,7 @@ impl Default for BalloonMaterial { } } -fn update_balloon_from_gas( - mut query: Query<(&mut Balloon, &IdealGas)>, -) { +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; diff --git a/src/simulator/core.rs b/src/simulator/core.rs index 0c85f5a..924c874 100644 --- a/src/simulator/core.rs +++ b/src/simulator/core.rs @@ -48,7 +48,8 @@ impl Plugin for CorePhysicsPlugin { SimulationUpdateOrder::Forces, SimulationUpdateOrder::Last, ).chain() - .before(PhysicsSet::Prepare), + .before(PhysicsSet::Prepare) + .run_if(in_state(SimState::Running)), ); } } @@ -56,9 +57,9 @@ impl Plugin for CorePhysicsPlugin { #[derive(States, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)] pub enum SimState { #[default] + Loading, Running, Stopped, - Anomaly, } #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] diff --git a/src/simulator/forces/aero.rs b/src/simulator/forces/aero.rs index 4e0c5ca..1301d90 100644 --- a/src/simulator/forces/aero.rs +++ b/src/simulator/forces/aero.rs @@ -4,7 +4,7 @@ use avian3d::{math::PI, prelude::*}; 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; @@ -68,14 +68,13 @@ 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()), + PI * balloon.shape.diameter(), 1.17, // default drag coefficient for a sphere ); } @@ -91,11 +90,6 @@ 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() { diff --git a/src/simulator/forces/body.rs b/src/simulator/forces/body.rs index 9aec62d..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, @@ -79,6 +80,7 @@ fn update_weight_parameters( /// 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 df25625..fbd2f5d 100644 --- a/src/simulator/forces/mod.rs +++ b/src/simulator/forces/mod.rs @@ -12,7 +12,7 @@ pub use aero::Drag; #[allow(unused_imports)] pub use body::{Buoyancy, Weight}; -use super::{Atmosphere, Density, SimulatedBody, SimulationUpdateOrder, Volume}; +use super::{Atmosphere, Balloon, Density, SimulatedBody, SimulationUpdateOrder, SimState, Volume}; pub struct ForcesPlugin; impl Plugin for ForcesPlugin { @@ -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)); @@ -102,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 00764ef..cbb3e1d 100644 --- a/src/simulator/ideal_gas.rs +++ b/src/simulator/ideal_gas.rs @@ -123,6 +123,11 @@ impl IdealGas { 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); } @@ -173,5 +178,6 @@ fn update_ideal_gas_from_atmosphere( 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/properties.rs b/src/simulator/properties.rs index a01c421..bf82cf6 100644 --- a/src/simulator/properties.rs +++ b/src/simulator/properties.rs @@ -43,7 +43,7 @@ impl Plugin for CorePropertiesPlugin { } /// Temperature (K) -#[derive(Component,Debug, Default, Clone, Copy, PartialEq, Reflect)] +#[derive(Component,Debug, Clone, Copy, PartialEq, Reflect)] pub struct Temperature(pub Scalar); impl Temperature { @@ -66,6 +66,12 @@ impl Temperature { } } +impl Default for Temperature { + fn default() -> Self { + Temperature::STANDARD + } +} + impl Add for Temperature { type Output = Temperature; @@ -99,7 +105,7 @@ impl Div for Temperature { } /// Pressure (Pa) -#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Reflect)] +#[derive(Component, Debug, Clone, Copy, PartialEq, Reflect)] pub struct Pressure(pub Scalar); impl Pressure { @@ -122,6 +128,12 @@ impl Pressure { } } +impl Default for Pressure { + fn default() -> Self { + Pressure::STANDARD + } +} + impl Add for Pressure { type Output = Pressure;