diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index b0f08bacfa557..dd9364a2b8614 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -725,6 +725,8 @@ mod tests { AssetPath, AssetPlugin, AssetServer, Assets, InvalidGenerationError, LoadState, UnapprovedPathMode, UntypedHandle, }; + #[cfg(feature = "multi_threaded")] + use alloc::collections::BTreeMap; use alloc::{ boxed::Box, format, @@ -2602,4 +2604,440 @@ mod tests { r#"(text:"abc_def",dependencies:[],embedded_dependencies:[],sub_texts:[])"# ); } + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + #[derive(Asset, TypePath, Serialize, Deserialize)] + struct FakeGltf { + gltf_nodes: BTreeMap, + } + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + struct FakeGltfLoader; + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + impl AssetLoader for FakeGltfLoader { + type Asset = FakeGltf; + type Settings = (); + type Error = std::io::Error; + + async fn load( + &self, + reader: &mut dyn Reader, + _settings: &Self::Settings, + _load_context: &mut LoadContext<'_>, + ) -> Result { + use std::io::{Error, ErrorKind}; + + let mut bytes = vec![]; + reader.read_to_end(&mut bytes).await?; + ron::de::from_bytes(&bytes) + .map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string())) + } + + fn extensions(&self) -> &[&str] { + &["gltf"] + } + } + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + #[derive(Asset, TypePath, Serialize, Deserialize)] + struct FakeBsn { + parent_bsn: Option, + nodes: BTreeMap, + } + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + // This loader loads the BSN but as an "inlined" scene. We read the original BSN and create a + // scene that holds all the data including parents. + // TODO: It would be nice if the inlining was actually done as an `AssetTransformer`, but + // `Process` currently has no way to load nested assets. + struct FakeBsnLoader; + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + impl AssetLoader for FakeBsnLoader { + type Asset = FakeBsn; + type Settings = (); + type Error = std::io::Error; + + async fn load( + &self, + reader: &mut dyn Reader, + _settings: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + use std::io::{Error, ErrorKind}; + + let mut bytes = vec![]; + reader.read_to_end(&mut bytes).await?; + let bsn: FakeBsn = ron::de::from_bytes(&bytes) + .map_err(|err| Error::new(ErrorKind::InvalidData, err.to_string()))?; + + if bsn.parent_bsn.is_none() { + return Ok(bsn); + } + + let parent_bsn = bsn.parent_bsn.unwrap(); + let parent_bsn = load_context + .loader() + .immediate() + .load(parent_bsn) + .await + .map_err(|err| Error::new(ErrorKind::InvalidData, err))?; + let mut new_bsn: FakeBsn = parent_bsn.take(); + for (name, node) in bsn.nodes { + new_bsn.nodes.insert(name, node); + } + Ok(new_bsn) + } + + fn extensions(&self) -> &[&str] { + &["bsn"] + } + } + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + #[derive(TypePath)] + struct GltfToBsn; + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + impl AssetTransformer for GltfToBsn { + type AssetInput = FakeGltf; + type AssetOutput = FakeBsn; + type Settings = (); + type Error = std::io::Error; + + async fn transform<'a>( + &'a self, + mut asset: TransformedAsset, + _settings: &'a Self::Settings, + ) -> Result, Self::Error> { + let bsn = FakeBsn { + parent_bsn: None, + // Pretend we converted all the glTF nodes into BSN's format. + nodes: core::mem::take(&mut asset.get_mut().gltf_nodes), + }; + Ok(asset.replace_asset(bsn)) + } + } + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + #[derive(TypePath)] + struct FakeBsnSaver; + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + impl AssetSaver for FakeBsnSaver { + type Asset = FakeBsn; + type Error = std::io::Error; + type OutputLoader = FakeBsnLoader; + type Settings = (); + + async fn save( + &self, + writer: &mut crate::io::Writer, + asset: crate::saver::SavedAsset<'_, Self::Asset>, + _settings: &Self::Settings, + ) -> Result<(), Self::Error> { + use std::io::{Error, ErrorKind}; + + use ron::ser::PrettyConfig; + + let ron_string = + ron::ser::to_string_pretty(asset.get(), PrettyConfig::new().new_line("\n")) + .map_err(|err| Error::new(ErrorKind::InvalidData, err))?; + + writer.write_all(ron_string.as_bytes()).await + } + } + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + #[test] + fn asset_processor_loading_can_read_processed_assets() { + use crate::transformer::IdentityAssetTransformer; + + let AppWithProcessor { + mut app, + source_dir, + processed_dir, + } = create_app_with_asset_processor(); + + // This processor loads a gltf file, converts it to BSN and then saves out the BSN. + type GltfProcessor = LoadTransformAndSave; + // This processor loads a BSN file (which "inlines" parent BSNs at load), and then saves the + // inlined BSN. + type BsnProcessor = + LoadTransformAndSave, FakeBsnSaver>; + app.register_asset_loader(FakeBsnLoader) + .register_asset_loader(FakeGltfLoader) + .register_asset_processor(GltfProcessor::new(GltfToBsn, FakeBsnSaver)) + .register_asset_processor(BsnProcessor::new( + IdentityAssetTransformer::new(), + FakeBsnSaver, + )) + .set_default_asset_processor::("gltf") + .set_default_asset_processor::("bsn"); + + let gltf_path = Path::new("abc.gltf"); + source_dir.insert_asset_text( + gltf_path, + r#"( + gltf_nodes: { + "name": "thing", + "position": "123", + } +)"#, + ); + let bsn_path = Path::new("def.bsn"); + // The bsn tries to load the gltf as a bsn. This only works if the bsn can read processed + // assets. + source_dir.insert_asset_text( + bsn_path, + r#"( + parent_bsn: Some("abc.gltf"), + nodes: { + "position": "456", + "color": "red", + }, +)"#, + ); + + // Start the app, which also starts the asset processor. + app.update(); + + // Wait for all processing to finish. + bevy_tasks::block_on( + app.world() + .resource::() + .data() + .wait_until_finished(), + ); + + let processed_bsn = processed_dir.get_asset(bsn_path).unwrap(); + let processed_bsn = str::from_utf8(processed_bsn.value()).unwrap(); + // The processed bsn should have been "inlined", so no parent and "overlaid" nodes. + assert_eq!( + processed_bsn, + r#"( + parent_bsn: None, + nodes: { + "color": "red", + "name": "thing", + "position": "456", + }, +)"# + ); + } + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + #[test] + fn asset_processor_loading_can_read_source_assets() { + let AppWithProcessor { + mut app, + source_dir, + processed_dir, + } = create_app_with_asset_processor(); + + #[derive(Serialize, Deserialize)] + struct FakeGltfxData { + // These are the file paths to the gltfs. + gltfs: Vec, + } + + #[derive(Asset, TypePath)] + struct FakeGltfx { + gltfs: Vec, + } + + #[derive(TypePath)] + struct FakeGltfxLoader; + + impl AssetLoader for FakeGltfxLoader { + type Asset = FakeGltfx; + type Error = std::io::Error; + type Settings = (); + + async fn load( + &self, + reader: &mut dyn Reader, + _settings: &Self::Settings, + load_context: &mut LoadContext<'_>, + ) -> Result { + use std::io::{Error, ErrorKind}; + + let mut buf = vec![]; + reader.read_to_end(&mut buf).await?; + + let gltfx_data: FakeGltfxData = ron::de::from_bytes(&buf) + .map_err(|err| Error::new(ErrorKind::InvalidData, err))?; + + let mut gltfs = vec![]; + for gltf in gltfx_data.gltfs.into_iter() { + // gltfx files come from "generic" software that doesn't know anything about + // Bevy, so it needs to load the source assets to make sense. + let gltf = load_context + .loader() + .immediate() + .load(gltf) + .await + .map_err(|err| Error::new(ErrorKind::InvalidData, err))?; + gltfs.push(gltf.take()); + } + + Ok(FakeGltfx { gltfs }) + } + + fn extensions(&self) -> &[&str] { + &["gltfx"] + } + } + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + #[derive(TypePath)] + struct GltfxToBsn; + + // The asset processor currently requires multi_threaded. + #[cfg(feature = "multi_threaded")] + impl AssetTransformer for GltfxToBsn { + type AssetInput = FakeGltfx; + type AssetOutput = FakeBsn; + type Settings = (); + type Error = std::io::Error; + + async fn transform<'a>( + &'a self, + mut asset: TransformedAsset, + _settings: &'a Self::Settings, + ) -> Result, Self::Error> { + let gltfx = asset.get_mut(); + + // Merge together all the gltfs from the gltfx into one big bsn. + let bsn = gltfx.gltfs.drain(..).fold( + FakeBsn { + parent_bsn: None, + nodes: Default::default(), + }, + |mut bsn, gltf| { + for (key, value) in gltf.gltf_nodes { + bsn.nodes.insert(key, value); + } + bsn + }, + ); + + Ok(asset.replace_asset(bsn)) + } + } + + // This processor loads a gltf file, converts it to BSN and then saves out the BSN. + type GltfProcessor = LoadTransformAndSave; + // This processor loads a gltfx file (including its gltf files) and converts it to BSN. + type GltfxProcessor = LoadTransformAndSave; + app.register_asset_loader(FakeGltfLoader) + .register_asset_loader(FakeGltfxLoader) + .register_asset_loader(FakeBsnLoader) + .register_asset_processor(GltfProcessor::new(GltfToBsn, FakeBsnSaver)) + .register_asset_processor(GltfxProcessor::new(GltfxToBsn, FakeBsnSaver)) + .set_default_asset_processor::("gltf") + .set_default_asset_processor::("gltfx"); + + let gltf_path_1 = Path::new("abc.gltf"); + source_dir.insert_asset_text( + gltf_path_1, + r#"( + gltf_nodes: { + "name": "thing", + "position": "123", + } +)"#, + ); + let gltf_path_2 = Path::new("def.gltf"); + source_dir.insert_asset_text( + gltf_path_2, + r#"( + gltf_nodes: { + "velocity": "456", + "color": "red", + } +)"#, + ); + + let gltfx_path = Path::new("xyz.gltfx"); + source_dir.insert_asset_text( + gltfx_path, + r#"( + gltfs: ["abc.gltf", "def.gltf"], +)"#, + ); + + // Start the app, which also starts the asset processor. + app.update(); + + // Wait for all processing to finish. + bevy_tasks::block_on( + app.world() + .resource::() + .data() + .wait_until_finished(), + ); + + // Sanity check that the two gltf files were actually processed. + let processed_gltf_1 = processed_dir.get_asset(gltf_path_1).unwrap(); + let processed_gltf_1 = str::from_utf8(processed_gltf_1.value()).unwrap(); + assert_eq!( + processed_gltf_1, + r#"( + parent_bsn: None, + nodes: { + "name": "thing", + "position": "123", + }, +)"# + ); + let processed_gltf_2 = processed_dir.get_asset(gltf_path_2).unwrap(); + let processed_gltf_2 = str::from_utf8(processed_gltf_2.value()).unwrap(); + assert_eq!( + processed_gltf_2, + r#"( + parent_bsn: None, + nodes: { + "color": "red", + "velocity": "456", + }, +)"# + ); + + // The processed gltfx should have been able to load and merge the gltfs despite them having + // been processed into bsn. + + // Blocked on https://github.com/bevyengine/bevy/issues/21269. This is the actual assertion. + // let processed_gltfx = processed_dir.get_asset(gltfx_path).unwrap(); + // let processed_gltfx = str::from_utf8(processed_gltfx.value()).unwrap(); + // assert_eq!( + // processed_gltfx, + // r#"( + // parent_bsn: None, + // nodes: { + // "color": "red", + // "name": "thing", + // "position": "123", + // "velocity": "456", + // }, + // )"# + // ); + + // This assertion exists to "prove" that this problem exists. + assert!(processed_dir.get_asset(gltfx_path).is_none()); + } }