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

add a method on AssetServer to create an asset from an existing one #1665

Closed
wants to merge 15 commits into from
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@ path = "examples/app/without_winit.rs"
name = "asset_loading"
path = "examples/asset/asset_loading.rs"

[[example]]
name = "asset_transformation"
path = "examples/asset/asset_transformation.rs"

[[example]]
name = "custom_asset"
path = "examples/asset/custom_asset.rs"
Expand Down
104 changes: 98 additions & 6 deletions crates/bevy_asset/src/asset_server.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
use crate::{
path::{AssetPath, AssetPathId, SourcePathId},
Asset, AssetIo, AssetIoError, AssetLifecycle, AssetLifecycleChannel, AssetLifecycleEvent,
AssetLoader, Assets, Handle, HandleId, HandleUntyped, LabelId, LoadContext, LoadState,
RefChange, RefChangeChannel, SourceInfo, SourceMeta,
Asset, AssetDynamic, AssetIo, AssetIoError, AssetLifecycle, AssetLifecycleChannel,
AssetLifecycleEvent, AssetLoader, Assets, Handle, HandleId, HandleUntyped, LabelId,
LoadContext, LoadState, RefChange, RefChangeChannel, SourceInfo, SourceMeta,
};
use anyhow::Result;
use bevy_ecs::system::{Res, ResMut};
use bevy_log::warn;
use bevy_log::{debug, warn};
use bevy_tasks::TaskPool;
use bevy_utils::{HashMap, Uuid};
use crossbeam_channel::TryRecvError;
Expand Down Expand Up @@ -237,7 +237,52 @@ impl AssetServer {
self.load_untyped(path).typed()
}

async fn load_async(
/// Create a new asset from another one
pub fn create_from<From: Asset, To: Asset, Func>(
&self,
from_handle: Handle<From>,
transform: Func,
) -> Handle<To>
where
Func: FnOnce(&From) -> Option<To>,
Func: Send + 'static,
{
let new_handle = HandleId::random::<To>();
let to_handle = Handle::strong(
new_handle,
self.server.asset_ref_counter.channel.sender.clone(),
);

let asset_lifecycles = self.server.asset_lifecycles.read();
if let Some(asset_lifecycle) = asset_lifecycles.get(&From::TYPE_UUID) {
asset_lifecycle.create_asset_from(
from_handle.into(),
new_handle,
To::TYPE_UUID,
Box::new(|asset: &dyn AssetDynamic| {
if let Some(transformed) = transform(
asset
.downcast_ref::<From>()
// this downcast can't fail as we know the actual types here
.expect("Error converting an asset to its type, please open an issue in Bevy GitHub repository"),
) {
Some(Box::new(transformed))
} else {
warn!(
"Error creating a new asset from {}, attempting to convert to {}",
std::any::type_name::<From>(),
std::any::type_name::<To>()
);
None
}
}),
);
}

to_handle
}

async fn load_async<'a>(
&self,
asset_path: AssetPath<'_>,
force: bool,
Expand Down Expand Up @@ -506,8 +551,9 @@ impl AssetServer {
.downcast_ref::<AssetLifecycleChannel<T>>()
.unwrap();

let mut rerun = vec![];
loop {
match channel.receiver.try_recv() {
match channel.from_asset_server.try_recv() {
Ok(AssetLifecycleEvent::Create(result)) => {
// update SourceInfo if this asset was loaded from an AssetPath
if let HandleId::AssetPathId(id) = result.id {
Expand Down Expand Up @@ -536,12 +582,58 @@ impl AssetServer {
}
assets.remove(handle_id);
}
Ok(AssetLifecycleEvent::CreateFrom {
from,
to,
to_type_uuid,
transform,
}) => {
if let Some(original) = assets.get(from) {
if let Some(new) = transform(original) {
if T::TYPE_UUID == to_type_uuid {
// New asset is of same type as original, add it to the `assets`
if let Ok(new_asset) = new.downcast::<T>() {
let _ = assets.set(to, *new_asset);
} else {
warn!(
"Failed to downcast transformed asset to {}.",
std::any::type_name::<T>()
);
}
} else {
// Converting to another asset type, use its `asset_lifecycle` to create it
asset_lifecycles
.get(&to_type_uuid)
.unwrap()
.create_asset(to, new, 0);
}
} else {
// Log as debug here, it should have already been logged as warn
debug!(
"Failed to transform asset from {}.",
std::any::type_name::<T>()
);
}
} else {
// Asset is not yet ready, retry next frame
rerun.push(AssetLifecycleEvent::CreateFrom {
from,
to,
to_type_uuid,
transform,
});
}
}
Err(TryRecvError::Empty) => {
break;
}
Err(TryRecvError::Disconnected) => panic!("AssetChannel disconnected."),
}
}

for message in rerun.into_iter() {
channel.to_system.send(message).unwrap();
}
}
}

Expand Down
53 changes: 45 additions & 8 deletions crates/bevy_asset/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ use crate::{
};
use anyhow::Result;
use bevy_ecs::system::{Res, ResMut};
use bevy_reflect::{TypeUuid, TypeUuidDynamic};
use bevy_log::warn;
use bevy_reflect::{TypeUuid, TypeUuidDynamic, Uuid};
use bevy_tasks::TaskPool;
use bevy_utils::{BoxedFuture, HashMap};
use crossbeam_channel::{Receiver, Sender};
Expand Down Expand Up @@ -158,48 +159,84 @@ pub struct AssetResult<T> {
/// A channel to send and receive [`AssetResult`]s
#[derive(Debug)]
pub struct AssetLifecycleChannel<T> {
pub sender: Sender<AssetLifecycleEvent<T>>,
pub receiver: Receiver<AssetLifecycleEvent<T>>,
pub to_system: Sender<AssetLifecycleEvent<T>>,
pub from_asset_server: Receiver<AssetLifecycleEvent<T>>,
}

pub enum AssetLifecycleEvent<T> {
Create(AssetResult<T>),
CreateFrom {
from: HandleId,
to: HandleId,
to_type_uuid: Uuid,
transform: Box<dyn (FnOnce(&T) -> Option<Box<dyn AssetDynamic>>) + Send>,
},
Free(HandleId),
}

pub trait AssetLifecycle: Downcast + Send + Sync + 'static {
fn create_asset(&self, id: HandleId, asset: Box<dyn AssetDynamic>, version: usize);
fn create_asset_from(
&self,
from: HandleId,
to: HandleId,
to_uuid: Uuid,
transform: Box<dyn (FnOnce(&dyn AssetDynamic) -> Option<Box<dyn AssetDynamic>>) + Send>,
);
fn free_asset(&self, id: HandleId);
}
impl_downcast!(AssetLifecycle);

impl<T: AssetDynamic> AssetLifecycle for AssetLifecycleChannel<T> {
fn create_asset(&self, id: HandleId, asset: Box<dyn AssetDynamic>, version: usize) {
if let Ok(asset) = asset.downcast::<T>() {
self.sender
self.to_system
.send(AssetLifecycleEvent::Create(AssetResult {
asset,
id,
version,
}))
.unwrap()
} else {
panic!(
warn!(
"Failed to downcast asset to {}.",
std::any::type_name::<T>()
);
}
}

/// Convert an asset from type `T` to type with `To::TYPE_UUID == to_uuid`.
/// It is the responsibility of the caller to ensure that input/output of `transform`
/// matches those types
fn create_asset_from(
&self,
from: HandleId,
to: HandleId,
to_type_uuid: Uuid,
transform: Box<dyn (FnOnce(&dyn AssetDynamic) -> Option<Box<dyn AssetDynamic>>) + Send>,
) {
self.to_system
.send(AssetLifecycleEvent::CreateFrom {
from,
to,
to_type_uuid,
transform: Box::new(|asset: &T| transform(asset)),
})
.unwrap();
}

fn free_asset(&self, id: HandleId) {
self.sender.send(AssetLifecycleEvent::Free(id)).unwrap();
self.to_system.send(AssetLifecycleEvent::Free(id)).unwrap();
}
}

impl<T> Default for AssetLifecycleChannel<T> {
fn default() -> Self {
let (sender, receiver) = crossbeam_channel::unbounded();
AssetLifecycleChannel { sender, receiver }
let (to_system, from_asset_server) = crossbeam_channel::unbounded();
AssetLifecycleChannel {
to_system,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When reading through this I stumbled on the name to_system several times because it seems like it must be connected to the IntoSystem but that doesn't make sense in context. I think I actually prefer the original sender and receiver, they're slightly more cryptic but they're conventional which makes them instantly recognizable.

Copy link
Member Author

@mockersf mockersf Mar 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree on that, but I guess it's a taste/habit situation. Plus I don't like having a variable named after its type.

I'm open to changing it back if others feel the same way

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your reason for making the change is good, I don't object to keeping it.

from_asset_server,
}
}
}

Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ Example | File | Description
Example | File | Description
--- | --- | ---
`asset_loading` | [`asset/asset_loading.rs`](./asset/asset_loading.rs) | Demonstrates various methods to load assets
`asset_transformation` | [`asset/asset_transformation.rs`](./asset/asset_transformation.rs) | Create new assets from an existing one, with some transformation
`custom_asset` | [`asset/custom_asset.rs`](./asset/custom_asset.rs) | Implements a custom asset loader
`custom_asset_io` | [`asset/custom_asset_io.rs`](./asset/custom_asset_io.rs) | Implements a custom asset io loader
`hot_asset_reloading` | [`asset/hot_asset_reloading.rs`](./asset/hot_asset_reloading.rs) | Demonstrates automatic reloading of assets when modified on disk
Expand Down
60 changes: 60 additions & 0 deletions examples/asset/asset_transformation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use bevy::{prelude::*, render::texture::TextureFormatPixelInfo};

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_startup_system(setup.system())
.run();
}

fn filter_pixels(filter: usize, image: &Image) -> Vec<u8> {
image
.data
.iter()
.enumerate()
.map(|(i, v)| {
if i / image.texture_descriptor.format.pixel_size() % filter == 0 {
0
} else {
*v
}
})
.collect()
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let texture_handle = asset_server.load("branding/icon.png");

// new texture with every third pixel removed
let texture_handle_1 = asset_server.create_from(texture_handle.clone(), |texture: &Image| {
Some(Image {
data: filter_pixels(3, texture),
..texture.clone()
})
});

// new texture with every second pixel removed
let texture_handle_2 = asset_server.create_from(texture_handle.clone(), |texture: &Image| {
Some(Image {
data: filter_pixels(2, texture),
..texture.clone()
})
});

commands.spawn_bundle(OrthographicCameraBundle::new_2d());

commands.spawn_bundle(SpriteBundle {
texture: texture_handle,
transform: Transform::from_xyz(-300.0, 0.0, 0.0),
..Default::default()
});
commands.spawn_bundle(SpriteBundle {
texture: texture_handle_1,
..Default::default()
});
commands.spawn_bundle(SpriteBundle {
texture: texture_handle_2,
transform: Transform::from_xyz(300.0, 0.0, 0.0),
..Default::default()
});
}