diff --git a/.github/workflows/assign_pr_to_project.yml b/.github/workflows/assign_pr_to_project.yml new file mode 100644 index 0000000000..e61d281c2a --- /dev/null +++ b/.github/workflows/assign_pr_to_project.yml @@ -0,0 +1,48 @@ +name: 🔸Auto assign pr +on: + workflow_dispatch: + inputs: + pr_number: + type: string + description: "Run workflow for this PR number" + required: true + project_id: + type: string + description: "Github Project Number" + required: true + default: "16" + pull_request: + types: + - opened + +env: + GH_TOKEN: ${{ github.token }} + +jobs: + get-pr-repo: + runs-on: ubuntu-latest + outputs: + pr_repo_name: ${{ steps.get-repo-name.outputs.repo_name || github.event.pull_request.head.repo.full_name }} + + # INFO `github.event.pull_request.head.repo.full_name` is not available on manual triggered (dispatched) runs + steps: + - name: Get PR repo name + if: ${{ github.event_name == 'workflow_dispatch' }} + id: get-repo-name + run: | + repo_name=$(gh pr view ${{ inputs.pr_number }} --json headRepository,headRepositoryOwner --repo ${{ github.repository }} | jq -r '.headRepositoryOwner.login + "/" + .headRepository.name') + echo "repo_name=$repo_name" >> $GITHUB_OUTPUT + + auto-assign-pr: + needs: + - get-pr-repo + if: ${{ needs.get-pr-repo.outputs.pr_repo_name == github.repository }} + uses: ynput/ops-repo-automation/.github/workflows/pr_to_project.yml@main + with: + repo: "${{ github.repository }}" + project_id: ${{ inputs.project_id != '' && fromJSON(inputs.project_id) || 16 }} + pull_request_number: ${{ github.event.pull_request.number || fromJSON(inputs.pr_number) }} + secrets: + # INFO fallback to default `github.token` is required for PRs from forks + # INFO organization secrets won't be available to forks + token: ${{ secrets.YNPUT_BOT_TOKEN || github.token}} diff --git a/.github/workflows/validate_pr_labels.yml b/.github/workflows/validate_pr_labels.yml new file mode 100644 index 0000000000..f25e263c98 --- /dev/null +++ b/.github/workflows/validate_pr_labels.yml @@ -0,0 +1,18 @@ +name: 🔎 Validate PR Labels +on: + pull_request: + types: + - opened + - edited + - labeled + - unlabeled + +jobs: + validate-type-label: + uses: ynput/ops-repo-automation/.github/workflows/validate_pr_labels.yml@main + with: + repo: "${{ github.repository }}" + pull_request_number: ${{ github.event.pull_request.number }} + query_prefix: "type: " + secrets: + token: ${{ secrets.YNPUT_BOT_TOKEN }} diff --git a/client/ayon_houdini/api/pipeline.py b/client/ayon_houdini/api/pipeline.py index 337e24c4a4..f0f4d3db99 100644 --- a/client/ayon_houdini/api/pipeline.py +++ b/client/ayon_houdini/api/pipeline.py @@ -243,6 +243,7 @@ def containerise(name, "namespace": namespace, "loader": str(loader), "representation": context["representation"]["id"], + "project_name": context["project"]["name"] } lib.imprint(container, data) @@ -285,6 +286,13 @@ def parse_container(container): pass data[name] = value + # Support project name in container as optional attribute + for name in ["project_name"]: + parm = container.parm(name) + if not parm: + continue + data[name] = parm.eval() + # Backwards compatibility pre-schemas for containers data["schema"] = data.get("schema", "openpype:container-1.0") diff --git a/client/ayon_houdini/plugins/create/create_alembic_camera.py b/client/ayon_houdini/plugins/create/create_alembic_camera.py index 5593452d77..fee86e91f1 100644 --- a/client/ayon_houdini/plugins/create/create_alembic_camera.py +++ b/client/ayon_houdini/plugins/create/create_alembic_camera.py @@ -2,6 +2,7 @@ """Creator plugin for creating alembic camera products.""" from ayon_houdini.api import plugin from ayon_core.pipeline import CreatorError +from ayon_core.lib import EnumDef import hou @@ -14,10 +15,16 @@ class CreateAlembicCamera(plugin.HoudiniCreator): product_type = "camera" icon = "camera" + # Default render target + render_target = "local" + def create(self, product_name, instance_data, pre_create_data): import hou instance_data.update({"node_type": "alembic"}) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["render_target"] = pre_create_data["render_target"] instance = super(CreateAlembicCamera, self).create( product_name, @@ -54,3 +61,21 @@ def get_network_categories(self): hou.ropNodeTypeCategory(), hou.objNodeTypeCategory() ] + + def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + + return [ + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + return attrs + self.get_instance_attr_defs() diff --git a/client/ayon_houdini/plugins/create/create_arnold_ass.py b/client/ayon_houdini/plugins/create/create_arnold_ass.py index ec7ba508e0..9adb9a2d28 100644 --- a/client/ayon_houdini/plugins/create/create_arnold_ass.py +++ b/client/ayon_houdini/plugins/create/create_arnold_ass.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating Arnold ASS files.""" from ayon_houdini.api import plugin -from ayon_core.lib import BoolDef +from ayon_core.lib import EnumDef class CreateArnoldAss(plugin.HoudiniCreator): @@ -17,13 +17,16 @@ class CreateArnoldAss(plugin.HoudiniCreator): # will override it by the value in the project settings ext = ".ass" + # Default render target + render_target = "local" + def create(self, product_name, instance_data, pre_create_data): import hou instance_data.update({"node_type": "arnold"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) - creator_attributes["farm"] = pre_create_data["farm"] + creator_attributes["render_target"] = pre_create_data["render_target"] instance = super(CreateArnoldAss, self).create( product_name, @@ -57,10 +60,17 @@ def create(self, product_name, instance_data, pre_create_data): self.lock_parameters(instance_node, to_lock) def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + return [ - BoolDef("farm", - label="Submitting to Farm", - default=False) + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) ] def get_pre_create_attr_defs(self): diff --git a/client/ayon_houdini/plugins/create/create_bgeo.py b/client/ayon_houdini/plugins/create/create_bgeo.py index be51eda411..884f0d92b5 100644 --- a/client/ayon_houdini/plugins/create/create_bgeo.py +++ b/client/ayon_houdini/plugins/create/create_bgeo.py @@ -3,7 +3,7 @@ from ayon_houdini.api import plugin from ayon_core.pipeline import CreatorError import hou -from ayon_core.lib import EnumDef, BoolDef +from ayon_core.lib import EnumDef class CreateBGEO(plugin.HoudiniCreator): @@ -13,6 +13,9 @@ class CreateBGEO(plugin.HoudiniCreator): product_type = "pointcache" icon = "gears" + # Default render target + render_target = "local" + def get_publish_families(self): return ["pointcache", "bgeo"] @@ -21,7 +24,7 @@ def create(self, product_name, instance_data, pre_create_data): instance_data.update({"node_type": "geometry"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) - creator_attributes["farm"] = pre_create_data["farm"] + creator_attributes["render_target"] = pre_create_data["render_target"] instance = super(CreateBGEO, self).create( product_name, @@ -63,10 +66,17 @@ def create(self, product_name, instance_data, pre_create_data): instance_node.setParms(parms) def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + return [ - BoolDef("farm", - label="Submitting to Farm", - default=False) + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) ] def get_pre_create_attr_defs(self): diff --git a/client/ayon_houdini/plugins/create/create_hda.py b/client/ayon_houdini/plugins/create/create_hda.py index 7613735f7b..0f411bb01f 100644 --- a/client/ayon_houdini/plugins/create/create_hda.py +++ b/client/ayon_houdini/plugins/create/create_hda.py @@ -141,6 +141,12 @@ def set_tool_submenu(hda_def, # endregion +def promote_spare_parameters(node): + """Promote spare parameters to HDA node type definition""" + ptg = node.parmTemplateGroup() + node.type().definition().setParmTemplateGroup(ptg) + + class CreateHDA(plugin.HoudiniCreator): """Publish Houdini Digital Asset file.""" @@ -178,12 +184,27 @@ def create_instance_node( if pre_create_data is None: pre_create_data = {} + use_promote_spare_parameters = pre_create_data.get( + "use_promote_spare_parameters", True) + if self.selected_nodes: # if we have `use selection` enabled, and we have some # selected nodes ... - if self.selected_nodes[0].type().name() == "subnet": - to_hda = self.selected_nodes[0] + one_node_selected = len(self.selected_nodes) == 1 + first_selected_node = self.selected_nodes[0] + + # If only an HDA is selected, publish just that node as HDA. + if one_node_selected and first_selected_node.type().definition(): + to_hda = first_selected_node + use_promote_spare_parameters = False + + # If only a single subnet is selected, turn that into the HDA. + elif one_node_selected and first_selected_node.type().name() == "subnet": + to_hda = first_selected_node to_hda.setName("{}_subnet".format(node_name), unique_name=True) + + # Collapse all selected nodes into a subnet and turn that into + # the HDA. else: parent_node = self.selected_nodes[0].parent() subnet = parent_node.collapseIntoSubnet( @@ -202,6 +223,7 @@ def create_instance_node( to_hda = parent_node.createNode( "subnet", node_name="{}_subnet".format(node_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. @@ -222,8 +244,17 @@ def create_instance_node( name=type_name, description=node_name, hda_file_name="$HIP/{}.hda".format(node_name), - ignore_external_references=True + ignore_external_references=True, + min_num_inputs=0, + max_num_inputs=len(to_hda.inputs()) or 1, ) + + if use_promote_spare_parameters: + # Similar to Houdini default behavior, when enabled this will + # promote spare parameters to type properties on initial + # creation of the HDA. + promote_spare_parameters(hda_node) + hda_node.layoutChildren() elif self._check_existing(folder_path, node_name): raise CreatorError( @@ -279,6 +310,12 @@ def get_pre_create_attr_defs(self): "'AYON/project_name/your_HDA_name'", default=True, label="Use Project as menu entry"), + BoolDef("use_promote_spare_parameters", + tooltip="Similar to Houdini default behavior, when " + "enabled this will promote spare parameters to " + "type properties on initial creation of the HDA.", + default=True, + label="Promote Spare Parameters"), ] def get_dynamic_data( diff --git a/client/ayon_houdini/plugins/create/create_model.py b/client/ayon_houdini/plugins/create/create_model.py index 92eee306ae..8c5e5aa89a 100644 --- a/client/ayon_houdini/plugins/create/create_model.py +++ b/client/ayon_houdini/plugins/create/create_model.py @@ -12,7 +12,7 @@ """ from ayon_houdini.api import plugin -from ayon_core.lib import BoolDef +from ayon_core.lib import EnumDef import hou @@ -24,6 +24,9 @@ class CreateModel(plugin.HoudiniCreator): product_type = "model" icon = "cube" + # Default render target + render_target = "local" + def get_publish_families(self): return ["model", "abc"] @@ -31,7 +34,7 @@ def create(self, product_name, instance_data, pre_create_data): instance_data.update({"node_type": "alembic"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) - creator_attributes["farm"] = pre_create_data["farm"] + creator_attributes["render_target"] = pre_create_data["render_target"] instance = super(CreateModel, self).create( product_name, @@ -130,10 +133,17 @@ def get_obj_output(self, obj_node): key=lambda node: node.evalParm('outputidx')) def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + return [ - BoolDef("farm", - label="Submitting to Farm", - default=False) + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) ] def get_pre_create_attr_defs(self): diff --git a/client/ayon_houdini/plugins/create/create_pointcache.py b/client/ayon_houdini/plugins/create/create_pointcache.py index 969e2a65e7..3288c2655b 100644 --- a/client/ayon_houdini/plugins/create/create_pointcache.py +++ b/client/ayon_houdini/plugins/create/create_pointcache.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating pointcache alembics.""" from ayon_houdini.api import plugin -from ayon_core.lib import BoolDef +from ayon_core.lib import EnumDef import hou @@ -14,6 +14,9 @@ class CreatePointCache(plugin.HoudiniCreator): product_type = "pointcache" icon = "gears" + # Default render target + render_target = "local" + def get_publish_families(self): return ["pointcache", "abc"] @@ -21,7 +24,7 @@ def create(self, product_name, instance_data, pre_create_data): instance_data.update({"node_type": "alembic"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) - creator_attributes["farm"] = pre_create_data["farm"] + creator_attributes["render_target"] = pre_create_data["render_target"] instance = super(CreatePointCache, self).create( product_name, @@ -114,10 +117,17 @@ def get_obj_output(self, obj_node): key=lambda node: node.evalParm('outputidx')) def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + return [ - BoolDef("farm", - label="Submitting to Farm", - default=False) + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) ] def get_pre_create_attr_defs(self): diff --git a/client/ayon_houdini/plugins/create/create_redshift_proxy.py b/client/ayon_houdini/plugins/create/create_redshift_proxy.py index d9bfe6cdd2..12dc7fb520 100644 --- a/client/ayon_houdini/plugins/create/create_redshift_proxy.py +++ b/client/ayon_houdini/plugins/create/create_redshift_proxy.py @@ -2,7 +2,7 @@ """Creator plugin for creating Redshift proxies.""" from ayon_houdini.api import plugin import hou -from ayon_core.lib import BoolDef +from ayon_core.lib import EnumDef class CreateRedshiftProxy(plugin.HoudiniCreator): @@ -12,6 +12,9 @@ class CreateRedshiftProxy(plugin.HoudiniCreator): product_type = "redshiftproxy" icon = "magic" + # Default render target + render_target = "local" + def create(self, product_name, instance_data, pre_create_data): # Redshift provides a `Redshift_Proxy_Output` node type which shows @@ -24,7 +27,7 @@ def create(self, product_name, instance_data, pre_create_data): instance_data.update({"node_type": "Redshift_Proxy_Output"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) - creator_attributes["farm"] = pre_create_data["farm"] + creator_attributes["render_target"] = pre_create_data["render_target"] instance = super(CreateRedshiftProxy, self).create( product_name, @@ -53,10 +56,17 @@ def get_network_categories(self): ] def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + return [ - BoolDef("farm", - label="Submitting to Farm", - default=False) + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) ] def get_pre_create_attr_defs(self): diff --git a/client/ayon_houdini/plugins/create/create_review.py b/client/ayon_houdini/plugins/create/create_review.py index 601349e3ad..cf98294ab0 100644 --- a/client/ayon_houdini/plugins/create/create_review.py +++ b/client/ayon_houdini/plugins/create/create_review.py @@ -16,6 +16,9 @@ class CreateReview(plugin.HoudiniCreator): icon = "video-camera" review_color_space = "" node_type = "opengl" + + # Default render target + render_target = "local" # TODO: Publish families should reflect the node type, # such as `rop.flipbook` for flipbook nodes @@ -35,6 +38,9 @@ def apply_settings(self, project_settings): def create(self, product_name, instance_data, pre_create_data): self.node_type = pre_create_data.get("node_type") + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["render_target"] = pre_create_data["render_target"] instance_data.update({"node_type": self.node_type}) instance_data["imageFormat"] = pre_create_data.get("imageFormat") @@ -121,9 +127,23 @@ def create(self, product_name, instance_data, pre_create_data): to_lock = ["id", "productType"] self.lock_parameters(instance_node, to_lock) + + def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + return [ + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] + def get_pre_create_attr_defs(self): - attrs = super(CreateReview, self).get_pre_create_attr_defs() + attrs = super().get_pre_create_attr_defs() image_format_enum = [ "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", @@ -165,4 +185,4 @@ def get_pre_create_attr_defs(self): default=1.0, minimum=0.0001, decimals=3) - ] + ] + self.get_instance_attr_defs() diff --git a/client/ayon_houdini/plugins/create/create_staticmesh.py b/client/ayon_houdini/plugins/create/create_staticmesh.py index 17b646040c..6a9fef08ed 100644 --- a/client/ayon_houdini/plugins/create/create_staticmesh.py +++ b/client/ayon_houdini/plugins/create/create_staticmesh.py @@ -16,9 +16,15 @@ class CreateStaticMesh(plugin.HoudiniCreator): default_variants = ["Main"] + # Default render target + render_target = "local" + def create(self, product_name, instance_data, pre_create_data): instance_data.update({"node_type": "filmboxfbx"}) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["render_target"] = pre_create_data["render_target"] instance = super(CreateStaticMesh, self).create( product_name, @@ -57,11 +63,25 @@ def get_network_categories(self): hou.objNodeTypeCategory(), hou.sopNodeTypeCategory() ] + + def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + + return [ + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] def get_pre_create_attr_defs(self): """Add settings for users. """ - attrs = super(CreateStaticMesh, self).get_pre_create_attr_defs() + attrs = super().get_pre_create_attr_defs() createsubnetroot = BoolDef("createsubnetroot", tooltip="Create an extra root for the " "Export node when it's a " @@ -85,7 +105,9 @@ def get_pre_create_attr_defs(self): default=False, label="Convert Units") - return attrs + [createsubnetroot, vcformat, convert_units] + return attrs + [ + createsubnetroot, vcformat, convert_units + ] + self.get_instance_attr_defs() def get_dynamic_data( self, diff --git a/client/ayon_houdini/plugins/create/create_usd.py b/client/ayon_houdini/plugins/create/create_usd.py index 585ba667de..3a0e743bdf 100644 --- a/client/ayon_houdini/plugins/create/create_usd.py +++ b/client/ayon_houdini/plugins/create/create_usd.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating USDs.""" from ayon_houdini.api import plugin +from ayon_core.lib import EnumDef import hou @@ -14,9 +15,15 @@ class CreateUSD(plugin.HoudiniCreator): enabled = False description = "Create USD" + # Default render target + render_target = "local" + def create(self, product_name, instance_data, pre_create_data): instance_data.update({"node_type": "usd"}) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["render_target"] = pre_create_data["render_target"] instance = super(CreateUSD, self).create( product_name, @@ -52,3 +59,21 @@ def get_network_categories(self): def get_publish_families(self): return ["usd", "usdrop"] + + def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + + return [ + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + return attrs + self.get_instance_attr_defs() \ No newline at end of file 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/create/create_usd_look.py b/client/ayon_houdini/plugins/create/create_usd_look.py index 93aa486abb..2e621a0037 100644 --- a/client/ayon_houdini/plugins/create/create_usd_look.py +++ b/client/ayon_houdini/plugins/create/create_usd_look.py @@ -3,6 +3,7 @@ import inspect from ayon_houdini.api import plugin +from ayon_core.lib import EnumDef import hou @@ -17,9 +18,15 @@ class CreateUSDLook(plugin.HoudiniCreator): enabled = True description = "Create USD Look" + # Default render target + render_target = "local" + def create(self, product_name, instance_data, pre_create_data): instance_data.update({"node_type": "usd"}) + creator_attributes = instance_data.setdefault( + "creator_attributes", dict()) + creator_attributes["render_target"] = pre_create_data["render_target"] instance = super(CreateUSDLook, self).create( product_name, @@ -70,3 +77,21 @@ def get_network_categories(self): def get_publish_families(self): return ["usd", "look", "usdrop"] + + def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + + return [ + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] + + def get_pre_create_attr_defs(self): + attrs = super().get_pre_create_attr_defs() + return attrs + self.get_instance_attr_defs() \ No newline at end of file diff --git a/client/ayon_houdini/plugins/create/create_vbd_cache.py b/client/ayon_houdini/plugins/create/create_vbd_cache.py index 804865860e..3870f403b5 100644 --- a/client/ayon_houdini/plugins/create/create_vbd_cache.py +++ b/client/ayon_houdini/plugins/create/create_vbd_cache.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating VDB Caches.""" from ayon_houdini.api import plugin -from ayon_core.lib import BoolDef +from ayon_core.lib import EnumDef import hou @@ -14,13 +14,16 @@ class CreateVDBCache(plugin.HoudiniCreator): product_type = "vdbcache" icon = "cloud" + # Default render target + render_target = "local" + def create(self, product_name, instance_data, pre_create_data): import hou instance_data.update({"node_type": "geometry"}) creator_attributes = instance_data.setdefault( "creator_attributes", dict()) - creator_attributes["farm"] = pre_create_data["farm"] + creator_attributes["render_target"] = pre_create_data["render_target"] instance = super(CreateVDBCache, self).create( product_name, instance_data, @@ -109,10 +112,17 @@ def get_obj_output(self, obj_node): key=lambda node: node.evalParm('outputidx')) def get_instance_attr_defs(self): + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + return [ - BoolDef("farm", - label="Submitting to Farm", - default=False) + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) ] def get_pre_create_attr_defs(self): diff --git a/client/ayon_houdini/plugins/load/load_hda.py b/client/ayon_houdini/plugins/load/load_hda.py index fcf0e834f8..41ad6aef42 100644 --- a/client/ayon_houdini/plugins/load/load_hda.py +++ b/client/ayon_houdini/plugins/load/load_hda.py @@ -75,6 +75,16 @@ def update(self, container, context): "representation": repre_entity["id"] }) + # Move the Extra parameter folder to the back. + parm_group = hda_node.parmTemplateGroup() + # The name 'Extra' is a hard coded name in AYON. + parm_folder = parm_group.findFolder("Extra") + # Remove `Extra` AYON parameters + parm_group.remove(parm_folder.name()) + # Add `Extra` AYON parameters back + parm_group.append(parm_folder) + hda_node.setParmTemplateGroup(parm_group) + def remove(self, container): node = container["node"] parent = node.parent() diff --git a/client/ayon_houdini/plugins/publish/collect_cache_farm.py b/client/ayon_houdini/plugins/publish/collect_cache_farm.py index 68053cf8fb..53f13af366 100644 --- a/client/ayon_houdini/plugins/publish/collect_cache_farm.py +++ b/client/ayon_houdini/plugins/publish/collect_cache_farm.py @@ -10,15 +10,15 @@ class CollectFarmCacheFamily(plugin.HoudiniInstancePlugin): """Collect publish.hou family for caching on farm as early as possible.""" order = pyblish.api.CollectorOrder - 0.45 - families = ["ass", "pointcache", "redshiftproxy", "vdbcache", "model"] + families = ["ass", "pointcache", "redshiftproxy", + "vdbcache", "model", "staticMesh", + "rop.opengl", "usdrop", "camera"] targets = ["local", "remote"] label = "Collect Data for Cache" def process(self, instance): - creator_attribute = instance.data["creator_attributes"] - farm_enabled = creator_attribute["farm"] - instance.data["farm"] = farm_enabled - if not farm_enabled: + + if not instance.data["farm"]: self.log.debug("Caching on farm is disabled. " "Skipping farm collecting.") return 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_farm_instances.py b/client/ayon_houdini/plugins/publish/collect_farm_instances.py index f14ff65518..d2b39beece 100644 --- a/client/ayon_houdini/plugins/publish/collect_farm_instances.py +++ b/client/ayon_houdini/plugins/publish/collect_farm_instances.py @@ -5,13 +5,16 @@ class CollectFarmInstances(plugin.HoudiniInstancePlugin): """Collect instances for farm render.""" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder - 0.49 families = ["mantra_rop", "karma_rop", "redshift_rop", "arnold_rop", "vray_rop", - "usdrender"] + "usdrender", + "ass","pointcache", "redshiftproxy", + "vdbcache", "model", "staticMesh", + "rop.opengl", "usdrop", "camera"] targets = ["local", "remote"] label = "Collect farm instances" 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", diff --git a/client/ayon_houdini/plugins/publish/extract_hda.py b/client/ayon_houdini/plugins/publish/extract_hda.py index e4449d11f8..3d49c3332f 100644 --- a/client/ayon_houdini/plugins/publish/extract_hda.py +++ b/client/ayon_houdini/plugins/publish/extract_hda.py @@ -1,11 +1,25 @@ # -*- coding: utf-8 -*- import os -from pprint import pformat +import contextlib + import hou import pyblish.api + +from ayon_core.pipeline import PublishError from ayon_houdini.api import plugin +@contextlib.contextmanager +def revert_original_parm_template_group(node: "hou.OpNode"): + """Restore parm template group after the context""" + parm_group = node.parmTemplateGroup() + try: + yield + finally: + # Set the original + node.setParmTemplateGroup(parm_group) + + class ExtractHDA(plugin.HoudiniExtractorPlugin): order = pyblish.api.ExtractorOrder @@ -13,7 +27,7 @@ class ExtractHDA(plugin.HoudiniExtractorPlugin): families = ["hda"] def process(self, instance): - self.log.info(pformat(instance.data)) + hda_node = hou.node(instance.data.get("instance_node")) hda_def = hda_node.type().definition() hda_options = hda_def.options() @@ -23,7 +37,26 @@ def process(self, instance): 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) + + with revert_original_parm_template_group(hda_node): + # Remove our own custom parameters so that if the HDA definition + # has "Save Spare Parameters" enabled, we don't save our custom + # attributes + # Get our custom `Extra` AYON parameters + parm_group = hda_node.parmTemplateGroup() + # The name 'Extra' is a hard coded name in AYON. + parm_folder = parm_group.findFolder("Extra") + if not parm_folder: + raise PublishError( + f"Extra parm folder does not exist: {hda_node.path()}" + ) + + # Remove `Extra` AYON parameters + parm_group.remove(parm_folder.name()) + hda_node.setParmTemplateGroup(parm_group) + + # Save the HDA file + hda_def.save(hda_def.libraryFilePath(), hda_node, hda_options) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/client/ayon_houdini/plugins/publish/extract_rop.py b/client/ayon_houdini/plugins/publish/extract_rop.py index 74a5f52154..7e5c5798cd 100644 --- a/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/client/ayon_houdini/plugins/publish/extract_rop.py @@ -19,6 +19,7 @@ def process(self, instance: pyblish.api.Instance): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") return + creator_attribute = instance.data["creator_attributes"] files = instance.data["frames"] first_file = files[0] if isinstance(files, (list, tuple)) else files @@ -29,7 +30,11 @@ def process(self, instance: pyblish.api.Instance): ) ext = ext.lstrip(".") - self.render_rop(instance) + # Value `local` is used as a fallback if the `render_target` key is missing. + # This key might be absent because render targets are not yet implemented + # for all product types that use this plugin. + if creator_attribute.get("render_target", "local") == "local": + self.render_rop(instance) self.validate_expected_frames(instance) # In some cases representation name is not the the extension diff --git a/client/ayon_houdini/version.py b/client/ayon_houdini/version.py index bc4015c5c2..a08a5c36c6 100644 --- a/client/ayon_houdini/version.py +++ b/client/ayon_houdini/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'houdini' version.""" -__version__ = "0.3.17+dev" +__version__ = "0.4.1+dev" diff --git a/package.py b/package.py index f4663525bf..5304cac495 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "houdini" title = "Houdini" -version = "0.3.17+dev" +version = "0.4.1+dev" app_host_name = "houdini" client_dir = "ayon_houdini"