Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ParticleTextureModifier serialization #192

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/billboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
2 changes: 1 addition & 1 deletion examples/circle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion examples/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
);
Expand Down
171 changes: 171 additions & 0 deletions examples/serde_asset.rs
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>> {
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<AssetServer>,
mut effects: ResMut<Assets<EffectAsset>>,
) {
// 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<EffectAsset>) -> 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<AssetEvent<EffectAsset>>,
effects: Res<Assets<EffectAsset>>,
query: Query<Entity, With<ParticleEffect>>,
) {
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<AssetServer>,
// mut contexts: bevy_egui::EguiContexts,
// effects: ResMut<Assets<EffectAsset>>,
// ) {
// 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();
// }
// });
// }
136 changes: 130 additions & 6 deletions src/asset.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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::<EffectAsset>(bytes)?;
load_context.set_default_asset(LoadedAsset::new(custom_asset));
let mut effect = ron::de::from_bytes::<EffectAsset>(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::<ParticleTextureModifier>()
{
// 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(())
})
}
Expand All @@ -418,6 +440,93 @@ impl AssetLoader for EffectAssetLoader {
}
}

/// Stores a handle and its path to enable serialization.
///
/// This type derefs to [`Handle<T>`] for convenience, and serializes as an
/// [`AssetPath`].
#[derive(Debug, Reflect)]
#[reflect_value(Serialize, Deserialize)]
pub struct AssetHandle<T: Asset> {
/// Handle to asset at runtime.
pub handle: Handle<T>,
/// Path to the actual asset, for serialization.
pub asset_path: Arc<AssetPath<'static>>,
}

impl<T: Asset> AssetHandle<T> {
/// Create a new [`AssetHandle`] from a runtime [`Handle`] and an
/// [`AssetPath`].
pub fn new(handle: Handle<T>, asset_path: impl Into<AssetPath<'static>>) -> Self {
let asset_path = Arc::new(asset_path.into());
Self { handle, asset_path }
}
}

impl<T: Asset> Default for AssetHandle<T> {
fn default() -> Self {
Self {
handle: Default::default(),
asset_path: Arc::new("".into()),
}
}
}

impl<T: Asset> Clone for AssetHandle<T> {
fn clone(&self) -> Self {
Self {
handle: self.handle.clone(),
asset_path: self.asset_path.clone(),
}
}
}

impl<T: Asset> PartialEq for AssetHandle<T> {
fn eq(&self, other: &Self) -> bool {
self.handle == other.handle
}
}

impl<T: Asset> std::ops::Deref for AssetHandle<T> {
type Target = Handle<T>;

fn deref(&self) -> &Self::Target {
&self.handle
}
}

impl<T: Asset> From<Handle<T>> for AssetHandle<T> {
fn from(handle: Handle<T>) -> Self {
Self {
handle,
..default()
}
}
}

impl<T: Asset> Serialize for AssetHandle<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.asset_path.serialize(serializer)
}
}

impl<'de, T: Asset> Deserialize<'de> for AssetHandle<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let asset_path = AssetPath::deserialize(deserializer)?;
let handle = Handle::<T>::weak(asset_path.get_id().into());

Ok(Self {
handle,
asset_path: Arc::new(asset_path),
})
}
}

#[cfg(test)]
mod tests {
use crate::*;
Expand Down Expand Up @@ -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::<AssetServer>().unwrap();
let image_handle = asset_server.load("cloud.png");
let asset_handle: AssetHandle<Image> = AssetHandle::new(image_handle, "cloud.png");
let s = ron::to_string(&asset_handle).unwrap();
let asset_handle_de: AssetHandle<Image> = ron::from_str(&s).unwrap();

assert_eq!(asset_handle, asset_handle_de);
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
Loading
Loading