diff --git a/crates/bevy_gltf_blueprints/src/animation.rs b/crates/bevy_gltf_blueprints/src/animation.rs index 4a18ec4..ee400b3 100644 --- a/crates/bevy_gltf_blueprints/src/animation.rs +++ b/crates/bevy_gltf_blueprints/src/animation.rs @@ -3,8 +3,8 @@ use bevy::utils::HashMap; #[derive(Component, Reflect, Default, Debug)] #[reflect(Component)] -/// storage for animations for a given entity (hierarchy), essentially a clone of gltf's `named_animations` -pub struct Animations { +/// storage for animations for a given entity's BLUEPRINT (ie for example a characters animations), essentially a clone of gltf's `named_animations` +pub struct BlueprintAnimations { pub named_animations: HashMap>, } @@ -13,4 +13,70 @@ pub struct Animations { /// so that the root entity knows which of its children contains an actualy `AnimationPlayer` component /// this is for convenience, because currently , Bevy's gltf parsing inserts `AnimationPlayers` "one level down" /// ie armature/root for animated models, which means more complex queries to trigger animations that we want to avoid -pub struct AnimationPlayerLink(pub Entity); +pub struct BlueprintAnimationPlayerLink(pub Entity); + +#[derive(Component, Reflect, Default, Debug)] +#[reflect(Component)] +/// storage for animations for a given entity (hierarchy), essentially a clone of gltf's `named_animations` +pub struct InstanceAnimations { + pub named_animations: HashMap>, +} + +#[derive(Component, Debug)] +/// Stop gap helper component : this is inserted into a "root" entity (an entity representing a whole gltf file) +/// so that the root entity knows which of its children contains an actualy `AnimationPlayer` component +/// this is for convenience, because currently , Bevy's gltf parsing inserts `AnimationPlayers` "one level down" +/// ie armature/root for animated models, which means more complex queries to trigger animations that we want to avoid +pub struct InstanceAnimationPlayerLink(pub Entity); + +/// Stores Animation information: name, frame informations etc +#[derive(Reflect, Default, Debug)] +pub struct AnimationInfo { + pub name: String, + pub frame_start: f32, + pub frame_end: f32, + pub frames_length: f32, + pub frame_start_override: f32, + pub frame_end_override: f32, +} + +/// Stores information about animations, to make things a bit easier api wise: +/// these components are automatically inserted by gltf_auto_export on entities that have animations +#[derive(Component, Reflect, Default, Debug)] +#[reflect(Component)] +pub struct AnimationInfos { + pub animations: Vec, +} + +pub struct AnimationMarker { + pub frame: u32, + pub name: String, +} + +/// Stores information about animation markers: practical for adding things like triggering events at specific keyframes etc +/// it is essentiall a hashmap of AnimationName => HashMap +#[derive(Component, Reflect, Default, Debug)] +#[reflect(Component)] +pub struct AnimationMarkers(pub HashMap>>); + +// FIXME: ugh, ugly, there has to be a better way to do this ? +#[derive(Component, Default, Debug)] +pub struct AnimationMarkerTrackers(pub HashMap>>); + +#[derive(Default, Debug)] +pub struct AnimationMarkerTracker { + // pub frame:u32, + // pub name: String, + // pub processed_for_cycle: bool, + pub prev_frame: u32, +} + +/// Event that gets triggered once a specific marker inside an animation has been reached (frame based) +/// Provides some usefull information about which entity , wich animation, wich frame & which marker got triggered +#[derive(Event, Debug)] +pub struct AnimationMarkerReached { + pub entity: Entity, + pub animation_name: String, + pub frame: u32, + pub marker_name: String, +} diff --git a/crates/bevy_gltf_blueprints/src/lib.rs b/crates/bevy_gltf_blueprints/src/lib.rs index 36ac25b..1d4ce32 100644 --- a/crates/bevy_gltf_blueprints/src/lib.rs +++ b/crates/bevy_gltf_blueprints/src/lib.rs @@ -123,9 +123,16 @@ impl Plugin for BlueprintsPlugin { .register_type::() .register_type::() .register_type::() - .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::() + .register_type::>() + .register_type::() + .register_type::>>() + .register_type::>>>() + .add_event::() .register_type::() - .register_type::>() .register_type::>>() .insert_resource(BluePrintsConfig { format: self.format, diff --git a/crates/bevy_gltf_blueprints/src/spawn_from_blueprints.rs b/crates/bevy_gltf_blueprints/src/spawn_from_blueprints.rs index f977c61..16367df 100644 --- a/crates/bevy_gltf_blueprints/src/spawn_from_blueprints.rs +++ b/crates/bevy_gltf_blueprints/src/spawn_from_blueprints.rs @@ -2,7 +2,7 @@ use std::path::{Path, PathBuf}; use bevy::{gltf::Gltf, prelude::*, utils::HashMap}; -use crate::{Animations, BluePrintsConfig}; +use crate::{BluePrintsConfig, BlueprintAnimations}; /// this is a flag component for our levels/game world #[derive(Component)] @@ -279,11 +279,12 @@ pub(crate) fn spawn_from_blueprints( transform: transforms, ..Default::default() }, - Animations { - named_animations: gltf.named_animations.clone(), - }, Spawned, OriginalChildren(original_children), + BlueprintAnimations { + // these are animations specific to the inside of the blueprint + named_animations: gltf.named_animations.clone(), + }, )); if add_to_world.is_some() { diff --git a/crates/bevy_gltf_blueprints/src/spawn_post_process.rs b/crates/bevy_gltf_blueprints/src/spawn_post_process.rs index b53f4d7..a88e950 100644 --- a/crates/bevy_gltf_blueprints/src/spawn_post_process.rs +++ b/crates/bevy_gltf_blueprints/src/spawn_post_process.rs @@ -5,7 +5,7 @@ use bevy::prelude::*; use bevy::scene::SceneInstance; // use bevy::utils::hashbrown::HashSet; -use super::{AnimationPlayerLink, Animations}; +use super::{BlueprintAnimationPlayerLink, BlueprintAnimations}; use super::{SpawnHere, Spawned}; use crate::{ AssetsToLoad, BlueprintAssetsLoaded, CopyComponents, InBlueprint, NoInBlueprint, @@ -24,7 +24,7 @@ pub(crate) fn spawned_blueprint_post_process( Entity, &Children, &OriginalChildren, - &Animations, + &BlueprintAnimations, Option<&NoInBlueprint>, Option<&Name>, ), @@ -85,7 +85,9 @@ pub(crate) fn spawned_blueprint_post_process( // FIXME: stopgap solution: since we cannot use an AnimationPlayer at the root entity level // and we cannot update animation clips so that the EntityPaths point to one level deeper, // BUT we still want to have some marker/control at the root entity level, we add this - commands.entity(original).insert(AnimationPlayerLink(added)); + commands + .entity(original) + .insert(BlueprintAnimationPlayerLink(added)); } } } diff --git a/examples/bevy_registry_export/basic/assets/registry.json b/examples/bevy_registry_export/basic/assets/registry.json index 879ccd1..9aa5141 100644 --- a/examples/bevy_registry_export/basic/assets/registry.json +++ b/examples/bevy_registry_export/basic/assets/registry.json @@ -3373,6 +3373,17 @@ "type": "object", "typeInfo": "Struct" }, + "bevy_gltf_blueprints::animation::Animated": { + "additionalProperties": false, + "isComponent": true, + "isResource": false, + "properties": {}, + "required": [], + "short_name": "Animated", + "title": "bevy_gltf_blueprints::animation::Animated", + "type": "object", + "typeInfo": "Struct" + }, "bevy_gltf_blueprints::animation::Animations": { "additionalProperties": false, "isComponent": true, @@ -3433,6 +3444,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, @@ -10691,6 +10718,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/Cargo.toml b/testing/bevy_example/Cargo.toml index 3dbc4ca..04043fb 100644 --- a/testing/bevy_example/Cargo.toml +++ b/testing/bevy_example/Cargo.toml @@ -14,3 +14,4 @@ bevy_rapier3d = { version = "0.25.0", features = ["serde-serialize", "debug-rend bevy_asset_loader = { version = "0.20", features = ["standard_dynamic_assets"] } bevy_editor_pls = { version = "0.8" } rand = "0.8.5" +json-writer ="0.3" \ No newline at end of file diff --git a/testing/bevy_example/assets/registry.json b/testing/bevy_example/assets/registry.json index 33887c3..03cf205 100644 --- a/testing/bevy_example/assets/registry.json +++ b/testing/bevy_example/assets/registry.json @@ -110,6 +110,19 @@ "type": "array", "typeInfo": "List" }, + "alloc::vec::Vec": { + "isComponent": false, + "isResource": false, + "items": { + "type": { + "$ref": "#/$defs/bevy_gltf_blueprints::animation::AnimationInfo" + } + }, + "short_name": "Vec", + "title": "alloc::vec::Vec", + "type": "array", + "typeInfo": "List" + }, "alloc::vec::Vec": { "isComponent": false, "isResource": false, @@ -2947,6 +2960,39 @@ "type": "object", "typeInfo": "Struct" }, + "bevy_example::game::animation::Marker1": { + "additionalProperties": false, + "isComponent": true, + "isResource": false, + "properties": {}, + "required": [], + "short_name": "Marker1", + "title": "bevy_example::game::animation::Marker1", + "type": "object", + "typeInfo": "Struct" + }, + "bevy_example::game::animation::Marker2": { + "additionalProperties": false, + "isComponent": true, + "isResource": false, + "properties": {}, + "required": [], + "short_name": "Marker2", + "title": "bevy_example::game::animation::Marker2", + "type": "object", + "typeInfo": "Struct" + }, + "bevy_example::game::animation::Marker3": { + "additionalProperties": false, + "isComponent": true, + "isResource": false, + "properties": {}, + "required": [], + "short_name": "Marker3", + "title": "bevy_example::game::animation::Marker3", + "type": "object", + "typeInfo": "Struct" + }, "bevy_example::test_components::AComponentWithAnExtremlyExageratedOrMaybeNotButCouldBeNameOrWut": { "additionalProperties": false, "isComponent": true, @@ -3516,7 +3562,91 @@ "type": "object", "typeInfo": "Struct" }, - "bevy_gltf_blueprints::animation::Animations": { + "bevy_gltf_blueprints::animation::AnimationInfo": { + "additionalProperties": false, + "isComponent": false, + "isResource": false, + "properties": { + "frame_end": { + "type": { + "$ref": "#/$defs/f32" + } + }, + "frame_end_override": { + "type": { + "$ref": "#/$defs/f32" + } + }, + "frame_start": { + "type": { + "$ref": "#/$defs/f32" + } + }, + "frame_start_override": { + "type": { + "$ref": "#/$defs/f32" + } + }, + "frames_length": { + "type": { + "$ref": "#/$defs/f32" + } + }, + "name": { + "type": { + "$ref": "#/$defs/alloc::string::String" + } + } + }, + "required": [ + "name", + "frame_start", + "frame_end", + "frames_length", + "frame_start_override", + "frame_end_override" + ], + "short_name": "AnimationInfo", + "title": "bevy_gltf_blueprints::animation::AnimationInfo", + "type": "object", + "typeInfo": "Struct" + }, + "bevy_gltf_blueprints::animation::AnimationInfos": { + "additionalProperties": false, + "isComponent": true, + "isResource": false, + "properties": { + "animations": { + "type": { + "$ref": "#/$defs/alloc::vec::Vec" + } + } + }, + "required": [ + "animations" + ], + "short_name": "AnimationInfos", + "title": "bevy_gltf_blueprints::animation::AnimationInfos", + "type": "object", + "typeInfo": "Struct" + }, + "bevy_gltf_blueprints::animation::AnimationMarkers": { + "isComponent": true, + "isResource": false, + "items": false, + "prefixItems": [ + { + "type": { + "$ref": "#/$defs/bevy_utils::hashbrown::HashMap, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>" + } + } + ], + "short_name": "AnimationMarkers", + "title": "bevy_gltf_blueprints::animation::AnimationMarkers", + "type": "array", + "typeInfo": "TupleStruct" + }, + "bevy_gltf_blueprints::animation::BlueprintAnimations": { "additionalProperties": false, "isComponent": true, "isResource": false, @@ -3530,8 +3660,27 @@ "required": [ "named_animations" ], - "short_name": "Animations", - "title": "bevy_gltf_blueprints::animation::Animations", + "short_name": "BlueprintAnimations", + "title": "bevy_gltf_blueprints::animation::BlueprintAnimations", + "type": "object", + "typeInfo": "Struct" + }, + "bevy_gltf_blueprints::animation::InstanceAnimations": { + "additionalProperties": false, + "isComponent": true, + "isResource": false, + "properties": { + "named_animations": { + "type": { + "$ref": "#/$defs/bevy_utils::hashbrown::HashMap, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>" + } + } + }, + "required": [ + "named_animations" + ], + "short_name": "InstanceAnimations", + "title": "bevy_gltf_blueprints::animation::InstanceAnimations", "type": "object", "typeInfo": "Struct" }, @@ -10863,6 +11012,32 @@ "type": "object", "typeInfo": "Map" }, + "bevy_utils::hashbrown::HashMap, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>": { + "additionalProperties": { + "type": { + "$ref": "#/$defs/bevy_utils::hashbrown::HashMap, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>" + } + }, + "isComponent": false, + "isResource": false, + "short_name": "HashMap, DefaultHashBuilder>, DefaultHashBuilder>", + "title": "bevy_utils::hashbrown::HashMap, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>, bevy_utils::hashbrown::hash_map::DefaultHashBuilder>", + "type": "object", + "typeInfo": "Map" + }, + "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/assets/testing.blend b/testing/bevy_example/assets/testing.blend index 5af50a4..bdce4f5 100644 Binary files a/testing/bevy_example/assets/testing.blend and b/testing/bevy_example/assets/testing.blend differ diff --git a/testing/bevy_example/expected_screenshot.png b/testing/bevy_example/expected_screenshot.png index 94da691..43569fc 100644 Binary files a/testing/bevy_example/expected_screenshot.png and b/testing/bevy_example/expected_screenshot.png differ diff --git a/testing/bevy_example/src/game/animation.rs b/testing/bevy_example/src/game/animation.rs new file mode 100644 index 0000000..326b52b --- /dev/null +++ b/testing/bevy_example/src/game/animation.rs @@ -0,0 +1,278 @@ +use std::time::Duration; + +use bevy_gltf_blueprints::{ + AnimationInfos, AnimationMarkerReached, AnimationMarkerTrackers, AnimationMarkers, + BlueprintAnimationPlayerLink, BlueprintAnimations, BlueprintName, BlueprintsList, + GltfBlueprintsSet, InstanceAnimationPlayerLink, InstanceAnimations, +}; + +use bevy::{gltf::Gltf, prelude::*}; +use bevy_gltf_worlflow_examples_common_rapier::{AppState, GameState}; + +#[derive(Component, Reflect, Default, Debug)] +#[reflect(Component)] +/// flag component for testing +pub struct Marker1; + +#[derive(Component, Reflect, Default, Debug)] +#[reflect(Component)] +/// flag component for testing +pub struct Marker2; + +#[derive(Component, Reflect, Default, Debug)] +#[reflect(Component)] +/// flag component for testing +pub struct Marker3; + +#[derive(Resource)] +pub struct AnimTest(Handle); + +pub fn setup_main_scene_animations(asset_server: Res, mut commands: Commands) { + commands.insert_resource(AnimTest(asset_server.load("models/World.glb"))); +} + +pub fn animations( + added_animation_players: Query<(Entity, &Name, &AnimationPlayer)>, + added_animation_infos: Query<(Entity, &Name, &AnimationInfos), (Added)>, + animtest: Res, + mut commands: Commands, + assets_gltf: Res>, + parents: Query<&Parent>, +) { + for (entity, name, animation_infos) in added_animation_infos.iter() { + //println!("animated stuf {:?} on entity {}", animation_infos, name); + let gltf = assets_gltf.get(&animtest.0).unwrap(); + let mut matching_data = true; + for animation_info in &animation_infos.animations { + if !gltf.named_animations.contains_key(&animation_info.name) { + matching_data = false; + break; + } + } + if matching_data { + println!( + "inserting Animations components into {} ({:?})", + name, entity + ); + println!("Found match {:?}", gltf.named_animations); + commands.entity(entity).insert(InstanceAnimations { + named_animations: gltf.named_animations.clone(), + }); + for ancestor in parents.iter_ancestors(entity) { + if added_animation_players.contains(ancestor) { + // println!("found match with animationPlayer !! {:?}",names.get(ancestor)); + commands + .entity(entity) + .insert(InstanceAnimationPlayerLink(ancestor)); + } + // info!("{:?} is an ancestor of {:?}", ancestor, player); + } + } + println!(""); + } +} + +pub fn play_animations( + animated_marker1: Query< + (&InstanceAnimationPlayerLink, &InstanceAnimations), + (With, With), + >, + animated_marker2: Query< + (&InstanceAnimationPlayerLink, &InstanceAnimations), + (With, With), + >, + animated_marker3: Query< + ( + &InstanceAnimationPlayerLink, + &InstanceAnimations, + &BlueprintAnimationPlayerLink, + &BlueprintAnimations, + ), + (With, With), + >, + + mut animation_players: Query<&mut AnimationPlayer>, + keycode: Res>, +) { + if keycode.just_pressed(KeyCode::KeyM) { + for (link, animations) in animated_marker1.iter() { + println!("animations {:?}", animations.named_animations); + let mut animation_player = animation_players.get_mut(link.0).unwrap(); + let anim_name = "Blueprint1_move"; + animation_player + .play_with_transition( + animations + .named_animations + .get(anim_name) + .expect("animation name should be in the list") + .clone(), + Duration::from_secs(5), + ) + .repeat(); + } + } + if keycode.just_pressed(KeyCode::KeyJ) { + for (link, animations) in animated_marker1.iter() { + println!("animations {:?}", animations.named_animations); + let mut animation_player = animation_players.get_mut(link.0).unwrap(); + let anim_name = "Blueprint1_jump"; + animation_player + .play_with_transition( + animations + .named_animations + .get(anim_name) + .expect("animation name should be in the list") + .clone(), + Duration::from_secs(5), + ) + .repeat(); + } + } + + if keycode.just_pressed(KeyCode::KeyA) { + for (link, animations) in animated_marker2.iter() { + println!("animations {:?}", animations.named_animations); + let mut animation_player = animation_players.get_mut(link.0).unwrap(); + let anim_name = "Blueprint1_move"; + animation_player + .play_with_transition( + animations + .named_animations + .get(anim_name) + .expect("animation name should be in the list") + .clone(), + Duration::from_secs(5), + ) + .repeat(); + } + } + if keycode.just_pressed(KeyCode::KeyB) { + for (link, animations) in animated_marker2.iter() { + println!("animations {:?}", animations.named_animations); + let mut animation_player = animation_players.get_mut(link.0).unwrap(); + let anim_name = "Blueprint1_jump"; + animation_player + .play_with_transition( + animations + .named_animations + .get(anim_name) + .expect("animation name should be in the list") + .clone(), + Duration::from_secs(5), + ) + .repeat(); + } + } + + // play instance animation + if keycode.just_pressed(KeyCode::KeyW) { + for (link, animations, _, _) in animated_marker3.iter() { + println!("animations {:?}", animations.named_animations); + let mut animation_player = animation_players.get_mut(link.0).unwrap(); + let anim_name = "Blueprint8_move"; + animation_player + .play_with_transition( + animations + .named_animations + .get(anim_name) + .expect("animation name should be in the list") + .clone(), + Duration::from_secs(5), + ) + .repeat(); + } + } + // play blueprint animation + if keycode.just_pressed(KeyCode::KeyX) { + for (_, _, link, animations) in animated_marker3.iter() { + println!("animations {:?}", animations.named_animations); + let mut animation_player = animation_players.get_mut(link.0).unwrap(); + let anim_name = "Walk"; + animation_player + .play_with_transition( + animations + .named_animations + .get(anim_name) + .expect("animation name should be in the list") + .clone(), + Duration::from_secs(5), + ) + .repeat(); + } + } +} + +pub fn trigger_event_based_on_animation_marker( + animation_infos: Query<( + Entity, + &AnimationMarkers, + &InstanceAnimationPlayerLink, + &InstanceAnimations, + &AnimationInfos, + )>, + animation_players: Query<&AnimationPlayer>, + animation_clips: Res>, + mut animation_marker_events: EventWriter, +) { + for (entity, markers, link, animations, animation_infos) in animation_infos.iter() { + let animation_player = animation_players.get(link.0).unwrap(); + let animation_clip = animation_clips.get(animation_player.animation_clip()); + + if animation_clip.is_some() { + // if marker_trackers.0.contains_key(k) + // marker_trackers.0 + // println!("Entity {:?} markers {:?}", entity, markers); + // println!("Player {:?} {}", animation_player.elapsed(), animation_player.completions()); + + // FIMXE: yikes ! very inneficient ! perhaps add boilerplate to the "start playing animation" code so we know what is playing + let animation_name = animations.named_animations.iter().find_map(|(key, value)| { + if value == animation_player.animation_clip() { + Some(key) + } else { + None + } + }); + if animation_name.is_some() { + let animation_name = animation_name.unwrap(); + + let animation_length_seconds = animation_clip.unwrap().duration(); + let animation_length_frames = animation_infos + .animations + .iter() + .find(|anim| &anim.name == animation_name) + .unwrap() + .frames_length; + // TODO: we also need to take playback speed into account + let time_in_animation = animation_player.elapsed() + - (animation_player.completions() as f32) * animation_length_seconds; + let frame_seconds = + (animation_length_frames as f32 / animation_length_seconds) * time_in_animation; + let frame = frame_seconds as u32; + + let matching_animation_marker = &markers.0[animation_name]; + if matching_animation_marker.contains_key(&frame) { + let matching_markers_per_frame = matching_animation_marker.get(&frame).unwrap(); + // println!("FOUND A MARKER {:?} at frame {}", matching_markers_per_frame, frame); + //emit an event , something like AnimationMarkerReached(entity, animation_name, frame, marker_name) + // FIXME: problem, this can fire multiple times in a row, depending on animation length , speed , etc + for marker_name in matching_markers_per_frame { + animation_marker_events.send(AnimationMarkerReached { + entity: entity, + animation_name: animation_name.clone(), + frame: frame, + marker_name: marker_name.clone(), + }); + } + } + } + } + } +} + +pub fn react_to_animation_markers( + mut animation_marker_events: EventReader, +) { + for event in animation_marker_events.read() { + println!("animation marker event {:?}", event) + } +} diff --git a/testing/bevy_example/src/game/mod.rs b/testing/bevy_example/src/game/mod.rs index 973140b..d6d6d14 100644 --- a/testing/bevy_example/src/game/mod.rs +++ b/testing/bevy_example/src/game/mod.rs @@ -1,52 +1,47 @@ +pub mod animation; pub mod in_game; -use std::{ - fs::{self}, - time::Duration, -}; - -use bevy_gltf_blueprints::{AnimationPlayerLink, BlueprintName, BlueprintsList}; +pub use animation::*; pub use in_game::*; +use std::{collections::HashMap, fs, time::Duration}; + +use bevy_gltf_blueprints::{ + AnimationInfos, AnimationMarkerReached, AnimationMarkerTrackers, AnimationMarkers, + BlueprintAnimationPlayerLink, BlueprintAnimations, BlueprintName, BlueprintsList, + GltfBlueprintsSet, InstanceAnimationPlayerLink, InstanceAnimations, +}; + use bevy::{ - prelude::*, render::view::screenshot::ScreenshotManager, time::common_conditions::on_timer, - window::PrimaryWindow, + ecs::query, gltf::Gltf, prelude::*, render::view::screenshot::ScreenshotManager, + time::common_conditions::on_timer, window::PrimaryWindow, }; use bevy_gltf_worlflow_examples_common_rapier::{AppState, GameState}; use crate::{TupleTestF32, UnitTest}; +use json_writer::to_json_string; fn start_game(mut next_app_state: ResMut>) { next_app_state.set(AppState::AppLoading); } // if the export from Blender worked correctly, we should have animations (simplified here by using AnimationPlayerLink) -// if the export from Blender worked correctly, we should have an Entity called "Cylinder" that has two components: UnitTest, TupleTestF32 // if the export from Blender worked correctly, we should have an Entity called "Blueprint4_nested" that has a child called "Blueprint3" that has a "BlueprintName" component with value Blueprint3 // if the export from Blender worked correctly, we should have a blueprints_list +// if the export from Blender worked correctly, we should have the correct tree of entities #[allow(clippy::too_many_arguments)] fn validate_export( parents: Query<&Parent>, children: Query<&Children>, names: Query<&Name>, blueprints: Query<(Entity, &Name, &BlueprintName)>, - animation_player_links: Query<(Entity, &AnimationPlayerLink)>, - exported_cylinder: Query<(Entity, &Name, &UnitTest, &TupleTestF32)>, + animation_player_links: Query<(Entity, &BlueprintAnimationPlayerLink)>, empties_candidates: Query<(Entity, &Name, &GlobalTransform)>, blueprints_list: Query<(Entity, &BlueprintsList)>, + root: Query<(Entity, &Name, &Children), (Without, With)>, ) { let animations_found = !animation_player_links.is_empty(); - let mut cylinder_found = false; - if let Ok(nested_cylinder) = exported_cylinder.get_single() { - let parent_name = names - .get(parents.get(nested_cylinder.0).unwrap().get()) - .unwrap(); - cylinder_found = parent_name.to_string() == *"Cube.001" - && nested_cylinder.1.to_string() == *"Cylinder" - && nested_cylinder.3 .0 == 75.1; - } - let mut nested_blueprint_found = false; for (entity, name, blueprint_name) in blueprints.iter() { if name.to_string() == *"Blueprint4_nested" && blueprint_name.0 == *"Blueprint4_nested" { @@ -71,14 +66,46 @@ fn validate_export( break; } } - + // check if there are blueprints_list components let blueprints_list_found = !blueprints_list.is_empty(); + // there should be no entity named xxx____bak as it means an error in the Blender side export process + let mut exported_names_correct = true; + for name in names.iter() { + if name.to_string().ends_with("___bak") { + exported_names_correct = false; + break; + } + } + + // generate parent/child "tree" + if !root.is_empty() { + let root = root.single(); + let mut tree: HashMap> = HashMap::new(); + + for child in children.iter_descendants(root.0) { + let child_name: String = names + .get(child) + .map_or(String::from("no_name"), |e| e.to_string()); //|e| e.to_string(), || "no_name".to_string()); + //println!(" child {}", child_name); + let parent = parents.get(child).unwrap(); + let parent_name: String = names + .get(parent.get()) + .map_or(String::from("no_name"), |e| e.to_string()); //|e| e.to_string(), || "no_name".to_string()); + tree.entry(parent_name) + .or_default() + .push(child_name.clone()); + } + + let hierarchy = to_json_string(&tree); + fs::write("bevy_hierarchy.json", hierarchy).expect("unable to write hierarchy file") + } + fs::write( "bevy_diagnostics.json", format!( - "{{ \"animations\": {}, \"cylinder_found\": {} , \"nested_blueprint_found\": {}, \"empty_found\": {}, \"blueprints_list_found\": {} }}", - animations_found, cylinder_found, nested_blueprint_found, empty_found, blueprints_list_found + "{{ \"animations\": {}, \"nested_blueprint_found\": {}, \"empty_found\": {}, \"blueprints_list_found\": {}, \"exported_names_correct\": {} }}", + animations_found, nested_blueprint_found, empty_found, blueprints_list_found, exported_names_correct ), ) .expect("Unable to write file"); @@ -100,15 +127,28 @@ fn exit_game(mut app_exit_events: ResMut>) { pub struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { - app.add_systems(Update, (spawn_test).run_if(in_state(GameState::InGame))) + app.register_type::() + .register_type::() + .register_type::() + + .add_systems(Update, (spawn_test).run_if(in_state(GameState::InGame))) .add_systems(Update, validate_export) .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(OnEnter(AppState::MenuRunning), setup_main_scene_animations) + .add_systems(Update, (animations, trigger_event_based_on_animation_marker) + .run_if(in_state(AppState::AppRunning)) + .after(GltfBlueprintsSet::AfterSpawn) + ) + .add_systems(Update, play_animations) + .add_systems(Update, react_to_animation_markers) + + /* .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))), - ) // shut down the app after this time + ) // shut down the app after this time*/ ; } } diff --git a/tools/gltf_auto_export/auto_export/export_gltf.py b/tools/gltf_auto_export/auto_export/export_gltf.py index 11fb4da..2c08dfc 100644 --- a/tools/gltf_auto_export/auto_export/export_gltf.py +++ b/tools/gltf_auto_export/auto_export/export_gltf.py @@ -1,5 +1,7 @@ import os import bpy + +from .get_standard_exporter_settings import get_standard_exporter_settings from .preferences import (AutoExportGltfPreferenceNames) def generate_gltf_export_preferences(addon_prefs): @@ -37,10 +39,31 @@ def generate_gltf_export_preferences(addon_prefs): export_optimize_animation_size=False ) + + + for key in addon_prefs.__annotations__.keys(): if str(key) not in AutoExportGltfPreferenceNames: #print("overriding setting", key, "value", getattr(addon_prefs,key)) - gltf_export_preferences[key] = getattr(addon_prefs,key) + gltf_export_preferences[key] = getattr(addon_prefs, key) + + + """standard_gltf_exporter_settings = get_standard_exporter_settings() + print("standard settings", standard_gltf_exporter_settings) + + constant_keys = [ + 'export_cameras', + 'export_extras', # For custom exported properties. + 'export_lights', + ] + + # a certain number of essential params should NEVER be overwritten , no matter the settings of the standard exporter + for key in standard_gltf_exporter_settings.keys(): + if str(key) not in constant_keys: + gltf_export_preferences[key] = standard_gltf_exporter_settings.get(key) + + print("final export preferences", gltf_export_preferences)""" + return gltf_export_preferences diff --git a/tools/gltf_auto_export/auto_export/get_standard_exporter_settings.py b/tools/gltf_auto_export/auto_export/get_standard_exporter_settings.py new file mode 100644 index 0000000..4af09ef --- /dev/null +++ b/tools/gltf_auto_export/auto_export/get_standard_exporter_settings.py @@ -0,0 +1,9 @@ +import bpy + +def get_standard_exporter_settings(): + settings_key = 'glTF2ExportSettings' + for scene in bpy.data.scenes: + if settings_key in scene: + settings = scene[settings_key] + #print("standard exporter settings", settings, dict(settings)) + return dict(settings) \ No newline at end of file diff --git a/tools/gltf_auto_export/helpers/helpers_scenes.py b/tools/gltf_auto_export/helpers/helpers_scenes.py index 55009c9..6eb061b 100644 --- a/tools/gltf_auto_export/helpers/helpers_scenes.py +++ b/tools/gltf_auto_export/helpers/helpers_scenes.py @@ -20,87 +20,149 @@ def remove_unwanted_custom_properties(object): for component_name in object.keys(): if not is_component_valid(object, component_name): to_remove.append(component_name) - for cp in custom_properties_to_filter_out + to_remove: if cp in object: del object[cp] -def duplicate_object(object): - obj_copy = object.copy() - if object.data: - data = object.data.copy() - obj_copy.data = data - if object.animation_data and object.animation_data.action: - obj_copy.animation_data.action = object.animation_data.action.copy() - return obj_copy +# TODO: rename actions ? +# reference https://github.com/KhronosGroup/glTF-Blender-IO/blob/main/addons/io_scene_gltf2/blender/exp/animation/gltf2_blender_gather_action.py#L481 +def copy_animation_data(source, target): + if source.animation_data and source.animation_data: + ad = source.animation_data -#also removes unwanted custom_properties for all objects in hiearchy -def duplicate_object_recursive(object, parent, collection): - original_name = object.name - object.name = original_name + "____bak" - copy = duplicate_object(object) - copy.name = original_name - collection.objects.link(copy) + blender_actions = [] + blender_tracks = {} - remove_unwanted_custom_properties(copy) + # TODO: this might need to be modified/ adapted to match the standard gltf exporter settings + for track in ad.nla_tracks: + non_muted_strips = [strip for strip in track.strips if strip.action is not None and strip.mute is False] + for strip in non_muted_strips: #t.strips: + # print(" ", source.name,'uses',strip.action.name, "active", strip.active, "action", strip.action) + blender_actions.append(strip.action) + blender_tracks[strip.action.name] = track.name - if parent: + # Remove duplicate actions. + blender_actions = list(set(blender_actions)) + # sort animations alphabetically (case insensitive) so they have a defined order and match Blender's Action list + blender_actions.sort(key = lambda a: a.name.lower()) + + markers_per_animation = {} + animations_infos = [] + + for action in blender_actions: + animation_name = blender_tracks[action.name] + animations_infos.append( + f'(name: "{animation_name}", frame_start: {action.frame_range[0]}, frame_end: {action.frame_range[1]}, frames_length: {action.frame_range[1] - action.frame_range[0]}, frame_start_override: {action.frame_start}, frame_end_override: {action.frame_end})' + ) + markers_per_animation[animation_name] = {} + + for marker in action.pose_markers: + if marker.frame not in markers_per_animation[animation_name]: + markers_per_animation[animation_name][marker.frame] = [] + markers_per_animation[animation_name][marker.frame].append(marker.name) + + """if target.animation_data == None: + target.animation_data_create() + target.animation_data.action = source.animation_data.action.copy()""" + # alternative method, using the built-in link animation operator + with bpy.context.temp_override(active_object=source, selected_editable_objects=[target]): + bpy.ops.object.make_links_data(type='ANIMATION') + # we add an "AnimationInfos" component + target['AnimationInfos'] = f'(animations: {animations_infos})'.replace("'","") + + markers_formated = '{' + for animation in markers_per_animation.keys(): + markers_formated += f'"{animation}":' + markers_formated += "{" + for frame in markers_per_animation[animation].keys(): + markers = markers_per_animation[animation][frame] + markers_formated += f"{frame}:{markers}, ".replace("'", '"') + markers_formated += '}, ' + markers_formated += '}' + target["AnimationMarkers"] = f'( {markers_formated} )' + + """print("copying animation data for", source.name, target.animation_data) + properties = [p.identifier for p in source.animation_data.bl_rna.properties if not p.is_readonly] + for prop in properties: + print("copying stuff", prop) + setattr(target.animation_data, prop, getattr(source.animation_data, prop))""" + + + +def duplicate_object(object, parent, combine_mode, destination_collection, library_collections, legacy_mode, nester=""): + copy = None + if object.instance_type == 'COLLECTION' and (combine_mode == 'Split' or (combine_mode == 'EmbedExternal' and (object.instance_collection.name in library_collections)) ): + #print("creating empty for", object.name, object.instance_collection.name, library_collections, combine_mode) + collection_name = object.instance_collection.name + original_name = object.name + + object.name = original_name + "____bak" + empty_obj = make_empty(original_name, object.location, object.rotation_euler, object.scale, destination_collection) + """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 + if not legacy_mode: + 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["AnimationMarkers"] = '({"animation_name": {5: "Marker_1"} })' + + #'({5: "sdf"})'#.replace('"',"'") #f"({json.dumps(dict(animation_foo))})" + #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(): + if component_name not in custom_properties_to_filter_out and is_component_valid(object, component_name): #copy only valid properties + empty_obj[component_name] = component_value + copy = empty_obj + else: + # for objects which are NOT collection instances + # we create a copy of our object and its children, to leave the original one as it is + original_name = object.name + object.name = original_name + "____bak" + copy = object.copy() + copy.name = original_name + + + destination_collection.objects.link(copy) + + """if object.parent == None: + if parent_empty is not None: + copy.parent = parent_empty + """ + + # print(nester, "copy", copy) + # do this both for empty replacements & normal copies + if parent is not None: copy.parent = parent + remove_unwanted_custom_properties(copy) + copy_animation_data(object, copy) for child in object.children: - duplicate_object_recursive(child, copy, collection) - return copy - + duplicate_object(child, copy, combine_mode, destination_collection, library_collections, legacy_mode, nester+" ") # copies the contents of a collection into another one while replacing library instances with empties def copy_hollowed_collection_into(source_collection, destination_collection, parent_empty=None, filter=None, library_collections=[], addon_prefs={}): collection_instances_combine_mode = getattr(addon_prefs, "collection_instances_combine_mode") legacy_mode = getattr(addon_prefs, "export_legacy_mode") collection_instances_combine_mode= collection_instances_combine_mode + for object in source_collection.objects: + if object.name.endswith("____bak"): # some objects could already have been handled, ignore them + continue if filter is not None and filter(object) is False: continue #check if a specific collection instance does not have an ovveride for combine_mode combine_mode = object['_combine'] if '_combine' in object else collection_instances_combine_mode - - if object.instance_type == 'COLLECTION' and (combine_mode == 'Split' or (combine_mode == 'EmbedExternal' and (object.instance_collection.name in library_collections)) ): - #print("creating empty for", object.name, object.instance_collection.name, library_collections, combine_mode) - collection_name = object.instance_collection.name - original_name = object.name - - object.name = original_name + "____bak" - empty_obj = make_empty(original_name, object.location, object.rotation_euler, object.scale, destination_collection) - """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 - if not legacy_mode: - root_node = CollectionNode() - root_node.name = "root" - children_per_collection = {} - print("collection stuff", original_name) - 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(): - if component_name not in custom_properties_to_filter_out and is_component_valid(object, component_name): #copy only valid properties - empty_obj[component_name] = component_value - if parent_empty is not None: - empty_obj.parent = parent_empty - else: - - # we create a copy of our object and its children, to leave the original one as it is - if object.parent == None: - copy = duplicate_object_recursive(object, None, destination_collection) - - if parent_empty is not None: - copy.parent = parent_empty - - # for every sub-collection of the source, copy its content into a new sub-collection of the destination + parent = parent_empty + duplicate_object(object, parent, combine_mode, destination_collection, library_collections, legacy_mode) + + # for every child-collection of the source, copy its content into a new sub-collection of the destination for collection in source_collection.children: original_name = collection.name collection.name = original_name + "____bak" @@ -108,7 +170,6 @@ def copy_hollowed_collection_into(source_collection, destination_collection, par if parent_empty is not None: collection_placeholder.parent = parent_empty - copy_hollowed_collection_into( source_collection = collection, destination_collection = destination_collection, @@ -117,6 +178,8 @@ def copy_hollowed_collection_into(source_collection, destination_collection, par library_collections = library_collections, addon_prefs=addon_prefs ) + + return {} @@ -138,14 +201,14 @@ def clear_hollow_scene(temp_scene, original_root_collection): # reset original names restore_original_names(original_root_collection) - # remove empties (only needed when we go via ops ????) + # remove any data we created temp_root_collection = temp_scene.collection - temp_scene_objects = [o for o in temp_root_collection.objects] + temp_scene_objects = [o for o in temp_root_collection.all_objects] for object in temp_scene_objects: + print("removing", object.name) bpy.data.objects.remove(object, do_unlink=True) # remove the temporary scene - bpy.data.scenes.remove(temp_scene) - + bpy.data.scenes.remove(temp_scene, do_unlink=True) # convenience utility to get lists of scenes def get_scenes(addon_prefs): @@ -160,9 +223,6 @@ 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 diff --git a/tools/gltf_auto_export/tests/expected_bevy_hierarchy.json b/tools/gltf_auto_export/tests/expected_bevy_hierarchy.json new file mode 100644 index 0000000..8a1165b --- /dev/null +++ b/tools/gltf_auto_export/tests/expected_bevy_hierarchy.json @@ -0,0 +1 @@ +{"b_Tail02_013":["b_Tail03_014"],"Blueprint4_nested.001":["Blueprint3"],"Collection 2 1":["Empty_in_sub_collection"],"b_Root_00":["b_Hip_01"],"b_LeftForeArm_010":["b_LeftHand_011"],"b_Spine01_02":["b_Spine02_03"],"Blueprint7_hierarchy.001":["Blueprint4_nested.001","Cube.001"],"b_RightLeg01_019":["b_RightLeg02_020"],"b_LeftUpperArm_09":["b_LeftForeArm_010"],"no_name":["Parent_Object","lighting_components_World","assets_list_World_components","Collection","Collection 2"],"Blueprint3":["Blueprint3_mesh","Blueprint3_mesh"],"world":["no_name"],"Parent_Object":["Cube.003","Blueprint1","Cylinder.001"],"Light":["Light","DirectionalLight Gizmo"],"Blueprint1.001":["Blueprint1_mesh"],"Blueprint7_hierarchy":["Cube.001"],"Spot":["Spot"],"b_Hip_01":["b_Spine01_02","b_Tail01_012","b_LeftLeg01_015","b_RightLeg01_019"],"Cylinder":["Cylinder.001","Cylinder.001"],"Collection 2":["Collection 2 1","Empty_in_collection","Spot"],"b_RightForeArm_07":["b_RightHand_08"],"Blueprint3_mesh":["Cylinder","Cylinder"],"Blueprint4_nested":["Blueprint3"],"Fox_mesh":["fox1"],"b_LeftLeg01_015":["b_LeftLeg02_016"],"b_Neck_04":["b_Head_05"],"b_RightFoot01_021":["b_RightFoot02_022"],"Blueprint1_mesh":["Cube.001","Cube.001"],"b_Tail01_012":["b_Tail02_013"],"Fox":["Fox_mesh","_rootJoint"],"Collection":["Blueprint1.001","Blueprint4_nested","Blueprint6_animated","Blueprint7_hierarchy","Camera","Cube","Empty","Light","Plane"],"Cube":["Cube"],"_rootJoint":["b_Root_00"],"b_RightLeg02_020":["b_RightFoot01_021"],"b_RightUpperArm_06":["b_RightForeArm_07"],"Plane":["Plane"],"Camera":["Camera Gizmo"],"Blueprint6_animated":["Fox"],"b_Spine02_03":["b_Neck_04","b_RightUpperArm_06","b_LeftUpperArm_09"],"b_LeftLeg02_016":["b_LeftFoot01_017"],"b_LeftFoot01_017":["b_LeftFoot02_018"],"Cube.001":["Cube.002","Cylinder","Cube.002","Cylinder"],"Cylinder.001":["Cylinder.002","Blueprint7_hierarchy.001","Empty_as_child"],"Blueprint1":["Blueprint1_mesh"]} \ No newline at end of file diff --git a/tools/gltf_auto_export/tests/test_basic.py b/tools/gltf_auto_export/tests/test_basic.py index 7d01954..8d3c1e5 100644 --- a/tools/gltf_auto_export/tests/test_basic.py +++ b/tools/gltf_auto_export/tests/test_basic.py @@ -19,7 +19,6 @@ def setup_data(request): def finalizer(): print("\nPerforming teardown...") - get_orphan_data() if os.path.exists(models_path): shutil.rmtree(models_path) @@ -38,7 +37,10 @@ def setup_data(request): def get_orphan_data(): orphan_meshes = [m.name for m in bpy.data.meshes if m.users == 0] - # print("orphan meshes before", orphan_meshes) + orphan_objects = [m.name for m in bpy.data.objects if m.users == 0] + + #print("orphan meshes before", orphan_meshes) + return orphan_meshes + orphan_objects def test_export_do_not_export_blueprints(setup_data): auto_export_operator = bpy.ops.export_scenes.auto_gltf @@ -61,6 +63,9 @@ def test_export_do_not_export_blueprints(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"], "library", "Blueprint1.glb")) == False + orphan_data = get_orphan_data() + assert len(orphan_data) == 0 + def test_export_custom_blueprints_path(setup_data): auto_export_operator = bpy.ops.export_scenes.auto_gltf @@ -83,6 +88,7 @@ def test_export_custom_blueprints_path(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"], "another_library_path", "Blueprint1.glb")) == True + assert len(get_orphan_data()) == 0 def test_export_materials_library(setup_data): auto_export_operator = bpy.ops.export_scenes.auto_gltf @@ -107,7 +113,7 @@ def test_export_materials_library(setup_data): assert os.path.exists(os.path.join(setup_data["models_path"], "library", "Blueprint1.glb")) == True assert os.path.exists(os.path.join(setup_data["materials_path"], "testing_materials_library.glb")) == True - + assert len(get_orphan_data()) == 0 def test_export_materials_library_custom_path(setup_data): auto_export_operator = bpy.ops.export_scenes.auto_gltf @@ -134,6 +140,7 @@ def test_export_materials_library_custom_path(setup_data): assert os.path.exists(os.path.join(setup_data["models_path"], "library", "Blueprint1.glb")) == True assert os.path.exists(os.path.join(setup_data["materials_path"], "testing_materials_library.glb")) == False assert os.path.exists(os.path.join(setup_data["other_materials_path"], "testing_materials_library.glb")) == True + assert len(get_orphan_data()) == 0 def test_export_collection_instances_combine_mode(setup_data): # TODO: change & check this auto_export_operator = bpy.ops.export_scenes.auto_gltf @@ -160,6 +167,7 @@ def test_export_collection_instances_combine_mode(setup_data): # TODO: change & 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")) == False + assert len(get_orphan_data()) == 0 def test_export_do_not_export_marked_assets(setup_data): @@ -188,6 +196,7 @@ def test_export_do_not_export_marked_assets(setup_data): assert os.path.exists(os.path.join(setup_data["models_path"], "library", "Blueprint3.glb")) == True assert os.path.exists(os.path.join(setup_data["models_path"], "library", "Blueprint4_nested.glb")) == True assert os.path.exists(os.path.join(setup_data["models_path"], "library", "Blueprint5.glb")) == False + assert len(get_orphan_data()) == 0 def test_export_separate_dynamic_and_static_objects(setup_data): @@ -216,6 +225,7 @@ 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 + assert len(get_orphan_data()) == 0 def test_export_should_not_generate_orphan_data(setup_data): @@ -239,4 +249,5 @@ def test_export_should_not_generate_orphan_data(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"], "library", "Blueprint1.glb")) == False + assert len(get_orphan_data()) == 0 diff --git a/tools/gltf_auto_export/tests/test_bevy_integration.py b/tools/gltf_auto_export/tests/test_bevy_integration.py index e45114c..8de7f73 100644 --- a/tools/gltf_auto_export/tests/test_bevy_integration.py +++ b/tools/gltf_auto_export/tests/test_bevy_integration.py @@ -5,6 +5,7 @@ import json import pytest import shutil +import filecmp from PIL import Image from pixelmatch.contrib.PIL import pixelmatch @@ -29,6 +30,10 @@ def setup_data(request): diagnostics_file_path = os.path.join(root_path, "bevy_diagnostics.json") if os.path.exists(diagnostics_file_path): os.remove(diagnostics_file_path) + + hierarchy_file_path = os.path.join(root_path, "bevy_hierarchy.json") + if os.path.exists(hierarchy_file_path): + os.remove(hierarchy_file_path) screenshot_observed_path = os.path.join(root_path, "screenshot.png") if os.path.exists(screenshot_observed_path): @@ -56,7 +61,8 @@ def test_export_complex(setup_data): # we use the global settings for that export_props = { "main_scene_names" : ['World'], - "library_scene_names": ['Library'] + "library_scene_names": ['Library'], + # "export_format":'GLTF_SEPARATE' } 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() @@ -107,9 +113,15 @@ def test_export_complex(setup_data): diagnostics = json.load(diagnostics_file) print("diagnostics", diagnostics) assert diagnostics["animations"] == True - assert diagnostics["cylinder_found"] == True assert diagnostics["empty_found"] == True assert diagnostics["blueprints_list_found"] == True + assert diagnostics["exported_names_correct"] == True + + with open(os.path.join(root_path, "bevy_hierarchy.json")) as hierarchy_file: + with open(os.path.join(os.path.dirname(__file__), "expected_bevy_hierarchy.json")) as expexted_hierarchy_file: + hierarchy = json.load(hierarchy_file) + expected = json.load(expexted_hierarchy_file) + assert sorted(hierarchy.items()) == sorted(expected.items()) # last but not least, do a visual compare screenshot_expected_path = os.path.join(root_path, "expected_screenshot.png")