From a7871f54f03d1100423e1414872d7b3f613c3e98 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 24 Sep 2021 19:05:46 +0200 Subject: [PATCH 1/7] hda publishing wip --- .../houdini/plugins/create/create_hda.py | 47 ++++++++++++++++ .../hosts/houdini/plugins/load/load_hda.py | 56 +++++++++++++++++++ .../plugins/publish/collect_active_state.py | 4 +- .../plugins/publish/collect_instances.py | 6 +- .../houdini/plugins/publish/extract_hda.py | 43 ++++++++++++++ .../plugins/publish/validate_bypass.py | 2 +- openpype/plugins/publish/integrate_new.py | 3 +- 7 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/create/create_hda.py create mode 100644 openpype/hosts/houdini/plugins/load/load_hda.py create mode 100644 openpype/hosts/houdini/plugins/publish/extract_hda.py diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py new file mode 100644 index 0000000000..d58e0c5e52 --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from openpype.hosts.houdini.api import plugin +from avalon.houdini import lib +import hou + + +class CreateHDA(plugin.Creator): + """Publish Houdini Digital Asset file.""" + + name = "hda" + label = "Houdini Digital Asset (Hda)" + family = "hda" + icon = "gears" + maintain_selection = False + + def __init__(self, *args, **kwargs): + super(CreateHDA, self).__init__(*args, **kwargs) + self.data.pop("active", None) + + def _process(self, instance): + + out = hou.node("/obj") + self.nodes = hou.selectedNodes() + + if (self.options or {}).get("useSelection") and self.nodes: + to_hda = self.nodes[0] + if len(self.nodes) > 1: + subnet = out.createNode( + "subnet", node_name="{}_subnet".format(self.name)) + to_hda = subnet + else: + subnet = out.createNode( + "subnet", node_name="{}_subnet".format(self.name)) + subnet.moveToGoodPosition() + to_hda = subnet + + hda_node = to_hda.createDigitalAsset( + name=self.name, + hda_file_name="$HIP/{}.hda".format(self.name) + ) + hda_node.setName(self.name) + hou.moveNodesTo(self.nodes, hda_node) + hda_node.layoutChildren() + + lib.imprint(hda_node, self.data) + + return hda_node diff --git a/openpype/hosts/houdini/plugins/load/load_hda.py b/openpype/hosts/houdini/plugins/load/load_hda.py new file mode 100644 index 0000000000..5e04d83e86 --- /dev/null +++ b/openpype/hosts/houdini/plugins/load/load_hda.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +from avalon import api + +from avalon.houdini import pipeline, lib + + +class HdaLoader(api.Loader): + """Load Houdini Digital Asset file.""" + + families = ["hda"] + label = "Load Hda" + representations = ["hda"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + + import os + import hou + + # Format file name, Houdini only wants forward slashes + file_path = os.path.normpath(self.fname) + file_path = file_path.replace("\\", "/") + + # Get the root node + obj = hou.node("/obj") + + # Create a unique name + counter = 1 + namespace = namespace or context["asset"]["name"] + formatted = "{}_{}".format(namespace, name) if namespace else name + node_name = "{0}_{1:03d}".format(formatted, counter) + + hou.hda.installFile(file_path) + print("installing {}".format(name)) + hda_node = obj.createNode(name, node_name) + + self[:] = [hda_node] + + return pipeline.containerise( + node_name, + namespace, + [hda_node], + context, + self.__class__.__name__, + suffix="", + ) + + def update(self, container, representation): + hda_node = container["node"] + hda_def = hda_node.type().definition() + + + def remove(self, container): + pass diff --git a/openpype/hosts/houdini/plugins/publish/collect_active_state.py b/openpype/hosts/houdini/plugins/publish/collect_active_state.py index 1193f0cd19..862d5720e1 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_active_state.py +++ b/openpype/hosts/houdini/plugins/publish/collect_active_state.py @@ -23,8 +23,10 @@ class CollectInstanceActiveState(pyblish.api.InstancePlugin): return # Check bypass state and reverse + active = True node = instance[0] - active = not node.isBypassed() + if hasattr(node, "isBypassed"): + active = not node.isBypassed() # Set instance active state instance.data.update( diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index 1b36526783..ac081ac297 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -31,6 +31,7 @@ class CollectInstances(pyblish.api.ContextPlugin): def process(self, context): nodes = hou.node("/out").children() + nodes += hou.node("/obj").children() # Include instances in USD stage only when it exists so it # remains backwards compatible with version before houdini 18 @@ -49,9 +50,12 @@ class CollectInstances(pyblish.api.ContextPlugin): has_family = node.evalParm("family") assert has_family, "'%s' is missing 'family'" % node.name() + self.log.info("processing {}".format(node)) + data = lib.read(node) # Check bypass state and reverse - data.update({"active": not node.isBypassed()}) + if hasattr(node, "isBypassed"): + data.update({"active": not node.isBypassed()}) # temporarily translation of `active` to `publish` till issue has # been resolved, https://github.com/pyblish/pyblish-base/issues/307 diff --git a/openpype/hosts/houdini/plugins/publish/extract_hda.py b/openpype/hosts/houdini/plugins/publish/extract_hda.py new file mode 100644 index 0000000000..301dd4e297 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/extract_hda.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +import os + +from pprint import pformat + +import pyblish.api +import openpype.api + + +class ExtractHDA(openpype.api.Extractor): + + order = pyblish.api.ExtractorOrder + label = "Extract HDA" + hosts = ["houdini"] + families = ["hda"] + + def process(self, instance): + self.log.info(pformat(instance.data)) + hda_node = instance[0] + hda_def = hda_node.type().definition() + hda_options = hda_def.options() + hda_options.setSaveInitialParmsAndContents(True) + + next_version = instance.data["anatomyData"]["version"] + self.log.info("setting version: {}".format(next_version)) + hda_def.setVersion(str(next_version)) + hda_def.setOptions(hda_options) + hda_def.save(hda_def.libraryFilePath(), hda_node, hda_options) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + file = os.path.basename(hda_def.libraryFilePath()) + staging_dir = os.path.dirname(hda_def.libraryFilePath()) + self.log.info("Using HDA from {}".format(hda_def.libraryFilePath())) + + representation = { + 'name': 'hda', + 'ext': 'hda', + 'files': file, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) diff --git a/openpype/hosts/houdini/plugins/publish/validate_bypass.py b/openpype/hosts/houdini/plugins/publish/validate_bypass.py index 79c67c3008..fc4e18f701 100644 --- a/openpype/hosts/houdini/plugins/publish/validate_bypass.py +++ b/openpype/hosts/houdini/plugins/publish/validate_bypass.py @@ -35,5 +35,5 @@ class ValidateBypassed(pyblish.api.InstancePlugin): def get_invalid(cls, instance): rop = instance[0] - if rop.isBypassed(): + if hasattr(rop, "isBypassed") and rop.isBypassed(): return [rop] diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 3bff3ff79c..e9f9e87d52 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -98,7 +98,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "camerarig", "redshiftproxy", "effect", - "xgen" + "xgen", + "hda" ] exclude_families = ["clip"] db_representation_context_keys = [ From 2589ce46711fbf19d4c3c9a56fe8c10cad01ef01 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Sep 2021 14:09:02 +0200 Subject: [PATCH 2/7] hda updating --- .../hosts/houdini/plugins/create/create_hda.py | 3 +++ openpype/hosts/houdini/plugins/load/load_hda.py | 16 +++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index d58e0c5e52..05307d4c56 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -41,6 +41,9 @@ class CreateHDA(plugin.Creator): hda_node.setName(self.name) hou.moveNodesTo(self.nodes, hda_node) hda_node.layoutChildren() + # delete node created by Avalon in /out + # this needs to be addressed in future Houdini workflow refactor. + hou.node("/out/{}".format(self.name)).destroy() lib.imprint(hda_node, self.data) diff --git a/openpype/hosts/houdini/plugins/load/load_hda.py b/openpype/hosts/houdini/plugins/load/load_hda.py index 5e04d83e86..f923b699d2 100644 --- a/openpype/hosts/houdini/plugins/load/load_hda.py +++ b/openpype/hosts/houdini/plugins/load/load_hda.py @@ -15,7 +15,6 @@ class HdaLoader(api.Loader): color = "orange" def load(self, context, name=None, namespace=None, data=None): - import os import hou @@ -33,7 +32,6 @@ class HdaLoader(api.Loader): node_name = "{0}_{1:03d}".format(formatted, counter) hou.hda.installFile(file_path) - print("installing {}".format(name)) hda_node = obj.createNode(name, node_name) self[:] = [hda_node] @@ -48,9 +46,17 @@ class HdaLoader(api.Loader): ) def update(self, container, representation): - hda_node = container["node"] - hda_def = hda_node.type().definition() + import hou + hda_node = container["node"] + file_path = api.get_representation_path(representation) + file_path = file_path.replace("\\", "/") + hou.hda.installFile(file_path) + defs = hda_node.type().allInstalledDefinitions() + def_paths = [d.libraryFilePath() for d in defs] + new = def_paths.index(file_path) + defs[new].setIsPreferred(True) def remove(self, container): - pass + node = container["node"] + node.destroy() From b56623ff2d45e991d2e14cc63bda4397e4821b59 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 27 Sep 2021 14:10:37 +0200 Subject: [PATCH 3/7] remove unused import --- openpype/hosts/houdini/plugins/load/load_hda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/load/load_hda.py b/openpype/hosts/houdini/plugins/load/load_hda.py index f923b699d2..6610d5e513 100644 --- a/openpype/hosts/houdini/plugins/load/load_hda.py +++ b/openpype/hosts/houdini/plugins/load/load_hda.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from avalon import api -from avalon.houdini import pipeline, lib +from avalon.houdini import pipeline class HdaLoader(api.Loader): From 4446c535448b4d01a87dcb1cfaddd1dd36004720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 6 Oct 2021 16:26:21 +0200 Subject: [PATCH 4/7] add parent to qt windows --- .../hosts/houdini/startup/MainMenuCommon.xml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index 76585085e2..cb73d2643f 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -7,24 +7,30 @@ @@ -45,7 +51,8 @@ publish.show(parent) From 30e431d85e83c8924f8a373c814d20b9ecc42144 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 8 Oct 2021 16:08:15 +0200 Subject: [PATCH 5/7] check existing subset wip --- .../houdini/plugins/create/create_hda.py | 58 ++++++++++++++++--- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index 05307d4c56..775c51166a 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from openpype.hosts.houdini.api import plugin from avalon.houdini import lib +from avalon import io import hou @@ -17,34 +18,75 @@ class CreateHDA(plugin.Creator): super(CreateHDA, self).__init__(*args, **kwargs) self.data.pop("active", None) + def _check_existing(self, subset_name): + # type: (str) -> bool + """Check if existing subset name versions already exists.""" + # Get all subsets of the current asset + subset_docs = io.find( + { + "type": "subset", + "parent": self.data["asset"] + }, + {"name": 1} + ) + existing_subset_names = set(subset_docs.distinct("name")) + existing_subset_names_low = { + _name.lower() for _name in existing_subset_names + } + return subset_name.lower() in existing_subset_names_low + def _process(self, instance): + # get selected nodes out = hou.node("/obj") self.nodes = hou.selectedNodes() if (self.options or {}).get("useSelection") and self.nodes: + # if we have `use selection` enabled and we have some + # selected nodes ... to_hda = self.nodes[0] if len(self.nodes) > 1: + # if there is more then one node, create subnet first subnet = out.createNode( "subnet", node_name="{}_subnet".format(self.name)) to_hda = subnet else: + # in case of no selection, just create subnet node subnet = out.createNode( "subnet", node_name="{}_subnet".format(self.name)) subnet.moveToGoodPosition() to_hda = subnet - hda_node = to_hda.createDigitalAsset( - name=self.name, - hda_file_name="$HIP/{}.hda".format(self.name) - ) + if not to_hda.type().definition(): + # if node type has not its definition, it is not user + # created hda. We test if hda can be created from the node. + + if not to_hda.canCreateDigitalAsset(): + raise Exception( + "cannot create hda from node {}".format(to_hda)) + + hda_node = to_hda.createDigitalAsset( + name=self.name, + hda_file_name="$HIP/{}.hda".format(self.name) + ) + hou.moveNodesTo(self.nodes, hda_node) + hda_node.layoutChildren() + else: + hda_node = to_hda + hda_node.setName(self.name) - hou.moveNodesTo(self.nodes, hda_node) - hda_node.layoutChildren() + # delete node created by Avalon in /out # this needs to be addressed in future Houdini workflow refactor. + hou.node("/out/{}".format(self.name)).destroy() - lib.imprint(hda_node, self.data) + try: + lib.imprint(hda_node, self.data) + except hou.OperationFailed as e: + raise plugin.OpenPypeCreatorError( + ("Cannot set metadata on asset. Might be that it already is " + "OpenPype asset.") + ) - return hda_node + return hda_node \ No newline at end of file From 9065204e95b964e76b4ea1beebca55933eab7549 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 15 Oct 2021 01:32:20 +0200 Subject: [PATCH 6/7] subset check and documentation --- openpype/hosts/houdini/api/plugin.py | 3 ++- .../houdini/plugins/create/create_hda.py | 24 ++++++++++-------- website/docs/artist_hosts_houdini.md | 25 +++++++++++++++++++ 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index efdaa60084..63d9bba470 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Houdini specific Avalon/Pyblish plugin definitions.""" import sys +from avalon.api import CreatorError from avalon import houdini import six @@ -8,7 +9,7 @@ import hou from openpype.api import PypeCreatorMixin -class OpenPypeCreatorError(Exception): +class OpenPypeCreatorError(CreatorError): pass diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index 775c51166a..63d235d235 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -22,12 +22,13 @@ class CreateHDA(plugin.Creator): # type: (str) -> bool """Check if existing subset name versions already exists.""" # Get all subsets of the current asset + asset_id = io.find_one({"name": self.data["asset"], "type": "asset"}, + projection={"_id": True})['_id'] subset_docs = io.find( { "type": "subset", - "parent": self.data["asset"] - }, - {"name": 1} + "parent": asset_id + }, {"name": 1} ) existing_subset_names = set(subset_docs.distinct("name")) existing_subset_names_low = { @@ -36,7 +37,7 @@ class CreateHDA(plugin.Creator): return subset_name.lower() in existing_subset_names_low def _process(self, instance): - + subset_name = self.data["subset"] # get selected nodes out = hou.node("/obj") self.nodes = hou.selectedNodes() @@ -60,26 +61,29 @@ class CreateHDA(plugin.Creator): if not to_hda.type().definition(): # if node type has not its definition, it is not user # created hda. We test if hda can be created from the node. - if not to_hda.canCreateDigitalAsset(): raise Exception( "cannot create hda from node {}".format(to_hda)) hda_node = to_hda.createDigitalAsset( - name=self.name, - hda_file_name="$HIP/{}.hda".format(self.name) + name=subset_name, + hda_file_name="$HIP/{}.hda".format(subset_name) ) hou.moveNodesTo(self.nodes, hda_node) hda_node.layoutChildren() else: + if self._check_existing(subset_name): + raise plugin.OpenPypeCreatorError( + ("subset {} is already published with different HDA" + "definition.").format(subset_name)) hda_node = to_hda - hda_node.setName(self.name) + hda_node.setName(subset_name) # delete node created by Avalon in /out # this needs to be addressed in future Houdini workflow refactor. - hou.node("/out/{}".format(self.name)).destroy() + hou.node("/out/{}".format(subset_name)).destroy() try: lib.imprint(hda_node, self.data) @@ -89,4 +93,4 @@ class CreateHDA(plugin.Creator): "OpenPype asset.") ) - return hda_node \ No newline at end of file + return hda_node diff --git a/website/docs/artist_hosts_houdini.md b/website/docs/artist_hosts_houdini.md index d2aadf05cb..bd422b046e 100644 --- a/website/docs/artist_hosts_houdini.md +++ b/website/docs/artist_hosts_houdini.md @@ -76,3 +76,28 @@ I've selected `vdb1` and went **OpenPype -> Create** and selected **VDB Cache**. geometry ROP in `/out` and sets its paths to output vdb files. During the publishing process whole dops are cooked. +## Publishing Houdini Digital Assets (HDA) + +You can publish most of the nodes in Houdini as hda for easy interchange of data between Houdini instances or even +other DCCs with Houdini Engine. + +## Creating HDA + +Simply select nodes you want to include in hda and go **OpenPype -> Create** and select **Houdini digital asset (hda)**. +You can even use already existing hda as a selected node, and it will be published (see below for limitation). + +:::caution HDA Workflow limitations +As long as the hda is of same type - it is created from different nodes but using the same (subset) name, everything +is ok. But once you've published version of hda subset, you cannot change its type. For example, you create hda **Foo** +from *Cube* and *Sphere* - it will create hda subset named `hdaFoo` with the same type. You publish it as version 1. +Then you create version 2 with added *Torus*. Then you create version 3 from the scratch from completely different nodes, +but still using resulting subset name `hdaFoo`. Everything still works as expected. But then you use already +existing hda as a base, for example from different artist. Its type cannot be changed from what it was and so even if +it is named `hdaFoo` it has different type. It could be published, but you would never load it and retain ability to +switch versions between different hda types. +::: + +## Loading HDA + +When you load hda, it will install its type in your hip file and add published version as its definition file. When +you switch version via Scene Manager, it will add its definition and set it as preferred. \ No newline at end of file From ecd485309e6e08cf4c090b46e4378464c5e9ce6c Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 15 Oct 2021 14:31:04 +0200 Subject: [PATCH 7/7] fix hound --- openpype/hosts/houdini/plugins/create/create_hda.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py index 63d235d235..2af1e4a257 100644 --- a/openpype/hosts/houdini/plugins/create/create_hda.py +++ b/openpype/hosts/houdini/plugins/create/create_hda.py @@ -74,8 +74,8 @@ class CreateHDA(plugin.Creator): else: if self._check_existing(subset_name): raise plugin.OpenPypeCreatorError( - ("subset {} is already published with different HDA" - "definition.").format(subset_name)) + ("subset {} is already published with different HDA" + "definition.").format(subset_name)) hda_node = to_hda hda_node.setName(subset_name) @@ -87,7 +87,7 @@ class CreateHDA(plugin.Creator): try: lib.imprint(hda_node, self.data) - except hou.OperationFailed as e: + except hou.OperationFailed: raise plugin.OpenPypeCreatorError( ("Cannot set metadata on asset. Might be that it already is " "OpenPype asset.")