From c1995646d25d625ef575d2734bd709e10f5aeb50 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 May 2024 07:52:48 +0200 Subject: [PATCH 01/36] Fix workfile path validation for certain nodes that come with expression on string parms by default --- .../houdini/plugins/publish/validate_workfile_paths.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_workfile_paths.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_workfile_paths.py index 7984b7615c..cc10407df6 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_workfile_paths.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_workfile_paths.py @@ -69,6 +69,12 @@ def get_invalid(cls): if param.node().type().name() not in cls.node_types: continue + if param.keyframes(): + # Calling `.unexpandedString()` below fails if param has + # keyframes - so for now we will skip those params. These are + # e.g. present in `filecache` nodes. + continue + if any( v for v in cls.prohibited_vars if v in param.unexpandedString()): From 3dae818df602dc70fbed84086da9021fa958df9f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 May 2024 07:54:07 +0200 Subject: [PATCH 02/36] Include staging dir with the frames to be published --- .../hosts/houdini/plugins/publish/collect_frames.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_frames.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_frames.py index b38ebc6e2f..6cd6e7456a 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_frames.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_frames.py @@ -60,7 +60,10 @@ def process(self, instance): # todo: `frames` currently conflicts with "explicit frames" for a # for a custom frame list. So this should be refactored. - instance.data.update({"frames": result}) + instance.data.update({ + "frames": result, + "stagingDir": os.path.dirname(output) + }) @staticmethod def create_file_list(match, start_frame, end_frame): From 24dfc22c74fc85eebf098fee8d83a7a7fe5eb6af Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 May 2024 07:55:22 +0200 Subject: [PATCH 03/36] Add Dynamic (in-memory) runtime creator for Houdini + add Generic ROP creator for Houdini --- .../houdini/plugins/create/create_dynamic.py | 134 ++++ .../houdini/plugins/create/create_generic.py | 584 ++++++++++++++++++ .../houdini/plugins/publish/collect_frames.py | 2 +- .../publish/collect_houdini_batch_families.py | 112 ++++ .../houdini/plugins/publish/extract_rop.py | 44 ++ 5 files changed, 875 insertions(+), 1 deletion(-) create mode 100644 client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py create mode 100644 client/ayon_core/hosts/houdini/plugins/create/create_generic.py create mode 100644 client/ayon_core/hosts/houdini/plugins/publish/collect_houdini_batch_families.py create mode 100644 client/ayon_core/hosts/houdini/plugins/publish/extract_rop.py diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py b/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py new file mode 100644 index 0000000000..1ee8a6402d --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py @@ -0,0 +1,134 @@ +import os + +from ayon_core.pipeline.create import ( + Creator, + CreatedInstance, + get_product_name +) +from ayon_api import get_folder_by_path, get_task_by_name + + +def create_representation_data(files): + """Create representation data needed for `instance.data['representations']""" + first_file = files[0] + folder, filename = os.path.split(first_file) + ext = os.path.splitext(filename)[-1].strip(".") + return { + "name": ext, + "ext": ext, + "files": files if len(files) > 1 else first_file, + "stagingDir": folder, + } + + +class CreateRuntimeInstance(Creator): + """Create in-memory instances for dynamic PDG publishing of files. + + These instances do not persist and are meant for headless automated + publishing. The created instances are transient and will be gone on + resetting the `CreateContext` since they will not be recollected. + + """ + # TODO: This should be a global HIDDEN creator instead! + identifier = "io.openpype.creators.houdini.batch" + label = "Ingest" + product_type = "dynamic" # not actually used + icon = "gears" + + def create(self, product_name, instance_data, pre_create_data): + + # Unfortunately the Create Context will provide the product name + # even before the `create` call without listening to pre create data + # or the instance data - so instead we ignore the product name here + # and redefine it ourselves based on the `variant` in instance data + product_type = pre_create_data.get("product_type") or instance_data["product_type"] + project_name = self.create_context.project_name + folder_entity = get_folder_by_path(project_name, + instance_data["folderPath"]) + task_entity = get_task_by_name(project_name, + folder_id=folder_entity["id"], + task_name=instance_data["task"]) + product_name = self._get_product_name_dynamic( + self.create_context.project_name, + folder_entity=folder_entity, + task_entity=task_entity, + variant=instance_data["variant"], + product_type=product_type + ) + + custom_instance_data = pre_create_data.get("instance_data") + if custom_instance_data: + instance_data.update(custom_instance_data) + + # TODO: Add support for multiple representations + files = pre_create_data["files"] + representations = [create_representation_data(files)] + instance_data["representations"] = representations + + # We ingest it as a different product type then the creator's generic + # ingest product type. For example, we specify `pointcache` + instance = CreatedInstance( + product_type=product_type, + product_name=product_name, + data=instance_data, + creator=self + ) + self._add_instance_to_context(instance) + + return instance + + # Instances are all dynamic at run-time and cannot be persisted or + # re-collected + def collect_instances(self): + pass + + def update_instances(self, update_list): + pass + + def remove_instances(self, instances): + for instance in instances: + self._remove_instance_from_context(instance) + + # def get_publish_families(self): + # return [self.product_type] + + def _get_product_name_dynamic( + self, + project_name, + folder_entity, + task_entity, + variant, + product_type, + host_name=None, + instance=None + ): + """Implementation similar to `self.get_product_name` but taking + `productType` as argument instead of using the 'generic' product type + on the Creator itself.""" + if host_name is None: + host_name = self.create_context.host_name + + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + + dynamic_data = self.get_dynamic_data( + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance + ) + + return get_product_name( + project_name, + task_name, + task_type, + host_name, + product_type, + variant, + dynamic_data=dynamic_data, + project_settings=self.project_settings + ) \ No newline at end of file diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_generic.py b/client/ayon_core/hosts/houdini/plugins/create/create_generic.py new file mode 100644 index 0000000000..7d1389bb8e --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/create/create_generic.py @@ -0,0 +1,584 @@ +from ayon_core.hosts.houdini.api import plugin +from ayon_core.hosts.houdini.api.lib import ( + lsattr, read +) +from ayon_core.pipeline.create import ( + CreatedInstance, + get_product_name +) +from ayon_api import get_folder_by_path, get_task_by_name +from ayon_core.lib import ( + AbstractAttrDef, + BoolDef, + NumberDef, + EnumDef, + TextDef, + UISeparatorDef, + UILabelDef, + FileDef +) + +import hou +import json + + +def attribute_def_to_parm_template(attribute_def, key=None): + """AYON Attribute Definition to Houdini Parm Template. + + Arguments: + attribute_def (AbstractAttrDef): Attribute Definition. + + Returns: + hou.ParmTemplate: Parm Template matching the Attribute Definition. + """ + + if key is None: + key = attribute_def.key + + if isinstance(attribute_def, BoolDef): + return hou.ToggleParmTemplate(name=key, + label=attribute_def.label, + default_value=attribute_def.default, + help=attribute_def.tooltip) + elif isinstance(attribute_def, NumberDef): + if attribute_def.decimals == 0: + return hou.IntParmTemplate( + name=key, + label=attribute_def.label, + default_value=(attribute_def.default,), + help=attribute_def.tooltip, + min=attribute_def.minimum, + max=attribute_def.maximum, + num_components=1 + ) + else: + return hou.FloatParmTemplate( + name=key, + label=attribute_def.label, + default_value=(attribute_def.default,), + help=attribute_def.tooltip, + min=attribute_def.minimum, + max=attribute_def.maximum, + num_components=1 + ) + elif isinstance(attribute_def, EnumDef): + # TODO: Support multiselection EnumDef + # We only support enums that do not allow multiselection + # as a dedicated houdini parm. + if not attribute_def.multiselection: + labels = [item["label"] for item in attribute_def.items] + values = [item["value"] for item in attribute_def.items] + + print(attribute_def.default) + + return hou.StringParmTemplate( + name=key, + label=attribute_def.label, + default_value=(attribute_def.default,), + help=attribute_def.tooltip, + num_components=1, + menu_labels=labels, + menu_items=values, + menu_type=hou.menuType.Normal + ) + elif isinstance(attribute_def, TextDef): + return hou.StringParmTemplate( + name=key, + label=attribute_def.label, + default_value=(attribute_def.default,), + help=attribute_def.tooltip, + num_components=1 + ) + elif isinstance(attribute_def, UISeparatorDef): + return hou.SeparatorParmTemplate( + name=key, + label=attribute_def.label, + ) + elif isinstance(attribute_def, UILabelDef): + return hou.LabelParmTemplate( + name=key, + label=attribute_def.label, + ) + elif isinstance(attribute_def, FileDef): + # TODO: Support FileDef + pass + + # Unsupported attribute definition. We'll store value as JSON so just + # turn it into a string `JSON::` value + json_value = json.dumps(getattr(attribute_def, "default", None), + default=str) + return hou.StringParmTemplate( + name=key, + label=attribute_def.label, + default_value=f"JSON::{json_value}", + help=getattr(attribute_def, "tooltip", None), + num_components=1 + ) + + +def set_values(node: "hou.OpNode", values: dict): + """ + + Parms must exist on the node already. + + """ + for key, value in values.items(): + + parm = node.parm(key) + + try: + unexpanded_value = parm.unexpandedString() + if unexpanded_value == value: + # Allow matching expressions + continue + except hou.OperationFailed: + pass + + if parm.rawValue() == value: + continue + + if parm.eval() == value: + # Needs no change + continue + + # TODO: Set complex data types as `JSON:` + parm.set(value) + + +class CreateHoudiniGeneric(plugin.HoudiniCreator): + """Generic creator to ingest arbitrary products""" + + host_name = "houdini" + + identifier = "io.ayon.creators.houdini.publish" + label = "Generic" + product_type = "generic" + icon = "male" + description = "Make any ROP node publishable." + + # TODO: Override "create" to create the AYON publish attributes on the + # selected node so it becomes a publishable instance. + render_target = "local_no_render" + + def get_detail_description(self): + return ( + """Publish any ROP node.""" + ) + + def create(self, product_name, instance_data, pre_create_data): + + product_type = pre_create_data.get("productType", "pointcache") + instance_data["productType"] = product_type + + # Unfortunately the Create Context will provide the product name + # even before the `create` call without listening to pre create data + # or the instance data - so instead we ignore the product name here + # and redefine it ourselves based on the `variant` in instance data + project_name = self.create_context.project_name + folder_entity = get_folder_by_path(project_name, + instance_data["folderPath"]) + task_entity = get_task_by_name(project_name, + folder_id=folder_entity["id"], + task_name=instance_data["task"]) + product_name = self._get_product_name_dynamic( + self.create_context.project_name, + folder_entity=folder_entity, + task_entity=task_entity, + variant=instance_data["variant"], + product_type=product_type + ) + + for node in hou.selectedNodes(): + if node.parm("AYON_creator_identifier"): + # Continue if already existing attributes + continue + + # Enforce new style instance id otherwise first save may adjust + # this to the `AVALON_INSTANCE_ID` instead + instance_data["id"] = plugin.AYON_INSTANCE_ID + + instance_data["instance_node"] = node.path() + instance_data["instance_id"] = node.path() + created_instance = CreatedInstance( + product_type, product_name, instance_data.copy(), self + ) + + # Imprint on the selected node + self.imprint(created_instance, values=instance_data, update=False) + + # Add instance + self._add_instance_to_context(created_instance) + + def collect_instances(self): + for node in lsattr("AYON_id", plugin.AYON_INSTANCE_ID): + + creator_identifier_parm = node.parm("AYON_creator_identifier") + if not creator_identifier_parm: + continue + + # creator instance + creator_id = creator_identifier_parm.eval() + if creator_id != self.identifier: + continue + + # Read all attributes starting with `ayon_` + node_data = { + key.removeprefix("AYON_"): value + for key, value in read(node).items() + if key.startswith("AYON_") + } + + # Node paths are always the full node path since that is unique + # Because it's the node's path it's not written into attributes + # but explicitly collected + node_path = node.path() + node_data["instance_id"] = node_path + node_data["instance_node"] = node_path + node_data["families"] = self.get_publish_families() + + # Read creator and publish attributes + publish_attributes = {} + creator_attributes = {} + for key, value in dict(node_data).items(): + if key.startswith("publish_attributes_"): + if value == 0 or value == 1: + value = bool(value) + plugin_name, plugin_key = key[len("publish_attributes_"):].split("_", 1) + publish_attributes.setdefault(plugin_name, {})[plugin_key] = value + del node_data[key] # remove from original + elif key.startswith("creator_attributes_"): + creator_key = key[len("creator_attributes_"):] + creator_attributes[creator_key] = value + del node_data[key] # remove from original + + node_data["creator_attributes"] = creator_attributes + node_data["publish_attributes"] = publish_attributes + + created_instance = CreatedInstance.from_existing( + node_data, self + ) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + # Overridden to pass `created_instance` to `self.imprint` + for created_inst, changes in update_list: + new_values = { + key: changes[key].new_value + for key in changes.changed_keys + } + # Update parm templates and values + self.imprint( + created_inst, + new_values, + update=True + ) + + def get_product_name( + self, + project_name, + folder_entity, + task_entity, + variant, + host_name=None, + instance=None + ): + if instance is not None: + self.product_type = instance.data["productType"] + product_name = super(CreateHoudiniGeneric, self).get_product_name( + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance) + self.product_type = "generic" + return product_name + + else: + return "<-- defined on create -->" + + def create_attribute_def_parms(self, + node: "hou.OpNode", + created_instance: CreatedInstance): + # We imprint all the attributes into an AYON tab on the node in which + # we have a list folder called `attributes` in which we have + # - Instance Attributes + # - Creator Attributes + # - Publish Attributes + # With also a separate `advanced` section for specific attributes + parm_group = node.parmTemplateGroup() + + # Create default folder parm structure + ayon_folder = parm_group.findFolder("AYON") + if not ayon_folder: + ayon_folder = hou.FolderParmTemplate("folder", "AYON") + parm_group.addParmTemplate(ayon_folder) + + attributes_folder = parm_group.find("AYON_attributes") + if not attributes_folder: + attributes_folder = hou.FolderParmTemplate( + "AYON_attributes", + "Attributes", + folder_type=hou.folderType.Collapsible + ) + ayon_folder.addParmTemplate(attributes_folder) + + # Create Instance, Creator and Publish attributes folders + instance_attributes_folder = parm_group.find("AYON_instance_attributes") + if not instance_attributes_folder: + instance_attributes_folder = hou.FolderParmTemplate( + "AYON_instance_attributes", + "Instance Attributes", + folder_type=hou.folderType.Simple + ) + attributes_folder.addParmTemplate(instance_attributes_folder) + + creator_attributes_folder = parm_group.find("AYON_creator_attributes") + if not creator_attributes_folder: + creator_attributes_folder = hou.FolderParmTemplate( + "AYON_creator_attributes", + "Creator Attributes", + folder_type=hou.folderType.Simple + ) + attributes_folder.addParmTemplate(creator_attributes_folder) + + publish_attributes_folder = parm_group.find("AYON_publish_attributes") + if not publish_attributes_folder: + publish_attributes_folder = hou.FolderParmTemplate( + "AYON_publish_attributes", + "Publish Attributes", + folder_type=hou.folderType.Simple + ) + attributes_folder.addParmTemplate(publish_attributes_folder) + + # Create Advanced Folder + advanced_folder = parm_group.find("AYON_advanced") + if not advanced_folder: + advanced_folder = hou.FolderParmTemplate( + "AYON_advanced", + "Advanced", + folder_type=hou.folderType.Collapsible + ) + ayon_folder.addParmTemplate(advanced_folder) + + # Get the creator and publish attribute definitions so that we can + # generate matching Houdini parm types, including label, tooltips, etc. + creator_attribute_defs = created_instance.creator_attributes.attr_defs + for attr_def in creator_attribute_defs: + parm_template = attribute_def_to_parm_template( + attr_def, + key=f"AYON_creator_attributes_{attr_def.key}") + + name = parm_template.name() + existing = parm_group.find(name) + if existing: + # Remove from Parm Group - and also from the folder itself + # because that reference is not live anymore to the parm + # group itself so will still have the parm template + parm_group.remove(name) + creator_attributes_folder.setParmTemplates([ + t for t in creator_attributes_folder.parmTemplates() + if t.name() != name + ]) + creator_attributes_folder.addParmTemplate(parm_template) + + for plugin_name, plugin_attr_values in created_instance.publish_attributes.items(): + prefix = f"AYON_publish_attributes_{plugin_name}_" + for attr_def in plugin_attr_values.attr_defs: + parm_template = attribute_def_to_parm_template( + attr_def, + key=f"{prefix}{attr_def.key}" + ) + + name = parm_template.name() + existing = parm_group.find(name) + if existing: + # Remove from Parm Group - and also from the folder itself + # because that reference is not live anymore to the parm + # group itself so will still have the parm template + parm_group.remove(name) + publish_attributes_folder.setParmTemplates([ + t for t in publish_attributes_folder.parmTemplates() + if t.name() != name + ]) + publish_attributes_folder.addParmTemplate(parm_template) + + # TODO + # Add the Folder Path, Task Name, Product Type, Variant, Product Name + # and Active state in Instance attributes + for attribute in [ + hou.StringParmTemplate( + "AYON_folderPath", "Folder Path", + num_components=1, + default_value=("$AYON_FOLDER_PATH",) + ), + hou.StringParmTemplate( + "AYON_task", "Task Name", + num_components=1, + default_value=("$AYON_TASK_NAME",) + ), + hou.StringParmTemplate( + "AYON_productType", "Product Type", + num_components=1, + default_value=("pointcache",) + ), + hou.StringParmTemplate( + "AYON_variant", "Variant", + num_components=1, + default_value=(self.default_variant,) + ), + hou.StringParmTemplate( + "AYON_productName", "Product Name", + num_components=1, + default_value=('`chs("AYON_productType")``chs("AYON_variant")`',) + ), + hou.ToggleParmTemplate( + "AYON_active", "Active", + default_value=True + ) + ]: + if not parm_group.find(attribute.name()): + instance_attributes_folder.addParmTemplate(attribute) + + # Add the Creator Identifier and ID in advanced + for attribute in [ + hou.StringParmTemplate( + "AYON_id", "ID", + num_components=1, + default_value=(plugin.AYON_INSTANCE_ID,) + ), + hou.StringParmTemplate( + "AYON_creator_identifier", "Creator Identifier", + num_components=1, + default_value=(self.identifier,) + ), + ]: + if not parm_group.find(attribute.name()): + advanced_folder.addParmTemplate(attribute) + + # Ensure all folders are up-to-date if they had previously existed + # already + for folder in [ayon_folder, + attributes_folder, + instance_attributes_folder, + publish_attributes_folder, + creator_attributes_folder, + advanced_folder]: + if parm_group.find(folder.name()): + parm_group.replace(folder.name(), folder) # replace + node.setParmTemplateGroup(parm_group) + + def imprint(self, + created_instance: CreatedInstance, + values: dict, + update=False): + + # Do not ever write these into the node. + values.pop("instance_node", None) + values.pop("instance_id", None) + values.pop("families", None) + if not values: + return + + instance_node = hou.node(created_instance.get("instance_node")) + + # Update attribute definition parms + self.create_attribute_def_parms(instance_node, created_instance) + + # Creator attributes to parms + creator_attributes = values.pop("creator_attributes", {}) + parm_values = {} + for attr, value in creator_attributes.items(): + key = f"AYON_creator_attributes_{attr}" + parm_values[key] = value + + # Publish attributes to parms + publish_attributes = values.pop("publish_attributes", {}) + for plugin_name, plugin_attr_values in publish_attributes.items(): + for attr, value in plugin_attr_values.items(): + key = f"AYON_publish_attributes_{plugin_name}_{attr}" + parm_values[key] = value + + # The remainder attributes are stored without any prefixes + # Prefix all values with `AYON_` + parm_values.update( + {f"AYON_{key}": value for key, value in values.items()} + ) + + set_values(instance_node, parm_values) + + # TODO: Update defaults for Variant, Product Type, Product Name + # on the node so Houdini doesn't show them bold after save + + def get_publish_families(self): + return [self.product_type] + + def get_instance_attr_defs(self): + """get instance attribute definitions. + + Attributes defined in this method are exposed in + publish tab in the publisher UI. + """ + + render_target_items = { + "local": "Local machine rendering", + "local_no_render": "Use existing frames (local)", + "farm": "Farm Rendering", + } + + return [ + BoolDef("review", + label="Review", + tooltip="Mark as reviewable", + default=True), + EnumDef("render_target", + items=render_target_items, + label="Render target", + default=self.render_target) + ] + + def get_pre_create_attr_defs(self): + return [ + TextDef("productType", + label="Product Type", + tooltip="Publish product type", + default="pointcache") + ] + + def _get_product_name_dynamic( + self, + project_name, + folder_entity, + task_entity, + variant, + product_type, + host_name=None, + instance=None + ): + if host_name is None: + host_name = self.create_context.host_name + + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + + dynamic_data = self.get_dynamic_data( + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance + ) + + return get_product_name( + project_name, + task_name, + task_type, + host_name, + product_type, + variant, + dynamic_data=dynamic_data, + project_settings=self.project_settings + ) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_frames.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_frames.py index 6cd6e7456a..7a77750c23 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_frames.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_frames.py @@ -17,7 +17,7 @@ class CollectFrames(pyblish.api.InstancePlugin): label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", "mantraifd", "redshiftproxy", "review", - "pointcache"] + "pointcache", "rop"] def process(self, instance): diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_houdini_batch_families.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_houdini_batch_families.py new file mode 100644 index 0000000000..e09de96097 --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_houdini_batch_families.py @@ -0,0 +1,112 @@ +import os +import pyblish.api +import hou +from ayon_core.hosts.houdini.api import lib + + +class CollectNoProductTypeFamilyGeneric(pyblish.api.InstancePlugin): + """Collect data for caching to Deadline.""" + + order = pyblish.api.CollectorOrder - 0.49 + families = ["generic"] + hosts = ["houdini"] + targets = ["local", "remote"] + label = "Collect Data for Cache" + + def process(self, instance): + # Do not allow `productType` to creep into the pyblish families + # so that e.g. any regular plug-ins for `pointcache` or alike do + # not trigger. + instance.data["family"] = "generic" + # TODO: Do not add the dynamic 'rop' family in the collector? + instance.data["families"] = ["generic", "rop"] + self.log.info("Generic..") + + +class CollectNoProductTypeFamilyDynamic(pyblish.api.InstancePlugin): + """Collect data for caching to Deadline.""" + + order = pyblish.api.CollectorOrder - 0.49 + families = ["dynamic"] + hosts = ["houdini"] + targets = ["local", "remote"] + label = "Collect Data for Cache" + + def process(self, instance): + # Do not allow `productType` to creep into the pyblish families + # so that e.g. any regular plug-ins for `pointcache` or alike do + # not trigger. + instance.data["family"] = "dynamic" + instance.data["families"] = ["dynamic"] + + +# TODO: Implement for generic rop class +class CollectDataforCache(pyblish.api.InstancePlugin): + """Collect data for caching to Deadline.""" + + # Run after Collect Frames + order = pyblish.api.CollectorOrder + 0.11 + families = ["todo"] + hosts = ["houdini"] + 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: + self.log.debug("Caching on farm is disabled. " + "Skipping farm collecting.") + return + + # Why do we need this particular collector to collect the expected + # output files from a ROP node. Don't we have a dedicated collector + # for that yet? + # Collect expected files + ropnode = hou.node(instance.data["instance_node"]) + output_parm = lib.get_output_parameter(ropnode) + expected_filepath = output_parm.eval() + instance.data.setdefault("files", list()) + instance.data.setdefault("expectedFiles", list()) + if instance.data.get("frames"): + files = self.get_files(instance, expected_filepath) + # list of files + instance.data["files"].extend(files) + else: + # single file + instance.data["files"].append(output_parm.eval()) + cache_files = {"_": instance.data["files"]} + # Convert instance family to pointcache if it is bgeo or abc + # because ??? + for family in instance.data["families"]: + if family == "bgeo" or "abc": + instance.data["productType"] = "pointcache" + break + instance.data.update({ + "plugin": "Houdini", + "publish": True + }) + instance.data["families"].append("publish.hou") + instance.data["expectedFiles"].append(cache_files) + + self.log.debug("{}".format(instance.data)) + + def get_files(self, instance, output_parm): + """Get the files with the frame range data + + Args: + instance (_type_): instance + output_parm (_type_): path of output parameter + + Returns: + files: a list of files + """ + directory = os.path.dirname(output_parm) + + files = [ + os.path.join(directory, frame).replace("\\", "/") + for frame in instance.data["frames"] + ] + + return files \ No newline at end of file diff --git a/client/ayon_core/hosts/houdini/plugins/publish/extract_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/extract_rop.py new file mode 100644 index 0000000000..e121f7f4e0 --- /dev/null +++ b/client/ayon_core/hosts/houdini/plugins/publish/extract_rop.py @@ -0,0 +1,44 @@ +import pyblish.api +from ayon_core.pipeline import publish +from ayon_core.hosts.houdini.api import lib + +import hou + + +class ExtractROP(publish.Extractor): + """Render a ROP node and add representation to the instance""" + + label = "Extract ROP" + families = ["rop"] + hosts = ["houdini"] + + order = pyblish.api.ExtractorOrder + 0.1 + + def process(self, instance): + + if instance.data.get('farm'): + # Will be submitted to farm instead - not rendered locally + return + + files = instance.data["frames"] + first_file = files[0] if isinstance(files, (list, tuple)) else files + _, ext = lib.splitext( + first_file, allowed_multidot_extensions=[ + ".ass.gz", ".bgeo.sc", ".bgeo.gz", + ".bgeo.lzma", ".bgeo.bz2"]) + ext = ext.lstrip(".") # strip starting dot + + # prepare representation + representation = { + "name": ext, + "ext": ext, + "files": files, + "stagingDir": instance.data["stagingDir"] + } + + # render rop + ropnode = hou.node(instance.data.get("instance_node")) + lib.render_rop(ropnode) + + # add representation + instance.data.setdefault("representations", []).append(representation) From 17d42f8f21e5efb2a6ae2aeb4cf112a82f3fcd3f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 May 2024 15:55:09 +0200 Subject: [PATCH 04/36] Preserve `dynamic` family for publishing logic --- .../ayon_core/hosts/houdini/plugins/create/create_dynamic.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py b/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py index 1ee8a6402d..875c2d8c59 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py @@ -64,6 +64,7 @@ def create(self, product_name, instance_data, pre_create_data): files = pre_create_data["files"] representations = [create_representation_data(files)] instance_data["representations"] = representations + instance_data["families"] = ["dynamic"] # We ingest it as a different product type then the creator's generic # ingest product type. For example, we specify `pointcache` @@ -89,9 +90,6 @@ def remove_instances(self, instances): for instance in instances: self._remove_instance_from_context(instance) - # def get_publish_families(self): - # return [self.product_type] - def _get_product_name_dynamic( self, project_name, From dcd7d1101b92c0ff86befc2acc40e76a39e941dc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 May 2024 15:55:52 +0200 Subject: [PATCH 05/36] Remove unused logic --- .../publish/collect_houdini_batch_families.py | 72 ------------------- 1 file changed, 72 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_houdini_batch_families.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_houdini_batch_families.py index e09de96097..159b3c7fbc 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_houdini_batch_families.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_houdini_batch_families.py @@ -38,75 +38,3 @@ def process(self, instance): # not trigger. instance.data["family"] = "dynamic" instance.data["families"] = ["dynamic"] - - -# TODO: Implement for generic rop class -class CollectDataforCache(pyblish.api.InstancePlugin): - """Collect data for caching to Deadline.""" - - # Run after Collect Frames - order = pyblish.api.CollectorOrder + 0.11 - families = ["todo"] - hosts = ["houdini"] - 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: - self.log.debug("Caching on farm is disabled. " - "Skipping farm collecting.") - return - - # Why do we need this particular collector to collect the expected - # output files from a ROP node. Don't we have a dedicated collector - # for that yet? - # Collect expected files - ropnode = hou.node(instance.data["instance_node"]) - output_parm = lib.get_output_parameter(ropnode) - expected_filepath = output_parm.eval() - instance.data.setdefault("files", list()) - instance.data.setdefault("expectedFiles", list()) - if instance.data.get("frames"): - files = self.get_files(instance, expected_filepath) - # list of files - instance.data["files"].extend(files) - else: - # single file - instance.data["files"].append(output_parm.eval()) - cache_files = {"_": instance.data["files"]} - # Convert instance family to pointcache if it is bgeo or abc - # because ??? - for family in instance.data["families"]: - if family == "bgeo" or "abc": - instance.data["productType"] = "pointcache" - break - instance.data.update({ - "plugin": "Houdini", - "publish": True - }) - instance.data["families"].append("publish.hou") - instance.data["expectedFiles"].append(cache_files) - - self.log.debug("{}".format(instance.data)) - - def get_files(self, instance, output_parm): - """Get the files with the frame range data - - Args: - instance (_type_): instance - output_parm (_type_): path of output parameter - - Returns: - files: a list of files - """ - directory = os.path.dirname(output_parm) - - files = [ - os.path.join(directory, frame).replace("\\", "/") - for frame in instance.data["frames"] - ] - - return files \ No newline at end of file From 2d178ade4d61b90864db61f4422a77b8979462c5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 May 2024 16:00:42 +0200 Subject: [PATCH 06/36] Allow passing `representations` in instance data from Create Context `CreatedInstance` data --- client/ayon_core/plugins/publish/collect_from_create_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_from_create_context.py b/client/ayon_core/plugins/publish/collect_from_create_context.py index b99866fed9..d13a7747a2 100644 --- a/client/ayon_core/plugins/publish/collect_from_create_context.py +++ b/client/ayon_core/plugins/publish/collect_from_create_context.py @@ -89,7 +89,7 @@ def create_instance( "productType": in_data["productType"], "family": in_data["productType"], "families": instance_families, - "representations": [], + "representations": in_data.get("representations", []), "thumbnailSource": thumbnail_path }) for key, value in in_data.items(): From 5bcc8d7cb5194e52ccdb5907ff720c0dea568701 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 22 May 2024 20:16:49 +0200 Subject: [PATCH 07/36] Fix publish representation data --- .../ayon_core/hosts/houdini/plugins/create/create_dynamic.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py b/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py index 875c2d8c59..b301d83f28 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_dynamic.py @@ -12,6 +12,10 @@ def create_representation_data(files): """Create representation data needed for `instance.data['representations']""" first_file = files[0] folder, filename = os.path.split(first_file) + + # Files should be filename only in representation + files = [os.path.basename(filepath) for filepath in files] + ext = os.path.splitext(filename)[-1].strip(".") return { "name": ext, From 4eba35af82a5f0d43509d9f87ffe3047cb2898e2 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 23 May 2024 17:54:29 +0300 Subject: [PATCH 08/36] add 'Make AYON Publishable' to OPMenu --- .../ayon_core/hosts/houdini/startup/OPmenu.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/client/ayon_core/hosts/houdini/startup/OPmenu.xml b/client/ayon_core/hosts/houdini/startup/OPmenu.xml index 0a7b265fa1..c95f9b6cb7 100644 --- a/client/ayon_core/hosts/houdini/startup/OPmenu.xml +++ b/client/ayon_core/hosts/houdini/startup/OPmenu.xml @@ -5,6 +5,24 @@ + + + + + hasattr(kwargs["node"], "render") + + + + + + + opmenu.unsynchronize From 7aa80fb3c0e1956ec6e64daa728b01c027cc7c4c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 23 May 2024 18:00:10 +0300 Subject: [PATCH 09/36] add 'generic' family to collect farm instances and skip render if local publishing --- .../hosts/houdini/plugins/publish/collect_farm_instances.py | 3 ++- .../ayon_core/hosts/houdini/plugins/publish/extract_rop.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_farm_instances.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_farm_instances.py index 586aa2da57..ee2b35045d 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_farm_instances.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_farm_instances.py @@ -9,7 +9,8 @@ class CollectFarmInstances(pyblish.api.InstancePlugin): "karma_rop", "redshift_rop", "arnold_rop", - "vray_rop"] + "vray_rop", + "generic"] hosts = ["houdini"] targets = ["local", "remote"] diff --git a/client/ayon_core/hosts/houdini/plugins/publish/extract_rop.py b/client/ayon_core/hosts/houdini/plugins/publish/extract_rop.py index e121f7f4e0..ffe93d9d7a 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/extract_rop.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/extract_rop.py @@ -37,8 +37,10 @@ def process(self, instance): } # render rop - ropnode = hou.node(instance.data.get("instance_node")) - lib.render_rop(ropnode) + creator_attribute = instance.data["creator_attributes"] + if creator_attribute.get("render_target") == "local": + ropnode = hou.node(instance.data.get("instance_node")) + lib.render_rop(ropnode) # add representation instance.data.setdefault("representations", []).append(representation) From e301f8b8e4e2178704c06fe7fc07436155b9e44a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 3 Jun 2024 23:59:16 +0200 Subject: [PATCH 10/36] Refactor `ayon_core.hosts.houdini` usage in HDA to `ayon_houdini` --- .../client/ayon_houdini/plugins/create/create_generic.py | 4 ++-- .../plugins/publish/collect_houdini_batch_families.py | 3 --- .../client/ayon_houdini/plugins/publish/extract_rop.py | 2 +- server_addon/houdini/client/ayon_houdini/startup/OPmenu.xml | 2 +- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index 7d1389bb8e..e8727eae1a 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -1,5 +1,5 @@ -from ayon_core.hosts.houdini.api import plugin -from ayon_core.hosts.houdini.api.lib import ( +from ayon_houdini.api import plugin +from ayon_houdini.api.lib import ( lsattr, read ) from ayon_core.pipeline.create import ( diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py index 159b3c7fbc..da38b8877e 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py @@ -1,7 +1,4 @@ -import os import pyblish.api -import hou -from ayon_core.hosts.houdini.api import lib class CollectNoProductTypeFamilyGeneric(pyblish.api.InstancePlugin): diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py index ffe93d9d7a..4266441455 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/extract_rop.py @@ -1,6 +1,6 @@ import pyblish.api from ayon_core.pipeline import publish -from ayon_core.hosts.houdini.api import lib +from ayon_houdini.api import lib import hou diff --git a/server_addon/houdini/client/ayon_houdini/startup/OPmenu.xml b/server_addon/houdini/client/ayon_houdini/startup/OPmenu.xml index b429e50ebe..56bf9a7f80 100644 --- a/server_addon/houdini/client/ayon_houdini/startup/OPmenu.xml +++ b/server_addon/houdini/client/ayon_houdini/startup/OPmenu.xml @@ -13,7 +13,7 @@ Date: Fri, 21 Jun 2024 14:33:03 +0200 Subject: [PATCH 11/36] Add todo --- .../client/ayon_houdini/plugins/create/create_dynamic.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_dynamic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_dynamic.py index b301d83f28..abb83b42b1 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_dynamic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_dynamic.py @@ -32,8 +32,13 @@ class CreateRuntimeInstance(Creator): publishing. The created instances are transient and will be gone on resetting the `CreateContext` since they will not be recollected. + TODO: The goal is for this runtime instance to be so generic that it can + run anywhere, globally - and needs no knowledge about its host. It's + the simplest 'entry point' to ingesting something from anywhere. + """ # TODO: This should be a global HIDDEN creator instead! + # TODO: This should eventually not have a Houdini specific identifier identifier = "io.openpype.creators.houdini.batch" label = "Ingest" product_type = "dynamic" # not actually used From 97d66a8c18c179b260fc0e27f60d8a9496dd3c1e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 14:33:21 +0200 Subject: [PATCH 12/36] Docstring + cosmetics --- .../client/ayon_houdini/plugins/create/create_generic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index e8727eae1a..e249744f03 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -117,7 +117,9 @@ def attribute_def_to_parm_template(attribute_def, key=None): def set_values(node: "hou.OpNode", values: dict): - """ + """Set parm values only if both the raw value (e.g. expression) or the + evaluated value differ. This way we preserve expressions if they happen + to evaluate to a matching value. Parms must exist on the node already. @@ -161,9 +163,7 @@ class CreateHoudiniGeneric(plugin.HoudiniCreator): render_target = "local_no_render" def get_detail_description(self): - return ( - """Publish any ROP node.""" - ) + return "Publish any ROP node." def create(self, product_name, instance_data, pre_create_data): From 2d817560ddad7bcd3fa78748d1f7bdc159b5a8ad Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 16:09:41 +0200 Subject: [PATCH 13/36] Fix certain parms only getting generated on first save. --- .../ayon_houdini/plugins/create/create_generic.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index e249744f03..8507680c75 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -203,12 +203,18 @@ def create(self, product_name, instance_data, pre_create_data): product_type, product_name, instance_data.copy(), self ) - # Imprint on the selected node - self.imprint(created_instance, values=instance_data, update=False) - # Add instance self._add_instance_to_context(created_instance) + # Imprint on the selected node + # NOTE: We imprint after `_add_instance_to_context` to ensure + # the imprinted data directly contains also the instance + # attributes for the product type. Otherwise, they will appear + # after first save. + self.imprint(created_instance, + values=created_instance.data_to_store(), + update=False) + def collect_instances(self): for node in lsattr("AYON_id", plugin.AYON_INSTANCE_ID): From 8c9afe0627424e3d5f4645edb39af05118a123b8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 18:16:08 +0200 Subject: [PATCH 14/36] Add todo --- .../client/ayon_houdini/plugins/create/create_generic.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index 8507680c75..a4825e49fa 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -533,6 +533,10 @@ def get_instance_attr_defs(self): } return [ + # TODO: This review toggle may be odd - because a regular + # pointcache creator does not have the review toggle but with + # this it does. Is that confusing? Can we make it so that `review` + # only shows when relevant? BoolDef("review", label="Review", tooltip="Mark as reviewable", From e49be709dbd86df7eba5e4918ea7f372c892e987 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 18:46:30 +0200 Subject: [PATCH 15/36] Set variant parm default to `$OS` (node name) --- .../houdini/client/ayon_houdini/plugins/create/create_generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index a4825e49fa..f66d025221 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -161,6 +161,7 @@ class CreateHoudiniGeneric(plugin.HoudiniCreator): # TODO: Override "create" to create the AYON publish attributes on the # selected node so it becomes a publishable instance. render_target = "local_no_render" + default_variant = "$OS" def get_detail_description(self): return "Publish any ROP node." From 15d1f6813d2bf2269e88a9ae6bc495026751edf5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 19:21:34 +0200 Subject: [PATCH 16/36] Do not show the generic creator in the tab menus --- .../client/ayon_houdini/plugins/create/create_generic.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index f66d025221..6e8acf3863 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -593,3 +593,7 @@ def _get_product_name_dynamic( dynamic_data=dynamic_data, project_settings=self.project_settings ) + + def get_network_categories(self): + # Do not show anywhere in TAB menus since it applies to existing nodes + return [] From 981e2eae4d640e1defe2bd69fe6f9206d847155b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 19:30:46 +0200 Subject: [PATCH 17/36] Support `LabsKarma` node out of the box --- server_addon/houdini/client/ayon_houdini/api/lib.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server_addon/houdini/client/ayon_houdini/api/lib.py b/server_addon/houdini/client/ayon_houdini/api/lib.py index 671265fae9..308df7334f 100644 --- a/server_addon/houdini/client/ayon_houdini/api/lib.py +++ b/server_addon/houdini/client/ayon_houdini/api/lib.py @@ -104,6 +104,8 @@ def get_output_parameter(node): return node.parm("outputimage") elif node_type == "vray_renderer": return node.parm("SettingsOutput_img_file_path") + elif node_type == "labs::karma::2.0": + return node.parm("file") raise TypeError("Node type '%s' not supported" % node_type) From 4c7a20105d221e55ed055e5c3608b17e69f20116 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 21 Jun 2024 23:16:23 +0200 Subject: [PATCH 18/36] Draft: Automatically make nodes publishable on creation --- .../plugins/create/create_generic.py | 7 +- .../ayon_houdini/startup/scripts/OnCreated.py | 72 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index 6e8acf3863..c741c5b9e8 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -189,7 +189,12 @@ def create(self, product_name, instance_data, pre_create_data): product_type=product_type ) - for node in hou.selectedNodes(): + if pre_create_data.get("node"): + nodes = [pre_create_data.get("node")] + else: + nodes = hou.selectedNodes() + + for node in nodes: if node.parm("AYON_creator_identifier"): # Continue if already existing attributes continue diff --git a/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py b/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py new file mode 100644 index 0000000000..574b09e965 --- /dev/null +++ b/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py @@ -0,0 +1,72 @@ +# Any code here will run on any node being created in Houdini +# As such, preferably the code here should run fast to avoid slowing down node +# creation. Note: It does not trigger on existing nodes for scene open nor on +# node copy-pasting. +from ayon_core.lib import env_value_to_bool + + +def make_publishable(node, product_type): + # TODO: Can we make this imprinting much faster? Unfortunately + # CreateContext initialization is very slow. + from ayon_core.pipeline import registered_host + from ayon_core.pipeline.create import CreateContext + + host = registered_host() + context = CreateContext(host) + + variant = node.name() + + # Apply the instance creation to the node + context.create( + creator_identifier="io.ayon.creators.houdini.publish", + variant=variant, + pre_create_data={ + "productType": product_type, + "node": node + } + ) + + +def autocreate_publishable(node): + node_type = node.type().name() + + # TODO: Move this choice of automatic 'imprint' to settings so studio can + # configure which nodes should get automatically imprinted on creation + mapping = { + # Pointcache + "alembic": "pointcache", + "rop_alembic": "pointcache", + "geometry": "pointcache", + "rop_geometry": "pointcache", + # FBX + "filmboxfbx": "fbx", + "rop_fbx": "fbx", + # USD + "usd": "usd", + "usd_rop": "usd", + "usdexport": "usd", + "comp": "imagesequence", + "opengl": "review", + # Render + "arnold": "render", + "labs::karma::2.0": "render", + "karma": "render", + "usdrender": "render", + "vray_renderer": "render" + } + product_type = mapping.get(node_type, None) + if product_type: + print(node.parm("id")) + make_publishable(node, product_type) + + +if env_value_to_bool("AYON_HOUDINI_AUTOCREATE"): + # TODO: For now 'hide' this behind `AYON_HOUDINI_AUTOCREATE` env var + # because it breaks existing creators, since it will also try and + # auto-apply to e.g. the node created by the legacy pointcache creator + # TODO: We could also reverse this logic and have all legacy creators + # force disable `AYON_HOUDINI_CREATE` in a context manager as it creates + # the nodes so that this functionality can be easily 'disabled' + # temporarily + node = kwargs["node"] + autocreate_publishable(node) From cb74f1e09c71fbc4c69468f765cf6149c07850e5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 09:20:31 +0200 Subject: [PATCH 19/36] Support auto create without breaking the older creators --- server_addon/houdini/client/ayon_houdini/api/lib.py | 13 +++++++++++++ .../houdini/client/ayon_houdini/api/plugin.py | 13 ++++++++++--- .../ayon_houdini/startup/scripts/OnCreated.py | 3 +-- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/lib.py b/server_addon/houdini/client/ayon_houdini/api/lib.py index 308df7334f..331f239440 100644 --- a/server_addon/houdini/client/ayon_houdini/api/lib.py +++ b/server_addon/houdini/client/ayon_houdini/api/lib.py @@ -1195,3 +1195,16 @@ def prompt_reset_context(): update_content_on_context_change() dialog.deleteLater() + + +@contextmanager +def no_auto_create_publishable(): + value = os.environ.get("AYON_HOUDINI_AUTOCREATE") + os.environ["AYON_HOUDINI_AUTOCREATE"] = "0" + try: + yield + finally: + if value is None: + del os.environ["AYON_HOUDINI_AUTOCREATE"] + else: + os.environ["AYON_HOUDINI_AUTOCREATE"] = value diff --git a/server_addon/houdini/client/ayon_houdini/api/plugin.py b/server_addon/houdini/client/ayon_houdini/api/plugin.py index 9c6bba925a..ea09f5574c 100644 --- a/server_addon/houdini/client/ayon_houdini/api/plugin.py +++ b/server_addon/houdini/client/ayon_houdini/api/plugin.py @@ -20,7 +20,13 @@ ) from ayon_core.lib import BoolDef -from .lib import imprint, read, lsattr, add_self_publish_button +from .lib import ( + imprint, + read, + lsattr, + add_self_publish_button, + no_auto_create_publishable +) SETTINGS_CATEGORY = "houdini" @@ -192,8 +198,9 @@ def create(self, product_name, instance_data, pre_create_data): folder_path = instance_data["folderPath"] - instance_node = self.create_instance_node( - folder_path, product_name, "/out", node_type) + with no_auto_create_publishable(): + instance_node = self.create_instance_node( + folder_path, product_name, "/out", node_type) self.customize_node_look(instance_node) diff --git a/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py b/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py index 574b09e965..9ea12ab022 100644 --- a/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py +++ b/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py @@ -56,11 +56,10 @@ def autocreate_publishable(node): } product_type = mapping.get(node_type, None) if product_type: - print(node.parm("id")) make_publishable(node, product_type) -if env_value_to_bool("AYON_HOUDINI_AUTOCREATE"): +if env_value_to_bool("AYON_HOUDINI_AUTOCREATE", default=True): # TODO: For now 'hide' this behind `AYON_HOUDINI_AUTOCREATE` env var # because it breaks existing creators, since it will also try and # auto-apply to e.g. the node created by the legacy pointcache creator From afde2a8e24c14f6328a97847f7cde39da08a9e92 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 11:52:56 +0200 Subject: [PATCH 20/36] Add "usdrender_rop" --- .../houdini/client/ayon_houdini/startup/scripts/OnCreated.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py b/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py index 9ea12ab022..a1660525fe 100644 --- a/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py +++ b/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py @@ -52,6 +52,7 @@ def autocreate_publishable(node): "labs::karma::2.0": "render", "karma": "render", "usdrender": "render", + "usdrender_rop": "render", "vray_renderer": "render" } product_type = mapping.get(node_type, None) From d1d2ca5753bea4e263329ccc4793a8b16ac7a1e7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 12:08:13 +0200 Subject: [PATCH 21/36] Remove implemented todo --- .../client/ayon_houdini/startup/scripts/OnCreated.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py b/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py index a1660525fe..e788a71c9d 100644 --- a/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py +++ b/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py @@ -61,12 +61,5 @@ def autocreate_publishable(node): if env_value_to_bool("AYON_HOUDINI_AUTOCREATE", default=True): - # TODO: For now 'hide' this behind `AYON_HOUDINI_AUTOCREATE` env var - # because it breaks existing creators, since it will also try and - # auto-apply to e.g. the node created by the legacy pointcache creator - # TODO: We could also reverse this logic and have all legacy creators - # force disable `AYON_HOUDINI_CREATE` in a context manager as it creates - # the nodes so that this functionality can be easily 'disabled' - # temporarily node = kwargs["node"] autocreate_publishable(node) From 9f1eca7bd76969acc4749d61ef99426c4af6d056 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 18:06:20 +0200 Subject: [PATCH 22/36] Prepare for node type products to be defined from settings --- .../client/ayon_houdini/api/node_wrap.py | 55 ++++++++++++++++ .../plugins/create/create_generic.py | 64 +++++++++++++++++-- .../ayon_houdini/startup/scripts/OnCreated.py | 63 +++--------------- 3 files changed, 121 insertions(+), 61 deletions(-) create mode 100644 server_addon/houdini/client/ayon_houdini/api/node_wrap.py diff --git a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py new file mode 100644 index 0000000000..7488ea6fc1 --- /dev/null +++ b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py @@ -0,0 +1,55 @@ + + +def make_publishable(node, product_type): + # TODO: Can we make this imprinting much faster? Unfortunately + # CreateContext initialization is very slow. + from ayon_core.pipeline import registered_host + from ayon_core.pipeline.create import CreateContext + + host = registered_host() + context = CreateContext(host) + + variant = node.name() + + # Apply the instance creation to the node + context.create( + creator_identifier="io.ayon.creators.houdini.publish", + variant=variant, + pre_create_data={ + "productType": product_type, + "node": node + } + ) + + +def autocreate_publishable(node): + node_type = node.type().name() + + # TODO: Move this choice of automatic 'imprint' to settings so studio can + # configure which nodes should get automatically imprinted on creation + mapping = { + # Pointcache + "alembic": "pointcache", + "rop_alembic": "pointcache", + "geometry": "pointcache", + "rop_geometry": "pointcache", + # FBX + "filmboxfbx": "fbx", + "rop_fbx": "fbx", + # USD + "usd": "usd", + "usd_rop": "usd", + "usdexport": "usd", + "comp": "imagesequence", + "opengl": "review", + # Render + "arnold": "render", + "labs::karma::2.0": "render", + "karma": "render", + "usdrender": "render", + "usdrender_rop": "render", + "vray_renderer": "render" + } + product_type = mapping.get(node_type, None) + if product_type: + make_publishable(node, product_type) \ No newline at end of file diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index c741c5b9e8..d2e90cf870 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -1,3 +1,6 @@ +import dataclasses +from typing import Dict, List, Optional + from ayon_houdini.api import plugin from ayon_houdini.api.lib import ( lsattr, read @@ -147,6 +150,23 @@ def set_values(node: "hou.OpNode", values: dict): parm.set(value) +@dataclasses.dataclass +class NodeTypeProductTypes: + """Product type settings for a node type. + + Define the available product types the user can set on a ROP based on + node type. + + When 'strict' an enum attribute is created and the user can not type a + custom product type, otherwise a string attribute is + created with a menu right hand side to help pick a type but allow custom + types. + """ + product_types: List[str] + default: Optional[str] = None + strict: bool = True + + class CreateHoudiniGeneric(plugin.HoudiniCreator): """Generic creator to ingest arbitrary products""" @@ -158,11 +178,28 @@ class CreateHoudiniGeneric(plugin.HoudiniCreator): icon = "male" description = "Make any ROP node publishable." - # TODO: Override "create" to create the AYON publish attributes on the - # selected node so it becomes a publishable instance. render_target = "local_no_render" default_variant = "$OS" + # TODO: Move this to project settings + node_type_product_types: Dict[str, NodeTypeProductTypes] = { + "alembic": NodeTypeProductTypes( + product_types=["pointcache", "model"], + default="pointcache" + ), + "rop_fbx": NodeTypeProductTypes( + product_types=["fbx", "pointcache", "model"], + default="fbx" + ) + } + node_type_product_types_default = NodeTypeProductTypes( + product_types=list(sorted( + {"ass", "pointcache", "model", "render", + "camera", "imagesequence", "review", "vdbcache", "fbx"})), + default="pointcache", + strict=False + ) + def get_detail_description(self): return "Publish any ROP node." @@ -415,7 +452,20 @@ def create_attribute_def_parms(self, ]) publish_attributes_folder.addParmTemplate(parm_template) - # TODO + # Define the product types picker options + node_type_product_types: NodeTypeProductTypes = ( + self.node_type_product_types.get( + node.type().name(), self.node_type_product_types_default + )) + product_type_kwargs = { + "menu_items": node_type_product_types.product_types, + "default_value": (node_type_product_types.default,) + } + if node_type_product_types.strict: + product_type_kwargs["menu_type"] = hou.menuType.Normal + else: + product_type_kwargs["menu_type"] = hou.menuType.StringReplace + # Add the Folder Path, Task Name, Product Type, Variant, Product Name # and Active state in Instance attributes for attribute in [ @@ -432,7 +482,7 @@ def create_attribute_def_parms(self, hou.StringParmTemplate( "AYON_productType", "Product Type", num_components=1, - default_value=("pointcache",) + **product_type_kwargs ), hou.StringParmTemplate( "AYON_variant", "Variant", @@ -557,8 +607,10 @@ def get_pre_create_attr_defs(self): return [ TextDef("productType", label="Product Type", - tooltip="Publish product type", - default="pointcache") + tooltip="Publish product type. When set to " + "`__use_node_default__` it will use the project " + "settings to define the default value.", + default="__use_node_default__") ] def _get_product_name_dynamic( diff --git a/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py b/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py index e788a71c9d..e73655207c 100644 --- a/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py +++ b/server_addon/houdini/client/ayon_houdini/startup/scripts/OnCreated.py @@ -2,64 +2,17 @@ # As such, preferably the code here should run fast to avoid slowing down node # creation. Note: It does not trigger on existing nodes for scene open nor on # node copy-pasting. -from ayon_core.lib import env_value_to_bool +from ayon_core.lib import env_value_to_bool, is_dev_mode_enabled +from ayon_houdini.api import node_wrap -def make_publishable(node, product_type): - # TODO: Can we make this imprinting much faster? Unfortunately - # CreateContext initialization is very slow. - from ayon_core.pipeline import registered_host - from ayon_core.pipeline.create import CreateContext - - host = registered_host() - context = CreateContext(host) - - variant = node.name() - - # Apply the instance creation to the node - context.create( - creator_identifier="io.ayon.creators.houdini.publish", - variant=variant, - pre_create_data={ - "productType": product_type, - "node": node - } - ) - - -def autocreate_publishable(node): - node_type = node.type().name() - - # TODO: Move this choice of automatic 'imprint' to settings so studio can - # configure which nodes should get automatically imprinted on creation - mapping = { - # Pointcache - "alembic": "pointcache", - "rop_alembic": "pointcache", - "geometry": "pointcache", - "rop_geometry": "pointcache", - # FBX - "filmboxfbx": "fbx", - "rop_fbx": "fbx", - # USD - "usd": "usd", - "usd_rop": "usd", - "usdexport": "usd", - "comp": "imagesequence", - "opengl": "review", - # Render - "arnold": "render", - "labs::karma::2.0": "render", - "karma": "render", - "usdrender": "render", - "usdrender_rop": "render", - "vray_renderer": "render" - } - product_type = mapping.get(node_type, None) - if product_type: - make_publishable(node, product_type) +# Allow easier development by automatic reloads +# TODO: remove this +if is_dev_mode_enabled(): + import importlib + importlib.reload(node_wrap) if env_value_to_bool("AYON_HOUDINI_AUTOCREATE", default=True): node = kwargs["node"] - autocreate_publishable(node) + node_wrap.autocreate_publishable(node) From 037c64f0cf73fd5801ea79d11da4496caf3b5db7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 20:02:04 +0200 Subject: [PATCH 23/36] Move the node type defaults to Creator in preparation of move to settings --- .../client/ayon_houdini/api/node_wrap.py | 61 ++++++------ .../plugins/create/create_generic.py | 98 ++++++++++++++----- 2 files changed, 103 insertions(+), 56 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py index 7488ea6fc1..0f4c850d71 100644 --- a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py +++ b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py @@ -1,10 +1,10 @@ +from ayon_core.pipeline import registered_host +from ayon_core.pipeline.create import CreateContext -def make_publishable(node, product_type): +def make_publishable(node): # TODO: Can we make this imprinting much faster? Unfortunately # CreateContext initialization is very slow. - from ayon_core.pipeline import registered_host - from ayon_core.pipeline.create import CreateContext host = registered_host() context = CreateContext(host) @@ -16,40 +16,35 @@ def make_publishable(node, product_type): creator_identifier="io.ayon.creators.houdini.publish", variant=variant, pre_create_data={ - "productType": product_type, "node": node } ) +# TODO: Move this choice of automatic 'imprint' to settings so studio can +# configure which nodes should get automatically imprinted on creation +AUTO_CREATE_NODE_TYPES = { + "alembic", + "rop_alembic", + "geometry", + "rop_geometry", + "filmboxfbx", + "rop_fbx", + "usd", + "usd_rop", + "usdexport", + "comp", + "opengl", + "arnold", + "labs::karma::2.0", + "karma", + "usdrender", + "usdrender_rop", + "vray_renderer", +} + + def autocreate_publishable(node): node_type = node.type().name() - - # TODO: Move this choice of automatic 'imprint' to settings so studio can - # configure which nodes should get automatically imprinted on creation - mapping = { - # Pointcache - "alembic": "pointcache", - "rop_alembic": "pointcache", - "geometry": "pointcache", - "rop_geometry": "pointcache", - # FBX - "filmboxfbx": "fbx", - "rop_fbx": "fbx", - # USD - "usd": "usd", - "usd_rop": "usd", - "usdexport": "usd", - "comp": "imagesequence", - "opengl": "review", - # Render - "arnold": "render", - "labs::karma::2.0": "render", - "karma": "render", - "usdrender": "render", - "usdrender_rop": "render", - "vray_renderer": "render" - } - product_type = mapping.get(node_type, None) - if product_type: - make_publishable(node, product_type) \ No newline at end of file + if node_type in AUTO_CREATE_NODE_TYPES: + make_publishable(node) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index d2e90cf870..f432136469 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -167,6 +167,33 @@ class NodeTypeProductTypes: strict: bool = True +# Re-usable defaults +GEO_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["pointcache", "model"], + default="pointcache" +) +FBX_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["fbx", "pointcache", "model"], + default="fbx" +) +USD_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["usd", "pointcache"], + default="usd" +) +COMP_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["imagesequence", "render"], + default="imagesequence" +) +REVIEW_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["review"], + default="review" +) +RENDER_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["render", "prerender"], + default="render" +) + + class CreateHoudiniGeneric(plugin.HoudiniCreator): """Generic creator to ingest arbitrary products""" @@ -183,29 +210,44 @@ class CreateHoudiniGeneric(plugin.HoudiniCreator): # TODO: Move this to project settings node_type_product_types: Dict[str, NodeTypeProductTypes] = { - "alembic": NodeTypeProductTypes( - product_types=["pointcache", "model"], - default="pointcache" - ), - "rop_fbx": NodeTypeProductTypes( - product_types=["fbx", "pointcache", "model"], - default="fbx" - ) + "alembic": GEO_PRODUCT_TYPES, + "rop_alembic": GEO_PRODUCT_TYPES, + "geometry": GEO_PRODUCT_TYPES, + "rop_geometry": GEO_PRODUCT_TYPES, + "filmboxfbx": FBX_PRODUCT_TYPES, + "rop_fbx": FBX_PRODUCT_TYPES, + "usd": USD_PRODUCT_TYPES, + "usd_rop": USD_PRODUCT_TYPES, + "usdexport": USD_PRODUCT_TYPES, + "comp": COMP_PRODUCT_TYPES, + "opengl": REVIEW_PRODUCT_TYPES, + "arnold": RENDER_PRODUCT_TYPES, + "labs::karma::2.0": RENDER_PRODUCT_TYPES, + "karma": RENDER_PRODUCT_TYPES, + "usdrender": RENDER_PRODUCT_TYPES, + "usdrender_rop": RENDER_PRODUCT_TYPES, + "vray_renderer": RENDER_PRODUCT_TYPES } + node_type_product_types_default = NodeTypeProductTypes( product_types=list(sorted( - {"ass", "pointcache", "model", "render", - "camera", "imagesequence", "review", "vdbcache", "fbx"})), + { + "ass", "pointcache", "model", "render", "camera", + "imagesequence", "review", "vdbcache", "fbx" + })), default="pointcache", strict=False ) + USE_DEFAULT_PRODUCT_TYPE = "__use_node_default__" + def get_detail_description(self): return "Publish any ROP node." def create(self, product_name, instance_data, pre_create_data): - product_type = pre_create_data.get("productType", "pointcache") + product_type = pre_create_data.get("productType", + self.USE_DEFAULT_PRODUCT_TYPE) instance_data["productType"] = product_type # Unfortunately the Create Context will provide the product name @@ -218,13 +260,6 @@ def create(self, product_name, instance_data, pre_create_data): task_entity = get_task_by_name(project_name, folder_id=folder_entity["id"], task_name=instance_data["task"]) - product_name = self._get_product_name_dynamic( - self.create_context.project_name, - folder_entity=folder_entity, - task_entity=task_entity, - variant=instance_data["variant"], - product_type=product_type - ) if pre_create_data.get("node"): nodes = [pre_create_data.get("node")] @@ -240,10 +275,26 @@ def create(self, product_name, instance_data, pre_create_data): # this to the `AVALON_INSTANCE_ID` instead instance_data["id"] = plugin.AYON_INSTANCE_ID + # When using default product type base it on node type settings + node_product_type = product_type + if node_product_type == self.USE_DEFAULT_PRODUCT_TYPE: + node_type = node.type().name() + node_product_type = self.node_type_product_types.get( + node_type, self.node_type_product_types_default + ).default + + product_name = self._get_product_name_dynamic( + self.create_context.project_name, + folder_entity=folder_entity, + task_entity=task_entity, + variant=instance_data["variant"], + product_type=node_product_type + ) + instance_data["instance_node"] = node.path() instance_data["instance_id"] = node.path() created_instance = CreatedInstance( - product_type, product_name, instance_data.copy(), self + node_product_type, product_name, instance_data.copy(), self ) # Add instance @@ -607,10 +658,11 @@ def get_pre_create_attr_defs(self): return [ TextDef("productType", label="Product Type", - tooltip="Publish product type. When set to " - "`__use_node_default__` it will use the project " - "settings to define the default value.", - default="__use_node_default__") + tooltip=( + "Publish product type. When set to " + f"{self.USE_DEFAULT_PRODUCT_TYPE} it will use " + "the project settings to define the default value."), + default=self.USE_DEFAULT_PRODUCT_TYPE) ] def _get_product_name_dynamic( From 2a76da81d2bb0a70ee4f8d7c9ede7b1f2cae15aa Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 20:20:36 +0200 Subject: [PATCH 24/36] Fix karma labs parm --- server_addon/houdini/client/ayon_houdini/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/lib.py b/server_addon/houdini/client/ayon_houdini/api/lib.py index 331f239440..c44a93940b 100644 --- a/server_addon/houdini/client/ayon_houdini/api/lib.py +++ b/server_addon/houdini/client/ayon_houdini/api/lib.py @@ -105,7 +105,7 @@ def get_output_parameter(node): elif node_type == "vray_renderer": return node.parm("SettingsOutput_img_file_path") elif node_type == "labs::karma::2.0": - return node.parm("file") + return node.parm("picture") raise TypeError("Node type '%s' not supported" % node_type) From bfaf156a4e5ea84fc0a6f8ef3bebb373572f50af Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 20:29:24 +0200 Subject: [PATCH 25/36] Remove simple runtime instance creator to separate for another PR --- .../plugins/create/create_dynamic.py | 141 ------------------ .../publish/collect_houdini_batch_families.py | 20 +-- 2 files changed, 1 insertion(+), 160 deletions(-) delete mode 100644 server_addon/houdini/client/ayon_houdini/plugins/create/create_dynamic.py diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_dynamic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_dynamic.py deleted file mode 100644 index abb83b42b1..0000000000 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_dynamic.py +++ /dev/null @@ -1,141 +0,0 @@ -import os - -from ayon_core.pipeline.create import ( - Creator, - CreatedInstance, - get_product_name -) -from ayon_api import get_folder_by_path, get_task_by_name - - -def create_representation_data(files): - """Create representation data needed for `instance.data['representations']""" - first_file = files[0] - folder, filename = os.path.split(first_file) - - # Files should be filename only in representation - files = [os.path.basename(filepath) for filepath in files] - - ext = os.path.splitext(filename)[-1].strip(".") - return { - "name": ext, - "ext": ext, - "files": files if len(files) > 1 else first_file, - "stagingDir": folder, - } - - -class CreateRuntimeInstance(Creator): - """Create in-memory instances for dynamic PDG publishing of files. - - These instances do not persist and are meant for headless automated - publishing. The created instances are transient and will be gone on - resetting the `CreateContext` since they will not be recollected. - - TODO: The goal is for this runtime instance to be so generic that it can - run anywhere, globally - and needs no knowledge about its host. It's - the simplest 'entry point' to ingesting something from anywhere. - - """ - # TODO: This should be a global HIDDEN creator instead! - # TODO: This should eventually not have a Houdini specific identifier - identifier = "io.openpype.creators.houdini.batch" - label = "Ingest" - product_type = "dynamic" # not actually used - icon = "gears" - - def create(self, product_name, instance_data, pre_create_data): - - # Unfortunately the Create Context will provide the product name - # even before the `create` call without listening to pre create data - # or the instance data - so instead we ignore the product name here - # and redefine it ourselves based on the `variant` in instance data - product_type = pre_create_data.get("product_type") or instance_data["product_type"] - project_name = self.create_context.project_name - folder_entity = get_folder_by_path(project_name, - instance_data["folderPath"]) - task_entity = get_task_by_name(project_name, - folder_id=folder_entity["id"], - task_name=instance_data["task"]) - product_name = self._get_product_name_dynamic( - self.create_context.project_name, - folder_entity=folder_entity, - task_entity=task_entity, - variant=instance_data["variant"], - product_type=product_type - ) - - custom_instance_data = pre_create_data.get("instance_data") - if custom_instance_data: - instance_data.update(custom_instance_data) - - # TODO: Add support for multiple representations - files = pre_create_data["files"] - representations = [create_representation_data(files)] - instance_data["representations"] = representations - instance_data["families"] = ["dynamic"] - - # We ingest it as a different product type then the creator's generic - # ingest product type. For example, we specify `pointcache` - instance = CreatedInstance( - product_type=product_type, - product_name=product_name, - data=instance_data, - creator=self - ) - self._add_instance_to_context(instance) - - return instance - - # Instances are all dynamic at run-time and cannot be persisted or - # re-collected - def collect_instances(self): - pass - - def update_instances(self, update_list): - pass - - def remove_instances(self, instances): - for instance in instances: - self._remove_instance_from_context(instance) - - def _get_product_name_dynamic( - self, - project_name, - folder_entity, - task_entity, - variant, - product_type, - host_name=None, - instance=None - ): - """Implementation similar to `self.get_product_name` but taking - `productType` as argument instead of using the 'generic' product type - on the Creator itself.""" - if host_name is None: - host_name = self.create_context.host_name - - task_name = task_type = None - if task_entity: - task_name = task_entity["name"] - task_type = task_entity["taskType"] - - dynamic_data = self.get_dynamic_data( - project_name, - folder_entity, - task_entity, - variant, - host_name, - instance - ) - - return get_product_name( - project_name, - task_name, - task_type, - host_name, - product_type, - variant, - dynamic_data=dynamic_data, - project_settings=self.project_settings - ) \ No newline at end of file diff --git a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py index da38b8877e..52f965db08 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/publish/collect_houdini_batch_families.py @@ -16,22 +16,4 @@ def process(self, instance): # not trigger. instance.data["family"] = "generic" # TODO: Do not add the dynamic 'rop' family in the collector? - instance.data["families"] = ["generic", "rop"] - self.log.info("Generic..") - - -class CollectNoProductTypeFamilyDynamic(pyblish.api.InstancePlugin): - """Collect data for caching to Deadline.""" - - order = pyblish.api.CollectorOrder - 0.49 - families = ["dynamic"] - hosts = ["houdini"] - targets = ["local", "remote"] - label = "Collect Data for Cache" - - def process(self, instance): - # Do not allow `productType` to creep into the pyblish families - # so that e.g. any regular plug-ins for `pointcache` or alike do - # not trigger. - instance.data["family"] = "dynamic" - instance.data["families"] = ["dynamic"] + instance.data["families"] = ["generic", "rop"] \ No newline at end of file From cbd3d5377113436923e4dd272f350fb6e5c23f5f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 20:41:47 +0200 Subject: [PATCH 26/36] Remove logic related to simple runtime instance creator to separate for another PR --- client/ayon_core/plugins/publish/collect_from_create_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_from_create_context.py b/client/ayon_core/plugins/publish/collect_from_create_context.py index d13a7747a2..b99866fed9 100644 --- a/client/ayon_core/plugins/publish/collect_from_create_context.py +++ b/client/ayon_core/plugins/publish/collect_from_create_context.py @@ -89,7 +89,7 @@ def create_instance( "productType": in_data["productType"], "family": in_data["productType"], "families": instance_families, - "representations": in_data.get("representations", []), + "representations": [], "thumbnailSource": thumbnail_path }) for key, value in in_data.items(): From c0d682953eb07358bf5aeb327e6291640cf84b8c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 22:08:13 +0200 Subject: [PATCH 27/36] Cleanup --- .../client/ayon_houdini/plugins/create/create_generic.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index f432136469..a9e4b36cfe 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -71,9 +71,6 @@ def attribute_def_to_parm_template(attribute_def, key=None): if not attribute_def.multiselection: labels = [item["label"] for item in attribute_def.items] values = [item["value"] for item in attribute_def.items] - - print(attribute_def.default) - return hou.StringParmTemplate( name=key, label=attribute_def.label, From 33bc530f984ee8fc4b4f28349449eec7ac31c8a3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 22:23:59 +0200 Subject: [PATCH 28/36] Implement more product types --- .../plugins/create/create_generic.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index a9e4b36cfe..5d63f3fed8 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -173,6 +173,10 @@ class NodeTypeProductTypes: product_types=["fbx", "pointcache", "model"], default="fbx" ) +FBX_ONLY_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["fbx"], + default="fbx" +) USD_PRODUCT_TYPES = NodeTypeProductTypes( product_types=["usd", "pointcache"], default="usd" @@ -189,6 +193,10 @@ class NodeTypeProductTypes: product_types=["render", "prerender"], default="render" ) +GLTF_PRODUCT_TYPES = NodeTypeProductTypes( + product_types=["gltf"], + default="gltf" +) class CreateHoudiniGeneric(plugin.HoudiniCreator): @@ -219,11 +227,15 @@ class CreateHoudiniGeneric(plugin.HoudiniCreator): "comp": COMP_PRODUCT_TYPES, "opengl": REVIEW_PRODUCT_TYPES, "arnold": RENDER_PRODUCT_TYPES, - "labs::karma::2.0": RENDER_PRODUCT_TYPES, "karma": RENDER_PRODUCT_TYPES, "usdrender": RENDER_PRODUCT_TYPES, "usdrender_rop": RENDER_PRODUCT_TYPES, - "vray_renderer": RENDER_PRODUCT_TYPES + "vray_renderer": RENDER_PRODUCT_TYPES, + "labs::karma::2.0": RENDER_PRODUCT_TYPES, + "kinefx::rop_fbxanimoutput": FBX_ONLY_PRODUCT_TYPES, + "kinefx::rop_fbxcharacteroutput": FBX_ONLY_PRODUCT_TYPES, + "kinefx::rop_gltfcharacteroutput": GLTF_PRODUCT_TYPES, + "rop_gltf": GLTF_PRODUCT_TYPES } node_type_product_types_default = NodeTypeProductTypes( From fe2de0cd21e6b507f19c12cf77c43954b65f3d67 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 22:24:37 +0200 Subject: [PATCH 29/36] Draft: Take the list from the Creator directly for now --- .../client/ayon_houdini/api/node_wrap.py | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py index 0f4c850d71..53789a0e3e 100644 --- a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py +++ b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py @@ -23,25 +23,13 @@ def make_publishable(node): # TODO: Move this choice of automatic 'imprint' to settings so studio can # configure which nodes should get automatically imprinted on creation -AUTO_CREATE_NODE_TYPES = { - "alembic", - "rop_alembic", - "geometry", - "rop_geometry", - "filmboxfbx", - "rop_fbx", - "usd", - "usd_rop", - "usdexport", - "comp", - "opengl", - "arnold", - "labs::karma::2.0", - "karma", - "usdrender", - "usdrender_rop", - "vray_renderer", -} +# TODO: Do not import and reload the creator plugin file +from ayon_houdini.plugins.create import create_generic +import importlib +importlib.reload(create_generic) +AUTO_CREATE_NODE_TYPES = set( + create_generic.CreateHoudiniGeneric.node_type_product_types.keys() +) def autocreate_publishable(node): From 71b20e0e5920bf9241714afd13ba06524fb4aecc Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 22:26:45 +0200 Subject: [PATCH 30/36] Cosmetics --- server_addon/houdini/client/ayon_houdini/api/node_wrap.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py index 53789a0e3e..0734da6da5 100644 --- a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py +++ b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py @@ -5,16 +5,13 @@ def make_publishable(node): # TODO: Can we make this imprinting much faster? Unfortunately # CreateContext initialization is very slow. - host = registered_host() context = CreateContext(host) - variant = node.name() - # Apply the instance creation to the node context.create( creator_identifier="io.ayon.creators.houdini.publish", - variant=variant, + variant=node.name(), pre_create_data={ "node": node } From 0125413f04ed2244061292709ef7620d7cf6af40 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 24 Jun 2024 22:37:58 +0200 Subject: [PATCH 31/36] Support many more node types for `get_output_parameter`, also see #692 --- server_addon/houdini/client/ayon_houdini/api/lib.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/server_addon/houdini/client/ayon_houdini/api/lib.py b/server_addon/houdini/client/ayon_houdini/api/lib.py index c44a93940b..a6b0934013 100644 --- a/server_addon/houdini/client/ayon_houdini/api/lib.py +++ b/server_addon/houdini/client/ayon_houdini/api/lib.py @@ -107,6 +107,19 @@ def get_output_parameter(node): elif node_type == "labs::karma::2.0": return node.parm("picture") + if isinstance(node, hou.RopNode): + # Use the parm name fallback that SideFX applies for detecting output + # files from PDG/TOPs graphs for ROP nodes. See #ayon-core/692 + parm_names = [ + "vm_picture", "sopoutput", "dopoutput", "lopoutput", "picture", + "copoutput", "filename", "usdfile", "file", "output", + "outputfilepath", "outputimage", "outfile" + ] + for name in parm_names: + parm = node.parm(name) + if parm: + return parm + raise TypeError("Node type '%s' not supported" % node_type) From 6cb8b5d2c899cd5132d4d9399896252651e9aa17 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Jun 2024 17:00:51 +0200 Subject: [PATCH 32/36] Only create on RopNode type --- server_addon/houdini/client/ayon_houdini/api/node_wrap.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py index 0734da6da5..cc56066198 100644 --- a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py +++ b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py @@ -30,6 +30,10 @@ def make_publishable(node): def autocreate_publishable(node): + # For now only consider RopNode + if not isinstance(node, hou.RopNode): + return + node_type = node.type().name() if node_type in AUTO_CREATE_NODE_TYPES: make_publishable(node) From f689cee92928162f420092eeed7ac03b59391483 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Jun 2024 17:07:00 +0200 Subject: [PATCH 33/36] Fix import --- server_addon/houdini/client/ayon_houdini/api/node_wrap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py index cc56066198..42d46ffcfe 100644 --- a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py +++ b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py @@ -1,3 +1,5 @@ +import hou + from ayon_core.pipeline import registered_host from ayon_core.pipeline.create import CreateContext From 939ee37f69825eb3a563bf9ca3d8c7438757653b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Jun 2024 17:12:51 +0200 Subject: [PATCH 34/36] Fix using node name by default with `$OS` from publisher UI --- .../client/ayon_houdini/api/node_wrap.py | 2 +- .../plugins/create/create_generic.py | 20 ++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py index 42d46ffcfe..e484bb5265 100644 --- a/server_addon/houdini/client/ayon_houdini/api/node_wrap.py +++ b/server_addon/houdini/client/ayon_houdini/api/node_wrap.py @@ -13,7 +13,7 @@ def make_publishable(node): # Apply the instance creation to the node context.create( creator_identifier="io.ayon.creators.houdini.publish", - variant=node.name(), + variant="__use_node_name__", pre_create_data={ "node": node } diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index 5d63f3fed8..de6e3d5d90 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -202,6 +202,9 @@ class NodeTypeProductTypes: class CreateHoudiniGeneric(plugin.HoudiniCreator): """Generic creator to ingest arbitrary products""" + USE_DEFAULT_PRODUCT_TYPE = "__use_node_default__" + USE_DEFAULT_NODE_NAME = "__use_node_name__" + host_name = "houdini" identifier = "io.ayon.creators.houdini.publish" @@ -211,7 +214,8 @@ class CreateHoudiniGeneric(plugin.HoudiniCreator): description = "Make any ROP node publishable." render_target = "local_no_render" - default_variant = "$OS" + default_variant = USE_DEFAULT_NODE_NAME + default_variants = ["Main", USE_DEFAULT_NODE_NAME] # TODO: Move this to project settings node_type_product_types: Dict[str, NodeTypeProductTypes] = { @@ -248,8 +252,6 @@ class CreateHoudiniGeneric(plugin.HoudiniCreator): strict=False ) - USE_DEFAULT_PRODUCT_TYPE = "__use_node_default__" - def get_detail_description(self): return "Publish any ROP node." @@ -275,6 +277,8 @@ def create(self, product_name, instance_data, pre_create_data): else: nodes = hou.selectedNodes() + source_variant = instance_data["variant"] + for node in nodes: if node.parm("AYON_creator_identifier"): # Continue if already existing attributes @@ -292,14 +296,20 @@ def create(self, product_name, instance_data, pre_create_data): node_type, self.node_type_product_types_default ).default + # Allow variant to be based off of the created node name + variant = source_variant + if variant == self.USE_DEFAULT_NODE_NAME: + variant = node.name() + product_name = self._get_product_name_dynamic( self.create_context.project_name, folder_entity=folder_entity, task_entity=task_entity, - variant=instance_data["variant"], + variant=variant, product_type=node_product_type ) + instance_data["variant"] = variant instance_data["instance_node"] = node.path() instance_data["instance_id"] = node.path() created_instance = CreatedInstance( @@ -547,7 +557,7 @@ def create_attribute_def_parms(self, hou.StringParmTemplate( "AYON_variant", "Variant", num_components=1, - default_value=(self.default_variant,) + default_value=("$OS",) ), hou.StringParmTemplate( "AYON_productName", "Product Name", From 0579901e3754758b60576e4f3ab388e175a3875e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Jun 2024 17:21:17 +0200 Subject: [PATCH 35/36] Update server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py Co-authored-by: Mustafa Taher --- .../houdini/client/ayon_houdini/plugins/create/create_generic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index de6e3d5d90..3f0add720d 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -232,6 +232,7 @@ class CreateHoudiniGeneric(plugin.HoudiniCreator): "opengl": REVIEW_PRODUCT_TYPES, "arnold": RENDER_PRODUCT_TYPES, "karma": RENDER_PRODUCT_TYPES, + "ifd": RENDER_PRODUCT_TYPES, "usdrender": RENDER_PRODUCT_TYPES, "usdrender_rop": RENDER_PRODUCT_TYPES, "vray_renderer": RENDER_PRODUCT_TYPES, From 47658e68fc5c5eeb302ad0d3b137798c44276ed4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 26 Jun 2024 23:52:06 +0200 Subject: [PATCH 36/36] Add more issues as todo --- .../client/ayon_houdini/plugins/create/create_generic.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py index 3f0add720d..7012e42f13 100644 --- a/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py +++ b/server_addon/houdini/client/ayon_houdini/plugins/create/create_generic.py @@ -563,6 +563,11 @@ def create_attribute_def_parms(self, hou.StringParmTemplate( "AYON_productName", "Product Name", num_components=1, + # TODO: This default value should adhere more to AYON's + # product name templates. We might need to avoid making + # this field editable and set up callbacks so it evaluates + # dynamically using the `get_product_name` logic based off + # of other attributes on the node default_value=('`chs("AYON_productType")``chs("AYON_variant")`',) ), hou.ToggleParmTemplate( @@ -668,6 +673,8 @@ def get_instance_attr_defs(self): label="Review", tooltip="Mark as reviewable", default=True), + # TODO: This render target isn't actually 'accurate' for render + # instances that should also allow a split export+render workflow. EnumDef("render_target", items=render_target_items, label="Render target",