diff --git a/testing/auto_export_template.blend b/testing/auto_export_template.blend deleted file mode 100644 index a951a89..0000000 Binary files a/testing/auto_export_template.blend and /dev/null differ diff --git a/testing/bevy_example/assets/testing.blend b/testing/bevy_example/assets/testing.blend index 28b93dd..4b2fdbf 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 new file mode 100644 index 0000000..f0e0e70 Binary files /dev/null and b/testing/bevy_example/expected_screenshot.png differ diff --git a/testing/bevy_example/src/game/mod.rs b/testing/bevy_example/src/game/mod.rs index 1584153..38145f0 100644 --- a/testing/bevy_example/src/game/mod.rs +++ b/testing/bevy_example/src/game/mod.rs @@ -1,15 +1,93 @@ pub mod in_game; -use std::time::Duration; +use std::{ + fs::{self}, + time::Duration, +}; +use bevy_gltf_blueprints::{AnimationPlayerLink, BlueprintName}; pub use in_game::*; -use bevy::{prelude::*, time::common_conditions::on_timer}; +use bevy::{ + prelude::*, render::view::screenshot::ScreenshotManager, time::common_conditions::on_timer, + window::PrimaryWindow, +}; use bevy_gltf_worlflow_examples_common::{AppState, GameState}; +use crate::{TupleTestF32, UnitTest}; + 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 + +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)>, + empties_candidates: Query<(Entity, &Name, &GlobalTransform)>, +) { + 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" { + if let Ok(cur_children) = children.get(entity) { + for child in cur_children.iter() { + if let Ok((_, child_name, child_blueprint_name)) = blueprints.get(*child) { + if child_name.to_string() == *"Blueprint3" + && child_blueprint_name.0 == *"Blueprint3" + { + nested_blueprint_found = true; + } + } + } + } + } + } + + let mut empty_found = false; + for (_, name, _) in empties_candidates.iter() { + if name.to_string() == *"Empty" { + empty_found = true; + break; + } + } + + fs::write( + "bevy_diagnostics.json", + format!( + "{{ \"animations\": {}, \"cylinder_found\": {} , \"nested_blueprint_found\": {}, \"empty_found\": {} }}", + animations_found, cylinder_found, nested_blueprint_found, empty_found + ), + ) + .expect("Unable to write file"); +} + +fn generate_screenshot( + main_window: Query>, + mut screenshot_manager: ResMut, +) { + screenshot_manager + .save_screenshot_to_disk(main_window.single(), "screenshot.png") + .unwrap(); +} + fn exit_game(mut app_exit_events: ResMut>) { app_exit_events.send(bevy::app::AppExit); } @@ -18,11 +96,14 @@ 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))) + .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, exit_game.run_if(on_timer(Duration::from_secs_f32(0.5))), ) // shut down the app after this time - .add_systems(OnEnter(AppState::AppRunning), setup_game); + ; } } diff --git a/testing/bevy_example/src/test_components.rs b/testing/bevy_example/src/test_components.rs index 58a19c2..8c340e1 100644 --- a/testing/bevy_example/src/test_components.rs +++ b/testing/bevy_example/src/test_components.rs @@ -7,11 +7,11 @@ use std::ops::Range; #[derive(Component, Reflect, Default, Debug)] #[reflect(Component)] -struct UnitTest; +pub struct UnitTest; #[derive(Component, Reflect, Default, Debug, Deref, DerefMut)] #[reflect(Component)] -struct TupleTestF32(f32); +pub struct TupleTestF32(pub f32); #[derive(Component, Reflect, Default, Debug, Deref, DerefMut)] #[reflect(Component)] diff --git a/tools/gltf_auto_export/helpers/helpers_scenes.py b/tools/gltf_auto_export/helpers/helpers_scenes.py index a8e4a27..90416ba 100644 --- a/tools/gltf_auto_export/helpers/helpers_scenes.py +++ b/tools/gltf_auto_export/helpers/helpers_scenes.py @@ -24,12 +24,38 @@ def remove_unwanted_custom_properties(object): 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 + +#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) + + remove_unwanted_custom_properties(copy) + + if parent: + copy.parent = parent + + for child in object.children: + duplicate_object_recursive(child, copy, collection) + return copy + + # 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 filter is not None and filter(object) is False: continue @@ -54,19 +80,13 @@ def copy_hollowed_collection_into(source_collection, destination_collection, par if parent_empty is not None: empty_obj.parent = parent_empty else: - # we create a copy of our object, to leave the original one as it is - original_name = object.name - object.name = original_name + "____bak" - copy = object.copy() - copy.name = original_name - remove_unwanted_custom_properties(copy) + + # 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 - destination_collection.objects.link(copy) - else: - # root_objects.append(object) - destination_collection.objects.link(copy) + 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 for collection in source_collection.children: diff --git a/tools/gltf_auto_export/helpers/to_remove_later.py b/tools/gltf_auto_export/helpers/to_remove_later.py index cdeba37..595bc93 100644 --- a/tools/gltf_auto_export/helpers/to_remove_later.py +++ b/tools/gltf_auto_export/helpers/to_remove_later.py @@ -219,4 +219,24 @@ def did_export_parameters_change(current_params, previous_params): print("copying ", key,"to", components_holder) if not key in components_holder: components_holder[key] = components[key] - """ \ No newline at end of file + """ + +# potentially useful alternative +def duplicate_object2(object, original_name): + print("copy object", object) + + with bpy.context.temp_override(object=object, active_object = object): + bpy.ops.object.duplicate(linked=False) + new_obj = bpy.context.active_object + + print("new obj", new_obj, "bpy.context.view_layer", bpy.context.view_layer.objects) + for obj in bpy.context.view_layer.objects: + print("obj", obj) + bpy.context.view_layer.update() + new_obj.name = original_name + + if object.animation_data: + print("OJECT ANIMATION") + new_obj.animation_data.action = object.animation_data.action.copy() + + return new_obj \ No newline at end of file diff --git a/tools/gltf_auto_export/pytest.ini b/tools/gltf_auto_export/pytest.ini index 319620f..33c0e52 100644 --- a/tools/gltf_auto_export/pytest.ini +++ b/tools/gltf_auto_export/pytest.ini @@ -4,3 +4,6 @@ addopts = -svv testpaths = tests +# dependencies: +# pytest_blender +# pixelmatch \ No newline at end of file diff --git a/tools/gltf_auto_export/tests/test_bevy_integration.py b/tools/gltf_auto_export/tests/test_bevy_integration.py index 64bdf47..70ba140 100644 --- a/tools/gltf_auto_export/tests/test_bevy_integration.py +++ b/tools/gltf_auto_export/tests/test_bevy_integration.py @@ -5,6 +5,9 @@ import json import pytest import shutil +from PIL import Image +from pixelmatch.contrib.PIL import pixelmatch + @pytest.fixture def setup_data(request): print("\nSetting up resources...") @@ -25,7 +28,13 @@ def setup_data(request): 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) + + screenshot_observed_path = os.path.join(root_path, "screenshot.png") + if os.path.exists(screenshot_observed_path): + os.remove(screenshot_observed_path) request.addfinalizer(finalizer) @@ -65,7 +74,8 @@ def test_export_complex(setup_data): export_output_folder="./models", export_scene_settings=True, export_blueprints=True, - export_legacy_mode=False + export_legacy_mode=False, + export_animations=True ) # blueprint1 => has an instance, got changed, should export # blueprint2 => has NO instance, but marked as asset, should export @@ -80,6 +90,8 @@ def test_export_complex(setup_data): assert os.path.exists(os.path.join(models_path, "library", "Blueprint3.glb")) == True assert os.path.exists(os.path.join(models_path, "library", "Blueprint4_nested.glb")) == True assert os.path.exists(os.path.join(models_path, "library", "Blueprint5.glb")) == False + assert os.path.exists(os.path.join(models_path, "library", "Blueprint6_animated.glb")) == True + assert os.path.exists(os.path.join(models_path, "library", "Blueprint7_hierarchy.glb")) == True # now run bevy bla = "cargo run --features bevy/dynamic_linking" @@ -91,3 +103,23 @@ def test_export_complex(setup_data): 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 + + with open(os.path.join(root_path, "bevy_diagnostics.json")) as diagnostics_file: + diagnostics = json.load(diagnostics_file) + print("diagnostics", diagnostics) + assert diagnostics["animations"] == True + assert diagnostics["cylinder_found"] == True + assert diagnostics["empty_found"] == True + + # last but not least, do a visual compare + screenshot_expected_path = os.path.join(root_path, "expected_screenshot.png") + screenshot_observed_path = os.path.join(root_path, "screenshot.png") + img_a = Image.open(screenshot_expected_path) + img_b = Image.open(screenshot_observed_path) + img_diff = Image.new("RGBA", img_a.size) + mismatch = pixelmatch(img_a, img_b, img_diff, includeAA=True) + print("image mismatch", mismatch) + assert mismatch < 50 + + +