From c2167056720a93764eb3f4409350820fba476330 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 16:59:48 +0100 Subject: [PATCH 1/7] Blender: Implement USD extractor and loader --- client/ayon_core/hosts/blender/api/lib.py | 59 ++++++++++++++ client/ayon_core/hosts/blender/api/plugin.py | 3 +- .../blender/plugins/create/create_usd.py | 30 +++++++ .../hosts/blender/plugins/load/load_abc.py | 27 +++++-- .../plugins/publish/collect_instance.py | 2 +- .../blender/plugins/publish/extract_usd.py | 79 +++++++++++++++++++ 6 files changed, 190 insertions(+), 10 deletions(-) create mode 100644 client/ayon_core/hosts/blender/plugins/create/create_usd.py create mode 100644 client/ayon_core/hosts/blender/plugins/publish/extract_usd.py diff --git a/client/ayon_core/hosts/blender/api/lib.py b/client/ayon_core/hosts/blender/api/lib.py index 458a275b51..031a25e791 100644 --- a/client/ayon_core/hosts/blender/api/lib.py +++ b/client/ayon_core/hosts/blender/api/lib.py @@ -365,3 +365,62 @@ def maintained_time(): yield finally: bpy.context.scene.frame_current = current_time + + +def get_all_parents(obj): + """Get all recursive parents of object. + + Arguments: + obj (bpy.types.Object): Object to get all parents for. + + Returns: + List[bpy.types.Object]: All parents of object + + """ + result = [] + while True: + obj = obj.parent + if not obj: + break + result.append(obj) + return result + + +def get_highest_root(objects): + """Get the highest object (the least parents) among the objects. + + If multiple objects have the same amount of parents (or no parents) the + first object found in the input iterable will be returned. + + Note that this will *not* return objects outside of the input list, as + such it will not return the root of node from a child node. It is purely + intended to find the highest object among a list of objects. To instead + get the root from one object use, e.g. `get_all_parents(obj)[-1]` + + Arguments: + objects (List[bpy.types.Object]): Objects to find the highest root in. + + Returns: + Optional[bpy.types.Object]: First highest root found or None if no + `bpy.types.Object` found in input list. + + """ + included_objects = {obj.name_full for obj in objects} + num_parents_to_obj = {} + for obj in objects: + if isinstance(obj, bpy.types.Object): + parents = get_all_parents(obj) + # included parents + parents = [parent for parent in parents if + parent.name_full in included_objects] + if not parents: + # A node without parents must be a highest root + return obj + + num_parents_to_obj.setdefault(len(parents), obj) + + if not num_parents_to_obj: + return + + minimum_parent = min(num_parents_to_obj) + return num_parents_to_obj[minimum_parent] diff --git a/client/ayon_core/hosts/blender/api/plugin.py b/client/ayon_core/hosts/blender/api/plugin.py index 6c9bfb6569..383dd1e5c6 100644 --- a/client/ayon_core/hosts/blender/api/plugin.py +++ b/client/ayon_core/hosts/blender/api/plugin.py @@ -26,7 +26,8 @@ from .ops import ( ) from .lib import imprint -VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"] +VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx", + ".usd", ".usdc", ".usda"] def prepare_scene_name( diff --git a/client/ayon_core/hosts/blender/plugins/create/create_usd.py b/client/ayon_core/hosts/blender/plugins/create/create_usd.py new file mode 100644 index 0000000000..2c2d0c46c6 --- /dev/null +++ b/client/ayon_core/hosts/blender/plugins/create/create_usd.py @@ -0,0 +1,30 @@ +"""Create a USD Export.""" + +from ayon_core.hosts.blender.api import plugin, lib + + +class CreateUSD(plugin.BaseCreator): + """Create USD Export""" + + identifier = "io.openpype.creators.blender.usd" + name = "usdMain" + label = "USD" + product_type = "usd" + icon = "gears" + + def create( + self, product_name: str, instance_data: dict, pre_create_data: dict + ): + # Run parent create method + collection = super().create( + product_name, instance_data, pre_create_data + ) + + if pre_create_data.get("use_selection"): + objects = lib.get_selection() + for obj in objects: + collection.objects.link(obj) + if obj.type == 'EMPTY': + objects.extend(obj.children) + + return collection diff --git a/client/ayon_core/hosts/blender/plugins/load/load_abc.py b/client/ayon_core/hosts/blender/plugins/load/load_abc.py index 938ae6106b..877cf0ca49 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_abc.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_abc.py @@ -26,10 +26,11 @@ class CacheModelLoader(plugin.AssetLoader): Note: At least for now it only supports Alembic files. """ - product_types = {"model", "pointcache", "animation"} - representations = ["abc"] + product_types = {"model", "pointcache", "animation", "usd"} + representations = ["abc", "usd"] - label = "Load Alembic" + # TODO: Should USD loader be a separate loader instead? + label = "Load Alembic/USD" icon = "code-fork" color = "orange" @@ -53,10 +54,21 @@ class CacheModelLoader(plugin.AssetLoader): plugin.deselect_all() relative = bpy.context.preferences.filepaths.use_relative_paths - bpy.ops.wm.alembic_import( - filepath=libpath, - relative_path=relative - ) + + if any(libpath.lower().endswith(ext) + for ext in [".usd", ".usda", ".usdc"]): + # USD + bpy.ops.wm.usd_import( + filepath=libpath, + relative_path=relative + ) + + else: + # Alembic + bpy.ops.wm.alembic_import( + filepath=libpath, + relative_path=relative + ) imported = lib.get_selection() @@ -161,7 +173,6 @@ class CacheModelLoader(plugin.AssetLoader): self._link_objects(objects, asset_group, containers, asset_group) - product_type = context["product"]["productType"] asset_group[AVALON_PROPERTY] = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, diff --git a/client/ayon_core/hosts/blender/plugins/publish/collect_instance.py b/client/ayon_core/hosts/blender/plugins/publish/collect_instance.py index d47c69a270..314ffd368a 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/collect_instance.py +++ b/client/ayon_core/hosts/blender/plugins/publish/collect_instance.py @@ -12,7 +12,7 @@ class CollectBlenderInstanceData(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder hosts = ["blender"] families = ["model", "pointcache", "animation", "rig", "camera", "layout", - "blendScene"] + "blendScene", "usd"] label = "Collect Instance" def process(self, instance): diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py b/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py new file mode 100644 index 0000000000..74d0756133 --- /dev/null +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py @@ -0,0 +1,79 @@ +import os + +import bpy + +from ayon_core.pipeline import publish +from ayon_core.hosts.blender.api import plugin, lib + + +class ExtractUSD(publish.Extractor): + """Extract as USD.""" + + label = "Extract USD" + hosts = ["blender"] + families = ["usd"] + + def process(self, instance): + + # Ignore runtime instances (e.g. USD layers) + # TODO: This is better done via more specific `families` + if not instance.data.get("transientData", {}).get("instance_node"): + return + + # Define extract output file path + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.usd" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.debug("Performing extraction..") + + # Select all members to "export selected" + plugin.deselect_all() + + selected = [] + for obj in instance: + if isinstance(obj, bpy.types.Object): + obj.select_set(True) + selected.append(obj) + + root = lib.get_highest_root(objects=instance[:]) + if not root: + instance_node = instance.data["transientData"]["instance_node"] + raise publish.KnownPublishError( + f"No root object found in instance: {instance_node.name}" + ) + self.log.debug(f"Exporting using active root: {root.name}") + + context = plugin.create_blender_context( + active=root, selected=selected) + + # Export USD + bpy.ops.wm.usd_export( + context, + filepath=filepath, + selected_objects_only=True, + export_textures=False, + relative_paths=False, + export_animation=False, + export_hair=False, + export_uvmaps=True, + # TODO: add for new version of Blender (4+?) + # export_mesh_colors=True, + export_normals=True, + export_materials=True, + use_instancing=True + ) + + plugin.deselect_all() + + # Add representation + representation = { + 'name': 'usd', + 'ext': 'usd', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data.setdefault("representations", []).append(representation) + self.log.debug("Extracted instance '%s' to: %s", + instance.name, representation) From f856f5237c2735eb04663e857ca81677a517f0cd Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 17:07:14 +0100 Subject: [PATCH 2/7] Fix export for recent blender versions --- .../blender/plugins/publish/extract_usd.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py b/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py index 74d0756133..70092ded7b 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py @@ -49,21 +49,21 @@ class ExtractUSD(publish.Extractor): active=root, selected=selected) # Export USD - bpy.ops.wm.usd_export( - context, - filepath=filepath, - selected_objects_only=True, - export_textures=False, - relative_paths=False, - export_animation=False, - export_hair=False, - export_uvmaps=True, - # TODO: add for new version of Blender (4+?) - # export_mesh_colors=True, - export_normals=True, - export_materials=True, - use_instancing=True - ) + with bpy.context.temp_override(**context): + bpy.ops.wm.usd_export( + filepath=filepath, + selected_objects_only=True, + export_textures=False, + relative_paths=False, + export_animation=False, + export_hair=False, + export_uvmaps=True, + # TODO: add for new version of Blender (4+?) + # export_mesh_colors=True, + export_normals=True, + export_materials=True, + use_instancing=True + ) plugin.deselect_all() From 5660ed58d39ff4755b2db97511c65aaabfe147f7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Mar 2024 17:08:35 +0100 Subject: [PATCH 3/7] Fix refactor --- client/ayon_core/hosts/blender/plugins/load/load_abc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/hosts/blender/plugins/load/load_abc.py b/client/ayon_core/hosts/blender/plugins/load/load_abc.py index 877cf0ca49..2fec4cc78b 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_abc.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_abc.py @@ -173,6 +173,7 @@ class CacheModelLoader(plugin.AssetLoader): self._link_objects(objects, asset_group, containers, asset_group) + product_type = context["product"]["productType"] asset_group[AVALON_PROPERTY] = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, From 78da85398d2ffb86673acf0f88dba94c54ad140a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 4 Apr 2024 15:39:04 +0200 Subject: [PATCH 4/7] Refactor filename since it's now not only Alembic but also USD --- .../hosts/blender/plugins/load/{load_abc.py => load_cache.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename client/ayon_core/hosts/blender/plugins/load/{load_abc.py => load_cache.py} (100%) diff --git a/client/ayon_core/hosts/blender/plugins/load/load_abc.py b/client/ayon_core/hosts/blender/plugins/load/load_cache.py similarity index 100% rename from client/ayon_core/hosts/blender/plugins/load/load_abc.py rename to client/ayon_core/hosts/blender/plugins/load/load_cache.py From 6bb585c6a92b62f70fbfb2355ed45e3ff70f0d9a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 3 May 2024 20:31:40 +0200 Subject: [PATCH 5/7] Change label to `Load Cache` --- client/ayon_core/hosts/blender/plugins/load/load_cache.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/ayon_core/hosts/blender/plugins/load/load_cache.py b/client/ayon_core/hosts/blender/plugins/load/load_cache.py index 65d45e6fc4..30c847f89d 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_cache.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_cache.py @@ -29,8 +29,7 @@ class CacheModelLoader(plugin.AssetLoader): product_types = {"model", "pointcache", "animation", "usd"} representations = {"abc", "usd"} - # TODO: Should USD loader be a separate loader instead? - label = "Load Alembic/USD" + label = "Load Cache" icon = "code-fork" color = "orange" From bd0509a2c64d9488e5c79dbf9ea27e1b885b064d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 3 May 2024 20:42:16 +0200 Subject: [PATCH 6/7] Allow publishing USD from `model` family and expose it in settings --- .../hosts/blender/plugins/publish/extract_usd.py | 11 +++++++++++ .../blender/server/settings/publish_plugins.py | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py b/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py index 70092ded7b..1d4fa3d7ac 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_usd.py @@ -77,3 +77,14 @@ class ExtractUSD(publish.Extractor): instance.data.setdefault("representations", []).append(representation) self.log.debug("Extracted instance '%s' to: %s", instance.name, representation) + + +class ExtractModelUSD(ExtractUSD): + """Extract model as USD.""" + + label = "Extract USD (Model)" + hosts = ["blender"] + families = ["model"] + + # Driven by settings + optional = True diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index e998d7b057..8db8c5be46 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -151,6 +151,10 @@ class PublishPluginsModel(BaseSettingsModel): default_factory=ExtractPlayblastModel, title="Extract Playblast" ) + ExtractModelUSD: ValidatePluginModel = SettingsField( + default_factory=ValidatePluginModel, + title="Extract Model USD" + ) DEFAULT_BLENDER_PUBLISH_SETTINGS = { @@ -348,5 +352,10 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { }, indent=4 ) + }, + "ExtractModelUSD": { + "enabled": True, + "optional": True, + "active": True } } From 8131b53983301b0d5f4e3a5a6a8fd42eff0eedbb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 3 May 2024 20:42:41 +0200 Subject: [PATCH 7/7] Bump blender server addon version --- server_addon/blender/package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/blender/package.py b/server_addon/blender/package.py index 667076e533..d2c02a4909 100644 --- a/server_addon/blender/package.py +++ b/server_addon/blender/package.py @@ -1,3 +1,3 @@ name = "blender" title = "Blender" -version = "0.1.8" +version = "0.1.9"