From 0606b1a5efd952d2eef44eed64e4a3a1a77b5267 Mon Sep 17 00:00:00 2001 From: Al McElrath Date: Tue, 27 Jun 2023 12:32:43 -0700 Subject: [PATCH 1/4] ParticleTextureModifier serialization --- examples/billboard.rs | 2 +- examples/circle.rs | 2 +- examples/gradient.rs | 2 +- examples/serde_asset.rs | 168 ++++++++++++++++++++++++++++++++++++++++ src/asset.rs | 114 ++++++++++++++++++++++++++- src/lib.rs | 2 +- src/modifier/render.rs | 15 ++-- 7 files changed, 288 insertions(+), 17 deletions(-) create mode 100644 examples/serde_asset.rs diff --git a/examples/billboard.rs b/examples/billboard.rs index 740451a8..b675d3d9 100644 --- a/examples/billboard.rs +++ b/examples/billboard.rs @@ -86,7 +86,7 @@ fn setup( lifetime: 5_f32.into(), }) .render(ParticleTextureModifier { - texture: texture_handle, + texture: texture_handle.into(), }) .render(BillboardModifier {}) .render(ColorOverLifetimeModifier { gradient }) diff --git a/examples/circle.rs b/examples/circle.rs index c5fa9fd2..e64dce09 100644 --- a/examples/circle.rs +++ b/examples/circle.rs @@ -77,7 +77,7 @@ fn setup( lifetime: 5_f32.into(), }) .render(ParticleTextureModifier { - texture: texture_handle.clone(), + texture: texture_handle.clone().into(), }) .render(ColorOverLifetimeModifier { gradient }) .render(SizeOverLifetimeModifier { diff --git a/examples/gradient.rs b/examples/gradient.rs index dcdf5bdc..3188774f 100644 --- a/examples/gradient.rs +++ b/examples/gradient.rs @@ -87,7 +87,7 @@ fn setup( lifetime: 5_f32.into(), }) .render(ParticleTextureModifier { - texture: texture_handle.clone(), + texture: texture_handle.clone().into(), }) .render(ColorOverLifetimeModifier { gradient }), ); diff --git a/examples/serde_asset.rs b/examples/serde_asset.rs new file mode 100644 index 00000000..5887fda5 --- /dev/null +++ b/examples/serde_asset.rs @@ -0,0 +1,168 @@ +//! This example demonstrates saving and loading an EffectAsset. + +use bevy::{ + log::LogPlugin, + prelude::*, + render::{render_resource::WgpuFeatures, settings::WgpuSettings, RenderPlugin}, +}; +use bevy_inspector_egui::{bevy_egui, egui, quick::WorldInspectorPlugin}; + +use bevy_hanabi::prelude::*; + +fn main() -> Result<(), Box> { + let mut wgpu_settings = WgpuSettings::default(); + wgpu_settings + .features + .set(WgpuFeatures::VERTEX_WRITABLE_STORAGE, true); + + App::default() + .insert_resource(ClearColor(Color::DARK_GRAY)) + .add_plugins( + DefaultPlugins + .set(LogPlugin { + level: bevy::log::Level::WARN, + filter: "bevy_hanabi=warn,spawn=trace".to_string(), + }) + .set(RenderPlugin { wgpu_settings }), + ) + .add_system(bevy::window::close_on_esc) + .add_plugin(HanabiPlugin) + .add_plugin(WorldInspectorPlugin::default()) + .add_startup_system(setup) + .add_system(respawn) + .add_system(load_save_ui) + .run(); + + Ok(()) +} + +const COLOR: Vec4 = Vec4::new(0.7, 0.7, 1.0, 1.0); +const PATH: &str = "disk.effect"; + +fn setup( + mut commands: Commands, + asset_server: Res, + mut effects: ResMut>, +) { + // Spawn camera. + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(0.0, 3.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + + // Try to load the asset first. If it doesn't exist yet, create it. + let effect = match asset_server + .asset_io() + .get_metadata(std::path::Path::new(PATH)) + { + Ok(metadata) => { + assert!(metadata.is_file()); + asset_server.load(PATH) + } + Err(_) => effects.add( + EffectAsset { + name: "💾".to_owned(), + capacity: 32768, + spawner: Spawner::rate(48.0.into()), + ..Default::default() + } + .init(InitPositionSphereModifier { + center: Vec3::ZERO, + radius: 1., + dimension: ShapeDimension::Volume, + }) + .init(InitAttributeModifier { + attribute: Attribute::VELOCITY, + value: ValueOrProperty::Value((Vec3::Y * 2.).into()), + }) + .init(InitLifetimeModifier { + lifetime: 0.9.into(), + }) + .render(ParticleTextureModifier { + // Need to supply a handle and a path in order to save it later. + texture: AssetHandle::new(asset_server.load("cloud.png"), "cloud.png"), + }) + .render(SetColorModifier { + color: COLOR.into(), + }) + .render(SizeOverLifetimeModifier { + gradient: { + let mut gradient = Gradient::new(); + gradient.add_key(0.0, Vec2::splat(0.1)); + gradient.add_key(0.1, Vec2::splat(1.0)); + gradient.add_key(1.0, Vec2::splat(0.01)); + gradient + }, + }), + ), + }; + + spawn_effect(&mut commands, effect); +} + +fn spawn_effect(commands: &mut Commands, effect: Handle) -> Entity { + commands + .spawn(( + Name::new("💾"), + ParticleEffectBundle { + effect: ParticleEffect::new(effect), + ..Default::default() + }, + )) + .id() +} + +// Respawn effects when the asset changes. +fn respawn( + mut commands: Commands, + mut effect_events: EventReader>, + effects: Res>, + query: Query>, +) { + for event in effect_events.iter() { + match event { + AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { + for entity in query.iter() { + commands.entity(entity).despawn(); + let mut handle = handle.clone(); + handle.make_strong(&effects); + spawn_effect(&mut commands, handle); + } + return; + } + _ => (), + } + } +} + +fn load_save_ui( + asset_server: Res, + mut contexts: bevy_egui::EguiContexts, + effects: ResMut>, +) { + use std::io::Write; + + egui::Window::new("💾").show(contexts.ctx_mut(), |ui| { + // You can edit the asset on disk and click load to see changes. + let load = ui.button("Load"); + if load.clicked() { + // Reload the asset. + asset_server.reload_asset(PATH); + } + + // Save effect to PATH. + let save = ui.button("Save"); + if save.clicked() { + let (_handle, effect) = effects.iter().next().unwrap(); + let ron = ron::ser::to_string_pretty(&effect, Default::default()).unwrap(); + let mut file = std::fs::File::create(format!( + "{}/{}/{}", + std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_owned()), + "assets", + PATH + )) + .unwrap(); + file.write(ron.as_bytes()).unwrap(); + } + }); +} diff --git a/src/asset.rs b/src/asset.rs index 33a62864..b701f8e4 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1,5 +1,8 @@ +use std::sync::Arc; + use bevy::{ - asset::{AssetLoader, LoadContext, LoadedAsset}, + asset::{Asset, AssetLoader, AssetPath, LoadContext, LoadedAsset}, + prelude::*, reflect::{FromReflect, Reflect, TypeUuid}, utils::{BoxedFuture, HashSet}, }; @@ -8,7 +11,8 @@ use serde::{Deserialize, Serialize}; use crate::{ graph::Value, modifier::{init::InitModifier, render::RenderModifier, update::UpdateModifier}, - BoxedModifier, ParticleLayout, Property, PropertyLayout, SimulationSpace, Spawner, + BoxedModifier, ParticleLayout, ParticleTextureModifier, Property, PropertyLayout, + SimulationSpace, Spawner, }; /// Type of motion integration applied to the particles of a system. @@ -254,8 +258,27 @@ impl AssetLoader for EffectAssetLoader { load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result<(), anyhow::Error>> { Box::pin(async move { - let custom_asset = ron::de::from_bytes::(bytes)?; - load_context.set_default_asset(LoadedAsset::new(custom_asset)); + let mut effect = ron::de::from_bytes::(bytes).map_err(|e| { + // Include path in error. + anyhow::anyhow!(format!("{}:{}", load_context.path().display(), e)) + })?; + + // Load particle textures as dependent assets. + let mut asset_paths = Vec::new(); + for modifier in effect.modifiers.iter_mut() { + if let Some(ParticleTextureModifier { + texture: AssetHandle { handle, asset_path }, + }) = modifier + .as_any_mut() + .downcast_mut::() + { + // Upgrade the path. + *handle = load_context.get_handle(handle.clone()); + asset_paths.push((**asset_path).clone()); + } + } + + load_context.set_default_asset(LoadedAsset::new(effect).with_dependencies(asset_paths)); Ok(()) }) } @@ -265,6 +288,89 @@ impl AssetLoader for EffectAssetLoader { } } +/// Stores a handle and its path. Derefs to Handle. Serializes as an AssetPath. +#[derive(Debug, Reflect, FromReflect)] +#[reflect_value(Serialize, Deserialize)] +pub struct AssetHandle { + /// Handle to asset. + pub handle: Handle, + /// Path to asset. + pub asset_path: Arc>, +} + +impl AssetHandle { + /// Creates a new AssetHandle from a Handle and AssetPath. + pub fn new(handle: Handle, asset_path: impl Into>) -> Self { + let asset_path = Arc::new(asset_path.into()); + Self { handle, asset_path } + } +} + +impl Default for AssetHandle { + fn default() -> Self { + Self { + handle: Default::default(), + asset_path: Arc::new("()".into()), + } + } +} + +impl Clone for AssetHandle { + fn clone(&self) -> Self { + Self { + handle: self.handle.clone(), + asset_path: self.asset_path.clone(), + } + } +} + +impl PartialEq for AssetHandle { + fn eq(&self, other: &Self) -> bool { + self.handle == other.handle + } +} + +impl std::ops::Deref for AssetHandle { + type Target = Handle; + + fn deref(&self) -> &Self::Target { + &self.handle + } +} + +impl From> for AssetHandle { + fn from(handle: Handle) -> Self { + Self { + handle, + ..default() + } + } +} + +impl Serialize for AssetHandle { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.asset_path.serialize(serializer) + } +} + +impl<'de, T: Asset> Deserialize<'de> for AssetHandle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let asset_path = AssetPath::deserialize(deserializer)?; + let handle = Handle::::weak(asset_path.get_id().into()); + + Ok(Self { + handle, + asset_path: Arc::new(asset_path), + }) + } +} + #[cfg(test)] mod tests { use crate::*; diff --git a/src/lib.rs b/src/lib.rs index d4d26123..045f81da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,7 @@ mod test_utils; use properties::{Property, PropertyInstance}; -pub use asset::{EffectAsset, MotionIntegration, SimulationCondition}; +pub use asset::{AssetHandle, EffectAsset, MotionIntegration, SimulationCondition}; pub use attributes::*; pub use bundle::ParticleEffectBundle; pub use gradient::{Gradient, GradientKey}; diff --git a/src/modifier/render.rs b/src/modifier/render.rs index 329dff2d..8383347d 100644 --- a/src/modifier/render.rs +++ b/src/modifier/render.rs @@ -8,8 +8,8 @@ use serde::{Deserialize, Serialize}; use std::hash::{Hash, Hasher}; use crate::{ - calc_func_id, Attribute, BoxedModifier, Gradient, Modifier, ModifierContext, ShaderCode, - ToWgslString, Value, + calc_func_id, AssetHandle, Attribute, BoxedModifier, Gradient, Modifier, ModifierContext, + ShaderCode, ToWgslString, Value, }; /// Particle rendering shader code generation context. @@ -106,11 +106,8 @@ macro_rules! impl_mod_render { #[derive(Default, Debug, Clone, PartialEq, Reflect, FromReflect, Serialize, Deserialize)] pub struct ParticleTextureModifier { /// The texture image to modulate the particle color with. - #[serde(skip)] - // TODO - Clarify if Modifier needs to be serializable, or we need another on-disk - // representation... NOTE - Need to keep a strong handle here, nothing else will keep that - // texture loaded currently. - pub texture: Handle, + // NOTE - Need to keep a strong handle here, nothing else will keep that texture loaded currently. + pub texture: AssetHandle, } impl_mod_render!(ParticleTextureModifier, &[]); // TODO - should require some UV maybe? @@ -118,7 +115,7 @@ impl_mod_render!(ParticleTextureModifier, &[]); // TODO - should require some UV #[typetag::serde] impl RenderModifier for ParticleTextureModifier { fn apply(&self, context: &mut RenderContext) { - context.set_particle_texture(self.texture.clone()); + context.set_particle_texture(self.texture.handle.clone()); } } @@ -361,7 +358,7 @@ mod tests { fn mod_particle_texture() { let texture = Handle::::default(); let modifier = ParticleTextureModifier { - texture: texture.clone(), + texture: texture.clone().into(), }; let mut context = RenderContext::default(); From 7dc0aa2f115f4d871233b88ebc267ccd0231b1ed Mon Sep 17 00:00:00 2001 From: Al McElrath Date: Tue, 27 Jun 2023 15:43:01 -0700 Subject: [PATCH 2/4] write_all --- examples/serde_asset.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/serde_asset.rs b/examples/serde_asset.rs index 5887fda5..457a11dd 100644 --- a/examples/serde_asset.rs +++ b/examples/serde_asset.rs @@ -162,7 +162,7 @@ fn load_save_ui( PATH )) .unwrap(); - file.write(ron.as_bytes()).unwrap(); + file.write_all(ron.as_bytes()).unwrap(); } }); } From 5b4207ea2026b2c1c5373478b299668303f19b09 Mon Sep 17 00:00:00 2001 From: Al McElrath Date: Sun, 2 Jul 2023 13:36:25 -0700 Subject: [PATCH 3/4] test_asset_handle --- src/asset.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/asset.rs b/src/asset.rs index bf870665..774ddb30 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -633,4 +633,19 @@ mod tests { let _effect_serde: EffectAsset = ron::from_str(&s).unwrap(); // assert_eq!(effect, effect_serde); } + + #[test] + fn test_asset_handle() { + let mut app = App::new(); + app.add_plugins(MinimalPlugins); + app.add_plugin(AssetPlugin::default()); + + let asset_server = app.world.get_resource::().unwrap(); + let image_handle = asset_server.load("cloud.png"); + let asset_handle: AssetHandle = AssetHandle::new(image_handle, "cloud.png"); + let s = ron::to_string(&asset_handle).unwrap(); + let asset_handle_de: AssetHandle = ron::from_str(&s).unwrap(); + + assert_eq!(asset_handle, asset_handle_de); + } } From e65cbdbe5b8c7bb973fff81dc17c5430eb83e97a Mon Sep 17 00:00:00 2001 From: Jerome Humbert Date: Tue, 11 Jul 2023 21:54:27 +0100 Subject: [PATCH 4/4] cargo fmt --- src/asset.rs | 8 +++++--- src/modifier/render.rs | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/asset.rs b/src/asset.rs index f4f4768a..40f4256a 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use bevy::{ asset::{Asset, AssetLoader, AssetPath, Handle, LoadContext, LoadedAsset}, reflect::{Reflect, ReflectDeserialize, ReflectSerialize, TypeUuid}, - utils::{BoxedFuture, HashSet, default}, + utils::{default, BoxedFuture, HashSet}, }; use serde::{Deserialize, Serialize}; @@ -442,7 +442,8 @@ impl AssetLoader for EffectAssetLoader { /// Stores a handle and its path to enable serialization. /// -/// This type derefs to [`Handle`] for convenience, and serializes as an [`AssetPath`]. +/// This type derefs to [`Handle`] for convenience, and serializes as an +/// [`AssetPath`]. #[derive(Debug, Reflect)] #[reflect_value(Serialize, Deserialize)] pub struct AssetHandle { @@ -453,7 +454,8 @@ pub struct AssetHandle { } impl AssetHandle { - /// Create a new [`AssetHandle`] from a runtime [`Handle`] and an [`AssetPath`]. + /// Create a new [`AssetHandle`] from a runtime [`Handle`] and an + /// [`AssetPath`]. pub fn new(handle: Handle, asset_path: impl Into>) -> Self { let asset_path = Arc::new(asset_path.into()); Self { handle, asset_path } diff --git a/src/modifier/render.rs b/src/modifier/render.rs index 638fd30b..b8d16e35 100644 --- a/src/modifier/render.rs +++ b/src/modifier/render.rs @@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize}; use std::hash::Hash; use crate::{ - calc_func_id, Attribute, BoxedModifier, CpuValue, Gradient, Modifier, ModifierContext, - ShaderCode, ToWgslString, AssetHandle, + calc_func_id, AssetHandle, Attribute, BoxedModifier, CpuValue, Gradient, Modifier, + ModifierContext, ShaderCode, ToWgslString, }; /// Particle rendering shader code generation context. @@ -107,7 +107,8 @@ macro_rules! impl_mod_render { #[derive(Default, Debug, Clone, PartialEq, Reflect, Serialize, Deserialize)] pub struct ParticleTextureModifier { /// The texture image to modulate the particle color with. - // NOTE - Need to keep a strong handle here, nothing else will keep that texture loaded currently. + // NOTE - Need to keep a strong handle here, nothing else will keep that texture loaded + // currently. pub texture: AssetHandle, }