diff --git a/examples/billboard.rs b/examples/billboard.rs index 058fd397..d5dc49b0 100644 --- a/examples/billboard.rs +++ b/examples/billboard.rs @@ -90,7 +90,7 @@ fn setup( .init(init_age) .init(init_lifetime) .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 ac898f89..b6372e75 100644 --- a/examples/circle.rs +++ b/examples/circle.rs @@ -85,7 +85,7 @@ fn setup( .init(init_age) .init(init_lifetime) .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 15951c75..7e37bccb 100644 --- a/examples/gradient.rs +++ b/examples/gradient.rs @@ -92,7 +92,7 @@ fn setup( .init(init_age) .init(init_lifetime) .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..893b82c7 --- /dev/null +++ b/examples/serde_asset.rs @@ -0,0 +1,171 @@ +//! 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_systems(Update, bevy::window::close_on_esc) + .add_plugins(HanabiPlugin) + // Have to wait for update. + // .add_plugins(WorldInspectorPlugin::default()) + .add_systems(Startup, setup) + .add_systems(Update, respawn) + //.add_systems(Update, 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(_) => { + let writer = ExprWriter::new(); + + let lifetime = writer.lit(0.9).expr(); + let init_lifetime = InitAttributeModifier::new(Attribute::LIFETIME, lifetime); + + let velocity = writer.lit(Vec3::Y * 2.).expr(); + let init_velocity = InitAttributeModifier::new(Attribute::VELOCITY, velocity); + + effects.add( + EffectAsset::new(32768, Spawner::rate(48.0.into()), writer.finish()) + .with_name("💾") + .init(InitPositionSphereModifier { + center: Vec3::ZERO, + radius: 1., + dimension: ShapeDimension::Volume, + }) + .init(init_velocity) + .init(init_lifetime) + .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 + }, + screen_space_size: false, + }), + ) + } + }; + + 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_all(ron.as_bytes()).unwrap(); +// } +// }); +// } diff --git a/src/asset.rs b/src/asset.rs index 24d859f8..40f4256a 100644 --- a/src/asset.rs +++ b/src/asset.rs @@ -1,14 +1,17 @@ +use std::sync::Arc; + use bevy::{ - asset::{AssetLoader, LoadContext, LoadedAsset}, - reflect::{Reflect, TypeUuid}, - utils::{BoxedFuture, HashSet}, + asset::{Asset, AssetLoader, AssetPath, Handle, LoadContext, LoadedAsset}, + reflect::{Reflect, ReflectDeserialize, ReflectSerialize, TypeUuid}, + utils::{default, BoxedFuture, HashSet}, }; use serde::{Deserialize, Serialize}; use crate::{ graph::Value, modifier::{init::InitModifier, render::RenderModifier, update::UpdateModifier}, - BoxedModifier, Module, ParticleLayout, Property, PropertyLayout, SimulationSpace, Spawner, + BoxedModifier, Module, ParticleLayout, ParticleTextureModifier, Property, PropertyLayout, + SimulationSpace, Spawner, }; /// Type of motion integration applied to the particles of a system. @@ -407,8 +410,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(()) }) } @@ -418,6 +440,93 @@ 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`]. +#[derive(Debug, Reflect)] +#[reflect_value(Serialize, Deserialize)] +pub struct AssetHandle { + /// Handle to asset at runtime. + pub handle: Handle, + /// Path to the actual asset, for serialization. + pub asset_path: Arc>, +} + +impl AssetHandle { + /// 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 } + } +} + +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::*; @@ -524,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_plugins(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); + } } diff --git a/src/lib.rs b/src/lib.rs index 1994bfc4..f8cf9123 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,7 +160,7 @@ mod test_utils; use properties::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 e288fa06..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, + calc_func_id, AssetHandle, Attribute, BoxedModifier, CpuValue, Gradient, Modifier, + ModifierContext, ShaderCode, ToWgslString, }; /// Particle rendering shader code generation context. @@ -107,11 +107,9 @@ 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. - #[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? @@ -119,7 +117,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()); } } @@ -321,7 +319,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();