From 006e3c16f70aa1e0671108492780d5defb4853d2 Mon Sep 17 00:00:00 2001 From: "kaosat.dev" Date: Thu, 16 May 2024 14:09:40 +0200 Subject: [PATCH] feat(blenvy): split up user added assets from generated assets * added Asset propertygroup & User Assets collections to collections & scenes to store them explicitely * overhauled UI to make use of this : also means names & paths are editable after the fact * overhauled testing asset hierarchy scan accordingly * added expertimental export of bevy_asset_loader compatible asset.ron files per level * related tweaks & additions --- tools/blenvy/TODO.md | 3 + tools/blenvy/__init__.py | 3 +- tools/blenvy/assets/asset_helpers.py | 30 +++++++-- tools/blenvy/assets/assets_registry.py | 14 +++-- tools/blenvy/assets/assets_scan.py | 24 +++++-- tools/blenvy/assets/operators.py | 63 +++++++++++++------ tools/blenvy/assets/ui.py | 37 ++++++----- tools/blenvy/blueprints/blueprint_helpers.py | 29 +-------- tools/blenvy/blueprints/ui.py | 11 ++-- .../auto_export/export_main_scenes.py | 2 +- 10 files changed, 137 insertions(+), 79 deletions(-) diff --git a/tools/blenvy/TODO.md b/tools/blenvy/TODO.md index 9439cd9..9b9606e 100644 --- a/tools/blenvy/TODO.md +++ b/tools/blenvy/TODO.md @@ -46,6 +46,9 @@ Blueprints: - export path - [ ] blueprint selection for nested blueprints is broken + - [ ] scan & inject on load + - [ ] scan & inject on save + General issues: - there is no safeguard for naming collisions for naming across blender files diff --git a/tools/blenvy/__init__.py b/tools/blenvy/__init__.py index 769cd27..02b3acd 100644 --- a/tools/blenvy/__init__.py +++ b/tools/blenvy/__init__.py @@ -53,7 +53,7 @@ from .gltf_auto_export.ui.operators import (OT_OpenFolderbrowser, SCENES_LIST_OT # asset management from .assets.ui import Blenvy_assets -from .assets.assets_registry import AssetsRegistry +from .assets.assets_registry import Asset, AssetsRegistry from .assets.operators import OT_Add_asset_filebrowser, OT_add_bevy_asset, OT_remove_bevy_asset, OT_test_bevy_assets # blueprints management @@ -146,6 +146,7 @@ classes = [ BlenvyManager, OT_switch_bevy_tooling, + Asset, AssetsRegistry, OT_add_bevy_asset, OT_remove_bevy_asset, diff --git a/tools/blenvy/assets/asset_helpers.py b/tools/blenvy/assets/asset_helpers.py index ba8a0b1..4ea7cba 100644 --- a/tools/blenvy/assets/asset_helpers.py +++ b/tools/blenvy/assets/asset_helpers.py @@ -1,10 +1,30 @@ import json -def get_assets(scene_or_collection): - assets = json.loads(scene_or_collection.get('assets')) if 'assets' in scene_or_collection else [] - return assets +def get_user_assets(scene_or_collection): + user_assets = getattr(scene_or_collection, 'user_assets', []) + return user_assets -def does_asset_exist(assets, asset_path): - in_list = [asset for asset in assets if (asset["path"] == asset_path)] +def get_generated_assets(scene_or_collection): + generated_assets = [] + return generated_assets + +def get_user_assets_as_list(scene_or_collection): + raw = get_user_assets(scene_or_collection) + result = [] + for asset in raw: + result.append({"name": asset.name, "path": asset.path, "type": "MODEL", "internal": False, "parent": None}) + return result + +def upsert_asset(scene_or_collection, asset): + new_asset = scene_or_collection.user_assets.add() + new_asset.name = asset["name"] + new_asset.path = asset["path"] + +def remove_asset(scene_or_collection, asset): + scene_or_collection.user_assets.remove(scene_or_collection.user_assets.find(asset["path"])) + +def does_asset_exist(scene_or_collection, ref_asset): + user_assets = getattr(scene_or_collection, 'user_assets', []) + in_list = [asset for asset in user_assets if (asset.path == ref_asset["path"])] in_list = len(in_list) > 0 return in_list \ No newline at end of file diff --git a/tools/blenvy/assets/assets_registry.py b/tools/blenvy/assets/assets_registry.py index 7780682..8f4948f 100644 --- a/tools/blenvy/assets/assets_registry.py +++ b/tools/blenvy/assets/assets_registry.py @@ -1,11 +1,12 @@ import bpy -import json -import os -import uuid from pathlib import Path from bpy_types import (PropertyGroup) from bpy.props import (StringProperty, BoolProperty, FloatProperty, FloatVectorProperty, IntProperty, IntVectorProperty, EnumProperty, PointerProperty, CollectionProperty) +# Asset property group +class Asset(PropertyGroup): + name: StringProperty(name="asset name") # type: ignore + path: StringProperty(name="asset path") # type: ignore # this is where we store the information for all available assets # @@ -34,14 +35,19 @@ class AssetsRegistry(PropertyGroup): subtype='FILE_PATH' ) # type: ignore + @classmethod def register(cls): + bpy.types.Scene.user_assets = CollectionProperty(name="user assets", type=Asset) + bpy.types.Collection.user_assets = CollectionProperty(name="user assets", type=Asset) bpy.types.WindowManager.assets_registry = PointerProperty(type=AssetsRegistry) + @classmethod def unregister(cls): del bpy.types.WindowManager.assets_registry - + del bpy.types.Scene.user_assets + del bpy.types.Collection.user_assets def add_asset(self, name, type, path, internal): # internal means it cannot be edited by the user, aka auto generated in_list = [asset for asset in self.assets_list if (asset["path"] == path)] diff --git a/tools/blenvy/assets/assets_scan.py b/tools/blenvy/assets/assets_scan.py index 02e7837..abf054c 100644 --- a/tools/blenvy/assets/assets_scan.py +++ b/tools/blenvy/assets/assets_scan.py @@ -2,7 +2,7 @@ import os import json import bpy -from .asset_helpers import does_asset_exist, get_assets +from .asset_helpers import does_asset_exist, get_user_assets, get_user_assets_as_list def scan_assets(scene, blueprints_data, addon_prefs): export_root_path = getattr(addon_prefs, "export_root_path") @@ -48,6 +48,19 @@ def scan_assets(scene, blueprints_data, addon_prefs): print("blueprint assets", blueprint_assets_list) +def get_userTextures(): + # TODO: limit this to the ones actually in use + # fetch images/textures + # see https://blender.stackexchange.com/questions/139859/how-to-get-absolute-file-path-for-linked-texture-image + textures = [] + for ob in bpy.data.objects: + if ob.type == "MESH": + for mat_slot in ob.material_slots: + if mat_slot.material: + if mat_slot.material.node_tree: + textures.extend([x.image.filepath for x in mat_slot.material.node_tree.nodes if x.type=='TEX_IMAGE']) + print("textures", textures) + def get_blueprint_assets_tree(blueprint, blueprints_data, parent, addon_prefs): export_blueprints_path = getattr(addon_prefs, "export_blueprints_path") export_gltf_extension = getattr(addon_prefs, "export_gltf_extension") @@ -64,15 +77,16 @@ def get_blueprint_assets_tree(blueprint, blueprints_data, parent, addon_prefs): # get the injected path of the external blueprints blueprint_exported_path = child_blueprint.collection['export_path'] if 'export_path' in child_blueprint.collection else None if blueprint_exported_path is not None: - assets_list.append({"name": child_blueprint.name, "path": blueprint_exported_path, "type": "MODEL", "internal": True, "parent": blueprint.name}) + assets_list.append({"name": child_blueprint.name, "path": blueprint_exported_path, "type": "MODEL", "generated": True,"internal":blueprint.local, "parent": blueprint.name}) # and add sub stuff sub_assets_lists = get_blueprint_assets_tree(child_blueprint, blueprints_data, parent=child_blueprint.name, addon_prefs=addon_prefs) assets_list += sub_assets_lists - direct_assets = get_assets(blueprint.collection) + direct_assets = get_user_assets_as_list(blueprint.collection) for asset in direct_assets: asset["parent"] = parent + asset["internal"] = blueprint.local assets_list += direct_assets return assets_list @@ -81,7 +95,7 @@ def get_main_scene_assets_tree(main_scene, blueprints_data, addon_prefs): export_gltf_extension = getattr(addon_prefs, "export_gltf_extension") blueprint_instance_names_for_scene = blueprints_data.blueprint_instances_per_main_scene.get(main_scene.name, None) - assets_list = get_assets(main_scene) # FIXME: problem, we already have the blueprint assets stored there + assets_list = get_user_assets_as_list(main_scene) if blueprint_instance_names_for_scene: for blueprint_name in blueprint_instance_names_for_scene: blueprint = blueprints_data.blueprints_per_name.get(blueprint_name, None) @@ -93,7 +107,7 @@ def get_main_scene_assets_tree(main_scene, blueprints_data, addon_prefs): # get the injected path of the external blueprints blueprint_exported_path = blueprint.collection['export_path'] if 'export_path' in blueprint.collection else None if blueprint_exported_path is not None and not does_asset_exist(assets_list, blueprint_exported_path): - assets_list.append({"name": blueprint.name, "path": blueprint_exported_path, "type": "MODEL", "internal": True, "parent": None}) + assets_list.append({"name": blueprint.name, "path": blueprint_exported_path, "type": "MODEL", "generated": True, "internal":blueprint.local, "parent": None}) assets_list += get_blueprint_assets_tree(blueprint, blueprints_data, parent=blueprint.name, addon_prefs=addon_prefs) diff --git a/tools/blenvy/assets/operators.py b/tools/blenvy/assets/operators.py index 0143481..6ce7df0 100644 --- a/tools/blenvy/assets/operators.py +++ b/tools/blenvy/assets/operators.py @@ -4,6 +4,7 @@ import bpy from bpy_types import (Operator) from bpy.props import (BoolProperty, StringProperty, EnumProperty) +from .asset_helpers import does_asset_exist, get_user_assets, remove_asset, upsert_asset from .assets_scan import get_main_scene_assets_tree from ..core.path_helpers import absolute_path_from_blend_file @@ -53,24 +54,26 @@ class OT_add_bevy_asset(Operator): def execute(self, context): assets = [] blueprint_assets = self.target_type == 'BLUEPRINT' + target = None if blueprint_assets: - assets = json.loads(bpy.data.collections[self.target_name].get('assets')) if 'assets' in bpy.data.collections[self.target_name] else [] + target = bpy.data.collections[self.target_name] else: - assets = json.loads(bpy.data.scenes[self.target_name].get('assets')) if 'assets' in bpy.data.scenes[self.target_name] else [] + target = bpy.data.scenes[self.target_name] + assets = get_user_assets(target) + asset = {"name": self.asset_name, "type": self.asset_type, "path": self.asset_path} + if not does_asset_exist(target, asset): + upsert_asset(target, asset) - in_list = [asset for asset in assets if (asset["path"] == self.asset_path)] - in_list = len(in_list) > 0 - if not in_list: - assets.append({"name": self.asset_name, "type": self.asset_type, "path": self.asset_path, "internal": False}) + #assets.append({"name": self.asset_name, "type": self.asset_type, "path": self.asset_path, "internal": False}) # reset controls context.window_manager.assets_registry.asset_name_selector = "" context.window_manager.assets_registry.asset_type_selector = "MODEL" context.window_manager.assets_registry.asset_path_selector = "" - if blueprint_assets: + """if blueprint_assets: bpy.data.collections[self.target_name]["assets"] = json.dumps(assets) else: - bpy.data.scenes[self.target_name]["assets"] = json.dumps(assets) + bpy.data.scenes[self.target_name]["assets"] = json.dumps(assets)""" return {'FINISHED'} @@ -114,16 +117,11 @@ class OT_remove_bevy_asset(Operator): assets = [] blueprint_assets = self.target_type == 'BLUEPRINT' if blueprint_assets: - assets = json.loads(bpy.data.collections[self.target_name].get('assets')) if 'assets' in bpy.data.collections[self.target_name] else [] + target = bpy.data.collections[self.target_name] else: - assets = json.loads(bpy.data.scenes[self.target_name].get('assets')) if 'assets' in bpy.data.scenes[self.target_name] else [] - - assets = [asset for asset in assets if (asset["path"] != self.asset_path)] - if blueprint_assets: - bpy.data.collections[self.target_name]["assets"] = json.dumps(assets) - else: - bpy.data.scenes[self.target_name]["assets"] = json.dumps(assets) - #context.window_manager.assets_registry.remove_asset(self.asset_path) + target = bpy.data.scenes[self.target_name] + remove_asset(target, {"path": self.asset_path}) + return {'FINISHED'} @@ -167,6 +165,33 @@ class OT_Add_asset_filebrowser(Operator, ImportHelper): from types import SimpleNamespace + +def write_ron_assets_file(level_name, assets_hierarchy, internal_only=False): + # just for testing, this uses the format of bevy_asset_loader's asset files + ''' + ({ + "world":File (path: "models/StartLevel.glb"), + "level1":File (path: "models/Level1.glb"), + "level2":File (path: "models/Level2.glb"), + + "models": Folder ( + path: "models/library", + ), + "materials": Folder ( + path: "materials", + ), + }) + ''' + formated_assets = [] + for asset in assets_hierarchy: + if asset["internal"] or not internal_only: + bla = f'\n "{asset["name"]}": File ( path: "{asset["path"]}" ),' + formated_assets.append(bla) + with open(f"testing/bevy_example/assets/assets_{level_name}.assets.ron", "w") as assets_file: + assets_file.write("({") + assets_file.writelines(formated_assets) + assets_file.write("\n})") + class OT_test_bevy_assets(Operator): """Test assets""" bl_idname = "bevyassets.test" @@ -183,5 +208,7 @@ class OT_test_bevy_assets(Operator): for scene in bpy.data.scenes: if scene.name != "Library": assets_hierarchy = get_main_scene_assets_tree(scene, blueprints_data, settings) - scene["assets_hierarchy"] = json.dumps(assets_hierarchy) + scene["assets"] = json.dumps(assets_hierarchy) + write_ron_assets_file(scene.name, assets_hierarchy, internal_only=False) + return {'FINISHED'} diff --git a/tools/blenvy/assets/ui.py b/tools/blenvy/assets/ui.py index 2d73d47..9e7b4ee 100644 --- a/tools/blenvy/assets/ui.py +++ b/tools/blenvy/assets/ui.py @@ -1,10 +1,10 @@ from types import SimpleNamespace import bpy from .assets_scan import get_main_scene_assets_tree -from .asset_helpers import get_assets +from .asset_helpers import get_user_assets -def draw_assets(layout, name, title, asset_registry, assets, target_type, target_name, editable=True): +def draw_assets(layout, name, title, asset_registry, target_type, target_name, editable=True, user_assets= [], generated_assets = []): header, panel = layout.box().panel(f"assets{name}", default_closed=False) header.label(text=title) if panel: @@ -29,20 +29,28 @@ def draw_assets(layout, name, title, asset_registry, assets, target_type, target add_asset.asset_name = asset_registry.asset_name_selector add_asset.asset_type = asset_registry.asset_type_selector add_asset.asset_path = asset_registry.asset_path_selector + panel.separator() - #assets = json.loads(blueprint.collection["assets"]) if "assets" in blueprint.collection else [] - for asset in assets: + for asset in user_assets: row = panel.row() - row.label(text=asset["name"]) - row.label(text=asset["type"]) - row.label(text=asset["path"]) - if not asset["internal"] and editable: + row.prop(asset, "name", text="") + #row.prop(asset, "path", text="") + row.label(text=asset.path) + asset_selector = row.operator(operator="asset.open_filebrowser", text="", icon="FILE_FOLDER") + + remove_asset = row.operator(operator="bevyassets.remove", text="", icon="TRASH") + remove_asset.target_type = target_type + remove_asset.target_name = target_name + remove_asset.asset_path = asset.path + """if not asset["internal"] and editable: remove_asset = row.operator(operator="bevyassets.remove", text="", icon="TRASH") remove_asset.target_type = target_type remove_asset.target_name = target_name remove_asset.asset_path = asset["path"] else: - row.label(text="") + row.label(text="")""" + for asset in generated_assets: + pass return panel @@ -78,15 +86,16 @@ class Blenvy_assets(bpy.types.Panel): if panel: for scene in bpy.data.scenes: if scene.name != "Library": # FIXME: hack for testing - get_main_scene_assets_tree(scene, blueprints_data, settings) + #get_main_scene_assets_tree(scene, blueprints_data, settings) - direct_assets = get_assets(scene) + user_assets = get_user_assets(scene) row = panel.row() - scene_assets_panel = draw_assets(layout=row, name=scene.name, title=f"{scene.name} Assets", asset_registry=asset_registry, assets=direct_assets, target_type="SCENE", target_name=scene.name) - if scene.name in blueprints_data.blueprint_instances_per_main_scene: + scene_assets_panel = draw_assets(layout=row, name=scene.name, title=f"{scene.name} Assets", asset_registry=asset_registry, user_assets=user_assets, target_type="SCENE", target_name=scene.name) + """if scene.name in blueprints_data.blueprint_instances_per_main_scene: for blueprint_name in blueprints_data.blueprint_instances_per_main_scene[scene.name].keys(): blueprint = blueprints_data.blueprints_per_name[blueprint_name] - blueprint_assets = get_assets(blueprint.collection) + blueprint_assets = get_user_assets(blueprint.collection) if scene_assets_panel: row = scene_assets_panel.row() draw_assets(layout=row, name=blueprint.name, title=f"{blueprint.name} Assets", asset_registry=asset_registry, assets=blueprint_assets, target_type="BLUEPRINT", target_name=blueprint.name) +""" \ No newline at end of file diff --git a/tools/blenvy/blueprints/blueprint_helpers.py b/tools/blenvy/blueprints/blueprint_helpers.py index 26bc51a..d5fdb3e 100644 --- a/tools/blenvy/blueprints/blueprint_helpers.py +++ b/tools/blenvy/blueprints/blueprint_helpers.py @@ -37,19 +37,6 @@ def inject_blueprints_list_into_main_scene(scene, blueprints_data, addon_prefs): assets_list_data = {} blueprint_instance_names_for_scene = blueprints_data.blueprint_instances_per_main_scene.get(scene.name, None) - # find all blueprints used in a scene - blueprints_in_scene = [] - if blueprint_instance_names_for_scene: # what are the blueprints used in this scene, inject those into the assets list component - children_per_blueprint = {} - for blueprint_name in blueprint_instance_names_for_scene: - blueprint = blueprints_data.blueprints_per_name.get(blueprint_name, None) - if blueprint: - children_per_blueprint[blueprint_name] = blueprint.nested_blueprints - blueprints_in_scene += blueprint.nested_blueprints - assets_list_data["BlueprintsList"] = f"({json.dumps(dict(children_per_blueprint))})" - print(blueprint_instance_names_for_scene) - # add_scene_property(scene, assets_list_name, assets_list_data) - blueprint_assets_list = [] if blueprint_instance_names_for_scene: for blueprint_name in blueprint_instance_names_for_scene: @@ -67,25 +54,15 @@ def inject_blueprints_list_into_main_scene(scene, blueprints_data, addon_prefs): blueprint_assets_list.append({"name": blueprint.name, "path": blueprint_exported_path, "type": "MODEL", "internal": True}) - # fetch images/textures - # see https://blender.stackexchange.com/questions/139859/how-to-get-absolute-file-path-for-linked-texture-image - textures = [] - for ob in bpy.data.objects: - if ob.type == "MESH": - for mat_slot in ob.material_slots: - if mat_slot.material: - if mat_slot.material.node_tree: - textures.extend([x.image.filepath for x in mat_slot.material.node_tree.nodes if x.type=='TEX_IMAGE']) - print("textures", textures) + assets_list_name = f"assets_{scene.name}" - assets_list_data = {"blueprints": json.dumps(blueprint_assets_list), "sounds":[], "images":[]} scene["assets"] = json.dumps(blueprint_assets_list) print("blueprint assets", blueprint_assets_list) - add_scene_property(scene, assets_list_name, assets_list_data) + """add_scene_property(scene, assets_list_name, assets_list_data) for blueprint in blueprint_assets_list: - bpy.context.window_manager.assets_registry.add_asset(**blueprint) + bpy.context.window_manager.assets_registry.add_asset(**blueprint)""" def remove_blueprints_list_from_main_scene(scene): assets_list = None diff --git a/tools/blenvy/blueprints/ui.py b/tools/blenvy/blueprints/ui.py index 57082c1..ef967f3 100644 --- a/tools/blenvy/blueprints/ui.py +++ b/tools/blenvy/blueprints/ui.py @@ -1,6 +1,8 @@ import bpy import json +from ..assets.asset_helpers import get_user_assets + from ..assets.ui import draw_assets class GLTF_PT_auto_export_blueprints_list(bpy.types.Panel): @@ -33,11 +35,10 @@ class GLTF_PT_auto_export_blueprints_list(bpy.types.Panel): select_blueprint.blueprint_collection_name = blueprint.collection.name select_blueprint.blueprint_scene_name = blueprint.scene.name - assets = json.loads(blueprint.collection["assets"]) if "assets" in blueprint.collection else [] - - draw_assets(layout=layout, name=blueprint.name, title="Assets", asset_registry=asset_registry, assets=assets, target_type="BLUEPRINT", target_name=blueprint.name) + user_assets = get_user_assets(blueprint.collection) + draw_assets(layout=layout, name=blueprint.name, title="Assets", asset_registry=asset_registry, user_assets=user_assets, target_type="BLUEPRINT", target_name=blueprint.name) else: - assets = json.loads(blueprint.collection["assets"]) if "assets" in blueprint.collection else [] - draw_assets(layout=layout, name=blueprint.name, title="Assets", asset_registry=asset_registry, assets=assets, target_type="BLUEPRINT", target_name=blueprint.name, editable=False) + assets = get_user_assets(blueprint.collection) + draw_assets(layout=layout, name=blueprint.name, title="Assets", asset_registry=asset_registry, user_assets=user_assets, target_type="BLUEPRINT", target_name=blueprint.name, editable=False) row.label(text="External") diff --git a/tools/blenvy/gltf_auto_export/auto_export/export_main_scenes.py b/tools/blenvy/gltf_auto_export/auto_export/export_main_scenes.py index 2da7fb2..e4b24c6 100644 --- a/tools/blenvy/gltf_auto_export/auto_export/export_main_scenes.py +++ b/tools/blenvy/gltf_auto_export/auto_export/export_main_scenes.py @@ -29,7 +29,7 @@ def export_main_scene(scene, blend_file_path, addon_prefs, blueprints_data): if export_blueprints : gltf_output_path = os.path.join(export_levels_path_full, scene.name) - inject_blueprints_list_into_main_scene(scene, blueprints_data, addon_prefs) + #inject_blueprints_list_into_main_scene(scene, blueprints_data, addon_prefs) return if export_separate_dynamic_and_static_objects: #print("SPLIT STATIC AND DYNAMIC")