From 24dfc22c74fc85eebf098fee8d83a7a7fe5eb6af Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 20 May 2024 07:55:22 +0200 Subject: [PATCH] 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)