feat(Blenvy):

* Blender: some additional component filtering for upgrade ui
 * Bevy:
   * experimenting with overhauled blueprint spawning logic, sub-blueprint instance tracking etc
   * added a more simple blend file to test things out with
This commit is contained in:
kaosat.dev 2024-07-01 23:35:59 +02:00
parent 6cbb144746
commit 07d58467c4
9 changed files with 376 additions and 43 deletions

View File

@ -107,7 +107,9 @@ impl Plugin for BlueprintsPlugin {
(
blueprints_prepare_spawn,
blueprints_check_assets_loading,
blueprints_spawn,
blueprints_assets_ready,
blueprints_check_blueprints_spawning,
// blueprints_spawn,
/*(
prepare_blueprints,
@ -145,6 +147,7 @@ impl Plugin for BlueprintsPlugin {
)*/
.add_systems(Update, react_to_asset_changes)
// .add_systems(Update, track_sub_blueprints)
;
}
}

View File

@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};
use bevy::{asset::LoadedUntypedAsset, gltf::Gltf, prelude::*, render::view::visibility, scene::SceneInstance, transform::commands, utils::hashbrown::HashMap};
use bevy::{asset::LoadedUntypedAsset, gltf::Gltf, prelude::*, render::view::visibility, scene::SceneInstance, transform::commands, utils::{hashbrown::HashMap}};
use serde_json::Value;
use crate::{BlueprintAssets, BlueprintAssetsLoadState, AssetLoadTracker, BlenvyConfig, BlueprintAnimations, BlueprintAssetsLoaded, BlueprintAssetsNotLoaded};
@ -65,6 +65,7 @@ pub enum BlueprintEvent {
/// event fired when a blueprint has finished loading its assets & before it attempts spawning
AssetsLoaded {
entity: Entity,
blueprint_name: String,
blueprint_path: String,
// TODO: add assets list ?
@ -73,18 +74,40 @@ pub enum BlueprintEvent {
/// - all its assets have been loaded
/// - the spawning attempt has been sucessfull
Spawned {
entity: Entity,
blueprint_name: String,
blueprint_path: String,
},
///
Ready {
entity: Entity,
blueprint_path: String,
}
}
// TODO: move this somewhere else ?
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// component used to mark any entity as Dynamic: aka add this to make sure your entity is going to be saved
pub struct DynamicBlueprintInstance;
// TODO: move these somewhere else ?
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// component gets added when a blueprint starts spawning, removed when spawning is done
pub struct BlueprintSpawning;
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
/// component gets added when a blueprint spawning is done
pub struct BlueprintSpawned;
use gltf::Gltf as RawGltf;
pub(crate) fn blueprints_prepare_spawn(
@ -101,8 +124,8 @@ asset_server: Res<AssetServer>,
) {
for (entity, blueprint_info, parent, all_assets) in blueprint_instances_to_spawn.iter() {
println!("Detected blueprint to spawn: {:?} path:{:?}", blueprint_info.name, blueprint_info.path);
println!("all assets {:?}", all_assets);
info!("BLUEPRINT: to spawn detected: {:?} path:{:?}", blueprint_info.name, blueprint_info.path);
//println!("all assets {:?}", all_assets);
//////////////
// we add the asset of the blueprint itself
@ -124,7 +147,7 @@ asset_server: Res<AssetServer>,
// and we also add all its assets
/* prefetch attempt */
let gltf = RawGltf::open(format!("assets/{}", blueprint_info.path)).unwrap();// RawGltf::open("examples/Box.gltf")?;
let gltf = RawGltf::open(format!("assets/{}", blueprint_info.path)).unwrap();
for scene in gltf.scenes() {
let foo_extras = scene.extras().clone().unwrap();
@ -137,7 +160,7 @@ asset_server: Res<AssetServer>,
let assets_raw = &lookup["BlueprintAssets"];
//println!("ASSETS RAW {}", assets_raw);
let all_assets: BlueprintAssets = ron::from_str(&assets_raw.as_str().unwrap()).unwrap();
println!("all_assets {:?}", all_assets);
// println!("all_assets {:?}", all_assets);
for asset in all_assets.assets.iter() {
let untyped_handle = asset_server.load_untyped(&asset.path);
@ -174,6 +197,9 @@ asset_server: Res<AssetServer>,
} else {
commands.entity(entity).insert(BlueprintAssetsLoaded);
}
commands.entity(entity).insert(BlueprintSpawning);
}
}
@ -216,8 +242,8 @@ pub(crate) fn blueprints_check_assets_loading(
if all_loaded {
assets_to_load.all_loaded = true;
println!("LOADING: in progress for ALL assets of {:?} (instance of {}), preparing for spawn", entity_name, blueprint_info.path);
blueprint_events.send(BlueprintEvent::AssetsLoaded {blueprint_name:"".into(), blueprint_path: blueprint_info.path.clone() });
// println!("LOADING: DONE for ALL assets of {:?} (instance of {}), preparing for spawn", entity_name, blueprint_info.path);
// blueprint_events.send(BlueprintEvent::AssetsLoaded {blueprint_name:"".into(), blueprint_path: blueprint_info.path.clone() });
commands
.entity(entity)
@ -226,7 +252,7 @@ pub(crate) fn blueprints_check_assets_loading(
//.remove::<BlueprintAssetsLoadState>() //REMOVE it in release mode/ when hot reload is off, keep it for dev/hot reload
;
}else {
println!("LOADING: done for ALL assets of {:?} (instance of {}): {} ",entity_name, blueprint_info.path, progress * 100.0);
// println!("LOADING: in progress for ALL assets of {:?} (instance of {}): {} ",entity_name, blueprint_info.path, progress * 100.0);
}
}
}
@ -302,30 +328,30 @@ pub(crate) fn react_to_asset_changes(
}
pub(crate) fn blueprints_spawn(
spawn_placeholders: Query<
(
Entity,
&BlueprintInfo,
Option<&Transform>,
Option<&Parent>,
Option<&AddToGameWorld>,
Option<&Name>,
),
(
With<BlueprintAssetsLoaded>,
Added<BlueprintAssetsLoaded>,
Without<BlueprintAssetsNotLoaded>,
),
>,
pub(crate) fn blueprints_assets_ready(spawn_placeholders: Query<
(
Entity,
&BlueprintInfo,
Option<&Transform>,
Option<&Parent>,
Option<&AddToGameWorld>,
Option<&Name>,
),
(
With<BlueprintAssetsLoaded>,
Added<BlueprintAssetsLoaded>,
Without<BlueprintAssetsNotLoaded>,
),
>,
mut commands: Commands,
mut game_world: Query<Entity, With<GameWorldTag>>,
assets_gltf: Res<Assets<Gltf>>,
asset_server: Res<AssetServer>,
children: Query<&Children>,
) {
children: Query<&Children>,)
{
for (
entity,
blueprint_info,
@ -336,7 +362,7 @@ pub(crate) fn blueprints_spawn(
) in spawn_placeholders.iter()
{
info!(
"all assets loaded, attempting to spawn blueprint {:?} for entity {:?}, id: {:?}, parent:{:?}",
"BLUEPRINT: all assets loaded, attempting to spawn blueprint SCENE {:?} for entity {:?}, id: {:?}, parent:{:?}",
blueprint_info.name, name, entity, original_parent
);
@ -385,25 +411,137 @@ pub(crate) fn blueprints_spawn(
..Default::default()
},
Spawned,
BlueprintInstanceReady, // FIXME: not sure if this is should be added here or in the post process
OriginalChildren(original_children),
BlueprintAnimations {
// these are animations specific to the inside of the blueprint
named_animations: named_animations//gltf.named_animations.clone(),
},
));
if add_to_world.is_some() {
/* if add_to_world.is_some() {
let world = game_world
.get_single_mut()
.expect("there should be a game world present");
commands.entity(world).add_child(entity);
}
} */
}
}
#[derive(Component, Reflect, Debug, Default)]
#[reflect(Component)]
pub struct SubBlueprintsSpawnTracker{
sub_blueprint_instances: HashMap<Entity, bool>
}
#[derive(Component, Reflect, Debug)]
#[reflect(Component)]
pub struct SpawnTrackRoot(Entity);
pub(crate) fn blueprints_check_blueprints_spawning(
foo: Query<(Entity, Option<&Name>, Option<&Children>, Option<&SpawnTrackRoot>), (With<BlueprintSpawning>, Added<SceneInstance>)>,
spawning_blueprints: Query<(Entity, Option<&Name>, Option<&Children>), Added<BlueprintSpawned>>,
mut trackers: Query<(Entity, &mut SubBlueprintsSpawnTracker)>,
with_blueprint_infos : Query<(Entity, Option<&Name>), With<BlueprintInfo>>,
all_children: Query<&Children>,
mut commands: Commands,
) {
for (entity, name, children, track_root) in foo.iter(){
info!("Done spawning blueprint scene for {:?} (track root: {:?})", name, track_root);
let mut sub_blueprint_instances: Vec<Entity> = vec![];
let mut tracker_data: HashMap<Entity, bool> = HashMap::new();
if children.is_some() {
//println!("has children");
let children = children
.expect("there should be some children of the current blueprint instance");
for child in all_children.iter_descendants(entity) {
if with_blueprint_infos.get(child).is_ok() {
sub_blueprint_instances.push(child);
tracker_data.insert(child, false);
commands.entity(child).insert(SpawnTrackRoot(entity));// Injecting to know which entity is the root
}
}
/*for child in children.iter() {
// println!(" child: {:?}", child);
/*if with_blueprint_infos.get(*child).is_ok() {
sub_blueprint_instances.push(*child);
} */
all_children
if let Ok(sub_children) = all_children.get(*child) {
for sub_child in sub_children.iter() {
if with_blueprint_infos.get(*sub_child).is_ok() {
sub_blueprint_instances.push(*sub_child);
}
}
}
}*/
}
if let Some(track_root) = track_root {
//println!("got some root");
if let Ok((s_entity, mut tracker)) = trackers.get_mut(track_root.0) {
// println!("found the tracker, setting loaded for {}", entity);
tracker.sub_blueprint_instances.entry(entity).or_insert(true);
tracker.sub_blueprint_instances.insert(entity, true);
// TODO: ugh, my limited rust knowledge, this is bad code
let mut all_spawned = true;
for key in tracker.sub_blueprint_instances.keys() {
let val = tracker.sub_blueprint_instances[key];
println!("Key: {key}, Spawned {}", val);
}
for val in tracker.sub_blueprint_instances.values() {
println!("spawned {}", val);
if !val {
all_spawned = false;
break;
}
}
if all_spawned {
println!("ALLLLL SPAAAAWNED for {}", track_root.0)
}
}
}
println!("sub blueprint instances {:?}", sub_blueprint_instances);
commands.entity(entity)
.insert(SubBlueprintsSpawnTracker{sub_blueprint_instances: tracker_data.clone()});
}
/*for(entity, name, children) in spawning_blueprints.iter() {
println!("checking for spawning state of sub blueprints for {:?}", name);
}*/
}
/*
BlueprintSpawning
- Blueprint Load Assets
- Blueprint Assets Ready: spawn Blueprint's scene
- Blueprint Scene Ready:
- get list of sub Blueprints if any, inject blueprints spawn tracker
=> annoying issue with the "nested" useless root node created by blender
=> distinguish between blueprint instances inside blueprint instances vs blueprint instances inside blueprints ??
- Blueprint sub_blueprints Ready
*/
// could be done differently, by notifying each parent of a spawning blueprint that this child is done spawning ?
// perhaps using component hooks or observers (ie , if a ComponentSpawning + Parent)
pub fn track_sub_blueprints(
spawning_blueprints: Query<(Entity, Option<&Name>, Option<&Children>), Added<BlueprintSpawned>>
) {
for(entity, name, children) in spawning_blueprints.iter() {
println!("checking for spawning state of sub blueprints for {:?}", name);
}
}

View File

@ -5,7 +5,7 @@ use bevy::prelude::*;
use bevy::scene::SceneInstance;
// use bevy::utils::hashbrown::HashSet;
use crate::{BlueprintAnimationPlayerLink, BlueprintAnimations, BlueprintInfo};
use crate::{BlueprintAnimationPlayerLink, BlueprintAnimations, BlueprintInfo, BlueprintSpawned, BlueprintSpawning};
use crate::{SpawnHere, Spawned};
use crate::{
BlueprintEvent, CopyComponents, InBlueprint, NoInBlueprint, OriginalChildren
@ -63,7 +63,7 @@ pub(crate) fn spawned_blueprint_post_process(
// can be usefull to filter out anything that came from blueprints vs normal children
if no_inblueprint.is_none() {
for child in all_children.iter_descendants(root_entity) {
commands.entity(child).insert(InBlueprint);
commands.entity(child).insert(InBlueprint); // we do this here in order to avoid doing it to normal children
}
}
@ -101,11 +101,18 @@ pub(crate) fn spawned_blueprint_post_process(
// commands.entity(original).remove::<Handle<Scene>>(); // FIXME: if we delete the handle to the scene, things get despawned ! not what we want
//commands.entity(original).remove::<BlueprintAssetsLoadState>(); // 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::<BlueprintAssetsLoaded>();
commands.entity(root_entity).despawn_recursive();
commands.entity(root_entity).despawn_recursive(); // Remove the root entity that comes from the spawned-in scene
commands.entity(original).insert( Visibility::Visible
);
blueprint_events.send(BlueprintEvent::Spawned {blueprint_name: blueprint_info.name.clone(), blueprint_path: blueprint_info.path.clone() });
// blueprint_events.send(BlueprintEvent::Spawned {blueprint_name: blueprint_info.name.clone(), blueprint_path: blueprint_info.path.clone() });
commands.entity(original)
.insert(BlueprintSpawned)
.remove::<BlueprintSpawning>()
;
debug!("DONE WITH POST PROCESS");
info!("done spawning blueprint for entity {:?}", name);
}
}

View File

@ -128,15 +128,15 @@ fn process_tonemapping(
for (scene_id, tone_mapping) in tonemappings.iter(){
match tone_mapping {
BlenderToneMapping::None => {
println!("TONEMAPPING NONE");
//println!("TONEMAPPING NONE");
commands.entity(entity).remove::<Tonemapping>();
}
BlenderToneMapping::AgX => {
println!("TONEMAPPING Agx");
//println!("TONEMAPPING Agx");
commands.entity(entity).insert(Tonemapping::AgX);
}
BlenderToneMapping::Filmic => {
println!("TONEMAPPING Filmic");
//println!("TONEMAPPING Filmic");
commands.entity(entity).insert(Tonemapping::BlenderFilmic);
}
}

Binary file not shown.

View File

@ -1,5 +1,5 @@
use bevy::prelude::*;
use blenvy::{BluePrintBundle, BlueprintInfo, GameWorldTag, SpawnHere};
use blenvy::{BluePrintBundle, BlueprintInfo, DynamicBlueprintInstance, GameWorldTag, SpawnHere};
use crate::{GameState, InAppRunning};
//use bevy_rapier3d::prelude::Velocity;
@ -65,6 +65,7 @@ pub fn spawn_test(
blueprint: BlueprintInfo{name: "Blueprint1".into() , path:"blueprints/Blueprint1.glb".into()}, // FIXME
..Default::default()
},
DynamicBlueprintInstance,
bevy::prelude::Name::from(format!("test{}", name_index)),
// SpawnHere,
TransformBundle::from_transform(Transform::from_xyz(x, 2.0, y)),

View File

@ -156,8 +156,8 @@ impl Plugin for HiearchyDebugPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(Startup, setup_hierarchy_debug)
.add_systems(Update, check_for_component)
//.add_systems(Update, draw_hierarchy_debug)
// .add_systems(Update, check_for_component)
.add_systems(Update, draw_hierarchy_debug)
//.add_systems(Update, check_for_gltf_extras)
;

View File

@ -386,7 +386,7 @@ class BLENVY_PT_component_tools_panel(bpy.types.Panel):
self.draw_invalid_or_unregistered(layout, status, custom_property, item, item_type)
def gather_invalid_item_data(self, item, invalid_component_names, items_with_invalid_components, items_with_original_components, original_name, item_type):
blenvy_custom_properties = ['components_meta', 'bevy_components', 'user_assets', 'generated_assets' ] # some of our own hard coded custom properties that should be ignored
blenvy_custom_properties = ['components_meta', 'bevy_components', 'user_assets', 'generated_assets', 'BlueprintAssets', 'export_path' ] # some of our own hard coded custom properties that should be ignored
upgreadable_entries = []
if "components_meta" in item or hasattr(item, "components_meta"): # FIXME; wrong way of determining

View File

@ -0,0 +1,184 @@
import bpy
import os
import subprocess
import json
import pytest
import shutil
import filecmp
from PIL import Image
from pixelmatch.contrib.PIL import pixelmatch
from blenvy.add_ons.auto_export.common.prepare_and_export import prepare_and_export
@pytest.fixture
def setup_data(request):
print("\nSetting up resources...")
root_path = "../../testing/bevy_example"
assets_root_path = os.path.join(root_path, "assets")
blueprints_path = os.path.join(assets_root_path, "blueprints")
levels_path = os.path.join(assets_root_path, "levels")
models_path = os.path.join(assets_root_path, "models")
materials_path = os.path.join(assets_root_path, "materials")
yield {
"root_path": root_path,
"models_path": models_path,
"blueprints_path": blueprints_path,
"levels_path": levels_path,
"materials_path":materials_path
}
def finalizer():
#other_materials_path = os.path.join("../../testing", "other_materials")
print("\nPerforming teardown...")
if os.path.exists(blueprints_path):
shutil.rmtree(blueprints_path)
if os.path.exists(levels_path):
shutil.rmtree(levels_path)
if os.path.exists(models_path):
shutil.rmtree(models_path)
if os.path.exists(materials_path):
shutil.rmtree(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)
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):
os.remove(screenshot_observed_path)
#request.addfinalizer(finalizer)
return None
"""
- calls exporter on the testing scene
- launches bevy app & checks for output
- checks screenshot, hierarchy & diagnostics files generated on the bevy side against reference files
- if all worked => test is a-ok
- removes generated files
"""
def test_export_complex(setup_data):
root_path = setup_data["root_path"]
# with change detection
# first, configure things
# we use the global settings for that
export_props = {
}
gltf_settings = {
"export_animations": True,
"export_optimize_animation_size": False,
"export_apply":True
}
# store settings for the auto_export part
stored_auto_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_auto_settings.clear()
stored_auto_settings.write(json.dumps(export_props))
# and store settings for the gltf part
stored_gltf_settings = bpy.data.texts[".blenvy_gltf_settings"] if ".blenvy_gltf_settings" in bpy.data.texts else bpy.data.texts.new(".blenvy_gltf_settings")
stored_gltf_settings.clear()
stored_gltf_settings.write(json.dumps(gltf_settings))
# move the main cube
bpy.data.objects["Cube"].location = [1, 0, 0]
# move the cube in the library
# TODO: add back bpy.data.objects["Blueprint1_mesh"].location = [1, 2, 1]
blenvy = bpy.context.window_manager.blenvy
#blenvy.project_root_path =
#blenvy.blueprints_path
blenvy.auto_export.auto_export = True
blenvy.auto_export.export_scene_settings = True
blenvy.auto_export.export_blueprints = True
#blenvy.auto_export.export_materials_library = True
bpy.data.scenes['World'].blenvy_scene_type = 'Level' # set scene as main/level scene
bpy.data.scenes['Library'].blenvy_scene_type = 'Library' # set scene as Library scene
# scene asset
user_asset = bpy.data.scenes['World'].user_assets.add()
'''user_asset.name = "test_asset"
user_asset.path = "audio/fake.mp3"'''
# blueprint asset
#user_asset = bpy.data.collections['Blueprint4_nested'].user_assets.add()
'''user_asset.name = "yoho_audio"
user_asset.path = "audio/fake.mp3"'''
# we have to cheat, since we cannot rely on the data injected when saving the library file (since we are not saving it as part of the tests)
'''bpy.data.collections["External_blueprint"]["export_path"] = "blueprints/External_blueprint.glb"
bpy.data.collections["External_blueprint2"]["export_path"] = "blueprints/External_blueprint2.glb"
bpy.data.collections["External_blueprint3"]["export_path"] = "blueprints/External_blueprint3.glb"'''
# do the actual exporting
prepare_and_export()
# blueprint1 => has an instance, got changed, should export
# blueprint2 => has NO instance, but marked as asset, should export
# blueprint3 => has NO instance, not marked as asset, used inside blueprint 4: should export
# blueprint4 => has an instance, with nested blueprint3, should export
# blueprint5 => has NO instance, not marked as asset, should NOT export
'''assert os.path.exists(os.path.join(setup_data["levels_path"], "World.glb")) == True
assert os.path.exists(os.path.join(setup_data["blueprints_path"], "Blueprint1.glb")) == True
assert os.path.exists(os.path.join(setup_data["blueprints_path"], "Blueprint2.glb")) == True
assert os.path.exists(os.path.join(setup_data["blueprints_path"], "Blueprint3.glb")) == True
assert os.path.exists(os.path.join(setup_data["blueprints_path"], "Blueprint4_nested.glb")) == True
assert os.path.exists(os.path.join(setup_data["blueprints_path"], "Blueprint5.glb")) == False
assert os.path.exists(os.path.join(setup_data["blueprints_path"], "Blueprint6_animated.glb")) == True
assert os.path.exists(os.path.join(setup_data["blueprints_path"], "Blueprint7_hierarchy.glb")) == True'''
# 'assets_list_'+scene.name+"_components" should have been removed after the export
assets_list_object_name = "assets_list_"+"World"+"_components"
assets_list_object_present = assets_list_object_name in bpy.data.objects
assert assets_list_object_present == False
# now run bevy
command = "cargo run --features bevy/dynamic_linking"
FNULL = open(os.devnull, 'w') #use this if you want to suppress output to stdout from the subprocess
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["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(os.path.dirname(__file__), "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