From e4365b746b40d311c59b3305c39adc61dc1a0425 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 28 Jan 2022 18:18:06 +0100 Subject: [PATCH 01/66] Draft for allowing Arnold ROP render publishing to Deadline from Houdini --- .../plugins/create/create_arnold_rop.py | 50 ++++++ .../plugins/publish/collect_arnold_rop.py | 142 ++++++++++++++++++ .../plugins/publish/collect_instances.py | 2 +- .../plugins/publish/increment_current_file.py | 2 +- .../houdini/plugins/publish/save_scene.py | 3 +- .../publish/submit_houdini_render_deadline.py | 24 +-- 6 files changed, 208 insertions(+), 15 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/create/create_arnold_rop.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py new file mode 100644 index 0000000000..e0fad2a1cc --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -0,0 +1,50 @@ +from avalon import houdini + + +class CreateArnoldRop(houdini.Creator): + """Arnold ROP""" + + label = "Arnold ROP" + family = "arnold_rop" + icon = "magic" + defaults = ["master"] + + def __init__(self, *args, **kwargs): + super(CreateArnoldRop, self).__init__(*args, **kwargs) + + # Clear the family prefix from the subset + subset = self.data["subset"] + subset_no_prefix = subset[len(self.family):] + subset_no_prefix = subset_no_prefix[0].lower() + subset_no_prefix[1:] + self.data["subset"] = subset_no_prefix + + # Add chunk size attribute + self.data["chunkSize"] = 1 + + # Remove the active, we are checking the bypass flag of the nodes + self.data.pop("active", None) + + self.data.update({"node_type": "arnold"}) + + def process(self): + instance = super(CreateArnoldRop, self).process() + + basename = instance.name() + instance.setName(basename + "_ROP", unique_name=True) + + prefix = '${HIP}/render/${HIPNAME}/`chs("subset")`.$F4.exr' + parms = { + # Render frame range + "trange": 1, + + # Arnold ROP settings + "ar_picture": prefix, + "ar_exr_half_precision": 1 # half precision + } + instance.setParms(parms) + + # Lock some Avalon attributes + to_lock = ["family", "id"] + for name in to_lock: + parm = instance.parm(name) + parm.lock(True) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py new file mode 100644 index 0000000000..e5abfccca0 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -0,0 +1,142 @@ +import re +import os + +import hou +import avalon.io as io +import avalon.api as api +import pyblish.api + + +def get_top_referenced_parm(parm): + + processed = set() # disallow infinite loop + while True: + if parm.path() in processed: + raise RuntimeError("Parameter references result in cycle.") + + processed.add(parm.path()) + + ref = parm.getReferencedParm() + if ref.path() == parm.path(): + # It returns itself when it doesn't reference + # another parameter + return ref + else: + parm = ref + + +def evalParmNoFrame(node, parm, pad_character="#"): + + parameter = node.parm(parm) + assert parameter, "Parameter does not exist: %s.%s" % (node, parm) + + # If the parameter has a parameter reference, then get that + # parameter instead as otherwise `unexpandedString()` fails. + parameter = get_top_referenced_parm(parameter) + + # Substitute out the frame numbering with padded characters + try: + raw = parameter.unexpandedString() + except hou.Error as exc: + print("Failed: %s" % parameter) + raise RuntimeError(exc) + + def replace(match): + padding = 1 + n = match.group(2) + if n and int(n): + padding = int(n) + return pad_character * padding + + expression = re.sub(r"(\$F([0-9]*))", replace, raw) + + with hou.ScriptEvalContext(parameter): + return hou.expandStringAtFrame(expression, 0) + + +class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): + """Collect Arnold ROP Render Products + + Collects the instance.data["files"] for the render products. + + Provides: + instance -> files + + """ + + label = "Arnold ROP Render Products" + order = pyblish.api.CollectorOrder + 0.4 + hosts = ["houdini"] + families = ["arnold_rop"] + + def process(self, instance): + + rop = instance[0] + + # Collect chunkSize + chunk_size_parm = rop.parm("chunkSize") + if chunk_size_parm: + chunk_size = int(chunk_size_parm.eval()) + instance.data["chunkSize"] = chunk_size + self.log.debug("Chunk Size: %s" % chunk_size) + + default_prefix = evalParmNoFrame(rop, "ar_picture") + render_products = [] + + # Default beauty AOV + beauty_product = self.get_render_product_name(prefix=default_prefix, + suffix=None) + render_products.append(beauty_product) + + # TODO: Implement AOVS + # num_aovs = rop.evalParm("ar_aovs") + # for index in range(num_aovs): + # i = index + 1 + # + # # Skip disabled AOVs + # if not rop.evalParm("ar_enable_aov%s" % i): + # continue + # + # label = evalParmNoFrame(rop, "ar_aov_label%s" % i) + # if rop.evalParm("ar_aov_exr_enable_layer_name%s" % i): + # label = rop.evalParm("ar_aov_exr_layer_name%s" % i) + # + # aov_product = self.get_render_product_name(default_prefix, + # suffix=label) + # render_products.append(aov_product) + + for product in render_products: + self.log.debug("Found render product: %s" % product) + + filenames = list(render_products) + instance.data["files"] = filenames + + # For now by default do NOT try to publish the rendered output + instance.data["publishJobState"] = "Suspended" + + def get_render_product_name(self, prefix, suffix): + """Return the output filename using the AOV prefix and suffix""" + + # When AOV is explicitly defined in prefix we just swap it out + # directly with the AOV suffix to embed it. + # Note: ${AOV} seems to be evaluated in the parameter as %AOV% + has_aov_in_prefix = "%AOV%" in prefix + if has_aov_in_prefix: + # It seems that when some special separator characters are present + # before the %AOV% token that Redshift will secretly remove it if + # there is no suffix for the current product, for example: + # foo_%AOV% -> foo.exr + pattern = "%AOV%" if suffix else "[._-]?%AOV%" + product_name = re.sub(pattern, + suffix, + prefix, + flags=re.IGNORECASE) + else: + if suffix: + # Add ".{suffix}" before the extension + prefix_base, ext = os.path.splitext(prefix) + product_name = prefix_base + "." + suffix + ext + else: + product_name = prefix + + return product_name diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index 12d118f0cc..094f791a0b 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -109,6 +109,6 @@ class CollectInstances(pyblish.api.ContextPlugin): data["frameStart"] = node.evalParm("f1") data["frameEnd"] = node.evalParm("f2") - data["steps"] = node.evalParm("f3") + data["byFrameStep"] = node.evalParm("f3") return data diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 31c2954ee7..7a1ad09249 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -15,7 +15,7 @@ class IncrementCurrentFile(pyblish.api.InstancePlugin): label = "Increment current file" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["houdini"] - families = ["colorbleed.usdrender", "redshift_rop"] + families = ["colorbleed.usdrender", "redshift_rop", "arnold_rop"] targets = ["local"] def process(self, instance): diff --git a/openpype/hosts/houdini/plugins/publish/save_scene.py b/openpype/hosts/houdini/plugins/publish/save_scene.py index 1b12efa603..75ff4dcdfd 100644 --- a/openpype/hosts/houdini/plugins/publish/save_scene.py +++ b/openpype/hosts/houdini/plugins/publish/save_scene.py @@ -9,7 +9,8 @@ class SaveCurrentScene(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder - 0.49 hosts = ["houdini"] families = ["usdrender", - "redshift_rop"] + "redshift_rop", + "arnold_rop"] targets = ["local"] def process(self, instance): diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py index fa146c0d30..9613bae7fe 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -27,13 +27,12 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder hosts = ["houdini"] families = ["usdrender", - "redshift_rop"] - targets = ["local"] + "redshift_rop", + "arnold_rop"] def process(self, instance): context = instance.context - code = context.data["code"] filepath = context.data["currentFile"] filename = os.path.basename(filepath) comment = context.data.get("comment", "") @@ -42,16 +41,14 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): # Support code prefix label for batch name batch_name = filename - if code: - batch_name = "{0} - {1}".format(code, batch_name) # Output driver to render driver = instance[0] # StartFrame to EndFrame by byFrameStep frames = "{start}-{end}x{step}".format( - start=int(instance.data["startFrame"]), - end=int(instance.data["endFrame"]), + start=int(instance.data["frameStart"]), + end=int(instance.data["frameEnd"]), step=int(instance.data["byFrameStep"]), ) @@ -71,7 +68,7 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): "UserName": deadline_user, "Plugin": "Houdini", - "Pool": "houdini_redshift", # todo: remove hardcoded pool + #"Pool": "houdini", # todo(roy): Add pool options "Frames": frames, "ChunkSize": instance.data.get("chunkSize", 10), @@ -136,9 +133,12 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): def submit(self, instance, payload): - AVALON_DEADLINE = api.Session.get("AVALON_DEADLINE", - "http://localhost:8082") - assert AVALON_DEADLINE, "Requires AVALON_DEADLINE" + # get default deadline webservice url from deadline module + deadline_url = instance.context.data.get("defaultDeadline") + # if custom one is set in instance, use that + if instance.data.get("deadlineUrl"): + deadline_url = instance.data.get("deadlineUrl") + assert deadline_url, "Requires Deadline Webservice URL" plugin = payload["JobInfo"]["Plugin"] self.log.info("Using Render Plugin : {}".format(plugin)) @@ -147,7 +147,7 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(AVALON_DEADLINE) + url = "{}/api/jobs".format(deadline_url) response = requests.post(url, json=payload) if not response.ok: raise Exception(response.text) From 553518064efea3af8241a46164600d45b77cb3a7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 28 Jan 2022 18:28:14 +0100 Subject: [PATCH 02/66] Shush the hound --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 4 +--- .../plugins/publish/submit_houdini_render_deadline.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index e5abfccca0..93849502be 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -2,14 +2,12 @@ import re import os import hou -import avalon.io as io -import avalon.api as api import pyblish.api def get_top_referenced_parm(parm): - processed = set() # disallow infinite loop + processed = set() # disallow infinite loop while True: if parm.path() in processed: raise RuntimeError("Parameter references result in cycle.") diff --git a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 9613bae7fe..996c5a5d46 100644 --- a/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/default_modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -68,7 +68,7 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): "UserName": deadline_user, "Plugin": "Houdini", - #"Pool": "houdini", # todo(roy): Add pool options + # "Pool": "houdini", # todo(roy): Add pool options "Frames": frames, "ChunkSize": instance.data.get("chunkSize", 10), From 976c708d59ecb860c7b2eccd270f99cb6e0fe1ae Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 Aug 2022 12:11:09 +0200 Subject: [PATCH 03/66] Refactor to new houdini Creator --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index e0fad2a1cc..f9b5f0c53c 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -1,7 +1,7 @@ -from avalon import houdini +from openpype.hosts.houdini.api import plugin -class CreateArnoldRop(houdini.Creator): +class CreateArnoldRop(plugin.Creator): """Arnold ROP""" label = "Arnold ROP" @@ -26,8 +26,7 @@ class CreateArnoldRop(houdini.Creator): self.data.update({"node_type": "arnold"}) - def process(self): - instance = super(CreateArnoldRop, self).process() + def _process(self, instance): basename = instance.name() instance.setName(basename + "_ROP", unique_name=True) From 8bedec713d8d6e81bd6df43122ff6f8a780666e9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 Aug 2022 12:11:30 +0200 Subject: [PATCH 04/66] Enable AOVs + fix collected files for publishing --- .../plugins/publish/collect_arnold_rop.py | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 93849502be..dfb8f0d5ee 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -86,22 +86,28 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): suffix=None) render_products.append(beauty_product) - # TODO: Implement AOVS - # num_aovs = rop.evalParm("ar_aovs") - # for index in range(num_aovs): - # i = index + 1 - # - # # Skip disabled AOVs - # if not rop.evalParm("ar_enable_aov%s" % i): - # continue - # - # label = evalParmNoFrame(rop, "ar_aov_label%s" % i) - # if rop.evalParm("ar_aov_exr_enable_layer_name%s" % i): - # label = rop.evalParm("ar_aov_exr_layer_name%s" % i) - # - # aov_product = self.get_render_product_name(default_prefix, - # suffix=label) - # render_products.append(aov_product) + files_by_aov = { + "": self.generate_expected_files(instance, beauty_product) + } + + num_aovs = rop.evalParm("ar_aovs") + for index in range(num_aovs): + i = index + 1 + + # Skip disabled AOVs + if not rop.evalParm("ar_enable_aov%s" % i): + continue + + if rop.evalParm("ar_aov_exr_enable_layer_name%s" % i): + label = rop.evalParm("ar_aov_exr_layer_name%s" % i) + else: + label = evalParmNoFrame(rop, "ar_aov_label%s" % i) + + aov_product = self.get_render_product_name(default_prefix, + suffix=label) + render_products.append(aov_product) + files_by_aov[label] = self.generate_expected_files(instance, + aov_product) for product in render_products: self.log.debug("Found render product: %s" % product) @@ -111,6 +117,11 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" + instance.data["attachTo"] = [] # stub required data + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["expectedFiles"].append(files_by_aov) def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" @@ -138,3 +149,26 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): product_name = prefix return product_name + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files From 3f59f39847158975fed9467275729ba516507ce6 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 Aug 2022 12:16:23 +0200 Subject: [PATCH 05/66] Refactor submit deadline to use `AbstractSubmitDeadline` class --- .../publish/submit_houdini_render_deadline.py | 175 +++++++----------- 1 file changed, 71 insertions(+), 104 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 3cacc8dd16..8aabec6e30 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -1,16 +1,24 @@ -import os -import json -import getpass - -import requests -import pyblish.api - import hou +import os +import attr +import getpass +import pyblish.api + from openpype.pipeline import legacy_io +from openpype_modules.deadline import abstract_submit_deadline +from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo -class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): +@attr.s +class DeadlinePluginInfo(): + SceneFile = attr.ib(default=None) + OutputDriver = attr.ib(default=None) + Version = attr.ib(default=None) + IgnoreInputs = attr.ib(default=True) + + +class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): """Submit Solaris USD Render ROPs to Deadline. Renders are submitted to a Deadline Web Service as @@ -30,136 +38,95 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): "redshift_rop", "arnold_rop"] targets = ["local"] + use_published = True - def process(self, instance): + def get_job_info(self): + job_info = DeadlineJobInfo(Plugin="Houdini") + instance = self._instance context = instance.context + filepath = context.data["currentFile"] filename = os.path.basename(filepath) - comment = context.data.get("comment", "") - deadline_user = context.data.get("deadlineUser", getpass.getuser()) - jobname = "%s - %s" % (filename, instance.name) - # Support code prefix label for batch name - batch_name = filename + job_info.Name = "%s - %s" % (filename, instance.name) + job_info.BatchName = filename + job_info.Plugin = "Houdini" + job_info.UserName = context.data.get( + "deadlineUser", getpass.getuser()) - # Output driver to render - driver = instance[0] - - # StartFrame to EndFrame by byFrameStep + # Deadline requires integers in frame range frames = "{start}-{end}x{step}".format( start=int(instance.data["frameStart"]), end=int(instance.data["frameEnd"]), step=int(instance.data["byFrameStep"]), ) + job_info.Frames = frames - # Documentation for keys available at: - # https://docs.thinkboxsoftware.com - # /products/deadline/8.0/1_User%20Manual/manual - # /manual-submission.html#job-info-file-options - payload = { - "JobInfo": { - # Top-level group name - "BatchName": batch_name, + job_info.Pool = instance.data.get("primaryPool") + job_info.SecondaryPool = instance.data.get("secondaryPool") + job_info.ChunkSize = instance.data.get("chunkSize", 10) + job_info.Comment = context.data.get("comment") - # Job name, as seen in Monitor - "Name": jobname, - - # Arbitrary username, for visualisation in Monitor - "UserName": deadline_user, - - "Plugin": "Houdini", - "Pool": instance.data.get("primaryPool"), - "secondaryPool": instance.data.get("secondaryPool"), - "Frames": frames, - - "ChunkSize": instance.data.get("chunkSize", 10), - - "Comment": comment - }, - "PluginInfo": { - # Input - "SceneFile": filepath, - "OutputDriver": driver.path(), - - # Mandatory for Deadline - # Houdini version without patch number - "Version": hou.applicationVersionString().rsplit(".", 1)[0], - - "IgnoreInputs": True - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Include critical environment variables with submission + api.Session keys = [ - # Submit along the current Avalon tool setup that we launched - # this application with so the Render Slave can build its own - # similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9" - "AVALON_TOOLS", + "FTRACK_API_KEY", + "FTRACK_API_USER", + "FTRACK_SERVER", + "OPENPYPE_SG_USER", + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK", + "AVALON_APP_NAME", + "OPENPYPE_DEV", + "OPENPYPE_LOG_NO_COLORS", "OPENPYPE_VERSION" ] # Add mongo url if it's enabled - if context.data.get("deadlinePassMongoUrl"): + if self._instance.context.data.get("deadlinePassMongoUrl"): keys.append("OPENPYPE_MONGO") environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) + for key in keys: + val = environment.get(key) + if val: + job_info.EnvironmentKeyValue = "{key}={value}".format( + key=key, + value=val) + # to recognize job from PYPE for turning Event On/Off + job_info.EnvironmentKeyValue = "OPENPYPE_RENDER_JOB=1" - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) - - # Include OutputFilename entries - # The first entry also enables double-click to preview rendered - # frames from Deadline Monitor - output_data = {} for i, filepath in enumerate(instance.data["files"]): dirname = os.path.dirname(filepath) fname = os.path.basename(filepath) - output_data["OutputDirectory%d" % i] = dirname.replace("\\", "/") - output_data["OutputFilename%d" % i] = fname + job_info.OutputDirectory = dirname.replace("\\", "/") + job_info.OutputFilename = fname - # For now ensure destination folder exists otherwise HUSK - # will fail to render the output image. This is supposedly fixed - # in new production builds of Houdini - # TODO Remove this workaround with Houdini 18.0.391+ - if not os.path.exists(dirname): - self.log.info("Ensuring output directory exists: %s" % - dirname) - os.makedirs(dirname) + return job_info - payload["JobInfo"].update(output_data) + def get_plugin_info(self): - self.submit(instance, payload) + instance = self._instance + context = instance.context - def submit(self, instance, payload): + # Output driver to render + driver = instance[0] + hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0] - # get default deadline webservice url from deadline module - deadline_url = instance.context.data.get("defaultDeadline") - # if custom one is set in instance, use that - if instance.data.get("deadlineUrl"): - deadline_url = instance.data.get("deadlineUrl") - assert deadline_url, "Requires Deadline Webservice URL" + plugin_info = DeadlinePluginInfo( + SceneFile=context.data["currentFile"], + OutputDriver=driver.path(), + Version=hou_major_minor, + IgnoreInputs=True + ) - plugin = payload["JobInfo"]["Plugin"] - self.log.info("Using Render Plugin : {}".format(plugin)) + return attr.asdict(plugin_info) - self.log.info("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(deadline_url) - response = requests.post(url, json=payload) - if not response.ok: - raise Exception(response.text) + def process(self, instance): + super(HoudiniSubmitDeadline, self).process(instance) + # TODO: Avoid the need for this logic here, needed for submit publish # Store output dir for unified publisher (filesequence) output_dir = os.path.dirname(instance.data["files"][0]) instance.data["outputDir"] = output_dir - instance.data["deadlineSubmissionJob"] = response.json() + instance.data["toBeRenderedOn"] = "deadline" From 2b6ef60b654e383f9c17ec843747c30b3c1f0a76 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 Aug 2022 12:16:37 +0200 Subject: [PATCH 06/66] Submit publish job for houdini arnold rop --- .../modules/deadline/plugins/publish/submit_publish_job.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 379953c9e4..552aa253f2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -116,10 +116,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): deadline_plugin = "OpenPype" targets = ["local"] - hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"] + hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony", + "houdini"] families = ["render.farm", "prerender.farm", - "renderlayer", "imagesequence", "vrayscene"] + "renderlayer", "imagesequence", "vrayscene", + "arnold_rop"] aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE From 58b524407b5035f592773004c312f970ca374bf1 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 23 Aug 2022 12:18:43 +0200 Subject: [PATCH 07/66] Fix style issue / shush hound --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 4 ++-- .../plugins/publish/submit_houdini_render_deadline.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index dfb8f0d5ee..d90530d3a0 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -107,7 +107,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): suffix=label) render_products.append(aov_product) files_by_aov[label] = self.generate_expected_files(instance, - aov_product) + aov_product) for product in render_products: self.log.debug("Found render product: %s" % product) @@ -120,7 +120,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] # stub required data if "expectedFiles" not in instance.data: - instance.data["expectedFiles"] = list() + instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) def get_render_product_name(self, prefix, suffix): diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 8aabec6e30..6b6eebba7e 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -91,8 +91,9 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): val = environment.get(key) if val: job_info.EnvironmentKeyValue = "{key}={value}".format( - key=key, - value=val) + key=key, + value=val + ) # to recognize job from PYPE for turning Event On/Off job_info.EnvironmentKeyValue = "OPENPYPE_RENDER_JOB=1" From 4ffef19e28d1d8f22d36eb6416228545721eff4e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 13:59:19 +0100 Subject: [PATCH 08/66] Refactor to use new style creator --- .../plugins/create/create_arnold_rop.py | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index f9b5f0c53c..c01586ef11 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -1,7 +1,7 @@ from openpype.hosts.houdini.api import plugin -class CreateArnoldRop(plugin.Creator): +class CreateArnoldRop(plugin.HoudiniCreator): """Arnold ROP""" label = "Arnold ROP" @@ -9,41 +9,47 @@ class CreateArnoldRop(plugin.Creator): icon = "magic" defaults = ["master"] - def __init__(self, *args, **kwargs): - super(CreateArnoldRop, self).__init__(*args, **kwargs) + # Default extension + ext = ".exr" - # Clear the family prefix from the subset - subset = self.data["subset"] - subset_no_prefix = subset[len(self.family):] - subset_no_prefix = subset_no_prefix[0].lower() + subset_no_prefix[1:] - self.data["subset"] = subset_no_prefix - - # Add chunk size attribute - self.data["chunkSize"] = 1 + def create(self, subset_name, instance_data, pre_create_data): + import hou # Remove the active, we are checking the bypass flag of the nodes - self.data.pop("active", None) + instance_data.pop("active", None) + instance_data.update({"node_type": "arnold"}) - self.data.update({"node_type": "arnold"}) + # Add chunk size attribute + instance_data["chunkSize"] = 1 - def _process(self, instance): + instance = super(CreateArnoldRop, self).create( + subset_name, + instance_data, + pre_create_data) # type: plugin.CreatedInstance - basename = instance.name() - instance.setName(basename + "_ROP", unique_name=True) + instance_node = hou.node(instance.get("instance_node")) - prefix = '${HIP}/render/${HIPNAME}/`chs("subset")`.$F4.exr' + # Hide Properties Tab on Arnold ROP since that's used + # for rendering instead of .ass Archive Export + parm_template_group = instance_node.parmTemplateGroup() + parm_template_group.hideFolder("Properties", True) + instance_node.setParmTemplateGroup(parm_template_group) + + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4{}".format(subset_name, self.ext) + ) parms = { # Render frame range "trange": 1, # Arnold ROP settings - "ar_picture": prefix, + "ar_picture": filepath, "ar_exr_half_precision": 1 # half precision } - instance.setParms(parms) - # Lock some Avalon attributes + instance_node.setParms(parms) + + # Lock any parameters in this list to_lock = ["family", "id"] - for name in to_lock: - parm = instance.parm(name) - parm.lock(True) + self.lock_parameters(instance_node, to_lock) From 56885f0eb3755fa4469854b94f7d8cf8be9a1d3f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 14:26:13 +0100 Subject: [PATCH 09/66] Add identifier --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index c01586ef11..41c4fcfaef 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -4,6 +4,7 @@ from openpype.hosts.houdini.api import plugin class CreateArnoldRop(plugin.HoudiniCreator): """Arnold ROP""" + identifier = "io.openpype.creators.houdini.arnold_rop" label = "Arnold ROP" family = "arnold_rop" icon = "magic" From 2b0f19a8a61ed2035781933686580ece30769119 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 14:28:23 +0100 Subject: [PATCH 10/66] Fix arnold rop collector for new style creator --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index d90530d3a0..4d82b74aa2 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -69,7 +69,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): def process(self, instance): - rop = instance[0] + rop = hou.node(instance.data.get("instance_node")) # Collect chunkSize chunk_size_parm = rop.parm("chunkSize") From 6fdbf2b0e16f5c368b31427e01ccd25154982594 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 14:30:57 +0100 Subject: [PATCH 11/66] The 'publish' key in instance data is optional and may not exist --- openpype/modules/deadline/abstract_submit_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 512ff800ee..51f2df19be 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -659,7 +659,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): # test if there is instance of workfile waiting # to be published. - assert i.data["publish"] is True, ( + assert i.data.get("publish", True) is True, ( "Workfile (scene) must be published along") return i From 4c50564d029dde1bfcce9a73a866f0d54ef0cfbe Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 15:09:24 +0100 Subject: [PATCH 12/66] Fix node access --- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 6b6eebba7e..a25bd0f0bb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -111,7 +111,7 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): context = instance.context # Output driver to render - driver = instance[0] + driver = hou.node(instance.data["instance_node"]) hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0] plugin_info = DeadlinePluginInfo( From bbc20c16df3550bf0dcb340d88108c3bdb07a088 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 15:09:38 +0100 Subject: [PATCH 13/66] Fix job info usage --- .../publish/submit_houdini_render_deadline.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index a25bd0f0bb..d5c28994e0 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -88,20 +88,18 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) for key in keys: - val = environment.get(key) - if val: - job_info.EnvironmentKeyValue = "{key}={value}".format( - key=key, - value=val - ) + value = environment.get(key) + if value: + job_info.EnvironmentKeyValue[key] = value + # to recognize job from PYPE for turning Event On/Off - job_info.EnvironmentKeyValue = "OPENPYPE_RENDER_JOB=1" + job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" for i, filepath in enumerate(instance.data["files"]): dirname = os.path.dirname(filepath) fname = os.path.basename(filepath) - job_info.OutputDirectory = dirname.replace("\\", "/") - job_info.OutputFilename = fname + job_info.OutputDirectory += dirname.replace("\\", "/") + job_info.OutputFilename += fname return job_info From e2850d74e5e80bdeaae261ebbf2b2f0bfba8e47a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 15:12:50 +0100 Subject: [PATCH 14/66] Add dedicated collector for instance frame data --- .../publish/collect_instance_frame_data.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py diff --git a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py new file mode 100644 index 0000000000..584343cd64 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py @@ -0,0 +1,56 @@ +import hou + +import pyblish.api + + +class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin): + """Collect time range frame data for the instance node.""" + + order = pyblish.api.CollectorOrder + 0.001 + label = "Instance Node Frame Range" + hosts = ["houdini"] + + def process(self, instance): + + node_path = instance.data.get("instance_node") + node = hou.node(node_path) if node_path else None + if not node_path or not node: + self.log.debug("No instance node found for instance: " + "{}".format(instance)) + return + + frame_data = self.get_frame_data(node) + if not frame_data: + return + + self.log.info("Collected time data: {}".format(frame_data)) + instance.data.update(frame_data) + + def get_frame_data(self, node): + """Get the frame data: start frame, end frame and steps + Args: + node(hou.Node) + + Returns: + dict + + """ + + data = {} + + if node.parm("trange") is None: + self.log.debug("Node has no 'trange' parameter: " + "{}".format(node.path())) + return data + + if node.evalParm("trange") == 0: + # Ignore 'render current frame' + self.log.debug("Node '{}' has 'Render current frame' set. " + "Time range data ignored.".format(node.path())) + return data + + data["frameStart"] = node.evalParm("f1") + data["frameEnd"] = node.evalParm("f2") + data["byFrameStep"] = node.evalParm("f3") + + return data From b3e4d8efd2a949986335d05c9f7a34abbc2f911c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 15:16:07 +0100 Subject: [PATCH 15/66] Fix increment current file plug-in --- .../plugins/publish/increment_current_file.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 8947530874..3b6496c38b 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -5,6 +5,7 @@ from openpype.pipeline import registered_host from openpype.action import get_errored_plugins_from_data from openpype.hosts.houdini.api import HoudiniHost + class IncrementCurrentFile(pyblish.api.ContextPlugin): """Increment the current file. @@ -18,18 +19,8 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): families = ["workfile", "redshift_rop", "arnold_rop", "usdrender"] optional = True - def process(self, instance): + def process(self, context): - # This should be a ContextPlugin, but this is a workaround - # for a bug in pyblish to run once for a family: issue #250 - context = instance.context - key = "__hasRun{}".format(self.__class__.__name__) - if context.data.get(key, False): - return - else: - context.data[key] = True - - context = instance.context errored_plugins = get_errored_plugins_from_data(context) if any( plugin.__name__ == "HoudiniSubmitPublishDeadline" From bd3183bcbc25b670255416f302bd1b11d261abc8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 25 Jan 2023 15:17:07 +0100 Subject: [PATCH 16/66] Tweak error messages --- .../hosts/houdini/plugins/publish/increment_current_file.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 3b6496c38b..a307ee452c 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -4,6 +4,7 @@ from openpype.lib import version_up from openpype.pipeline import registered_host from openpype.action import get_errored_plugins_from_data from openpype.hosts.houdini.api import HoudiniHost +from openpype.pipeline.publish import KnownPublishError class IncrementCurrentFile(pyblish.api.ContextPlugin): @@ -26,7 +27,7 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): plugin.__name__ == "HoudiniSubmitPublishDeadline" for plugin in errored_plugins ): - raise RuntimeError( + raise KnownPublishError( "Skipping incrementing current file because " "submission to deadline failed." ) @@ -36,7 +37,7 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): current_file = host.current_file() assert ( context.data["currentFile"] == current_file - ), "Collected filename from current scene name." + ), "Collected filename mismatches from current scene name." new_filepath = version_up(current_file) host.save_workfile(new_filepath) From 5218b6550678cb3e3a5127a449a631652d62c249 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 11 Mar 2023 18:59:43 +0100 Subject: [PATCH 17/66] Shush hound --- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 3c7250e65b..d781496cde 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -12,6 +12,7 @@ from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo from openpype.lib import is_running_from_build + @attr.s class DeadlinePluginInfo(): SceneFile = attr.ib(default=None) From 414cd0cce113328755103f046e3a5aeef0e432e5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:09:56 +0800 Subject: [PATCH 18/66] add mantra and karma farm publishing for Houdini --- .../plugins/create/create_karma_rop.py | 114 ++++++++++++ .../plugins/create/create_mantra_rop.py | 78 ++++++++ .../publish/collect_instance_frame_data.py | 56 ++++++ .../plugins/publish/collect_instances.py | 2 +- .../plugins/publish/collect_karma_rop.py | 136 ++++++++++++++ .../plugins/publish/collect_mantra_rop.py | 158 ++++++++++++++++ .../plugins/publish/collect_redshift_rop.py | 41 +++- .../plugins/publish/increment_current_file.py | 7 +- .../deadline/abstract_submit_deadline.py | 2 +- .../publish/submit_houdini_render_deadline.py | 175 +++++++----------- .../plugins/publish/submit_publish_job.py | 7 +- 11 files changed, 666 insertions(+), 110 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/create/create_karma_rop.py create mode 100644 openpype/hosts/houdini/plugins/create/create_mantra_rop.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_karma_rop.py create mode 100644 openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py new file mode 100644 index 0000000000..f2a6908d01 --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +"""Creator plugin to create Karma ROP.""" +from openpype.hosts.houdini.api import plugin +from openpype.pipeline import CreatedInstance +from openpype.lib import BoolDef, EnumDef, NumberDef + + +class CreateKarmaROP(plugin.HoudiniCreator): + """Karma ROP""" + identifier = "io.openpype.creators.houdini.karma_rop" + label = "Karma ROP" + family = "karma_rop" + icon = "magic" + defaults = ["master"] + + def create(self, subset_name, instance_data, pre_create_data): + import hou # noqa + + instance_data.pop("active", None) + instance_data.update({"node_type": "karma"}) + # Add chunk size attribute + instance_data["chunkSize"] = 10 + + instance = super(CreateKarmaROP, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + + instance_node = hou.node(instance.get("instance_node")) + + ext = pre_create_data.get("image_format") + + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.{}".format(subset_name, ext) + ) + checkpoint = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.checkpoint".format(subset_name) + ) + + usd_directory = "{}{}".format( + hou.text.expandString("$HIP/pyblish/usd_renders/"), + "{}_$RENDERID".format(subset_name) + ) + + parms = { + # Render Frame Range + "trange": 1, + # Karma ROP Setting + "picture": filepath, + # Karma Checkpoint Setting + "productName": checkpoint, + # USD Output Directory + "savetodirectory": usd_directory, + } + + res_x = pre_create_data.get("res_x") + res_y = pre_create_data.get("res_y") + + if self.selected_nodes: + # If camera found in selection + # we will use as render camera + camera = None + for node in self.selected_nodes: + if node.type().name() == "cam": + camera = node.path() + camera_node = hou.node(camera) + has_camera = pre_create_data.get("cam_res") + if has_camera: + res_x = camera_node.evalParm("resx") + res_y = camera_node.evalParm("resy") + + if not camera: + self.log.warning("No render camera found in selection") + + + parms.update({ + "camera": camera or "", + "resolutionx": res_x, + "resolutiony": res_y, + }) + + instance_node.setParms(parms) + + # Lock some Avalon attributes + to_lock = ["family", "id"] + self.lock_parameters(instance_node, to_lock) + + def get_pre_create_attr_defs(self): + attrs = super(CreateKarmaROP, self).get_pre_create_attr_defs() + + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", + "rad", "rat", "rta", "sgi", "tga", "tif", + ] + + return attrs + [ + EnumDef("image_format", + image_format_enum, + default="exr", + label="Image Format Options"), + NumberDef("res_x", + label="width", + default=1920, + decimals=0), + NumberDef("res_y", + label="height", + default=720, + decimals=0), + BoolDef("cam_res", + label="Camera Resolution", + default=False) + ] diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py new file mode 100644 index 0000000000..b1ae996a30 --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +"""Creator plugin to create Mantra ROP.""" +from openpype.hosts.houdini.api import plugin +from openpype.pipeline import CreatedInstance +from openpype.lib import EnumDef + + +class CreateMantraROP(plugin.HoudiniCreator): + """Mantra ROP""" + identifier = "io.openpype.creators.houdini.mantra_rop" + label = "Mantra ROP" + family = "mantra_rop" + icon = "magic" + defaults = ["master"] + + def create(self, subset_name, instance_data, pre_create_data): + import hou # noqa + + instance_data.pop("active", None) + instance_data.update({"node_type": "ifd"}) + # Add chunk size attribute + instance_data["chunkSize"] = 10 + + instance = super(CreateMantraROP, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + + instance_node = hou.node(instance.get("instance_node")) + + ext = pre_create_data.get("image_format") + + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.{}".format(subset_name, ext) + ) + + parms = { + # Render Frame Range + "trange": 1, + # Mantra ROP Setting + "vm_picture": filepath, + } + + if self.selected_nodes: + # If camera found in selection + # we will use as render camera + camera = None + for node in self.selected_nodes: + if node.type().name() == "cam": + camera = node.path() + + if not camera: + self.log.warning("No render camera found in selection") + + parms.update({"camera": camera or ""}) + + instance_node.setParms(parms) + + # Lock some Avalon attributes + to_lock = ["family", "id"] + self.lock_parameters(instance_node, to_lock) + + def get_pre_create_attr_defs(self): + attrs = super(CreateMantraROP, self).get_pre_create_attr_defs() + + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", + "rad", "rat", "rta", "sgi", "tga", "tif", + ] + + return attrs + [ + EnumDef("image_format", + image_format_enum, + default="exr", + label="Image Format Options") + ] + # Extract Import Plane parameters(Should be in the setting) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py new file mode 100644 index 0000000000..584343cd64 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_instance_frame_data.py @@ -0,0 +1,56 @@ +import hou + +import pyblish.api + + +class CollectInstanceNodeFrameRange(pyblish.api.InstancePlugin): + """Collect time range frame data for the instance node.""" + + order = pyblish.api.CollectorOrder + 0.001 + label = "Instance Node Frame Range" + hosts = ["houdini"] + + def process(self, instance): + + node_path = instance.data.get("instance_node") + node = hou.node(node_path) if node_path else None + if not node_path or not node: + self.log.debug("No instance node found for instance: " + "{}".format(instance)) + return + + frame_data = self.get_frame_data(node) + if not frame_data: + return + + self.log.info("Collected time data: {}".format(frame_data)) + instance.data.update(frame_data) + + def get_frame_data(self, node): + """Get the frame data: start frame, end frame and steps + Args: + node(hou.Node) + + Returns: + dict + + """ + + data = {} + + if node.parm("trange") is None: + self.log.debug("Node has no 'trange' parameter: " + "{}".format(node.path())) + return data + + if node.evalParm("trange") == 0: + # Ignore 'render current frame' + self.log.debug("Node '{}' has 'Render current frame' set. " + "Time range data ignored.".format(node.path())) + return data + + data["frameStart"] = node.evalParm("f1") + data["frameEnd"] = node.evalParm("f2") + data["byFrameStep"] = node.evalParm("f3") + + return data diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index bb85630552..3fec2c4673 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -116,6 +116,6 @@ class CollectInstances(pyblish.api.ContextPlugin): data["frameStart"] = node.evalParm("f1") data["frameEnd"] = node.evalParm("f2") - data["steps"] = node.evalParm("f3") + data["byFrameStep"] = node.evalParm("f3") return data diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py new file mode 100644 index 0000000000..10c97269fc --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -0,0 +1,136 @@ +import re +import os + +import hou +import pyblish.api + + +def get_top_referenced_parm(parm): + + processed = set() # disallow infinite loop + while True: + if parm.path() in processed: + raise RuntimeError("Parameter references result in cycle.") + + processed.add(parm.path()) + + ref = parm.getReferencedParm() + if ref.path() == parm.path(): + # It returns itself when it doesn't reference + # another parameter + return ref + else: + parm = ref + + +def evalParmNoFrame(node, parm, pad_character="#"): + + parameter = node.parm(parm) + assert parameter, "Parameter does not exist: %s.%s" % (node, parm) + + # If the parameter has a parameter reference, then get that + # parameter instead as otherwise `unexpandedString()` fails. + parameter = get_top_referenced_parm(parameter) + + # Substitute out the frame numbering with padded characters + try: + raw = parameter.unexpandedString() + except hou.Error as exc: + print("Failed: %s" % parameter) + raise RuntimeError(exc) + + def replace(match): + padding = 1 + n = match.group(2) + if n and int(n): + padding = int(n) + return pad_character * padding + + expression = re.sub(r"(\$F([0-9]*))", replace, raw) + + with hou.ScriptEvalContext(parameter): + return hou.expandStringAtFrame(expression, 0) + + +class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): + """Collect Karma Render Products + + Collects the instance.data["files"] for the multipart render product. + + Provides: + instance -> files + + """ + + label = "Karma ROP Render Products" + order = pyblish.api.CollectorOrder + 0.4 + hosts = ["houdini"] + families = ["karma_rop"] + + def process(self, instance): + + rop = hou.node(instance.data.get("instance_node")) + + # Collect chunkSize + chunk_size_parm = rop.parm("chunkSize") + if chunk_size_parm: + chunk_size = int(chunk_size_parm.eval()) + instance.data["chunkSize"] = chunk_size + self.log.debug("Chunk Size: %s" % chunk_size) + + default_prefix = evalParmNoFrame(rop, "picture") + render_products = [] + + # Default beauty AOV + beauty_product = self.get_render_product_name( + prefix=default_prefix, suffix=None + ) + render_products.append(beauty_product) + + files_by_aov = { + "beauty": self.generate_expected_files(instance, + beauty_product) + } + + filenames = list(render_products) + instance.data["files"] = filenames + + for product in render_products: + self.log.debug("Found render product: %s" % product) + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["expectedFiles"].append(files_by_aov) + + def get_render_product_name(self, prefix, suffix): + if suffix: + # Add ".{suffix}" before the extension + prefix_base, ext = os.path.splitext(prefix) + product_name = prefix_base + "." + suffix + ext + else: + product_name = prefix + + return product_name + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py new file mode 100644 index 0000000000..c6cb2a6f49 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -0,0 +1,158 @@ +import re +import os + +import hou +import pyblish.api + + +def get_top_referenced_parm(parm): + + processed = set() # disallow infinite loop + while True: + if parm.path() in processed: + raise RuntimeError("Parameter references result in cycle.") + + processed.add(parm.path()) + + ref = parm.getReferencedParm() + if ref.path() == parm.path(): + # It returns itself when it doesn't reference + # another parameter + return ref + else: + parm = ref + + +def evalParmNoFrame(node, parm, pad_character="#"): + + parameter = node.parm(parm) + assert parameter, "Parameter does not exist: %s.%s" % (node, parm) + + # If the parameter has a parameter reference, then get that + # parameter instead as otherwise `unexpandedString()` fails. + parameter = get_top_referenced_parm(parameter) + + # Substitute out the frame numbering with padded characters + try: + raw = parameter.unexpandedString() + except hou.Error as exc: + print("Failed: %s" % parameter) + raise RuntimeError(exc) + + def replace(match): + padding = 1 + n = match.group(2) + if n and int(n): + padding = int(n) + return pad_character * padding + + expression = re.sub(r"(\$F([0-9]*))", replace, raw) + + with hou.ScriptEvalContext(parameter): + return hou.expandStringAtFrame(expression, 0) + + +class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): + """Collect Mantra Render Products + + Collects the instance.data["files"] for the render products. + + Provides: + instance -> files + + """ + + label = "Mantra ROP Render Products" + order = pyblish.api.CollectorOrder + 0.4 + hosts = ["houdini"] + families = ["mantra_rop"] + + def process(self, instance): + + rop = hou.node(instance.data.get("instance_node")) + + # Collect chunkSize + chunk_size_parm = rop.parm("chunkSize") + if chunk_size_parm: + chunk_size = int(chunk_size_parm.eval()) + instance.data["chunkSize"] = chunk_size + self.log.debug("Chunk Size: %s" % chunk_size) + + default_prefix = evalParmNoFrame(rop, "vm_picture") + render_products = [] + + # Default beauty AOV + beauty_product = self.get_render_product_name( + prefix=default_prefix, suffix=None + ) + render_products.append(beauty_product) + + files_by_aov = { + "beauty": self.generate_expected_files(instance, + beauty_product) + } + + aov_numbers = rop.evalParm("vm_numaux") + if aov_numbers > 0: + # get the filenames of the AOVs + for i in range(1, aov_numbers + 1): + var = rop.evalParm("vm_variable_plane%d" % i) + if var: + aov_name = "vm_filename_plane%d" % i + has_aov_path = rop.evalParm(aov_name) + if has_aov_path: + aov_prefix = evalParmNoFrame(rop, aov_name) + aov_product = self.get_render_product_name( + prefix=aov_prefix, suffix=None + ) + render_products.append(aov_product) + + files_by_aov[var] = self.generate_expected_files(instance, + aov_product) + for product in render_products: + self.log.debug("Found render product: %s" % product) + filenames = list(render_products) + instance.data["files"] = filenames + + # For now by default do NOT try to publish the rendered output + instance.data["publishJobState"] = "Suspended" + instance.data["attachTo"] = [] # stub required data + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["expectedFiles"].append(files_by_aov) + self.log.debug("expectedFiles: %s" % files_by_aov) + + + def get_render_product_name(self, prefix, suffix): + if suffix: + # Add ".{suffix}" before the extension + prefix_base, ext = os.path.splitext(prefix) + product_name = prefix_base + "." + suffix + ext + else: + product_name = prefix + + return product_name + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index f1d73d7523..1a0100dd73 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -87,6 +87,10 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): prefix=default_prefix, suffix=beauty_suffix ) render_products.append(beauty_product) + files_by_aov = { + "beauty": self.generate_expected_files(instance, + beauty_product) + } num_aovs = rop.evalParm("RS_aov") for index in range(num_aovs): @@ -104,11 +108,21 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): aov_product = self.get_render_product_name(aov_prefix, aov_suffix) render_products.append(aov_product) + files_by_aov[aov_suffix] = self.generate_expected_files(instance, + aov_product) + for product in render_products: self.log.debug("Found render product: %s" % product) + filenames = list(render_products) + instance.data["files"] = filenames - filenames = list(render_products) - instance.data["files"] = filenames + # For now by default do NOT try to publish the rendered output + instance.data["publishJobState"] = "Suspended" + instance.data["attachTo"] = [] # stub required data + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["expectedFiles"].append(files_by_aov) def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" @@ -133,3 +147,26 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): product_name = prefix return product_name + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files diff --git a/openpype/hosts/houdini/plugins/publish/increment_current_file.py b/openpype/hosts/houdini/plugins/publish/increment_current_file.py index 16d9ef9aec..3c71dd6287 100644 --- a/openpype/hosts/houdini/plugins/publish/increment_current_file.py +++ b/openpype/hosts/houdini/plugins/publish/increment_current_file.py @@ -14,7 +14,12 @@ class IncrementCurrentFile(pyblish.api.ContextPlugin): label = "Increment current file" order = pyblish.api.IntegratorOrder + 9.0 hosts = ["houdini"] - families = ["workfile"] + families = ["workfile", + "redshift_rop", + "arnold_rop", + "mantra_rop", + "karma_rop", + "usdrender"] optional = True def process(self, context): diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 648eb77007..aea18f6dfd 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -663,7 +663,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): # test if there is instance of workfile waiting # to be published. - assert i.data["publish"] is True, ( + assert i.data.get("publish", True) is True, ( "Workfile (scene) must be published along") return i diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 73ab689c9a..d8de47b596 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -1,19 +1,27 @@ +import hou + import os -import json +import attr import getpass from datetime import datetime - -import requests import pyblish.api -# import hou ??? - from openpype.pipeline import legacy_io from openpype.tests.lib import is_in_tests +from openpype_modules.deadline import abstract_submit_deadline +from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo from openpype.lib import is_running_from_build -class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): +@attr.s +class DeadlinePluginInfo(): + SceneFile = attr.ib(default=None) + OutputDriver = attr.ib(default=None) + Version = attr.ib(default=None) + IgnoreInputs = attr.ib(default=True) + + +class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): """Submit Solaris USD Render ROPs to Deadline. Renders are submitted to a Deadline Web Service as @@ -30,83 +38,55 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): order = pyblish.api.IntegratorOrder hosts = ["houdini"] families = ["usdrender", - "redshift_rop"] + "redshift_rop", + "mantra_rop", + "karma_rop"] targets = ["local"] + use_published = True - def process(self, instance): + def get_job_info(self): + job_info = DeadlineJobInfo(Plugin="Houdini") + instance = self._instance context = instance.context - code = context.data["code"] + filepath = context.data["currentFile"] filename = os.path.basename(filepath) - comment = context.data.get("comment", "") - deadline_user = context.data.get("deadlineUser", getpass.getuser()) - jobname = "%s - %s" % (filename, instance.name) - # Support code prefix label for batch name - batch_name = filename - if code: - batch_name = "{0} - {1}".format(code, batch_name) + job_info.Name = "%s - %s" % (filename, instance.name) + job_info.BatchName = filename + job_info.Plugin = "Houdini" + job_info.UserName = context.data.get( + "deadlineUser", getpass.getuser()) if is_in_tests(): - batch_name += datetime.now().strftime("%d%m%Y%H%M%S") + job_info.BatchName += datetime.now().strftime("%d%m%Y%H%M%S") - # Output driver to render - driver = instance[0] - - # StartFrame to EndFrame by byFrameStep + # Deadline requires integers in frame range frames = "{start}-{end}x{step}".format( start=int(instance.data["frameStart"]), end=int(instance.data["frameEnd"]), step=int(instance.data["byFrameStep"]), ) + job_info.Frames = frames - # Documentation for keys available at: - # https://docs.thinkboxsoftware.com - # /products/deadline/8.0/1_User%20Manual/manual - # /manual-submission.html#job-info-file-options - payload = { - "JobInfo": { - # Top-level group name - "BatchName": batch_name, + job_info.Pool = instance.data.get("primaryPool") + job_info.SecondaryPool = instance.data.get("secondaryPool") + job_info.ChunkSize = instance.data.get("chunkSize", 10) + job_info.Comment = context.data.get("comment") - # Job name, as seen in Monitor - "Name": jobname, - - # Arbitrary username, for visualisation in Monitor - "UserName": deadline_user, - - "Plugin": "Houdini", - "Pool": instance.data.get("primaryPool"), - "secondaryPool": instance.data.get("secondaryPool"), - "Frames": frames, - - "ChunkSize": instance.data.get("chunkSize", 10), - - "Comment": comment - }, - "PluginInfo": { - # Input - "SceneFile": filepath, - "OutputDriver": driver.path(), - - # Mandatory for Deadline - # Houdini version without patch number - "Version": hou.applicationVersionString().rsplit(".", 1)[0], - - "IgnoreInputs": True - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - # Include critical environment variables with submission + api.Session keys = [ - # Submit along the current Avalon tool setup that we launched - # this application with so the Render Slave can build its own - # similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9" - "AVALON_TOOLS" + "FTRACK_API_KEY", + "FTRACK_API_USER", + "FTRACK_SERVER", + "OPENPYPE_SG_USER", + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK", + "AVALON_APP_NAME", + "OPENPYPE_DEV", + "OPENPYPE_LOG_NO_COLORS", + "OPENPYPE_VERSION" ] # Add OpenPype version if we are running from build. @@ -114,61 +94,50 @@ class HoudiniSubmitRenderDeadline(pyblish.api.InstancePlugin): keys.append("OPENPYPE_VERSION") # Add mongo url if it's enabled - if context.data.get("deadlinePassMongoUrl"): + if self._instance.context.data.get("deadlinePassMongoUrl"): keys.append("OPENPYPE_MONGO") environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) + for key in keys: + value = environment.get(key) + if value: + job_info.EnvironmentKeyValue[key] = value - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) + # to recognize job from PYPE for turning Event On/Off + job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" - # Include OutputFilename entries - # The first entry also enables double-click to preview rendered - # frames from Deadline Monitor - output_data = {} for i, filepath in enumerate(instance.data["files"]): dirname = os.path.dirname(filepath) fname = os.path.basename(filepath) - output_data["OutputDirectory%d" % i] = dirname.replace("\\", "/") - output_data["OutputFilename%d" % i] = fname + job_info.OutputDirectory += dirname.replace("\\", "/") + job_info.OutputFilename += fname - # For now ensure destination folder exists otherwise HUSK - # will fail to render the output image. This is supposedly fixed - # in new production builds of Houdini - # TODO Remove this workaround with Houdini 18.0.391+ - if not os.path.exists(dirname): - self.log.info("Ensuring output directory exists: %s" % - dirname) - os.makedirs(dirname) + return job_info - payload["JobInfo"].update(output_data) + def get_plugin_info(self): - self.submit(instance, payload) + instance = self._instance + context = instance.context - def submit(self, instance, payload): + # Output driver to render + driver = hou.node(instance.data["instance_node"]) + hou_major_minor = hou.applicationVersionString().rsplit(".", 1)[0] - AVALON_DEADLINE = legacy_io.Session.get("AVALON_DEADLINE", - "http://localhost:8082") - assert AVALON_DEADLINE, "Requires AVALON_DEADLINE" + plugin_info = DeadlinePluginInfo( + SceneFile=context.data["currentFile"], + OutputDriver=driver.path(), + Version=hou_major_minor, + IgnoreInputs=True + ) - plugin = payload["JobInfo"]["Plugin"] - self.log.info("Using Render Plugin : {}".format(plugin)) + return attr.asdict(plugin_info) - self.log.info("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(AVALON_DEADLINE) - response = requests.post(url, json=payload) - if not response.ok: - raise Exception(response.text) + def process(self, instance): + super(HoudiniSubmitDeadline, self).process(instance) + # TODO: Avoid the need for this logic here, needed for submit publish # Store output dir for unified publisher (filesequence) output_dir = os.path.dirname(instance.data["files"][0]) instance.data["outputDir"] = output_dir - instance.data["deadlineSubmissionJob"] = response.json() + instance.data["toBeRenderedOn"] = "deadline" diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 4765772bcf..cd70201832 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -122,7 +122,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "celaction", "aftereffects", "harmony"] families = ["render.farm", "prerender.farm", - "renderlayer", "imagesequence", "maxrender", "vrayscene"] + "renderlayer", "imagesequence", + "maxrender", "vrayscene", + "mantra_rop", "karma_rop"] aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE @@ -140,7 +142,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "FTRACK_SERVER", "AVALON_APP_NAME", "OPENPYPE_USERNAME", - "OPENPYPE_SG_USER", + "OPENPYPE_VERSION", + "OPENPYPE_SG_USER" ] # Add OpenPype version if we are running from build. From ecc673295cc4bf8ddb4e725a027050bc67e79b31 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:24:00 +0800 Subject: [PATCH 19/66] hound fix --- openpype/hosts/houdini/plugins/create/create_karma_rop.py | 7 +++---- .../hosts/houdini/plugins/publish/collect_mantra_rop.py | 5 +---- .../hosts/houdini/plugins/publish/collect_redshift_rop.py | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index f2a6908d01..8d55298926 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -34,12 +34,12 @@ class CreateKarmaROP(plugin.HoudiniCreator): hou.text.expandString("$HIP/pyblish/"), "{}.$F4.{}".format(subset_name, ext) ) - checkpoint = "{}{}".format( + checkpoint = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), "{}.$F4.checkpoint".format(subset_name) ) - usd_directory = "{}{}".format( + usd_directory = "{}{}".format( hou.text.expandString("$HIP/pyblish/usd_renders/"), "{}_$RENDERID".format(subset_name) ) @@ -74,12 +74,11 @@ class CreateKarmaROP(plugin.HoudiniCreator): if not camera: self.log.warning("No render camera found in selection") - parms.update({ "camera": camera or "", "resolutionx": res_x, "resolutiony": res_y, - }) + }) instance_node.setParms(parms) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index c6cb2a6f49..1a881389db 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -22,7 +22,6 @@ def get_top_referenced_parm(parm): else: parm = ref - def evalParmNoFrame(node, parm, pad_character="#"): parameter = node.parm(parm) @@ -108,7 +107,7 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): render_products.append(aov_product) files_by_aov[var] = self.generate_expected_files(instance, - aov_product) + aov_product) # noqa for product in render_products: self.log.debug("Found render product: %s" % product) filenames = list(render_products) @@ -121,8 +120,6 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) - self.log.debug("expectedFiles: %s" % files_by_aov) - def get_render_product_name(self, prefix, suffix): if suffix: diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 1a0100dd73..60d3a977fc 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -109,7 +109,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): render_products.append(aov_product) files_by_aov[aov_suffix] = self.generate_expected_files(instance, - aov_product) + aov_product) # noqa for product in render_products: self.log.debug("Found render product: %s" % product) From c9a88d06abdf419fc08dde0529dd4f2de51775ad Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:28:38 +0800 Subject: [PATCH 20/66] hound fix --- openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 1a881389db..1eb850e52e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -22,6 +22,7 @@ def get_top_referenced_parm(parm): else: parm = ref + def evalParmNoFrame(node, parm, pad_character="#"): parameter = node.parm(parm) @@ -106,8 +107,8 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): ) render_products.append(aov_product) - files_by_aov[var] = self.generate_expected_files(instance, - aov_product) # noqa + files_by_aov[var] = self.generate_expected_files(instance, aov_product) # noqa + for product in render_products: self.log.debug("Found render product: %s" % product) filenames = list(render_products) From eee45f42879cfcd117dbd4ab70c0dd0e686cd583 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:29:21 +0800 Subject: [PATCH 21/66] hound fix --- openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 60d3a977fc..82874e546f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -90,7 +90,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): files_by_aov = { "beauty": self.generate_expected_files(instance, beauty_product) - } + } num_aovs = rop.evalParm("RS_aov") for index in range(num_aovs): From dfa2ee370580054e9979a070c6ea83dba03524dc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:30:11 +0800 Subject: [PATCH 22/66] hound fix --- openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 82874e546f..15df12e075 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -89,8 +89,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): render_products.append(beauty_product) files_by_aov = { "beauty": self.generate_expected_files(instance, - beauty_product) - } + beauty_product)} num_aovs = rop.evalParm("RS_aov") for index in range(num_aovs): From 63a168c2f25be5b479da5907c69f28810b06420d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 15:09:40 +0800 Subject: [PATCH 23/66] allows the user to choose image format for arnold rendering option --- .../plugins/create/create_arnold_rop.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 41c4fcfaef..824d891e04 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -1,5 +1,5 @@ from openpype.hosts.houdini.api import plugin - +from openpype.lib import EnumDef class CreateArnoldRop(plugin.HoudiniCreator): """Arnold ROP""" @@ -36,9 +36,11 @@ class CreateArnoldRop(plugin.HoudiniCreator): parm_template_group.hideFolder("Properties", True) instance_node.setParmTemplateGroup(parm_template_group) + ext = pre_create_data.get("image_format") + filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), - "{}.$F4{}".format(subset_name, self.ext) + "{}.$F4{}".format(subset_name, ext) ) parms = { # Render frame range @@ -54,3 +56,18 @@ class CreateArnoldRop(plugin.HoudiniCreator): # Lock any parameters in this list to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) + + def get_pre_create_attr_defs(self): + attrs = super(CreateArnoldRop, self).get_pre_create_attr_defs() + + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", + "rad", "rat", "rta", "sgi", "tga", "tif", + ] + + return attrs + [ + EnumDef("image_format", + image_format_enum, + default=self.ext, + label="Image Format Options") + ] From fd2d365f1e6ad254422759da011387d72e797367 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 15:11:23 +0800 Subject: [PATCH 24/66] hounf --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 1 + openpype/hosts/houdini/plugins/create/create_mantra_rop.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 824d891e04..e012dd467b 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -1,6 +1,7 @@ from openpype.hosts.houdini.api import plugin from openpype.lib import EnumDef + class CreateArnoldRop(plugin.HoudiniCreator): """Arnold ROP""" diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index b1ae996a30..0d7ccce099 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -75,4 +75,3 @@ class CreateMantraROP(plugin.HoudiniCreator): default="exr", label="Image Format Options") ] - # Extract Import Plane parameters(Should be in the setting) From 26ba2e6bee906dc1f69a625c24333a0a12264369 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 15:26:03 +0800 Subject: [PATCH 25/66] fix the syntax error --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index e012dd467b..0657d349f9 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -12,7 +12,7 @@ class CreateArnoldRop(plugin.HoudiniCreator): defaults = ["master"] # Default extension - ext = ".exr" + ext = "exr" def create(self, subset_name, instance_data, pre_create_data): import hou @@ -41,7 +41,7 @@ class CreateArnoldRop(plugin.HoudiniCreator): filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), - "{}.$F4{}".format(subset_name, ext) + "{}.$F4.{}".format(subset_name, ext) ) parms = { # Render frame range From ee99b21e97fcc3236693f2a5dfe846cd815d85d2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 22:24:28 +0800 Subject: [PATCH 26/66] add override camera resolution options in creator's setting --- .../houdini/plugins/create/create_mantra_rop.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 0d7ccce099..2632f8d6c0 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -2,7 +2,7 @@ """Creator plugin to create Mantra ROP.""" from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance -from openpype.lib import EnumDef +from openpype.lib import EnumDef, BoolDef class CreateMantraROP(plugin.HoudiniCreator): @@ -55,6 +55,9 @@ class CreateMantraROP(plugin.HoudiniCreator): parms.update({"camera": camera or ""}) + custom_res = pre_create_data.get("override_resolution") + if custom_res: + parms.update({"override_camerares": 1}) instance_node.setParms(parms) # Lock some Avalon attributes @@ -73,5 +76,10 @@ class CreateMantraROP(plugin.HoudiniCreator): EnumDef("image_format", image_format_enum, default="exr", - label="Image Format Options") + label="Image Format Options"), + BoolDef("override_resolution", + label="Override Camera Resolution", + tooltip="Override the current camera " + "resolution, recommended for IPR.", + default=False) ] From 80a5982a2460b09cc982fbc1076d26f9aab731e5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 22:27:33 +0800 Subject: [PATCH 27/66] allows to fully delete the render instances if users click the remove button in creator --- openpype/hosts/houdini/api/plugin.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 340a7f0770..6fb0f8b967 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -247,9 +247,22 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """ for instance in instances: instance_node = hou.node(instance.data.get("instance_node")) + node = instance.data.get("instance_node") + if instance_node: instance_node.destroy() + # for the extra render node from the plugins + # such as vray and redshift + ipr_node = hou.node("{}{}".format(node, + "_IPR")) + if ipr_node: + ipr_node.destroy() + re_node = hou.node("{}{}".format(node, + "_render_element")) + if re_node: + re_node.destroy() + self._remove_instance_from_context(instance) def get_pre_create_attr_defs(self): From d925322aa4e270595000b32a7aa00531130bf12c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 22:28:15 +0800 Subject: [PATCH 28/66] add vray render creator --- .../houdini/plugins/create/create_vray_rop.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/create/create_vray_rop.py diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py new file mode 100644 index 0000000000..445f55581c --- /dev/null +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +"""Creator plugin to create VRay ROP.""" +from openpype.hosts.houdini.api import plugin +from openpype.pipeline import CreatedInstance +from openpype.lib import EnumDef, BoolDef + + +class CreateVrayROP(plugin.HoudiniCreator): + """VRay ROP""" + + identifier = "io.openpype.creators.houdini.vray_rop" + label = "VRay ROP" + family = "vray_rop" + icon = "magic" + defaults = ["master"] + + ext = "exr" + + def create(self, subset_name, instance_data, pre_create_data): + import hou # + + instance_data.pop("active", None) + instance_data.update({"node_type": "vray_renderer"}) + # Add chunk size attribute + instance_data["chunkSize"] = 10 + + instance = super(CreateVrayROP, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + + instance_node = hou.node(instance.get("instance_node")) + + # Add IPR for Vray + basename = instance_node.name() + try: + ipr_rop = instance_node.parent().createNode( + "vray", node_name=basename + "_IPR" + ) + except hou.OperationFailed: + raise plugin.OpenPypeCreatorError( + "Cannot create Vray render node. " + "Make sure Vray installed and enabled!" + ) + + ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1)) + ipr_rop.parm("rop").set(instance_node.path()) + + ext = pre_create_data.get("image_format") + + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.{}".format(subset_name, ext) + ) + + parms = { + "trange": 1, + "SettingsOutput_img_file_path": filepath, + "SettingsEXR_bits_per_channel": "16" # half precision + } + + if self.selected_nodes: + # set up the render camera from the selected node + camera = None + for node in self.selected_nodes: + if node.type().name() == "cam": + camera = node.path() + parms.update({ + "render_camera": camera or "" + }) + + #TODO:Add Render Element + has_re = pre_create_data.get("render_element_enabled") + if has_re: + re_rop = instance_node.parent().createNode( + "vray_render_channels", + node_name=basename + "_render_element" + ) + # move the render element node next to the vray renderer node + re_rop.setPosition(instance_node.position() + hou.Vector2(0, 1)) + re_path = re_rop.path() + parms.update({ + "use_render_channels": 1, + "render_network_render_channels": re_path + }) + else: + parms.update({ + "use_render_channels": 0 + }) + + custom_res = pre_create_data.get("override_resolution") + if custom_res: + parms.update({"override_camerares": 1}) + + instance_node.setParms(parms) + + # lock parameters from AVALON + to_lock = ["family", "id"] + self.lock_parameters(instance_node, to_lock) + + def get_pre_create_attr_defs(self): + attrs = super(CreateVrayROP, self).get_pre_create_attr_defs() + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", + "rad", "rat", "rta", "sgi", "tga", "tif", + ] + + return attrs + [ + EnumDef("image_format", + image_format_enum, + default=self.ext, + label="Image Format Options"), + BoolDef("override_resolution", + label="Override Camera Resolution", + tooltip="Override the current camera " + "resolution, recommended for IPR.", + default=False), + BoolDef("render_element_enabled", + label="Render Element", + tooltip="Create Render Element Node " + "if enabled", + default=False) + ] +# ${HIP}/render/${HIPNAME}.${AOV}.$F4.exr From 6c242d9285f3d6c924098b04049fd93525614a8b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Apr 2023 22:30:12 +0800 Subject: [PATCH 29/66] hound --- openpype/hosts/houdini/plugins/create/create_vray_rop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 445f55581c..f0430a2892 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -56,11 +56,11 @@ class CreateVrayROP(plugin.HoudiniCreator): parms = { "trange": 1, "SettingsOutput_img_file_path": filepath, - "SettingsEXR_bits_per_channel": "16" # half precision + "SettingsEXR_bits_per_channel": "16" # half precision } if self.selected_nodes: - # set up the render camera from the selected node + # set up the render camera from the selected node camera = None for node in self.selected_nodes: if node.type().name() == "cam": @@ -69,7 +69,7 @@ class CreateVrayROP(plugin.HoudiniCreator): "render_camera": camera or "" }) - #TODO:Add Render Element + # Enable render element has_re = pre_create_data.get("render_element_enabled") if has_re: re_rop = instance_node.parent().createNode( From 7fe588fdea99f085dddcd7e37ff44961c37fff03 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 13 Apr 2023 14:44:47 +0800 Subject: [PATCH 30/66] roy's comment --- openpype/hosts/houdini/api/plugin.py | 13 ------------- .../houdini/plugins/create/create_vray_rop.py | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 6fb0f8b967..340a7f0770 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -247,22 +247,9 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): """ for instance in instances: instance_node = hou.node(instance.data.get("instance_node")) - node = instance.data.get("instance_node") - if instance_node: instance_node.destroy() - # for the extra render node from the plugins - # such as vray and redshift - ipr_node = hou.node("{}{}".format(node, - "_IPR")) - if ipr_node: - ipr_node.destroy() - re_node = hou.node("{}{}".format(node, - "_render_element")) - if re_node: - re_node.destroy() - self._remove_instance_from_context(instance) def get_pre_create_attr_defs(self): diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index f0430a2892..74e53eed15 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin to create VRay ROP.""" +import hou + from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance from openpype.lib import EnumDef, BoolDef @@ -17,7 +19,6 @@ class CreateVrayROP(plugin.HoudiniCreator): ext = "exr" def create(self, subset_name, instance_data, pre_create_data): - import hou # instance_data.pop("active", None) instance_data.update({"node_type": "vray_renderer"}) @@ -98,6 +99,21 @@ class CreateVrayROP(plugin.HoudiniCreator): to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) + def remove_instances(self, instances): + for instance in instances: + node = instance.data.get("instance_node") + # for the extra render node from the plugins + # such as vray and redshift + ipr_node = hou.node("{}{}".format(node, "_IPR")) + if ipr_node: + ipr_node.destroy() + re_node = hou.node("{}{}".format(node, + "_render_element")) + if re_node: + re_node.destroy() + + return super(CreateVrayROP, self).remove_instances(instances) + def get_pre_create_attr_defs(self): attrs = super(CreateVrayROP, self).get_pre_create_attr_defs() image_format_enum = [ From 99b8cbd9ed3112be46da2413545b4a506abf371e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 13 Apr 2023 22:52:58 +0800 Subject: [PATCH 31/66] wip vray collector and update vray creator --- .../houdini/plugins/create/create_vray_rop.py | 25 +-- .../plugins/publish/collect_vray_rop.py | 147 ++++++++++++++++++ 2 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_vray_rop.py diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 74e53eed15..cd80ad2d93 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -47,16 +47,8 @@ class CreateVrayROP(plugin.HoudiniCreator): ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1)) ipr_rop.parm("rop").set(instance_node.path()) - ext = pre_create_data.get("image_format") - - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.{}".format(subset_name, ext) - ) - parms = { "trange": 1, - "SettingsOutput_img_file_path": filepath, "SettingsEXR_bits_per_channel": "16" # half precision } @@ -71,8 +63,16 @@ class CreateVrayROP(plugin.HoudiniCreator): }) # Enable render element + ext = pre_create_data.get("image_format") has_re = pre_create_data.get("render_element_enabled") if has_re: + # Vray has its own tag for AOV file output + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.${}.$F4.{}".format(subset_name, + "AOV", + ext) + ) re_rop = instance_node.parent().createNode( "vray_render_channels", node_name=basename + "_render_element" @@ -84,9 +84,15 @@ class CreateVrayROP(plugin.HoudiniCreator): "use_render_channels": 1, "render_network_render_channels": re_path }) + else: + filepath = "{}{}".format( + hou.text.expandString("$HIP/pyblish/"), + "{}.$F4.{}".format(subset_name, ext) + ) parms.update({ - "use_render_channels": 0 + "use_render_channels": 0, + "SettingsOutput_img_file_path": filepath }) custom_res = pre_create_data.get("override_resolution") @@ -137,4 +143,5 @@ class CreateVrayROP(plugin.HoudiniCreator): "if enabled", default=False) ] + # ${HIP}/render/${HIPNAME}.${AOV}.$F4.exr diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py new file mode 100644 index 0000000000..90a6f10585 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -0,0 +1,147 @@ +import re +import os + +import hou +import pyblish.api + + +def get_top_referenced_parm(parm): + + processed = set() # disallow infinite loop + while True: + if parm.path() in processed: + raise RuntimeError("Parameter references result in cycle.") + + processed.add(parm.path()) + + ref = parm.getReferencedParm() + if ref.path() == parm.path(): + # It returns itself when it doesn't reference + # another parameter + return ref + else: + parm = ref + + +def evalParmNoFrame(node, parm, pad_character="#"): + + parameter = node.parm(parm) + assert parameter, "Parameter does not exist: %s.%s" % (node, parm) + + # If the parameter has a parameter reference, then get that + # parameter instead as otherwise `unexpandedString()` fails. + parameter = get_top_referenced_parm(parameter) + + # Substitute out the frame numbering with padded characters + try: + raw = parameter.unexpandedString() + except hou.Error as exc: + print("Failed: %s" % parameter) + raise RuntimeError(exc) + + def replace(match): + padding = 1 + n = match.group(2) + if n and int(n): + padding = int(n) + return pad_character * padding + + expression = re.sub(r"(\$F([0-9]*))", replace, raw) + + with hou.ScriptEvalContext(parameter): + return hou.expandStringAtFrame(expression, 0) + + +class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): + """Collect Vray Render Products + + Collects the instance.data["files"] for the render products. + + Provides: + instance -> files + + """ + + label = "VRay ROP Render Products" + order = pyblish.api.CollectorOrder + 0.4 + hosts = ["houdini"] + families = ["vray_rop"] + + def process(self, instance): + + rop = hou.node(instance.data.get("instance_node")) + + # Collect chunkSize + chunk_size_parm = rop.parm("chunkSize") + if chunk_size_parm: + chunk_size = int(chunk_size_parm.eval()) + instance.data["chunkSize"] = chunk_size + self.log.debug("Chunk Size: %s" % chunk_size) + + beauty_product = evalParmNoFrame(rop, "SettingsOutput_img_file_path") + render_products = [] + # Default beauty AOV + render_products.append(beauty_product) + files_by_aov = { + "RGB Color": self.generate_expected_files(instance, + beauty_product) + } + # TODO: add render elements if render element + + for product in render_products: + self.log.debug("Found render product: %s" % product) + filenames = list(render_products) + instance.data["files"] = filenames + self.log.debug("files:{}".format(render_products)) + + # For now by default do NOT try to publish the rendered output + instance.data["publishJobState"] = "Suspended" + instance.data["attachTo"] = [] # stub required data + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["expectedFiles"].append(files_by_aov) + self.log.debug("expectedFiles:{}".format(files_by_aov)) + + def get_render_element_name(self, prefix, suffix="AOV"): + """Return the output filename using the AOV prefix and suffix + """ + # need a rewrite + basename = os.path.basename(prefix) + filename, ext = os.path.splitext(basename) + aov_parm = "${%s}" % suffix + # prefix = ${HIP}/render/${HIPNAME}.${AOV}.$F4.exr + prefix = prefix.replace(filename.split("."), + filename.split(".") + + aov_parm) + # find the render element names + """ + children = hou.node("renderelement node").children() + for c in children: + print c # all the aov names + add into aov_list except "channelsContainer" + if c only has channelsContainer please dont do anything + """ + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + pparts = file.split("#") + padding = "%0{}d".format(len(pparts) - 1) + file = pparts[0] + padding + pparts[-1] + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files From 3e4c0cb47ea44dc568019a977417086e72168ce7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 14 Apr 2023 20:49:32 +0800 Subject: [PATCH 32/66] add vray collector --- .../houdini/plugins/create/create_vray_rop.py | 5 +- .../plugins/publish/collect_redshift_rop.py | 2 +- .../plugins/publish/collect_vray_rop.py | 56 +++++++++++-------- 3 files changed, 38 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index cd80ad2d93..bb2d025cde 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -64,8 +64,8 @@ class CreateVrayROP(plugin.HoudiniCreator): # Enable render element ext = pre_create_data.get("image_format") - has_re = pre_create_data.get("render_element_enabled") - if has_re: + instance_data["RenderElement"] = pre_create_data.get("render_element_enabled") # noqa + if pre_create_data.get("render_element_enabled", True): # Vray has its own tag for AOV file output filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), @@ -82,6 +82,7 @@ class CreateVrayROP(plugin.HoudiniCreator): re_path = re_rop.path() parms.update({ "use_render_channels": 1, + "SettingsOutput_img_file_path": filepath, "render_network_render_channels": re_path }) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 15df12e075..ac55a01209 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -88,7 +88,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): ) render_products.append(beauty_product) files_by_aov = { - "beauty": self.generate_expected_files(instance, + "_": self.generate_expected_files(instance, beauty_product)} num_aovs = rop.evalParm("RS_aov") diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 90a6f10585..f246de6e53 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -78,15 +78,21 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): instance.data["chunkSize"] = chunk_size self.log.debug("Chunk Size: %s" % chunk_size) - beauty_product = evalParmNoFrame(rop, "SettingsOutput_img_file_path") + default_prefix = evalParmNoFrame(rop, "SettingsOutput_img_file_path") render_products = [] - # Default beauty AOV + # TODO: add render elements if render element + + beauty_product = self.get_beauty_render_product(default_prefix) render_products.append(beauty_product) files_by_aov = { - "RGB Color": self.generate_expected_files(instance, - beauty_product) - } - # TODO: add render elements if render element + "RGB Color": self.generate_expected_files(instance, + beauty_product) + } + render_element, aov = self.get_render_element_name(rop, default_prefix) + if render_element is not None: + render_products.append(render_element) + files_by_aov[aov] = self.generate_expected_files(instance, + render_element) for product in render_products: self.log.debug("Found render product: %s" % product) @@ -103,25 +109,31 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"].append(files_by_aov) self.log.debug("expectedFiles:{}".format(files_by_aov)) - def get_render_element_name(self, prefix, suffix="AOV"): + def get_beauty_render_product(self, prefix, suffix=""): + """Return the beauty output filename if render element enabled + """ + aov_parm = ".{}".format(suffix) + beauty_product = None + if aov_parm in prefix: + beauty_product = prefix.replace(aov_parm, "") + else: + beauty_product = prefix + + return beauty_product + + def get_render_element_name(self, node, prefix, suffix=""): """Return the output filename using the AOV prefix and suffix """ # need a rewrite - basename = os.path.basename(prefix) - filename, ext = os.path.splitext(basename) - aov_parm = "${%s}" % suffix - # prefix = ${HIP}/render/${HIPNAME}.${AOV}.$F4.exr - prefix = prefix.replace(filename.split("."), - filename.split(".") + - aov_parm) - # find the render element names - """ - children = hou.node("renderelement node").children() - for c in children: - print c # all the aov names - add into aov_list except "channelsContainer" - if c only has channelsContainer please dont do anything - """ + re_path = node.evalParm("render_network_render_channels") + node_children = hou.node(re_path).children() + for element in node_children: + if element != "channelsContainer": + render_product = prefix.replace(suffix, str(element)) + else: + self.log.debug("skipping non render element output..") + continue + return render_product, str(element) def generate_expected_files(self, instance, path): """Create expected files in instance data""" From 1e0670d0860192d529a215477f711be51c437ddb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 17 Apr 2023 17:03:28 +0800 Subject: [PATCH 33/66] fix the vray collector and hound fix --- .../plugins/publish/collect_redshift_rop.py | 2 +- .../plugins/publish/collect_vray_rop.py | 35 ++++++++++--------- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index ac55a01209..353a3756db 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -89,7 +89,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): render_products.append(beauty_product) files_by_aov = { "_": self.generate_expected_files(instance, - beauty_product)} + beauty_product)} num_aovs = rop.evalParm("RS_aov") for index in range(num_aovs): diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index f246de6e53..263abb375e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -85,14 +85,16 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): beauty_product = self.get_beauty_render_product(default_prefix) render_products.append(beauty_product) files_by_aov = { - "RGB Color": self.generate_expected_files(instance, - beauty_product) - } - render_element, aov = self.get_render_element_name(rop, default_prefix) - if render_element is not None: - render_products.append(render_element) - files_by_aov[aov] = self.generate_expected_files(instance, - render_element) + "RGB Color": self.generate_expected_files(instance, + beauty_product)} + + if instance.data.get("RenderElement", True): + render_element = self.get_render_element_name(rop, default_prefix) + if render_element: + for aov, renderpass in render_element.items(): + render_products.append(renderpass) + files_by_aov[aov] = self.generate_expected_files(instance, + renderpass) for product in render_products: self.log.debug("Found render product: %s" % product) @@ -124,16 +126,17 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): def get_render_element_name(self, node, prefix, suffix=""): """Return the output filename using the AOV prefix and suffix """ + render_element_dict = {} # need a rewrite re_path = node.evalParm("render_network_render_channels") - node_children = hou.node(re_path).children() - for element in node_children: - if element != "channelsContainer": - render_product = prefix.replace(suffix, str(element)) - else: - self.log.debug("skipping non render element output..") - continue - return render_product, str(element) + if re_path: + node_children = hou.node(re_path).children() + for element in node_children: + if element.shaderName() != "vray:SettingsRenderChannels": + aov = str(element) + render_product = prefix.replace(suffix, aov) + render_element_dict[aov] = render_product + return render_element_dict def generate_expected_files(self, instance, path): """Create expected files in instance data""" From 664637219d9e673b3d1823cce0ee6a04502ef99a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 17 Apr 2023 17:04:48 +0800 Subject: [PATCH 34/66] hound fix --- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 263abb375e..6ec9e0b37e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -93,8 +93,7 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): if render_element: for aov, renderpass in render_element.items(): render_products.append(renderpass) - files_by_aov[aov] = self.generate_expected_files(instance, - renderpass) + files_by_aov[aov] = self.generate_expected_files(instance, renderpass) # noqa for product in render_products: self.log.debug("Found render product: %s" % product) From 3cf5667787141512dfaf27896666f86968b54c51 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Apr 2023 21:34:01 +0800 Subject: [PATCH 35/66] adding vray_rop families into houdini deadline submission and submit publish job --- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 3 ++- .../modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 2bddb28792..6a62ee0ea8 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -41,7 +41,8 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): "redshift_rop", "arnold_rop", "mantra_rop", - "karma_rop"] + "karma_rop", + "vray_rop"] targets = ["local"] use_published = True diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 58c9ffc9a0..c382a7e7ec 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -125,7 +125,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "renderlayer", "imagesequence", "vrayscene", "maxrender", "arnold_rop", "mantra_rop", - "karma_rop"] + "karma_rop", "vray_rop"] aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE From 8e3cc04cdbc14de43cc9c7a5153791df1f7156ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Apr 2023 21:56:07 +0800 Subject: [PATCH 36/66] add farm instance to the render creators --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 2 ++ openpype/hosts/houdini/plugins/create/create_karma_rop.py | 2 ++ openpype/hosts/houdini/plugins/create/create_mantra_rop.py | 2 ++ openpype/hosts/houdini/plugins/create/create_redshift_rop.py | 2 ++ openpype/hosts/houdini/plugins/create/create_vray_rop.py | 2 ++ 5 files changed, 10 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 0657d349f9..382279e812 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -23,6 +23,8 @@ class CreateArnoldRop(plugin.HoudiniCreator): # Add chunk size attribute instance_data["chunkSize"] = 1 + # Submit for job publishing + instance_data["farm"] = True instance = super(CreateArnoldRop, self).create( subset_name, diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index 8d55298926..4326a98af4 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -20,6 +20,8 @@ class CreateKarmaROP(plugin.HoudiniCreator): instance_data.update({"node_type": "karma"}) # Add chunk size attribute instance_data["chunkSize"] = 10 + # Submit for job publishing + instance_data["farm"] = True instance = super(CreateKarmaROP, self).create( subset_name, diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 2632f8d6c0..7ccb554be0 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -20,6 +20,8 @@ class CreateMantraROP(plugin.HoudiniCreator): instance_data.update({"node_type": "ifd"}) # Add chunk size attribute instance_data["chunkSize"] = 10 + # Submit for job publishing + instance_data["farm"] = True instance = super(CreateMantraROP, self).create( subset_name, diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 2cbe9bfda1..1fb9ab2f67 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -19,6 +19,8 @@ class CreateRedshiftROP(plugin.HoudiniCreator): instance_data.update({"node_type": "Redshift_ROP"}) # Add chunk size attribute instance_data["chunkSize"] = 10 + # Submit for job publishing + instance_data["farm"] = True # Clear the family prefix from the subset subset = subset_name diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index bb2d025cde..40981da430 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -24,6 +24,8 @@ class CreateVrayROP(plugin.HoudiniCreator): instance_data.update({"node_type": "vray_renderer"}) # Add chunk size attribute instance_data["chunkSize"] = 10 + # Submit for job publishing + instance_data["farm"] = True instance = super(CreateVrayROP, self).create( subset_name, From e3b5aa0f3fd48ae5b9cf3753d636593b069de809 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 24 Apr 2023 16:22:19 +0800 Subject: [PATCH 37/66] clarify the directory for each renderer's rop --- .../houdini/plugins/create/create_arnold_rop.py | 4 ++-- .../houdini/plugins/create/create_karma_rop.py | 8 ++++---- .../houdini/plugins/create/create_mantra_rop.py | 4 ++-- .../hosts/houdini/plugins/create/create_vray_rop.py | 13 +++++++------ .../houdini/plugins/publish/collect_mantra_rop.py | 4 +++- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 382279e812..2ae6727ce4 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -42,8 +42,8 @@ class CreateArnoldRop(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.{}".format(subset_name, ext) + hou.text.expandString("$HIP/pyblish/renders/"), + "{}/{}.$F4.{}".format(subset_name, subset_name, ext) ) parms = { # Render frame range diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index 4326a98af4..e2fe7f40be 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -33,8 +33,8 @@ class CreateKarmaROP(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.{}".format(subset_name, ext) + hou.text.expandString("$HIP/pyblish/render/"), + "{}/{}.$F4.{}".format(subset_name, subset_name, ext) ) checkpoint = "{}{}".format( hou.text.expandString("$HIP/pyblish/"), @@ -42,8 +42,8 @@ class CreateKarmaROP(plugin.HoudiniCreator): ) usd_directory = "{}{}".format( - hou.text.expandString("$HIP/pyblish/usd_renders/"), - "{}_$RENDERID".format(subset_name) + hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), + "{}_$RENDERID".format(subset_name, subset_name, ext) ) parms = { diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 7ccb554be0..83332ec775 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -33,8 +33,8 @@ class CreateMantraROP(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.{}".format(subset_name, ext) + hou.text.expandString("$HIP/pyblish/render/"), + "{}/{}.$F4.{}".format(subset_name, subset_name, ext) ) parms = { diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 40981da430..e4875d5b0d 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -70,10 +70,11 @@ class CreateVrayROP(plugin.HoudiniCreator): if pre_create_data.get("render_element_enabled", True): # Vray has its own tag for AOV file output filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.${}.$F4.{}".format(subset_name, - "AOV", - ext) + hou.text.expandString("$HIP/pyblish/renders/"), + "{}/{}.${}.$F4.{}".format(subset_name, + subset_name, + "AOV", + ext) ) re_rop = instance_node.parent().createNode( "vray_render_channels", @@ -90,8 +91,8 @@ class CreateVrayROP(plugin.HoudiniCreator): else: filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.{}".format(subset_name, ext) + hou.text.expandString("$HIP/pyblish/renders/"), + "{}/{}.$F4.{}".format(subset_name, subset_name, ext) ) parms.update({ "use_render_channels": 0, diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 1eb850e52e..8f06eb12cf 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -99,8 +99,10 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): var = rop.evalParm("vm_variable_plane%d" % i) if var: aov_name = "vm_filename_plane%d" % i + aov_boolean = "vm_usefile_plane%d" % i + aov_enabled = rop.evalParm(aov_boolean) has_aov_path = rop.evalParm(aov_name) - if has_aov_path: + if has_aov_path and aov_enabled == 1: aov_prefix = evalParmNoFrame(rop, aov_name) aov_product = self.get_render_product_name( prefix=aov_prefix, suffix=None From 4274300874dc2a3bdf6d91c79ccebd480a0ddd7e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 20:55:40 +0800 Subject: [PATCH 38/66] oscar's comments --- .../plugins/create/create_arnold_rop.py | 7 +++--- .../plugins/create/create_karma_rop.py | 25 +++++++++---------- .../plugins/create/create_mantra_rop.py | 7 +++--- .../houdini/plugins/create/create_vray_rop.py | 13 +++++++--- 4 files changed, 30 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 2ae6727ce4..9634bf1bd9 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -41,9 +41,10 @@ class CreateArnoldRop(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/renders/"), - "{}/{}.$F4.{}".format(subset_name, subset_name, ext) + filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + ext=ext, ) parms = { # Render frame range diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index e2fe7f40be..891a6c9794 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -32,18 +32,19 @@ class CreateKarmaROP(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/render/"), - "{}/{}.$F4.{}".format(subset_name, subset_name, ext) + filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + ext=ext, ) - checkpoint = "{}{}".format( - hou.text.expandString("$HIP/pyblish/"), - "{}.$F4.checkpoint".format(subset_name) + checkpoint = "{cp_dir}{subset_name}.$F4.checkpoint".format( + cp_dir= hou.text.expandString("$HIP/pyblish/"), + subset_name= subset_name ) - usd_directory = "{}{}".format( - hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), - "{}_$RENDERID".format(subset_name, subset_name, ext) + usd_directory = "{usd_dir}{subset_name}_$RENDERID".format( + usd_dir = hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), + subset_name=subset_name ) parms = { @@ -66,12 +67,10 @@ class CreateKarmaROP(plugin.HoudiniCreator): camera = None for node in self.selected_nodes: if node.type().name() == "cam": - camera = node.path() - camera_node = hou.node(camera) has_camera = pre_create_data.get("cam_res") if has_camera: - res_x = camera_node.evalParm("resx") - res_y = camera_node.evalParm("resy") + res_x = node.evalParm("resx") + res_y = node.evalParm("resy") if not camera: self.log.warning("No render camera found in selection") diff --git a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py index 83332ec775..5ca53e96de 100644 --- a/openpype/hosts/houdini/plugins/create/create_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_mantra_rop.py @@ -32,9 +32,10 @@ class CreateMantraROP(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/render/"), - "{}/{}.$F4.{}".format(subset_name, subset_name, ext) + filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + ext=ext, ) parms = { diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index e4875d5b0d..d9cbea2e53 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -69,6 +69,12 @@ class CreateVrayROP(plugin.HoudiniCreator): instance_data["RenderElement"] = pre_create_data.get("render_element_enabled") # noqa if pre_create_data.get("render_element_enabled", True): # Vray has its own tag for AOV file output + filepath = "{renders_dir}{subset_name}/{subset_name}.${aov}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + aov = "AOV", + ext=ext, + ) filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/renders/"), "{}/{}.${}.$F4.{}".format(subset_name, @@ -90,9 +96,10 @@ class CreateVrayROP(plugin.HoudiniCreator): }) else: - filepath = "{}{}".format( - hou.text.expandString("$HIP/pyblish/renders/"), - "{}/{}.$F4.{}".format(subset_name, subset_name, ext) + filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + ext=ext, ) parms.update({ "use_render_channels": 0, From 685d5285922eb82540b20442b76e33b18b6983bf Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:04:43 +0800 Subject: [PATCH 39/66] hound fix --- .../hosts/houdini/plugins/create/create_karma_rop.py | 7 ++++--- .../hosts/houdini/plugins/create/create_vray_rop.py | 10 +++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index 891a6c9794..acf6d25b3e 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -38,12 +38,13 @@ class CreateKarmaROP(plugin.HoudiniCreator): ext=ext, ) checkpoint = "{cp_dir}{subset_name}.$F4.checkpoint".format( - cp_dir= hou.text.expandString("$HIP/pyblish/"), - subset_name= subset_name + cp_dir=hou.text.expandString("$HIP/pyblish/"), + subset_name=subset_name ) usd_directory = "{usd_dir}{subset_name}_$RENDERID".format( - usd_dir = hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), + usd_dir=hou.text.expandString( + "$HIP/pyblish/renders/usd_renders/"), subset_name=subset_name ) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index d9cbea2e53..dcad2ca6b2 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -69,11 +69,11 @@ class CreateVrayROP(plugin.HoudiniCreator): instance_data["RenderElement"] = pre_create_data.get("render_element_enabled") # noqa if pre_create_data.get("render_element_enabled", True): # Vray has its own tag for AOV file output - filepath = "{renders_dir}{subset_name}/{subset_name}.${aov}.$F4.{ext}".format( + filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, - aov = "AOV", - ext=ext, + fmt="${aov}.$F4.{ext}".format(aov = "AOV", + ext=ext,) ) filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/renders/"), @@ -96,10 +96,10 @@ class CreateVrayROP(plugin.HoudiniCreator): }) else: - filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( + filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, - ext=ext, + fmt="$F4.{ext}".format(ext=ext) ) parms.update({ "use_render_channels": 0, From a376db5e5845a5b4384c40cc7b608176afef20b5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:08:05 +0800 Subject: [PATCH 40/66] hound fix --- openpype/hosts/houdini/plugins/create/create_karma_rop.py | 3 +-- openpype/hosts/houdini/plugins/create/create_vray_rop.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_karma_rop.py b/openpype/hosts/houdini/plugins/create/create_karma_rop.py index acf6d25b3e..edfb992e1a 100644 --- a/openpype/hosts/houdini/plugins/create/create_karma_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_karma_rop.py @@ -43,8 +43,7 @@ class CreateKarmaROP(plugin.HoudiniCreator): ) usd_directory = "{usd_dir}{subset_name}_$RENDERID".format( - usd_dir=hou.text.expandString( - "$HIP/pyblish/renders/usd_renders/"), + usd_dir=hou.text.expandString("$HIP/pyblish/renders/usd_renders/"), # noqa subset_name=subset_name ) diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index dcad2ca6b2..0a74d93c99 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -72,8 +72,8 @@ class CreateVrayROP(plugin.HoudiniCreator): filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, - fmt="${aov}.$F4.{ext}".format(aov = "AOV", - ext=ext,) + fmt="${aov}.$F4.{ext}".format(aov="AOV", + ext=ext) ) filepath = "{}{}".format( hou.text.expandString("$HIP/pyblish/renders/"), From 6ec63c9e6013a0b39f93f370fded8aaaf2be2341 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:10:25 +0800 Subject: [PATCH 41/66] add to get ocio color management preference before rendering --- openpype/hosts/houdini/api/lib.py | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 2e58f3dd98..df5c95578b 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import sys import os +import re import uuid import logging from contextlib import contextmanager @@ -581,3 +582,73 @@ def splitext(name, allowed_multidot_extensions): return name[:-len(ext)], ext return os.path.splitext(name) + +def get_top_referenced_parm(parm): + + processed = set() # disallow infinite loop + while True: + if parm.path() in processed: + raise RuntimeError("Parameter references result in cycle.") + + processed.add(parm.path()) + + ref = parm.getReferencedParm() + if ref.path() == parm.path(): + # It returns itself when it doesn't reference + # another parameter + return ref + else: + parm = ref + + +def evalParmNoFrame(node, parm, pad_character="#"): + + parameter = node.parm(parm) + assert parameter, "Parameter does not exist: %s.%s" % (node, parm) + + # If the parameter has a parameter reference, then get that + # parameter instead as otherwise `unexpandedString()` fails. + parameter = get_top_referenced_parm(parameter) + + # Substitute out the frame numbering with padded characters + try: + raw = parameter.unexpandedString() + except hou.Error as exc: + print("Failed: %s" % parameter) + raise RuntimeError(exc) + + def replace(match): + padding = 1 + n = match.group(2) + if n and int(n): + padding = int(n) + return pad_character * padding + + expression = re.sub(r"(\$F([0-9]*))", replace, raw) + + with hou.ScriptEvalContext(parameter): + return hou.expandStringAtFrame(expression, 0) + + +def get_color_management_preferences(): + """Get default OCIO preferences""" + data = { + "config": hou.Color.ocio_configPath() + + } + + # Get default display and view from OCIO + display = hou.Color.ocio_defaultDisplay() + disp_regex = re.compile(r"^(?P.+-)(?P.+)$") + disp_match = disp_regex.match(display) + + view = hou.Color.ocio_defaultView() + view_regex = re.compile(r"^(?P.+- )(?P.+)$") + view_match = view_regex.match(view) + data.update({ + "display": disp_match.group("display"), + "view": view_match.group("view") + + }) + + return data From c1d4ddf0d6b509e5cc8b2210b702d1eb8f067344 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:11:44 +0800 Subject: [PATCH 42/66] hound fix --- openpype/hosts/houdini/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index df5c95578b..a33ba7aad2 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -583,6 +583,7 @@ def splitext(name, allowed_multidot_extensions): return os.path.splitext(name) + def get_top_referenced_parm(parm): processed = set() # disallow infinite loop @@ -633,7 +634,7 @@ def evalParmNoFrame(node, parm, pad_character="#"): def get_color_management_preferences(): """Get default OCIO preferences""" data = { - "config": hou.Color.ocio_configPath() + "config": hou.Color.ocio_configPath() } From cc4a91b01a26ea3836ab101c62d0907cea56ce27 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:22:38 +0800 Subject: [PATCH 43/66] add colorspace parameters for publish job submission --- openpype/hosts/houdini/api/colorspace.py | 55 ++++++++++++++ .../plugins/publish/collect_arnold_rop.py | 67 +++++------------ .../plugins/publish/collect_karma_rop.py | 70 +++++------------ .../plugins/publish/collect_mantra_rop.py | 75 ++++++------------- .../plugins/publish/collect_redshift_rop.py | 72 ++++++------------ .../plugins/publish/collect_vray_rop.py | 72 +++++------------- 6 files changed, 155 insertions(+), 256 deletions(-) create mode 100644 openpype/hosts/houdini/api/colorspace.py diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py new file mode 100644 index 0000000000..b615bc199c --- /dev/null +++ b/openpype/hosts/houdini/api/colorspace.py @@ -0,0 +1,55 @@ +import attr +import hou +from openpype.hosts.houdini.api.lib import get_color_management_preferences + + +@attr.s +class LayerMetadata(object): + """Data class for Render Layer metadata.""" + frameStart = attr.ib() + frameEnd = attr.ib() + + +@attr.s +class RenderProduct(object): + """Getting Colorspace as + Specific Render Product Parameter for submitting + publish job. + + """ + colorspace = attr.ib() # colorspace + view = attr.ib() + productName = attr.ib(default=None) + +class ARenderProduct(object): + + def __init__(self): + """Constructor.""" + # Initialize + self.layer_data = self._get_layer_data() + self.layer_data.products = self.get_colorspace_data() + + def _get_layer_data(self): + return LayerMetadata( + frameStart=int(hou.playbar.frameRange()[0]), + frameEnd=int(hou.playbar.frameRange()[1]), + ) + + def get_colorspace_data(self): + """To be implemented by renderer class. + + This should return a list of RenderProducts. + + Returns: + list: List of RenderProduct + + """ + data = get_color_management_preferences() + colorspace_data = [ + RenderProduct( + colorspace=data["display"], + view=data["view"], + productName="" + ) + ] + return colorspace_data diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 4d82b74aa2..7ec4ead968 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -4,52 +4,13 @@ import os import hou import pyblish.api - -def get_top_referenced_parm(parm): - - processed = set() # disallow infinite loop - while True: - if parm.path() in processed: - raise RuntimeError("Parameter references result in cycle.") - - processed.add(parm.path()) - - ref = parm.getReferencedParm() - if ref.path() == parm.path(): - # It returns itself when it doesn't reference - # another parameter - return ref - else: - parm = ref - - -def evalParmNoFrame(node, parm, pad_character="#"): - - parameter = node.parm(parm) - assert parameter, "Parameter does not exist: %s.%s" % (node, parm) - - # If the parameter has a parameter reference, then get that - # parameter instead as otherwise `unexpandedString()` fails. - parameter = get_top_referenced_parm(parameter) - - # Substitute out the frame numbering with padded characters - try: - raw = parameter.unexpandedString() - except hou.Error as exc: - print("Failed: %s" % parameter) - raise RuntimeError(exc) - - def replace(match): - padding = 1 - n = match.group(2) - if n and int(n): - padding = int(n) - return pad_character * padding - - expression = re.sub(r"(\$F([0-9]*))", replace, raw) - - with hou.ScriptEvalContext(parameter): - return hou.expandStringAtFrame(expression, 0) +from openpype.hosts.houdini.api.lib import ( + evalParmNoFrame, + get_color_management_preferences +) +from openpype.hosts.houdini.api import( + colorspace +) class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): @@ -114,6 +75,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): filenames = list(render_products) instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -123,6 +85,12 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) + # update the colorspace data + colorspace_data = get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" @@ -157,9 +125,10 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): file = os.path.basename(path) if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) if "%" not in file: return path diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index 10c97269fc..11747a4100 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -4,52 +4,13 @@ import os import hou import pyblish.api - -def get_top_referenced_parm(parm): - - processed = set() # disallow infinite loop - while True: - if parm.path() in processed: - raise RuntimeError("Parameter references result in cycle.") - - processed.add(parm.path()) - - ref = parm.getReferencedParm() - if ref.path() == parm.path(): - # It returns itself when it doesn't reference - # another parameter - return ref - else: - parm = ref - - -def evalParmNoFrame(node, parm, pad_character="#"): - - parameter = node.parm(parm) - assert parameter, "Parameter does not exist: %s.%s" % (node, parm) - - # If the parameter has a parameter reference, then get that - # parameter instead as otherwise `unexpandedString()` fails. - parameter = get_top_referenced_parm(parameter) - - # Substitute out the frame numbering with padded characters - try: - raw = parameter.unexpandedString() - except hou.Error as exc: - print("Failed: %s" % parameter) - raise RuntimeError(exc) - - def replace(match): - padding = 1 - n = match.group(2) - if n and int(n): - padding = int(n) - return pad_character * padding - - expression = re.sub(r"(\$F([0-9]*))", replace, raw) - - with hou.ScriptEvalContext(parameter): - return hou.expandStringAtFrame(expression, 0) +from openpype.hosts.houdini.api.lib import ( + evalParmNoFrame, + get_color_management_preferences +) +from openpype.hosts.houdini.api import( + colorspace +) class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): @@ -94,6 +55,7 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): filenames = list(render_products) instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() for product in render_products: self.log.debug("Found render product: %s" % product) @@ -102,13 +64,18 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) + # update the colorspace data + colorspace_data = get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + def get_render_product_name(self, prefix, suffix): + product_name = prefix if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) product_name = prefix_base + "." + suffix + ext - else: - product_name = prefix return product_name @@ -119,9 +86,10 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): file = os.path.basename(path) if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) if "%" not in file: return path diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 8f06eb12cf..9c9a15f8f7 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -4,52 +4,13 @@ import os import hou import pyblish.api - -def get_top_referenced_parm(parm): - - processed = set() # disallow infinite loop - while True: - if parm.path() in processed: - raise RuntimeError("Parameter references result in cycle.") - - processed.add(parm.path()) - - ref = parm.getReferencedParm() - if ref.path() == parm.path(): - # It returns itself when it doesn't reference - # another parameter - return ref - else: - parm = ref - - -def evalParmNoFrame(node, parm, pad_character="#"): - - parameter = node.parm(parm) - assert parameter, "Parameter does not exist: %s.%s" % (node, parm) - - # If the parameter has a parameter reference, then get that - # parameter instead as otherwise `unexpandedString()` fails. - parameter = get_top_referenced_parm(parameter) - - # Substitute out the frame numbering with padded characters - try: - raw = parameter.unexpandedString() - except hou.Error as exc: - print("Failed: %s" % parameter) - raise RuntimeError(exc) - - def replace(match): - padding = 1 - n = match.group(2) - if n and int(n): - padding = int(n) - return pad_character * padding - - expression = re.sub(r"(\$F([0-9]*))", replace, raw) - - with hou.ScriptEvalContext(parameter): - return hou.expandStringAtFrame(expression, 0) +from openpype.hosts.houdini.api.lib import ( + evalParmNoFrame, + get_color_management_preferences +) +from openpype.hosts.houdini.api import( + colorspace +) class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): @@ -113,8 +74,10 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): for product in render_products: self.log.debug("Found render product: %s" % product) - filenames = list(render_products) - instance.data["files"] = filenames + + filenames = list(render_products) + instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -124,13 +87,18 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) + # update the colorspace data + colorspace_data = get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + def get_render_product_name(self, prefix, suffix): + product_name = prefix if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) product_name = prefix_base + "." + suffix + ext - else: - product_name = prefix return product_name @@ -141,9 +109,10 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): file = os.path.basename(path) if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) if "%" not in file: return path diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 353a3756db..ab00e3c339 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -4,52 +4,13 @@ import os import hou import pyblish.api - -def get_top_referenced_parm(parm): - - processed = set() # disallow infinite loop - while True: - if parm.path() in processed: - raise RuntimeError("Parameter references result in cycle.") - - processed.add(parm.path()) - - ref = parm.getReferencedParm() - if ref.path() == parm.path(): - # It returns itself when it doesn't reference - # another parameter - return ref - else: - parm = ref - - -def evalParmNoFrame(node, parm, pad_character="#"): - - parameter = node.parm(parm) - assert parameter, "Parameter does not exist: %s.%s" % (node, parm) - - # If the parameter has a parameter reference, then get that - # parameter instead as otherwise `unexpandedString()` fails. - parameter = get_top_referenced_parm(parameter) - - # Substitute out the frame numbering with padded characters - try: - raw = parameter.unexpandedString() - except hou.Error as exc: - print("Failed: %s" % parameter) - raise RuntimeError(exc) - - def replace(match): - padding = 1 - n = match.group(2) - if n and int(n): - padding = int(n) - return pad_character * padding - - expression = re.sub(r"(\$F([0-9]*))", replace, raw) - - with hou.ScriptEvalContext(parameter): - return hou.expandStringAtFrame(expression, 0) +from openpype.hosts.houdini.api.lib import ( + evalParmNoFrame, + get_color_management_preferences +) +from openpype.hosts.houdini.api import( + colorspace +) class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): @@ -112,8 +73,10 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): for product in render_products: self.log.debug("Found render product: %s" % product) - filenames = list(render_products) - instance.data["files"] = filenames + + filenames = list(render_products) + instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -123,6 +86,12 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"] = list() instance.data["expectedFiles"].append(files_by_aov) + # update the colorspace data + colorspace_data = get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + def get_render_product_name(self, prefix, suffix): """Return the output filename using the AOV prefix and suffix""" @@ -154,9 +123,10 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): file = os.path.basename(path) if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) if "%" not in file: return path diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 6ec9e0b37e..aa05cf6736 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -4,52 +4,13 @@ import os import hou import pyblish.api - -def get_top_referenced_parm(parm): - - processed = set() # disallow infinite loop - while True: - if parm.path() in processed: - raise RuntimeError("Parameter references result in cycle.") - - processed.add(parm.path()) - - ref = parm.getReferencedParm() - if ref.path() == parm.path(): - # It returns itself when it doesn't reference - # another parameter - return ref - else: - parm = ref - - -def evalParmNoFrame(node, parm, pad_character="#"): - - parameter = node.parm(parm) - assert parameter, "Parameter does not exist: %s.%s" % (node, parm) - - # If the parameter has a parameter reference, then get that - # parameter instead as otherwise `unexpandedString()` fails. - parameter = get_top_referenced_parm(parameter) - - # Substitute out the frame numbering with padded characters - try: - raw = parameter.unexpandedString() - except hou.Error as exc: - print("Failed: %s" % parameter) - raise RuntimeError(exc) - - def replace(match): - padding = 1 - n = match.group(2) - if n and int(n): - padding = int(n) - return pad_character * padding - - expression = re.sub(r"(\$F([0-9]*))", replace, raw) - - with hou.ScriptEvalContext(parameter): - return hou.expandStringAtFrame(expression, 0) +from openpype.hosts.houdini.api.lib import ( + evalParmNoFrame, + get_color_management_preferences +) +from openpype.hosts.houdini.api import( + colorspace +) class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): @@ -97,9 +58,9 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): for product in render_products: self.log.debug("Found render product: %s" % product) - filenames = list(render_products) - instance.data["files"] = filenames - self.log.debug("files:{}".format(render_products)) + filenames = list(render_products) + instance.data["files"] = filenames + instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output instance.data["publishJobState"] = "Suspended" @@ -110,6 +71,12 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): instance.data["expectedFiles"].append(files_by_aov) self.log.debug("expectedFiles:{}".format(files_by_aov)) + # update the colorspace data + colorspace_data = get_color_management_preferences() + instance.data["colorspaceConfig"] = colorspace_data["config"] + instance.data["colorspaceDisplay"] = colorspace_data["display"] + instance.data["colorspaceView"] = colorspace_data["view"] + def get_beauty_render_product(self, prefix, suffix=""): """Return the beauty output filename if render element enabled """ @@ -144,9 +111,10 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): file = os.path.basename(path) if "#" in file: - pparts = file.split("#") - padding = "%0{}d".format(len(pparts) - 1) - file = pparts[0] + padding + pparts[-1] + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) if "%" not in file: return path From f1aff5bcf2cd5ed167651d8946825b6be4c734a8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 21:24:38 +0800 Subject: [PATCH 44/66] hound fix --- openpype/hosts/houdini/api/colorspace.py | 1 + openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 2 +- openpype/hosts/houdini/plugins/publish/collect_karma_rop.py | 2 +- openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py | 2 +- openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py | 2 +- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/api/colorspace.py b/openpype/hosts/houdini/api/colorspace.py index b615bc199c..7047644225 100644 --- a/openpype/hosts/houdini/api/colorspace.py +++ b/openpype/hosts/houdini/api/colorspace.py @@ -21,6 +21,7 @@ class RenderProduct(object): view = attr.ib() productName = attr.ib(default=None) + class ARenderProduct(object): def __init__(self): diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 7ec4ead968..2fd419ef9b 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -8,7 +8,7 @@ from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) -from openpype.hosts.houdini.api import( +from openpype.hosts.houdini.api import ( colorspace ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index 11747a4100..b87bb06767 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -8,7 +8,7 @@ from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) -from openpype.hosts.houdini.api import( +from openpype.hosts.houdini.api import ( colorspace ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index 9c9a15f8f7..c4460f5350 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -8,7 +8,7 @@ from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) -from openpype.hosts.houdini.api import( +from openpype.hosts.houdini.api import ( colorspace ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index ab00e3c339..dbb15ab88f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -8,7 +8,7 @@ from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) -from openpype.hosts.houdini.api import( +from openpype.hosts.houdini.api import ( colorspace ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index aa05cf6736..d4fe37f993 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -8,7 +8,7 @@ from openpype.hosts.houdini.api.lib import ( evalParmNoFrame, get_color_management_preferences ) -from openpype.hosts.houdini.api import( +from openpype.hosts.houdini.api import ( colorspace ) From 5e9ae9153b9af411ff2102102d09356d87d8208c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 21:45:52 +0800 Subject: [PATCH 45/66] bug fix redshift creators --- .../plugins/create/create_redshift_rop.py | 63 +++++++++++++++---- .../houdini/plugins/create/create_vray_rop.py | 2 - .../plugins/publish/submit_publish_job.py | 3 +- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 1fb9ab2f67..4f9c00b3a3 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- """Creator plugin to create Redshift ROP.""" +import hou # noqa + from openpype.hosts.houdini.api import plugin from openpype.pipeline import CreatedInstance +from openpype.lib import EnumDef class CreateRedshiftROP(plugin.HoudiniCreator): @@ -11,9 +14,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator): family = "redshift_rop" icon = "magic" defaults = ["master"] + ext = "exr" def create(self, subset_name, instance_data, pre_create_data): - import hou # noqa instance_data.pop("active", None) instance_data.update({"node_type": "Redshift_ROP"}) @@ -22,12 +25,6 @@ class CreateRedshiftROP(plugin.HoudiniCreator): # Submit for job publishing instance_data["farm"] = True - # Clear the family prefix from the subset - subset = subset_name - subset_no_prefix = subset[len(self.family):] - subset_no_prefix = subset_no_prefix[0].lower() + subset_no_prefix[1:] - subset_name = subset_no_prefix - instance = super(CreateRedshiftROP, self).create( subset_name, instance_data, @@ -36,11 +33,10 @@ class CreateRedshiftROP(plugin.HoudiniCreator): instance_node = hou.node(instance.get("instance_node")) basename = instance_node.name() - instance_node.setName(basename + "_ROP", unique_name=True) # Also create the linked Redshift IPR Rop try: - ipr_rop = self.parent.createNode( + ipr_rop = instance_node.parent().createNode( "Redshift_IPR", node_name=basename + "_IPR" ) except hou.OperationFailed: @@ -52,19 +48,60 @@ class CreateRedshiftROP(plugin.HoudiniCreator): ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1)) # Set the linked rop to the Redshift ROP - ipr_rop.parm("linked_rop").set(ipr_rop.relativePathTo(instance)) + ipr_rop.parm("linked_rop").set(instance_node.path()) + + ext = pre_create_data.get("image_format") + filepath ="{renders_dir}{subset_name}/{subset_name}.{fmt}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + subset_name=subset_name, + fmt="${aov}.$F4.{ext}".format(aov="AOV", + ext=ext) + ) - prefix = '${HIP}/render/${HIPNAME}/`chs("subset")`.${AOV}.$F4.exr' parms = { # Render frame range "trange": 1, # Redshift ROP settings - "RS_outputFileNamePrefix": prefix, - "RS_outputMultilayerMode": 0, # no multi-layered exr + "RS_outputFileNamePrefix": filepath, + "RS_outputMultilayerMode": "1", # no multi-layered exr "RS_outputBeautyAOVSuffix": "beauty", } + + if self.selected_nodes: + # set up the render camera from the selected node + camera = None + for node in self.selected_nodes: + if node.type().name() == "cam": + camera = node.path() + parms.update({ + "RS_renderCamera": camera or "" + }) instance_node.setParms(parms) # Lock some Avalon attributes to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) + + def remove_instances(self, instances): + for instance in instances: + node = instance.data.get("instance_node") + + ipr_node = hou.node(f"{node}_IPR") + if ipr_node: + ipr_node.destroy() + + return super(CreateRedshiftROP, self).remove_instances(instances) + + def get_pre_create_attr_defs(self): + attrs = super(CreateRedshiftROP, self).get_pre_create_attr_defs() + image_format_enum = [ + "bmp", "cin", "exr", "jpg", "pic", "pic.gz", "png", + "rad", "rat", "rta", "sgi", "tga", "tif", + ] + + return attrs + [ + EnumDef("image_format", + image_format_enum, + default=self.ext, + label="Image Format Options") + ] diff --git a/openpype/hosts/houdini/plugins/create/create_vray_rop.py b/openpype/hosts/houdini/plugins/create/create_vray_rop.py index 0a74d93c99..1de9be4ed6 100644 --- a/openpype/hosts/houdini/plugins/create/create_vray_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_vray_rop.py @@ -154,5 +154,3 @@ class CreateVrayROP(plugin.HoudiniCreator): "if enabled", default=False) ] - -# ${HIP}/render/${HIPNAME}.${AOV}.$F4.exr diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index c1c9d8c062..afe5c59834 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -125,7 +125,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "renderlayer", "imagesequence", "vrayscene", "maxrender", "arnold_rop", "mantra_rop", - "karma_rop", "vray_rop"] + "karma_rop", "vray_rop", + "redshift_rop"] aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE From 708c125433aad8c88b56e86b1a54965d433ebe3d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 21:49:20 +0800 Subject: [PATCH 46/66] hound fix --- .../houdini/plugins/create/create_redshift_rop.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 4f9c00b3a3..4ec88fc468 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -51,11 +51,10 @@ class CreateRedshiftROP(plugin.HoudiniCreator): ipr_rop.parm("linked_rop").set(instance_node.path()) ext = pre_create_data.get("image_format") - filepath ="{renders_dir}{subset_name}/{subset_name}.{fmt}".format( - renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), + filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( + renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, - fmt="${aov}.$F4.{ext}".format(aov="AOV", - ext=ext) + fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) ) parms = { @@ -74,8 +73,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): if node.type().name() == "cam": camera = node.path() parms.update({ - "RS_renderCamera": camera or "" - }) + "RS_renderCamera": camera or ""}) instance_node.setParms(parms) # Lock some Avalon attributes From 34b387a29395e4bc05d911d151236a77c230c387 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 21:51:31 +0800 Subject: [PATCH 47/66] hound fix --- openpype/hosts/houdini/plugins/create/create_redshift_rop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 4ec88fc468..242915ed6d 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -53,8 +53,8 @@ class CreateRedshiftROP(plugin.HoudiniCreator): ext = pre_create_data.get("image_format") filepath = "{renders_dir}{subset_name}/{subset_name}.{fmt}".format( renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), - subset_name=subset_name, - fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) + subset_name=subset_name, + fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) ) parms = { From 8d729995791997e9717e1463c04f9e5305e9ecce Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 21:52:38 +0800 Subject: [PATCH 48/66] hound fix --- openpype/hosts/houdini/plugins/create/create_redshift_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 242915ed6d..e14ff15bf8 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -55,7 +55,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): renders_dir=hou.text.expandString("$HIP/pyblish/renders/"), subset_name=subset_name, fmt="${aov}.$F4.{ext}".format(aov="AOV", ext=ext) - ) + ) parms = { # Render frame range From cccc0acd1dc9c6fd9d00fa56abfaebc1de1d327d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 23 May 2023 22:00:40 +0800 Subject: [PATCH 49/66] add docs --- website/docs/artist_hosts_houdini.md | 24 +++++++++++++----- .../assets/houdini_render_publish_creator.png | Bin 0 -> 82356 bytes 2 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 website/docs/assets/houdini_render_publish_creator.png diff --git a/website/docs/artist_hosts_houdini.md b/website/docs/artist_hosts_houdini.md index 8874a0b5cf..0471765365 100644 --- a/website/docs/artist_hosts_houdini.md +++ b/website/docs/artist_hosts_houdini.md @@ -14,7 +14,7 @@ sidebar_label: Houdini - [Library Loader](artist_tools_library-loader) ## Publishing Alembic Cameras -You can publish baked camera in Alembic format. +You can publish baked camera in Alembic format. Select your camera and go **OpenPype -> Create** and select **Camera (abc)**. This will create Alembic ROP in **out** with path and frame range already set. This node will have a name you've @@ -30,7 +30,7 @@ You can use any COP node and publish the image sequence generated from it. For e ![Noise COP](assets/houdini_imagesequence_cop.png) To publish the output of the `radialblur1` go to **OpenPype -> Create** and -select **Composite (Image Sequence)**. If you name the variant *Noise* this will create the `/out/imagesequenceNoise` Composite ROP with the frame range set. +select **Composite (Image Sequence)**. If you name the variant *Noise* this will create the `/out/imagesequenceNoise` Composite ROP with the frame range set. When you hit **Publish** it will render image sequence from selected node. @@ -56,14 +56,14 @@ Now select the `output0` node and go **OpenPype -> Create** and select **Point C Alembic ROP `/out/pointcacheStrange` ## Publishing Reviews (OpenGL) -To generate a review output from Houdini you need to create a **review** instance. +To generate a review output from Houdini you need to create a **review** instance. Go to **OpenPype -> Create** and select **Review**. ![Houdini Create Review](assets/houdini_review_create_attrs.png) -On create, with the **Use Selection** checkbox enabled it will set up the first -camera found in your selection as the camera for the OpenGL ROP node and other -non-cameras are set in **Force Objects**. It will then render those even if +On create, with the **Use Selection** checkbox enabled it will set up the first +camera found in your selection as the camera for the OpenGL ROP node and other +non-cameras are set in **Force Objects**. It will then render those even if their display flag is disabled in your scene. ## Redshift @@ -71,6 +71,18 @@ their display flag is disabled in your scene. This part of documentation is still work in progress. ::: +## Publishing Render to Deadline +Five Renderers(Arnold, Redshift, Mantra, Karma, VRay) are supported for Render Publishing. +They are named with the suffix("_ROP") +To submit render to deadline, you need to create a **Render** instance. +Go to **Openpype -> Create** and select **Publish**. Before clicking **Create** button, +you need select your preferred image rendering format. You can also enable the **Use selection** to +select your render camera. +![Houdini Create Render](assets/houdini_render_publish_creator.png) + +All the render outputs are stored in the pyblish/render directory within your project path.\ +For Karma-specific render, it also outputs the USD render as default. + ## USD (experimental support) ### Publishing USD You can publish your Solaris Stage as USD file. diff --git a/website/docs/assets/houdini_render_publish_creator.png b/website/docs/assets/houdini_render_publish_creator.png new file mode 100644 index 0000000000000000000000000000000000000000..5dd73d296a416a49291c2bb8729541780178ac60 GIT binary patch literal 82356 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 Date: Wed, 24 May 2023 17:43:35 +0200 Subject: [PATCH 50/66] Collect frame range for all rop nodes --- .../houdini/plugins/publish/collect_frames.py | 4 +- .../plugins/publish/collect_instances.py | 6 --- .../publish/collect_rop_frame_range.py | 41 +++++++++++++++++++ 3 files changed, 42 insertions(+), 9 deletions(-) create mode 100644 openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py diff --git a/openpype/hosts/houdini/plugins/publish/collect_frames.py b/openpype/hosts/houdini/plugins/publish/collect_frames.py index 059793e3c5..91a3d9d170 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_frames.py +++ b/openpype/hosts/houdini/plugins/publish/collect_frames.py @@ -11,15 +11,13 @@ from openpype.hosts.houdini.api import lib class CollectFrames(pyblish.api.InstancePlugin): """Collect all frames which would be saved from the ROP nodes""" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder + 0.01 label = "Collect Frames" families = ["vdbcache", "imagesequence", "ass", "redshiftproxy", "review"] def process(self, instance): ropnode = hou.node(instance.data["instance_node"]) - frame_data = lib.get_frame_data(ropnode) - instance.data.update(frame_data) start_frame = instance.data.get("frameStart", None) end_frame = instance.data.get("frameEnd", None) diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py index 5d5347f96e..5fa3e9655e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_instances.py +++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py @@ -70,16 +70,10 @@ class CollectInstances(pyblish.api.ContextPlugin): if "active" in data: data["publish"] = data["active"] - data.update(self.get_frame_data(node)) - # Create nice name if the instance has a frame range. label = data.get("name", node.name()) label += " (%s)" % data["asset"] # include asset in name - if "frameStart" in data and "frameEnd" in data: - frames = "[{frameStart} - {frameEnd}]".format(**data) - label = "{} {}".format(label, frames) - instance = context.create_instance(label) # Include `families` using `family` data diff --git a/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py new file mode 100644 index 0000000000..2a6be6b9f1 --- /dev/null +++ b/openpype/hosts/houdini/plugins/publish/collect_rop_frame_range.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +"""Collector plugin for frames data on ROP instances.""" +import hou # noqa +import pyblish.api +from openpype.hosts.houdini.api import lib + + +class CollectRopFrameRange(pyblish.api.InstancePlugin): + """Collect all frames which would be saved from the ROP nodes""" + + order = pyblish.api.CollectorOrder + label = "Collect RopNode Frame Range" + + def process(self, instance): + + node_path = instance.data.get("instance_node") + if node_path is None: + # Instance without instance node like a workfile instance + return + + ropnode = hou.node(node_path) + frame_data = lib.get_frame_data(ropnode) + + if "frameStart" in frame_data and "frameEnd" in frame_data: + + # Log artist friendly message about the collected frame range + message = ( + "Frame range {0[frameStart]} - {0[frameEnd]}" + ).format(frame_data) + if frame_data.get("step", 1.0) != 1.0: + message += " with step {0[step]}".format(frame_data) + self.log.info(message) + + instance.data.update(frame_data) + + # Add frame range to label if the instance has a frame range. + label = instance.data.get("label", instance.data["name"]) + instance.data["label"] = ( + "{0} [{1[frameStart]} - {1[frameEnd]}]".format(label, + frame_data) + ) From 47e5d3646046041e1cf55e55e51e0a5818360add Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 1 Jun 2023 17:01:25 +0200 Subject: [PATCH 51/66] :recycle: incorporating comments --- .../houdini/plugins/publish/collect_arnold_rop.py | 15 +++++---------- .../houdini/plugins/publish/collect_karma_rop.py | 2 +- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 2fd419ef9b..946eed3301 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -1,16 +1,12 @@ -import re import os +import re import hou import pyblish.api +from openpype.hosts.houdini.api import colorspace from openpype.hosts.houdini.api.lib import ( - evalParmNoFrame, - get_color_management_preferences -) -from openpype.hosts.houdini.api import ( - colorspace -) + evalParmNoFrame, get_color_management_preferences) class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): @@ -52,7 +48,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): } num_aovs = rop.evalParm("ar_aovs") - for index in range(num_aovs): + for index in range(1, num_aovs + 1): i = index + 1 # Skip disabled AOVs @@ -97,8 +93,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): # When AOV is explicitly defined in prefix we just swap it out # directly with the AOV suffix to embed it. # Note: ${AOV} seems to be evaluated in the parameter as %AOV% - has_aov_in_prefix = "%AOV%" in prefix - if has_aov_in_prefix: + if "%AOV%" in prefix: # It seems that when some special separator characters are present # before the %AOV% token that Redshift will secretly remove it if # there is no suffix for the current product, for example: diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index b87bb06767..a41e19d93f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -75,7 +75,7 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) - product_name = prefix_base + "." + suffix + ext + product_name = "{}.{}{}".format(prefix_base, suffix,ext) return product_name From 7b454a92ceaa70ccab5990663055ae63ada38940 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 1 Jun 2023 17:24:56 +0200 Subject: [PATCH 52/66] :bug: show arnold render settings --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 9634bf1bd9..3bb736995e 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -33,11 +33,6 @@ class CreateArnoldRop(plugin.HoudiniCreator): instance_node = hou.node(instance.get("instance_node")) - # Hide Properties Tab on Arnold ROP since that's used - # for rendering instead of .ass Archive Export - parm_template_group = instance_node.parmTemplateGroup() - parm_template_group.hideFolder("Properties", True) - instance_node.setParmTemplateGroup(parm_template_group) ext = pre_create_data.get("image_format") From 1818d061df127549bc18a70ba52692bd04983897 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 1 Jun 2023 17:52:03 +0200 Subject: [PATCH 53/66] :rotating_light: fix hound --- openpype/hosts/houdini/plugins/create/create_arnold_rop.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py index 3bb736995e..bddf26dbd5 100644 --- a/openpype/hosts/houdini/plugins/create/create_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_arnold_rop.py @@ -33,7 +33,6 @@ class CreateArnoldRop(plugin.HoudiniCreator): instance_node = hou.node(instance.get("instance_node")) - ext = pre_create_data.get("image_format") filepath = "{renders_dir}{subset_name}/{subset_name}.$F4.{ext}".format( From 15ce81e267903862ba9ad3cc440ef7cc72da889b Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 1 Jun 2023 17:56:56 +0200 Subject: [PATCH 54/66] :rotating_light: fix linter errors --- openpype/hosts/houdini/plugins/publish/collect_karma_rop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py index a41e19d93f..eabb1128d8 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_karma_rop.py @@ -75,7 +75,7 @@ class CollectKarmaROPRenderProducts(pyblish.api.InstancePlugin): if suffix: # Add ".{suffix}" before the extension prefix_base, ext = os.path.splitext(prefix) - product_name = "{}.{}{}".format(prefix_base, suffix,ext) + product_name = "{}.{}{}".format(prefix_base, suffix, ext) return product_name From 4eaeb5682c643b9bc9842f042875b16bd069c84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Fri, 2 Jun 2023 00:17:37 +0200 Subject: [PATCH 55/66] Remove default windowFlags as it makes the publisher window not show minimize/maximize hints --- openpype/tools/publisher/window.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 6ab444109e..006098cb37 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -66,8 +66,7 @@ class PublisherWindow(QtWidgets.QDialog): on_top_flag = QtCore.Qt.Dialog self.setWindowFlags( - self.windowFlags() - | QtCore.Qt.WindowTitleHint + QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowMaximizeButtonHint | QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowCloseButtonHint From 51a9d6b73ce9d6f7760a8e8265219cb1db2f8b8a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 2 Jun 2023 14:46:30 +0100 Subject: [PATCH 56/66] Improve error feedback when no renderable cameras exist --- .../maya/plugins/publish/collect_arnold_scene_source.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index 0845f653b1..eda7efa244 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -35,6 +35,11 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): # camera. cameras = cmds.ls(type="camera", long=True) renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)] + if not renderable: + raise ValueError( + "No renderable cameraes found, which is required for " + "publishing ASS." + ) camera = renderable[0] for node in instance.data["contentMembers"]: camera_shapes = cmds.listRelatives( From aa7dceb79c59f7aafb1909de8ee0c5af56465c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Fri, 2 Jun 2023 18:04:22 +0200 Subject: [PATCH 57/66] Add 'user' details on workfile manager details pane tab for Unix platform --- openpype/tools/workfiles/window.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 31ecf50d3b..34f7f24b02 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -1,6 +1,7 @@ import os import datetime import copy +import platform from qtpy import QtCore, QtWidgets, QtGui from openpype.client import ( @@ -94,6 +95,18 @@ class SidePanelWidget(QtWidgets.QWidget): self._on_note_change() self.save_clicked.emit() + def get_user_name(self, file): + """Get user name from file path""" + # Only run on Unix because pwd module is not available on Windows. + # NOTE: we tried adding "win32security" module but it was not working + # on all hosts so we decided to just support Linux until migration + # to Ayon + if platform.system() != "Windows": + import pwd + + filestat = os.stat(file) + return pwd.getpwuid(filestat.st_uid).pw_name + def set_context(self, asset_id, task_name, filepath, workfile_doc): # Check if asset, task and file are selected # NOTE workfile document is not requirement @@ -134,7 +147,9 @@ class SidePanelWidget(QtWidgets.QWidget): "Created:", creation_time.strftime(datetime_format), "Modified:", - modification_time.strftime(datetime_format) + modification_time.strftime(datetime_format), + "User:", + self.get_user_name(filepath), ) self._details_input.appendHtml("
".join(lines)) From 9c4e1ad4b1cd7938e007a65e7e50f87224e56763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Fri, 2 Jun 2023 18:08:28 +0200 Subject: [PATCH 58/66] Only add User details if platform isn't windows --- openpype/tools/workfiles/window.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 34f7f24b02..afaf7b9967 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -148,9 +148,12 @@ class SidePanelWidget(QtWidgets.QWidget): creation_time.strftime(datetime_format), "Modified:", modification_time.strftime(datetime_format), - "User:", - self.get_user_name(filepath), ) + if platform.system() != "Windows": + lines += ( + "User:", + self.get_user_name(filepath), + ) self._details_input.appendHtml("
".join(lines)) def get_workfile_data(self): From d37ccb4718e3fa9299d098844ddb0a92c1a09552 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 5 Jun 2023 10:26:42 +0200 Subject: [PATCH 59/66] :recycle: resolve few conversations --- .../houdini/plugins/publish/collect_arnold_rop.py | 15 ++++++--------- .../publish/submit_houdini_render_deadline.py | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 946eed3301..614785487f 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -49,16 +49,14 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): num_aovs = rop.evalParm("ar_aovs") for index in range(1, num_aovs + 1): - i = index + 1 - # Skip disabled AOVs - if not rop.evalParm("ar_enable_aov%s" % i): + if not rop.evalParm("ar_enable_aovP{}".format(index)): continue - if rop.evalParm("ar_aov_exr_enable_layer_name%s" % i): - label = rop.evalParm("ar_aov_exr_layer_name%s" % i) + if rop.evalParm("ar_aov_exr_enable_layer_name{}".format(index)): + label = rop.evalParm("ar_aov_exr_layer_name{}".format(index)) else: - label = evalParmNoFrame(rop, "ar_aov_label%s" % i) + label = evalParmNoFrame(rop, "ar_aov_label{}".format(index)) aov_product = self.get_render_product_name(default_prefix, suffix=label) @@ -67,10 +65,9 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): aov_product) for product in render_products: - self.log.debug("Found render product: %s" % product) + self.log.debug("Found render product: {}".format(product)) - filenames = list(render_products) - instance.data["files"] = filenames + instance.data["files"] = list(render_products) instance.data["renderProducts"] = colorspace.ARenderProduct() # For now by default do NOT try to publish the rendered output diff --git a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py index 6a62ee0ea8..254914a850 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -55,7 +55,7 @@ class HoudiniSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): filepath = context.data["currentFile"] filename = os.path.basename(filepath) - job_info.Name = "%s - %s" % (filename, instance.name) + job_info.Name = "{} - {}".format(filename, instance.name) job_info.BatchName = filename job_info.Plugin = "Houdini" job_info.UserName = context.data.get( From d46cee554d929d434859f38daf0660021dff53dc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 5 Jun 2023 17:03:23 +0800 Subject: [PATCH 60/66] fix the bug of not being able to use repair action --- .../maya/plugins/publish/validate_arnold_scene_source_cbid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py index e27723e104..8ce76c8d04 100644 --- a/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py +++ b/openpype/hosts/maya/plugins/publish/validate_arnold_scene_source_cbid.py @@ -70,5 +70,5 @@ class ValidateArnoldSceneSourceCbid(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - for content_node, proxy_node in cls.get_invalid_couples(cls, instance): - lib.set_id(proxy_node, lib.get_id(content_node), overwrite=False) + for content_node, proxy_node in cls.get_invalid_couples(instance): + lib.set_id(proxy_node, lib.get_id(content_node), overwrite=True) From c12e6995dea9e7e2b0929f4790affbe0e8ca62d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 5 Jun 2023 14:49:58 +0200 Subject: [PATCH 61/66] Update openpype/tools/workfiles/window.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/workfiles/window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index afaf7b9967..907598e9df 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -149,10 +149,11 @@ class SidePanelWidget(QtWidgets.QWidget): "Modified:", modification_time.strftime(datetime_format), ) - if platform.system() != "Windows": + username = self.get_user_name(filepath) + if username: lines += ( "User:", - self.get_user_name(filepath), + username, ) self._details_input.appendHtml("
".join(lines)) From 7c70ea968dc604f27cb4f0519fcef5eb297df3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 5 Jun 2023 15:07:35 +0200 Subject: [PATCH 62/66] Update openpype/tools/workfiles/window.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/workfiles/window.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 907598e9df..5bf6df35ca 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -101,11 +101,12 @@ class SidePanelWidget(QtWidgets.QWidget): # NOTE: we tried adding "win32security" module but it was not working # on all hosts so we decided to just support Linux until migration # to Ayon - if platform.system() != "Windows": - import pwd + if platform.system().lower() == "window": + return None + import pwd - filestat = os.stat(file) - return pwd.getpwuid(filestat.st_uid).pw_name + filestat = os.stat(file) + return pwd.getpwuid(filestat.st_uid).pw_name def set_context(self, asset_id, task_name, filepath, workfile_doc): # Check if asset, task and file are selected From bde15953595e57c30ea8b39becbdb4b81b9e537d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Mon, 5 Jun 2023 15:17:10 +0200 Subject: [PATCH 63/66] Update openpype/tools/workfiles/window.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/tools/workfiles/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/workfiles/window.py b/openpype/tools/workfiles/window.py index 5bf6df35ca..53f8894665 100644 --- a/openpype/tools/workfiles/window.py +++ b/openpype/tools/workfiles/window.py @@ -101,7 +101,7 @@ class SidePanelWidget(QtWidgets.QWidget): # NOTE: we tried adding "win32security" module but it was not working # on all hosts so we decided to just support Linux until migration # to Ayon - if platform.system().lower() == "window": + if platform.system().lower() == "windows": return None import pwd From a6d5b23fa765dd298b1183a4c0c0a843c5b10b62 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 5 Jun 2023 15:13:37 +0100 Subject: [PATCH 64/66] Update openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py Co-authored-by: Roy Nieterau --- .../hosts/maya/plugins/publish/collect_arnold_scene_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index eda7efa244..d72a428624 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -37,7 +37,7 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)] if not renderable: raise ValueError( - "No renderable cameraes found, which is required for " + "No renderable cameras found, which is required for " "publishing ASS." ) camera = renderable[0] From 791dd3ee6eabb4a4cb5ce77c4c366b61c5e92a3b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 5 Jun 2023 15:21:39 +0100 Subject: [PATCH 65/66] Debug logging instead of error --- .../publish/collect_arnold_scene_source.py | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index d72a428624..b7fa9bb6f9 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -35,18 +35,16 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): # camera. cameras = cmds.ls(type="camera", long=True) renderable = [c for c in cameras if cmds.getAttr("%s.renderable" % c)] - if not renderable: - raise ValueError( - "No renderable cameras found, which is required for " - "publishing ASS." - ) - camera = renderable[0] - for node in instance.data["contentMembers"]: - camera_shapes = cmds.listRelatives( - node, shapes=True, type="camera" - ) - if camera_shapes: - camera = node - instance.data["camera"] = camera + if renderable: + camera = renderable[0] + for node in instance.data["contentMembers"]: + camera_shapes = cmds.listRelatives( + node, shapes=True, type="camera" + ) + if camera_shapes: + camera = node + instance.data["camera"] = camera + else: + self.log.debug("No renderable cameraes found.") self.log.debug("data: {}".format(instance.data)) From 4fa079c312bafb037e163c53e7e98e4a80d796ae Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Mon, 5 Jun 2023 15:42:06 +0100 Subject: [PATCH 66/66] Update openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- .../hosts/maya/plugins/publish/collect_arnold_scene_source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py index b7fa9bb6f9..f160a3a0c5 100644 --- a/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py +++ b/openpype/hosts/maya/plugins/publish/collect_arnold_scene_source.py @@ -45,6 +45,6 @@ class CollectArnoldSceneSource(pyblish.api.InstancePlugin): camera = node instance.data["camera"] = camera else: - self.log.debug("No renderable cameraes found.") + self.log.debug("No renderable cameras found.") self.log.debug("data: {}".format(instance.data))