diff --git a/crates/bevy_gltf_blueprints/src/lib.rs b/crates/bevy_gltf_blueprints/src/lib.rs index 366f05f..78642cd 100644 --- a/crates/bevy_gltf_blueprints/src/lib.rs +++ b/crates/bevy_gltf_blueprints/src/lib.rs @@ -120,6 +120,9 @@ impl Plugin for BlueprintsPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() + .register_type::>() + .register_type::>>() .insert_resource(BluePrintsConfig { format: self.format, library_folder: self.library_folder.clone(), @@ -140,11 +143,24 @@ impl Plugin for BlueprintsPlugin { .add_systems( Update, ( - spawn_from_blueprints, - compute_scene_aabbs.run_if(aabbs_enabled), - apply_deferred.run_if(aabbs_enabled), + ( + prepare_blueprints, + check_for_loaded, + spawn_from_blueprints, + apply_deferred, + ) + .chain(), + (compute_scene_aabbs, apply_deferred) + .chain() + .run_if(aabbs_enabled), apply_deferred, - materials_inject.run_if(materials_library_enabled), + ( + materials_inject, + check_for_material_loaded, + materials_inject2, + ) + .chain() + .run_if(materials_library_enabled), ) .chain() .in_set(GltfBlueprintsSet::Spawn), diff --git a/crates/bevy_gltf_blueprints/src/materials.rs b/crates/bevy_gltf_blueprints/src/materials.rs index 5f0140a..030ee90 100644 --- a/crates/bevy_gltf_blueprints/src/materials.rs +++ b/crates/bevy_gltf_blueprints/src/materials.rs @@ -4,6 +4,7 @@ use bevy::{ asset::{AssetServer, Assets, Handle}, ecs::{ component::Component, + entity::Entity, query::{Added, With}, reflect::ReflectComponent, system::{Commands, Query, Res, ResMut}, @@ -16,7 +17,7 @@ use bevy::{ render::mesh::Mesh, }; -use crate::BluePrintsConfig; +use crate::{AssetLoadTracker, AssetsToLoad, BluePrintsConfig}; #[derive(Component, Reflect, Default, Debug)] #[reflect(Component)] @@ -26,10 +27,113 @@ pub struct MaterialInfo { pub source: String, } +/// flag component +#[derive(Component)] +pub(crate) struct BlueprintMaterialAssetsLoaded; +/// flag component +#[derive(Component)] +pub(crate) struct BlueprintMaterialAssetsNotLoaded; + /// system that injects / replaces materials from material library pub(crate) fn materials_inject( + blueprints_config: ResMut, + material_infos: Query<(Entity, &MaterialInfo), Added>, + asset_server: Res, + mut commands: Commands, +) { + for (entity, material_info) in material_infos.iter() { + let model_file_name = format!( + "{}_materials_library.{}", + &material_info.source, &blueprints_config.format + ); + let materials_path = Path::new(&blueprints_config.material_library_folder) + .join(Path::new(model_file_name.as_str())); + let material_name = &material_info.name; + let material_full_path = materials_path.to_str().unwrap().to_string() + "#" + material_name; // TODO: yikes, cleanup + + if blueprints_config + .material_library_cache + .contains_key(&material_full_path) + { + debug!("material is cached, retrieving"); + blueprints_config + .material_library_cache + .get(&material_full_path) + .expect("we should have the material available"); + commands + .entity(entity) + .insert(BlueprintMaterialAssetsLoaded); + } else { + let material_file_handle: Handle = asset_server.load(materials_path.clone()); + let material_file_id = material_file_handle.id(); + let asset_infos: Vec> = vec![ + AssetLoadTracker { + name: material_full_path, + id: material_file_id, + loaded: false, + handle: material_file_handle.clone(), + } + ]; + + commands + .entity(entity) + .insert(AssetsToLoad { + all_loaded: false, + asset_infos, + ..Default::default() + }) + .insert(BlueprintMaterialAssetsNotLoaded); + /**/ + } + } +} + +// TODO, merge with check_for_loaded, make generic ? +pub(crate) fn check_for_material_loaded( + mut blueprint_assets_to_load: Query< + (Entity, &mut AssetsToLoad), + With, + >, + asset_server: Res, + mut commands: Commands, +) { + for (entity, mut assets_to_load) in blueprint_assets_to_load.iter_mut() { + let mut all_loaded = true; + let mut loaded_amount = 0; + let total = assets_to_load.asset_infos.len(); + for tracker in assets_to_load.asset_infos.iter_mut() { + let asset_id = tracker.id; + let loaded = asset_server.is_loaded_with_dependencies(asset_id); + tracker.loaded = loaded; + if loaded { + loaded_amount += 1; + } else { + all_loaded = false; + } + } + let progress: f32 = loaded_amount as f32 / total as f32; + assets_to_load.progress = progress; + + if all_loaded { + assets_to_load.all_loaded = true; + commands + .entity(entity) + .insert(BlueprintMaterialAssetsLoaded) + .remove::(); + } + } +} + +/// system that injects / replaces materials from material library +pub(crate) fn materials_inject2( mut blueprints_config: ResMut, - material_infos: Query<(&MaterialInfo, &Children), Added>, + material_infos: Query< + (&MaterialInfo, &Children), + ( + Added, + With, + ), + >, with_materials_and_meshes: Query< (), ( @@ -38,9 +142,9 @@ pub(crate) fn materials_inject( With>, ), >, - models: Res>, - + assets_gltf: Res>, asset_server: Res, + mut commands: Commands, ) { for (material_info, children) in material_infos.iter() { @@ -66,9 +170,9 @@ pub(crate) fn materials_inject( .expect("we should have the material available"); material_found = Some(material); } else { - let my_gltf: Handle = asset_server.load(materials_path.clone()); - let mat_gltf = models - .get(my_gltf.id()) + let model_handle: Handle = asset_server.load(materials_path.clone()); // FIXME: kinda weird now + let mat_gltf = assets_gltf + .get(model_handle.id()) .expect("material should have been preloaded"); if mat_gltf.named_materials.contains_key(material_name) { let material = mat_gltf diff --git a/crates/bevy_gltf_blueprints/src/spawn_from_blueprints.rs b/crates/bevy_gltf_blueprints/src/spawn_from_blueprints.rs index 3701fa5..cf679b7 100644 --- a/crates/bevy_gltf_blueprints/src/spawn_from_blueprints.rs +++ b/crates/bevy_gltf_blueprints/src/spawn_from_blueprints.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use bevy::{gltf::Gltf, prelude::*}; +use bevy::{gltf::Gltf, prelude::*, utils::HashMap}; use crate::{Animations, BluePrintsConfig}; @@ -46,8 +46,144 @@ pub struct AddToGameWorld; /// helper component, just to transfer child data pub(crate) struct OriginalChildren(pub Vec); -/// main spawning functions, +#[derive(Component, Reflect, Default, Debug)] +#[reflect(Component)] +pub struct BlueprintsList(pub HashMap>); + +#[derive(Default, Debug)] +pub(crate) struct AssetLoadTracker { + #[allow(dead_code)] + pub name: String, + pub id: AssetId, + pub loaded: bool, + #[allow(dead_code)] + pub handle: Handle, +} +#[derive(Component, Debug)] +pub(crate) struct AssetsToLoad { + pub all_loaded: bool, + pub asset_infos: Vec>, + pub progress: f32, +} +impl Default for AssetsToLoad { + fn default() -> Self { + Self { all_loaded: Default::default(), asset_infos: Default::default(), progress: Default::default() } + } +} + +/// flag component +#[derive(Component)] +pub(crate) struct BlueprintAssetsLoaded; +/// flag component +#[derive(Component)] +pub(crate) struct BlueprintAssetsNotLoaded; + +/// spawning prepare function, /// * also takes into account the already exisiting "override" components, ie "override components" > components from blueprint +pub(crate) fn prepare_blueprints( + spawn_placeholders: Query< + ( + Entity, + &BlueprintName, + Option<&Parent>, + Option<&Library>, + Option<&Name>, + Option<&BlueprintsList>, + ), + (Added, Added, Without), + >, + + mut commands: Commands, + asset_server: Res, + blueprints_config: Res, +) { + for (entity, blupeprint_name, original_parent, library_override, name, blueprints_list) in + spawn_placeholders.iter() + { + debug!( + "requesting to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}", + blupeprint_name.0, name, entity, original_parent + ); + + // println!("main model path {:?}", model_path); + if blueprints_list.is_some() { + let blueprints_list = blueprints_list.unwrap(); + // println!("blueprints list {:?}", blueprints_list.0.keys()); + let mut asset_infos: Vec> = vec![]; + let library_path = + library_override.map_or_else(|| &blueprints_config.library_folder, |l| &l.0); + for (blueprint_name, _) in blueprints_list.0.iter() { + let model_file_name = format!("{}.{}", &blueprint_name, &blueprints_config.format); + let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str())); + + let model_handle: Handle = asset_server.load(model_path.clone()); + let model_id = model_handle.id(); + let loaded = asset_server.is_loaded_with_dependencies(model_id); + if !loaded { + asset_infos.push(AssetLoadTracker { + name: model_path.to_string_lossy().into(), + id: model_id, + loaded: false, + handle: model_handle.clone(), + }); + } + } + // if not all assets are already loaded, inject a component to signal that we need them to be loaded + if !asset_infos.is_empty() { + commands + .entity(entity) + .insert(AssetsToLoad { + all_loaded: false, + asset_infos, + ..Default::default() + }) + .insert(BlueprintAssetsNotLoaded); + } else { + commands.entity(entity).insert(BlueprintAssetsLoaded); + } + } else { + // in case there are no blueprintsList + commands.entity(entity).insert(BlueprintAssetsLoaded); + } + } +} + +pub(crate) fn check_for_loaded( + mut blueprint_assets_to_load: Query< + (Entity, &mut AssetsToLoad), + With, + >, + asset_server: Res, + mut commands: Commands, +) { + for (entity, mut assets_to_load) in blueprint_assets_to_load.iter_mut() { + let mut all_loaded = true; + let mut loaded_amount = 0; + let total = assets_to_load.asset_infos.len(); + for tracker in assets_to_load.asset_infos.iter_mut() { + let asset_id = tracker.id; + let loaded = asset_server.is_loaded_with_dependencies(asset_id); + tracker.loaded = loaded; + if loaded { + loaded_amount += 1; + } else { + all_loaded = false; + } + } + let progress: f32 = loaded_amount as f32 / total as f32; + // println!("progress: {}",progress); + assets_to_load.progress = progress; + + if all_loaded { + assets_to_load.all_loaded = true; + commands + .entity(entity) + .insert(BlueprintAssetsLoaded) + .remove::(); + } + } +} + pub(crate) fn spawn_from_blueprints( spawn_placeholders: Query< ( @@ -59,7 +195,11 @@ pub(crate) fn spawn_from_blueprints( Option<&AddToGameWorld>, Option<&Name>, ), - (Added, Added, Without), + ( + With, + Added, + Without, + ), >, mut commands: Commands, @@ -81,18 +221,11 @@ pub(crate) fn spawn_from_blueprints( name, ) in spawn_placeholders.iter() { - debug!( - "need to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}", + info!( + "attempting to spawn {:?} for entity {:?}, id: {:?}, parent:{:?}", blupeprint_name.0, name, entity, original_parent ); - let mut original_children: Vec = vec![]; - if let Ok(c) = children.get(entity) { - for child in c.iter() { - original_children.push(*child); - } - } - let what = &blupeprint_name.0; let model_file_name = format!("{}.{}", &what, &blueprints_config.format); @@ -101,8 +234,8 @@ pub(crate) fn spawn_from_blueprints( library_override.map_or_else(|| &blueprints_config.library_folder, |l| &l.0); let model_path = Path::new(&library_path).join(Path::new(model_file_name.as_str())); - debug!("attempting to spawn {:?}", model_path); - let model_handle: Handle = asset_server.load(model_path); + // info!("attempting to spawn {:?}", model_path); + let model_handle: Handle = asset_server.load(model_path); // FIXME: kinda weird now let gltf = assets_gltf .get(&model_handle) @@ -123,6 +256,12 @@ pub(crate) fn spawn_from_blueprints( transforms = *transform.unwrap(); } + let mut original_children: Vec = vec![]; + if let Ok(c) = children.get(entity) { + for child in c.iter() { + original_children.push(*child); + } + } commands.entity(entity).insert(( SceneBundle { scene: scene.clone(), diff --git a/crates/bevy_gltf_blueprints/src/spawn_post_process.rs b/crates/bevy_gltf_blueprints/src/spawn_post_process.rs index 2caa625..b53f4d7 100644 --- a/crates/bevy_gltf_blueprints/src/spawn_post_process.rs +++ b/crates/bevy_gltf_blueprints/src/spawn_post_process.rs @@ -1,12 +1,16 @@ use std::any::TypeId; +use bevy::gltf::Gltf; use bevy::prelude::*; use bevy::scene::SceneInstance; // use bevy::utils::hashbrown::HashSet; use super::{AnimationPlayerLink, Animations}; use super::{SpawnHere, Spawned}; -use crate::{CopyComponents, InBlueprint, NoInBlueprint, OriginalChildren}; +use crate::{ + AssetsToLoad, BlueprintAssetsLoaded, CopyComponents, InBlueprint, NoInBlueprint, + OriginalChildren, +}; /// this system is in charge of doing any necessary post processing after a blueprint scene has been spawned /// - it removes one level of useless nesting @@ -89,6 +93,8 @@ pub(crate) fn spawned_blueprint_post_process( commands.entity(original).remove::(); commands.entity(original).remove::(); commands.entity(original).remove::>(); + commands.entity(original).remove::>(); // also clear the sub assets tracker to free up handles, perhaps just freeing up the handles and leave the rest would be better ? + commands.entity(original).remove::(); commands.entity(root_entity).despawn_recursive(); } } diff --git a/examples/bevy_gltf_blueprints/animation/src/game/in_game.rs b/examples/bevy_gltf_blueprints/animation/src/game/in_game.rs index fcf711b..454ce27 100644 --- a/examples/bevy_gltf_blueprints/animation/src/game/in_game.rs +++ b/examples/bevy_gltf_blueprints/animation/src/game/in_game.rs @@ -30,7 +30,7 @@ pub fn setup_game( SceneBundle { // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene scene: models - .get(game_assets.world.id()) + .get(game_assets.world.clone().unwrap().id()) .expect("main level should have been loaded") .scenes[0] .clone(), diff --git a/examples/bevy_gltf_blueprints/basic/src/game/in_game.rs b/examples/bevy_gltf_blueprints/basic/src/game/in_game.rs index 3af11d5..231137b 100644 --- a/examples/bevy_gltf_blueprints/basic/src/game/in_game.rs +++ b/examples/bevy_gltf_blueprints/basic/src/game/in_game.rs @@ -20,7 +20,7 @@ pub fn setup_game( SceneBundle { // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene scene: models - .get(game_assets.world.id()) + .get(game_assets.world.clone().unwrap().id()) .expect("main level should have been loaded") .scenes[0] .clone(), diff --git a/examples/bevy_gltf_blueprints/basic_xpbd_physics/src/game/in_game.rs b/examples/bevy_gltf_blueprints/basic_xpbd_physics/src/game/in_game.rs index 3fa60da..6853655 100644 --- a/examples/bevy_gltf_blueprints/basic_xpbd_physics/src/game/in_game.rs +++ b/examples/bevy_gltf_blueprints/basic_xpbd_physics/src/game/in_game.rs @@ -20,7 +20,7 @@ pub fn setup_game( SceneBundle { // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene scene: models - .get(game_assets.world.id()) + .get(game_assets.world.clone().unwrap().id()) .expect("main level should have been loaded") .scenes[0] .clone(), diff --git a/examples/bevy_gltf_blueprints/materials/src/game/in_game.rs b/examples/bevy_gltf_blueprints/materials/src/game/in_game.rs index e5fc8b8..6bdf1d9 100644 --- a/examples/bevy_gltf_blueprints/materials/src/game/in_game.rs +++ b/examples/bevy_gltf_blueprints/materials/src/game/in_game.rs @@ -21,7 +21,7 @@ pub fn setup_game( SceneBundle { // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene scene: models - .get(game_assets.world.id()) + .get(game_assets.world.clone().unwrap().id()) .expect("main level should have been loaded") .scenes[0] .clone(), diff --git a/examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/src/game/in_game.rs b/examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/src/game/in_game.rs index 75be98a..11ee36d 100644 --- a/examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/src/game/in_game.rs +++ b/examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/src/game/in_game.rs @@ -21,7 +21,7 @@ pub fn setup_game( SceneBundle { // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene scene: models - .get(game_assets.world.id()) + .get(game_assets.world.clone().unwrap().id()) .expect("main level should have been loaded") .scenes[0] .clone(), diff --git a/examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/src/game/level_transitions.rs b/examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/src/game/level_transitions.rs index 41d5c90..4dcb69c 100644 --- a/examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/src/game/level_transitions.rs +++ b/examples/bevy_gltf_blueprints/multiple_levels_multiple_blendfiles/src/game/level_transitions.rs @@ -76,7 +76,7 @@ pub fn trigger_level_transition( } else if target_level == "Level2" { level = game_assets.level2.clone().unwrap(); } else { - level = game_assets.world.clone(); + level = game_assets.world.clone().unwrap(); } info!("spawning new level"); commands.spawn(( diff --git a/examples/bevy_registry_export/basic/src/game/in_game.rs b/examples/bevy_registry_export/basic/src/game/in_game.rs index 75be98a..11ee36d 100644 --- a/examples/bevy_registry_export/basic/src/game/in_game.rs +++ b/examples/bevy_registry_export/basic/src/game/in_game.rs @@ -21,7 +21,7 @@ pub fn setup_game( SceneBundle { // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene scene: models - .get(game_assets.world.id()) + .get(game_assets.world.clone().unwrap().id()) .expect("main level should have been loaded") .scenes[0] .clone(), diff --git a/examples/common/src/assets/assets_game.rs b/examples/common/src/assets/assets_game.rs index 988e964..5bb5986 100644 --- a/examples/common/src/assets/assets_game.rs +++ b/examples/common/src/assets/assets_game.rs @@ -5,8 +5,8 @@ use bevy_asset_loader::prelude::*; #[derive(AssetCollection, Resource)] pub struct GameAssets { - #[asset(key = "world")] - pub world: Handle, + #[asset(key = "world", optional)] + pub world: Option>, #[asset(key = "world_dynamic", optional)] pub world_dynamic: Option>, @@ -16,8 +16,8 @@ pub struct GameAssets { #[asset(key = "level2", optional)] pub level2: Option>, - #[asset(key = "models", collection(typed, mapped))] - pub models: HashMap>, + #[asset(key = "models", collection(typed, mapped), optional)] + pub models: Option>>, #[asset(key = "materials", collection(typed, mapped), optional)] pub materials: Option>>, diff --git a/testing/bevy_example/assets/assets_game.assets.ron b/testing/bevy_example/assets/assets_game.assets.ron index 5b1e969..40825c0 100644 --- a/testing/bevy_example/assets/assets_game.assets.ron +++ b/testing/bevy_example/assets/assets_game.assets.ron @@ -1,6 +1,6 @@ ({ - "world":File (path: "models/World.glb"), + /*"world":File (path: "models/World.glb"), "models": Folder ( path: "models/library", - ), + ),*/ }) \ No newline at end of file diff --git a/testing/bevy_example/assets/registry.json b/testing/bevy_example/assets/registry.json index 65dea5f..33887c3 100644 --- a/testing/bevy_example/assets/registry.json +++ b/testing/bevy_example/assets/registry.json @@ -3576,6 +3576,22 @@ "type": "array", "typeInfo": "TupleStruct" }, + "bevy_gltf_blueprints::spawn_from_blueprints::BlueprintsList": { + "isComponent": true, + "isResource": false, + "items": false, + "prefixItems": [ + { + "type": { + "$ref": "#/$defs/bevy_utils::hashbrown::HashMap, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>" + } + } + ], + "short_name": "BlueprintsList", + "title": "bevy_gltf_blueprints::spawn_from_blueprints::BlueprintsList", + "type": "array", + "typeInfo": "TupleStruct" + }, "bevy_gltf_blueprints::spawn_from_blueprints::SpawnHere": { "additionalProperties": false, "isComponent": true, @@ -10834,6 +10850,19 @@ "type": "object", "typeInfo": "Value" }, + "bevy_utils::hashbrown::HashMap, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>": { + "additionalProperties": { + "type": { + "$ref": "#/$defs/alloc::vec::Vec" + } + }, + "isComponent": false, + "isResource": false, + "short_name": "HashMap, DefaultHashBuilder>", + "title": "bevy_utils::hashbrown::HashMap, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>", + "type": "object", + "typeInfo": "Map" + }, "bevy_utils::smallvec::SmallVec<[bevy_ecs::entity::Entity; 8]>": { "isComponent": false, "isResource": false, diff --git a/testing/bevy_example/src/core/mod.rs b/testing/bevy_example/src/core/mod.rs index 1ef7f3e..339ea40 100644 --- a/testing/bevy_example/src/core/mod.rs +++ b/testing/bevy_example/src/core/mod.rs @@ -11,6 +11,7 @@ impl Plugin for CorePlugin { legacy_mode: false, library_folder: "models/library".into(), format: GltfFormat::GLB, + material_library: true, aabbs: true, ..Default::default() }, diff --git a/testing/bevy_example/src/game/in_game.rs b/testing/bevy_example/src/game/in_game.rs index 75be98a..4873f4e 100644 --- a/testing/bevy_example/src/game/in_game.rs +++ b/testing/bevy_example/src/game/in_game.rs @@ -1,37 +1,27 @@ use bevy::prelude::*; -use bevy_gltf_blueprints::{BluePrintBundle, BlueprintName, GameWorldTag}; -use bevy_gltf_worlflow_examples_common_rapier::{assets::GameAssets, GameState, InAppRunning}; +use bevy_gltf_blueprints::{ + BluePrintBundle, BlueprintName, GameWorldTag, +}; +use bevy_gltf_worlflow_examples_common_rapier::{GameState, InAppRunning}; use bevy_rapier3d::prelude::Velocity; use rand::Rng; pub fn setup_game( mut commands: Commands, - game_assets: Res, - models: Res>, + asset_server: Res, mut next_game_state: ResMut>, ) { - commands.insert_resource(AmbientLight { - color: Color::WHITE, - brightness: 0.2, - }); // here we actually spawn our game world/level - commands.spawn(( SceneBundle { - // note: because of this issue https://github.com/bevyengine/bevy/issues/10436, "world" is now a gltf file instead of a scene - scene: models - .get(game_assets.world.id()) - .expect("main level should have been loaded") - .scenes[0] - .clone(), + scene: asset_server.load("models/World.glb#Scene0"), ..default() }, bevy::prelude::Name::from("world"), GameWorldTag, InAppRunning, )); - next_game_state.set(GameState::InGame) } diff --git a/testing/bevy_example/src/game/mod.rs b/testing/bevy_example/src/game/mod.rs index 99b9b3f..2cd7144 100644 --- a/testing/bevy_example/src/game/mod.rs +++ b/testing/bevy_example/src/game/mod.rs @@ -97,9 +97,10 @@ impl Plugin for GamePlugin { fn build(&self, app: &mut App) { app.add_systems(Update, (spawn_test).run_if(in_state(GameState::InGame))) .add_systems(Update, validate_export) - .add_systems(Update, generate_screenshot.run_if(on_timer(Duration::from_secs_f32(0.2)))) // TODO: run once .add_systems(OnEnter(AppState::MenuRunning), start_game) .add_systems(OnEnter(AppState::AppRunning), setup_game) + + .add_systems(Update, generate_screenshot.run_if(on_timer(Duration::from_secs_f32(0.2)))) // TODO: run once .add_systems( Update, exit_game.run_if(on_timer(Duration::from_secs_f32(0.5))), diff --git a/tools/gltf_auto_export/auto_export/export_main_scenes.py b/tools/gltf_auto_export/auto_export/export_main_scenes.py index 8bdb018..25cfd37 100644 --- a/tools/gltf_auto_export/auto_export/export_main_scenes.py +++ b/tools/gltf_auto_export/auto_export/export_main_scenes.py @@ -4,7 +4,7 @@ import bpy from ..helpers.generate_and_export import generate_and_export from .export_gltf import (generate_gltf_export_preferences, export_gltf) from ..modules.bevy_dynamic import is_object_dynamic, is_object_static -from ..helpers.helpers_scenes import clear_hollow_scene, copy_hollowed_collection_into +from ..helpers.helpers_scenes import clear_hollow_scene, copy_hollowed_collection_into, inject_blueprints_list_into_main_scene # export all main scenes @@ -29,6 +29,8 @@ def export_main_scene(scene, folder_path, addon_prefs, library_collections): } if export_blueprints : + inject_blueprints_list_into_main_scene(scene) + if export_separate_dynamic_and_static_objects: #print("SPLIT STATIC AND DYNAMIC") # first export static objects diff --git a/tools/gltf_auto_export/helpers/helpers_collections.py b/tools/gltf_auto_export/helpers/helpers_collections.py index edb3214..219add2 100644 --- a/tools/gltf_auto_export/helpers/helpers_collections.py +++ b/tools/gltf_auto_export/helpers/helpers_collections.py @@ -36,16 +36,23 @@ def get_marked_collections(scene, addon_prefs): return (collection_names, marked_collections) # gets all collections within collections that might also be relevant -def get_sub_collections(collections, parent, children_per_collection): +def get_sub_collections(collections, parent=None, children_per_collection=None): + if parent == None: + parent = CollectionNode() + if children_per_collection == None: + children_per_collection = {} + collection_names = set() used_collections = [] for root_collection in collections: - node = Node(name=root_collection.name, parent=parent) + #print("collections", collections) + node = CollectionNode(name=root_collection.name, parent=parent) parent.children.append(node) #print("root collection", root_collection.name) for collection in traverse_tree(root_collection): # TODO: filter out COLLECTIONS that have the flatten flag (unlike the flatten flag on colleciton instances themselves) + #print("sub", collection) node_name = collection.name children_per_collection[node_name] = [] #print(" scanning", collection.name) @@ -53,12 +60,15 @@ def get_sub_collections(collections, parent, children_per_collection): #print("FLATTEN", object.name, 'Flatten' in object) if object.instance_type == 'COLLECTION' : # and not 'Flatten' in object: collection_name = object.instance_collection.name + #print("sub obj", collection_name) + # FIXME: not sure: + children_per_collection[node_name].append(collection_name) + (sub_names, sub_collections) = get_sub_collections([object.instance_collection], node, children_per_collection) if len(list(sub_names)) > 0: children_per_collection[node_name] += (list(sub_names)) #print(" found sub collection in use", object.name, object.instance_collection) - if not collection_name in collection_names: collection_names.add(collection_name) used_collections.append(object.instance_collection) @@ -77,7 +87,7 @@ def flatten_collection_tree(node, children_per_collection): children_per_collection[node.name] = list(set( children_per_collection[node.name])) -class Node : +class CollectionNode : def __init__(self, name="", parent=None): self.name = name self.children = [] @@ -93,7 +103,7 @@ def get_exportable_collections(main_scenes, library_scenes, addon_prefs): all_collections = [] all_collection_names = [] - root_node = Node() + root_node = CollectionNode() root_node.name = "root" children_per_collection = {} diff --git a/tools/gltf_auto_export/helpers/helpers_scenes.py b/tools/gltf_auto_export/helpers/helpers_scenes.py index e5b4dfb..6275231 100644 --- a/tools/gltf_auto_export/helpers/helpers_scenes.py +++ b/tools/gltf_auto_export/helpers/helpers_scenes.py @@ -1,5 +1,6 @@ +import json import bpy -from .helpers_collections import (set_active_collection) +from .helpers_collections import (CollectionNode, get_sub_collections, get_used_collections, set_active_collection) from .object_makers import (make_empty) @@ -72,6 +73,13 @@ def copy_hollowed_collection_into(source_collection, destination_collection, par """we inject the collection/blueprint name, as a component called 'BlueprintName', but we only do this in the empty, not the original object""" empty_obj['BlueprintName'] = '"'+collection_name+'"' if legacy_mode else '("'+collection_name+'")' empty_obj['SpawnHere'] = '()' + # we also inject a list of all sub blueprints, so that the bevy side can preload them + root_node = CollectionNode() + root_node.name = "root" + children_per_collection = {} + get_sub_collections([object.instance_collection], root_node, children_per_collection) + empty_obj["BlueprintsList"] = f"({json.dumps(dict(children_per_collection))})" + #empty_obj["Assets"] = {"Animations": [], "Materials": [], "Models":[], "Textures":[], "Audio":[], "Other":[]} # we copy custom properties over from our original object to our empty for component_name, component_value in object.items(): @@ -149,3 +157,35 @@ def get_scenes(addon_prefs): return [level_scene_names, level_scenes, library_scene_names, library_scenes] + + +def inject_blueprints_list_into_main_scene(scene): + print("injecting assets/blueprints data into scene") + root_collection = scene.collection + assets_list = None + for object in scene.objects: + if object.name == "assets_list"+scene.name: + assets_list = object + break + + if assets_list is None: + assets_list = make_empty('assets_list_'+scene.name+"_components", [0,0,0], [0,0,0], [0,0,0], root_collection) + + # find all blueprints used in a scene + # TODO: export a tree rather than a flat list ? because you could have potential clashing items in flat lists (amongst other issues) + (collection_names, collections) = get_used_collections(scene) + root_node = CollectionNode() + root_node.name = "root" + children_per_collection = {} + + #print("collection_names", collection_names, "collections", collections) + (bla, bli ) = get_sub_collections(collections, root_node, children_per_collection) + # what about marked assets ? + # what about audio assets ? + # what about materials ? + # object['MaterialInfo'] = '(name: "'+material.name+'", source: "'+current_project_name + '")' + + #assets_list["blueprints_direct"] = list(collection_names) + assets_list["BlueprintsList"] = f"({json.dumps(dict(children_per_collection))})" + #assets_list["Materials"]= '()' + # print("assets list", assets_list["BlueprintsList"], children_per_collection) diff --git a/tools/gltf_auto_export/tests/test_basic.py b/tools/gltf_auto_export/tests/test_basic.py index 0ebe949..7d01954 100644 --- a/tools/gltf_auto_export/tests/test_basic.py +++ b/tools/gltf_auto_export/tests/test_basic.py @@ -19,6 +19,8 @@ def setup_data(request): def finalizer(): print("\nPerforming teardown...") + get_orphan_data() + if os.path.exists(models_path): shutil.rmtree(models_path) @@ -33,9 +35,14 @@ def setup_data(request): return None + +def get_orphan_data(): + orphan_meshes = [m.name for m in bpy.data.meshes if m.users == 0] + # print("orphan meshes before", orphan_meshes) + def test_export_do_not_export_blueprints(setup_data): auto_export_operator = bpy.ops.export_scenes.auto_gltf - + # first, configure things # we use the global settings for that export_props = { @@ -57,7 +64,6 @@ def test_export_do_not_export_blueprints(setup_data): def test_export_custom_blueprints_path(setup_data): auto_export_operator = bpy.ops.export_scenes.auto_gltf - # first, configure things # we use the global settings for that export_props = { @@ -210,3 +216,27 @@ def test_export_separate_dynamic_and_static_objects(setup_data): assert os.path.exists(os.path.join(setup_data["models_path"], "World.glb")) == True assert os.path.exists(os.path.join(setup_data["models_path"], "World_dynamic.glb")) == True + + +def test_export_should_not_generate_orphan_data(setup_data): + auto_export_operator = bpy.ops.export_scenes.auto_gltf + + # first, configure things + # we use the global settings for that + export_props = { + "main_scene_names" : ['World'], + "library_scene_names": ['Library'] + } + stored_settings = bpy.data.texts[".gltf_auto_export_settings"] if ".gltf_auto_export_settings" in bpy.data.texts else bpy.data.texts.new(".gltf_auto_export_settings") + stored_settings.clear() + stored_settings.write(json.dumps(export_props)) + + auto_export_operator( + direct_mode=True, + export_output_folder="./models", + export_scene_settings=True, + export_blueprints=False, + ) + assert os.path.exists(os.path.join(setup_data["models_path"], "World.glb")) == True + assert os.path.exists(os.path.join(setup_data["models_path"], "library", "Blueprint1.glb")) == False + diff --git a/tools/gltf_auto_export/tests/test_bevy_integration.py b/tools/gltf_auto_export/tests/test_bevy_integration.py index afd5255..efa30fe 100644 --- a/tools/gltf_auto_export/tests/test_bevy_integration.py +++ b/tools/gltf_auto_export/tests/test_bevy_integration.py @@ -16,18 +16,16 @@ def setup_data(request): root_path = "../../testing/bevy_example" assets_root_path = os.path.join(root_path, "assets") models_path = os.path.join(assets_root_path, "models") - #materials_path = os.path.join("../../testing", "materials") + materials_path = os.path.join(assets_root_path, "materials") #other_materials_path = os.path.join("../../testing", "other_materials") print("\nPerforming teardown...") if os.path.exists(models_path): shutil.rmtree(models_path) - """if os.path.exists(materials_path): + if os.path.exists(materials_path): shutil.rmtree(materials_path) - if os.path.exists(other_materials_path): - shutil.rmtree(other_materials_path)""" diagnostics_file_path = os.path.join(root_path, "bevy_diagnostics.json") if os.path.exists(diagnostics_file_path): os.remove(diagnostics_file_path) @@ -75,7 +73,8 @@ def test_export_complex(setup_data): export_scene_settings=True, export_blueprints=True, export_legacy_mode=False, - export_animations=True + export_animations=True, + export_materials_library=True ) # blueprint1 => has an instance, got changed, should export # blueprint2 => has NO instance, but marked as asset, should export @@ -95,11 +94,7 @@ def test_export_complex(setup_data): # now run bevy command = "cargo run --features bevy/dynamic_linking" - # assert getattr(propertyGroup, 'a') == 0.5714026093482971 FNULL = open(os.devnull, 'w') #use this if you want to suppress output to stdout from the subprocess - filename = "my_file.dat" - args = command - #subprocess.call(args, stdout=FNULL, stderr=FNULL, shell=False, cwd=bevy_run_exec_path) return_code = subprocess.call(["cargo", "run", "--features", "bevy/dynamic_linking"], cwd=root_path) print("RETURN CODE OF BEVY APP", return_code) assert return_code == 0