diff --git a/app.py b/app.py index bc3d925..dcf16b3 100644 --- a/app.py +++ b/app.py @@ -50,23 +50,21 @@ def init_app(self): "and try again." ) - def execute_render(self, node: hou.Node, network: str): + def execute_render(self, node: hou.Node): """Start farm render Args: node (hou.Node): RenderMan node - network (str): Network type """ - self.handler.execute_render(node, network) + self.handler.execute_render(node) - def submit_to_farm(self, node: hou.Node, network: str): + def submit_to_farm(self, node: hou.Node): """Start local render Args: node (hou.Node): RenderMan node - network (str): Network type """ - self.handler.submit_to_farm(node, network) + self.handler.submit_to_farm(node) def copy_to_clipboard(self, node, network=None): """Copy render path to clipboard @@ -80,10 +78,7 @@ def copy_to_clipboard(self, node, network=None): @staticmethod def get_all_renderman_nodes() -> tuple[Node]: """Get all nodes from node type sgtk_hdprman""" - lop_nodes = hou.lopNodeTypeCategory().nodeType("sgtk_hdprman").instances() - rop_nodes = hou.ropNodeTypeCategory().nodeType("sgtk_ris").instances() - nodes = lop_nodes + rop_nodes - return nodes + return hou.ropNodeTypeCategory().nodeType("sgtk_ris").instances() def get_output_path( self, node: hou.Node, aov_name: str, network: str = "rop" @@ -107,14 +102,22 @@ def validate_node(self, node: hou.Node, network: str) -> str: """ return self.handler.validate_node(node, network) - def setup_aovs(self, node: hou.Node, show_notif: bool = True) -> bool: + def setup_light_groups(self, node: hou.Node) -> bool: + """Setup light groups on the light nodes + + Args: + node (hou.Node): RenderMan node + """ + return self.handler.setup_light_groups(node) + + def setup_aovs(self, node: hou.Node, show_notification: bool = True) -> bool: """Setup outputs on the RenderMan node with correct aovs Args: node (hou.Node): RenderMan node - show_notif (bool): Show notification when successfully set up AOVs + show_notification (bool): Show notification when successfully set up AOVs """ - return self.handler.setup_aovs(node, show_notif) + return self.handler.setup_aovs(node, show_notification) def get_output_paths(self, node: hou.Node) -> list[str]: """Get output paths for the RenderMan node diff --git a/info.yml b/info.yml index eae9be4..e9ebb1e 100644 --- a/info.yml +++ b/info.yml @@ -40,8 +40,9 @@ configuration: items: key: { type: str } type: { type: str } + # value: { type: str } expression: { type: str } - group: { type: str, allows_empty: True } + group: { type: str } post_task_script: type: str diff --git a/otls/PythonModule.py b/otls/PythonModule.py index 65821da..6cf3f03 100644 --- a/otls/PythonModule.py +++ b/otls/PythonModule.py @@ -14,9 +14,9 @@ def render(node: hou.Node, on_farm: bool = False): return if on_farm: - app.submit_to_farm(node, "rop") + app.submit_to_farm(node) else: - app.execute_render(node, "rop") + app.execute_render(node) def copy_to_clipboard(node: hou.Node): @@ -24,17 +24,25 @@ def copy_to_clipboard(node: hou.Node): eng = sgtk.platform.current_engine() app = eng.apps["tk-houdini-renderman"] - app.copy_to_clipboard(node.node("render"), "rop") + app.copy_to_clipboard(node.node("render")) hou.ui.displayMessage("Copied path to clipboard.") -def setup_aovs(node: hou.Node, show_notif: bool = True) -> bool: +def setup_light_groups(node: hou.Node) -> bool: import sgtk eng = sgtk.platform.current_engine() app = eng.apps["tk-houdini-renderman"] - return app.setup_aovs(node, show_notif) + return app.setup_light_groups(node) + + +def setup_aovs(node: hou.Node, show_notification: bool = True) -> bool: + import sgtk + + eng = sgtk.platform.current_engine() + app = eng.apps["tk-houdini-renderman"] + return app.setup_aovs(node, show_notification) def get_output_paths(node: hou.Node): diff --git a/otls/create_otl.py b/otls/create_otl.py index fdd7724..df6996c 100644 --- a/otls/create_otl.py +++ b/otls/create_otl.py @@ -1,6 +1,9 @@ -import os +import logging import re +import string import xml.etree.ElementTree as ET +from enum import Enum +from typing import Callable import hou @@ -9,605 +12,1277 @@ # exec(open(r"D:\Developer\Pipeline\tk-houdini-renderman\otls\create_otl.py").read()) -# -# Functions -# -def convert_naming_scheme(naming_scheme): - if naming_scheme == hou.parmNamingScheme.Base1: - return "1", "2", "3", "4" - elif naming_scheme == hou.parmNamingScheme.XYZW: - return "x", "y", "z", "w" - elif naming_scheme == hou.parmNamingScheme.XYWH: - return "x", "y", "w", "h" - elif naming_scheme == hou.parmNamingScheme.UVW: - return "u", "v", "w" - elif naming_scheme == hou.parmNamingScheme.RGBA: - return "r", "g", "b", "a" - elif naming_scheme == hou.parmNamingScheme.MinMax: - return "min", "max" - elif naming_scheme == hou.parmNamingScheme.MaxMin: - return "max", "min" - elif naming_scheme == hou.parmNamingScheme.StartEnd: - return "start", "end" - elif naming_scheme == hou.parmNamingScheme.BeginEnd: - return "begin", "end" - - -def link_parm(origin, parm_name, level=1, prepend="", append=""): - org_parm = origin.parmTemplateGroup().find(parm_name) - if not org_parm: - print("parm not found: ", parm_name) - return - - parm_type = "ch" - if org_parm.dataType() == hou.parmData.String: - parm_type = "chsop" - - if org_parm.numComponents() == 1: - origin.parm(parm_name).setExpression( - '{}("{}{}")'.format(parm_type, "../" * level, prepend + parm_name + append) - ) - else: - scheme = convert_naming_scheme(org_parm.namingScheme()) - for i in range(org_parm.numComponents()): - origin.parm(parm_name + scheme[i]).setExpression( +class OTLTypes(Enum): + DRIVER = "driver" + LOP = "lop" + DRIVER_SG = "driver_sg" + LOP_SG = "lop_sg" + + +# 0: Beauty 16 bit DWAa +# 1: Shading 16 bit DWAa +# 2: Lighting 16 bit DWAa +# 3: Utility 32 bit ZIP +# 4: Deep +# X: Denoise +# X: Cryptomatte +class OutputIdentifier(Enum): + BEAUTY = "Beauty" + SHADING = "Shading" + LIGHTING = "Lighting" + UTILITY = "Utility" + CRYPTOMATTE = "Cryptomatte" + DEEP = "Deep" + + +OUTPUT_FILES = { + OutputIdentifier.BEAUTY: { + "asRGBA": True, + "bitdepth": "half", + "compression": "dwaa", + "options": { + "beauty": {"name": "Beauty + Alpha", "aovs": ["Ci", "a"], "default": True}, + }, + }, + OutputIdentifier.SHADING: { + "asRGBA": False, + "bitdepth": "half", + "compression": "dwaa", + "options": { + "albedo": {"name": "Albedo", "aovs": ["albedo"]}, + "emissive": { + "name": "Emissive", + "aovs": ["emissive"], + }, + "diffuse": { + "name": "(In)direct Diffuse", + "aovs": ["directDiffuse", "indirectDiffuse"], + }, + "diffuseU": { + "name": "(In)direct Diffuse Unoccluded", + "aovs": ["directDiffuseUnoccluded", "indirectDiffuseUnoccluded"], + }, + "specular": { + "name": "(In)direct Specular", + "aovs": ["directSpecular", "indirectSpecular"], + }, + "specularU": { + "name": "(In)direct Specular Unoccluded", + "aovs": ["directSpecularUnoccluded", "indirectSpecularUnoccluded"], + }, + "subsurface": {"name": "Subsurface", "aovs": ["albedo"]}, + "lobes_diffuse": { + "name": "(In)direct Diffuse", + "aovs": ["directDiffuseLobe", "indirectDiffuseLobe"], + }, + "lobes_specularPrimary": { + "name": "(In)direct Specular Primary", + "aovs": ["directSpecularPrimaryLobe", "indirectSpecularPrimaryLobe"], + }, + "lobes_specularRough": { + "name": "(In)direct Specular Rough", + "aovs": ["directSpecularRoughLobe", "indirectSpecularRoughLobe"], + }, + "lobes_specularClearcoat": { + "name": "(In)direct Specular Clearcoat", + "aovs": [ + "directSpecularClearcoatLobe", + "indirectSpecularClearcoatLobe", + ], + }, + "lobes_specularIridescence": { + "name": "(In)direct Specular Iridescence", + "aovs": [ + "directSpecularIridescenceLobe", + "indirectSpecularIridescenceLobe", + ], + }, + "lobes_specularFuzz": { + "name": "(In)direct Specular Fuzz", + "aovs": ["directSpecularFuzzLobe", "indirectSpecularFuzzLobe"], + }, + "lobes_specularGlass": { + "name": "(In)direct Specular Glass", + "aovs": ["directSpecularGlassLobe", "indirectSpecularGlassLobe"], + }, + "lobes_subsurface": { + "name": "Subsurface", + "aovs": ["subsurfaceLobe"], + }, + "lobes_transmissiveSingleScatter": { + "name": "Transmissive Single Scatter", + "aovs": ["transmissiveSingleScatterLobe"], + }, + "lobes_transmissiveGlass": { + "name": "Transmissive Glass", + "aovs": ["transmissiveGlassLobe"], + }, + }, + }, + OutputIdentifier.LIGHTING: { + "asRGBA": False, + "bitdepth": "half", + "compression": "dwaa", + "notes": { + "shadow": "Enable the Holdout tag for an object to show up in the shadow AOVs", + }, + "options": { + "shadow_shadowOccluded": {"name": "Occluded", "aovs": ["occluded"]}, + "shadow_shadowUnoccluded": {"name": "Unoccluded", "aovs": ["unoccluded"]}, + "shadow_shadow": {"name": "Shadow", "aovs": ["shadow"]}, + }, + }, + OutputIdentifier.UTILITY: { + "asRGBA": False, + "bitdepth": "full", + "compression": "zips", + "options": { + "curvature": {"name": "Curvature", "aovs": ["curvature"]}, + "motionVector": {"name": "Motion Vector World Space", "aovs": ["shadow"]}, + "motionVectorCamera": { + "name": "Motion Vector Camera Space", + "aovs": ["shadow"], + }, + "sep1": "-", + "pWorld": {"name": "Position (world-space)", "aovs": ["__Pworld"]}, + "nWorld": {"name": "Normal (world-space)", "aovs": ["__Nworld"]}, + "depthAA": { + "name": "Depth (Anti-Aliased) + Facing Ratio", + "aovs": ["__depth"], + }, + "depth": {"name": "Depth (Aliased)", "aovs": ["z"]}, + "st": {"name": "Texture Coordinates (UV maps)", "aovs": ["__st"]}, + "pRef": {"name": "Reference Position", "aovs": ["__Pref"]}, + "nRef": {"name": "Reference Normal", "aovs": ["__Nref"]}, + "pRefWorld": {"name": "Reference World Position", "aovs": ["__WPref"]}, + "nRefWorld": {"name": "Reference World Normal", "aovs": ["__WNref"]}, + }, + }, + OutputIdentifier.CRYPTOMATTE: { + "asRGBA": False, + "bitdepth": "half", + "compression": "zips", + "options": { + "cryptoMaterial": { + "name": "Material", + "aovs": [], + }, + "cryptoName": { + "name": "Name", + "aovs": [], + }, + "cryptoPath": { + "name": "Path", + "aovs": [], + }, + }, + }, + OutputIdentifier.DEEP: { + "asRGBA": False, + "bitdepth": "half", + "compression": "dwaa", + "options": { + "deep": { + "name": "Deep", + "aovs": ["Ci", "a"], + }, + }, + }, +} + +PARM_MAPPING = { + "ri_statistics_level": "xn__ristatisticslevel_n3ak", + "ri_statistics_xmlfilename": "xn__ristatisticsxmlfilename_febk", + "ri_hider_samplemotion": "xn__rihidersamplemotion_w6af", + "integrator": "xn__riintegratorname_01ak", + # Cryptomatte + "ri:samplefilter0:name": "xn__risamplefilter0name_w6an", + "ri:samplefilter0:PxrCryptomatte:filename": "xn__risamplefilter0PxrCryptomattefilename_70bno", + "ri:samplefilter0:PxrCryptomatte:layer": "xn__risamplefilter0PxrCryptomattelayer_cwbno", +} + + +class CreateOtl: + def __init__(self, otl_type: OTLTypes): + self._otl_type = otl_type + + self._is_shotgrid = otl_type.value.endswith("sg") + self._is_lop = otl_type.value.startswith("lop") + self._context_type = "lop" if self._is_lop else "driver" + self._context_name = "stage" if self._is_lop else "out" + + self._node_name = "sgtk_ris" if self._is_shotgrid else "RenderMan_Renderer" + self._otl_name = "SGTK_RenderMan" if self._is_shotgrid else "RenderMan_Renderer" + + def _parm_name(self, sop_name: str) -> str: + """Get the parameter name for the current context + + Args: + sop_name (str): Source name + Returns: + str: Corrected name + """ + if self._is_lop: + if sop_name in PARM_MAPPING.keys(): + return PARM_MAPPING[sop_name] + return sop_name + + def _link_parm( + self, + node: hou.Node, + parm_name: str, + level: int = 1, + prepend: str = "", + append: str = "", + ): + """ + Link a parameter from the source node to a destination node + + Args: + node (hou.None): Node to add the expression to + parm_name (str): Parameter key on the source node + level (int): Levels between source and destination node + prepend (str): String to prepend to source parameter key + append (str): String to append to source parameter key + """ + dist_name = self._parm_name(parm_name) + org_parm = node.parmTemplateGroup().find(dist_name) + if not org_parm: + logging.error("parm not found: ", parm_name) + return + + if self._is_lop and level != 1: + level -= 1 + + parm_type = "ch" + if org_parm.dataType() == hou.parmData.String: + parm_type = "chsop" + + if org_parm.numComponents() == 1: + node.parm(dist_name).setExpression( '{}("{}{}")'.format( - parm_type, "../" * level, prepend + parm_name + append + scheme[i] + parm_type, "../" * level, prepend + parm_name + append + ) + ) + else: + scheme = self._convert_naming_scheme(org_parm.namingScheme()) + for i in range(org_parm.numComponents()): + node.parm(dist_name + scheme[i]).setExpression( + '{}("{}{}")'.format( + parm_type, + "../" * level, + prepend + parm_name + append + scheme[i], + ) + ) + + def _link_deep_parms( + self, node: hou.Node, parms: list[str], prepend: str = "", append: str = "" + ): + """ + Link a list of parameters from the source node to a destination node, including items in folders + + Args: + node (hou.None): Node to add the expression to + parms (list[str]): A list of parameter keys on the source node + prepend (str): String to prepend to source parameter key + append (str): String to append to source parameter key + """ + for parm in parms: + if parm.type() == hou.parmTemplateType.Folder: + self._link_deep_parms(node, parm.parmTemplates(), prepend, append) + else: + self._link_parm(node, parm.name(), 2, prepend, append) + + def _set_deep_conditional( + self, + parms: tuple[hou.ParmTemplate, ...], + cond_type: hou.parmCondType, + modifier: Callable[[str], str], + ): + """ + Modify the conditionals of a list of parm templates + + Args: + parms (tuple[hou.ParmTemplate, ...]): List of ParmTemplates to modify + cond_type (hou.parmCondType): The type of conditional to modify + modifier (Callable[[str], str]): The function which is called on the source conditional + """ + for parm in parms: + if parm.type() == hou.parmTemplateType.Folder: + new_parms = self._set_deep_conditional( + parm.parmTemplates(), cond_type, modifier + ) + parm.setParmTemplates(new_parms) + elif cond_type in parm.conditionals(): + parm.setConditional(cond_type, modifier(parm.conditionals()[cond_type])) + return parms + + def _reference_parm( + self, + node: hou.Node, + dest: hou.ParmTemplateGroup, + parm: str, + conditional: list[hou.parmCondType, str] = None, + ): + """ + Create a reference of a parameter to a template group + + Args: + node (hou.Node): The node to get the parameter from + dest (hou.ParmTemplateGroup): The ParmTemplateGroup to add the reference to + parm (str): The parameter key + conditional (list[hou.parmCondType, str]): An optional conditional + """ + org_parms = node.parmTemplateGroup() + org_parm = org_parms.find(parm) + if not org_parm: + logging.error("Parm not found: ", parm) + return + + if conditional: + org_parm.setConditional(conditional[0], conditional[1]) + + if hasattr(dest, "append"): + dest.append(org_parm) + elif hasattr(dest, "addParmTemplate"): + dest.addParmTemplate(org_parm) + else: + logging.error("Undefined method") + return + + self._link_parm(node, parm) + + def _rename_deep_parms( + self, parms: list[hou.ParmTemplate], prepend: str = "", append: str = "" + ) -> list[hou.ParmTemplate]: + """ + Prepend and/or append a string to a list of parameter templates + + Args: + parms (list[hou.ParmTemplate]): List of ParmTemplates to modify + prepend (str): String to prepend to the name + append (str): String to append to the name + + Returns: + list[hou.ParmTemplate]: Modified list of ParmTemplates + """ + for parm in parms: + parm.setName(prepend + parm.name() + append) + if parm.type() == hou.parmTemplateType.Folder: + renamed = self._rename_deep_parms(parm.parmTemplates(), prepend, append) + parm.setParmTemplates(renamed) + return parms + + def _set_parm(self, node: hou.Node, parm_name: str, value: any): + """ + Set the value of a parameter, with the context corrected name + + Args: + node (hou.Node): Node containing parameter to modify + parm_name (str): Name of the parameter + value (any): Value to set parameter to + """ + node.parm(self._parm_name(parm_name)).set(value) + + def _set_parm_expression(self, node: hou.Node, parm_name: str, value: str): + """ + Set the value of a parameter, with the context corrected name, to an expression + + Args: + node (hou.Node): Node containing parameter to modify + parm_name (str): Name of the parameter + value (str): Expression to set parameter to + """ + node.parm(self._parm_name(parm_name)).setExpression(value) + + @staticmethod + def _setup_custom_aovs(folder: hou.FolderParmTemplate): + """ + Add custom AOV collapsible block + + Args: + folder (hou.FolderParmTemplate): Folder to add the custom AOV block to + """ + name = folder.label().replace(" ", "") + + disable = "{{aov{}CustomDisable_# == 1}}".format(name) + custom_folder = hou.FolderParmTemplate( + "{}Custom".format(name.lower()), + "Custom AOVs", + folder_type=hou.folderType.Collapsible, + ) + custom = hou.FolderParmTemplate( + "{}CustomAOVs".format(name.lower()), + "Extra Image Planes", + folder_type=hou.folderType.MultiparmBlock, + ) + custom.addParmTemplate( + hou.ToggleParmTemplate("aov{}CustomDisable_#".format(name), "Disable AOV") + ) + custom.addParmTemplate( + hou.StringParmTemplate( + "aov{}CustomName_#".format(name), "Name", 1, disable_when=disable + ) + ) + custom.addParmTemplate( + hou.MenuParmTemplate( + "aov{}CustomSource_#".format(name), + "Source", + ("color", "float", "integer", "vector", "normal", "point"), + ("Color", "Float", "Integer", "Vector", "Normal", "Point"), + join_with_next=True, + disable_when=disable, + ) + ) + custom.addParmTemplate( + hou.StringParmTemplate( + "aov{}CustomLPE_#".format(name), + "LPE", + 1, + is_label_hidden=True, + disable_when=disable, + ) + ) + + custom_folder.addParmTemplate(custom) + folder.addParmTemplate(custom_folder) + + @staticmethod + def _get_metadata_block(): + """ + Get a MultiparmBlock to set up metadata with + + Returns: + hou.FolderParmTemplate: MultiparmBlock with metadata entries + """ + metadata_entries = hou.FolderParmTemplate( + "metadata_entries", "Entries", folder_type=hou.folderType.MultiparmBlock + ) + metadata_entries.addParmTemplate( + hou.StringParmTemplate("metadata_#_key", "Key", 1, join_with_next=True) + ) + + metadata_types = [ + {"key": "float", "name": "Float", "type": "float", "components": 1}, + {"key": "int", "name": "Integer", "type": "int", "components": 1}, + {"key": "string", "name": "String", "type": "string", "components": 1}, + {"key": "v2f", "name": "Vector 2 Float", "type": "float", "components": 2}, + {"key": "v2i", "name": "Vector 2 Int", "type": "int", "components": 2}, + {"key": "v3f", "name": "Vector 3 Float", "type": "float", "components": 3}, + {"key": "v3i", "name": "Vector 3 Int", "type": "int", "components": 3}, + {"key": "box2f", "name": "Box 2 Float", "type": "float", "components": 4}, + {"key": "box2i", "name": "Box 2 Int", "type": "int", "components": 4}, + {"key": "m33f", "name": "Matrix 3x3", "type": "float", "components": 9}, + {"key": "m44f", "name": "Matrix 4x4", "type": "float", "components": 16}, + ] + metadata_names = [md_type["key"] for md_type in metadata_types] + metadata_labels = [md_type["name"] for md_type in metadata_types] + + metadata_entries.addParmTemplate( + hou.MenuParmTemplate( + "metadata_#_type", " Type", metadata_names, metadata_labels + ) + ) + + for md_type in metadata_types: + if md_type["type"] == "float": + parm = hou.FloatParmTemplate( + f"metadata_#_{md_type['key']}", "Value", md_type["components"] + ) + elif md_type["type"] == "int": + parm = hou.IntParmTemplate( + f"metadata_#_{md_type['key']}", "Value", md_type["components"] + ) + elif md_type["type"] == "string": + parm = hou.StringParmTemplate( + f"metadata_#_{md_type['key']}", "Value", md_type["components"] + ) + parm.setConditional( + hou.parmCondType.HideWhen, f"{{ metadata_#_type != {md_type['key']} }}" + ) + metadata_entries.addParmTemplate(parm) + + return metadata_entries + + def _add_output_file( + self, output_id: OutputIdentifier, folder: hou.FolderParmTemplate + ): + """ + Add aov toggles for a specific output file + + Args: + output_id (OutputIdentifier): Output identifier + folder (hou.FolderParmTemplate): Folder to add the toggles to + """ + output_file = OUTPUT_FILES[output_id] + subfolders = {} + + for key, value in output_file["options"].items(): + add_folder = folder + if "_" in key: + subfolder_id = key.split("_")[0] + if subfolder_id not in subfolders: + subfolder_name = string.capwords( + self._space_camel_case(subfolder_id) + ) + add_folder = hou.FolderParmTemplate( + subfolder_id, + subfolder_name, + folder_type=hou.folderType.Simple, + ) + + # Add folder note + if "notes" in output_file and subfolder_id in output_file["notes"]: + note = hou.LabelParmTemplate( + f"{subfolder_id}Note", + "Note", + column_labels=(output_file["notes"][subfolder_id],), + ) + note.setLabelParmType(hou.labelParmType.Message) + add_folder.addParmTemplate(note) + + subfolders[subfolder_id] = add_folder + else: + add_folder = subfolders[subfolder_id] + + if type(value) is str: + add_folder.addParmTemplate(hou.SeparatorParmTemplate(f"aov{key}")) + else: + toggle = hou.ToggleParmTemplate(key, value["name"]) + if "default" in value: + toggle.setDefaultValue(value["default"]) + add_folder.addParmTemplate(toggle) + + for subfolder in subfolders.values(): + folder.addParmTemplate(subfolder) + + @staticmethod + def _convert_naming_scheme(naming_scheme: hou.parmNamingScheme) -> tuple[str, ...]: + """ + Convert Houdini naming scheme to name suffixes + + Args: + naming_scheme (hou.parmNamingScheme): The naming scheme + + Returns: + tuple[str, ...]: Suffixes for the components + """ + if naming_scheme == hou.parmNamingScheme.Base1: + return "1", "2", "3", "4" + elif naming_scheme == hou.parmNamingScheme.XYZW: + return "x", "y", "z", "w" + elif naming_scheme == hou.parmNamingScheme.XYWH: + return "x", "y", "w", "h" + elif naming_scheme == hou.parmNamingScheme.UVW: + return "u", "v", "w" + elif naming_scheme == hou.parmNamingScheme.RGBA: + return "r", "g", "b", "a" + elif naming_scheme == hou.parmNamingScheme.MinMax: + return "min", "max" + elif naming_scheme == hou.parmNamingScheme.MaxMin: + return "max", "min" + elif naming_scheme == hou.parmNamingScheme.StartEnd: + return "start", "end" + elif naming_scheme == hou.parmNamingScheme.BeginEnd: + return "begin", "end" + + @staticmethod + def _space_camel_case(text: str) -> str: + """ + Convert camel case to spaced string + + Args: + text (str): Camel cased string + + Returns: + str: Spaced string + """ + return re.sub(r"((?<=[a-z])[A-Z]|(?" + + def get_parm_name(self) -> str: + """ + Get the parm name of this AOVOption + + Returns: + str: Parm name + """ + if self.group: + return f"{self.group}_{self.key}" + return self.key + + def is_active(self, node: hou.Node) -> bool: + return node.parm(self.get_parm_name()).eval() == 1 + + +@dataclass +class AOVSeparator: + key: str + + +@dataclass +class CustomAOV: + name: str + type: str + lpe: str + + def get_format(self): + if self.type == "color": + return "color3f" + if self.type == "float": + return "float" + if self.type == "integer": + return "int" + if self.type == "vector" or self.type == "normal" or self.type == "point": + return "color3f" + + +@dataclass +class OutputFile: + identifier: OutputIdentifier + as_rgba: bool + bitdepth: Bitdepth + compression: Compression + options: Optional[list[Union[AOVOption, AOVSeparator]]] + notes: Optional[dict] = field(default_factory=dict) + has_custom: Optional[bool] = False + can_denoise: Optional[bool] = True + + def __repr__(self): + return f"" + + def has_active_aovs(self, node: hou.Node) -> bool: + for option in self.options: + if type(option) is not AOVSeparator: + if option.is_active(node): + return True + + return False + + def has_active_custom_aovs(self, node: hou.Node) -> bool: + if self.has_custom: + name = self.identifier + count = node.parm(f"{name.lower()}CustomAOVs").eval() + for i in range(1, count + 1): + if not node.parm(f"aov{name}CustomDisable_{i}").evalAsInt(): + return True + + return False + + def get_active_custom_aovs(self, node: hou.Node) -> list[CustomAOV]: + aovs = [] + if self.has_custom: + name = self.identifier + count = node.parm(f"{name.lower()}CustomAOVs").eval() + for i in range(1, count + 1): + if not node.parm(f"aov{name}CustomDisable_{i}").evalAsInt(): + aov_name = node.parm(f"aov{name}CustomName_{i}").evalAsString() + aov_type = node.parm(f"aov{name}CustomSource_{i}").evalAsString() + aov_value = node.parm(f"aov{name}CustomLPE_{i}").evalAsString() + + if " " in aov_name: + raise Exception( + f'A custom aov under {name} has an invalid name: "{aov_name}"' + ) + + aovs.append(CustomAOV(aov_name, aov_type, aov_value)) + + is_lop = isinstance(node, hou.LopNode) + + # Add light groups to custom AOVs if Lighting file + if self.identifier == OutputIdentifier.LIGHTING: + light_group_count = node.parm("light_groups_select").eval() + + for j in range(1, light_group_count + 1): + light_group_name = node.parm(f"light_group_name_{j}").eval() + + prefix = "" if is_lop else "color lpe:" + + aovs.append( + CustomAOV( + f"LG_{light_group_name}", + "color", + f"{prefix}C.*", + ) + ) + + # Add tees to custom AOVs if Utility file + if self.identifier == OutputIdentifier.UTILITY: + tee_count = node.parm("tees").eval() + + for j in range(1, tee_count + 1): + tee_name = node.parm(f"teeName_{j}").eval() + aovs.append( + CustomAOV( + tee_name, + node.parm(f"teeType_{j}").evalAsString(), + tee_name if is_lop else "", + ) + ) + + return aovs + + def get_aovs(self): + aovs = [] + for option in self.options: + if type(option) is not AOVSeparator: + aovs += option.aovs + + return aovs + + def get_active_aovs(self, node: hou.Node) -> list[str]: + active_aovs = [] + for option in self.options: + if type(option) is not AOVSeparator: + if option.is_active(node): + active_aovs += option.aovs + + return active_aovs + + def get_inactive_aovs(self, node: hou.Node) -> list[str]: + inactive_aovs = [] + for option in self.options: + if type(option) is not AOVSeparator: + if not option.is_active(node): + inactive_aovs += option.aovs + + return inactive_aovs + + +output_files = [ + OutputFile( + OutputIdentifier.BEAUTY, + True, + Bitdepth.HALF, + Compression.DWAA, + [AOVOption("beauty", "Beauty + Alpha", ["Ci", "a"], True)], + ), + OutputFile( + OutputIdentifier.SHADING, + False, + Bitdepth.HALF, + Compression.DWAA, + [ + AOVOption("albedo", "Albedo", ["albedo"]), + AOVOption("emissive", "Emissive", ["emissive"]), + AOVOption( + "diffuse", "(In)direct Diffuse", ["directDiffuse", "indirectDiffuse"] + ), + AOVOption( + "diffuseU", + "(In)direct Diffuse Unoccluded", + ["directDiffuseUnoccluded", "indirectDiffuseUnoccluded"], + ), + AOVOption( + "specular", + "(In)direct Specular", + ["directSpecular", "indirectSpecular"], + ), + AOVOption( + "specularU", + "(In)direct Specular Unoccluded", + ["directSpecularUnoccluded", "indirectSpecularUnoccluded"], + ), + AOVOption("subsurface", "Subsurface", ["albedo"]), + AOVOption( + "diffuse", + "(In)direct Diffuse", + ["directDiffuseLobe", "indirectDiffuseLobe"], + group="lobes", + ), + AOVOption( + "specularPrimary", + "(In)direct Specular Primary", + ["directSpecularPrimaryLobe", "indirectSpecularPrimaryLobe"], + group="lobes", + ), + AOVOption( + "specularRough", + "(In)direct Specular Rough", + ["directSpecularRoughLobe", "indirectSpecularRoughLobe"], + group="lobes", + ), + AOVOption( + "specularClearcoat", + "(In)direct Specular Clearcoat", + [ + "directSpecularClearcoatLobe", + "indirectSpecularClearcoatLobe", + ], + group="lobes", + ), + AOVOption( + "specularIridescence", + "(In)direct Specular Iridescence", + [ + "directSpecularIridescenceLobe", + "indirectSpecularIridescenceLobe", + ], + group="lobes", + ), + AOVOption( + "specularFuzz", + "(In)direct Specular Fuzz", + ["directSpecularFuzzLobe", "indirectSpecularFuzzLobe"], + group="lobes", + ), + AOVOption( + "specularGlass", + "(In)direct Specular Glass", + ["directSpecularGlassLobe", "indirectSpecularGlassLobe"], + group="lobes", + ), + AOVOption("subsurface", "Subsurface", ["subsurfaceLobe"], group="lobes"), + AOVOption( + "transmissiveSingleScatter", + "Transmissive Single Scatter", + ["transmissiveSingleScatterLobe"], + group="lobes", + ), + AOVOption( + "transmissiveGlass", + "Transmissive Glass", + ["transmissiveGlassLobe"], + group="lobes", + ), + ], + has_custom=True, + ), + OutputFile( + OutputIdentifier.LIGHTING, + False, + Bitdepth.HALF, + Compression.DWAA, + [ + AOVOption("shadowOccluded", "Occluded", ["occluded"], group="shadow"), + AOVOption("shadowUnoccluded", "Unoccluded", ["unoccluded"], group="shadow"), + AOVOption("shadow", "Shadow", ["shadow"], group="shadow"), + ], + notes={ + "shadow": "Enable the Holdout tag for an object to show up in the shadow AOVs", + }, + has_custom=True, + ), + OutputFile( + OutputIdentifier.UTILITY, + False, + Bitdepth.FULL, + Compression.ZIPS, + [ + AOVOption("curvature", "Curvature", ["curvature"]), + AOVOption("motionVector", "Motion Vector World Space", ["dPdtime"]), + AOVOption( + "motionVectorCamera", + "Motion Vector Camera Space", + ["dPcameradtime"], + ), + AOVSeparator("sep1"), + AOVOption("pWorld", "Position (world-space)", ["__Pworld"]), + AOVOption("nWorld", "Normal (world-space)", ["__Nworld"]), + AOVOption( + "depthAA", + "Depth (Anti-Aliased) + Facing Ratio", + ["__depth"], + ), + AOVOption("depth", "Depth (Aliased)", ["z"]), + AOVOption("st", "Texture Coordinates (UV maps)", ["__st"]), + AOVOption("pRef", "Reference Position", ["__Pref"]), + AOVOption("nRef", "Reference Normal", ["__Nref"]), + AOVOption("pRefWorld", "Reference World Position", ["__WPref"]), + AOVOption("nRefWorld", "Reference World Normal", ["__WNref"]), + ], + has_custom=True, + can_denoise=False, + ), + OutputFile( + OutputIdentifier.DEEP, + True, + Bitdepth.HALF, + Compression.DWAA, + [ + AOVOption( + "deep", + "Deep", + ["Ci", "a"], + ), + ], + can_denoise=False, + ), + OutputFile( + OutputIdentifier.CRYPTOMATTE, + False, + Bitdepth.HALF, + Compression.ZIPS, + [ + AOVOption("cryptoMaterial", "Material", ["user:__materialid"]), + AOVOption("cryptoName", "Name", ["identifier:object"]), + AOVOption("cryptoPath", "Path", ["identifier:name"]), + ], + ), +] diff --git a/python/datamodel/lpe.py b/python/datamodel/lpe.py new file mode 100644 index 0000000..541ca91 --- /dev/null +++ b/python/datamodel/lpe.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + +from .render_engine import RenderEngine + + +@dataclass +class LPEControl: + renderer: RenderEngine + lop_control: str + lop_light_group: str + sop_light_group: str + + def get_control(self) -> str: + return self.lop_control + + def get_light_group(self, is_lop: bool = False) -> str: + return self.lop_light_group if is_lop else self.sop_light_group diff --git a/python/datamodel/metadata.py b/python/datamodel/metadata.py new file mode 100644 index 0000000..0ae352f --- /dev/null +++ b/python/datamodel/metadata.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + + +@dataclass +class MetaData: + key: str + type: str + value: any diff --git a/python/datamodel/render_engine.py b/python/datamodel/render_engine.py new file mode 100644 index 0000000..e64e017 --- /dev/null +++ b/python/datamodel/render_engine.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class RenderEngine(str, Enum): + RENDERMAN = "RenderMan" + KARMA = "Karma" diff --git a/python/tk_houdini_renderman/farm_dialog.py b/python/tk_houdini_renderman/farm_dialog.py index 321161b..895ad11 100644 --- a/python/tk_houdini_renderman/farm_dialog.py +++ b/python/tk_houdini_renderman/farm_dialog.py @@ -156,7 +156,7 @@ def __submit_to_farm(self): houdini_version = str(houdini_version[0]) + "." + str(houdini_version[1]) if self.network == "lop": - render_rop_node = os.path.join(self.node.path(), "rop_usdrender") + render_rop_node = os.path.join(self.node.path(), "render") render_rop_node = render_rop_node.replace(os.sep, "/") else: @@ -182,7 +182,8 @@ def __submit_to_farm(self): "EnvironmentKeyValue0 = RENDER_ENGINE = RenderMan", ] - if post_task_script: + # TODO create post task script for lop denoise renders + if self.network == "rop" and post_task_script: job_info.append("PostTaskScript=" + post_task_script) for i, path in enumerate(self.render_paths): diff --git a/python/tk_houdini_renderman/handler.py b/python/tk_houdini_renderman/handler.py index 98378da..a01ae56 100644 --- a/python/tk_houdini_renderman/handler.py +++ b/python/tk_houdini_renderman/handler.py @@ -1,5 +1,4 @@ # MIT License -import json # Copyright (c) 2021 Netherlands Film Academy @@ -21,14 +20,20 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import json import os import platform import re import hou import sgtk +from PySide2.QtWidgets import QMessageBox from .farm_dialog import FarmSubmission +from ..datamodel import aov_file +from ..datamodel.lpe import LPEControl +from ..datamodel.metadata import MetaData +from ..datamodel.render_engine import RenderEngine class TkRenderManNodeHandler(object): @@ -37,13 +42,26 @@ def __init__(self, app): self.app = app self.sg = self.app.shotgun - def submit_to_farm(self, node: hou.Node, network: str): + @staticmethod + def _error(comment: str, error: Exception): + QMessageBox.critical( + hou.qt.mainWindow(), + "Error", + f"{comment}:\n{error}", + ) + + def submit_to_farm(self, node: hou.Node): """Start farm render Args: node (hou.Node): RenderMan node - network (str): Network type """ + if not self.setup_light_groups(node, RenderEngine.RENDERMAN, False): + return + if not self.setup_aovs(node, False): + return + + is_lop = isinstance(node, hou.LopNode) render_name = node.parm("name").eval() # Create directories @@ -66,25 +84,37 @@ def submit_to_farm(self, node: hou.Node, network: str): global submission # Start submission panel submission = FarmSubmission( - self.app, node, file_name, 50, framerange, render_paths, network=network + self.app, + node, + file_name, + 50, + framerange, + render_paths, + network="lop" if is_lop else "rop", ) submission.show() - def execute_render(self, node: hou.Node, network: str): + def execute_render(self, node: hou.Node): """Start local render Args: node (hou.Node): RenderMan node - network (str): Network type """ + if not self.setup_light_groups(node, RenderEngine.RENDERMAN, False): + return + if not self.setup_aovs(node, False): + return + + is_lop = isinstance(node, hou.LopNode) + # Create directories render_paths = self.get_output_paths(node) for path in render_paths: self.__create_directory(path) # Execute rendering - if network == "lop": - node.node("rop_usdrender").parm("execute").pressButton() + if is_lop: + node.node("render").parm("execute").pressButton() else: node.node("denoise" if node.evalParm("denoise") else "render").parm( "execute" @@ -116,10 +146,156 @@ def copy_to_clipboard(self, node: hou.Node, network: str = None): "Currently copying to clipboard is only supported on Windows." ) + def setup_light_groups( + self, + node: hou.Node, + render_engine: RenderEngine = RenderEngine.RENDERMAN, + show_notification: bool = True, + ) -> bool: + """This function clears all automated LPE tags from lights, + then it sets their tags according to user input, + after which it will set the proper render variables.""" + is_lop = isinstance(node, hou.LopNode) + + self.app.logger.debug("Setting up light groups") + + lpe_tags = [ + LPEControl( + RenderEngine.KARMA, + lop_control="xn__karmalightlpetag_control_4fbf", + lop_light_group="xn__karmalightlpetag_31af", + sop_light_group="vm_lpetag", + ), + LPEControl( + RenderEngine.RENDERMAN, + lop_control="xn__inputsrilightlightGroup_control_krbcf", + lop_light_group="xn__inputsrilightlightGroup_jebcf", + sop_light_group="lightGroup", + ), + ] + + lpe_tag = next( + lpe_tag for lpe_tag in lpe_tags if lpe_tag.renderer == render_engine + ) + + # First we clear all our LPE tags, so we can add them again later + stage = hou.node("/stage") + + all_nodes = stage.allSubChildren() + + for light_node in all_nodes: + if light_node.type().name().startswith("light"): + lpe_parm = light_node.parm(lpe_tag.get_light_group(is_lop)) + if lpe_parm: + expressions_to_keep = "" + for expression in lpe_parm.eval().split(): + # We only remove our own LPE tags so the custom ones remain. + if not expression.startswith("LG_"): + expressions_to_keep += expression + + lpe_parm.set(expressions_to_keep) + + # Now we add our LPE tags to the lights + light_group_count = node.parm("light_groups_select").eval() + light_groups_info = {} + + for i in range(1, light_group_count + 1): + # Collecting light group information from the node + light_group_name_parm = f"light_group_name_{i}" + selected_light_lops_parm = f"select_light_ops_{i}" + + light_group_name = node.parm(light_group_name_parm).eval() + selected_light_lops = node.parm(selected_light_lops_parm).eval() + + light_groups_info[light_group_name] = selected_light_lops.split() + + lights_list = [] + for light_group in light_groups_info: + if not re.match(r"^[A-Za-z0-9_]+$", light_group): + hou.ui.displayMessage( + f"Error: Invalid light group name: '{light_group}'. You can only use letters, numbers and " + f"underscores.", + severity=hou.severityType.Error, + ) + return False + + # Using the collected information to set LPE tags + for light in light_groups_info[light_group]: + try: + if light not in lights_list: + lights_list.append(light) + light_node = hou.node(light) + + if is_lop: + lpe_control_parm = light_node.parm(lpe_tag.get_control()) + lpe_control_parm.set("set") + lpe_control_parm.pressButton() + + lpe_param = light_node.parm(lpe_tag.get_light_group(is_lop)) + lpe_param.set(f"LG_{light_group}") + lpe_param.pressButton() + + else: + hou.ui.displayMessage( + f"Error: Node {light} is in several light groups. A light can only be in one group.", + severity=hou.severityType.Error, + ) + return False + except AttributeError as e: + hou.ui.displayMessage( + f"Error: Can't set LPE tags for node {light} in light group list {light_group}. \n{e}", + severity=hou.severityType.Error, + ) + return False + + if render_engine == RenderEngine.KARMA: + # Now we add the render vars to the Karma render settings node + karma_render_settings = node.node("karmarendersettings") + extra_render_variables = karma_render_settings.parm("extrarendervars") + + indices_to_remove = [] + # Collect our automated render variables, so we can remove only those + for i in range(1, extra_render_variables.eval() + 1): + if karma_render_settings.parm( + f"name{i}" + ) and karma_render_settings.parm(f"name{i}").eval().startswith("LG_"): + indices_to_remove.append(i) + + # Remove instances from the last to the first to avoid re-indexing issues + for i in reversed(indices_to_remove): + # Instance indices are 1-based, but removal is 0-based + karma_render_settings.parm("extrarendervars").removeMultiParmInstance( + i - 1 + ) + + # Add our automated light groups back in + for light_group in light_groups_info: + render_variable_index = extra_render_variables.eval() + 1 + extra_render_variables.set(render_variable_index) + karma_render_settings.parm(f"name{render_variable_index}").set( + f"LG_{light_group}" + ) + karma_render_settings.parm(f"format{render_variable_index}").set( + "color3f" + ) + karma_render_settings.parm(f"sourceName{render_variable_index}").set( + f"C.*" + ) + karma_render_settings.parm(f"sourceType{render_variable_index}").set( + "lpe" + ) + + if show_notification: + hou.ui.displayMessage( + f"Finished light group setup for {light_group_count} groups", + ) + + return True + @staticmethod def validate_node(node: hou.Node, network: str) -> bool: - """This function will make sure all the parameters - are filled in and setup correctly. + """ + This function will make sure all the parameters are filled in and setup correctly. Args: node (hou.Node): RenderMan node @@ -133,390 +309,418 @@ def validate_node(node: hou.Node, network: str) -> bool: severity=hou.severityType.Error, ) return False - elif not render_name.isalnum(): + if not render_name.isalnum(): hou.ui.displayMessage( "Name is not alphanumeric, please only use alphabet letters (a-z) and numbers (0-9).", severity=hou.severityType.Error, ) return False - # Check if camera exists - elif not hou.node(node.evalParm("camera")): - hou.ui.displayMessage( - "Invalid camera path.", severity=hou.severityType.Error - ) - return False + # Make sure the node has an input to render + if network == "lop": + inputs = node.inputs() + if len(inputs) <= 0: + hou.ui.displayMessage( + "Node doesn't have input, please connect this " + "ShotGrid RenderMan render node to " + "the stage to render.", + severity=hou.severityType.Error, + ) + return False + # Check if camera exists + if network == "lop": + stage = node.inputs()[0].stage() + if not stage.GetPrimAtPath(node.evalParm("camera")): + hou.ui.displayMessage( + "Invalid camera path.", severity=hou.severityType.Error + ) + return False else: - # Make sure the node has an input to render - if network == "lop": - inputs = node.inputs() - if len(inputs) <= 0: - hou.ui.displayMessage( - "Node doesn't have input, please connect this " - "ShotGrid RenderMan render node to " - "the stage to render.", - severity=hou.severityType.Error, - ) - return False - else: - return True - else: - return True - - def setup_aovs(self, node: hou.Node, show_notif: bool = True) -> bool: - rman = node.node("render") + if not hou.node(node.evalParm("camera")): + hou.ui.displayMessage( + "Invalid camera path.", severity=hou.severityType.Error + ) + return False - if not self.validate_node(node, "rop"): - return False + return True - denoise = node.node("denoise") - use_denoise = node.evalParm("denoise") + @staticmethod + def _lop_setup_custom_aovs(node: hou.Node, custom_aovs: list[aov_file.CustomAOV]): + for i, aov in enumerate(custom_aovs): + aov: aov_file.CustomAOV + node.parm(f"name{i + 1}").set(aov.name) + node.parm(f"format{i + 1}").set(aov.get_format()) + node.parm(f"dataType{i + 1}").set("") + node.parm(f"sourceName{i + 1}").set(aov.lpe) + node.parm(f"sourceType{i + 1}").set("lpe") - beauty = node.evalParm("aovBeauty") - deep = node.evalParm("aovDeep") + @staticmethod + def get_active_files(node: hou.Node): + output_files = 0 + active_files = [] + + for file in aov_file.output_files: + if file.has_active_aovs(node) or file.has_active_custom_aovs(node): + active_files.append(file) + if file.identifier != aov_file.OutputIdentifier.CRYPTOMATTE: + output_files += 1 + continue - aovs = node.parmsInFolder(("AOVs",)) + # If file is Lighting and there are light groups + if ( + file.identifier == aov_file.OutputIdentifier.LIGHTING + and node.parm("light_groups_select").eval() > 0 + ): + active_files.append(file) + output_files += 1 + continue - crypto = list( - filter(lambda parm: "Crypto" in parm.name() and parm.eval() == 1, aovs) - ) + # If file is Utility and there are tees + if ( + file.identifier == aov_file.OutputIdentifier.UTILITY + and node.parm("tees").eval() > 0 + ): + active_files.append(file) + output_files += 1 + continue + return [output_files, active_files] - def make_lightgroups(use_node): - light_group = node.parm(use_node.name().replace("Use", "")) - return light_group.parmTemplate().label(), light_group.eval() + def setup_aovs(self, node: hou.Node, show_notification: bool = True) -> bool: + is_lop = isinstance(node, hou.LopNode) - lightgroups = list( - map( - make_lightgroups, - list( - filter( - lambda parm: "LGUse" in parm.name() and parm.eval() == 1, aovs - ) - ), - ) - ) + # Validate node + if not self.validate_node(node, "lop" if is_lop else "driver"): + return False - tee_count = node.evalParm("tees") + use_denoise = node.parm("denoise").eval() + use_autocrop = node.parm("autocrop").eval() - shading = node.parmsInFolder(("AOVs", "Shading")) - shading = list( - filter(lambda parm: "aov" in parm.name() and parm.eval() == 1, shading) - ) + # Get active files + output_files, active_files = self.get_active_files(node) - lighting = node.parmsInFolder(("AOVs", "Lighting")) - lighting = list( - filter(lambda parm: "aov" in parm.name() and parm.eval() == 1, lighting) - ) + # Metadata + md_config = self.app.get_setting("render_metadata") - utility = node.parmsInFolder(("AOVs", "Utility")) - utility = list( - filter(lambda parm: "aov" in parm.name() and parm.eval() == 1, utility) + md_items = [ + MetaData("colorspace", "string", "ACES - ACEScg"), + ] + md_config_groups = {} + for md in md_config: + key = f'rmd_{md.get("key")}' + md_items.append( + MetaData( + key, + md.get("type"), + f'`{md.get("expression")}`' + if md.get("expression") + else md.get("value"), + ) + ) + group = md.get("group") + # TODO should use prefixed version in group mapping? + if md_config_groups.get(group): + md_config_groups.get(group).append(key) + else: + md_config_groups[group] = [key] + md_items.append( + MetaData("rmd_PostRenderGroups", "string", json.dumps(md_config_groups)) ) - # SET FILE COUNT - file_count = beauty + deep - if len(shading): - file_count += 1 - if len(lighting) or len(lightgroups): - file_count += 1 - if len(utility) or tee_count: - file_count += 1 - - rman.parm("ri_displays").set(0) - rman.parm("ri_displays").set(file_count) - - # SETUP FILES + md_artist = str(self.app.context.user["id"]) - # Autocrop - autocrop = node.evalParm("autocrop") - if autocrop: - for i in range(file_count): - rman.parm("ri_autocrop_" + str(i)).set("true") - - # Denoise - denoise.parm("output").set( - os.path.dirname(self.get_output_path(node, "denoise")) + self.app.logger.debug( + f"Setting up aovs for files: {', '.join([file.identifier.value for file in active_files])}" ) - # Statistics - rman.parm("ri_statistics_xmlfilename").set( - self.get_output_path(node, "stats")[:-3] + "xml" - ) - - # TODO add custom aovs - # 0: Beauty 16 bit DWAa - # 1: Shading 16 bit DWAa - # 2: Lighting 16 bit DWAa - # 3: Utility 32 bit ZIP - # 4: Deep - i = 0 - if beauty: - rman.parm("ri_display_" + str(i)).set(self.get_output_path(node, "beauty")) - - rman.parm("ri_asrgba_" + str(i)).set(not use_denoise) - rman.parm("ri_exrcompression_" + str(i)).set("dwaa") - rman.parm("ri_denoiseon_" + str(i)).set(use_denoise) - - i += 1 - if len(shading): - shading = list(map(lambda p: p.name().replace("aov", ""), shading)) - - rman.parm("ri_display_" + str(i)).set(self.get_output_path(node, "beauty")) - rman.parm("ri_asrgba_" + str(i)).set(0) - rman.parm("ri_exrcompression_" + str(i)).set("dwaa") - rman.parm("ri_denoiseon_" + str(i)).set(use_denoise) - - rman.parm("ri_quickaov_Ci_" + str(i)).set(0) - rman.parm("ri_quickaov_a_" + str(i)).set(0) - - rman.parm("ri_quickaov_albedo_" + str(i)).set("Albedo" in shading) - rman.parm("ri_quickaov_emissive_" + str(i)).set("Emissive" in shading) - rman.parm("ri_quickaov_directDiffuse_" + str(i)).set("Diffuse" in shading) - rman.parm("ri_quickaov_indirectDiffuse_" + str(i)).set("Diffuse" in shading) - rman.parm("ri_quickaov_directDiffuseUnoccluded_" + str(i)).set( - "DiffuseU" in shading - ) - rman.parm("ri_quickaov_indirectDiffuseUnoccluded_" + str(i)).set( - "DiffuseU" in shading - ) - rman.parm("ri_quickaov_directSpecular_" + str(i)).set("Specular" in shading) - rman.parm("ri_quickaov_indirectSpecular_" + str(i)).set( - "Specular" in shading - ) - rman.parm("ri_quickaov_directSpecularUnoccluded_" + str(i)).set( - "SpecularU" in shading - ) - rman.parm("ri_quickaov_indirectSpecularUnoccluded_" + str(i)).set( - "SpecularU" in shading - ) - rman.parm("ri_quickaov_subsurface_" + str(i)).set("Subsurface" in shading) + if is_lop: + node_rman = node.node("render_settings") + node_aovs = node.node("aovs") + node_products = node.node("output_files") + + if output_files > 1: + node_products.parm("products").set(output_files - 1) + + # Disable all + for group in node_aovs.parmTemplateGroup().parmTemplates(): + parms: list[hou.ParmTemplate, ...] = [ + parm + for parm in group.parmTemplates() + if "precision" not in parm.name() + ] + for parm in parms: + node_rman.parm(parm.name()).set(False) + node_aovs.parm(parm.name()).set(False) + + custom_aovs: list[aov_file.CustomAOV] = [] + + # Enable active AOVs + for i, file in enumerate(active_files): + file: aov_file.OutputFile + + # Crypto + if file.identifier == aov_file.OutputIdentifier.CRYPTOMATTE: + cryptomattes = [ + crypto + for crypto in file.options + if node.parm(crypto.key).eval() + ] + for j in range(0, len(file.options)): + if j < len(cryptomattes): + crypto = cryptomattes[j] + node_rman.parm(f"xn__risamplefilter{j}name_w6an").set( + "PxrCryptomatte" + ) + node_rman.parm( + f"xn__risamplefilter{j}PxrCryptomattefilename_70bno" + ).set(self.get_output_path(node, crypto.key)) + node_rman.parm( + f"xn__risamplefilter{j}PxrCryptomattelayer_cwbno" + ).set(crypto.aovs[0]) + else: + node_rman.parm(f"xn__risamplefilter{j}name_w6an").set( + "None" + ) + continue + + # Add custom AOVs + try: + local_custom_aovs = file.get_active_custom_aovs(node) + except Exception as e: + self._error(f"Something is wrong with one or more of the AOVs", e) + return False - rman.parm("ri_quickaov_directDiffuseLobe_" + str(i)).set( - "LobeDiffuse" in shading - ) - rman.parm("ri_quickaov_indirectDiffuseLobe_" + str(i)).set( - "LobeDiffuse" in shading - ) - rman.parm("ri_quickaov_directSpecularPrimaryLobe_" + str(i)).set( - "LobeSpecularPrimary" in shading - ) - rman.parm("ri_quickaov_indirectSpecularPrimaryLobe_" + str(i)).set( - "LobeSpecularPrimary" in shading - ) - rman.parm("ri_quickaov_directSpecularRoughLobe_" + str(i)).set( - "LobeSpecularRough" in shading - ) - rman.parm("ri_quickaov_indirectSpecularRoughLobe_" + str(i)).set( - "LobeSpecularRough" in shading - ) - rman.parm("ri_quickaov_directSpecularClearcoatLobe_" + str(i)).set( - "LobeSpecularClearcoat" in shading - ) - rman.parm("ri_quickaov_indirectSpecularClearcoatLobe_" + str(i)).set( - "LobeSpecularClearcoat" in shading - ) - rman.parm("ri_quickaov_directSpecularIridescenceLobe_" + str(i)).set( - "LobeSpecularIridescence" in shading - ) - rman.parm("ri_quickaov_indirectSpecularIridescenceLobe_" + str(i)).set( - "LobeSpecularIridescence" in shading - ) - rman.parm("ri_quickaov_directSpecularFuzzLobe_" + str(i)).set( - "LobeSpecularFuzz" in shading - ) - rman.parm("ri_quickaov_indirectSpecularFuzzLobe_" + str(i)).set( - "LobeSpecularFuzz" in shading - ) - rman.parm("ri_quickaov_directSpecularGlassLobe_" + str(i)).set( - "LobeSpecularGlass" in shading - ) - rman.parm("ri_quickaov_indirectSpecularGlassLobe_" + str(i)).set( - "LobeSpecularGlass" in shading - ) - rman.parm("ri_quickaov_subsurfaceLobe_" + str(i)).set( - "LobeSubsurface" in shading - ) - rman.parm("ri_quickaov_transmissiveSingleScatterLobe_" + str(i)).set( - "LobeTransmissiveSingleScatter" in shading - ) - rman.parm("ri_quickaov_transmissiveGlassLobe_" + str(i)).set( - "LobeTransmissiveGlass" in shading - ) + # For first aov + if i == 0: + # Set file output path + node_rman.parm("picture").set( + self.get_output_path(node, file.identifier.lower()) + ) - i += 1 - if len(lighting) or len(lightgroups): - lighting = list(map(lambda p: p.name().replace("aov", ""), lighting)) + # Set as RGBA + node_rman.parm(f"xn__driverparametersopenexrasrgba_bobkh").set( + file.as_rgba and not (file.can_denoise and use_denoise) + ) - rman.parm("ri_display_" + str(i)).set( - self.get_output_path(node, "lighting") - ) - rman.parm("ri_asrgba_" + str(i)).set(0) - rman.parm("ri_exrcompression_" + str(i)).set("dwaa") - rman.parm("ri_denoiseon_" + str(i)).set(use_denoise) + # Set output type + if file.identifier == aov_file.OutputIdentifier.DEEP: + node_rman.parm("productType").set("deepexr") + # Set use autocrop + node_rman.parm("xn__driverparametersopenexrautocrop_krbkh").set( + "on" if use_autocrop else "off" + ) + # Set bitdepth level + node_rman.parm("xn__driverparametersopenexrexrpixeltype_2xbkh").set( + file.bitdepth + ) + # Set compression type + node_rman.parm( + "xn__driverparametersopenexrexrcompression_c1bkh" + ).set(file.compression) + + # Add custom AOVs + node_rman.parm("extrarendervars").set(0) + node_rman.parm("extrarendervars").set(len(local_custom_aovs)) + self._lop_setup_custom_aovs(node_rman, local_custom_aovs) + # And the others + else: + custom_aovs += local_custom_aovs - rman.parm("ri_quickaov_Ci_" + str(i)).set(0) - rman.parm("ri_quickaov_a_" + str(i)).set(0) + # Set file settings + node_products.parm(f"primname_{i - 1}").set( + file.identifier.value.lower() + ) + # Set file output path + node_products.parm(f"productName_{i - 1}").set( + self.get_output_path(node, file.identifier.lower()) + ) + if file.identifier == aov_file.OutputIdentifier.DEEP: + node_products.parm(f"productType_{i - 1}").set("deepexr") + node_products.parm(f"doorderedVars_{i - 1}").set(True) + node_products.parm(f"orderedVars_{i - 1}").set( + " ".join( + [ + f"/Render/Products/Vars/{aov}" + for aov in file.get_active_aovs(node) + ] + + [ + f"/Render/Products/Vars/{aov.name}" + for aov in local_custom_aovs + ] + ) + ) + node_products.parm(f"autocrop_{i - 1}").set(use_autocrop) + node_products.parm(f"openexr_bitdepth_{i - 1}").set(file.bitdepth) + node_products.parm(f"openexr_compression_{i - 1}").set( + file.compression + ) - rman.parm("ri_quickaov_occluded_" + str(i)).set( - "ShadowOccluded" in lighting - ) - rman.parm("ri_quickaov_unoccluded_" + str(i)).set( - "ShadowUnoccluded" in lighting - ) - rman.parm("ri_quickaov_shadow_" + str(i)).set("Shadow" in lighting) + # Enable active AOVs + active_node = node_rman if i == 0 else node_aovs + for active_aov in file.get_active_aovs(node): + active_node.parm(active_aov).set(True) - # Lightgroups - rman.parm("ri_numcustomaovs_" + str(i)).set(len(lightgroups)) + node_custom_aovs = node.node("custom_aovs") + node_custom_aovs.parm("rendervars").set(0) + node_custom_aovs.parm("rendervars").set(len(custom_aovs)) + self._lop_setup_custom_aovs(node_custom_aovs, custom_aovs) - for j, group in enumerate(lightgroups): - rman.parm("ri_aovvariable_" + str(i) + "_" + str(j)).set(group[0]) - rman.parm("ri_aovsource_" + str(i) + "_" + str(j)).set( - f"color lpe:C.*" - ) + # Statistics + node_rman.parm("xn__ristatisticsxmlfilename_febk").set( + self.get_output_path(node, "stats")[:-3] + "xml" + ) - i += 1 - if len(utility) or tee_count: - utility = list(map(lambda p: p.name().replace("aov", ""), utility)) + # Metadata + # Check if custom metadata has valid keys + for j in range(1, node.evalParm("metadata_entries") + 1): + md_key = node.parm(f"metadata_{j}_key").eval() + if not re.match(r"^[A-Za-z0-9_]+$", md_key): + hou.ui.displayMessage( + f'The metadata key "{md_key}" is invalid. You can only use letters, numbers, and ' + f"underscores.", + severity=hou.severityType.Error, + ) + return False - rman.parm("ri_display_" + str(i)).set(self.get_output_path(node, "utility")) - rman.parm("ri_asrgba_" + str(i)).set(0) - rman.parm("ri_exrpixeltype_" + str(i)).set("float") + node_md = node.node("sg_metadata") - rman.parm("ri_quickaov_Ci_" + str(i)).set(0) - rman.parm("ri_quickaov_a_" + str(i)).set(0) + node_md.parm("artist").set(md_artist) - rman.parm("ri_quickaov_curvature_" + str(i)).set("Pworld" in utility) - rman.parm("ri_quickaov_dPdtime_" + str(i)).set("DTime" in utility) - rman.parm("ri_quickaov_dPcameradtime_" + str(i)).set( - "CameraDTime" in utility - ) + node_md.parm("metadata_entries").set(0) + node_md.parm("metadata_entries").set(len(md_items)) - rman.parm("ri_quickaov___Pworld_" + str(i)).set("Pworld" in utility) - rman.parm("ri_quickaov___Nworld_" + str(i)).set("Nworld" in utility) - rman.parm("ri_quickaov___depth_" + str(i)).set("Depth" in utility) - rman.parm("ri_quickaov___st_" + str(i)).set("ST" in utility) - rman.parm("ri_quickaov___Pref_" + str(i)).set("Pref" in utility) - rman.parm("ri_quickaov___Nref_" + str(i)).set("Nref" in utility) - rman.parm("ri_quickaov___WPref_" + str(i)).set("WPref" in utility) - rman.parm("ri_quickaov___WNref_" + str(i)).set("WNref" in utility) - - # Tees - rman.parm("ri_numcustomaovs_" + str(i)).set(tee_count) - - for j in range(tee_count): - rman.parm("ri_aovtype_" + str(i) + "_" + str(j)).set( - node.parm("teeType_" + str(j + 1)).evalAsString() - ) - rman.parm("ri_aovvariable_" + str(i) + "_" + str(j)).set( - node.evalParm("teeName_" + str(j + 1)) - ) + for i, item in enumerate(md_items): + item: MetaData - i += 1 - if deep: - rman.parm("ri_display_" + str(i)).set(self.get_output_path(node, "deep")) - rman.parm("ri_device_" + str(i)).set("deepexr") - - # CRYPTOMATTE - rman.parm("ri_samplefilters").set(len(crypto)) - for i, c in enumerate(crypto): - name = c.name()[3:] - cPath = "../aovs/" + name - rman.parm("ri_samplefilter" + str(i)).set(cPath) - node.parm("./aovs/" + name + "/filename").set( - self.get_output_path(node, name) - ) + node_md.parm(f"metadata_{i + 1}_key").set(item.key) + node_md.parm(f"metadata_{i + 1}_type").set(item.type) + if "`" in item.value: + expression = item.value[1:-1] + expression = re.sub( + r"(ch[a-z]*)(\()([\"'])", r"\1(\3../", expression + ) - # METADATA - md_config = self.app.get_setting("render_metadata") + node_md.parm(f"metadata_{i + 1}_{item.type}").setExpression( + expression + ) + else: + node_md.parm(f"metadata_{i + 1}_{item.type}").set(item.value) + else: + rman = node.node("render") + rman.parm("ri_displays").set(0) + rman.parm("ri_displays").set(output_files) - md_config_groups = {} - for md in md_config: - group = md.get("group") - if md_config_groups.get(group): - md_config_groups.get(group).append(md.get("key")) - else: - md_config_groups[group] = [md.get("key")] - md_config_groups = json.dumps(md_config_groups) - - md_count_node = node.evalParm("ri_exr_metadata") + (len(lightgroups) > 0) - md_count_external = len(md_config) - md_lg = {} - md_lg.update(lg for lg in lightgroups) - md_lg = json.dumps(md_lg) - md_parms = list( - filter( - lambda parm: "exr_metadata" in parm.name() - and parm.name() != "ri_exr_metadata", - node.parms(), + # Denoise + node.node("denoise").parm("output").set( + os.path.dirname(self.get_output_path(node, "denoise")) ) - ) - for f in range(file_count): - rman.parm("ri_exr_metadata_{}".format(f)).set( - md_count_node + md_count_external + (len(md_config) > 0) + # Statistics + rman.parm("ri_statistics_xmlfilename").set( + self.get_output_path(node, "stats")[:-3] + "xml" ) - rman.parm("ri_image_Artist_{}".format(f)).set( - str(self.app.context.user["id"]) - ) - for parm in md_parms: - name = parm.name().split("_") - index = -1 - if name[-1] == "": - index = -2 - name.insert(index, str(f)) - name = "_".join(name) - self.__set_expression(rman, parm.name(), name) - - if len(lightgroups): - rman.parm("ri_exr_metadata_key_{}_{}".format(f, md_count_node - 1)).set( - "rmd_RenderLightGroups" - ) - rman.parm( - "ri_exr_metadata_type_{}_{}".format(f, md_count_node - 1) - ).set("string") - rman.parm( - "ri_exr_metadata_string_{}_{}_".format(f, md_count_node - 1) - ).set(md_lg) - - for i in range(md_count_external): - item = md_config[i] - rman.parm("ri_exr_metadata_key_{}_{}".format(f, md_count_node + i)).set( - "rmd_{}".format(item.get("key")) - ) - rman.parm( - "ri_exr_metadata_type_{}_{}".format(f, md_count_node + i) - ).set(item.get("type")) - rman.parm( - "ri_exr_metadata_{}_{}_{}_".format( - item.get("type"), f, md_count_node + i - ) - ).setExpression(item.get("expression")) - - rman.parm( - "ri_exr_metadata_key_{}_{}".format(f, md_count_node + md_count_external) - ).set("rmd_PostRenderGroups") - rman.parm( - "ri_exr_metadata_type_{}_{}".format( - f, md_count_node + md_count_external - ) - ).set("string") - rman.parm( - "ri_exr_metadata_string_{}_{}_".format( - f, md_count_node + md_count_external + for i, file in enumerate(active_files): + file: aov_file.OutputFile + + # Crypto + if file.identifier == aov_file.OutputIdentifier.CRYPTOMATTE: + cryptomattes = [ + crypto + for crypto in file.options + if node.parm(crypto.key).eval() + ] + + rman.parm("ri_samplefilters").set(0) + rman.parm("ri_samplefilters").set(len(cryptomattes)) + for j, c in enumerate(cryptomattes): + name = f"Crypto{c.name}" + rman.parm(f"ri_samplefilter{j}").set(f"../aovs/{name}") + node.parm("./aovs/" + name + "/filename").set( + self.get_output_path(node, name) + ) + continue + + rman.parm(f"ri_display_{i}").set( + self.get_output_path(node, file.identifier.lower()) ) - ).set(md_config_groups) - msg = "Setup AOVs complete with " + str(file_count + len(crypto)) + " files." - if show_notif: + if file.identifier == aov_file.OutputIdentifier.DEEP: + rman.parm(f"ri_device_{i}").set("deepexr") + + rman.parm(f"ri_autocrop_{i}").set("on" if use_autocrop else "off") + rman.parm(f"ri_exrpixeltype_{i}").set(file.bitdepth) + rman.parm(f"ri_exrcompression_{i}").set(file.compression) + + denoise_on = file.can_denoise and use_denoise + rman.parm(f"ri_denoiseon_{i}").set(denoise_on) + + rman.parm(f"ri_asrgba_{i}").set(file.as_rgba and not denoise_on) + + # Disable defaults + rman.parm(f"ri_quickaov_Ci_{i}").set(False) + rman.parm(f"ri_quickaov_a_{i}").set(False) + + # Enable active AOVs + for aov in file.get_active_aovs(node): + rman.parm(f"ri_quickaov_{aov}_{i}").set(True) + + # Add custom AOVs + custom_aovs = file.get_active_custom_aovs(node) + + rman.parm(f"ri_numcustomaovs_{i}").set(0) + rman.parm(f"ri_numcustomaovs_{i}").set(len(custom_aovs)) + for j, aov in enumerate(custom_aovs): + aov: aov_file.CustomAOV + rman.parm(f"ri_aovvariable_{i}_{j}").set(aov.name) + rman.parm(f"ri_aovtype_{i}_{j}").set(aov.type) + rman.parm(f"ri_aovsource_{i}_{j}").set(aov.lpe) + + node_md = node.node("render") + for j in range(1, node.evalParm("metadata_entries") + 1): + md_key = node.parm(f"metadata_{j}_key").eval() + md_type = node.parm(f"metadata_{j}_type").evalAsString() + md_value_parm = node.parm(f"metadata_{j}_{md_type}") + try: + md_value = f"`{md_value_parm.expression()}`" + except: + md_value = md_value_parm.rawValue() + + md_items.append(MetaData(md_key, md_type, md_value)) + + node_md.parm(f"ri_exr_metadata_{i}").set(0) + node_md.parm(f"ri_exr_metadata_{i}").set(len(md_items)) + + node_md.parm(f"ri_image_Artist_{i}").set(md_artist) + + for j, item in enumerate(md_items): + item: MetaData + + node_md.parm(f"ri_exr_metadata_key_{i}_{j}").set(item.key) + node_md.parm(f"ri_exr_metadata_type_{i}_{j}").set(item.type) + if "`" in item.value: + expression = item.value[1:-1] + expression = re.sub( + r"(ch[a-z]*)(\()([\"'])", r"\1(\3../", expression + ) + + node_md.parm( + f"ri_exr_metadata_{item.type}_{i}_{j}_" + ).setExpression(expression) + else: + node_md.parm(f"ri_exr_metadata_{item.type}_{i}_{j}_").set( + item.value + ) + + msg = f"Setup AOVs complete with {len(active_files)} files." + if show_notification: hou.ui.displayMessage(msg) - print("[RenderMan Renderer] " + msg) + self.app.logger.debug(msg) return True @staticmethod - def __set_expression(node: hou.Node, source_parm: str, dist_parm: str): + def _set_expression(node: hou.Node, source_parm: str, dist_parm: str): org_parm = node.parm(dist_parm).parmTemplate() if not org_parm: print("parm not found: ", dist_parm) @@ -530,14 +734,15 @@ def __set_expression(node: hou.Node, source_parm: str, dist_parm: str): '{}("{}{}")'.format(parm_type, "../", source_parm) ) - def get_output_path(self, node: hou.Node, aov_name: str, network="rop") -> str: + def get_output_path(self, node: hou.Node, aov_name: str) -> str: """Calculate render path for an aov Args: node (hou.Node): RenderMan node aov_name (str): AOV name - network (str): Network type """ + is_lop = isinstance(node, hou.LopNode) + aov_name = aov_name[0].lower() + aov_name[1:] current_filepath = hou.hipFile.path() @@ -555,7 +760,7 @@ def get_output_path(self, node: hou.Node, aov_name: str, network="rop") -> str: # Because RenderMan in the rop network uses different # parameter names, we need to change some bits - if network == "rop": + if not is_lop: camera = node.parm("camera").eval() evaluate_parm = False @@ -582,7 +787,6 @@ def get_output_path(self, node: hou.Node, aov_name: str, network="rop") -> str: if evaluate_parm is True: fields["width"] = node.parm(resolution_x_field).eval() fields["height"] = node.parm(resolution_y_field).eval() - else: fields["width"] = resolution_x fields["height"] = resolution_y @@ -592,46 +796,25 @@ def get_output_path(self, node: hou.Node, aov_name: str, network="rop") -> str: def get_output_paths(self, node: hou.Node) -> list[str]: paths = [] - aovs = node.parmsInFolder(("AOVs",)) - - crypto = list( - filter(lambda parm: "Crypto" in parm.name() and parm.eval() == 1, aovs) - ) - - lightgroups = list( - filter(lambda parm: "LGUse" in parm.name() and parm.eval() == 1, aovs) - ) - - shading = node.parmsInFolder(("AOVs", "Shading")) - shading = list( - filter(lambda parm: "aov" in parm.name() and parm.eval() == 1, shading) - ) - - lighting = node.parmsInFolder(("AOVs", "Lighting")) - lighting = list( - filter(lambda parm: "aov" in parm.name() and parm.eval() == 1, lighting) - ) - - utility = node.parmsInFolder(("AOVs", "Utility")) - utility = list( - filter(lambda parm: "aov" in parm.name() and parm.eval() == 1, utility) - ) - - if node.evalParm("aovBeauty"): - paths.append(self.get_output_path(node, "beauty")) - if len(shading): - paths.append(self.get_output_path(node, "shading")) - if len(lighting) or len(lightgroups): - paths.append(self.get_output_path(node, "lighting")) - if len(utility) or node.evalParm("tees"): - paths.append(self.get_output_path(node, "utility")) - if node.evalParm("aovDeep"): - paths.append(self.get_output_path(node, "deep")) - - # Cryptomatte - for i, c in enumerate(crypto): - name = c.name()[3:] - paths.append(self.get_output_path(node, name)) + print(node.path()) + + try: + output_files, active_files = self.get_active_files(node) + for file in active_files: + file: aov_file.OutputFile + if file.identifier == aov_file.OutputIdentifier.CRYPTOMATTE: + for crypto in file.options: + print(crypto.key) + if node.parm(crypto.key).eval(): + paths.append(self.get_output_path(node, crypto.key)) + else: + paths.append(self.get_output_path(node, file.identifier.lower())) + except Exception as e: + self._error( + f'Something is wrong with one or more of the AOVs on node "{node.name()}"', + e, + ) + return [] # Denoise if node.evalParm("denoise"): diff --git a/python/tk_houdini_renderman/post_task_script.py b/python/tk_houdini_renderman/post_task_script.py index 8fa3866..2a09e1d 100644 --- a/python/tk_houdini_renderman/post_task_script.py +++ b/python/tk_houdini_renderman/post_task_script.py @@ -23,11 +23,11 @@ def __main__(*args): end_frame = task.GetEndFrame() for frame_num in range(start_frame, end_frame + 1): - output_filename = output_filename.replace("%04d", f"{frame_num:04}") + filename = output_filename.replace("%04d", f"{frame_num:04}") from_path = os.path.join( - output_directory, output_filename.replace("_denoise_", "_beauty_") + output_directory, filename.replace("_denoise_", "_beauty_") ) - to_path = os.path.join(output_directory, output_filename) + to_path = os.path.join(output_directory, filename) if os.path.exists(from_path): if not os.path.exists(to_path):