diff --git a/client/ayon_houdini/plugins/create/create_usd_componentbuilder.py b/client/ayon_houdini/plugins/create/create_usd_componentbuilder.py new file mode 100644 index 0000000000..67b21d1a49 --- /dev/null +++ b/client/ayon_houdini/plugins/create/create_usd_componentbuilder.py @@ -0,0 +1,70 @@ +import inspect + +from ayon_houdini.api import plugin +from ayon_core.pipeline import CreatedInstance, CreatorError + +import hou + + +class CreateUSDComponentBuilder(plugin.HoudiniCreator): + identifier = "io.ayon.creators.houdini.componentbuilder" + label = "USD Component Builder LOPs" + product_type = "usd" + icon = "cubes" + description = "Create USD from Component Builder LOPs" + + def get_detail_description(self): + return inspect.cleandoc(""" + Creates a USD publish from a Component Output LOP that is part of + a solaris component builder network. + + The created USD will contain the component builder LOPs and all its + dependencies inside the single product. + + To use it, select a Component Output LOP and click "Create" for + this creator. It will generate an instance for each selected + Component Output LOP. + """) + + def create(self, product_name, instance_data, pre_create_data): + nodes = hou.selectedNodes() + builders = [ + node for node in nodes + if node.type().nameWithCategory() == "Lop/componentoutput" + ] + for builder in builders: + self.create_for_instance_node(product_name, instance_data, builder) + + def create_for_instance_node( + self, product_name, instance_data, instance_node): + + try: + self.customize_node_look(instance_node) + instance_data["instance_node"] = instance_node.path() + instance_data["instance_id"] = instance_node.path() + instance_data["families"] = self.get_publish_families() + instance = CreatedInstance( + self.product_type, + product_name, + instance_data, + self) + self._add_instance_to_context(instance) + self.imprint(instance_node, instance.data_to_store()) + except hou.Error as er: + raise CreatorError("Creator error: {}".format(er)) from er + + # Lock any parameters in this list + to_lock = [ + # Lock some AYON attributes + "productType", + "id", + ] + self.lock_parameters(instance_node, to_lock) + + def get_network_categories(self): + # Do not expose via tab menu because currently it does not create any + # node, but only 'imprints' on an existing node. + return [] + + def get_publish_families(self): + return ["usd", "componentbuilder"] diff --git a/client/ayon_houdini/plugins/publish/collect_componentbuilder_lop.py b/client/ayon_houdini/plugins/publish/collect_componentbuilder_lop.py new file mode 100644 index 0000000000..b7a771f693 --- /dev/null +++ b/client/ayon_houdini/plugins/publish/collect_componentbuilder_lop.py @@ -0,0 +1,84 @@ +import os +from typing import List, Tuple +from pathlib import Path + +import pyblish.api + +from ayon_core.pipeline import AYONPyblishPluginMixin, PublishError +from ayon_houdini.api import plugin + +import hou +from pxr import Sdf, UsdUtils + + +def compute_all_dependencies( + filepath: str) -> Tuple[list[Sdf.Layer], list[str], list[str]]: + """Compute all dependencies for the given USD file.""" + # Only separated here for better type hints on returned values + return UsdUtils.ComputeAllDependencies(filepath) + + +class CollectComponentBuilderLOPs(plugin.HoudiniInstancePlugin, + AYONPyblishPluginMixin): + + # Run after `CollectResourcesPath` + order = pyblish.api.CollectorOrder + 0.496 + families = ["componentbuilder"] + label = "Collect Componentbuilder LOPs" + + def process(self, instance): + + node = hou.node(instance.data["instance_node"]) + + # Render the component builder LOPs + # TODO: Do we want this? or use existing frames? Usually a Collector + # should not 'extract' but in this case we need the resulting USD + # file. + node.cook(force=True) # required to clear existing errors + node.parm("execute").pressButton() + + errors = node.errors() + if errors: + for error in errors: + self.log.error(error) + raise PublishError(f"Failed to save to disk '{node.path()}'") + + # Define the main asset usd file + filepath = node.evalParm("lopoutput") + representations = instance.data.setdefault("representations", []) + representations.append({ + "name": "usd", + "ext": "usd", + "files": os.path.basename(filepath), + "stagingDir": os.path.dirname(filepath), + }) + + # Get all its files and dependencies + # TODO: Ignore any files that are not 'relative' to the USD file + layers, assets, unresolved_paths = compute_all_dependencies(filepath) + paths: List[str] = [] + paths.extend(layer.realPath for layer in layers) + paths.extend(assets) + + # Skip unresolved paths, but warn about them + for unresolved in unresolved_paths: + self.log.warning(f"Cannot be resolved: {unresolved}") + + self.log.debug(f"Collecting USD: {filepath}") + src_root_dir = os.path.dirname(filepath) + + # Used to compare resolved paths against + filepath = Path(filepath) + + # We keep the relative paths to the USD file + transfers = instance.data.setdefault("transfers", []) + publish_root = instance.data["publishDir"] + for src in paths: + + if filepath == Path(src): + continue + + relative_path = os.path.relpath(src, start=src_root_dir) + self.log.debug(f"Collected dependency: {relative_path}") + dest = os.path.normpath(os.path.join(publish_root, relative_path)) + transfers.append((src, dest)) diff --git a/client/ayon_houdini/plugins/publish/collect_output_node.py b/client/ayon_houdini/plugins/publish/collect_output_node.py index c7af43ec6a..e2ae431a3a 100644 --- a/client/ayon_houdini/plugins/publish/collect_output_node.py +++ b/client/ayon_houdini/plugins/publish/collect_output_node.py @@ -12,8 +12,6 @@ class CollectOutputSOPPath(plugin.HoudiniInstancePlugin): "camera", "vdbcache", "imagesequence", - "usd", - "usdrender", "redshiftproxy", "staticMesh", "model",