From e4365b746b40d311c59b3305c39adc61dc1a0425 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 28 Jan 2022 18:18:06 +0100 Subject: [PATCH 001/200] 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 002/200] 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 003/200] 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 004/200] 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 005/200] 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 006/200] 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 007/200] 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 008/200] 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 009/200] 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 010/200] 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 011/200] 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 012/200] 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 013/200] 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 014/200] 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 015/200] 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 016/200] 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 017/200] 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 c104805830c349260b30756b19560836bd9866a6 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:17:17 +0100 Subject: [PATCH 018/200] adding creator, loader for redshift proxy in 3dsmax --- .../plugins/create/create_redshift_proxy.py | 26 +++++++ .../max/plugins/load/load_redshift_proxy.py | 59 ++++++++++++++ .../plugins/publish/extract_redshift_proxy.py | 78 +++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 openpype/hosts/max/plugins/create/create_redshift_proxy.py create mode 100644 openpype/hosts/max/plugins/load/load_redshift_proxy.py create mode 100644 openpype/hosts/max/plugins/publish/extract_redshift_proxy.py diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py new file mode 100644 index 0000000000..83ddc3a193 --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating camera.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance + + +class CreateRedshiftProxy(plugin.MaxCreator): + identifier = "io.openpype.creators.max.redshiftproxy" + label = "Redshift Proxy" + family = "redshiftproxy" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + sel_obj = list(rt.selection) + instance = super(CreateRedshiftProxy, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + container = rt.getNodeByName(instance.data.get("instance_node")) + # TODO: Disable "Add to Containers?" Panel + # parent the selected cameras into the container + for obj in sel_obj: + obj.parent = container + # for additional work on the node: + # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py new file mode 100644 index 0000000000..7a5e94158f --- /dev/null +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -0,0 +1,59 @@ +import os +import clique + +from openpype.pipeline import ( + load, + get_representation_path +) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib + + +class RedshiftProxyLoader(load.LoaderPlugin): + """Redshift Proxy Loader""" + + families = ["redshiftproxy"] + representations = ["rs"] + order = -9 + icon = "code-fork" + color = "white" + + def load(self, context, name=None, namespace=None, data=None): + from pymxs import runtime as rt + + filepath = os.path.normpath(self.fname) + rs_proxy = rt.RedshiftProxy() + rs_proxy.file = filepath + files_in_folder = os.listdir(os.path.dirname(filepath)) + collections, remainder = clique.assemble(files_in_folder) + if collections: + rs_proxy.is_sequence = True + + container = rt.container() + container.name = f"{name}" + rs_proxy.Parent = container + + asset = rt.getNodeByName(f"{name}") + + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + proxy_objects = self.get_container_children(node) + for proxy in proxy_objects: + proxy.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) + + def remove(self, container): + from pymxs import runtime as rt + + node = rt.getNodeByName(container["instance_node"]) + rt.delete(node) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py new file mode 100644 index 0000000000..f9dd726ef4 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -0,0 +1,78 @@ +import os +import pyblish.api +from openpype.pipeline import ( + publish, + OptionalPyblishPluginMixin +) +from pymxs import runtime as rt +from openpype.hosts.max.api import ( + maintained_selection, + get_all_children +) + + +class ExtractCameraAlembic(publish.Extractor, + OptionalPyblishPluginMixin): + """ + Extract Camera with AlembicExport + """ + + order = pyblish.api.ExtractorOrder - 0.1 + label = "Extract RedShift Proxy" + hosts = ["max"] + families = ["redshiftproxy"] + + def process(self, instance): + container = instance.data["instance_node"] + start = int(instance.context.data.get("frameStart")) + end = int(instance.context.data.get("frameEnd")) + + self.log.info("Extracting Redshift Proxy...") + stagingdir = self.staging_dir(instance) + rs_filename = "{name}.rs".format(**instance.data) + + rs_filepath = os.path.join(stagingdir, rs_filename) + + # MaxScript command for export + export_cmd = ( + f""" +fn ProxyExport fp selected:true compress:false connectivity:false startFrame: endFrame: camera:undefined warnExisting:true transformPivotToOrigin:false = ( + if startFrame == unsupplied then ( + startFrame = (currentTime.frame as integer) + ) + + if endFrame == unsupplied then ( + endFrame = (currentTime.frame as integer) + ) + + ret = rsProxy fp selected compress connectivity startFrame endFrame camera warnExisting transformPivotToOrigin + + ret +) +execute = ProxyExport fp selected:true compress:false connectivity:false startFrame:{start} endFrame:{end} warnExisting:false transformPivotToOrigin:bTransformPivotToOrigin + + """) # noqa + + with maintained_selection(): + # select and export + rt.select(container.Children) + rt.execute(export_cmd) + + self.log.info("Performing Extraction ...") + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'rs', + 'ext': 'rs', + # need to count the files + 'files': rs_filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, + rs_filepath)) + + # TODO: set sequence + def get_rsfiles(self, container, startFrame, endFrame): + pass From 5af9867dedda3eb154fedd4aba0c93d8438b80df Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:25:00 +0100 Subject: [PATCH 019/200] update fix --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index f9dd726ef4..85d249b020 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -6,8 +6,7 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection, - get_all_children + maintained_selection ) From 9cbac449fded786bc033931a07c9e44dd18907e2 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:26:06 +0100 Subject: [PATCH 020/200] change the name --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 85d249b020..938a7e8c2c 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -10,7 +10,7 @@ from openpype.hosts.max.api import ( ) -class ExtractCameraAlembic(publish.Extractor, +class ExtractRedshiftProxy(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with AlembicExport From 119bb1a586548e6997f3e2724660c19e0d346a56 Mon Sep 17 00:00:00 2001 From: moonyuet Date: Tue, 14 Mar 2023 16:37:25 +0100 Subject: [PATCH 021/200] update the loader and creator --- openpype/hosts/max/plugins/create/create_redshift_proxy.py | 5 +---- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index 83ddc3a193..ca0891fc5b 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -18,9 +18,6 @@ class CreateRedshiftProxy(plugin.MaxCreator): instance_data, pre_create_data) # type: CreatedInstance container = rt.getNodeByName(instance.data.get("instance_node")) - # TODO: Disable "Add to Containers?" Panel - # parent the selected cameras into the container + for obj in sel_obj: obj.parent = container - # for additional work on the node: - # instance_node = rt.getNodeByName(instance.get("instance_node")) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 7a5e94158f..13003d764a 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -10,8 +10,8 @@ from openpype.hosts.max.api import lib class RedshiftProxyLoader(load.LoaderPlugin): - """Redshift Proxy Loader""" + label = "Load Redshift Proxy" families = ["redshiftproxy"] representations = ["rs"] order = -9 @@ -21,7 +21,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): from pymxs import runtime as rt - filepath = os.path.normpath(self.fname) + filepath = self.filepath_from_context(context) rs_proxy = rt.RedshiftProxy() rs_proxy.file = filepath files_in_folder = os.listdir(os.path.dirname(filepath)) @@ -30,7 +30,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): rs_proxy.is_sequence = True container = rt.container() - container.name = f"{name}" + container.name = name rs_proxy.Parent = container asset = rt.getNodeByName(f"{name}") From f3bd329d5a40793a6e083198326b22b43a58c621 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 18:48:58 +0800 Subject: [PATCH 022/200] add validator for checking if the current renderer is redshift before the extraction --- .../validate_renderer_redshift_proxy.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py new file mode 100644 index 0000000000..3a921c386e --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +import pyblish.api +from openpype.pipeline import PublishValidationError +from pymxs import runtime as rt +from openpype.pipeline.publish import RepairAction +from openpype.hosts.max.api.lib import get_current_renderer + + +class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): + """ + Validates Redshift as the current renderer for creating + Redshift Proxy + """ + + order = pyblish.api.ValidatorOrder + families = ["redshiftproxy"] + hosts = ["max"] + label = "Redshift Renderer" + actions = [RepairAction] + + def process(self, instance): + invalid = self.get_all_renderer(instance) + if invalid: + raise PublishValidationError("Please install Redshift for 3dsMax" + " before using this!") + invalid = self.get_current_renderer(instance) + if invalid: + raise PublishValidationError("Current Renderer is not Redshift") + + def get_all_renderer(self, instance): + invalid = list() + max_renderers_list = str(rt.RendererClass.classes) + if "Redshift_Renderer" not in max_renderers_list: + invalid.append(max_renderers_list) + + return invalid + + def get_current_renderer(self, instance): + invalid = list() + renderer_class = get_current_renderer() + current_renderer = str(renderer_class).split(":")[0] + if current_renderer != "Redshift_Renderer": + invalid.append(current_renderer) + + return invalid + + @classmethod + def repair(cls, instance): + if "Redshift_Renderer" in str(rt.RendererClass.classes[2]()): + rt.renderers.production = rt.RendererClass.classes[2]() From 91abe54b01ce08856004b7e395735c9508ab8300 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:35:02 +0800 Subject: [PATCH 023/200] add the extractor for redshift proxy --- .../plugins/publish/extract_redshift_proxy.py | 48 ++++++++----------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 938a7e8c2c..1616ead0ac 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -6,7 +6,8 @@ from openpype.pipeline import ( ) from pymxs import runtime as rt from openpype.hosts.max.api import ( - maintained_selection + maintained_selection, + get_all_children ) @@ -29,35 +30,22 @@ class ExtractRedshiftProxy(publish.Extractor, self.log.info("Extracting Redshift Proxy...") stagingdir = self.staging_dir(instance) rs_filename = "{name}.rs".format(**instance.data) - rs_filepath = os.path.join(stagingdir, rs_filename) + rs_filepath = rs_filepath.replace("\\", "/") - # MaxScript command for export - export_cmd = ( - f""" -fn ProxyExport fp selected:true compress:false connectivity:false startFrame: endFrame: camera:undefined warnExisting:true transformPivotToOrigin:false = ( - if startFrame == unsupplied then ( - startFrame = (currentTime.frame as integer) - ) - - if endFrame == unsupplied then ( - endFrame = (currentTime.frame as integer) - ) - - ret = rsProxy fp selected compress connectivity startFrame endFrame camera warnExisting transformPivotToOrigin - - ret -) -execute = ProxyExport fp selected:true compress:false connectivity:false startFrame:{start} endFrame:{end} warnExisting:false transformPivotToOrigin:bTransformPivotToOrigin - - """) # noqa + rs_filenames = self.get_rsfiles(instance, start, end) with maintained_selection(): # select and export - rt.select(container.Children) - rt.execute(export_cmd) + # con = rt.getNodeByName(container) + rt.select(get_all_children(rt.getNodeByName(container))) + # Redshift rsProxy command + # rsProxy fp selected compress connectivity startFrame endFrame + # camera warnExisting transformPivotToOrigin + rt.rsProxy(rs_filepath, 1, 0, 0, start, end, 0, 1, 1) self.log.info("Performing Extraction ...") + if "representations" not in instance.data: instance.data["representations"] = [] @@ -65,13 +53,19 @@ execute = ProxyExport fp selected:true compress:false connectivity:false startFr 'name': 'rs', 'ext': 'rs', # need to count the files - 'files': rs_filename, + 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], "stagingDir": stagingdir, } instance.data["representations"].append(representation) self.log.info("Extracted instance '%s' to: %s" % (instance.name, - rs_filepath)) + stagingdir)) # TODO: set sequence - def get_rsfiles(self, container, startFrame, endFrame): - pass + def get_rsfiles(self, instance, startFrame, endFrame): + rs_filenames = [] + rs_name = instance.data["name"] + for frame in range(startFrame, endFrame + 1): + rs_filename = "%s.%04d.rs" % (rs_name, frame) + rs_filenames.append(rs_filename) + + return rs_filenames From be6813293c605d5b477af72fedcca047b6e7f0c0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:36:22 +0800 Subject: [PATCH 024/200] shut hound --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 1616ead0ac..8924242a93 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -53,7 +53,7 @@ class ExtractRedshiftProxy(publish.Extractor, 'name': 'rs', 'ext': 'rs', # need to count the files - 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], + 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], # noqa "stagingDir": stagingdir, } instance.data["representations"].append(representation) From f25b5d309ad212b273a2ba6ccdfa6b1d950e5a25 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:42:28 +0800 Subject: [PATCH 025/200] cleanup --- .../max/plugins/publish/extract_redshift_proxy.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 8924242a93..bf16c8d4a9 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -1,9 +1,6 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish from pymxs import runtime as rt from openpype.hosts.max.api import ( maintained_selection, @@ -11,10 +8,9 @@ from openpype.hosts.max.api import ( ) -class ExtractRedshiftProxy(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractRedshiftProxy(publish.Extractor): """ - Extract Camera with AlembicExport + Extract Redshift Proxy """ order = pyblish.api.ExtractorOrder - 0.1 From 6d51333a20e13b07d8f32ea69c299163b1c90fc4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:45:52 +0800 Subject: [PATCH 026/200] add docstrings --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index bf16c8d4a9..c91391429d 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -10,7 +10,7 @@ from openpype.hosts.max.api import ( class ExtractRedshiftProxy(publish.Extractor): """ - Extract Redshift Proxy + Extract Redshift Proxy with rsProxy """ order = pyblish.api.ExtractorOrder - 0.1 From b9ec96fdd64b4d8e7f770dfc722e459f28cd596e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 22:48:21 +0800 Subject: [PATCH 027/200] add docstring for the rs loader --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 13003d764a..30879bca78 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -11,6 +11,8 @@ from openpype.hosts.max.api import lib class RedshiftProxyLoader(load.LoaderPlugin): + """Load rs files with Redshift Proxy""" + label = "Load Redshift Proxy" families = ["redshiftproxy"] representations = ["rs"] From 3ba7b9b1ffc8ba9877024f8175af8adcbd984e08 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 23:05:25 +0800 Subject: [PATCH 028/200] fix selection of children --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index c91391429d..5aba257443 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -33,8 +33,8 @@ class ExtractRedshiftProxy(publish.Extractor): with maintained_selection(): # select and export - # con = rt.getNodeByName(container) - rt.select(get_all_children(rt.getNodeByName(container))) + con = rt.getNodeByName(container) + rt.select(con.Children) # Redshift rsProxy command # rsProxy fp selected compress connectivity startFrame endFrame # camera warnExisting transformPivotToOrigin From 35448073aba93b3ead99513b0d831accb00c76cb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Mar 2023 23:07:05 +0800 Subject: [PATCH 029/200] hound fix --- openpype/hosts/max/plugins/publish/extract_redshift_proxy.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 5aba257443..0a3579d687 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -2,10 +2,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection class ExtractRedshiftProxy(publish.Extractor): From 14b8139a5cfe92680233343d8e4120ae2253865f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Apr 2023 16:47:15 +0800 Subject: [PATCH 030/200] Roy's comment & fix the loader update --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 30879bca78..fd79a2b97c 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -35,7 +35,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): container.name = name rs_proxy.Parent = container - asset = rt.getNodeByName(f"{name}") + asset = rt.getNodeByName(name) return containerise( name, [asset], context, loader=self.__class__.__name__) @@ -45,10 +45,10 @@ class RedshiftProxyLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - - proxy_objects = self.get_container_children(node) - for proxy in proxy_objects: - proxy.source = path + for children in node.Children: + children_node = rt.getNodeByName(children.name) + for proxy in children_node.Children: + proxy.file = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) From 6423479078a7305d9e73f55243eb42796acb7820 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Apr 2023 16:50:33 +0800 Subject: [PATCH 031/200] add switch version in the loader --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index fd79a2b97c..9451e5299b 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -54,6 +54,9 @@ class RedshiftProxyLoader(load.LoaderPlugin): "representation": str(representation["_id"]) }) + def switch(self, container, representation): + self.update(container, representation) + def remove(self, container): from pymxs import runtime as rt From 122a4dc9db074f7bd14421e1d8b9244a05318da7 Mon Sep 17 00:00:00 2001 From: Michael reda Date: Wed, 5 Apr 2023 12:14:06 +0200 Subject: [PATCH 032/200] add sync to specific projects or listen only --- openpype/modules/kitsu/kitsu_module.py | 16 ++++++++++-- .../modules/kitsu/utils/update_op_with_zou.py | 25 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index b91373af20..f4e3dd5691 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,19 +124,31 @@ def push_to_zou(login, password): @cli_main.command() +@click.option("-prjs", "--projects", envvar="SYNC_PROJECTS", help="Sync specific kitsu projects") @click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password): +def sync_service(login, password, projects="^"): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password + projects (str): specific kitsu projects + + SYNC_PROJECTS: + *: all projects + ^: dont sync any project just listen + "project01 project02 ...": to choose custom projects + + """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - sync_all_projects(login, password) + projects = projects.strip() + projects = projects.split(' ') + + sync_all_projects(login, password, specific_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4f4f0810bc..a397198a13 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -359,7 +359,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: def sync_all_projects( - login: str, password: str, ignore_projects: list = None + login: str, password: str, ignore_projects: list = None, specific_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -367,6 +367,7 @@ def sync_all_projects( login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names + specific_projects (list): List of synced project names Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -381,7 +382,27 @@ def sync_all_projects( dbcon = AvalonMongoDB() dbcon.install() all_projects = gazu.project.all_projects() - for project in all_projects: + + + project_to_sync = [] + if specific_projects == ['*']: + project_to_sync = all_projects + + elif specific_projects == ['^']: + return + + elif isinstance(specific_projects, list): + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in specific_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exists in kitsu.' + f' Please make sure you write the project correctly.') + else: + return + + for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: continue sync_project_from_kitsu(dbcon, project) From 442236284bc87cf3a4aff4d3ae622beaaf946c4c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Apr 2023 17:53:42 +0800 Subject: [PATCH 033/200] add docs --- website/docs/artist_hosts_3dsmax.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/website/docs/artist_hosts_3dsmax.md b/website/docs/artist_hosts_3dsmax.md index 12c1f40181..fffab8ca5d 100644 --- a/website/docs/artist_hosts_3dsmax.md +++ b/website/docs/artist_hosts_3dsmax.md @@ -30,7 +30,7 @@ By clicking the icon ```OpenPype Menu``` rolls out. Choose ```OpenPype Menu > Launcher``` to open the ```Launcher``` window. -When opened you can **choose** the **project** to work in from the list. Then choose the particular **asset** you want to work on then choose **task** +When opened you can **choose** the **project** to work in from the list. Then choose the particular **asset** you want to work on then choose **task** and finally **run 3dsmax by its icon** in the tools. ![Menu OpenPype](assets/3dsmax_tray_OP.png) @@ -65,13 +65,13 @@ If not any workfile present simply hit ```Save As``` and keep ```Subversion``` e ![Save As Dialog](assets/3dsmax_SavingFirstFile_OP.png) -OpenPype correctly names it and add version to the workfile. This basically happens whenever user trigger ```Save As``` action. Resulting into incremental version numbers like +OpenPype correctly names it and add version to the workfile. This basically happens whenever user trigger ```Save As``` action. Resulting into incremental version numbers like ```workfileName_v001``` ```workfileName_v002``` - etc. + etc. Basically meaning user is free of guessing what is the correct naming and other necessities to keep everything in order and managed. @@ -105,13 +105,13 @@ Before proceeding further please check [Glossary](artist_concepts.md) and [What ### Intro -Current OpenPype integration (ver 3.15.0) supports only ```PointCache``` and ```Camera``` families now. +Current OpenPype integration (ver 3.15.0) supports only ```PointCache```, ```Camera```, ```Geometry``` and ```Redshift Proxy``` families now. **Pointcache** family being basically any geometry outputted as Alembic cache (.abc) format **Camera** family being 3dsmax Camera object with/without animation outputted as native .max, FBX, Alembic format - +**Redshift Proxy** family being Redshift Proxy object with/without animation outputted as rs format(Redshift Proxy's very own format) --- :::note Work in progress @@ -119,7 +119,3 @@ This part of documentation is still work in progress. ::: ## ...to be added - - - - From 414cd0cce113328755103f046e3a5aeef0e432e5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 11 Apr 2023 16:09:56 +0800 Subject: [PATCH 034/200] 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 035/200] 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 036/200] 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 037/200] 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 038/200] 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 039/200] 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 040/200] 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 041/200] 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 042/200] 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 043/200] 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 044/200] 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 045/200] 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 046/200] 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 047/200] 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 048/200] 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 049/200] 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 050/200] 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 051/200] 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 052/200] 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 3c801ca1187ac3d1cf1d0da50cf27eceaae5fa30 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 18 Apr 2023 22:39:07 +0200 Subject: [PATCH 053/200] Fix imports --- openpype/hosts/maya/api/setdress.py | 8 +++----- openpype/hosts/maya/plugins/load/load_assembly.py | 15 ++++++--------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/api/setdress.py b/openpype/hosts/maya/api/setdress.py index 159bfe9eb3..0bb1f186eb 100644 --- a/openpype/hosts/maya/api/setdress.py +++ b/openpype/hosts/maya/api/setdress.py @@ -28,7 +28,9 @@ from openpype.pipeline import ( ) from openpype.hosts.maya.api.lib import ( matrix_equals, - unique_namespace + unique_namespace, + get_container_transforms, + DEFAULT_MATRIX ) log = logging.getLogger("PackageLoader") @@ -183,8 +185,6 @@ def _add(instance, representation_id, loaders, namespace, root="|"): """ - from openpype.hosts.maya.lib import get_container_transforms - # Process within the namespace with namespaced(namespace, new=False) as namespace: @@ -379,8 +379,6 @@ def update_scene(set_container, containers, current_data, new_data, new_file): """ - from openpype.hosts.maya.lib import DEFAULT_MATRIX, get_container_transforms - set_namespace = set_container['namespace'] project_name = legacy_io.active_project() diff --git a/openpype/hosts/maya/plugins/load/load_assembly.py b/openpype/hosts/maya/plugins/load/load_assembly.py index 902f38695c..275f21be5d 100644 --- a/openpype/hosts/maya/plugins/load/load_assembly.py +++ b/openpype/hosts/maya/plugins/load/load_assembly.py @@ -1,8 +1,14 @@ +import maya.cmds as cmds + from openpype.pipeline import ( load, remove_container ) +from openpype.hosts.maya.api.pipeline import containerise +from openpype.hosts.maya.api.lib import unique_namespace +from openpype.hosts.maya.api import setdress + class AssemblyLoader(load.LoaderPlugin): @@ -16,9 +22,6 @@ class AssemblyLoader(load.LoaderPlugin): def load(self, context, name, namespace, data): - from openpype.hosts.maya.api.pipeline import containerise - from openpype.hosts.maya.api.lib import unique_namespace - asset = context['asset']['name'] namespace = namespace or unique_namespace( asset + "_", @@ -26,8 +29,6 @@ class AssemblyLoader(load.LoaderPlugin): suffix="_", ) - from openpype.hosts.maya.api import setdress - containers = setdress.load_package( filepath=self.fname, name=name, @@ -50,15 +51,11 @@ class AssemblyLoader(load.LoaderPlugin): def update(self, container, representation): - from openpype import setdress return setdress.update_package(container, representation) def remove(self, container): """Remove all sub containers""" - from openpype import setdress - import maya.cmds as cmds - # Remove all members member_containers = setdress.get_contained_containers(container) for member_container in member_containers: From e3b5aa0f3fd48ae5b9cf3753d636593b069de809 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 24 Apr 2023 16:22:19 +0800 Subject: [PATCH 054/200] 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 7b6df29203ea2b093bc68bc4f9aec0fa6a167b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 12:48:51 +0200 Subject: [PATCH 055/200] Feature: Blender hook to execute python scripts at launch --- .../hooks/pre_add_run_python_script_arg.py | 61 +++++++++++++++++++ website/docs/dev_blender.md | 61 +++++++++++++++++++ website/sidebars.js | 1 + 3 files changed, 123 insertions(+) create mode 100644 openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py create mode 100644 website/docs/dev_blender.md diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py new file mode 100644 index 0000000000..7cf7b0f852 --- /dev/null +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -0,0 +1,61 @@ +from pathlib import Path + +from openpype.lib import PreLaunchHook +from openpype.settings.lib import get_project_settings + + +class AddPythonScriptToLaunchArgs(PreLaunchHook): + """Add python script to be executed before Blender launch.""" + + # Append after file argument + order = 15 + app_groups = [ + "blender", + ] + + def execute(self): + # Check enabled in settings + project_name = self.data["project_name"] + project_settings = get_project_settings(project_name) + host_name = self.application.host_name + host_settings = project_settings.get(host_name) + if not host_settings: + self.log.info(f"""Host "{host_name}" doesn\'t have settings""") + return None + + # Add path to workfile to arguments + for python_script_path in self.launch_context.data.get( + "python_scripts", [] + ): + self.log.info( + f"Adding python script {python_script_path} to launch" + ) + # Test script path exists + if not Path(python_script_path).exists(): + raise ValueError( + f"Python script {python_script_path} doesn't exist." + ) + + if "--" in self.launch_context.launch_args: + # Insert before separator + separator_index = self.launch_context.launch_args.index("--") + self.launch_context.launch_args.insert( + separator_index, + "-P", + ) + self.launch_context.launch_args.insert( + separator_index + 1, + Path(python_script_path).as_posix(), + ) + else: + self.launch_context.launch_args.extend( + ["-P", Path(python_script_path).as_posix()] + ) + + # Ensure separator + if "--" not in self.launch_context.launch_args: + self.launch_context.launch_args.append("--") + + self.launch_context.launch_args.extend( + [*self.launch_context.data.get("script_args", [])] + ) diff --git a/website/docs/dev_blender.md b/website/docs/dev_blender.md new file mode 100644 index 0000000000..228447fb64 --- /dev/null +++ b/website/docs/dev_blender.md @@ -0,0 +1,61 @@ +--- +id: dev_blender +title: Blender integration +sidebar_label: Blender integration +toc_max_heading_level: 4 +--- + +## Run python script at launch +In case you need to execute a python script when Blender is started (aka [`-P`](https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html#python-options)), for example to programmatically modify a blender file for conformation, you can create an OpenPype hook as follows: + +```python +from openpype.hosts.blender.hooks.pre_add_run_python_script_arg import AddPythonScriptToLaunchArgs +from openpype.lib import PreLaunchHook + + +class MyHook(PreLaunchHook): + """Add python script to be executed before Blender launch.""" + + order = AddPythonScriptToLaunchArgs.order - 1 + app_groups = [ + "blender", + ] + + def execute(self): + self.launch_context.data.setdefault("python_scripts", []).append( + "/path/to/my_script.py" + ) +``` + +You can write a bare python script, as you could run into the [Text Editor](https://docs.blender.org/manual/en/latest/editors/text_editor.html). + +### Python script with arguments +#### Adding arguments +In case you need to pass arguments to your script, you can append them to `self.launch_context.data["script_args"]`: + +```python +self.launch_context.data.setdefault("script_args", []).append( + "--my-arg", + "value", + ) +``` + +#### Parsing arguments +You can parse arguments in your script using [argparse](https://docs.python.org/3/library/argparse.html) as follows: + +```python +import argparse + +parser = argparse.ArgumentParser( + description="Parsing arguments for my_script.py" +) +parser.add_argument( + "--my-arg", + nargs="?", + help="My argument", +) +args, unknown = arg_parser.parse_known_args( + sys.argv[sys.argv.index("--") + 1 :] +) +print(args.my_arg) +``` diff --git a/website/sidebars.js b/website/sidebars.js index 93887e00f6..c204c3fb45 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -179,6 +179,7 @@ module.exports = { ] }, "dev_deadline", + "dev_blender", "dev_colorspace" ] }; From 6de1710810b028949c86276088a988ccb83f06e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 12:53:20 +0200 Subject: [PATCH 056/200] clean Path --- .../hosts/blender/hooks/pre_add_run_python_script_arg.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 7cf7b0f852..9ae96327ca 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -31,7 +31,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): f"Adding python script {python_script_path} to launch" ) # Test script path exists - if not Path(python_script_path).exists(): + python_script_path = Path(python_script_path) + if not python_script_path.exists(): raise ValueError( f"Python script {python_script_path} doesn't exist." ) @@ -45,11 +46,11 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): ) self.launch_context.launch_args.insert( separator_index + 1, - Path(python_script_path).as_posix(), + python_script_path.as_posix(), ) else: self.launch_context.launch_args.extend( - ["-P", Path(python_script_path).as_posix()] + ["-P", python_script_path.as_posix()] ) # Ensure separator From cdf9a10aa19b39c698a57b97abbb5858b6571de6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 19:10:55 +0800 Subject: [PATCH 057/200] roy's comment --- openpype/hosts/max/api/lib.py | 9 ++++++++- .../max/plugins/create/create_redshift_proxy.py | 7 ++++--- .../max/plugins/publish/extract_redshift_proxy.py | 3 +-- .../publish/validate_renderer_redshift_proxy.py | 14 +++++++++----- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ad9a450cad..27d4598a3a 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -128,7 +128,14 @@ def get_all_children(parent, node_type=None): def get_current_renderer(): - """get current renderer""" + """ + Notes: + Get current renderer for Max + + Returns: + "{Current Renderer}:{Current Renderer}" + e.g. "Redshift_Renderer:Redshift_Renderer" + """ return rt.renderers.production diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index ca0891fc5b..1bddbdafae 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -18,6 +18,7 @@ class CreateRedshiftProxy(plugin.MaxCreator): instance_data, pre_create_data) # type: CreatedInstance container = rt.getNodeByName(instance.data.get("instance_node")) - - for obj in sel_obj: - obj.parent = container + if self.selected_nodes: + sel_obj = list(self.selected_nodes) + for obj in sel_obj: + obj.parent = container diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index 0a3579d687..eb1673c4fa 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -45,7 +45,6 @@ class ExtractRedshiftProxy(publish.Extractor): representation = { 'name': 'rs', 'ext': 'rs', - # need to count the files 'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], # noqa "stagingDir": stagingdir, } @@ -53,7 +52,7 @@ class ExtractRedshiftProxy(publish.Extractor): self.log.info("Extracted instance '%s' to: %s" % (instance.name, stagingdir)) - # TODO: set sequence + def get_rsfiles(self, instance, startFrame, endFrame): rs_filenames = [] rs_name = instance.data["name"] diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py index 3a921c386e..c834f12ae2 100644 --- a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -22,12 +22,13 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): invalid = self.get_all_renderer(instance) if invalid: raise PublishValidationError("Please install Redshift for 3dsMax" - " before using this!") + " before using the Redshift proxy instance") invalid = self.get_current_renderer(instance) if invalid: - raise PublishValidationError("Current Renderer is not Redshift") + raise PublishValidationError("The Redshift proxy extraction discontinued" + "since the current renderer is not Redshift") - def get_all_renderer(self, instance): + def get_redshift_renderer(self, instance): invalid = list() max_renderers_list = str(rt.RendererClass.classes) if "Redshift_Renderer" not in max_renderers_list: @@ -46,5 +47,8 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - if "Redshift_Renderer" in str(rt.RendererClass.classes[2]()): - rt.renderers.production = rt.RendererClass.classes[2]() + renderer_count = len(rt.RendererClass.classes) + for r in range(renderer_count): + if "Redshift_Renderer" in str(rt.RendererClass.classes[r]()): + rt.renderers.production = rt.RendererClass.classes[r]() + break From a20d37c68045d73b5f442f503dcbaf31bb8892b5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 19:13:33 +0800 Subject: [PATCH 058/200] hound fix --- .../hosts/max/plugins/publish/extract_redshift_proxy.py | 1 - .../max/plugins/publish/validate_renderer_redshift_proxy.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py index eb1673c4fa..3b44099609 100644 --- a/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/extract_redshift_proxy.py @@ -52,7 +52,6 @@ class ExtractRedshiftProxy(publish.Extractor): self.log.info("Extracted instance '%s' to: %s" % (instance.name, stagingdir)) - def get_rsfiles(self, instance, startFrame, endFrame): rs_filenames = [] rs_name = instance.data["name"] diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py index c834f12ae2..6f8a92a93c 100644 --- a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -22,11 +22,11 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): invalid = self.get_all_renderer(instance) if invalid: raise PublishValidationError("Please install Redshift for 3dsMax" - " before using the Redshift proxy instance") + " before using the Redshift proxy instance") # noqa invalid = self.get_current_renderer(instance) if invalid: - raise PublishValidationError("The Redshift proxy extraction discontinued" - "since the current renderer is not Redshift") + raise PublishValidationError("The Redshift proxy extraction" + "discontinued since the current renderer is not Redshift") # noqa def get_redshift_renderer(self, instance): invalid = list() From 610a65420d521b26a3c44792368cbd4b9cec6219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 14:02:18 +0200 Subject: [PATCH 059/200] remove useless settings --- .../hosts/blender/hooks/pre_add_run_python_script_arg.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 9ae96327ca..ff3683baa9 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -14,15 +14,6 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): ] def execute(self): - # Check enabled in settings - project_name = self.data["project_name"] - project_settings = get_project_settings(project_name) - host_name = self.application.host_name - host_settings = project_settings.get(host_name) - if not host_settings: - self.log.info(f"""Host "{host_name}" doesn\'t have settings""") - return None - # Add path to workfile to arguments for python_script_path in self.launch_context.data.get( "python_scripts", [] From 4fe7ce64a2a834adcdd2763a22b8c93d532c0ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 26 Apr 2023 14:05:39 +0200 Subject: [PATCH 060/200] changes from comments --- .../hosts/blender/hooks/pre_add_run_python_script_arg.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index ff3683baa9..0f959b8f54 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -1,7 +1,6 @@ from pathlib import Path from openpype.lib import PreLaunchHook -from openpype.settings.lib import get_project_settings class AddPythonScriptToLaunchArgs(PreLaunchHook): @@ -14,10 +13,11 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): ] def execute(self): + if not self.launch_context.data.get("python_scripts"): + return + # Add path to workfile to arguments - for python_script_path in self.launch_context.data.get( - "python_scripts", [] - ): + for python_script_path in self.launch_context.data["python_scripts"]: self.log.info( f"Adding python script {python_script_path} to launch" ) From edec4a2b1995adbeda6f5b681b9488f7fb3bcb08 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 26 Apr 2023 20:17:36 +0800 Subject: [PATCH 061/200] roy's comment --- .../publish/validate_renderer_redshift_proxy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py index 6f8a92a93c..bc82f82f3b 100644 --- a/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py +++ b/openpype/hosts/max/plugins/publish/validate_renderer_redshift_proxy.py @@ -19,7 +19,7 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): actions = [RepairAction] def process(self, instance): - invalid = self.get_all_renderer(instance) + invalid = self.get_redshift_renderer(instance) if invalid: raise PublishValidationError("Please install Redshift for 3dsMax" " before using the Redshift proxy instance") # noqa @@ -47,8 +47,8 @@ class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - renderer_count = len(rt.RendererClass.classes) - for r in range(renderer_count): - if "Redshift_Renderer" in str(rt.RendererClass.classes[r]()): - rt.renderers.production = rt.RendererClass.classes[r]() + for Renderer in rt.RendererClass.classes: + renderer = Renderer() + if "Redshift_Renderer" in str(renderer): + rt.renderers.production = renderer break From 4274300874dc2a3bdf6d91c79ccebd480a0ddd7e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 27 Apr 2023 20:55:40 +0800 Subject: [PATCH 062/200] 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 063/200] 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 064/200] 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 065/200] 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 066/200] 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 067/200] 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 068/200] 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 84cef9d3cf6135fd26b490f57e9bc68d9da36ace Mon Sep 17 00:00:00 2001 From: Michael reda Date: Tue, 2 May 2023 11:12:18 +0200 Subject: [PATCH 069/200] handel long lines --- openpype/modules/kitsu/kitsu_module.py | 19 ++++++-- .../modules/kitsu/utils/update_op_with_zou.py | 48 +++++++++---------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index f4e3dd5691..7c9d888aa7 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,10 +124,23 @@ def push_to_zou(login, password): @cli_main.command() -@click.option("-prjs", "--projects", envvar="SYNC_PROJECTS", help="Sync specific kitsu projects") -@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( - "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" + "-prjs", + "--projects", + envvar="SYNC_PROJECTS", + help="Sync specific kitsu projects" +) +@click.option( + "-l", + "--login", + envvar="KITSU_LOGIN", + help="Kitsu login" +) +@click.option( + "-p", + "--password", + envvar="KITSU_PWD", + help="Password for kitsu username" ) def sync_service(login, password, projects="^"): """Synchronize openpype database from Zou sever database. diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index a397198a13..ad8ccd9f3f 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -63,11 +63,11 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( - dbcon: AvalonMongoDB, - gazu_project: dict, - project_doc: dict, - entities_list: List[dict], - asset_doc_ids: Dict[str, dict], + dbcon: AvalonMongoDB, + gazu_project: dict, + project_doc: dict, + entities_list: List[dict], + asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. @@ -210,10 +210,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -350,7 +350,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -359,7 +359,8 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: def sync_all_projects( - login: str, password: str, ignore_projects: list = None, specific_projects: list = None + login: str, password: str, ignore_projects: list = None, + specific_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -383,7 +384,6 @@ def sync_all_projects( dbcon.install() all_projects = gazu.project.all_projects() - project_to_sync = [] if specific_projects == ['*']: project_to_sync = all_projects @@ -435,8 +435,8 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): # Do not sync closed kitsu project that is not found in openpype if ( - project['project_status_name'] == "Closed" - and not get_project(project['name']) + project['project_status_name'] == "Closed" + and not get_project(project['name']) ): return @@ -451,10 +451,10 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): all_entities = [ item for item in all_assets - + all_asset_types - + all_episodes - + all_seqs - + all_shots + + all_asset_types + + all_episodes + + all_seqs + + all_shots if naming_pattern.match(item["name"]) ] @@ -526,12 +526,12 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, - project, - project_dict, - all_entities, - zou_ids_and_asset_docs, - ) + dbcon, + project, + project_dict, + all_entities, + zou_ids_and_asset_docs, + ) ] ) From 2a9a7d6fd4f0a96dc70b1f61525d5b8f1ac05eb5 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:18:43 +0300 Subject: [PATCH 070/200] handle reviews --- openpype/modules/kitsu/kitsu_module.py | 19 ++---- .../modules/kitsu/utils/update_op_with_zou.py | 62 +++++++++---------- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 7c9d888aa7..2a3a0f3bff 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -125,9 +125,10 @@ def push_to_zou(login, password): @cli_main.command() @click.option( - "-prjs", - "--projects", - envvar="SYNC_PROJECTS", + "-prj", + "--project", + multiple=True, + default=[""] help="Sync specific kitsu projects" ) @click.option( @@ -142,26 +143,18 @@ def push_to_zou(login, password): envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password, projects="^"): +def sync_service(login, password, project): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password projects (str): specific kitsu projects - - SYNC_PROJECTS: - *: all projects - ^: dont sync any project just listen - "project01 project02 ...": to choose custom projects - - """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - projects = projects.strip() - projects = projects.split(' ') + projects = ' '.join(project) sync_all_projects(login, password, specific_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index ad8ccd9f3f..7983765e83 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -63,11 +63,11 @@ def set_op_project(dbcon: AvalonMongoDB, project_id: str): def update_op_assets( - dbcon: AvalonMongoDB, - gazu_project: dict, - project_doc: dict, - entities_list: List[dict], - asset_doc_ids: Dict[str, dict], + dbcon: AvalonMongoDB, + gazu_project: dict, + project_doc: dict, + entities_list: List[dict], + asset_doc_ids: Dict[str, dict], ) -> List[Dict[str, dict]]: """Update OpenPype assets. Set 'data' and 'parent' fields. @@ -210,10 +210,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -350,7 +350,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -358,9 +358,8 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: ) -def sync_all_projects( - login: str, password: str, ignore_projects: list = None, - specific_projects: list = None +def sync_all_projects(login: str, password: str, ignore_projects: list = None, + filter_projects: list = None ): """Update all OP projects in DB with Zou data. @@ -368,7 +367,7 @@ def sync_all_projects( login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names - specific_projects (list): List of synced project names + filter_projects (list): List of filter project names to sync with Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -385,22 +384,21 @@ def sync_all_projects( all_projects = gazu.project.all_projects() project_to_sync = [] - if specific_projects == ['*']: + + if not filter_projects: + # listen only + return + + if filter_projects == ['*']: project_to_sync = all_projects - elif specific_projects == ['^']: - return - - elif isinstance(specific_projects, list): - all_kitsu_projects = {p['name']: p for p in all_projects} - for proj_name in specific_projects: - if proj_name in all_kitsu_projects: - project_to_sync.append(all_kitsu_projects[proj_name]) - else: - log.info(f'`{proj_name}` project does not exists in kitsu.' - f' Please make sure you write the project correctly.') - else: - return + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in filter_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exist in Kitsu.' + f' Please make sure the project is spelled correctly.') for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: @@ -451,10 +449,10 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): all_entities = [ item for item in all_assets - + all_asset_types - + all_episodes - + all_seqs - + all_shots + + all_asset_types + + all_episodes + + all_seqs + + all_shots if naming_pattern.match(item["name"]) ] From 0a3d206680ed7b21cfb30f1800ae1625e284793d Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:23:17 +0300 Subject: [PATCH 071/200] solve syntex error --- openpype/modules/kitsu/kitsu_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 2a3a0f3bff..59ad2efd29 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -128,7 +128,7 @@ def push_to_zou(login, password): "-prj", "--project", multiple=True, - default=[""] + default=[""], help="Sync specific kitsu projects" ) @click.option( From 6e9b6f6bef0c290dd46b7b3669fad2d3b338c0e8 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:38:42 +0300 Subject: [PATCH 072/200] correct arg name in sync_all_projects --- openpype/modules/kitsu/kitsu_module.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 59ad2efd29..bd8ade62d8 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -149,12 +149,10 @@ def sync_service(login, password, project): Args: login (str): Kitsu user login password (str): Kitsu user password - projects (str): specific kitsu projects + project (str): specific kitsu projects """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - projects = ' '.join(project) - - sync_all_projects(login, password, specific_projects=projects) + sync_all_projects(login, password, filter_projects=project) start_listeners(login, password) From ad4f5876ce18239b244d59332ed40b5469cc0433 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 12:40:32 +0300 Subject: [PATCH 073/200] update condition to sync specific projects --- .../modules/kitsu/utils/update_op_with_zou.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 7983765e83..40e4191508 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -389,16 +389,18 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, # listen only return - if filter_projects == ['*']: + if '*' in filter_projects: + # all projects project_to_sync = all_projects - all_kitsu_projects = {p['name']: p for p in all_projects} - for proj_name in filter_projects: - if proj_name in all_kitsu_projects: - project_to_sync.append(all_kitsu_projects[proj_name]) - else: - log.info(f'`{proj_name}` project does not exist in Kitsu.' - f' Please make sure the project is spelled correctly.') + else: + all_kitsu_projects = {p['name']: p for p in all_projects} + for proj_name in filter_projects: + if proj_name in all_kitsu_projects: + project_to_sync.append(all_kitsu_projects[proj_name]) + else: + log.info(f'`{proj_name}` project does not exist in Kitsu.' + f' Please make sure the project is spelled correctly.') for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: From d6e7cd638d403f0d3f8ad53e7fef444122b1dc39 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 13:02:08 +0300 Subject: [PATCH 074/200] update kitsu docs --- website/docs/module_kitsu.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index d79c78fecf..4b827b3802 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -16,10 +16,18 @@ If you want to connect Kitsu to OpenPype you have to set the `Server` url in Kit This setting is available for all the users of the OpenPype instance. ## Synchronize -Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets. +Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu projects with `-proj, --project` and create/delete/update OP assets. Once this sync is done, the thread will automatically start a loop to listen to Kitsu events. +The args for `-proj, --project` accept multiple project name, `-proj *` to sync all active projects, and the default value to start a loop to listen to Kitsu events only without any sync. ```bash +// sync specific projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj project_name01 -proj project_name02 + +// sync all projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj * + +// start listen only openpype_console module kitsu sync-service -l me@domain.ext -p my_password ``` From 925ea3fe9312a3e78df1604e370a019cb2adbe5c Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:14:33 +0300 Subject: [PATCH 075/200] Update module_kitsu docs --- website/docs/module_kitsu.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 4b827b3802..970bfb275e 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -16,19 +16,22 @@ If you want to connect Kitsu to OpenPype you have to set the `Server` url in Kit This setting is available for all the users of the OpenPype instance. ## Synchronize -Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu projects with `-proj, --project` and create/delete/update OP assets. +Updating OP with Kitsu data is executed running the `sync-service`, which requires to provide your Kitsu credentials with `-l, --login` and `-p, --password` or by setting the environment variables `KITSU_LOGIN` and `KITSU_PWD`. This process will request data from Kitsu and create/delete/update OP assets. Once this sync is done, the thread will automatically start a loop to listen to Kitsu events. -The args for `-proj, --project` accept multiple project name, `-proj *` to sync all active projects, and the default value to start a loop to listen to Kitsu events only without any sync. +- `-prj, --project` This flag accepts multiple project name to sync specific projects, and the default to sync all projects. +- `-lo, --listen-only` This flag to run listen to Kitsu events only without any sync. + +Note: You must use one argument of `-pro` or `-lo`, because the listen only flag override syncing flag. ```bash -// sync specific projects then run listen -openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj project_name01 -proj project_name02 - // sync all projects then run listen -openpype_console module kitsu sync-service -l me@domain.ext -p my_password -proj * +openpype_console module kitsu sync-service -l me@domain.ext -p my_password + +// sync specific projects then run listen +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -prj project_name01 -prj project_name02 // start listen only -openpype_console module kitsu sync-service -l me@domain.ext -p my_password +openpype_console module kitsu sync-service -l me@domain.ext -p my_password -lo ``` ### Events listening From c5dd4d21eed2b7d9b67ecdb980a291742c050e7c Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:16:23 +0300 Subject: [PATCH 076/200] Add listen-only flag to sync --- openpype/modules/kitsu/kitsu_module.py | 27 +++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index bd8ade62d8..319b5de16b 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -124,13 +124,6 @@ def push_to_zou(login, password): @cli_main.command() -@click.option( - "-prj", - "--project", - multiple=True, - default=[""], - help="Sync specific kitsu projects" -) @click.option( "-l", "--login", @@ -143,16 +136,32 @@ def push_to_zou(login, password): envvar="KITSU_PWD", help="Password for kitsu username" ) -def sync_service(login, password, project): +@click.option( + "-prj", + "--project", + multiple=True, + default=[], + help="Sync specific kitsu projects" +) +@click.option( + "-lo", + "--listen_only/--listen-only", + default=False, + help="Listen to events only without any syncing" +) +def sync_service(login, password, project, listen_only): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password project (str): specific kitsu projects + listen_only (bool): run listen only without any syncing """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners - sync_all_projects(login, password, filter_projects=project) + if not listen_only: + sync_all_projects(login, password, filter_projects=project) + start_listeners(login, password) From 44e533ff3306aa218a25960a1a41062e3fa82b5a Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Wed, 3 May 2023 20:20:43 +0300 Subject: [PATCH 077/200] Update sync_all_projects function with filtered projects --- .../modules/kitsu/utils/update_op_with_zou.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 40e4191508..bfb4bd58fa 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -367,7 +367,7 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, login (str): Kitsu user login password (str): Kitsu user password ignore_projects (list): List of unsynced project names - filter_projects (list): List of filter project names to sync with + filter_projects (tuple): Tuple of filter project names to sync with Raises: gazu.exception.AuthFailedException: Wrong user login and/or password """ @@ -385,15 +385,7 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, project_to_sync = [] - if not filter_projects: - # listen only - return - - if '*' in filter_projects: - # all projects - project_to_sync = all_projects - - else: + if filter_projects: all_kitsu_projects = {p['name']: p for p in all_projects} for proj_name in filter_projects: if proj_name in all_kitsu_projects: @@ -401,6 +393,9 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, else: log.info(f'`{proj_name}` project does not exist in Kitsu.' f' Please make sure the project is spelled correctly.') + else: + # all project + project_to_sync = all_projects for project in project_to_sync: if ignore_projects and project["name"] in ignore_projects: From 8242e61ad893843f4539e6a1517e50aa4e785de2 Mon Sep 17 00:00:00 2001 From: Michael reda Date: Fri, 5 May 2023 13:54:24 +0200 Subject: [PATCH 078/200] update linting --- openpype/modules/kitsu/kitsu_module.py | 18 ++----- .../modules/kitsu/utils/update_op_with_zou.py | 54 ++++++++++--------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index 319b5de16b..dec19989ea 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -94,7 +94,7 @@ class KitsuModule(OpenPypeModule, IPluginPaths, ITrayAction): return { "publish": [os.path.join(current_dir, "plugins", "publish")], - "actions": [os.path.join(current_dir, "actions")] + "actions": [os.path.join(current_dir, "actions")], } def cli(self, click_group): @@ -124,30 +124,22 @@ def push_to_zou(login, password): @cli_main.command() +@click.option("-l", "--login", envvar="KITSU_LOGIN", help="Kitsu login") @click.option( - "-l", - "--login", - envvar="KITSU_LOGIN", - help="Kitsu login" -) -@click.option( - "-p", - "--password", - envvar="KITSU_PWD", - help="Password for kitsu username" + "-p", "--password", envvar="KITSU_PWD", help="Password for kitsu username" ) @click.option( "-prj", "--project", multiple=True, default=[], - help="Sync specific kitsu projects" + help="Sync specific kitsu projects", ) @click.option( "-lo", "--listen_only/--listen-only", default=False, - help="Listen to events only without any syncing" + help="Listen to events only without any syncing", ) def sync_service(login, password, project, listen_only): """Synchronize openpype database from Zou sever database. diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index bfb4bd58fa..4ad08bb739 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -94,9 +94,7 @@ def update_op_assets( if not item_doc: # Create asset op_asset = create_op_asset(item) insert_result = dbcon.insert_one(op_asset) - item_doc = get_asset_by_id( - project_name, insert_result.inserted_id - ) + item_doc = get_asset_by_id(project_name, insert_result.inserted_id) # Update asset item_data = deepcopy(item_doc["data"]) @@ -210,10 +208,10 @@ def update_op_assets( item.get("entity_type_id") if item_type == "Asset" else None - # Else, fallback on usual hierarchy - or item.get("parent_id") - or item.get("episode_id") - or item.get("source_id") + # Else, fallback on usual hierarchy + or item.get("parent_id") + or item.get("episode_id") + or item.get("source_id") ) # Substitute item type for general classification (assets or shots) @@ -329,7 +327,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "code": project_code, "fps": float(project["fps"]), "zou_id": project["id"], - "active": project['project_status_name'] != "Closed", + "active": project["project_status_name"] != "Closed", } ) @@ -350,7 +348,7 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: "config.tasks": { t["name"]: {"short_name": t.get("short_name", t["name"])} for t in gazu.task.all_task_types_for_project(project) - or gazu.task.all_task_types() + or gazu.task.all_task_types() }, "data": project_data, } @@ -358,8 +356,11 @@ def write_project_to_op(project: dict, dbcon: AvalonMongoDB) -> UpdateOne: ) -def sync_all_projects(login: str, password: str, ignore_projects: list = None, - filter_projects: list = None +def sync_all_projects( + login: str, + password: str, + ignore_projects: list = None, + filter_projects: list = None, ): """Update all OP projects in DB with Zou data. @@ -386,13 +387,15 @@ def sync_all_projects(login: str, password: str, ignore_projects: list = None, project_to_sync = [] if filter_projects: - all_kitsu_projects = {p['name']: p for p in all_projects} + all_kitsu_projects = {p["name"]: p for p in all_projects} for proj_name in filter_projects: if proj_name in all_kitsu_projects: project_to_sync.append(all_kitsu_projects[proj_name]) else: - log.info(f'`{proj_name}` project does not exist in Kitsu.' - f' Please make sure the project is spelled correctly.') + log.info( + f"`{proj_name}` project does not exist in Kitsu." + f" Please make sure the project is spelled correctly." + ) else: # all project project_to_sync = all_projects @@ -424,14 +427,13 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): # Get all statuses for projects from Kitsu all_status = gazu.project.all_project_status() for status in all_status: - if project['project_status_id'] == status['id']: - project['project_status_name'] = status['name'] + if project["project_status_id"] == status["id"]: + project["project_status_name"] = status["name"] break # Do not sync closed kitsu project that is not found in openpype - if ( - project['project_status_name'] == "Closed" - and not get_project(project['name']) + if project["project_status_name"] == "Closed" and not get_project( + project["name"] ): return @@ -460,7 +462,7 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): log.info("Project created: {}".format(project_name)) bulk_writes.append(write_project_to_op(project, dbcon)) - if project['project_status_name'] == "Closed": + if project["project_status_name"] == "Closed": return # Try to find project document @@ -521,12 +523,12 @@ def sync_project_from_kitsu(dbcon: AvalonMongoDB, project: dict): [ UpdateOne({"_id": id}, update) for id, update in update_op_assets( - dbcon, - project, - project_dict, - all_entities, - zou_ids_and_asset_docs, - ) + dbcon, + project, + project_dict, + all_entities, + zou_ids_and_asset_docs, + ) ] ) From 07cf84e6481d013e79493e3d87c0189ab9ecf2cd Mon Sep 17 00:00:00 2001 From: Michael reda Date: Fri, 5 May 2023 14:14:12 +0200 Subject: [PATCH 079/200] add variable name to `@click.option` --- openpype/modules/kitsu/kitsu_module.py | 11 +++++++---- openpype/modules/kitsu/utils/update_op_with_zou.py | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/modules/kitsu/kitsu_module.py b/openpype/modules/kitsu/kitsu_module.py index dec19989ea..8d2d5ccd60 100644 --- a/openpype/modules/kitsu/kitsu_module.py +++ b/openpype/modules/kitsu/kitsu_module.py @@ -131,29 +131,32 @@ def push_to_zou(login, password): @click.option( "-prj", "--project", + "projects", multiple=True, default=[], help="Sync specific kitsu projects", ) @click.option( "-lo", - "--listen_only/--listen-only", + "--listen-only", + "listen_only", + is_flag=True, default=False, help="Listen to events only without any syncing", ) -def sync_service(login, password, project, listen_only): +def sync_service(login, password, projects, listen_only): """Synchronize openpype database from Zou sever database. Args: login (str): Kitsu user login password (str): Kitsu user password - project (str): specific kitsu projects + projects (tuple): specific kitsu projects listen_only (bool): run listen only without any syncing """ from .utils.update_op_with_zou import sync_all_projects from .utils.sync_service import start_listeners if not listen_only: - sync_all_projects(login, password, filter_projects=project) + sync_all_projects(login, password, filter_projects=projects) start_listeners(login, password) diff --git a/openpype/modules/kitsu/utils/update_op_with_zou.py b/openpype/modules/kitsu/utils/update_op_with_zou.py index 4ad08bb739..b495cd1bea 100644 --- a/openpype/modules/kitsu/utils/update_op_with_zou.py +++ b/openpype/modules/kitsu/utils/update_op_with_zou.py @@ -360,7 +360,7 @@ def sync_all_projects( login: str, password: str, ignore_projects: list = None, - filter_projects: list = None, + filter_projects: tuple = None, ): """Update all OP projects in DB with Zou data. From a5efddf96926cd2a3137dc9a5ef6e7fd272d3326 Mon Sep 17 00:00:00 2001 From: Michael <71185790+Michaelredaa@users.noreply.github.com> Date: Fri, 5 May 2023 15:47:39 +0300 Subject: [PATCH 080/200] Update docs to be more specific MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: FΓ©lix David --- website/docs/module_kitsu.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/module_kitsu.md b/website/docs/module_kitsu.md index 970bfb275e..9695542723 100644 --- a/website/docs/module_kitsu.md +++ b/website/docs/module_kitsu.md @@ -30,7 +30,7 @@ openpype_console module kitsu sync-service -l me@domain.ext -p my_password // sync specific projects then run listen openpype_console module kitsu sync-service -l me@domain.ext -p my_password -prj project_name01 -prj project_name02 -// start listen only +// start listen only for all projects openpype_console module kitsu sync-service -l me@domain.ext -p my_password -lo ``` From 86fcab6f8d423fd6d719f5ba50f63e72e278e056 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 15:20:25 +0800 Subject: [PATCH 081/200] refractor the creator for custom modifiers --- .../hosts/max/plugins/create/create_redshift_proxy.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index 1bddbdafae..8c71feb40f 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -12,13 +12,8 @@ class CreateRedshiftProxy(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt - sel_obj = list(rt.selection) - instance = super(CreateRedshiftProxy, self).create( + + _ = super(CreateRedshiftProxy, self).create( subset_name, instance_data, pre_create_data) # type: CreatedInstance - container = rt.getNodeByName(instance.data.get("instance_node")) - if self.selected_nodes: - sel_obj = list(self.selected_nodes) - for obj in sel_obj: - obj.parent = container From e45098c2841c2390c42d7b46f2d1688a5220fa0a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 15:21:27 +0800 Subject: [PATCH 082/200] hound fix --- openpype/hosts/max/plugins/create/create_redshift_proxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_redshift_proxy.py b/openpype/hosts/max/plugins/create/create_redshift_proxy.py index 8c71feb40f..698ea82b69 100644 --- a/openpype/hosts/max/plugins/create/create_redshift_proxy.py +++ b/openpype/hosts/max/plugins/create/create_redshift_proxy.py @@ -11,7 +11,6 @@ class CreateRedshiftProxy(plugin.MaxCreator): icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt _ = super(CreateRedshiftProxy, self).create( subset_name, From 5e9ae9153b9af411ff2102102d09356d87d8208c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 8 May 2023 21:45:52 +0800 Subject: [PATCH 083/200] 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 084/200] 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 085/200] 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 086/200] 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 6252e4b6c5c57e2a0f9f60fd0ad53cdcb7d26c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 10 May 2023 10:07:12 +0200 Subject: [PATCH 087/200] skipping if python script doesn't exist --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 0f959b8f54..8015a15de8 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -24,8 +24,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): # Test script path exists python_script_path = Path(python_script_path) if not python_script_path.exists(): - raise ValueError( - f"Python script {python_script_path} doesn't exist." + raise self.log.warning( + f"Python script {python_script_path} doesn't exist. Skipped..." ) if "--" in self.launch_context.launch_args: From 52ad442a9cacdd7675bb49c9fbcd144a2400df51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Wed, 10 May 2023 10:10:12 +0200 Subject: [PATCH 088/200] lint --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 8015a15de8..2d1b773c5f 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -25,7 +25,8 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): python_script_path = Path(python_script_path) if not python_script_path.exists(): raise self.log.warning( - f"Python script {python_script_path} doesn't exist. Skipped..." + f"Python script {python_script_path} doesn't exist. " + "Skipped..." ) if "--" in self.launch_context.launch_args: From 9a6ae240e2fbafe693701cb791656e44e3440d74 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 May 2023 17:00:29 +0800 Subject: [PATCH 089/200] using currentfile for redshift renderer --- .../hosts/max/plugins/publish/collect_render.py | 6 ++++-- .../plugins/publish/submit_max_deadline.py | 16 +++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index b040467522..0d4dbc4521 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -5,7 +5,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name -from openpype.hosts.max.api.lib import get_max_version +from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -38,7 +38,8 @@ class CollectRender(pyblish.api.InstancePlugin): version_doc = get_last_version_by_subset_name(project_name, instance.name, asset_id) - + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] self.log.debug("version_doc: {0}".format(version_doc)) version_int = 1 if version_doc: @@ -59,6 +60,7 @@ class CollectRender(pyblish.api.InstancePlugin): "source": filepath, "expectedFiles": render_layer_files, "plugin": "3dsmax", + "renderer": renderer, "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], "version": version_int, diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index c728b6b9c7..0cf4990428 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -14,7 +14,6 @@ from openpype.pipeline import ( ) from openpype.settings import get_project_settings from openpype.hosts.max.api.lib import ( - get_current_renderer, get_multipass_setting ) from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -157,6 +156,12 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return plugin_payload + def from_published_scene(self, replace_in_path=True): + instance = self._instance + if instance.data["renderer"]== "Redshift_renderer": + file_path = self.scene_path + return file_path + def process_submission(self): instance = self._instance @@ -185,6 +190,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance job_info = copy.deepcopy(self.job_info) plugin_info = copy.deepcopy(self.plugin_info) + if instance.data["renderer"] == "Redshift_Renderer": + self.log.debug("Using Redshift...published scene wont be used..") plugin_data = {} project_setting = get_project_settings( legacy_io.Session["AVALON_PROJECT"] @@ -202,7 +209,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(expected_files[0]) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - filepath = self.from_published_scene() + filepath = self.scene_path def _clean_name(path): return os.path.splitext(os.path.basename(path))[0] @@ -214,9 +221,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, output_beauty = output_beauty.replace("\\", "/") plugin_data["RenderOutput"] = output_beauty - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] - if renderer in [ + if instance.data["renderer"] in [ "ART_Renderer", "Redshift_Renderer", "V_Ray_6_Hotfix_3", @@ -227,6 +232,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, render_elem_list = RenderSettings().get_render_element() for i, element in enumerate(render_elem_list): element = element.replace(orig_scene, new_scene) + element = element.replace("\\", "/") plugin_data["RenderElementOutputFilename%d" % i] = element # noqa self.log.debug("plugin data:{}".format(plugin_data)) From be386a36880a7868a4e46ba42fd49bfa08df6905 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 May 2023 17:05:56 +0800 Subject: [PATCH 090/200] hound fix --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 0cf4990428..a66d4d630a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -158,7 +158,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def from_published_scene(self, replace_in_path=True): instance = self._instance - if instance.data["renderer"]== "Redshift_renderer": + if instance.data["renderer"] == "Redshift_renderer": file_path = self.scene_path return file_path From 0ea7b25c4675f84181cd4635ae5d74e2b18b39fd Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 14:50:12 +0100 Subject: [PATCH 091/200] refactor: rt.execute(saveNodes) replaces with pymxs function - changed the function to no longer use the selection and instead feed it the nodes directly from get_all_children function. - removed maintained_seclection() as we're no longer overriding the selection of the Max scene. - black also used to format. --- .../plugins/publish/extract_max_scene_raw.py | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index c14fcdbd0b..0f1f6f5b3b 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import get_all_children -class ExtractMaxSceneRaw(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Raw Max Scene with SaveSelected """ @@ -20,9 +13,7 @@ class ExtractMaxSceneRaw(publish.Extractor, order = pyblish.api.ExtractorOrder - 0.2 label = "Extract Max Scene (Raw)" hosts = ["max"] - families = ["camera", - "maxScene", - "model"] + families = ["camera", "maxScene", "model"] optional = True def process(self, instance): @@ -37,26 +28,21 @@ class ExtractMaxSceneRaw(publish.Extractor, filename = "{name}.max".format(**instance.data) max_path = os.path.join(stagingdir, filename) - self.log.info("Writing max file '%s' to '%s'" % (filename, - max_path)) + self.log.info("Writing max file '%s' to '%s'" % (filename, max_path)) if "representations" not in instance.data: instance.data["representations"] = [] - # saving max scene - with maintained_selection(): - # need to figure out how to select the camera - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'saveNodes selection "{max_path}" quiet:true') + nodes = get_all_children(rt.getNodeByName(container)) + rt.saveNodes(nodes, max_path, quiet=True) self.log.info("Performing Extraction ...") representation = { - 'name': 'max', - 'ext': 'max', - 'files': filename, + "name": "max", + "ext": "max", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - max_path)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, max_path)) From 67cd145ce2cca0b0979eb017813713159eb413ed Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 15:05:00 +0100 Subject: [PATCH 092/200] refactor: replaced rt.export string with proper pymxs implementation - black used for formatting - moved the general flow around as each function call is now seperate instead of large string --- .../max/plugins/publish/extract_camera_abc.py | 45 ++++++------------- 1 file changed, 14 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 8c23ff9878..3ca72abd88 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractCameraAlembic(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with AlembicExport """ @@ -38,38 +31,28 @@ class ExtractCameraAlembic(publish.Extractor, path = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.StartFrame = {start} -AlembicExport.EndFrame = {end} -AlembicExport.CustomAttributes = true - -exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = True with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.exportFile(path, selectedOnly=True, using="AlembicExport", noPrompt=True) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - path)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, path)) From 8f5b14ad243153953e273a686453a2b50ee4a329 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 15:13:13 +0100 Subject: [PATCH 093/200] refactor: replaced rt.execute with pymxs implementation --- .../max/plugins/publish/extract_camera_fbx.py | 50 ++++++------------- 1 file changed, 14 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 7e92f355ed..c216e726dc 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractCameraFbx(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Camera with FbxExporter """ @@ -33,43 +26,28 @@ class ExtractCameraFbx(publish.Extractor, filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - self.log.info("Writing fbx file '%s' to '%s'" % (filename, - filepath)) + self.log.info("Writing fbx file '%s' to '%s'" % (filename, filepath)) - # Need to export: - # Animation = True - # Cameras = True - # AxisConversionMethod - fbx_export_cmd = ( - f""" - -FBXExporterSetParam "Animation" true -FBXExporterSetParam "Cameras" true -FBXExporterSetParam "AxisConversionMethod" "Animation" -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP - - """) - - self.log.debug(f"Executing command: {fbx_export_cmd}") + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(fbx_export_cmd) + rt.exportFile(filepath, selectedOnly=True, using="FBXEXP", noPrompt=True) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': filename, + "name": "fbx", + "ext": "fbx", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) From 63c463f618cbc8a94053117b02461cc4d67ae838 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 15:20:59 +0100 Subject: [PATCH 094/200] refactor: replaced rt.execute with proper pymxs --- .../max/plugins/publish/extract_model.py | 49 +++++++------------ 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 710ad5f97d..23fe59954c 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModel(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in Alembic Format """ @@ -36,39 +29,31 @@ class ExtractModel(publish.Extractor, filepath = os.path.join(stagingdir, filename) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (filename, - stagingdir)) + self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.CustomAttributes = true -AlembicExport.UVs = true -AlembicExport.VertexColors = true -AlembicExport.PreserveInstances = true - -exportFile @"{filepath}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {export_cmd}") + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = True + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_cmd) + rt.exportFile( + filepath, selectedOnly=True, using="AlembicExport", noPrompt=True + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': filename, + "name": "abc", + "ext": "abc", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) From 28078c0508598f7413dd9851a35f3d56f3ce05a1 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 15:48:55 +0100 Subject: [PATCH 095/200] refactor: replaced rt.execute where possible --- .../max/plugins/publish/extract_model_fbx.py | 55 +++++++------------ 1 file changed, 19 insertions(+), 36 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index ce58e8cc17..e2bbac4ac2 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModelFbx(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in FBX Format """ @@ -33,42 +26,32 @@ class ExtractModelFbx(publish.Extractor, stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) - filepath = os.path.join(stagingdir, - filename) - self.log.info("Writing FBX '%s' to '%s'" % (filepath, - stagingdir)) - - export_fbx_cmd = ( - f""" -FBXExporterSetParam "Animation" false -FBXExporterSetParam "Cameras" false -FBXExporterSetParam "Lights" false -FBXExporterSetParam "PointCache" false -FBXExporterSetParam "AxisConversionMethod" "Animation" -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true - -exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP - - """) - - self.log.debug(f"Executing command: {export_fbx_cmd}") + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing FBX '%s' to '%s'" % (filepath, stagingdir)) with maintained_selection(): + rt.FBXExporterSetParam("Animation", False) + rt.FBXExporterSetParam("Cameras", False) + rt.FBXExporterSetParam("Lights", False) + rt.FBXExporterSetParam("PointCache", False) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(export_fbx_cmd) + rt.execute( + f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP' + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'fbx', - 'ext': 'fbx', - 'files': filename, + "name": "fbx", + "ext": "fbx", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) From 1d7edf1fb2982353a2be3ea3405b20c1fb9479a4 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:00:17 +0100 Subject: [PATCH 096/200] fix: pymxs terrible argument handling - noPrompt doesn't seem to work unless you call rt.name and is also positional - using doesn't work as a string you need to feed it the actual rt object --- .../hosts/max/plugins/publish/extract_model.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 23fe59954c..56e791d2e7 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -31,18 +31,17 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): # We run the render self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.CustomAttributes = True - rt.AlembicExport.UVs = True - rt.AlembicExport.VertexColors = True - rt.AlembicExport.PreserveInstances = True - with maintained_selection(): + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = True + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - filepath, selectedOnly=True, using="AlembicExport", noPrompt=True + filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport ) self.log.info("Performing Extraction ...") From b3b07cec7cc772e15bdf03510cb8c1ba2808a5e9 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:04:38 +0100 Subject: [PATCH 097/200] fix: exportFile to use correct arguments --- .../max/plugins/publish/extract_camera_abc.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 3ca72abd88..db96470f17 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -33,16 +33,17 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): # We run the render self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.StartFrame = start - rt.AlembicExport.EndFrame = end - rt.AlembicExport.CustomAttributes = True - with maintained_selection(): + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = True # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.exportFile(path, selectedOnly=True, using="AlembicExport", noPrompt=True) + rt.exportFile( + path, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: From 4f2951b6ec64bacdaec96e20693436aecbd89dad Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:07:12 +0100 Subject: [PATCH 098/200] fix: rt.exportfile args --- .../max/plugins/publish/extract_camera_fbx.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index c216e726dc..16dea0b41e 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -28,16 +28,17 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) self.log.info("Writing fbx file '%s' to '%s'" % (filename, filepath)) - rt.FBXExporterSetParam("Animation", True) - rt.FBXExporterSetParam("Cameras", True) - rt.FBXExporterSetParam("AxisConversionMethod", "Animation") - rt.FBXExporterSetParam("UpAxis", "Y") - rt.FBXExporterSetParam("Preserveinstances", True) - with maintained_selection(): + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.exportFile(filepath, selectedOnly=True, using="FBXEXP", noPrompt=True) + rt.exportFile( + filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.FBXEXP + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: From 069bcba72f459245fecb025870f92537a4e6b1f7 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:16:57 +0100 Subject: [PATCH 099/200] fix: exportfile args --- .../max/plugins/publish/extract_model_obj.py | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 7bda237880..3d98f37263 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -1,18 +1,11 @@ import os import pyblish.api -from openpype.pipeline import ( - publish, - OptionalPyblishPluginMixin -) +from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children -class ExtractModelObj(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): """ Extract Geometry in OBJ Format """ @@ -33,27 +26,26 @@ class ExtractModelObj(publish.Extractor, stagingdir = self.staging_dir(instance) filename = "{name}.obj".format(**instance.data) - filepath = os.path.join(stagingdir, - filename) - self.log.info("Writing OBJ '%s' to '%s'" % (filepath, - stagingdir)) + filepath = os.path.join(stagingdir, filename) + self.log.info("Writing OBJ '%s' to '%s'" % (filepath, stagingdir)) with maintained_selection(): # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:ObjExp') # noqa + rt.exportFile( + filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.ObjExp + ) self.log.info("Performing Extraction ...") if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'obj', - 'ext': 'obj', - 'files': filename, + "name": "obj", + "ext": "obj", + "files": filename, "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, - filepath)) + self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) From cc23df7a13cf86074e28fcdcab0559e2506e3012 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:17:11 +0100 Subject: [PATCH 100/200] refactor: replaced rt.execute with proper function --- openpype/hosts/max/plugins/publish/extract_model_fbx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index e2bbac4ac2..0ffec94a59 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -39,8 +39,8 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): rt.FBXExporterSetParam("Preserveinstances", True) # select and export rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute( - f'exportFile @"{filepath}" #noPrompt selectedOnly:true using:FBXEXP' + rt.exportFile( + filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.FBXEXP ) self.log.info("Performing Extraction ...") From dc39fafffd0cd0a5bb3940e9115e7d35e28eaec6 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:23:32 +0100 Subject: [PATCH 101/200] refactor: removed use of rt.execute and replaced with pymxs --- .../max/plugins/publish/extract_pointcache.py | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 75d8a7972c..0936a149f3 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -41,10 +41,7 @@ import os import pyblish.api from openpype.pipeline import publish from pymxs import runtime as rt -from openpype.hosts.max.api import ( - maintained_selection, - get_all_children -) +from openpype.hosts.max.api import maintained_selection, get_all_children class ExtractAlembic(publish.Extractor): @@ -66,35 +63,26 @@ class ExtractAlembic(publish.Extractor): path = os.path.join(parent_dir, file_name) # We run the render - self.log.info("Writing alembic '%s' to '%s'" % (file_name, - parent_dir)) - - abc_export_cmd = ( - f""" -AlembicExport.ArchiveType = #ogawa -AlembicExport.CoordinateSystem = #maya -AlembicExport.StartFrame = {start} -AlembicExport.EndFrame = {end} - -exportFile @"{path}" #noPrompt selectedOnly:on using:AlembicExport - - """) - - self.log.debug(f"Executing command: {abc_export_cmd}") + self.log.info("Writing alembic '%s' to '%s'" % (file_name, parent_dir)) with maintained_selection(): + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end # select and export - rt.select(get_all_children(rt.getNodeByName(container))) - rt.execute(abc_export_cmd) + rt.exportFile( + path, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport + ) if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'abc', - 'ext': 'abc', - 'files': file_name, + "name": "abc", + "ext": "abc", + "files": file_name, "stagingDir": parent_dir, } instance.data["representations"].append(representation) From b229b992a2a8dcbaa9865be2b3c423d757c30fbb Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 16:29:04 +0100 Subject: [PATCH 102/200] refactor: removed configs from maintained_selection() block --- .../max/plugins/publish/extract_camera_abc.py | 11 ++++++----- .../max/plugins/publish/extract_camera_fbx.py | 11 ++++++----- .../hosts/max/plugins/publish/extract_model.py | 13 +++++++------ .../max/plugins/publish/extract_model_fbx.py | 15 ++++++++------- .../max/plugins/publish/extract_pointcache.py | 9 +++++---- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index db96470f17..32d9ab9317 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -33,12 +33,13 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): # We run the render self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + rt.AlembicExport.CustomAttributes = True + with maintained_selection(): - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.StartFrame = start - rt.AlembicExport.EndFrame = end - rt.AlembicExport.CustomAttributes = True # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index 16dea0b41e..f865f7ac6a 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -28,12 +28,13 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) self.log.info("Writing fbx file '%s' to '%s'" % (filename, filepath)) + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) + with maintained_selection(): - rt.FBXExporterSetParam("Animation", True) - rt.FBXExporterSetParam("Cameras", True) - rt.FBXExporterSetParam("AxisConversionMethod", "Animation") - rt.FBXExporterSetParam("UpAxis", "Y") - rt.FBXExporterSetParam("Preserveinstances", True) # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index 56e791d2e7..d4d59df29c 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -31,13 +31,14 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): # We run the render self.log.info("Writing alembic '%s' to '%s'" % (filename, stagingdir)) + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.CustomAttributes = True + rt.AlembicExport.UVs = True + rt.AlembicExport.VertexColors = True + rt.AlembicExport.PreserveInstances = True + with maintained_selection(): - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.CustomAttributes = True - rt.AlembicExport.UVs = True - rt.AlembicExport.VertexColors = True - rt.AlembicExport.PreserveInstances = True # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index 0ffec94a59..eefe5e7e72 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -29,14 +29,15 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): filepath = os.path.join(stagingdir, filename) self.log.info("Writing FBX '%s' to '%s'" % (filepath, stagingdir)) + rt.FBXExporterSetParam("Animation", False) + rt.FBXExporterSetParam("Cameras", False) + rt.FBXExporterSetParam("Lights", False) + rt.FBXExporterSetParam("PointCache", False) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) + with maintained_selection(): - rt.FBXExporterSetParam("Animation", False) - rt.FBXExporterSetParam("Cameras", False) - rt.FBXExporterSetParam("Lights", False) - rt.FBXExporterSetParam("PointCache", False) - rt.FBXExporterSetParam("AxisConversionMethod", "Animation") - rt.FBXExporterSetParam("UpAxis", "Y") - rt.FBXExporterSetParam("Preserveinstances", True) # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 0936a149f3..84352b489e 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -65,11 +65,12 @@ class ExtractAlembic(publish.Extractor): # We run the render self.log.info("Writing alembic '%s' to '%s'" % (file_name, parent_dir)) + rt.AlembicExport.ArchiveType = rt.name("ogawa") + rt.AlembicExport.CoordinateSystem = rt.name("maya") + rt.AlembicExport.StartFrame = start + rt.AlembicExport.EndFrame = end + with maintained_selection(): - rt.AlembicExport.ArchiveType = rt.name("ogawa") - rt.AlembicExport.CoordinateSystem = rt.name("maya") - rt.AlembicExport.StartFrame = start - rt.AlembicExport.EndFrame = end # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( From 4a128cc59b0f974181805dac69252fa4c559e916 Mon Sep 17 00:00:00 2001 From: JackP Date: Wed, 17 May 2023 18:06:33 +0100 Subject: [PATCH 103/200] refactor: updated black to be 79 charlines --- openpype/hosts/max/plugins/publish/extract_camera_abc.py | 5 ++++- openpype/hosts/max/plugins/publish/extract_camera_fbx.py | 9 +++++++-- .../hosts/max/plugins/publish/extract_max_scene_raw.py | 4 +++- openpype/hosts/max/plugins/publish/extract_model.py | 9 +++++++-- openpype/hosts/max/plugins/publish/extract_model_fbx.py | 9 +++++++-- openpype/hosts/max/plugins/publish/extract_model_obj.py | 9 +++++++-- openpype/hosts/max/plugins/publish/extract_pointcache.py | 5 ++++- 7 files changed, 39 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_camera_abc.py b/openpype/hosts/max/plugins/publish/extract_camera_abc.py index 32d9ab9317..6b3bb178a3 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_abc.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_abc.py @@ -43,7 +43,10 @@ class ExtractCameraAlembic(publish.Extractor, OptionalPyblishPluginMixin): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - path, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, ) self.log.info("Performing Extraction ...") diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py index f865f7ac6a..4b4b349e19 100644 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py @@ -38,7 +38,10 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.FBXEXP + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.FBXEXP, ) self.log.info("Performing Extraction ...") @@ -52,4 +55,6 @@ class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py index 0f1f6f5b3b..f0c2aff7f3 100644 --- a/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py +++ b/openpype/hosts/max/plugins/publish/extract_max_scene_raw.py @@ -45,4 +45,6 @@ class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin): "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, max_path)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, max_path) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model.py b/openpype/hosts/max/plugins/publish/extract_model.py index d4d59df29c..4c7c98e2cc 100644 --- a/openpype/hosts/max/plugins/publish/extract_model.py +++ b/openpype/hosts/max/plugins/publish/extract_model.py @@ -42,7 +42,10 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, ) self.log.info("Performing Extraction ...") @@ -56,4 +59,6 @@ class ExtractModel(publish.Extractor, OptionalPyblishPluginMixin): "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_model_fbx.py index eefe5e7e72..e6ccb24cdd 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_model_fbx.py @@ -41,7 +41,10 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.FBXEXP + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.FBXEXP, ) self.log.info("Performing Extraction ...") @@ -55,4 +58,6 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): "stagingDir": stagingdir, } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_model_obj.py b/openpype/hosts/max/plugins/publish/extract_model_obj.py index 3d98f37263..ed3d68c990 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_obj.py +++ b/openpype/hosts/max/plugins/publish/extract_model_obj.py @@ -33,7 +33,10 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - filepath, rt.name("noPrompt"), selectedOnly=True, using=rt.ObjExp + filepath, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.ObjExp, ) self.log.info("Performing Extraction ...") @@ -48,4 +51,6 @@ class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s" % (instance.name, filepath)) + self.log.info( + "Extracted instance '%s' to: %s" % (instance.name, filepath) + ) diff --git a/openpype/hosts/max/plugins/publish/extract_pointcache.py b/openpype/hosts/max/plugins/publish/extract_pointcache.py index 84352b489e..8658cecb1b 100644 --- a/openpype/hosts/max/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/max/plugins/publish/extract_pointcache.py @@ -74,7 +74,10 @@ class ExtractAlembic(publish.Extractor): # select and export rt.select(get_all_children(rt.getNodeByName(container))) rt.exportFile( - path, rt.name("noPrompt"), selectedOnly=True, using=rt.AlembicExport + path, + rt.name("noPrompt"), + selectedOnly=True, + using=rt.AlembicExport, ) if "representations" not in instance.data: From 2e7b0b9d954c606220f8ad89fdf86549dc38e3ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Fri, 19 May 2023 18:20:03 +0200 Subject: [PATCH 104/200] fix raise --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 2d1b773c5f..525905f1ab 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -24,7 +24,7 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): # Test script path exists python_script_path = Path(python_script_path) if not python_script_path.exists(): - raise self.log.warning( + self.log.warning( f"Python script {python_script_path} doesn't exist. " "Skipped..." ) From dc7373408f8d586f854b99da7c9eb799441a3fec Mon Sep 17 00:00:00 2001 From: Felix David Date: Mon, 22 May 2023 10:39:30 +0200 Subject: [PATCH 105/200] continue if not python script path --- openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py index 525905f1ab..559e9ae0ce 100644 --- a/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py +++ b/openpype/hosts/blender/hooks/pre_add_run_python_script_arg.py @@ -28,6 +28,7 @@ class AddPythonScriptToLaunchArgs(PreLaunchHook): f"Python script {python_script_path} doesn't exist. " "Skipped..." ) + continue if "--" in self.launch_context.launch_args: # Insert before separator From 14360f02176b3eb5fbb5a1bc9ba7ab54e33e6bd0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 22 May 2023 18:11:13 +0100 Subject: [PATCH 106/200] Changed name and path of the camera levels to fix problem with sequencer --- openpype/hosts/unreal/api/__init__.py | 4 + openpype/hosts/unreal/api/pipeline.py | 130 ++++++++++++ .../hosts/unreal/plugins/load/load_camera.py | 192 +++++++----------- .../hosts/unreal/plugins/load/load_layout.py | 152 ++------------ 4 files changed, 228 insertions(+), 250 deletions(-) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index de0fce13d5..ac6a91eae9 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -22,6 +22,8 @@ from .pipeline import ( show_tools_popup, instantiate, UnrealHost, + set_sequence_hierarchy, + generate_sequence, maintained_selection ) @@ -41,5 +43,7 @@ __all__ = [ "show_tools_popup", "instantiate", "UnrealHost", + "set_sequence_hierarchy", + "generate_sequence", "maintained_selection" ] diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index bb45fa8c01..5030e8ee86 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -9,12 +9,14 @@ import time import pyblish.api +from openpype.client import get_asset_by_name, get_assets from openpype.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, deregister_loader_plugin_path, deregister_creator_plugin_path, AYON_CONTAINER_ID, + legacy_io, ) from openpype.tools.utils import host_tools import openpype.hosts.unreal @@ -512,6 +514,134 @@ def get_subsequences(sequence: unreal.LevelSequence): return [] +def set_sequence_hierarchy( + seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths +): + # Get existing sequencer tracks or create them if they don't exist + tracks = seq_i.get_master_tracks() + subscene_track = None + visibility_track = None + for t in tracks: + if t.get_class() == unreal.MovieSceneSubTrack.static_class(): + subscene_track = t + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t + if not subscene_track: + subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) + if not visibility_track: + visibility_track = seq_i.add_master_track( + unreal.MovieSceneLevelVisibilityTrack) + + # Create the sub-scene section + subscenes = subscene_track.get_sections() + subscene = None + for s in subscenes: + if s.get_editor_property('sub_sequence') == seq_j: + subscene = s + break + if not subscene: + subscene = subscene_track.add_section() + subscene.set_row_index(len(subscene_track.get_sections())) + subscene.set_editor_property('sub_sequence', seq_j) + subscene.set_range( + min_frame_j, + max_frame_j + 1) + + # Create the visibility section + ar = unreal.AssetRegistryHelpers.get_asset_registry() + maps = [] + for m in map_paths: + # Unreal requires to load the level to get the map name + unreal.EditorLevelLibrary.save_all_dirty_levels() + unreal.EditorLevelLibrary.load_level(m) + maps.append(str(ar.get_asset_by_object_path(m).asset_name)) + + vis_section = visibility_track.add_section() + index = len(visibility_track.get_sections()) + + vis_section.set_range( + min_frame_j, + max_frame_j + 1) + vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) + vis_section.set_row_index(index) + vis_section.set_level_names(maps) + + if min_frame_j > 1: + hid_section = visibility_track.add_section() + hid_section.set_range( + 1, + min_frame_j) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + if max_frame_j < max_frame_i: + hid_section = visibility_track.add_section() + hid_section.set_range( + max_frame_j + 1, + max_frame_i + 1) + hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) + hid_section.set_row_index(index) + hid_section.set_level_names(maps) + + +def generate_sequence(h, h_dir): + tools = unreal.AssetToolsHelpers().get_asset_tools() + + sequence = tools.create_asset( + asset_name=h, + package_path=h_dir, + asset_class=unreal.LevelSequence, + factory=unreal.LevelSequenceFactoryNew() + ) + + project_name = legacy_io.active_project() + asset_data = get_asset_by_name( + project_name, + h_dir.split('/')[-1], + fields=["_id", "data.fps"] + ) + + start_frames = [] + end_frames = [] + + elements = list(get_assets( + project_name, + parent_ids=[asset_data["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + for e in elements: + start_frames.append(e.get('data').get('clipIn')) + end_frames.append(e.get('data').get('clipOut')) + + elements.extend(get_assets( + project_name, + parent_ids=[e["_id"]], + fields=["_id", "data.clipIn", "data.clipOut"] + )) + + min_frame = min(start_frames) + max_frame = max(end_frames) + + sequence.set_display_rate( + unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + sequence.set_playback_start(min_frame) + sequence.set_playback_end(max_frame) + + tracks = sequence.get_master_tracks() + track = None + for t in tracks: + if (t.get_class() == + unreal.MovieSceneCameraCutTrack.static_class()): + track = t + break + if not track: + track = sequence.add_master_track( + unreal.MovieSceneCameraCutTrack) + + return sequence, (min_frame, max_frame) + + @contextmanager def maintained_selection(): """Stub to be either implemented or replaced. diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 1bd398349f..02634104e7 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -6,13 +6,18 @@ import unreal from unreal import EditorAssetLibrary from unreal import EditorLevelLibrary from unreal import EditorLevelUtils -from openpype.client import get_assets, get_asset_by_name +from openpype.client import get_asset_by_name from openpype.pipeline import ( AYON_CONTAINER_ID, legacy_io, ) from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + generate_sequence, + set_sequence_hierarchy, + create_container, + imprint, +) class CameraLoader(plugin.Loader): @@ -24,32 +29,6 @@ class CameraLoader(plugin.Loader): icon = "cube" color = "orange" - def _set_sequence_hierarchy( - self, seq_i, seq_j, min_frame_j, max_frame_j - ): - tracks = seq_i.get_master_tracks() - track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - track = t - break - if not track: - track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - - subscenes = track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == seq_j: - subscene = s - break - if not subscene: - subscene = track.add_section() - subscene.set_row_index(len(track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - def _import_camera( self, world, sequence, bindings, import_fbx_settings, import_filename ): @@ -156,9 +135,9 @@ class CameraLoader(plugin.Loader): if not EditorAssetLibrary.does_asset_exist(master_level): EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") - level = f"{asset_path_parent}/{asset}_map.{asset}_map" + level = f"{asset_dir}/{asset}_map_camera.{asset}_map_camera" if not EditorAssetLibrary.does_asset_exist(level): - EditorLevelLibrary.new_level(f"{asset_path_parent}/{asset}_map") + EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map_camera") EditorLevelLibrary.load_level(master_level) EditorLevelUtils.add_level_to_world( @@ -169,27 +148,13 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(level) - project_name = legacy_io.active_project() - # TODO refactor - # - Creating of hierarchy should be a function in unreal integration - # - it's used in multiple loaders but must not be loader's logic - # - hard to say what is purpose of the loop - # - variables does not match their meaning - # - why scene is stored to sequences? - # - asset documents vs. elements - # - cleanup variable names in whole function - # - e.g. 'asset', 'asset_name', 'asset_data', 'asset_doc' - # - really inefficient queries of asset documents - # - existing asset in scene is considered as "with correct values" - # - variable 'elements' is modified during it's loop # Get all the sequences in the hierarchy. It will create them, if # they don't exist. - sequences = [] frame_ranges = [] - i = 0 - for h in hierarchy_dir_list: + sequences = [] + for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): root_content = EditorAssetLibrary.list_assets( - h, recursive=False, include_folder=False) + h_dir, recursive=False, include_folder=False) existing_sequences = [ EditorAssetLibrary.find_asset_data(asset) @@ -199,48 +164,10 @@ class CameraLoader(plugin.Loader): ] if not existing_sequences: - scene = tools.create_asset( - asset_name=hierarchy[i], - package_path=h, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) + sequence, frame_range = generate_sequence(h, h_dir) - asset_data = get_asset_by_name( - project_name, - h.split('/')[-1], - fields=["_id", "data.fps"] - ) - - start_frames = [] - end_frames = [] - - elements = list(get_assets( - project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) - - elements.extend(get_assets( - project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - scene.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - scene.set_playback_start(min_frame) - scene.set_playback_end(max_frame) - - sequences.append(scene) - frame_ranges.append((min_frame, max_frame)) + sequences.append(sequence) + frame_ranges.append(frame_range) else: for e in existing_sequences: sequences.append(e.get_asset()) @@ -248,8 +175,6 @@ class CameraLoader(plugin.Loader): e.get_asset().get_playback_start(), e.get_asset().get_playback_end())) - i += 1 - EditorAssetLibrary.make_directory(asset_dir) cam_seq = tools.create_asset( @@ -260,19 +185,24 @@ class CameraLoader(plugin.Loader): ) # Add sequences data to hierarchy - for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( + for i in range(len(sequences) - 1): + set_sequence_hierarchy( sequences[i], sequences[i + 1], - frame_ranges[i + 1][0], frame_ranges[i + 1][1]) + frame_ranges[i][1], + frame_ranges[i + 1][0], frame_ranges[i + 1][1], + [level]) + project_name = legacy_io.active_project() data = get_asset_by_name(project_name, asset)["data"] cam_seq.set_display_rate( unreal.FrameRate(data.get("fps"), 1.0)) cam_seq.set_playback_start(data.get('clipIn')) cam_seq.set_playback_end(data.get('clipOut') + 1) - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[-1], cam_seq, - data.get('clipIn'), data.get('clipOut')) + frame_ranges[-1][1], + data.get('clipIn'), data.get('clipOut'), + [level]) settings = unreal.MovieSceneUserImportFBXSettings() settings.set_editor_property('reduce_keys', False) @@ -307,7 +237,7 @@ class CameraLoader(plugin.Loader): key.set_time(unreal.FrameNumber(value=new_time)) # Create Asset Container - unreal_pipeline.create_container( + create_container( container=container_name, path=asset_dir) data = { @@ -322,7 +252,7 @@ class CameraLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container_name), data) EditorLevelLibrary.save_all_dirty_levels() @@ -360,7 +290,7 @@ class CameraLoader(plugin.Loader): sequences = ar.get_assets(filter) filter = unreal.ARFilter( class_names=["World"], - package_paths=[str(Path(asset_dir).parent.as_posix())], + package_paths=[asset_dir], recursive_paths=True) maps = ar.get_assets(filter) @@ -470,7 +400,7 @@ class CameraLoader(plugin.Loader): "representation": str(representation["_id"]), "parent": str(representation["parent"]) } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container.get('container_name')), data) EditorLevelLibrary.save_current_level() @@ -484,15 +414,15 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.load_level(master_level) def remove(self, container): - path = Path(container.get("namespace")) - parent_path = str(path.parent.as_posix()) + asset_dir = container.get('namespace') + path = Path(asset_dir) ar = unreal.AssetRegistryHelpers.get_asset_registry() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], - package_paths=[f"{str(path.as_posix())}"], + package_paths=[asset_dir], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) if not sequences: raise Exception("Could not find sequence.") @@ -500,11 +430,11 @@ class CameraLoader(plugin.Loader): world = ar.get_asset_by_object_path( EditorLevelLibrary.get_editor_world().get_path_name()) - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["World"], - package_paths=[f"{parent_path}"], + package_paths=[asset_dir], recursive_paths=True) - maps = ar.get_assets(filter) + maps = ar.get_assets(_filter) # There should be only one map in the list if not maps: @@ -534,12 +464,18 @@ class CameraLoader(plugin.Loader): root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() + _filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + levels = ar.get_assets(_filter) + master_level = levels[0].get_full_name() sequences = [master_sequence] @@ -547,10 +483,13 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None + visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - break + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: @@ -565,18 +504,45 @@ class CameraLoader(plugin.Loader): ss.set_row_index(i) i += 1 + if visibility_track: + sections = visibility_track.get_sections() + for ss in sections: + if (unreal.Name(f"{container.get('asset')}_map_camera") + in ss.get_level_names()): + visibility_track.remove_section(ss) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for ss in sections: + if prev_name != ss.get_level_names(): + i += 1 + ss.set_row_index(i) + prev_name = ss.get_level_names() if parent: break assert parent, "Could not find the parent sequence" - EditorAssetLibrary.delete_directory(str(path.as_posix())) + # Create a temporary level to delete the layout level. + EditorLevelLibrary.save_all_dirty_levels() + EditorAssetLibrary.make_directory(f"{root}/tmp") + tmp_level = f"{root}/tmp/temp_map" + if not EditorAssetLibrary.does_asset_exist(f"{tmp_level}.temp_map"): + EditorLevelLibrary.new_level(tmp_level) + else: + EditorLevelLibrary.load_level(tmp_level) + + # Delete the layout directory. + EditorAssetLibrary.delete_directory(asset_dir) + + EditorLevelLibrary.load_level(master_level) + EditorAssetLibrary.delete_directory(f"{root}/tmp") # Check if there isn't any more assets in the parent folder, and # delete it if not. asset_content = EditorAssetLibrary.list_assets( - parent_path, recursive=False, include_folder=True + path.parent.as_posix(), recursive=False, include_folder=True ) if len(asset_content) == 0: - EditorAssetLibrary.delete_directory(parent_path) + EditorAssetLibrary.delete_directory(path.parent.as_posix()) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index e5f32c3412..1dfe6f1f5c 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -13,7 +13,7 @@ from unreal import FBXImportType from unreal import MovieSceneLevelVisibilityTrack from unreal import MovieSceneSubTrack -from openpype.client import get_asset_by_name, get_assets, get_representations +from openpype.client import get_asset_by_name, get_representations from openpype.pipeline import ( discover_loader_plugins, loaders_from_representation, @@ -25,7 +25,13 @@ from openpype.pipeline import ( from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_current_project_settings from openpype.hosts.unreal.api import plugin -from openpype.hosts.unreal.api import pipeline as unreal_pipeline +from openpype.hosts.unreal.api.pipeline import ( + generate_sequence, + set_sequence_hierarchy, + create_container, + imprint, + ls, +) class LayoutLoader(plugin.Loader): @@ -91,77 +97,6 @@ class LayoutLoader(plugin.Loader): return None - @staticmethod - def _set_sequence_hierarchy( - seq_i, seq_j, max_frame_i, min_frame_j, max_frame_j, map_paths - ): - # Get existing sequencer tracks or create them if they don't exist - tracks = seq_i.get_master_tracks() - subscene_track = None - visibility_track = None - for t in tracks: - if t.get_class() == unreal.MovieSceneSubTrack.static_class(): - subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t - if not subscene_track: - subscene_track = seq_i.add_master_track(unreal.MovieSceneSubTrack) - if not visibility_track: - visibility_track = seq_i.add_master_track( - unreal.MovieSceneLevelVisibilityTrack) - - # Create the sub-scene section - subscenes = subscene_track.get_sections() - subscene = None - for s in subscenes: - if s.get_editor_property('sub_sequence') == seq_j: - subscene = s - break - if not subscene: - subscene = subscene_track.add_section() - subscene.set_row_index(len(subscene_track.get_sections())) - subscene.set_editor_property('sub_sequence', seq_j) - subscene.set_range( - min_frame_j, - max_frame_j + 1) - - # Create the visibility section - ar = unreal.AssetRegistryHelpers.get_asset_registry() - maps = [] - for m in map_paths: - # Unreal requires to load the level to get the map name - EditorLevelLibrary.save_all_dirty_levels() - EditorLevelLibrary.load_level(m) - maps.append(str(ar.get_asset_by_object_path(m).asset_name)) - - vis_section = visibility_track.add_section() - index = len(visibility_track.get_sections()) - - vis_section.set_range( - min_frame_j, - max_frame_j + 1) - vis_section.set_visibility(unreal.LevelVisibility.VISIBLE) - vis_section.set_row_index(index) - vis_section.set_level_names(maps) - - if min_frame_j > 1: - hid_section = visibility_track.add_section() - hid_section.set_range( - 1, - min_frame_j) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - if max_frame_j < max_frame_i: - hid_section = visibility_track.add_section() - hid_section.set_range( - max_frame_j + 1, - max_frame_i + 1) - hid_section.set_visibility(unreal.LevelVisibility.HIDDEN) - hid_section.set_row_index(index) - hid_section.set_level_names(maps) - def _transform_from_basis(self, transform, basis): """Transform a transform from a basis to a new basis.""" # Get the basis matrix @@ -352,63 +287,6 @@ class LayoutLoader(plugin.Loader): sec_params = section.get_editor_property('params') sec_params.set_editor_property('animation', animation) - @staticmethod - def _generate_sequence(h, h_dir): - tools = unreal.AssetToolsHelpers().get_asset_tools() - - sequence = tools.create_asset( - asset_name=h, - package_path=h_dir, - asset_class=unreal.LevelSequence, - factory=unreal.LevelSequenceFactoryNew() - ) - - project_name = legacy_io.active_project() - asset_data = get_asset_by_name( - project_name, - h_dir.split('/')[-1], - fields=["_id", "data.fps"] - ) - - start_frames = [] - end_frames = [] - - elements = list(get_assets( - project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) - - elements.extend(get_assets( - project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] - )) - - min_frame = min(start_frames) - max_frame = max(end_frames) - - sequence.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) - sequence.set_playback_start(min_frame) - sequence.set_playback_end(max_frame) - - tracks = sequence.get_master_tracks() - track = None - for t in tracks: - if (t.get_class() == - unreal.MovieSceneCameraCutTrack.static_class()): - track = t - break - if not track: - track = sequence.add_master_track( - unreal.MovieSceneCameraCutTrack) - - return sequence, (min_frame, max_frame) - def _get_repre_docs_by_version_id(self, data): version_ids = { element.get("version") @@ -696,7 +574,7 @@ class LayoutLoader(plugin.Loader): ] if not existing_sequences: - sequence, frame_range = self._generate_sequence(h, h_dir) + sequence, frame_range = generate_sequence(h, h_dir) sequences.append(sequence) frame_ranges.append(frame_range) @@ -716,7 +594,7 @@ class LayoutLoader(plugin.Loader): # sequences and frame_ranges have the same length for i in range(0, len(sequences) - 1): - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[i], sequences[i + 1], frame_ranges[i][1], frame_ranges[i + 1][0], frame_ranges[i + 1][1], @@ -729,7 +607,7 @@ class LayoutLoader(plugin.Loader): shot.set_playback_start(0) shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) if sequences: - self._set_sequence_hierarchy( + set_sequence_hierarchy( sequences[-1], shot, frame_ranges[-1][1], data.get('clipIn'), data.get('clipOut'), @@ -745,7 +623,7 @@ class LayoutLoader(plugin.Loader): EditorLevelLibrary.save_current_level() # Create Asset Container - unreal_pipeline.create_container( + create_container( container=container_name, path=asset_dir) data = { @@ -761,7 +639,7 @@ class LayoutLoader(plugin.Loader): "family": context["representation"]["context"]["family"], "loaded_assets": loaded_assets } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container_name), data) asset_content = EditorAssetLibrary.list_assets( @@ -843,7 +721,7 @@ class LayoutLoader(plugin.Loader): "parent": str(representation["parent"]), "loaded_assets": loaded_assets } - unreal_pipeline.imprint( + imprint( "{}/{}".format(asset_dir, container.get('container_name')), data) EditorLevelLibrary.save_current_level() @@ -870,7 +748,7 @@ class LayoutLoader(plugin.Loader): root = "/Game/Ayon" path = Path(container.get("namespace")) - containers = unreal_pipeline.ls() + containers = ls() layout_containers = [ c for c in containers if (c.get('asset_name') != container.get('asset_name') and From 4adf8388b4c08738c99b3f4fd8dfb778fb841dee Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 09:17:54 +0100 Subject: [PATCH 107/200] Fix problem with updating camera --- .../hosts/unreal/plugins/load/load_camera.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 02634104e7..9361ed2ac5 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -345,17 +345,21 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None + visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - break + if (t.get_class() == + unreal.MovieSceneLevelVisibilityTrack.static_class()): + visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: - if ss.get_sequence().get_name() == sequence_name: + if (ss.get_sequence().get_name() == + container.get('asset')): parent = s sub_scene = ss - # subscene_track.remove_section(ss) + subscene_track.remove_section(ss) break sequences.append(ss.get_sequence()) # Update subscenes indexes. @@ -364,10 +368,24 @@ class CameraLoader(plugin.Loader): ss.set_row_index(i) i += 1 + if visibility_track: + sections = visibility_track.get_sections() + for ss in sections: + if (unreal.Name(f"{container.get('asset')}_map") + in ss.get_level_names()): + visibility_track.remove_section(ss) + # Update visibility sections indexes. + i = -1 + prev_name = [] + for ss in sections: + if prev_name != ss.get_level_names(): + i += 1 + ss.set_row_index(i) + prev_name = ss.get_level_names() if parent: break - assert parent, "Could not find the parent sequence" + assert parent, "Could not find the parent sequence" EditorAssetLibrary.delete_asset(level_sequence.get_path_name()) From b7556f76d5cbc61841ec4593c99fa20dd42d7023 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 10:23:14 +0100 Subject: [PATCH 108/200] Fix sequence when updating camera... again --- .../hosts/unreal/plugins/load/load_camera.py | 43 ++++--------------- 1 file changed, 9 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 9361ed2ac5..6f20a466a9 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -270,7 +270,7 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/ayon" + root = "/Game/Ayon" asset_dir = container.get('namespace') @@ -283,16 +283,16 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_current_level() - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[asset_dir], recursive_paths=False) - sequences = ar.get_assets(filter) - filter = unreal.ARFilter( + sequences = ar.get_assets(_filter) + _filter = unreal.ARFilter( class_names=["World"], package_paths=[asset_dir], recursive_paths=True) - maps = ar.get_assets(filter) + maps = ar.get_assets(_filter) # There should be only one map in the list EditorLevelLibrary.load_level(maps[0].get_full_name()) @@ -328,14 +328,13 @@ class CameraLoader(plugin.Loader): # Remove the Level Sequence from the parent. # We need to traverse the hierarchy from the master sequence to find # the level sequence. - root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] - filter = unreal.ARFilter( + _filter = unreal.ARFilter( class_names=["LevelSequence"], package_paths=[f"{root}/{ms_asset}"], recursive_paths=False) - sequences = ar.get_assets(filter) + sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() sequences = [master_sequence] @@ -345,43 +344,19 @@ class CameraLoader(plugin.Loader): for s in sequences: tracks = s.get_master_tracks() subscene_track = None - visibility_track = None for t in tracks: if t.get_class() == unreal.MovieSceneSubTrack.static_class(): subscene_track = t - if (t.get_class() == - unreal.MovieSceneLevelVisibilityTrack.static_class()): - visibility_track = t if subscene_track: sections = subscene_track.get_sections() for ss in sections: - if (ss.get_sequence().get_name() == - container.get('asset')): + if ss.get_sequence().get_name() == sequence_name: parent = s sub_scene = ss - subscene_track.remove_section(ss) break sequences.append(ss.get_sequence()) - # Update subscenes indexes. - i = 0 - for ss in sections: + for i, ss in enumerate(sections): ss.set_row_index(i) - i += 1 - - if visibility_track: - sections = visibility_track.get_sections() - for ss in sections: - if (unreal.Name(f"{container.get('asset')}_map") - in ss.get_level_names()): - visibility_track.remove_section(ss) - # Update visibility sections indexes. - i = -1 - prev_name = [] - for ss in sections: - if prev_name != ss.get_level_names(): - i += 1 - ss.set_row_index(i) - prev_name = ss.get_level_names() if parent: break From bf816562f09dc53510a8834e8e3c5ae9ba8b6597 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 May 2023 10:48:17 +0100 Subject: [PATCH 109/200] Fix old camera not being deleted on update --- .../hosts/unreal/plugins/load/load_camera.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 6f20a466a9..8fd6cc3e80 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -270,17 +270,8 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() - root = "/Game/Ayon" - asset_dir = container.get('namespace') - context = representation.get("context") - - hierarchy = context.get('hierarchy').split("/") - h_dir = f"{root}/{hierarchy[0]}" - h_asset = hierarchy[0] - master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" - EditorLevelLibrary.save_current_level() _filter = unreal.ARFilter( @@ -295,7 +286,7 @@ class CameraLoader(plugin.Loader): maps = ar.get_assets(_filter) # There should be only one map in the list - EditorLevelLibrary.load_level(maps[0].get_full_name()) + EditorLevelLibrary.load_level(maps[0].get_asset().get_path_name()) level_sequence = sequences[0].get_asset() @@ -328,6 +319,7 @@ class CameraLoader(plugin.Loader): # Remove the Level Sequence from the parent. # We need to traverse the hierarchy from the master sequence to find # the level sequence. + root = "/Game/Ayon" namespace = container.get('namespace').replace(f"{root}/", "") ms_asset = namespace.split('/')[0] _filter = unreal.ARFilter( @@ -336,6 +328,12 @@ class CameraLoader(plugin.Loader): recursive_paths=False) sequences = ar.get_assets(_filter) master_sequence = sequences[0].get_asset() + _filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"{root}/{ms_asset}"], + recursive_paths=False) + levels = ar.get_assets(_filter) + master_level = levels[0].get_asset().get_path_name() sequences = [master_sequence] From cccc0acd1dc9c6fd9d00fa56abfaebc1de1d327d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 23 May 2023 22:00:40 +0800 Subject: [PATCH 110/200] 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: Tue, 23 May 2023 15:54:11 +0100 Subject: [PATCH 111/200] Long names fixes. --- .../hosts/maya/plugins/publish/validate_rig_output_ids.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index 499bfd4e37..cba70a21b7 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -55,7 +55,8 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): if shapes: instance_nodes.extend(shapes) - scene_nodes = cmds.ls(type="transform") + cmds.ls(type="mesh") + scene_nodes = cmds.ls(type="transform", long=True) + scene_nodes += cmds.ls(type="mesh", long=True) scene_nodes = set(scene_nodes) - set(instance_nodes) scene_nodes_by_basename = defaultdict(list) @@ -76,7 +77,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): if len(ids) > 1: cls.log.error( "\"{}\" id mismatch to: {}".format( - instance_node.longName(), matches + instance_node, matches ) ) invalid[instance_node] = matches From b5827e8cdfcde48a7a9eff6aa29a21f48541ab66 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 24 May 2023 12:18:05 +0100 Subject: [PATCH 112/200] Fix section range on update camera --- .../hosts/unreal/plugins/load/load_camera.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index d198be29f4..c4fe9df70b 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -387,6 +387,30 @@ class CameraLoader(plugin.Loader): str(representation["data"]["path"]) ) + # Set range of all sections + # Changing the range of the section is not enough. We need to change + # the frame of all the keys in the section. + project_name = legacy_io.active_project() + asset = container.get('asset') + data = get_asset_by_name(project_name, asset)["data"] + + for possessable in new_sequence.get_possessables(): + for tracks in possessable.get_tracks(): + for section in tracks.get_sections(): + section.set_range( + data.get('clipIn'), + data.get('clipOut') + 1) + for channel in section.get_all_channels(): + for key in channel.get_keys(): + old_time = key.get_time().get_editor_property( + 'frame_number') + old_time_value = old_time.get_editor_property( + 'value') + new_time = old_time_value + ( + data.get('clipIn') - data.get('frameStart') + ) + key.set_time(unreal.FrameNumber(value=new_time)) + data = { "representation": str(representation["_id"]), "parent": str(representation["parent"]) From b47143b472eec6bb35ced0bbbb1d2b9a77c9acd4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 17:41:36 +0200 Subject: [PATCH 113/200] collect frames to fix settings --- .../plugins/publish/collect_frames_fix.py | 21 ++++++++++++++----- .../defaults/project_settings/global.json | 8 ++++++- .../schemas/schema_global_publish.json | 20 ++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index bdd49585a5..837738eb06 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -26,11 +26,13 @@ class CollectFramesFixDef( targets = ["local"] hosts = ["nuke"] families = ["render", "prerender"] - enabled = True + + rewrite_version_enable = False def process(self, instance): attribute_values = self.get_attr_values_from_data(instance.data) frames_to_fix = attribute_values.get("frames_to_fix") + rewrite_version = attribute_values.get("rewrite_version") if frames_to_fix: @@ -71,10 +73,19 @@ class CollectFramesFixDef( @classmethod def get_attribute_defs(cls): - return [ + attributes = [ TextDef("frames_to_fix", label="Frames to fix", placeholder="5,10-15", - regex="[0-9,-]+"), - BoolDef("rewrite_version", label="Rewrite latest version", - default=False), + regex="[0-9,-]+") ] + + if cls.rewrite_version_enable: + attributes.append( + BoolDef( + "rewrite_version", + label="Rewrite latest version", + default=False + ) + ) + + return attributes diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 75f335f1de..002e547feb 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -46,6 +46,10 @@ "enabled": false, "families": [] }, + "CollectFramesFixDef": { + "enabled": true, + "rewrite_version_enable": true + }, "ValidateEditorialAssetName": { "enabled": true, "optional": false @@ -252,7 +256,9 @@ } }, { - "families": ["review"], + "families": [ + "review" + ], "hosts": [ "maya", "houdini" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index a7617918a3..8000e6156b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -81,6 +81,26 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "CollectFramesFixDef", + "label": "Collect Frames to Fix", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "rewrite_version_enable", + "label": "Rewrite latest version" + } + ] + }, { "type": "dict", "collapsible": true, From 31da5582fca78f9f84977700c69523e077df6e19 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 25 May 2023 21:39:07 +0200 Subject: [PATCH 114/200] make understandable label in settings --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 8000e6156b..3164cfb62d 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -97,7 +97,7 @@ { "type": "boolean", "key": "rewrite_version_enable", - "label": "Rewrite latest version" + "label": "Show 'Rewrite latest version' toggle" } ] }, From 682d8e6b0551935645724e0b632fb5736448979f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 12:28:45 +0800 Subject: [PATCH 115/200] custom script for setting frame range for read node --- openpype/hosts/nuke/api/lib.py | 30 ++++++ openpype/hosts/nuke/api/pipeline.py | 5 + openpype/widgets/custom_popup.py | 141 ++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 openpype/widgets/custom_popup.py diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index a439142051..1aa0a95c86 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2358,6 +2358,36 @@ class WorkfileSettings(object): # add colorspace menu item self.set_colorspace() + def reset_frame_range_read_nodes(self): + from openpype.widgets import custom_popup + parent = get_main_window() + dialog = custom_popup.CustomScriptDialog(parent=parent) + dialog.setWindowTitle("Frame Range for Read Node") + dialog.set_name("Frame Range: ") + dialog.set_line_edit("%s - %s" % (nuke.root().firstFrame(), + nuke.root().lastFrame())) + frame = dialog.widgets["line_edit"] + selection = dialog.widgets["selection"] + dialog.on_clicked.connect( + lambda: set_frame_range(frame, selection) + ) + def set_frame_range(frame, selection): + frame_range = frame.text() + selected = selection.isChecked() + for read_node in nuke.allNodes("Read"): + if selected: + if not nuke.selectedNodes(): + return + if read_node in nuke.selectedNodes(): + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + else: + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + dialog.show() + + return False + def set_favorites(self): from .utils import set_context_favorites diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index d649ffae7f..8d6be76e48 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -282,6 +282,11 @@ def _install_menu(): lambda: WorkfileSettings().set_context_settings() ) + menu.addSeparator() + menu.addCommand( + "Set Frame Range(Read Node)", + lambda: WorkfileSettings().reset_frame_range_read_nodes() + ) menu.addSeparator() menu.addCommand( "Build Workfile", diff --git a/openpype/widgets/custom_popup.py b/openpype/widgets/custom_popup.py new file mode 100644 index 0000000000..c4bb1ad43b --- /dev/null +++ b/openpype/widgets/custom_popup.py @@ -0,0 +1,141 @@ +import sys +import contextlib + +from PySide2 import QtCore, QtWidgets + + +class CustomScriptDialog(QtWidgets.QDialog): + """A Popup that moves itself to bottom right of screen on show event. + + The UI contains a message label and a red highlighted button to "show" + or perform another custom action from this pop-up. + + """ + + on_clicked = QtCore.Signal() + on_line_changed = QtCore.Signal(str) + + def __init__(self, parent=None, *args, **kwargs): + super(CustomScriptDialog, self).__init__(parent=parent, *args, **kwargs) + self.setContentsMargins(0, 0, 0, 0) + + # Layout + layout = QtWidgets.QVBoxLayout(self) + line_layout = QtWidgets.QHBoxLayout() + line_layout.setContentsMargins(10, 5, 10, 10) + selection_layout = QtWidgets.QHBoxLayout() + selection_layout.setContentsMargins(10, 5, 10, 10) + button_layout = QtWidgets.QHBoxLayout() + button_layout.setContentsMargins(10, 5, 10, 10) + + # Increase spacing slightly for readability + line_layout.setSpacing(10) + button_layout.setSpacing(8) + name = QtWidgets.QLabel("") + name.setStyleSheet(""" + QLabel { + font-size: 12px; + } + """) + line_edit = QtWidgets.QLineEdit("") + selection_name = QtWidgets.QLabel("Use Selection") + selection_name.setStyleSheet(""" + QLabel { + font-size: 12px; + } + """) + has_selection = QtWidgets.QCheckBox() + button = QtWidgets.QPushButton("Execute") + button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + cancel = QtWidgets.QPushButton("Cancel") + cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + + line_layout.addWidget(name) + line_layout.addWidget(line_edit) + selection_layout.addWidget(selection_name) + selection_layout.addWidget(has_selection) + button_layout.addWidget(button) + button_layout.addWidget(cancel) + layout.addLayout(line_layout) + layout.addLayout(selection_layout) + layout.addLayout(button_layout) + # Default size + self.resize(100, 30) + + self.widgets = { + "name": name, + "line_edit": line_edit, + "selection": has_selection, + "button": button, + "cancel": cancel + } + + # Signals + has_selection.toggled.connect(self.emit_click_with_state) + line_edit.textChanged.connect(self.on_line_edit_changed) + button.clicked.connect(self._on_clicked) + cancel.clicked.connect(self.close) + self.update_values() + # Set default title + self.setWindowTitle("Custom Popup") + + def update_values(self): + self.widgets["selection"].isChecked() + + def emit_click_with_state(self): + """Emit the on_clicked signal with the toggled state""" + checked = self.widgets["selection"].isChecked() + return checked + + def set_name(self, name): + self.widgets['name'].setText(name) + + def set_line_edit(self, line_edit): + self.widgets['line_edit'].setText(line_edit) + print(line_edit) + + def setButtonText(self, text): + self.widgets["button"].setText(text) + + def setCancelText(self, text): + self.widgets["cancel"].setText(text) + + def on_line_edit_changed(self): + line_edit = self.widgets['line_edit'].text() + self.on_line_changed.emit(line_edit) + return self.set_line_edit(line_edit) + + + def _on_clicked(self): + """Callback for when the 'show' button is clicked. + + Raises the parent (if any) + + """ + + parent = self.parent() + self.close() + + # Trigger the signal + self.on_clicked.emit() + + if parent: + parent.raise_() + + def showEvent(self, event): + + # Position popup based on contents on show event + return super(CustomScriptDialog, self).showEvent(event) + +@contextlib.contextmanager +def application(): + app = QtWidgets.QApplication(sys.argv) + yield + app.exec_() + +if __name__ == "__main__": + with application(): + dialog = CustomScriptDialog() + dialog.show() From 3ece2a8fcf73fa7d0da438ef056447fd7967c6ee Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 12:56:11 +0800 Subject: [PATCH 116/200] clean up & cosmetic fix --- openpype/hosts/nuke/api/lib.py | 5 ++++- openpype/widgets/custom_popup.py | 12 +++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 1aa0a95c86..94a0ff15ad 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2362,7 +2362,7 @@ class WorkfileSettings(object): from openpype.widgets import custom_popup parent = get_main_window() dialog = custom_popup.CustomScriptDialog(parent=parent) - dialog.setWindowTitle("Frame Range for Read Node") + dialog.setWindowTitle("Frame Range") dialog.set_name("Frame Range: ") dialog.set_line_edit("%s - %s" % (nuke.root().firstFrame(), nuke.root().lastFrame())) @@ -2371,9 +2371,12 @@ class WorkfileSettings(object): dialog.on_clicked.connect( lambda: set_frame_range(frame, selection) ) + def set_frame_range(frame, selection): frame_range = frame.text() selected = selection.isChecked() + if not nuke.allNodes("Read"): + return for read_node in nuke.allNodes("Read"): if selected: if not nuke.selectedNodes(): diff --git a/openpype/widgets/custom_popup.py b/openpype/widgets/custom_popup.py index c4bb1ad43b..85e31d6ce0 100644 --- a/openpype/widgets/custom_popup.py +++ b/openpype/widgets/custom_popup.py @@ -16,7 +16,9 @@ class CustomScriptDialog(QtWidgets.QDialog): on_line_changed = QtCore.Signal(str) def __init__(self, parent=None, *args, **kwargs): - super(CustomScriptDialog, self).__init__(parent=parent, *args, **kwargs) + super(CustomScriptDialog, self).__init__(parent=parent, + *args, + **kwargs) self.setContentsMargins(0, 0, 0, 0) # Layout @@ -30,7 +32,7 @@ class CustomScriptDialog(QtWidgets.QDialog): # Increase spacing slightly for readability line_layout.setSpacing(10) - button_layout.setSpacing(8) + button_layout.setSpacing(10) name = QtWidgets.QLabel("") name.setStyleSheet(""" QLabel { @@ -47,7 +49,7 @@ class CustomScriptDialog(QtWidgets.QDialog): has_selection = QtWidgets.QCheckBox() button = QtWidgets.QPushButton("Execute") button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) + QtWidgets.QSizePolicy.Maximum) cancel = QtWidgets.QPushButton("Cancel") cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) @@ -62,7 +64,7 @@ class CustomScriptDialog(QtWidgets.QDialog): layout.addLayout(selection_layout) layout.addLayout(button_layout) # Default size - self.resize(100, 30) + self.resize(100, 40) self.widgets = { "name": name, @@ -107,7 +109,6 @@ class CustomScriptDialog(QtWidgets.QDialog): self.on_line_changed.emit(line_edit) return self.set_line_edit(line_edit) - def _on_clicked(self): """Callback for when the 'show' button is clicked. @@ -129,6 +130,7 @@ class CustomScriptDialog(QtWidgets.QDialog): # Position popup based on contents on show event return super(CustomScriptDialog, self).showEvent(event) + @contextlib.contextmanager def application(): app = QtWidgets.QApplication(sys.argv) From 54cccc6a6af7f4d3e04e47173b3e302dbc5d2aa0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 12:57:05 +0800 Subject: [PATCH 117/200] hound --- openpype/widgets/custom_popup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/widgets/custom_popup.py b/openpype/widgets/custom_popup.py index 85e31d6ce0..be4b0c32d5 100644 --- a/openpype/widgets/custom_popup.py +++ b/openpype/widgets/custom_popup.py @@ -137,6 +137,7 @@ def application(): yield app.exec_() + if __name__ == "__main__": with application(): dialog = CustomScriptDialog() From 7e02416d30e4d33282a37b27331272701f276d17 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 17:35:13 +0800 Subject: [PATCH 118/200] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 1073a0e19e..19c1048496 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -150,7 +150,7 @@ class RenderProducts(object): def get_arnold_product_name(self, folder, startFrame, endFrame, fmt): """Get all the Arnold AOVs""" - aovs + aov_dict = {} amw = rt.MaxtoAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager @@ -187,7 +187,7 @@ class RenderProducts(object): render_element = render_element.replace("\\", "/") render_dict.update({renderpass: render_element}) - return render_dirname + return render_dict def image_format(self): return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa From e633cc7decbcfb8642f7d66ad17fe4219805e834 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 18:19:50 +0800 Subject: [PATCH 119/200] expected file can get the aov path --- openpype/hosts/max/api/lib_renderproducts.py | 4 +++- .../max/plugins/publish/collect_render.py | 19 ++----------------- .../plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 19c1048496..ba1ffc3a5e 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -70,7 +70,7 @@ class RenderProducts(object): return rgba_render_list, render_elem_list - def get_aov(self): + def get_aovs(self): folder = rt.maxFilePath folder = folder.replace("\\", "/") setting = self._project_settings @@ -177,6 +177,8 @@ class RenderProducts(object): render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() + if render_elem_num < 1: + return # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 652c2e1d2c..c4a44a5b11 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -45,23 +45,8 @@ class CollectRender(pyblish.api.InstancePlugin): } folder = folder.replace("\\", "/") - if aov_list: - if renderer in [ - "ART_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3" - "Redshift_Renderer", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - - render_element = RenderProducts().get_aov() - files_by_aov.update(render_element) - self.log.debug(files_by_aov) - - if renderer == "Arnold": - aovs = RenderProducts().get_aovs() - files_by_aov.update(aovs) + aovs = RenderProducts().get_aovs() + files_by_aov.update(aovs) if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From 2d3ba2af0576d5a201fafa0a0957e18d0aa9d00a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 19:36:14 +0800 Subject: [PATCH 120/200] add _beauty to subset name --- openpype/hosts/max/api/lib_renderproducts.py | 82 +++++++++++++------ .../max/plugins/publish/collect_render.py | 8 +- .../plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index ba1ffc3a5e..a93a1d821d 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -70,16 +70,26 @@ class RenderProducts(object): return rgba_render_list, render_elem_list - def get_aovs(self): + def get_aovs(self, container): folder = rt.maxFilePath + file = rt.maxFileName folder = folder.replace("\\", "/") setting = self._project_settings + render_folder = get_default_render_folder(setting) + filename, ext = os.path.splitext(file) + + output_file = os.path.join(folder, + render_folder, + filename, + container) + setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa startFrame = int(rt.rendStart) endFrame = int(rt.rendEnd) + 1 renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] + render_dict = {} if renderer in [ "ART_Renderer", "Redshift_Renderer", @@ -88,12 +98,22 @@ class RenderProducts(object): "Default_Scanline_Renderer", "Quicksilver_Hardware_Renderer", ]: - render_dict = self.get_render_elements_name( - folder, startFrame, endFrame, img_fmt) + render_name = self.get_render_elements_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) if renderer == "Arnold": - render_dict = self.get_arnold_product_name( - folder, startFrame, endFrame, img_fmt) + render_name = self.get_arnold_product_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_arnold_product( + output_file, name, startFrame, endFrame, img_fmt) + }) return render_dict @@ -148,9 +168,9 @@ class RenderProducts(object): return render_dirname - def get_arnold_product_name(self, folder, startFrame, endFrame, fmt): - """Get all the Arnold AOVs""" - aov_dict = {} + def get_arnold_product_name(self): + """Get all the Arnold AOVs name""" + aov_name = [] amw = rt.MaxtoAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager @@ -161,20 +181,27 @@ class RenderProducts(object): for i in range(aov_group_num): # get the specific AOV group for aov in aov_mgr.drivers[i].aov_list: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{aov.name}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - aov = str(aov.name) - aov_dict.update({aov: render_element}) + aov_name.append(aov.name) + # close the AOVs manager window amw.close() - return aov_dict + return aov_name - def get_render_elements_name(self, folder, startFrame, endFrame, fmt): - """Get all the render element output files. """ - render_dict = {} + def get_expected_arnold_product(self, folder, name, + startFrame, endFrame, fmt): + """Get all the expected Arnold AOVs""" + aov_list = [] + for f in range(startFrame, endFrame): + render_element = f"{folder}_{name}.{f}.{fmt}" + render_element = render_element.replace("\\", "/") + aov_list.append(render_element) + return aov_list + + def get_render_elements_name(self): + """Get all the render element names. """ + render_name = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num < 1: @@ -182,14 +209,21 @@ class RenderProducts(object): # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") - if renderlayer_name.enabled: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{renderpass}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - render_dict.update({renderpass: render_element}) + if renderlayer_name.enabled or "Cryptomatte" in renderlayer_name: + target, renderpass = str(renderlayer_name).split(":") + render_name.append(renderpass) + return render_name - return render_dict + def get_expected_render_elements(self, folder, name, + startFrame, endFrame, fmt): + """Get all the expected render element output files. """ + render_elements = [] + for f in range(startFrame, endFrame): + render_element = f"{folder}_{name}.{f}.{fmt}" + render_element = render_element.replace("\\", "/") + render_elements.append(render_element) + + return render_elements def image_format(self): return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index c4a44a5b11..1282c9b3fe 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -41,11 +41,11 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "_": beauty_list + "beauty": beauty_list } folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs() + aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: @@ -78,8 +78,8 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] data = { - "subset": instance.name, "asset": asset, + "subset": str(instance.name), "publish": True, "maxversion": str(get_max_version()), "imageFormat": img_format, @@ -95,5 +95,3 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) - files = instance.data["expectedFiles"] - self.log.debug("expectedFiles: {0}".format(files)) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 68eb0a437d..7133cff058 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) From 4303b281aab9c157d109e034c68d5d8816fd4450 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 19:38:05 +0800 Subject: [PATCH 121/200] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- openpype/hosts/max/plugins/publish/collect_render.py | 4 +--- .../modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a93a1d821d..b33d0c5751 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -103,7 +103,7 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, endFrame, img_fmt) }) if renderer == "Arnold": @@ -112,7 +112,7 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, endFrame, img_fmt) }) return render_dict diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 1282c9b3fe..a21ccf532e 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -6,7 +6,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace -from openpype.hosts.max.api.lib import get_max_version, get_current_renderer +from openpype.hosts.max.api.lib import get_max_version from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -29,8 +29,6 @@ class CollectRender(pyblish.api.InstancePlugin): context.data['currentFile'] = current_file asset = get_current_asset_name() - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] beauty_list, aov_list = RenderProducts().render_product(instance.name) full_render_list = list() if aov_list: diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From c14525f371aa8b8b2a524022f860cede764f7d0d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 26 May 2023 23:13:17 +0800 Subject: [PATCH 122/200] fix the wrong directory for rendering --- .../max/plugins/publish/collect_render.py | 2 +- .../deadline/abstract_submit_deadline.py | 2 +- .../plugins/publish/submit_max_deadline.py | 40 +++++++++++-------- .../plugins/publish/submit_publish_job.py | 6 ++- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index a21ccf532e..c8e407bbe4 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -39,7 +39,7 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "beauty": beauty_list + "max_beauty": beauty_list } folder = folder.replace("\\", "/") diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 558a637e4b..6694f638d6 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -582,7 +582,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): metadata_folder = metadata_folder.replace(orig_scene, new_scene) instance.data["publishRenderMetadataFolder"] = metadata_folder - + self.log.debug(f"MetadataFolder:{metadata_folder}") self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index c678c0fb6e..d2de9160fb 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -132,8 +132,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Add list of expected files to job # --------------------------------- - files = instance.data.get("files") - for filepath in files: + exp = instance.data.get("expectedFiles") + for filepath in self._iter_expected_files(exp): job_info.OutputDirectory += os.path.dirname(filepath) job_info.OutputFilename += os.path.basename(filepath) @@ -162,10 +162,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance filepath = self.scene_path - files = instance.data["files"] + files = instance.data["expectedFiles"] if not files: raise RuntimeError("No Render Elements found!") - output_dir = os.path.dirname(files[0]) + first_file = next(self._iter_expected_files(files)) + output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir instance.data["toBeRenderedOn"] = "deadline" @@ -202,17 +203,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(files[0]) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - filepath = self.scene_path - - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(filepath) - orig_scene = _clean_name(instance.context.data["currentFile"]) - - output_beauty = output_beauty.replace(orig_scene, new_scene) - output_beauty = output_beauty.replace("\\", "/") - plugin_data["RenderOutput"] = output_beauty + files = instance.data["expectedFiles"] + first_file = next(self._iter_expected_files(files)) + rgb_bname = os.path.basename(output_beauty) + dir = os.path.dirname(first_file) + plugin_data["RenderOutput"] = f"{dir}/{rgb_bname}" renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] @@ -226,14 +221,25 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, ]: render_elem_list = RenderSettings().get_render_element() for i, element in enumerate(render_elem_list): - element = element.replace(orig_scene, new_scene) - plugin_data["RenderElementOutputFilename%d" % i] = element # noqa + elem_bname = os.path.basename(element) + new_elem = f"{dir}/{elem_bname}" + plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) return job_info, plugin_info + @staticmethod + def _iter_expected_files(exp): + if isinstance(exp[0], dict): + for _aov, files in exp[0].items(): + for file in files: + yield file + else: + for file in exp: + yield file + @classmethod def get_attribute_defs(cls): defs = super(MaxSubmitDeadline, cls).get_attribute_defs() diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 68eb0a437d..fb0608908f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) @@ -488,11 +488,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) + if aov == "max_beauty": + subset_name = '{}_{}'.format(group_name, cam) else: subset_name = '{}_{}'.format(group_name, cam) else: if aov: subset_name = '{}_{}'.format(group_name, aov) + if aov == "max_beauty": + subset_name = '{}'.format(group_name) else: subset_name = '{}'.format(group_name) From 32562e0b39500996bc25ad2173d401b83d60a48f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 27 May 2023 00:53:40 +0800 Subject: [PATCH 123/200] give beauty name to RGB --- openpype/hosts/max/plugins/publish/collect_render.py | 2 +- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index c8e407bbe4..a21ccf532e 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -39,7 +39,7 @@ class CollectRender(pyblish.api.InstancePlugin): full_render_list = beauty_list files_by_aov = { - "max_beauty": beauty_list + "beauty": beauty_list } folder = folder.replace("\\", "/") diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index fb0608908f..7133cff058 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -488,15 +488,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) - if aov == "max_beauty": - subset_name = '{}_{}'.format(group_name, cam) else: subset_name = '{}_{}'.format(group_name, cam) else: if aov: subset_name = '{}_{}'.format(group_name, aov) - if aov == "max_beauty": - subset_name = '{}'.format(group_name) else: subset_name = '{}'.format(group_name) From 271d017bdb368b6cbfdb95087df6da957da43f4a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 27 May 2023 00:56:11 +0800 Subject: [PATCH 124/200] remove the print function, and set verify to true for payload in publishing job --- openpype/modules/deadline/abstract_submit_deadline.py | 1 - openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 6694f638d6..7938c27233 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -582,7 +582,6 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): metadata_folder = metadata_folder.replace(orig_scene, new_scene) instance.data["publishRenderMetadataFolder"] = metadata_folder - self.log.debug(f"MetadataFolder:{metadata_folder}") self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7133cff058..68eb0a437d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -348,7 +348,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) From b7b8125d70406ef867af53f8a8afe2640e657058 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 26 May 2023 23:23:06 +0200 Subject: [PATCH 125/200] Use .scriptlib for Resolve startup launch script entry point --- .../hooks/pre_resolve_launch_last_workfile.py | 35 ++++++++++++++++ openpype/hosts/resolve/startup.py | 40 +++++++++++++++++++ .../openpype_startup.scriptlib | 22 ++++++++++ openpype/hosts/resolve/utils.py | 8 ++++ 4 files changed, 105 insertions(+) create mode 100644 openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py create mode 100644 openpype/hosts/resolve/startup.py create mode 100644 openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py new file mode 100644 index 0000000000..6db3cc28b2 --- /dev/null +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -0,0 +1,35 @@ +import os + +from openpype.lib import PreLaunchHook + + +class ResolveLaunchLastWorkfile(PreLaunchHook): + """Special hook to open last workfile for Resolve. + + Checks 'start_last_workfile', if set to False, it will not open last + workfile. This property is set explicitly in Launcher. + """ + + # Execute after workfile template copy + order = 10 + app_groups = ["resolve"] + + def execute(self): + if not self.data.get("start_last_workfile"): + self.log.info("It is set to not start last workfile on start.") + return + + last_workfile = self.data.get("last_workfile_path") + if not last_workfile: + self.log.warning("Last workfile was not collected.") + return + + if not os.path.exists(last_workfile): + self.log.info("Current context does not have any workfile yet.") + return + + # Add path to launch environment for the startup script to pick up + self.log.info(f"Setting OPENPYPE_RESOLVE_OPEN_ON_LAUNCH to launch " + f"last workfile: {last_workfile}") + key = "OPENPYPE_RESOLVE_OPEN_ON_LAUNCH" + self.launch_context.env[key] = last_workfile diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py new file mode 100644 index 0000000000..4aeb106ef1 --- /dev/null +++ b/openpype/hosts/resolve/startup.py @@ -0,0 +1,40 @@ +import os + +# Importing this takes a little over a second and thus this means +# that we have about 1.5 seconds delay before the workfile will actually +# be opened at the minimum +import openpype.hosts.resolve.api + + +def launch_menu(): + from openpype.pipeline import install_host + print("Launching Resolve OpenPype menu..") + + # Activate resolve from openpype + install_host(openpype.hosts.resolve.api) + + openpype.hosts.resolve.api.launch_pype_menu() + + +def open_file(path): + # Avoid the need to "install" the host + openpype.hosts.resolve.api.bmdvr = resolve # noqa + openpype.hosts.resolve.api.bmdvf = fusion # noqa + openpype.hosts.resolve.api.open_file(path) + + +def main(): + # Open last workfile + workfile_path = os.environ.get("OPENPYPE_RESOLVE_OPEN_ON_LAUNCH") + if workfile_path: + open_file(workfile_path) + else: + print("No last workfile set to open. Skipping..") + + # Launch OpenPype menu + # TODO: Add a setting to enable/disable this + launch_menu() + + +if __name__ == "__main__": + main() diff --git a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib new file mode 100644 index 0000000000..9fca666d78 --- /dev/null +++ b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib @@ -0,0 +1,22 @@ +-- Run OpenPype's Python launch script for resolve +function file_exists(name) + local f = io.open(name, "r") + return f ~= nil and io.close(f) +end + + +openpype_root = os.getenv("OPENPYPE_ROOT") +if openpype_root ~= nil then + script = openpype_root .. "/openpype/hosts/resolve/startup.py" + script = fusion:MapPath(script) + + if file_exists(script) then + -- We must use RunScript to ensure it runs in a separate + -- process to Resolve itself to avoid a deadlock for + -- certain imports of OpenPype libraries or Qt + print("Running launch script: " .. script) + fusion:RunScript(script) + else + print("Launch script not found at: " .. script) + end +end \ No newline at end of file diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 9a161f4865..e2c8c4a05e 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -50,6 +50,14 @@ def setup(env): src = os.path.join(directory, script) dst = os.path.join(util_scripts_dir, script) + + # TODO: Make this a less hacky workaround + if script == "openpype_startup.scriptlib": + # Handle special case for scriptlib that needs to be a folder + # up from the Comp folder in the Fusion scripts + dst = os.path.join(os.path.dirname(util_scripts_dir), + script) + log.info("Copying `{}` to `{}`...".format(src, dst)) if os.path.isdir(src): shutil.copytree( From 00e89719dd75f465411e120fbb2c37dd8e495b7a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 26 May 2023 23:23:33 +0200 Subject: [PATCH 126/200] Do not prompt save project when not in a project (e.g. on Resolve launch) --- openpype/hosts/resolve/api/workio.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index 5ce73eea53..5966fa6a43 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -43,18 +43,22 @@ def open_file(filepath): """ Loading project """ + + from . import bmdvr + pm = get_project_manager() + page = bmdvr.GetCurrentPage() + if page is not None: + # Save current project only if Resolve has an active page, otherwise + # we consider Resolve being in a pre-launch state (no open UI yet) + project = pm.GetCurrentProject() + print(f"Saving current project: {project}") + pm.SaveProject() + file = os.path.basename(filepath) fname, _ = os.path.splitext(file) dname, _ = fname.split("_v") - - # deal with current project - project = pm.GetCurrentProject() - log.info(f"Test `pm`: {pm}") - pm.SaveProject() - try: - log.info(f"Test `dname`: {dname}") if not set_project_manager_to_folder_name(dname): raise # load project from input path @@ -72,6 +76,7 @@ def open_file(filepath): return False return True + def current_file(): pm = get_project_manager() current_dir = os.getenv("AVALON_WORKDIR") From 56642ac17572ca5c7c12bd97e5e717ce801518f0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:37:25 +0800 Subject: [PATCH 127/200] getting the filename from render settings and add save_scene before all the extractors running --- openpype/hosts/max/api/lib_renderproducts.py | 159 ++++++------------ .../hosts/max/plugins/create/create_render.py | 4 + .../max/plugins/publish/collect_render.py | 26 +-- .../hosts/max/plugins/publish/save_scene.py | 26 +++ .../plugins/publish/submit_max_deadline.py | 81 ++++++++- 5 files changed, 171 insertions(+), 125 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/save_scene.py diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index b33d0c5751..30c3c71cce 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -5,10 +5,8 @@ import os from pymxs import runtime as rt from openpype.hosts.max.api.lib import ( - get_current_renderer, - get_default_render_folder + get_current_renderer ) -from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_project_settings from openpype.pipeline import legacy_io @@ -22,66 +20,30 @@ class RenderProducts(object): legacy_io.Session["AVALON_PROJECT"] ) - def render_product(self, container): - folder = rt.maxFilePath - file = rt.maxFileName - folder = folder.replace("\\", "/") - setting = self._project_settings - render_folder = get_default_render_folder(setting) - filename, ext = os.path.splitext(file) + def get_beauty(self, container): + render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(folder, - render_folder, - filename, + output_file = os.path.join(render_dir, container) - # TODO: change the frame range follows the current render setting + + setting = self._project_settings + img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa + startFrame = int(rt.rendStart) endFrame = int(rt.rendEnd) + 1 - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - rgba_render_list = self.beauty_render_product(output_file, - startFrame, - endFrame, - img_fmt) - - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] - - render_elem_list = None - - if renderer in [ - "ART_Renderer", - "Redshift_Renderer", - "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3", - "Default_Scanline_Renderer", - "Quicksilver_Hardware_Renderer", - ]: - render_elem_list = self.render_elements_product(output_file, - startFrame, - endFrame, - img_fmt) - - if renderer == "Arnold": - render_elem_list = self.arnold_render_product(output_file, - startFrame, - endFrame, - img_fmt) - - return rgba_render_list, render_elem_list + render_dict = { + "beauty": self.get_expected_beauty( + output_file, startFrame, endFrame, img_fmt) + } + return render_dict def get_aovs(self, container): - folder = rt.maxFilePath - file = rt.maxFileName - folder = folder.replace("\\", "/") - setting = self._project_settings - render_folder = get_default_render_folder(setting) - filename, ext = os.path.splitext(file) + render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(folder, - render_folder, - filename, + output_file = os.path.join(render_dir, container) + setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa @@ -90,9 +52,9 @@ class RenderProducts(object): renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] render_dict = {} + if renderer in [ "ART_Renderer", - "Redshift_Renderer", "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3", "Default_Scanline_Renderer", @@ -105,6 +67,23 @@ class RenderProducts(object): name: self.get_expected_render_elements( output_file, name, startFrame, endFrame, img_fmt) }) + if renderer == "Redshift_Renderer": + render_name = self.get_render_elements_name() + if render_name: + rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles + if rs_AovFiles != True and img_fmt == "exr": + for name in render_name: + if name == "RsCryptomatte": + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) + else: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) if renderer == "Arnold": render_name = self.get_arnold_product_name() @@ -114,60 +93,31 @@ class RenderProducts(object): name: self.get_expected_arnold_product( output_file, name, startFrame, endFrame, img_fmt) }) + if renderer in [ + "V_Ray_6_Hotfix_3", + "V_Ray_GPU_6_Hotfix_3" + ]: + if img_fmt !="exr": + render_name = self.get_render_elements_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, startFrame, endFrame, img_fmt) + }) return render_dict - def beauty_render_product(self, folder, startFrame, endFrame, fmt): + def get_expected_beauty(self, folder, startFrame, endFrame, fmt): beauty_frame_range = [] for f in range(startFrame, endFrame): - beauty_output = f"{folder}.{f}.{fmt}" + frame = "%04d" % f + beauty_output = f"{folder}.{frame}.{fmt}" beauty_output = beauty_output.replace("\\", "/") beauty_frame_range.append(beauty_output) return beauty_frame_range - # TODO: Get the arnold render product - def arnold_render_product(self, folder, startFrame, endFrame, fmt): - """Get all the Arnold AOVs""" - aovs = [] - - amw = rt.MaxtoAOps.AOVsManagerWindow() - aov_mgr = rt.renderers.current.AOVManager - # Check if there is any aov group set in AOV manager - aov_group_num = len(aov_mgr.drivers) - if aov_group_num < 1: - return - for i in range(aov_group_num): - # get the specific AOV group - for aov in aov_mgr.drivers[i].aov_list: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{aov.name}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - aovs.append(render_element) - - # close the AOVs manager window - amw.close() - - return aovs - - def render_elements_product(self, folder, startFrame, endFrame, fmt): - """Get all the render element output files. """ - render_dirname = [] - - render_elem = rt.maxOps.GetCurRenderElementMgr() - render_elem_num = render_elem.NumRenderElements() - # get render elements from the renders - for i in range(render_elem_num): - renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") - if renderlayer_name.enabled: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{renderpass}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - render_dirname.append(render_element) - - return render_dirname - def get_arnold_product_name(self): """Get all the Arnold AOVs name""" aov_name = [] @@ -193,14 +143,15 @@ class RenderProducts(object): """Get all the expected Arnold AOVs""" aov_list = [] for f in range(startFrame, endFrame): - render_element = f"{folder}_{name}.{f}.{fmt}" + frame = "%04d" % f + render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") aov_list.append(render_element) return aov_list def get_render_elements_name(self): - """Get all the render element names. """ + """Get all the render element names for general """ render_name = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() @@ -209,9 +160,10 @@ class RenderProducts(object): # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) - if renderlayer_name.enabled or "Cryptomatte" in renderlayer_name: + if renderlayer_name.enabled: target, renderpass = str(renderlayer_name).split(":") render_name.append(renderpass) + return render_name def get_expected_render_elements(self, folder, name, @@ -219,7 +171,8 @@ class RenderProducts(object): """Get all the expected render element output files. """ render_elements = [] for f in range(startFrame, endFrame): - render_element = f"{folder}_{name}.{f}.{fmt}" + frame = "%04d" % f + render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") render_elements.append(render_element) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 68ae5eac72..78e9527bdf 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" +import os from openpype.hosts.max.api import plugin from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -14,6 +15,9 @@ class CreateRender(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt sel_obj = list(rt.selection) + file = rt.maxFileName + filename, _ = os.path.splitext(file) + instance_data["AssetName"] = filename instance = super(CreateRender, self).create( subset_name, instance_data, diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index a21ccf532e..5b3f99b2d0 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -29,19 +29,7 @@ class CollectRender(pyblish.api.InstancePlugin): context.data['currentFile'] = current_file asset = get_current_asset_name() - beauty_list, aov_list = RenderProducts().render_product(instance.name) - full_render_list = list() - if aov_list: - full_render_list.extend(iter(beauty_list)) - full_render_list.extend(iter(aov_list)) - - else: - full_render_list = beauty_list - - files_by_aov = { - "beauty": beauty_list - } - + files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) @@ -67,14 +55,14 @@ class CollectRender(pyblish.api.InstancePlugin): # OCIO config not support in # most of the 3dsmax renderers # so this is currently hard coded - setting = instance.context.data["project_settings"] - image_io = setting["global"]["imageio"] - instance.data["colorspaceConfig"] = image_io["ocio_config"]["filepath"][0] # noqa + # TODO: add options for redshift/vray ocio config + instance.data["colorspaceConfig"] = "" instance.data["colorspaceDisplay"] = "sRGB" - instance.data["colorspaceView"] = "ACES 1.0" + instance.data["colorspaceView"] = "ACES 1.0 SDR-video" instance.data["renderProducts"] = colorspace.ARenderProduct() + instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] - + # also need to get the render dir for coversion data = { "asset": asset, "subset": str(instance.name), @@ -84,7 +72,6 @@ class CollectRender(pyblish.api.InstancePlugin): "family": 'maxrender', "families": ['maxrender'], "source": filepath, - "files": full_render_list, "plugin": "3dsmax", "frameStart": int(rt.rendStart), "frameEnd": int(rt.rendEnd), @@ -93,3 +80,4 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) + self.log.debug("expectedFiles:{0}".format(instance.data["expectedFiles"])) diff --git a/openpype/hosts/max/plugins/publish/save_scene.py b/openpype/hosts/max/plugins/publish/save_scene.py new file mode 100644 index 0000000000..93d97a3de5 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/save_scene.py @@ -0,0 +1,26 @@ +import pyblish.api +import os + + +class SaveCurrentScene(pyblish.api.ContextPlugin): + """Save current scene + + """ + + label = "Save current file" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["max"] + families = ["maxrender", "workfile"] + + def process(self, context): + from pymxs import runtime as rt + folder = rt.maxFilePath + file = rt.maxFileName + current = os.path.join(folder, file) + assert context.data["currentFile"] == current + + if rt.checkForSave(): + self.log.debug("Skipping file save as there " + "are no modifications..") + return + rt.saveMaxFile(current) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index d2de9160fb..15aa521f43 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -78,7 +78,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info.BatchName = src_filename job_info.Plugin = instance.data["plugin"] job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - + job_info.EnableAutoTimeout = True # Deadline requires integers in frame range frames = "{start}-{end}".format( start=int(instance.data["frameStart"]), @@ -207,9 +207,13 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) - plugin_data["RenderOutput"] = f"{dir}/{rgb_bname}" - + beauty_name = f"{dir}/{rgb_bname}" + beauty_name = beauty_name.replace("\\", "/") + plugin_data["RenderOutput"] = beauty_name + # as 3dsmax has version with different languages + plugin_data["Language"] = "ENU" renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] if renderer in [ "ART_Renderer", @@ -223,13 +227,84 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, for i, element in enumerate(render_elem_list): elem_bname = os.path.basename(element) new_elem = f"{dir}/{elem_bname}" + new_elem = new_elem.replace("/", "\\") plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa + self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) return job_info, plugin_info + def from_published_scene(self, replace_in_path=True): + instance = self._instance + workfile_instance = self._get_workfile_instance(instance.context) + if workfile_instance is None: + return + + # determine published path from Anatomy. + template_data = workfile_instance.data.get("anatomyData") + rep = workfile_instance.data["representations"][0] + template_data["representation"] = rep.get("name") + template_data["ext"] = rep.get("ext") + template_data["comment"] = None + + anatomy = instance.context.data['anatomy'] + template_obj = anatomy.templates_obj["publish"]["path"] + template_filled = template_obj.format_strict(template_data) + file_path = os.path.normpath(template_filled) + + self.log.info("Using published scene for render {}".format(file_path)) + + if not os.path.exists(file_path): + self.log.error("published scene does not exist!") + raise + + if not replace_in_path: + return file_path + + # now we need to switch scene in expected files + # because token will now point to published + # scene file and that might differ from current one + def _clean_name(path): + return os.path.splitext(os.path.basename(path))[0] + + new_scene = _clean_name(file_path) + orig_scene = _clean_name(instance.data["AssetName"]) + expected_files = instance.data.get("expectedFiles") + + if isinstance(expected_files[0], dict): + # we have aovs and we need to iterate over them + new_exp = {} + for aov, files in expected_files[0].items(): + replaced_files = [] + for f in files: + replaced_files.append( + str(f).replace(orig_scene, new_scene) + ) + new_exp[aov] = replaced_files + # [] might be too much here, TODO + instance.data["expectedFiles"] = [new_exp] + else: + new_exp = [] + for f in expected_files: + new_exp.append( + str(f).replace(orig_scene, new_scene) + ) + instance.data["expectedFiles"] = new_exp + + metadata_folder = instance.data.get("publishRenderMetadataFolder") + if metadata_folder: + metadata_folder = metadata_folder.replace(orig_scene, + new_scene) + instance.data["publishRenderMetadataFolder"] = metadata_folder + + self.log.info("Scene name was switched {} -> {}".format( + orig_scene, new_scene + )) + + return file_path + @staticmethod def _iter_expected_files(exp): if isinstance(exp[0], dict): From f85051863ccfbd2f54e0f9198c1b3123f08391b3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:44:22 +0800 Subject: [PATCH 128/200] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 15 +++++++++------ .../hosts/max/plugins/publish/collect_render.py | 1 - .../plugins/publish/submit_max_deadline.py | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 30c3c71cce..68090dfefd 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -65,7 +65,8 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() @@ -76,13 +77,15 @@ class RenderProducts(object): if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) else: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) }) if renderer == "Arnold": @@ -95,15 +98,15 @@ class RenderProducts(object): }) if renderer in [ "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3" - ]: + "V_Ray_GPU_6_Hotfix_3"]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, startFrame, + endFrame, img_fmt) # noqa }) return render_dict diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 5b3f99b2d0..9137f8c854 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -80,4 +80,3 @@ class CollectRender(pyblish.api.InstancePlugin): } instance.data.update(data) self.log.info("data: {0}".format(data)) - self.log.debug("expectedFiles:{0}".format(instance.data["expectedFiles"])) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 15aa521f43..4682cc4487 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -207,7 +207,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) - beauty_name = f"{dir}/{rgb_bname}" + beauty_name = f"{dir}/{rgb_bname}" beauty_name = beauty_name.replace("\\", "/") plugin_data["RenderOutput"] = beauty_name # as 3dsmax has version with different languages From b89bb229a0e7bf4c8d8308776d3d288a65bdd387 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:48:09 +0800 Subject: [PATCH 129/200] hound --- openpype/hosts/max/api/lib_renderproducts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 68090dfefd..21c41446a9 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -72,7 +72,7 @@ class RenderProducts(object): render_name = self.get_render_elements_name() if render_name: rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles - if rs_AovFiles != True and img_fmt == "exr": + if rs_AovFiles == False and img_fmt == "exr": for name in render_name: if name == "RsCryptomatte": render_dict.update({ @@ -98,7 +98,8 @@ class RenderProducts(object): }) if renderer in [ "V_Ray_6_Hotfix_3", - "V_Ray_GPU_6_Hotfix_3"]: + "V_Ray_GPU_6_Hotfix_3" + ]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: From f030ca17d8a3a7d979f01f3b56b11732d6dcfb85 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:50:36 +0800 Subject: [PATCH 130/200] hound --- openpype/hosts/max/api/lib_renderproducts.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 21c41446a9..6bd7e5b7b0 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -72,14 +72,14 @@ class RenderProducts(object): render_name = self.get_render_elements_name() if render_name: rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles - if rs_AovFiles == False and img_fmt == "exr": + if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": render_dict.update({ - name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) - }) + name: self.get_expected_render_elements( + output_file, name, startFrame, + endFrame, img_fmt) + }) else: for name in render_name: render_dict.update({ @@ -99,7 +99,7 @@ class RenderProducts(object): if renderer in [ "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" - ]: + ]: if img_fmt !="exr": render_name = self.get_render_elements_name() if render_name: From 44c0f1cf8a52a5eb1b45d43310f2451aa966be01 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 12:51:26 +0800 Subject: [PATCH 131/200] hound --- openpype/hosts/max/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 6bd7e5b7b0..2fbb7e8ff3 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -100,7 +100,7 @@ class RenderProducts(object): "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" ]: - if img_fmt !="exr": + if img_fmt != "exr": render_name = self.get_render_elements_name() if render_name: for name in render_name: From 0fccdaf5f5d46e775863193ff7f3a44e3338eda9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 15:54:55 +0800 Subject: [PATCH 132/200] use expected files instead of files --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 4682cc4487..3fde667dfe 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -197,10 +197,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, else: plugin_data["DisableMultipass"] = 1 - files = instance.data.get("files") + files = instance.data.get("expectedFiles") if not files: raise RuntimeError("No render elements found") - old_output_dir = os.path.dirname(files[0]) + first_file = next(self._iter_expected_files(files)) + old_output_dir = os.path.dirname(first_file) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) files = instance.data["expectedFiles"] From 7820d92dc9fefa090e773e720d3a122989025b5c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 29 May 2023 09:37:26 +0100 Subject: [PATCH 133/200] Add pools as last attributes --- .../maya/plugins/create/create_render.py | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_render.py b/openpype/hosts/maya/plugins/create/create_render.py index 387b7321b9..4681175808 100644 --- a/openpype/hosts/maya/plugins/create/create_render.py +++ b/openpype/hosts/maya/plugins/create/create_render.py @@ -181,16 +181,34 @@ class CreateRender(plugin.Creator): primary_pool = pool_setting["primary_pool"] sorted_pools = self._set_default_pool(list(pools), primary_pool) - cmds.addAttr(self.instance, longName="primaryPool", - attributeType="enum", - enumName=":".join(sorted_pools)) + cmds.addAttr( + self.instance, + longName="primaryPool", + attributeType="enum", + enumName=":".join(sorted_pools) + ) + cmds.setAttr( + "{}.primaryPool".format(self.instance), + 0, + keyable=False, + channelBox=True + ) pools = ["-"] + pools secondary_pool = pool_setting["secondary_pool"] sorted_pools = self._set_default_pool(list(pools), secondary_pool) - cmds.addAttr("{}.secondaryPool".format(self.instance), - attributeType="enum", - enumName=":".join(sorted_pools)) + cmds.addAttr( + self.instance, + longName="secondaryPool", + attributeType="enum", + enumName=":".join(sorted_pools) + ) + cmds.setAttr( + "{}.secondaryPool".format(self.instance), + 0, + keyable=False, + channelBox=True + ) def _create_render_settings(self): """Create instance settings.""" @@ -260,6 +278,12 @@ class CreateRender(plugin.Creator): default_priority) self.data["tile_priority"] = tile_priority + strict_error_checking = maya_submit_dl.get("strict_error_checking", + True) + self.data["strict_error_checking"] = strict_error_checking + + # Pool attributes should be last since they will be recreated when + # the deadline server changes. pool_setting = (self._project_settings["deadline"] ["publish"] ["CollectDeadlinePools"]) @@ -272,9 +296,6 @@ class CreateRender(plugin.Creator): secondary_pool = pool_setting["secondary_pool"] self.data["secondaryPool"] = self._set_default_pool(pool_names, secondary_pool) - strict_error_checking = maya_submit_dl.get("strict_error_checking", - True) - self.data["strict_error_checking"] = strict_error_checking if muster_enabled: self.log.info(">>> Loading Muster credentials ...") From 1fe89ebbb6096245fdebd59a0f16b150cb33e529 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 29 May 2023 09:38:04 +0100 Subject: [PATCH 134/200] Fix getting server settings. --- .../maya/plugins/publish/collect_render.py | 2 +- .../collect_deadline_server_from_instance.py | 41 ++++++++++++++----- .../collect_default_deadline_server.py | 3 +- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 7c47f17acb..babd494758 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -336,7 +336,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): context.data["system_settings"]["modules"]["deadline"] ) if deadline_settings["enabled"]: - data["deadlineUrl"] = render_instance.data.get("deadlineUrl") + data["deadlineUrl"] = render_instance.data["deadlineUrl"] if self.sync_workfile_version: data["version"] = context.data["version"] diff --git a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py index 9981bead3e..2de6073e29 100644 --- a/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py +++ b/openpype/modules/deadline/plugins/publish/collect_deadline_server_from_instance.py @@ -5,23 +5,26 @@ This is resolving index of server lists stored in `deadlineServers` instance attribute or using default server if that attribute doesn't exists. """ +from maya import cmds + import pyblish.api class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): """Collect Deadline Webservice URL from instance.""" - order = pyblish.api.CollectorOrder + 0.415 + # Run before collect_render. + order = pyblish.api.CollectorOrder + 0.005 label = "Deadline Webservice from the Instance" families = ["rendering", "renderlayer"] + hosts = ["maya"] def process(self, instance): instance.data["deadlineUrl"] = self._collect_deadline_url(instance) self.log.info( "Using {} for submission.".format(instance.data["deadlineUrl"])) - @staticmethod - def _collect_deadline_url(render_instance): + def _collect_deadline_url(self, render_instance): # type: (pyblish.api.Instance) -> str """Get Deadline Webservice URL from render instance. @@ -49,8 +52,16 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): default_server = render_instance.context.data["defaultDeadline"] instance_server = render_instance.data.get("deadlineServers") if not instance_server: + self.log.debug("Using default server.") return default_server + # Get instance server as sting. + if isinstance(instance_server, int): + instance_server = cmds.getAttr( + "{}.deadlineServers".format(render_instance.data["objset"]), + asString=True + ) + default_servers = deadline_settings["deadline_urls"] project_servers = ( render_instance.context.data @@ -58,15 +69,23 @@ class CollectDeadlineServerFromInstance(pyblish.api.InstancePlugin): ["deadline"] ["deadline_servers"] ) - deadline_servers = { + if not project_servers: + self.log.debug("Not project servers found. Using default servers.") + return default_servers[instance_server] + + project_enabled_servers = { k: default_servers[k] for k in project_servers if k in default_servers } - # This is Maya specific and may not reflect real selection of deadline - # url as dictionary keys in Python 2 are not ordered - return deadline_servers[ - list(deadline_servers.keys())[ - int(render_instance.data.get("deadlineServers")) - ] - ] + + msg = ( + "\"{}\" server on instance is not enabled in project settings." + " Enabled project servers:\n{}".format( + instance_server, project_enabled_servers + ) + ) + assert instance_server in project_enabled_servers, msg + + self.log.debug("Using project approved server.") + return project_enabled_servers[instance_server] diff --git a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py index cb2b0cf156..1a0d615dc3 100644 --- a/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py +++ b/openpype/modules/deadline/plugins/publish/collect_default_deadline_server.py @@ -17,7 +17,8 @@ class CollectDefaultDeadlineServer(pyblish.api.ContextPlugin): `CollectDeadlineServerFromInstance`. """ - order = pyblish.api.CollectorOrder + 0.410 + # Run before collect_deadline_server_instance. + order = pyblish.api.CollectorOrder + 0.0025 label = "Default Deadline Webservice" pass_mongo_url = False From 71b3242abd277995482a410720a4a84a13818a3a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 29 May 2023 11:41:10 +0100 Subject: [PATCH 135/200] Missing deadlineUrl on instances metadata. --- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 68eb0a437d..22370dea14 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -1089,6 +1089,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): deadline_publish_job_id = \ self._submit_deadline_post_job(instance, render_job, instances) + # Inject deadline url to instances. + for inst in instances: + inst["deadlineUrl"] = self.deadline_url + # publish job file publish_job = { "asset": asset, From a4fd3c7c6dae31509fc37621878aa4600933f2ae Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 11:50:56 +0100 Subject: [PATCH 136/200] Minor refactor --- .../hosts/unreal/plugins/load/load_camera.py | 45 +++++++------------ 1 file changed, 15 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index c4fe9df70b..af7a594b41 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -89,10 +89,7 @@ class CameraLoader(plugin.Loader): hierarchy_dir_list.append(hierarchy_dir) asset = context.get('asset').get('name') suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) - else: - asset_name = "{}".format(name) + asset_name = f"{asset}_{name}" if asset else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() @@ -106,23 +103,15 @@ class CameraLoader(plugin.Loader): # Get highest number to make a unique name folders = [a for a in asset_content if a[-1] == "/" and f"{name}_" in a] - f_numbers = [] - for f in folders: - # Get number from folder name. Splits the string by "_" and - # removes the last element (which is a "/"). - f_numbers.append(int(f.split("_")[-1][:-1])) + # Get number from folder name. Splits the string by "_" and + # removes the last element (which is a "/"). + f_numbers = [int(f.split("_")[-1][:-1]) for f in folders] f_numbers.sort() - if not f_numbers: - unique_number = 1 - else: - unique_number = f_numbers[-1] + 1 + unique_number = f_numbers[-1] + 1 if f_numbers else 1 asset_dir, container_name = tools.create_unique_asset_name( f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="") - asset_path = Path(asset_dir) - asset_path_parent = str(asset_path.parent.as_posix()) - container_name += suffix EditorAssetLibrary.make_directory(asset_dir) @@ -163,17 +152,17 @@ class CameraLoader(plugin.Loader): asset).get_class().get_name() == 'LevelSequence' ] - if not existing_sequences: + if existing_sequences: + for seq in existing_sequences: + sequences.append(seq.get_asset()) + frame_ranges.append(( + seq.get_asset().get_playback_start(), + seq.get_asset().get_playback_end())) + else: sequence, frame_range = generate_sequence(h, h_dir) sequences.append(sequence) frame_ranges.append(frame_range) - else: - for e in existing_sequences: - sequences.append(e.get_asset()) - frame_ranges.append(( - e.get_asset().get_playback_start(), - e.get_asset().get_playback_end())) EditorAssetLibrary.make_directory(asset_dir) @@ -252,8 +241,7 @@ class CameraLoader(plugin.Loader): "parent": context["representation"]["parent"], "family": context["representation"]["context"]["family"] } - imprint( - "{}/{}".format(asset_dir, container_name), data) + imprint(f"{asset_dir}/{container_name}", data) EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(master_level) @@ -415,8 +403,7 @@ class CameraLoader(plugin.Loader): "representation": str(representation["_id"]), "parent": str(representation["parent"]) } - imprint( - "{}/{}".format(asset_dir, container.get('container_name')), data) + imprint(f"{asset_dir}/{container.get('container_name')}", data) EditorLevelLibrary.save_current_level() @@ -514,10 +501,8 @@ class CameraLoader(plugin.Loader): break sequences.append(ss.get_sequence()) # Update subscenes indexes. - i = 0 - for ss in sections: + for i, ss in enumerate(sections): ss.set_row_index(i) - i += 1 if visibility_track: sections = visibility_track.get_sections() From 79a0210c35eda89c355afd0497d2ae14fa9bcb45 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 11:56:03 +0100 Subject: [PATCH 137/200] Save whole hierarchy when loading camera or layout --- openpype/hosts/unreal/plugins/load/load_camera.py | 5 +++-- openpype/hosts/unreal/plugins/load/load_layout.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index af7a594b41..3ed7b055e3 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -246,8 +246,9 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_all_dirty_levels() EditorLevelLibrary.load_level(master_level) + # Save all assets in the hierarchy asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=True + hierarchy_dir_list[0], recursive=True, include_folder=False ) for a in asset_content: @@ -408,7 +409,7 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.save_current_level() asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + f"{root}/{ms_asset}", recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 5a3953f82e..51ca0383e0 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -642,8 +642,10 @@ class LayoutLoader(plugin.Loader): imprint( "{}/{}".format(asset_dir, container_name), data) + save_dir = hierarchy_dir_list[0] if create_sequences else asset_dir + asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + save_dir, recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) @@ -664,11 +666,12 @@ class LayoutLoader(plugin.Loader): asset_dir = container.get('namespace') context = representation.get("context") + hierarchy = context.get('hierarchy').split("/") + sequence = None master_level = None if create_sequences: - hierarchy = context.get('hierarchy').split("/") h_dir = f"{root}/{hierarchy[0]}" h_asset = hierarchy[0] master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" @@ -726,8 +729,10 @@ class LayoutLoader(plugin.Loader): EditorLevelLibrary.save_current_level() + save_dir = f"{root}/{hierarchy[0]}" if create_sequences else asset_dir + asset_content = EditorAssetLibrary.list_assets( - asset_dir, recursive=True, include_folder=False) + save_dir, recursive=True, include_folder=False) for a in asset_content: EditorAssetLibrary.save_asset(a) From ff2c494705f79d21b75dcfc77fb2c1a49b23bae8 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 12:10:23 +0100 Subject: [PATCH 138/200] Set view range in sequencer when creating sequences --- openpype/hosts/unreal/api/pipeline.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 5030e8ee86..72816c9b81 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -623,11 +623,18 @@ def generate_sequence(h, h_dir): min_frame = min(start_frames) max_frame = max(end_frames) + fps = asset_data.get('data').get("fps") + sequence.set_display_rate( - unreal.FrameRate(asset_data.get('data').get("fps"), 1.0)) + unreal.FrameRate(fps, 1.0)) sequence.set_playback_start(min_frame) sequence.set_playback_end(max_frame) + sequence.set_work_range_start(min_frame / fps) + sequence.set_work_range_end(max_frame / fps) + sequence.set_view_range_start(min_frame / fps) + sequence.set_view_range_end(max_frame / fps) + tracks = sequence.get_master_tracks() track = None for t in tracks: From 9d9eebfe09499474dac3504d7c4b252100e8c57d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 19:25:49 +0800 Subject: [PATCH 139/200] using current file to render and add validator for deadline publish in max hosts --- openpype/hosts/max/api/lib_renderproducts.py | 4 +- .../hosts/max/plugins/create/create_render.py | 14 ++++ .../max/plugins/publish/collect_render.py | 9 ++- .../hosts/max/plugins/publish/save_scene.py | 5 -- .../publish/validate_deadline_publish.py | 41 ++++++++++ .../plugins/publish/submit_max_deadline.py | 76 ++----------------- 6 files changed, 72 insertions(+), 77 deletions(-) create mode 100644 openpype/hosts/max/plugins/publish/validate_deadline_publish.py diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 2fbb7e8ff3..7e016f6f15 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -38,7 +38,7 @@ class RenderProducts(object): } return render_dict - def get_aovs(self, container): + def get_aovs(self, container, instance): render_dir = os.path.dirname(rt.rendOutputFilename) output_file = os.path.join(render_dir, @@ -71,7 +71,7 @@ class RenderProducts(object): if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = rt.Redshift_Renderer().SeparateAovFiles + rs_AovFiles = instance.data.get("separateAovFiles") if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 78e9527bdf..3d5ed781a4 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -2,6 +2,7 @@ """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin +from openpype.lib import BoolDef from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -18,6 +19,9 @@ class CreateRender(plugin.MaxCreator): file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename + instance_data["separateAovFiles"] = ( + pre_create_data.get("separateAovFiles")) + instance = super(CreateRender, self).create( subset_name, instance_data, @@ -40,3 +44,13 @@ class CreateRender(plugin.MaxCreator): RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) + + def get_pre_create_attr_defs(self): + attrs = super(CreateRender, self).get_pre_create_attr_defs() + + return attrs + [ + BoolDef("separateAovFiles", + label="Separate Aov Files", + default=False), + + ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 9137f8c854..14d23c42fc 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -6,7 +6,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace -from openpype.hosts.max.api.lib import get_max_version +from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -31,12 +31,14 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs(instance.name) + aovs = RenderProducts().get_aovs(instance.name, instance) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() + instance.data["files"] = list() instance.data["expectedFiles"].append(files_by_aov) + instance.data["files"].append(files_by_aov) img_format = RenderProducts().image_format() project_name = context.data["projectName"] @@ -62,6 +64,8 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["renderProducts"] = colorspace.ARenderProduct() instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] # also need to get the render dir for coversion data = { "asset": asset, @@ -71,6 +75,7 @@ class CollectRender(pyblish.api.InstancePlugin): "imageFormat": img_format, "family": 'maxrender', "families": ['maxrender'], + "renderer": renderer, "source": filepath, "plugin": "3dsmax", "frameStart": int(rt.rendStart), diff --git a/openpype/hosts/max/plugins/publish/save_scene.py b/openpype/hosts/max/plugins/publish/save_scene.py index 93d97a3de5..a40788ab41 100644 --- a/openpype/hosts/max/plugins/publish/save_scene.py +++ b/openpype/hosts/max/plugins/publish/save_scene.py @@ -18,9 +18,4 @@ class SaveCurrentScene(pyblish.api.ContextPlugin): file = rt.maxFileName current = os.path.join(folder, file) assert context.data["currentFile"] == current - - if rt.checkForSave(): - self.log.debug("Skipping file save as there " - "are no modifications..") - return rt.saveMaxFile(current) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py new file mode 100644 index 0000000000..f516e09337 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -0,0 +1,41 @@ +import os +import pyblish.api +from pymxs import runtime as rt +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.hosts.max.api.lib_rendersettings import RenderSettings + + +class ValidateDeadlinePublish(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates Render File Directory is + not the same in every submission + """ + + order = ValidateContentsOrder + families = ["maxrender"] + hosts = ["max"] + label = "Render Output for Deadline" + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + file = rt.maxFileName + filename, ext = os.path.splitext(file) + if filename not in rt.rendOutputFilename: + raise PublishValidationError( + "Directory of RenderOutput doesn't " + "have with the current Max SceneName " + "please repair to rename the folder!" + ) + + @classmethod + def repair(cls, instance): + container= instance.data.get("instance_node") + RenderSettings().render_output(container) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 3fde667dfe..d7ba7107a3 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -133,6 +133,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Add list of expected files to job # --------------------------------- exp = instance.data.get("expectedFiles") + for filepath in self._iter_expected_files(exp): job_info.OutputDirectory += os.path.dirname(filepath) job_info.OutputFilename += os.path.basename(filepath) @@ -204,8 +205,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, old_output_dir = os.path.dirname(first_file) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - files = instance.data["expectedFiles"] - first_file = next(self._iter_expected_files(files)) rgb_bname = os.path.basename(output_beauty) dir = os.path.dirname(first_file) beauty_name = f"{dir}/{rgb_bname}" @@ -231,6 +230,9 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, new_elem = new_elem.replace("/", "\\") plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa + if renderer == "Redshift_Renderer": + plugin_data["redshift_SeparateAovFiles"] = instance.data.get( + "separateAovFiles", False) self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) @@ -239,72 +241,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def from_published_scene(self, replace_in_path=True): instance = self._instance - workfile_instance = self._get_workfile_instance(instance.context) - if workfile_instance is None: - return - - # determine published path from Anatomy. - template_data = workfile_instance.data.get("anatomyData") - rep = workfile_instance.data["representations"][0] - template_data["representation"] = rep.get("name") - template_data["ext"] = rep.get("ext") - template_data["comment"] = None - - anatomy = instance.context.data['anatomy'] - template_obj = anatomy.templates_obj["publish"]["path"] - template_filled = template_obj.format_strict(template_data) - file_path = os.path.normpath(template_filled) - - self.log.info("Using published scene for render {}".format(file_path)) - - if not os.path.exists(file_path): - self.log.error("published scene does not exist!") - raise - - if not replace_in_path: - return file_path - - # now we need to switch scene in expected files - # because token will now point to published - # scene file and that might differ from current one - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(file_path) - orig_scene = _clean_name(instance.data["AssetName"]) - expected_files = instance.data.get("expectedFiles") - - if isinstance(expected_files[0], dict): - # we have aovs and we need to iterate over them - new_exp = {} - for aov, files in expected_files[0].items(): - replaced_files = [] - for f in files: - replaced_files.append( - str(f).replace(orig_scene, new_scene) - ) - new_exp[aov] = replaced_files - # [] might be too much here, TODO - instance.data["expectedFiles"] = [new_exp] - else: - new_exp = [] - for f in expected_files: - new_exp.append( - str(f).replace(orig_scene, new_scene) - ) - instance.data["expectedFiles"] = new_exp - - metadata_folder = instance.data.get("publishRenderMetadataFolder") - if metadata_folder: - metadata_folder = metadata_folder.replace(orig_scene, - new_scene) - instance.data["publishRenderMetadataFolder"] = metadata_folder - - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) - - return file_path + if instance.data["renderer"] == "Redshift_Renderer": + self.log.debug("Using Redshift...published scene wont be used..") + replace_in_path = False + return replace_in_path @staticmethod def _iter_expected_files(exp): From dbb175204adf16e763d2c2295d6ad675988453ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 19:29:35 +0800 Subject: [PATCH 140/200] hound --- openpype/hosts/max/plugins/publish/validate_deadline_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py index f516e09337..c5bc979043 100644 --- a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -37,5 +37,5 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - container= instance.data.get("instance_node") + container = instance.data.get("instance_node") RenderSettings().render_output(container) From eb5b5bf492c69d3369d944ff019f36a6e306b3bd Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 29 May 2023 12:59:54 +0100 Subject: [PATCH 141/200] Set back sequencer and viewport when updating layout or camera --- .../hosts/unreal/plugins/load/load_camera.py | 23 ++++++++++++-- .../hosts/unreal/plugins/load/load_layout.py | 31 ++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_camera.py b/openpype/hosts/unreal/plugins/load/load_camera.py index 3ed7b055e3..59ea14697d 100644 --- a/openpype/hosts/unreal/plugins/load/load_camera.py +++ b/openpype/hosts/unreal/plugins/load/load_camera.py @@ -3,9 +3,12 @@ from pathlib import Path import unreal -from unreal import EditorAssetLibrary -from unreal import EditorLevelLibrary -from unreal import EditorLevelUtils +from unreal import ( + EditorAssetLibrary, + EditorLevelLibrary, + EditorLevelUtils, + LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, +) from openpype.client import get_asset_by_name from openpype.pipeline import ( AYON_CONTAINER_ID, @@ -259,6 +262,13 @@ class CameraLoader(plugin.Loader): def update(self, container, representation): ar = unreal.AssetRegistryHelpers.get_asset_registry() + curr_level_sequence = LevelSequenceLib.get_current_level_sequence() + curr_time = LevelSequenceLib.get_current_time() + is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() + + editor_subsystem = unreal.UnrealEditorSubsystem() + vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() + asset_dir = container.get('namespace') EditorLevelLibrary.save_current_level() @@ -416,6 +426,13 @@ class CameraLoader(plugin.Loader): EditorLevelLibrary.load_level(master_level) + if curr_level_sequence: + LevelSequenceLib.open_level_sequence(curr_level_sequence) + LevelSequenceLib.set_current_time(curr_time) + LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) + + editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) + def remove(self, container): asset_dir = container.get('namespace') path = Path(asset_dir) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 51ca0383e0..86b2e1456c 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -5,13 +5,16 @@ import collections from pathlib import Path import unreal -from unreal import EditorAssetLibrary -from unreal import EditorLevelLibrary -from unreal import EditorLevelUtils -from unreal import AssetToolsHelpers -from unreal import FBXImportType -from unreal import MovieSceneLevelVisibilityTrack -from unreal import MovieSceneSubTrack +from unreal import ( + EditorAssetLibrary, + EditorLevelLibrary, + EditorLevelUtils, + AssetToolsHelpers, + FBXImportType, + MovieSceneLevelVisibilityTrack, + MovieSceneSubTrack, + LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, +) from openpype.client import get_asset_by_name, get_representations from openpype.pipeline import ( @@ -661,6 +664,13 @@ class LayoutLoader(plugin.Loader): ar = unreal.AssetRegistryHelpers.get_asset_registry() + curr_level_sequence = LevelSequenceLib.get_current_level_sequence() + curr_time = LevelSequenceLib.get_current_time() + is_cam_lock = LevelSequenceLib.is_camera_cut_locked_to_viewport() + + editor_subsystem = unreal.UnrealEditorSubsystem() + vp_loc, vp_rot = editor_subsystem.get_level_viewport_camera_info() + root = "/Game/Ayon" asset_dir = container.get('namespace') @@ -742,6 +752,13 @@ class LayoutLoader(plugin.Loader): elif prev_level: EditorLevelLibrary.load_level(prev_level) + if curr_level_sequence: + LevelSequenceLib.open_level_sequence(curr_level_sequence) + LevelSequenceLib.set_current_time(curr_time) + LevelSequenceLib.set_lock_camera_cut_to_viewport(is_cam_lock) + + editor_subsystem.set_level_viewport_camera_info(vp_loc, vp_rot) + def remove(self, container): """ Delete the layout. First, check if the assets loaded with the layout From e506d88ed3d113f608759e883dcdf17dcb6f5ccc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:33:40 +0800 Subject: [PATCH 142/200] style fix --- openpype/hosts/max/plugins/create/create_render.py | 4 +--- .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 3d5ed781a4..6c581cdcf4 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -47,10 +47,8 @@ class CreateRender(plugin.MaxCreator): def get_pre_create_attr_defs(self): attrs = super(CreateRender, self).get_pre_create_attr_defs() - return attrs + [ BoolDef("separateAovFiles", label="Separate Aov Files", - default=False), - + default=False) ] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index d7ba7107a3..b6a30e36b7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -232,7 +232,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if renderer == "Redshift_Renderer": plugin_data["redshift_SeparateAovFiles"] = instance.data.get( - "separateAovFiles", False) + "separateAovFiles") self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) From 5f763a4e8e65d0aeb8e4515e69ec768f6f2c5f4d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:36:38 +0800 Subject: [PATCH 143/200] dont add separate aov as instance data --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++-- openpype/hosts/max/plugins/create/create_render.py | 10 ---------- openpype/hosts/max/plugins/publish/collect_render.py | 2 +- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 7e016f6f15..a6427bf7c5 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -38,7 +38,7 @@ class RenderProducts(object): } return render_dict - def get_aovs(self, container, instance): + def get_aovs(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) output_file = os.path.join(render_dir, @@ -71,7 +71,7 @@ class RenderProducts(object): if renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = instance.data.get("separateAovFiles") + rs_AovFiles = rt.RedShift_Renderer().separateAovFiles if img_fmt == "exr" and not rs_AovFiles: for name in render_name: if name == "RsCryptomatte": diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 6c581cdcf4..be5dece05b 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -19,8 +19,6 @@ class CreateRender(plugin.MaxCreator): file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename - instance_data["separateAovFiles"] = ( - pre_create_data.get("separateAovFiles")) instance = super(CreateRender, self).create( subset_name, @@ -44,11 +42,3 @@ class CreateRender(plugin.MaxCreator): RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) - - def get_pre_create_attr_defs(self): - attrs = super(CreateRender, self).get_pre_create_attr_defs() - return attrs + [ - BoolDef("separateAovFiles", - label="Separate Aov Files", - default=False) - ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 14d23c42fc..77ab5f654d 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -31,7 +31,7 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") - aovs = RenderProducts().get_aovs(instance.name, instance) + aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) if "expectedFiles" not in instance.data: From 982186ffa08618f8d590e894356595f886af0f57 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 20:38:17 +0800 Subject: [PATCH 144/200] remove unused import --- openpype/hosts/max/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index be5dece05b..5ad895b86e 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -2,7 +2,6 @@ """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin -from openpype.lib import BoolDef from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings From bafc085ec19e624c163d6a862b9fcc6e0edab983 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 29 May 2023 22:56:47 +0800 Subject: [PATCH 145/200] updating validator's comment --- .../max/plugins/publish/validate_deadline_publish.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py index c5bc979043..b2f0e863f4 100644 --- a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -30,12 +30,14 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, filename, ext = os.path.splitext(file) if filename not in rt.rendOutputFilename: raise PublishValidationError( - "Directory of RenderOutput doesn't " - "have with the current Max SceneName " - "please repair to rename the folder!" + "Render output folder " + "doesn't match the max scene name! " + "Use Repair action to " + "fix the folder file path.." ) @classmethod def repair(cls, instance): container = instance.data.get("instance_node") RenderSettings().render_output(container) + cls.log.debug("Reset the render output folder...") From a4e9eaf3c38aaa4bb31784067d35d54656ae8333 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Mon, 29 May 2023 23:24:48 +0200 Subject: [PATCH 146/200] Update openpype/hosts/max/plugins/load/load_redshift_proxy.py Co-authored-by: Roy Nieterau --- openpype/hosts/max/plugins/load/load_redshift_proxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_redshift_proxy.py b/openpype/hosts/max/plugins/load/load_redshift_proxy.py index 9451e5299b..31692f6367 100644 --- a/openpype/hosts/max/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/max/plugins/load/load_redshift_proxy.py @@ -10,7 +10,6 @@ from openpype.hosts.max.api import lib class RedshiftProxyLoader(load.LoaderPlugin): - """Load rs files with Redshift Proxy""" label = "Load Redshift Proxy" From 279b3dc767fc870a8632abc625f1e2dbf3706add Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 30 May 2023 12:09:53 +0200 Subject: [PATCH 147/200] Set explicit startup script path --- .../resolve/hooks/pre_resolve_launch_last_workfile.py | 11 +++++++++++ .../utility_scripts/openpype_startup.scriptlib | 7 +++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py index 6db3cc28b2..2ad4352b82 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -1,6 +1,7 @@ import os from openpype.lib import PreLaunchHook +import openpype.hosts.resolve class ResolveLaunchLastWorkfile(PreLaunchHook): @@ -33,3 +34,13 @@ class ResolveLaunchLastWorkfile(PreLaunchHook): f"last workfile: {last_workfile}") key = "OPENPYPE_RESOLVE_OPEN_ON_LAUNCH" self.launch_context.env[key] = last_workfile + + # Set the openpype prelaunch startup script path for easy access + # in the LUA .scriptlib code + op_resolve_root = os.path.dirname(openpype.hosts.resolve.__file__) + script_path = os.path.join(op_resolve_root, "startup.py") + key = "OPENPYPE_RESOLVE_STARTUP_SCRIPT" + self.launch_context.env[key] = script_path + self.log.info("Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: " + f"{script_path}") + diff --git a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib index 9fca666d78..ec9b30a18d 100644 --- a/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib +++ b/openpype/hosts/resolve/utility_scripts/openpype_startup.scriptlib @@ -5,10 +5,9 @@ function file_exists(name) end -openpype_root = os.getenv("OPENPYPE_ROOT") -if openpype_root ~= nil then - script = openpype_root .. "/openpype/hosts/resolve/startup.py" - script = fusion:MapPath(script) +openpype_startup_script = os.getenv("OPENPYPE_RESOLVE_STARTUP_SCRIPT") +if openpype_startup_script ~= nil then + script = fusion:MapPath(openpype_startup_script) if file_exists(script) then -- We must use RunScript to ensure it runs in a separate From 990737623bcfab7c5f7502e3cb4a0f2156f0891c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 30 May 2023 12:20:17 +0200 Subject: [PATCH 148/200] Cosmetics --- openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py index 2ad4352b82..0e27ddb8c3 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_launch_last_workfile.py @@ -43,4 +43,3 @@ class ResolveLaunchLastWorkfile(PreLaunchHook): self.launch_context.env[key] = script_path self.log.info("Setting OPENPYPE_RESOLVE_STARTUP_SCRIPT to: " f"{script_path}") - From 307eca8c28aeb1afeb68ba1e93e63e993c21a084 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 30 May 2023 13:24:17 +0200 Subject: [PATCH 149/200] Cleanup Resolve startup script + add setting for launch menu on start --- openpype/hosts/resolve/startup.py | 46 ++++++++++++++----- .../defaults/project_settings/resolve.json | 1 + .../schema_project_resolve.json | 5 ++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py index 4aeb106ef1..79a64e0fbf 100644 --- a/openpype/hosts/resolve/startup.py +++ b/openpype/hosts/resolve/startup.py @@ -1,26 +1,44 @@ +"""This script is used as a startup script in Resolve through a .scriptlib file + +It triggers directly after the launch of Resolve and it's recommended to keep +it optimized for fast performance since the Resolve UI is actually interactive +while this is running. As such, there's nothing ensuring the user isn't +continuing manually before any of the logic here runs. As such we also try +to delay any imports as much as possible. + +This code runs in a separate process to the main Resolve process. + +""" import os -# Importing this takes a little over a second and thus this means -# that we have about 1.5 seconds delay before the workfile will actually -# be opened at the minimum import openpype.hosts.resolve.api -def launch_menu(): - from openpype.pipeline import install_host - print("Launching Resolve OpenPype menu..") +def ensure_installed_host(): + """Install resolve host with openpype and return the registered host. + + This function can be called multiple times without triggering an + additional install. + """ + from openpype.pipeline import install_host, registered_host + host = registered_host() + if host: + return host - # Activate resolve from openpype install_host(openpype.hosts.resolve.api) + return registered_host() + +def launch_menu(): + print("Launching Resolve OpenPype menu..") + ensure_installed_host() openpype.hosts.resolve.api.launch_pype_menu() def open_file(path): # Avoid the need to "install" the host - openpype.hosts.resolve.api.bmdvr = resolve # noqa - openpype.hosts.resolve.api.bmdvf = fusion # noqa - openpype.hosts.resolve.api.open_file(path) + host = ensure_installed_host() + host.open_file(path) def main(): @@ -32,8 +50,12 @@ def main(): print("No last workfile set to open. Skipping..") # Launch OpenPype menu - # TODO: Add a setting to enable/disable this - launch_menu() + from openpype.settings import get_project_settings + from openpype.pipeline.context_tools import get_current_project_name + project_name = get_current_project_name() + settings = get_project_settings(project_name) + if settings.get("resolve", {}).get("launch_openpype_menu_on_start", True): + launch_menu() if __name__ == "__main__": diff --git a/openpype/settings/defaults/project_settings/resolve.json b/openpype/settings/defaults/project_settings/resolve.json index 264f3bd902..56efa78e89 100644 --- a/openpype/settings/defaults/project_settings/resolve.json +++ b/openpype/settings/defaults/project_settings/resolve.json @@ -1,4 +1,5 @@ { + "launch_openpype_menu_on_start": false, "imageio": { "ocio_config": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json index b326f22394..6f98bdd3bd 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_resolve.json @@ -5,6 +5,11 @@ "label": "DaVinci Resolve", "is_file": true, "children": [ + { + "type": "boolean", + "key": "launch_openpype_menu_on_start", + "label": "Launch OpenPype menu on start of Resolve" + }, { "key": "imageio", "type": "dict", From 5fbae39a745b0016b2ada9d054b696aef2007fee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 30 May 2023 13:32:27 +0200 Subject: [PATCH 150/200] Ftrack: Role names are not case sensitive in ftrack event server status action (#5058) * statuser is not case sensitive about role names * safer role check --- .../ftrack/lib/ftrack_action_handler.py | 23 +++++++++++++++---- .../ftrack/scripts/sub_event_status.py | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/lib/ftrack_action_handler.py b/openpype/modules/ftrack/lib/ftrack_action_handler.py index 07b3a780a2..1be4353b26 100644 --- a/openpype/modules/ftrack/lib/ftrack_action_handler.py +++ b/openpype/modules/ftrack/lib/ftrack_action_handler.py @@ -234,6 +234,10 @@ class BaseAction(BaseHandler): if not settings_roles: return default + user_roles = { + role_name.lower() + for role_name in user_roles + } for role_name in settings_roles: if role_name.lower() in user_roles: return True @@ -264,8 +268,15 @@ class BaseAction(BaseHandler): return user_entity @classmethod - def get_user_roles_from_event(cls, session, event): - """Query user entity from event.""" + def get_user_roles_from_event(cls, session, event, lower=True): + """Get user roles based on data in event. + + Args: + session (ftrack_api.Session): Prepared ftrack session. + event (ftrack_api.event.Event): Event which is processed. + lower (Optional[bool]): Lower the role names. Default 'True'. + """ + not_set = object() user_roles = event["data"].get("user_roles", not_set) @@ -273,7 +284,10 @@ class BaseAction(BaseHandler): user_roles = [] user_entity = cls.get_user_entity_from_event(session, event) for role in user_entity["user_security_roles"]: - user_roles.append(role["security_role"]["name"].lower()) + role_name = role["security_role"]["name"] + if lower: + role_name = role_name.lower() + user_roles.append(role_name) event["data"]["user_roles"] = user_roles return user_roles @@ -322,7 +336,8 @@ class BaseAction(BaseHandler): if not settings.get(self.settings_enabled_key, True): return False - user_role_list = self.get_user_roles_from_event(session, event) + user_role_list = self.get_user_roles_from_event( + session, event, lower=False) if not self.roles_check(settings.get("role_list"), user_role_list): return False return True diff --git a/openpype/modules/ftrack/scripts/sub_event_status.py b/openpype/modules/ftrack/scripts/sub_event_status.py index dc5836e7f2..c6c2e9e1f6 100644 --- a/openpype/modules/ftrack/scripts/sub_event_status.py +++ b/openpype/modules/ftrack/scripts/sub_event_status.py @@ -296,9 +296,9 @@ def server_activity_validate_user(event): if not user_ent: return False - role_list = ["Pypeclub", "Administrator"] + role_list = {"pypeclub", "administrator"} for role in user_ent["user_security_roles"]: - if role["security_role"]["name"] in role_list: + if role["security_role"]["name"].lower() in role_list: return True return False From 4bc61d1c89f1c3adfbbc7ba80fa2fff5f156eec0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 30 May 2023 13:33:58 +0200 Subject: [PATCH 151/200] Fix border widget --- .../publisher/widgets/border_label_widget.py | 125 +++++++++++++----- 1 file changed, 94 insertions(+), 31 deletions(-) diff --git a/openpype/tools/publisher/widgets/border_label_widget.py b/openpype/tools/publisher/widgets/border_label_widget.py index 5617e159cd..1381d74eb3 100644 --- a/openpype/tools/publisher/widgets/border_label_widget.py +++ b/openpype/tools/publisher/widgets/border_label_widget.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +from math import ceil from qtpy import QtWidgets, QtCore, QtGui from openpype.style import get_objected_colors @@ -14,32 +15,44 @@ class _VLineWidget(QtWidgets.QWidget): It is expected that parent widget will set width. """ - def __init__(self, color, left, parent): + def __init__(self, color, line_size, left, parent): super(_VLineWidget, self).__init__(parent) self._color = color self._left = left + self._line_size = line_size + + def set_line_size(self, line_size): + self._line_size = line_size def paintEvent(self, event): if not self.isVisible(): return - if self._left: - pos_x = 0 - else: - pos_x = self.width() + pos_x = self._line_size * 0.5 + if not self._left: + pos_x = self.width() - pos_x + painter = QtGui.QPainter(self) painter.setRenderHints( QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) + if self._color: pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawLine(pos_x, 0, pos_x, self.height()) + painter.drawRect( + QtCore.QRectF( + pos_x, + -self._line_size, + pos_x + (self.width() * 2), + self.height() + (self._line_size * 2) + ) + ) painter.end() @@ -56,34 +69,46 @@ class _HBottomLineWidget(QtWidgets.QWidget): It is expected that parent widget will set height and radius. """ - def __init__(self, color, parent): + def __init__(self, color, line_size, parent): super(_HBottomLineWidget, self).__init__(parent) self._color = color self._radius = 0 + self._line_size = line_size def set_radius(self, radius): self._radius = radius + def set_line_size(self, line_size): + self._line_size = line_size + def paintEvent(self, event): if not self.isVisible(): return - rect = QtCore.QRect( - 0, -self._radius, self.width(), self.height() + self._radius + x_offset = self._line_size * 0.5 + rect = QtCore.QRectF( + x_offset, + -self._radius, + self.width() - (2 * x_offset), + (self.height() + self._radius) - x_offset ) painter = QtGui.QPainter(self) painter.setRenderHints( QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform ) + if self._color: pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawRoundedRect(rect, self._radius, self._radius) + if self._radius: + painter.drawRoundedRect(rect, self._radius, self._radius) + else: + painter.drawRect(rect) painter.end() @@ -102,30 +127,38 @@ class _HTopCornerLineWidget(QtWidgets.QWidget): It is expected that parent widget will set height and radius. """ - def __init__(self, color, left_side, parent): + + def __init__(self, color, line_size, left_side, parent): super(_HTopCornerLineWidget, self).__init__(parent) self._left_side = left_side + self._line_size = line_size self._color = color self._radius = 0 def set_radius(self, radius): self._radius = radius + def set_line_size(self, line_size): + self._line_size = line_size + def paintEvent(self, event): if not self.isVisible(): return - pos_y = self.height() / 2 - + pos_y = self.height() * 0.5 + x_offset = self._line_size * 0.5 if self._left_side: - rect = QtCore.QRect( - 0, pos_y, self.width() + self._radius, self.height() + rect = QtCore.QRectF( + x_offset, + pos_y, + self.width() + self._radius + x_offset, + self.height() ) else: - rect = QtCore.QRect( - -self._radius, + rect = QtCore.QRectF( + (-self._radius), pos_y, - self.width() + self._radius, + (self.width() + self._radius) - x_offset, self.height() ) @@ -138,10 +171,13 @@ class _HTopCornerLineWidget(QtWidgets.QWidget): pen = QtGui.QPen(self._color) else: pen = painter.pen() - pen.setWidth(1) + pen.setWidth(self._line_size) painter.setPen(pen) painter.setBrush(QtCore.Qt.transparent) - painter.drawRoundedRect(rect, self._radius, self._radius) + if self._radius: + painter.drawRoundedRect(rect, self._radius, self._radius) + else: + painter.drawRect(rect) painter.end() @@ -163,8 +199,10 @@ class BorderedLabelWidget(QtWidgets.QFrame): if color_value: color = color_value.get_qcolor() - top_left_w = _HTopCornerLineWidget(color, True, self) - top_right_w = _HTopCornerLineWidget(color, False, self) + line_size = 1 + + top_left_w = _HTopCornerLineWidget(color, line_size, True, self) + top_right_w = _HTopCornerLineWidget(color, line_size, False, self) label_widget = QtWidgets.QLabel(label, self) @@ -175,10 +213,10 @@ class BorderedLabelWidget(QtWidgets.QFrame): top_layout.addWidget(label_widget, 0) top_layout.addWidget(top_right_w, 1) - left_w = _VLineWidget(color, True, self) - right_w = _VLineWidget(color, False, self) + left_w = _VLineWidget(color, line_size, True, self) + right_w = _VLineWidget(color, line_size, False, self) - bottom_w = _HBottomLineWidget(color, self) + bottom_w = _HBottomLineWidget(color, line_size, self) center_layout = QtWidgets.QHBoxLayout() center_layout.setContentsMargins(5, 5, 5, 5) @@ -201,6 +239,7 @@ class BorderedLabelWidget(QtWidgets.QFrame): self._widget = None self._radius = 0 + self._line_size = line_size self._top_left_w = top_left_w self._top_right_w = top_right_w @@ -216,14 +255,38 @@ class BorderedLabelWidget(QtWidgets.QFrame): value, value, value, value ) + def set_line_size(self, line_size): + if self._line_size == line_size: + return + self._line_size = line_size + for widget in ( + self._top_left_w, + self._top_right_w, + self._left_w, + self._right_w, + self._bottom_w + ): + widget.set_line_size(line_size) + self._recalculate_sizes() + def showEvent(self, event): super(BorderedLabelWidget, self).showEvent(event) + self._recalculate_sizes() + def _recalculate_sizes(self): height = self._label_widget.height() - radius = (height + (height % 2)) / 2 + radius = int((height + (height % 2)) / 2) self._radius = radius - side_width = 1 + radius + radius_size = self._line_size + 1 + if radius_size < radius: + radius_size = radius + + if radius: + side_width = self._line_size + radius + else: + side_width = self._line_size + 1 + # Don't use fixed width/height as that would set also set # the other size (When fixed width is set then is also set # fixed height). @@ -231,8 +294,8 @@ class BorderedLabelWidget(QtWidgets.QFrame): self._left_w.setMaximumWidth(side_width) self._right_w.setMinimumWidth(side_width) self._right_w.setMaximumWidth(side_width) - self._bottom_w.setMinimumHeight(radius) - self._bottom_w.setMaximumHeight(radius) + self._bottom_w.setMinimumHeight(radius_size) + self._bottom_w.setMaximumHeight(radius_size) self._bottom_w.set_radius(radius) self._top_right_w.set_radius(radius) self._top_left_w.set_radius(radius) From 26b99db61e4ab17033bfaa5b0f9099a3b6561275 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 30 May 2023 15:18:53 +0200 Subject: [PATCH 152/200] removed unused import --- openpype/tools/publisher/widgets/border_label_widget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/widgets/border_label_widget.py b/openpype/tools/publisher/widgets/border_label_widget.py index 1381d74eb3..e5693368b1 100644 --- a/openpype/tools/publisher/widgets/border_label_widget.py +++ b/openpype/tools/publisher/widgets/border_label_widget.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- -from math import ceil from qtpy import QtWidgets, QtCore, QtGui from openpype.style import get_objected_colors From 2e58cd2e1a1a1dd23e0bc59bc1cbc414fa6d1ec9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 21:44:26 +0800 Subject: [PATCH 153/200] move the custom popup menu to nuke/startup and add the frame setting to Openpype tool menu --- openpype/hosts/nuke/api/lib.py | 32 --------- openpype/hosts/nuke/api/pipeline.py | 5 -- .../nuke/startup}/custom_popup.py | 71 ++++++++++++++----- .../defaults/project_settings/nuke.json | 7 ++ 4 files changed, 61 insertions(+), 54 deletions(-) rename openpype/{widgets => hosts/nuke/startup}/custom_popup.py (65%) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 94a0ff15ad..59a63d1373 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2358,38 +2358,6 @@ class WorkfileSettings(object): # add colorspace menu item self.set_colorspace() - def reset_frame_range_read_nodes(self): - from openpype.widgets import custom_popup - parent = get_main_window() - dialog = custom_popup.CustomScriptDialog(parent=parent) - dialog.setWindowTitle("Frame Range") - dialog.set_name("Frame Range: ") - dialog.set_line_edit("%s - %s" % (nuke.root().firstFrame(), - nuke.root().lastFrame())) - frame = dialog.widgets["line_edit"] - selection = dialog.widgets["selection"] - dialog.on_clicked.connect( - lambda: set_frame_range(frame, selection) - ) - - def set_frame_range(frame, selection): - frame_range = frame.text() - selected = selection.isChecked() - if not nuke.allNodes("Read"): - return - for read_node in nuke.allNodes("Read"): - if selected: - if not nuke.selectedNodes(): - return - if read_node in nuke.selectedNodes(): - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - else: - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - dialog.show() - - return False def set_favorites(self): from .utils import set_context_favorites diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 33e25d3c81..75b0f80d21 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -286,11 +286,6 @@ def _install_menu(): lambda: WorkfileSettings().set_context_settings() ) - menu.addSeparator() - menu.addCommand( - "Set Frame Range(Read Node)", - lambda: WorkfileSettings().reset_frame_range_read_nodes() - ) menu.addSeparator() menu.addCommand( "Build Workfile", diff --git a/openpype/widgets/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py similarity index 65% rename from openpype/widgets/custom_popup.py rename to openpype/hosts/nuke/startup/custom_popup.py index be4b0c32d5..c85577133c 100644 --- a/openpype/widgets/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -1,9 +1,26 @@ import sys import contextlib - +import re +import nuke from PySide2 import QtCore, QtWidgets +def get_main_window(): + """Acquire Nuke's main window""" + main_window = None + if main_window is None: + + top_widgets = QtWidgets.QApplication.topLevelWidgets() + name = "Foundry::UI::DockMainWindow" + for widget in top_widgets: + if ( + widget.inherits("QMainWindow") + and widget.metaObject().className() == name + ): + main_window = widget + break + return main_window + class CustomScriptDialog(QtWidgets.QDialog): """A Popup that moves itself to bottom right of screen on show event. @@ -14,6 +31,9 @@ class CustomScriptDialog(QtWidgets.QDialog): on_clicked = QtCore.Signal() on_line_changed = QtCore.Signal(str) + context = None + + def __init__(self, parent=None, *args, **kwargs): super(CustomScriptDialog, self).__init__(parent=parent, @@ -23,23 +43,25 @@ class CustomScriptDialog(QtWidgets.QDialog): # Layout layout = QtWidgets.QVBoxLayout(self) - line_layout = QtWidgets.QHBoxLayout() - line_layout.setContentsMargins(10, 5, 10, 10) + frame_layout = QtWidgets.QHBoxLayout() + frame_layout.setContentsMargins(10, 5, 10, 10) selection_layout = QtWidgets.QHBoxLayout() selection_layout.setContentsMargins(10, 5, 10, 10) button_layout = QtWidgets.QHBoxLayout() button_layout.setContentsMargins(10, 5, 10, 10) # Increase spacing slightly for readability - line_layout.setSpacing(10) + frame_layout.setSpacing(10) button_layout.setSpacing(10) - name = QtWidgets.QLabel("") + name = QtWidgets.QLabel("Frame Range: ") name.setStyleSheet(""" QLabel { font-size: 12px; } """) - line_edit = QtWidgets.QLineEdit("") + line_edit = QtWidgets.QLineEdit( + "%s-%s" % (nuke.root().firstFrame(), + nuke.root().lastFrame())) selection_name = QtWidgets.QLabel("Use Selection") selection_name.setStyleSheet(""" QLabel { @@ -54,13 +76,13 @@ class CustomScriptDialog(QtWidgets.QDialog): cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) - line_layout.addWidget(name) - line_layout.addWidget(line_edit) + frame_layout.addWidget(name) + frame_layout.addWidget(line_edit) selection_layout.addWidget(selection_name) selection_layout.addWidget(has_selection) button_layout.addWidget(button) button_layout.addWidget(cancel) - layout.addLayout(line_layout) + layout.addLayout(frame_layout) layout.addLayout(selection_layout) layout.addLayout(button_layout) # Default size @@ -73,7 +95,6 @@ class CustomScriptDialog(QtWidgets.QDialog): "button": button, "cancel": cancel } - # Signals has_selection.toggled.connect(self.emit_click_with_state) line_edit.textChanged.connect(self.on_line_edit_changed) @@ -115,18 +136,34 @@ class CustomScriptDialog(QtWidgets.QDialog): Raises the parent (if any) """ + frame_range = self.widgets['line_edit'].text() + selected = self.widgets["selection"].isChecked() + pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" + match = re.match(pattern, frame_range) + frame_start = int(match.group("start")) + frame_end = int(match.group("end")) + if not nuke.allNodes("Read"): + return + for read_node in nuke.allNodes("Read"): + if selected: + if not nuke.selectedNodes(): + return + if read_node in nuke.selectedNodes(): + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + read_node["first"].setValue(frame_start) + read_node["last"].setValue(frame_end) + else: + read_node["frame_mode"].setValue("start_at") + read_node["frame"].setValue(frame_range) + read_node["first"].setValue(frame_start) + read_node["last"].setValue(frame_end) - parent = self.parent() self.close() - # Trigger the signal - self.on_clicked.emit() - - if parent: - parent.raise_() + return False def showEvent(self, event): - # Position popup based on contents on show event return super(CustomScriptDialog, self).showEvent(event) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index f01bdf7d50..287d13e5c9 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -222,6 +222,13 @@ "title": "OpenPype Docs", "command": "import webbrowser;webbrowser.open(url='https://openpype.io/docs/artist_hosts_nuke_tut')", "tooltip": "Open the OpenPype Nuke user doc page" + }, + { + "type": "action", + "sourcetype": "python", + "title": "Set Frame Range(Read Node)", + "command": "from openpype.hosts.nuke.startup import custom_popup;from openpype.hosts.nuke.startup.custom_popup import get_main_window;custom_popup.CustomScriptDialog(parent=get_main_window()).show();", + "tooltip": "Set Frame Range for Read Node(s)" } ] }, From 3556b58fdc593eef0c22eb3d6b357e0c0ac0be7c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 21:46:25 +0800 Subject: [PATCH 154/200] hound fix --- openpype/hosts/nuke/api/lib.py | 1 - openpype/hosts/nuke/startup/custom_popup.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 59a63d1373..a439142051 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2358,7 +2358,6 @@ class WorkfileSettings(object): # add colorspace menu item self.set_colorspace() - def set_favorites(self): from .utils import set_context_favorites diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index c85577133c..dfbd590e03 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -21,6 +21,7 @@ def get_main_window(): break return main_window + class CustomScriptDialog(QtWidgets.QDialog): """A Popup that moves itself to bottom right of screen on show event. @@ -34,7 +35,6 @@ class CustomScriptDialog(QtWidgets.QDialog): context = None - def __init__(self, parent=None, *args, **kwargs): super(CustomScriptDialog, self).__init__(parent=parent, *args, From dddfeecceb4c711e1d8b848c8454d529992e6182 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 21:47:03 +0800 Subject: [PATCH 155/200] hound fix --- openpype/hosts/nuke/startup/custom_popup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index dfbd590e03..d400ed913c 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -34,7 +34,6 @@ class CustomScriptDialog(QtWidgets.QDialog): on_line_changed = QtCore.Signal(str) context = None - def __init__(self, parent=None, *args, **kwargs): super(CustomScriptDialog, self).__init__(parent=parent, *args, From 8f0821ab327db90a6fba5d95594bdcc461fb33e3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 30 May 2023 22:12:47 +0800 Subject: [PATCH 156/200] the dialog closes as usual by clicking execute button when there is no nuke nodes or no nuke nodes by selection --- openpype/hosts/nuke/startup/custom_popup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index d400ed913c..57d79f99ff 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -142,10 +142,12 @@ class CustomScriptDialog(QtWidgets.QDialog): frame_start = int(match.group("start")) frame_end = int(match.group("end")) if not nuke.allNodes("Read"): + self.close() return for read_node in nuke.allNodes("Read"): if selected: if not nuke.selectedNodes(): + self.close() return if read_node in nuke.selectedNodes(): read_node["frame_mode"].setValue("start_at") From de72f26f9451f48608229cc85868c1545a201afd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 30 May 2023 17:09:54 +0200 Subject: [PATCH 157/200] adding check also against class attribute --- openpype/plugins/publish/collect_frames_fix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index 837738eb06..ca1ccc19fd 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -66,7 +66,7 @@ class CollectFramesFixDef( self.log.debug("last_version_published_files::{}".format( instance.data["last_version_published_files"])) - if rewrite_version: + if self.rewrite_version_enable and rewrite_version: instance.data["version"] = version["name"] # limits triggering version validator instance.data.pop("latestVersion") From 04e6f5f4bb844b11dbb93475f5d023c2125651ff Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 30 May 2023 17:16:18 +0200 Subject: [PATCH 158/200] :bug: fix support for separate AOVs and some style issues --- openpype/hosts/max/api/lib_renderproducts.py | 76 +++++++++---------- .../max/plugins/publish/collect_render.py | 10 ++- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a6427bf7c5..81057db733 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -3,22 +3,20 @@ # arnold # https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html import os + from pymxs import runtime as rt -from openpype.hosts.max.api.lib import ( - get_current_renderer -) -from openpype.settings import get_project_settings + +from openpype.hosts.max.api.lib import get_current_renderer from openpype.pipeline import legacy_io +from openpype.settings import get_project_settings class RenderProducts(object): def __init__(self, project_settings=None): - self._project_settings = project_settings - if not self._project_settings: - self._project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) + self._project_settings = project_settings or get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) def get_beauty(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) @@ -29,14 +27,14 @@ class RenderProducts(object): setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa - startFrame = int(rt.rendStart) - endFrame = int(rt.rendEnd) + 1 + start_frame = int(rt.rendStart) + end_frame = int(rt.rendEnd) + 1 - render_dict = { + return { "beauty": self.get_expected_beauty( - output_file, startFrame, endFrame, img_fmt) + output_file, start_frame, end_frame, img_fmt + ) } - return render_dict def get_aovs(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) @@ -47,8 +45,8 @@ class RenderProducts(object): setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa - startFrame = int(rt.rendStart) - endFrame = int(rt.rendEnd) + 1 + start_frame = int(rt.rendStart) + end_frame = int(rt.rendEnd) + 1 renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] render_dict = {} @@ -65,38 +63,40 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) - if renderer == "Redshift_Renderer": + elif renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_AovFiles = rt.RedShift_Renderer().separateAovFiles - if img_fmt == "exr" and not rs_AovFiles: + rs_aov_files = rt.Execute("renderers.current.separateAovFiles") + # this doesn't work, always returns False + # rs_AovFiles = rt.RedShift_Renderer().separateAovFiles + if img_fmt == "exr" and not rs_aov_files: for name in render_name: if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) else: for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) - if renderer == "Arnold": + elif renderer == "Arnold": render_name = self.get_arnold_product_name() if render_name: for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( - output_file, name, startFrame, endFrame, img_fmt) + output_file, name, start_frame, end_frame, img_fmt) }) - if renderer in [ + elif renderer in [ "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3" ]: @@ -106,15 +106,15 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, startFrame, - endFrame, img_fmt) # noqa + output_file, name, start_frame, + end_frame, img_fmt) # noqa }) return render_dict - def get_expected_beauty(self, folder, startFrame, endFrame, fmt): + def get_expected_beauty(self, folder, start_frame, end_frame, fmt): beauty_frame_range = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f beauty_output = f"{folder}.{frame}.{fmt}" beauty_output = beauty_output.replace("\\", "/") @@ -134,19 +134,17 @@ class RenderProducts(object): return for i in range(aov_group_num): # get the specific AOV group - for aov in aov_mgr.drivers[i].aov_list: - aov_name.append(aov.name) - + aov_name.extend(aov.name for aov in aov_mgr.drivers[i].aov_list) # close the AOVs manager window amw.close() return aov_name def get_expected_arnold_product(self, folder, name, - startFrame, endFrame, fmt): + start_frame, end_frame, fmt): """Get all the expected Arnold AOVs""" aov_list = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") @@ -171,10 +169,10 @@ class RenderProducts(object): return render_name def get_expected_render_elements(self, folder, name, - startFrame, endFrame, fmt): + start_frame, end_frame, fmt): """Get all the expected render element output files. """ render_elements = [] - for f in range(startFrame, endFrame): + for f in range(start_frame, end_frame): frame = "%04d" % f render_element = f"{folder}_{name}.{frame}.{fmt}" render_element = render_element.replace("\\", "/") diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 77ab5f654d..db5c84fad9 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -66,7 +66,7 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] - # also need to get the render dir for coversion + # also need to get the render dir for conversion data = { "asset": asset, "subset": str(instance.name), @@ -84,4 +84,12 @@ class CollectRender(pyblish.api.InstancePlugin): "farm": True } instance.data.update(data) + + # TODO: this should be unified with maya and its "multipart" flag + # on instance. + if renderer == "Redshift_Renderer": + instance.data.update( + {"separateAovFiles": rt.Execute( + "renderers.current.separateAovFiles")}) + self.log.info("data: {0}".format(data)) From 4d76c2520f254991da405dc463418d9bb6e2cfc4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 30 May 2023 17:19:00 +0200 Subject: [PATCH 159/200] cleanup --- .../plugins/publish/collect_frames_fix.py | 62 ++++++++++--------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/openpype/plugins/publish/collect_frames_fix.py b/openpype/plugins/publish/collect_frames_fix.py index ca1ccc19fd..86e727b053 100644 --- a/openpype/plugins/publish/collect_frames_fix.py +++ b/openpype/plugins/publish/collect_frames_fix.py @@ -35,41 +35,47 @@ class CollectFramesFixDef( rewrite_version = attribute_values.get("rewrite_version") - if frames_to_fix: - instance.data["frames_to_fix"] = frames_to_fix + if not frames_to_fix: + return - subset_name = instance.data["subset"] - asset_name = instance.data["asset"] + instance.data["frames_to_fix"] = frames_to_fix - project_entity = instance.data["projectEntity"] - project_name = project_entity["name"] + subset_name = instance.data["subset"] + asset_name = instance.data["asset"] - version = get_last_version_by_subset_name(project_name, - subset_name, - asset_name=asset_name) - if not version: - self.log.warning("No last version found, " - "re-render not possible") - return + project_entity = instance.data["projectEntity"] + project_name = project_entity["name"] - representations = get_representations(project_name, - version_ids=[version["_id"]]) - published_files = [] - for repre in representations: - if repre["context"]["family"] not in self.families: - continue + version = get_last_version_by_subset_name( + project_name, + subset_name, + asset_name=asset_name + ) + if not version: + self.log.warning( + "No last version found, re-render not possible" + ) + return - for file_info in repre.get("files"): - published_files.append(file_info["path"]) + representations = get_representations( + project_name, version_ids=[version["_id"]] + ) + published_files = [] + for repre in representations: + if repre["context"]["family"] not in self.families: + continue - instance.data["last_version_published_files"] = published_files - self.log.debug("last_version_published_files::{}".format( - instance.data["last_version_published_files"])) + for file_info in repre.get("files"): + published_files.append(file_info["path"]) - if self.rewrite_version_enable and rewrite_version: - instance.data["version"] = version["name"] - # limits triggering version validator - instance.data.pop("latestVersion") + instance.data["last_version_published_files"] = published_files + self.log.debug("last_version_published_files::{}".format( + instance.data["last_version_published_files"])) + + if self.rewrite_version_enable and rewrite_version: + instance.data["version"] = version["name"] + # limits triggering version validator + instance.data.pop("latestVersion") @classmethod def get_attribute_defs(cls): From be523bc5ec967efce3181668a932eb644a5e8f59 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 30 May 2023 16:49:49 +0100 Subject: [PATCH 160/200] Use temp folder to copy commandlet project --- openpype/hosts/unreal/ue_workers.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/ue_workers.py b/openpype/hosts/unreal/ue_workers.py index e7a690ac9c..2b7e1375e6 100644 --- a/openpype/hosts/unreal/ue_workers.py +++ b/openpype/hosts/unreal/ue_workers.py @@ -6,6 +6,8 @@ import subprocess from distutils import dir_util from pathlib import Path from typing import List, Union +import tempfile +from distutils.dir_util import copy_tree import openpype.hosts.unreal.lib as ue_lib @@ -90,9 +92,20 @@ class UEProjectGenerationWorker(QtCore.QObject): ("Generating a new UE project ... 1 out of " f"{stage_count}")) + # Need to copy the commandlet project to a temporary folder where + # users don't need admin rights to write to. + cmdlet_tmp = tempfile.TemporaryDirectory() + cmdlet_filename = cmdlet_project.name + cmdlet_dir = cmdlet_project.parent.as_posix() + cmdlet_tmp_name = Path(cmdlet_tmp.name) + cmdlet_tmp_file = cmdlet_tmp_name.joinpath(cmdlet_filename) + copy_tree( + cmdlet_dir, + cmdlet_tmp_name.as_posix()) + commandlet_cmd = [ f"{ue_editor_exe.as_posix()}", - f"{cmdlet_project.as_posix()}", + f"{cmdlet_tmp_file.as_posix()}", "-run=AyonGenerateProject", f"{project_file.resolve().as_posix()}", ] @@ -111,6 +124,8 @@ class UEProjectGenerationWorker(QtCore.QObject): gen_process.stdout.close() return_code = gen_process.wait() + cmdlet_tmp.cleanup() + if return_code and return_code != 0: msg = ( f"Failed to generate {self.project_name} " From a215e4f00665f779f1056f5a128b3e2674e3c68f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 31 May 2023 03:26:47 +0000 Subject: [PATCH 161/200] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index c24388b2ff..5c7105e7e0 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.9-nightly.1" +__version__ = "3.15.9-nightly.2" From e8b47be8d562e8bb1edd77dd4aebf6e75f6fc720 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 May 2023 03:27:31 +0000 Subject: [PATCH 162/200] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 54a4ee6ac0..0036e121b7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9-nightly.2 - 3.15.9-nightly.1 - 3.15.8 - 3.15.8-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.2 - 3.14.2-nightly.5 - 3.14.2-nightly.4 - - 3.14.2-nightly.3 validations: required: true - type: dropdown From e6d10fa3358b61eae7c9461da2119af72adf9be6 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 31 May 2023 10:37:21 +0100 Subject: [PATCH 163/200] Update settings_project_global.md (#5045) --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index c17f707830..7bd24a5773 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -63,7 +63,7 @@ Example here describes use case for creation of new color coded review of png im ![global_oiio_transcode](assets/global_oiio_transcode.png) Another use case is to transcode in Maya only `beauty` render layers and use collected `Display` and `View` colorspaces from DCC. -![global_oiio_transcode_in_Maya](assets/global_oiio_transcode.png)n +![global_oiio_transcode_in_Maya](assets/global_oiio_transcode2.png) ## Profile filters From e22d1bf78b4e9d4aeeb7c0295c8bb92b1c5595d9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 10:45:47 +0100 Subject: [PATCH 164/200] Set dev mode off by default --- openpype/settings/defaults/project_settings/unreal.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index 737a17d289..92bdb468ba 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -15,6 +15,6 @@ "preroll_frames": 0, "render_format": "png", "project_setup": { - "dev_mode": true + "dev_mode": false } } From c2b753326fb6cb6460a43e401fb5731cdee1fabb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 11:59:21 +0100 Subject: [PATCH 165/200] Check if the Unreal app name follows the right format --- openpype/hosts/unreal/addon.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 1119b5c16c..fddceb00a8 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -1,5 +1,7 @@ import os +import re from openpype.modules import IHostAddon, OpenPypeModule +from openpype.widgets.message_window import Window UNREAL_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -19,6 +21,18 @@ class UnrealAddon(OpenPypeModule, IHostAddon): from .lib import get_compatible_integration + pattern = re.compile(r'^\d+-\d+$') + + if not pattern.match(app.name): + Window( + parent=None, + title="Unreal application name format", + message="Unreal application name must be in format '5-0' or '5-1'", + level="critical") + raise ValueError( + "Unreal application name must be in format '5-0' or '5-1'" + ) + ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", "UE_{}".format(ue_version), "Ayon" From 142ae35d9b81b5325c462b67cc870c959a801524 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 12:13:00 +0100 Subject: [PATCH 166/200] Hound fixes --- openpype/hosts/unreal/addon.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index fddceb00a8..16f6fcf27c 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -24,14 +24,13 @@ class UnrealAddon(OpenPypeModule, IHostAddon): pattern = re.compile(r'^\d+-\d+$') if not pattern.match(app.name): + msg = "Unreal application name must be in format '5-0' or '5-1'" Window( parent=None, title="Unreal application name format", - message="Unreal application name must be in format '5-0' or '5-1'", + message=msg, level="critical") - raise ValueError( - "Unreal application name must be in format '5-0' or '5-1'" - ) + raise ValueError(msg) ue_version = app.name.replace("-", ".") unreal_plugin_path = os.path.join( From 11728bae0e4f1a539d5959ec3cfe9fe523fe356a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 31 May 2023 20:02:05 +0800 Subject: [PATCH 167/200] roy's comment and uses import instead of from..import --- openpype/hosts/nuke/startup/custom_popup.py | 17 ++++------------- .../defaults/project_settings/nuke.json | 2 +- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index 57d79f99ff..7f60fdc70b 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -23,10 +23,7 @@ def get_main_window(): class CustomScriptDialog(QtWidgets.QDialog): - """A Popup that moves itself to bottom right of screen on show event. - - The UI contains a message label and a red highlighted button to "show" - or perform another custom action from this pop-up. + """A Custom Popup For Nuke Read Node """ @@ -95,7 +92,7 @@ class CustomScriptDialog(QtWidgets.QDialog): "cancel": cancel } # Signals - has_selection.toggled.connect(self.emit_click_with_state) + has_selection.toggled.connect(self.on_checked_changed) line_edit.textChanged.connect(self.on_line_edit_changed) button.clicked.connect(self._on_clicked) cancel.clicked.connect(self.close) @@ -104,10 +101,9 @@ class CustomScriptDialog(QtWidgets.QDialog): self.setWindowTitle("Custom Popup") def update_values(self): - self.widgets["selection"].isChecked() + return self.widgets["selection"].isChecked() - def emit_click_with_state(self): - """Emit the on_clicked signal with the toggled state""" + def on_checked_changed(self): checked = self.widgets["selection"].isChecked() return checked @@ -164,11 +160,6 @@ class CustomScriptDialog(QtWidgets.QDialog): return False - def showEvent(self, event): - # Position popup based on contents on show event - return super(CustomScriptDialog, self).showEvent(event) - - @contextlib.contextmanager def application(): app = QtWidgets.QApplication(sys.argv) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 287d13e5c9..c116540a99 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -227,7 +227,7 @@ "type": "action", "sourcetype": "python", "title": "Set Frame Range(Read Node)", - "command": "from openpype.hosts.nuke.startup import custom_popup;from openpype.hosts.nuke.startup.custom_popup import get_main_window;custom_popup.CustomScriptDialog(parent=get_main_window()).show();", + "command": "import openpype.hosts.nuke.startup.custom_popup as popup;popup.CustomScriptDialog(parent=popup.get_main_window()).show();", "tooltip": "Set Frame Range for Read Node(s)" } ] From 803dd565751bb65a6ef5b97e7c6f5447f4534717 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 31 May 2023 20:03:18 +0800 Subject: [PATCH 168/200] hound fix --- openpype/hosts/nuke/startup/custom_popup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py index 7f60fdc70b..76d5a25596 100644 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ b/openpype/hosts/nuke/startup/custom_popup.py @@ -160,6 +160,7 @@ class CustomScriptDialog(QtWidgets.QDialog): return False + @contextlib.contextmanager def application(): app = QtWidgets.QApplication(sys.argv) From 183b2866d9e1f1f7b611fa6860c1edc315798b66 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 31 May 2023 20:14:57 +0800 Subject: [PATCH 169/200] style fix --- openpype/settings/defaults/project_settings/nuke.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index c116540a99..35e5b1975c 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -226,7 +226,7 @@ { "type": "action", "sourcetype": "python", - "title": "Set Frame Range(Read Node)", + "title": "Set Frame Range (Read Node)", "command": "import openpype.hosts.nuke.startup.custom_popup as popup;popup.CustomScriptDialog(parent=popup.get_main_window()).show();", "tooltip": "Set Frame Range for Read Node(s)" } From 0635c39a37094bf4f78898a9a179fbf657baf021 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 31 May 2023 14:32:24 +0100 Subject: [PATCH 170/200] Improved error message --- openpype/hosts/unreal/addon.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index 16f6fcf27c..ed23950b35 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -24,7 +24,10 @@ class UnrealAddon(OpenPypeModule, IHostAddon): pattern = re.compile(r'^\d+-\d+$') if not pattern.match(app.name): - msg = "Unreal application name must be in format '5-0' or '5-1'" + msg = ( + "Unreal application key in the settings must be in format" + "'5-0' or '5-1'" + ) Window( parent=None, title="Unreal application name format", From 396486067bd4c33f0ac6ef75f0676e05b9b6c985 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 31 May 2023 13:43:50 +0000 Subject: [PATCH 171/200] [Automated] Release --- CHANGELOG.md | 335 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 337 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a33904735b..ec6544e659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,341 @@ # Changelog +## [3.15.9](https://github.com/ynput/OpenPype/tree/3.15.9) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.15.8...3.15.9) + +### **πŸ†• New features** + + +
+Blender: Implemented Loading of Alembic Camera #4990 + +Implemented loading of Alembic cameras in Blender. + + +___ + +
+ + +
+Unreal: Implemented Creator, Loader and Extractor for Levels #5008 + +Creator, Loader and Extractor for Unreal Levels have been implemented. + + +___ + +
+ +### **πŸš€ Enhancements** + + +
+Blender: Added setting for base unit scale #4987 + +A setting for the base unit scale has been added for Blender.The unit scale is automatically applied when opening a file or creating a new one. + + +___ + +
+ + +
+Unreal: Changed naming and path of Camera Levels #5010 + +The levels created for the camera in Unreal now include `_camera` in the name, to be better identifiable, and are placed in the camera folder. + + +___ + +
+ + +
+Settings: Added option to nest settings templates #5022 + +It is possible to nest settings templates in another templates. + + +___ + +
+ + +
+Enhancement/publisher: Remove "hit play to continue" label on continue #5029 + +Remove "hit play to continue" message on continue so that it doesn't show anymore when play was clicked. + + +___ + +
+ + +
+Ftrack: Limit number of ftrack events to query at once #5033 + +Limit the amount of ftrack events received from mongo at once to 100. + + +___ + +
+ + +
+General: Small code cleanups #5034 + +Small code cleanup and updates. + + +___ + +
+ + +
+Global: collect frames to fix with settings #5036 + +Settings for `Collect Frames to Fix` will allow disable per project the plugin. Also `Rewriting latest version` attribute is hiddable from settings. + + +___ + +
+ + +
+General: Publish plugin apply settings can expect only project settings #5037 + +Only project settings are passed to optional `apply_settings` method, if the method expects only one argument. + + +___ + +
+ +### **πŸ› Bug fixes** + + +
+Maya: Load Assembly fix invalid imports #4859 + +Refactors imports so they are now correct. + + +___ + +
+ + +
+Maya: Skipping rendersetup for members. #4973 + +When publishing a `rendersetup`, the objectset is and should be empty. + + +___ + +
+ + +
+Maya: Validate Rig Output IDs #5016 + +Absolute names of node were not used, so plugin did not fetch the nodes properly.Also missed pymel command. + + +___ + +
+ + +
+Deadline: escape rootless path in publish job #4910 + +If the publish path on Deadline job contains spaces or other characters, command was failing because the path wasn't properly escaped. This is fixing it. + + +___ + +
+ + +
+General: Company name and URL changed #4974 + +The current records were obsolete in inno_setup, changed to the up-to-date. +___ + +
+ + +
+Unreal: Fix usage of 'get_full_path' function #5014 + +This PR changes all the occurrences of `get_full_path` functions to alternatives to get the path of the objects. + + +___ + +
+ + +
+Unreal: Fix sequence frames validator to use correct data #5021 + +Fix sequence frames validator to use clipIn and clipOut data instead of frameStart and frameEnd. + + +___ + +
+ + +
+Unreal: Fix render instances collection to use correct data #5023 + +Fix render instances collection to use `frameStart` and `frameEnd` from the Project Manager, instead of the sequence's ones. + + +___ + +
+ + +
+Resolve: loader is opening even if no timeline in project #5025 + +Loader is opening now even no timeline is available in a project. + + +___ + +
+ + +
+nuke: callback for dirmapping is on demand #5030 + +Nuke was slowed down on processing due this callback. Since it is disabled by default it made sense to add it only on demand. + + +___ + +
+ + +
+Publisher: UI works with instances without label #5032 + +Publisher UI does not crash if instance don't have filled 'label' key in instance data. + + +___ + +
+ + +
+Publisher: Call explicitly prepared tab methods #5044 + +It is not possible to go to Create tab during publishing from OpenPype menu. + + +___ + +
+ + +
+Ftrack: Role names are not case sensitive in ftrack event server status action #5058 + +Event server status action is not case sensitive for role names of user. + + +___ + +
+ + +
+Publisher: Fix border widget #5063 + +Fixed border lines in Publisher UI to be painted correctly with correct indentation and size. + + +___ + +
+ + +
+Unreal: Fix Commandlet Project and Permissions #5066 + +Fix problem when creating an Unreal Project when Commandlet Project is in a protected location. + + +___ + +
+ + +
+Unreal: Added verification for Unreal app name format #5070 + +The Unreal app name is used to determine the Unreal version folder, so it is necessary that if follows the format `x-x`, where `x` is any integer. This PR adds a verification that the app name follows that format. + + +___ + +
+ +### **πŸ“ƒ Documentation** + + +
+Docs: Display wrong image in ExtractOIIOTranscode #5045 + +Wrong image display in `https://openpype.io/docs/project_settings/settings_project_global#extract-oiio-transcode`. + + +___ + +
+ +### **Merged pull requests** + + +
+Drop-down menu to list all families in create placeholder #4928 + +Currently in the create placeholder window, we need to write the family manually. This replace the text field by an enum field with all families for the current software. + + +___ + +
+ + +
+add sync to specific projects or listen only #4919 + +Extend kitsu sync service with additional arguments to sync specific projects. + + +___ + +
+ + + + ## [3.15.8](https://github.com/ynput/OpenPype/tree/3.15.8) diff --git a/openpype/version.py b/openpype/version.py index 5c7105e7e0..dd23138dee 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.9-nightly.2" +__version__ = "3.15.9" diff --git a/pyproject.toml b/pyproject.toml index a72a3d66d7..633899d3a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.8" # OpenPype +version = "3.15.9" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 776dcb5a6fcf65eec08f95655c870d7e43ec4ce9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 May 2023 13:44:50 +0000 Subject: [PATCH 172/200] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0036e121b7..aa5b8decdc 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.9 - 3.15.9-nightly.2 - 3.15.9-nightly.1 - 3.15.8 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.1 - 3.14.2 - 3.14.2-nightly.5 - - 3.14.2-nightly.4 validations: required: true - type: dropdown From 3955c466c3a0f01e20bf95bfa594b561884517e5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 31 May 2023 17:17:35 +0200 Subject: [PATCH 173/200] fix apply settings on hiero loader (#5073) --- openpype/hosts/hiero/plugins/load/load_clip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/load/load_clip.py b/openpype/hosts/hiero/plugins/load/load_clip.py index 77844d2448..c9bebfa8b2 100644 --- a/openpype/hosts/hiero/plugins/load/load_clip.py +++ b/openpype/hosts/hiero/plugins/load/load_clip.py @@ -41,8 +41,8 @@ class LoadClip(phiero.SequenceLoader): clip_name_template = "{asset}_{subset}_{representation}" + @classmethod def apply_settings(cls, project_settings, system_settings): - plugin_type_settings = ( project_settings .get("hiero", {}) From a30ba51508f03c1adbac8c434432a80184d0665b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 00:11:30 +0800 Subject: [PATCH 174/200] use nukescript's panel --- openpype/hosts/nuke/startup/custom_popup.py | 174 ------------------ .../startup/ops_frame_setting_for_read.py | 46 +++++ .../defaults/project_settings/nuke.json | 2 +- 3 files changed, 47 insertions(+), 175 deletions(-) delete mode 100644 openpype/hosts/nuke/startup/custom_popup.py create mode 100644 openpype/hosts/nuke/startup/ops_frame_setting_for_read.py diff --git a/openpype/hosts/nuke/startup/custom_popup.py b/openpype/hosts/nuke/startup/custom_popup.py deleted file mode 100644 index 76d5a25596..0000000000 --- a/openpype/hosts/nuke/startup/custom_popup.py +++ /dev/null @@ -1,174 +0,0 @@ -import sys -import contextlib -import re -import nuke -from PySide2 import QtCore, QtWidgets - - -def get_main_window(): - """Acquire Nuke's main window""" - main_window = None - if main_window is None: - - top_widgets = QtWidgets.QApplication.topLevelWidgets() - name = "Foundry::UI::DockMainWindow" - for widget in top_widgets: - if ( - widget.inherits("QMainWindow") - and widget.metaObject().className() == name - ): - main_window = widget - break - return main_window - - -class CustomScriptDialog(QtWidgets.QDialog): - """A Custom Popup For Nuke Read Node - - """ - - on_clicked = QtCore.Signal() - on_line_changed = QtCore.Signal(str) - context = None - - def __init__(self, parent=None, *args, **kwargs): - super(CustomScriptDialog, self).__init__(parent=parent, - *args, - **kwargs) - self.setContentsMargins(0, 0, 0, 0) - - # Layout - layout = QtWidgets.QVBoxLayout(self) - frame_layout = QtWidgets.QHBoxLayout() - frame_layout.setContentsMargins(10, 5, 10, 10) - selection_layout = QtWidgets.QHBoxLayout() - selection_layout.setContentsMargins(10, 5, 10, 10) - button_layout = QtWidgets.QHBoxLayout() - button_layout.setContentsMargins(10, 5, 10, 10) - - # Increase spacing slightly for readability - frame_layout.setSpacing(10) - button_layout.setSpacing(10) - name = QtWidgets.QLabel("Frame Range: ") - name.setStyleSheet(""" - QLabel { - font-size: 12px; - } - """) - line_edit = QtWidgets.QLineEdit( - "%s-%s" % (nuke.root().firstFrame(), - nuke.root().lastFrame())) - selection_name = QtWidgets.QLabel("Use Selection") - selection_name.setStyleSheet(""" - QLabel { - font-size: 12px; - } - """) - has_selection = QtWidgets.QCheckBox() - button = QtWidgets.QPushButton("Execute") - button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) - cancel = QtWidgets.QPushButton("Cancel") - cancel.setSizePolicy(QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) - - frame_layout.addWidget(name) - frame_layout.addWidget(line_edit) - selection_layout.addWidget(selection_name) - selection_layout.addWidget(has_selection) - button_layout.addWidget(button) - button_layout.addWidget(cancel) - layout.addLayout(frame_layout) - layout.addLayout(selection_layout) - layout.addLayout(button_layout) - # Default size - self.resize(100, 40) - - self.widgets = { - "name": name, - "line_edit": line_edit, - "selection": has_selection, - "button": button, - "cancel": cancel - } - # Signals - has_selection.toggled.connect(self.on_checked_changed) - line_edit.textChanged.connect(self.on_line_edit_changed) - button.clicked.connect(self._on_clicked) - cancel.clicked.connect(self.close) - self.update_values() - # Set default title - self.setWindowTitle("Custom Popup") - - def update_values(self): - return self.widgets["selection"].isChecked() - - def on_checked_changed(self): - checked = self.widgets["selection"].isChecked() - return checked - - def set_name(self, name): - self.widgets['name'].setText(name) - - def set_line_edit(self, line_edit): - self.widgets['line_edit'].setText(line_edit) - print(line_edit) - - def setButtonText(self, text): - self.widgets["button"].setText(text) - - def setCancelText(self, text): - self.widgets["cancel"].setText(text) - - def on_line_edit_changed(self): - line_edit = self.widgets['line_edit'].text() - self.on_line_changed.emit(line_edit) - return self.set_line_edit(line_edit) - - def _on_clicked(self): - """Callback for when the 'show' button is clicked. - - Raises the parent (if any) - - """ - frame_range = self.widgets['line_edit'].text() - selected = self.widgets["selection"].isChecked() - pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" - match = re.match(pattern, frame_range) - frame_start = int(match.group("start")) - frame_end = int(match.group("end")) - if not nuke.allNodes("Read"): - self.close() - return - for read_node in nuke.allNodes("Read"): - if selected: - if not nuke.selectedNodes(): - self.close() - return - if read_node in nuke.selectedNodes(): - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - read_node["first"].setValue(frame_start) - read_node["last"].setValue(frame_end) - else: - read_node["frame_mode"].setValue("start_at") - read_node["frame"].setValue(frame_range) - read_node["first"].setValue(frame_start) - read_node["last"].setValue(frame_end) - - self.close() - - return False - - -@contextlib.contextmanager -def application(): - app = QtWidgets.QApplication(sys.argv) - yield - app.exec_() - - -if __name__ == "__main__": - with application(): - dialog = CustomScriptDialog() - dialog.show() diff --git a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py new file mode 100644 index 0000000000..153effc7ad --- /dev/null +++ b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py @@ -0,0 +1,46 @@ +import nuke +import nukescripts +import re + + +class FrameSettingsPanel(nukescripts.PythonPanel): + def __init__(self, node): + nukescripts.PythonPanel.__init__(self, 'Frame Range') + self.read_node = node + # CREATE KNOBS + self.range = nuke.String_Knob('fRange', 'Frame Range', '%s-%s' % + (nuke.root().firstFrame(), nuke.root().lastFrame())) + self.selected = nuke.Boolean_Knob("selection") + # ADD KNOBS + self.addKnob(self.selected) + self.addKnob(self.range) + self.selected.setValue(False) + + def knobChanged(self, knob): + frame_range = self.range.value() + pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" + match = re.match(pattern, frame_range) + frame_start = int(match.group("start")) + frame_end = int(match.group("end")) + if not self.read_node: + return + for r in self.read_node: + if self.onchecked(): + if not nuke.selectedNodes(): + return + if r in nuke.selectedNodes(): + r["frame_mode"].setValue("start_at") + r["frame"].setValue(frame_range) + r["first"].setValue(frame_start) + r["last"].setValue(frame_end) + else: + r["frame_mode"].setValue("start_at") + r["frame"].setValue(frame_range) + r["first"].setValue(frame_start) + r["last"].setValue(frame_end) + + def onchecked(self): + if self.selected.value(): + return True + else: + return False diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 35e5b1975c..cb06ad0a3b 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -227,7 +227,7 @@ "type": "action", "sourcetype": "python", "title": "Set Frame Range (Read Node)", - "command": "import openpype.hosts.nuke.startup.custom_popup as popup;popup.CustomScriptDialog(parent=popup.get_main_window()).show();", + "command": "import ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", "tooltip": "Set Frame Range for Read Node(s)" } ] From 0655a6222bf3c69f4b4b3e9008c6c7803ffd3f94 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 00:12:22 +0800 Subject: [PATCH 175/200] hound fix --- openpype/hosts/nuke/startup/ops_frame_setting_for_read.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py index 153effc7ad..bf98ef83f6 100644 --- a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py +++ b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py @@ -9,11 +9,14 @@ class FrameSettingsPanel(nukescripts.PythonPanel): self.read_node = node # CREATE KNOBS self.range = nuke.String_Knob('fRange', 'Frame Range', '%s-%s' % - (nuke.root().firstFrame(), nuke.root().lastFrame())) + (nuke.root().firstFrame(), + nuke.root().lastFrame())) self.selected = nuke.Boolean_Knob("selection") + self.info = nuke.Help_Knob("Instruction") # ADD KNOBS self.addKnob(self.selected) self.addKnob(self.range) + self.addKnob(self.info) self.selected.setValue(False) def knobChanged(self, knob): From aeaad018c1af7b30c9d1377d27ed91bec65e91cd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 31 May 2023 18:12:48 +0200 Subject: [PATCH 176/200] :rotating_light: make peace with the hound :dog: --- openpype/hosts/max/api/lib_renderproducts.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 81057db733..94b0aeb913 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -15,14 +15,12 @@ class RenderProducts(object): def __init__(self, project_settings=None): self._project_settings = project_settings or get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) + legacy_io.Session["AVALON_PROJECT"]) def get_beauty(self, container): render_dir = os.path.dirname(rt.rendOutputFilename) - output_file = os.path.join(render_dir, - container) + output_file = os.path.join(render_dir, container) setting = self._project_settings img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa From 021ea8d6380e439ccddcdbf3ac78542b4329a7e1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 1 Jun 2023 00:30:19 +0800 Subject: [PATCH 177/200] update the command --- openpype/settings/defaults/project_settings/nuke.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index cb06ad0a3b..a0caa40396 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -227,7 +227,7 @@ "type": "action", "sourcetype": "python", "title": "Set Frame Range (Read Node)", - "command": "import ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", + "command": "import openpype.hosts.nuke.startup.ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", "tooltip": "Set Frame Range for Read Node(s)" } ] From 717a2bc81c418a0d77cc7fd730bcb1d4ec18ae90 Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Thu, 1 Jun 2023 01:04:10 +0300 Subject: [PATCH 178/200] update letterbox docs --- website/docs/project_settings/settings_project_global.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 7bd24a5773..5ddf247d98 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -170,12 +170,10 @@ A profile may generate multiple outputs from a single input. Each output must de - **`Letter Box`** - **Enabled** - Enable letter boxes - - **Ratio** - Ratio of letter boxes - - **Type** - **Letterbox** (horizontal bars) or **Pillarbox** (vertical bars) + - **Ratio** - Ratio of letter boxes. Ratio type is calculated from output image dimensions. If letterbox ratio > image ratio, _letterbox_ is applied. Otherwise _pillarbox_ will be rendered. - **Fill color** - Fill color of boxes (RGBA: 0-255) - **Line Thickness** - Line thickness on the edge of box (set to `0` to turn off) - - **Fill color** - Line color on the edge of box (RGBA: 0-255) - - **Example** + - **Line color** - Line color on the edge of box (RGBA: 0-255) ![global_extract_review_letter_box_settings](assets/global_extract_review_letter_box_settings.png) ![global_extract_review_letter_box](assets/global_extract_review_letter_box.png) From b048a4a40ea11661be95a61e5cf58076f0b404fe Mon Sep 17 00:00:00 2001 From: Alexey Bogomolov Date: Thu, 1 Jun 2023 01:05:02 +0300 Subject: [PATCH 179/200] update letterbox screenshot --- ...bal_extract_review_letter_box_settings.png | Bin 6926 -> 27150 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/docs/project_settings/assets/global_extract_review_letter_box_settings.png b/website/docs/project_settings/assets/global_extract_review_letter_box_settings.png index 80e00702e6193dbac34e4976d78133c2ae413d0c..76dd9b372a03142c6849b6ce5347cd780a2415fe 100644 GIT binary patch literal 27150 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 From 5379679f869db99bf169263ca43bb6f068df9dbf Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:23:26 +0000 Subject: [PATCH 180/200] update README.md [skip ci] --- README.md | 75 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 514ffb62c0..8757e3db92 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -[![All Contributors](https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-28-orange.svg?style=flat-square)](#contributors-) OpenPype ==== @@ -303,41 +303,44 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Milan Kolar

πŸ’» πŸ“– πŸš‡ πŸ’Ό πŸ–‹ πŸ” 🚧 πŸ“† πŸ‘€ πŸ§‘β€πŸ« πŸ’¬

Jakub JeΕΎek

πŸ’» πŸ“– πŸš‡ πŸ–‹ πŸ‘€ 🚧 πŸ§‘β€πŸ« πŸ“† πŸ’¬

OndΕ™ej Samohel

πŸ’» πŸ“– πŸš‡ πŸ–‹ πŸ‘€ 🚧 πŸ§‘β€πŸ« πŸ“† πŸ’¬

Jakub Trllo

πŸ’» πŸ“– πŸš‡ πŸ‘€ 🚧 πŸ’¬

Petr Kalis

πŸ’» πŸ“– πŸš‡ πŸ‘€ 🚧 πŸ’¬

64qam

πŸ’» πŸ‘€ πŸ“– πŸš‡ πŸ“† 🚧 πŸ–‹ πŸ““

Roy Nieterau

πŸ’» πŸ“– πŸ‘€ πŸ§‘β€πŸ« πŸ’¬

Toke Jepsen

πŸ’» πŸ“– πŸ‘€ πŸ§‘β€πŸ« πŸ’¬

Jiri Sindelar

πŸ’» πŸ‘€ πŸ“– πŸ–‹ βœ… πŸ““

Simone Barbieri

πŸ’» πŸ“–

karimmozilla

πŸ’»

Allan I. A.

πŸ’»

murphy

πŸ’» πŸ‘€ πŸ““ πŸ“– πŸ“†

Wijnand Koreman

πŸ’»

Bo Zhou

πŸ’»

ClΓ©ment Hector

πŸ’» πŸ‘€

David Lai

πŸ’» πŸ‘€

Derek

πŸ’» πŸ“–

GΓ‘bor Marinov

πŸ’» πŸ“–

icyvapor

πŸ’» πŸ“–

JΓ©rΓ΄me LORRAIN

πŸ’»

David Morris-Oliveros

πŸ’»

BenoitConnan

πŸ’»

Malthaldar

πŸ’»

Sven Neve

πŸ’»

zafrs

πŸ’»

FΓ©lix David

πŸ’» πŸ“–
Milan Kolar
Milan Kolar

πŸ’» πŸ“– πŸš‡ πŸ’Ό πŸ–‹ πŸ” 🚧 πŸ“† πŸ‘€ πŸ§‘β€πŸ« πŸ’¬
Jakub JeΕΎek
Jakub JeΕΎek

πŸ’» πŸ“– πŸš‡ πŸ–‹ πŸ‘€ 🚧 πŸ§‘β€πŸ« πŸ“† πŸ’¬
OndΕ™ej Samohel
OndΕ™ej Samohel

πŸ’» πŸ“– πŸš‡ πŸ–‹ πŸ‘€ 🚧 πŸ§‘β€πŸ« πŸ“† πŸ’¬
Jakub Trllo
Jakub Trllo

πŸ’» πŸ“– πŸš‡ πŸ‘€ 🚧 πŸ’¬
Petr Kalis
Petr Kalis

πŸ’» πŸ“– πŸš‡ πŸ‘€ 🚧 πŸ’¬
64qam
64qam

πŸ’» πŸ‘€ πŸ“– πŸš‡ πŸ“† 🚧 πŸ–‹ πŸ““
Roy Nieterau
Roy Nieterau

πŸ’» πŸ“– πŸ‘€ πŸ§‘β€πŸ« πŸ’¬
Toke Jepsen
Toke Jepsen

πŸ’» πŸ“– πŸ‘€ πŸ§‘β€πŸ« πŸ’¬
Jiri Sindelar
Jiri Sindelar

πŸ’» πŸ‘€ πŸ“– πŸ–‹ βœ… πŸ““
Simone Barbieri
Simone Barbieri

πŸ’» πŸ“–
karimmozilla
karimmozilla

πŸ’»
Allan I. A.
Allan I. A.

πŸ’»
murphy
murphy

πŸ’» πŸ‘€ πŸ““ πŸ“– πŸ“†
Wijnand Koreman
Wijnand Koreman

πŸ’»
Bo Zhou
Bo Zhou

πŸ’»
ClΓ©ment Hector
ClΓ©ment Hector

πŸ’» πŸ‘€
David Lai
David Lai

πŸ’» πŸ‘€
Derek
Derek

πŸ’» πŸ“–
GΓ‘bor Marinov
GΓ‘bor Marinov

πŸ’» πŸ“–
icyvapor
icyvapor

πŸ’» πŸ“–
JΓ©rΓ΄me LORRAIN
JΓ©rΓ΄me LORRAIN

πŸ’»
David Morris-Oliveros
David Morris-Oliveros

πŸ’»
BenoitConnan
BenoitConnan

πŸ’»
Malthaldar
Malthaldar

πŸ’»
Sven Neve
Sven Neve

πŸ’»
zafrs
zafrs

πŸ’»
FΓ©lix David
FΓ©lix David

πŸ’» πŸ“–
Alexey Bogomolov
Alexey Bogomolov

πŸ’»
From 28e9da8918b86d6ef30f2ffb549483edef3e7a24 Mon Sep 17 00:00:00 2001 From: "allcontributors[bot]" <46447321+allcontributors[bot]@users.noreply.github.com> Date: Thu, 1 Jun 2023 07:23:27 +0000 Subject: [PATCH 181/200] update .all-contributorsrc [skip ci] --- .all-contributorsrc | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.all-contributorsrc b/.all-contributorsrc index b30f3b2499..60812cdb3c 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,6 +1,6 @@ { "projectName": "OpenPype", - "projectOwner": "pypeclub", + "projectOwner": "ynput", "repoType": "github", "repoHost": "https://github.com", "files": [ @@ -319,8 +319,18 @@ "code", "doc" ] + }, + { + "login": "movalex", + "name": "Alexey Bogomolov", + "avatar_url": "https://avatars.githubusercontent.com/u/11698866?v=4", + "profile": "http://abogomolov.com", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, - "skipCi": true + "skipCi": true, + "commitType": "docs" } From eeaa79125ce111272e94e3f5c7f2d6c7f3b154f3 Mon Sep 17 00:00:00 2001 From: JackP Date: Thu, 1 Jun 2023 10:06:08 +0100 Subject: [PATCH 182/200] refactor: use actual pymxs implementation --- .../hosts/max/plugins/load/load_model_fbx.py | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model_fbx.py b/openpype/hosts/max/plugins/load/load_model_fbx.py index 01e6acae12..61101c482d 100644 --- a/openpype/hosts/max/plugins/load/load_model_fbx.py +++ b/openpype/hosts/max/plugins/load/load_model_fbx.py @@ -1,8 +1,5 @@ import os -from openpype.pipeline import ( - load, - get_representation_path -) +from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection @@ -24,10 +21,7 @@ class FbxModelLoader(load.LoaderPlugin): rt.FBXImporterSetParam("Animation", False) rt.FBXImporterSetParam("Cameras", False) rt.FBXImporterSetParam("Preserveinstances", True) - rt.importFile( - filepath, - rt.name("noPrompt"), - using=rt.FBXIMP) + rt.importFile(filepath, rt.name("noPrompt"), using=rt.FBXIMP) container = rt.getNodeByName(f"{name}") if not container: @@ -38,7 +32,8 @@ class FbxModelLoader(load.LoaderPlugin): selection.Parent = container return containerise( - name, [container], context, loader=self.__class__.__name__) + name, [container], context, loader=self.__class__.__name__ + ) def update(self, container, representation): from pymxs import runtime as rt @@ -46,24 +41,21 @@ class FbxModelLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) rt.select(node.Children) - fbx_reimport_cmd = ( - f""" -FBXImporterSetParam "Animation" false -FBXImporterSetParam "Cameras" false -FBXImporterSetParam "AxisConversionMethod" true -FbxExporterSetParam "UpAxis" "Y" -FbxExporterSetParam "Preserveinstances" true -importFile @"{path}" #noPrompt using:FBXIMP - """) - rt.execute(fbx_reimport_cmd) + rt.FBXImporterSetParam("Animation", False) + rt.FBXImporterSetParam("Cameras", False) + rt.FBXImporterSetParam("AxisConversionMethod", True) + rt.FBXImporterSetParam("UpAxis", "Y") + rt.FBXImporterSetParam("Preserveinstances", True) + rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP) with maintained_selection(): rt.select(node) - lib.imprint(container["instance_node"], { - "representation": str(representation["_id"]) - }) + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) def switch(self, container, representation): self.update(container, representation) From e51967a6596efc2d0464728a4df9ac19d232995b Mon Sep 17 00:00:00 2001 From: JackP Date: Thu, 1 Jun 2023 10:09:50 +0100 Subject: [PATCH 183/200] refactor: use correct pymxs --- .../hosts/max/plugins/load/load_pointcache.py | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index b3e12adc7b..5fb9772f87 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -5,9 +5,7 @@ Because of limited api, alembics can be only loaded, but not easily updated. """ import os -from openpype.pipeline import ( - load, get_representation_path -) +from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib @@ -15,9 +13,7 @@ from openpype.hosts.max.api import lib class AbcLoader(load.LoaderPlugin): """Alembic loader.""" - families = ["camera", - "animation", - "pointcache"] + families = ["camera", "animation", "pointcache"] label = "Load Alembic" representations = ["abc"] order = -10 @@ -30,21 +26,17 @@ class AbcLoader(load.LoaderPlugin): file_path = os.path.normpath(self.fname) abc_before = { - c for c in rt.rootNode.Children + c + for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } - abc_export_cmd = (f""" -AlembicImport.ImportToRoot = false - -importFile @"{file_path}" #noPrompt - """) - - self.log.debug(f"Executing command: {abc_export_cmd}") - rt.execute(abc_export_cmd) + rt.AlembicImport.ImportToRoot = False + rt.importFile(file_path, rt.name("noPrompt")) abc_after = { - c for c in rt.rootNode.Children + c + for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } @@ -57,7 +49,8 @@ importFile @"{file_path}" #noPrompt abc_container = abc_containers.pop() return containerise( - name, [abc_container], context, loader=self.__class__.__name__) + name, [abc_container], context, loader=self.__class__.__name__ + ) def update(self, container, representation): from pymxs import runtime as rt @@ -69,9 +62,10 @@ importFile @"{file_path}" #noPrompt for alembic_object in alembic_objects: alembic_object.source = path - lib.imprint(container["instance_node"], { - "representation": str(representation["_id"]) - }) + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) def switch(self, container, representation): self.update(container, representation) From 31b331811cf20c4a8b750d34d11a463aaf28d7ff Mon Sep 17 00:00:00 2001 From: JackP Date: Thu, 1 Jun 2023 10:15:05 +0100 Subject: [PATCH 184/200] refactor: use proper pymxs --- openpype/hosts/max/plugins/load/load_model.py | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index 95ee014e07..febcaed8be 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -1,8 +1,5 @@ - import os -from openpype.pipeline import ( - load, get_representation_path -) +from openpype.pipeline import load, get_representation_path from openpype.hosts.max.api.pipeline import containerise from openpype.hosts.max.api import lib from openpype.hosts.max.api.lib import maintained_selection @@ -24,24 +21,20 @@ class ModelAbcLoader(load.LoaderPlugin): file_path = os.path.normpath(self.fname) abc_before = { - c for c in rt.rootNode.Children + c + for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } - abc_import_cmd = (f""" -AlembicImport.ImportToRoot = false -AlembicImport.CustomAttributes = true -AlembicImport.UVs = true -AlembicImport.VertexColors = true - -importFile @"{file_path}" #noPrompt - """) - - self.log.debug(f"Executing command: {abc_import_cmd}") - rt.execute(abc_import_cmd) + rt.AlembicImport.ImportToRoot = False + rt.AlembicImport.CustomAttributes = True + rt.AlembicImport.UVs = True + rt.AlembicImport.VertexColors = True + rt.importFile(filepath, rt.name("noPrompt")) abc_after = { - c for c in rt.rootNode.Children + c + for c in rt.rootNode.Children if rt.classOf(c) == rt.AlembicContainer } @@ -54,10 +47,12 @@ importFile @"{file_path}" #noPrompt abc_container = abc_containers.pop() return containerise( - name, [abc_container], context, loader=self.__class__.__name__) + name, [abc_container], context, loader=self.__class__.__name__ + ) def update(self, container, representation): from pymxs import runtime as rt + path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) rt.select(node.Children) @@ -76,9 +71,10 @@ importFile @"{file_path}" #noPrompt with maintained_selection(): rt.select(node) - lib.imprint(container["instance_node"], { - "representation": str(representation["_id"]) - }) + lib.imprint( + container["instance_node"], + {"representation": str(representation["_id"])}, + ) def switch(self, container, representation): self.update(container, representation) From d4a807194ebb0971a629f608f3fc2ecf84723394 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 1 Jun 2023 11:24:34 +0200 Subject: [PATCH 185/200] Resolve: Make sure scripts dir exists (#5078) * make sure scripts dir exists * use exist_ok in makedirs --- openpype/hosts/resolve/utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py index 9a161f4865..1213fd9e7a 100644 --- a/openpype/hosts/resolve/utils.py +++ b/openpype/hosts/resolve/utils.py @@ -29,6 +29,9 @@ def setup(env): log.info("Utility Scripts Dir: `{}`".format(util_scripts_paths)) log.info("Utility Scripts: `{}`".format(scripts)) + # Make sure scripts dir exists + os.makedirs(util_scripts_dir, exist_ok=True) + # make sure no script file is in folder for script in os.listdir(util_scripts_dir): path = os.path.join(util_scripts_dir, script) From 3fdbcd3247ad5bbd13230d17bdbec90f65b3985c Mon Sep 17 00:00:00 2001 From: JackP Date: Thu, 1 Jun 2023 10:31:50 +0100 Subject: [PATCH 186/200] fix: incorrect var name --- openpype/hosts/max/plugins/load/load_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_model.py b/openpype/hosts/max/plugins/load/load_model.py index febcaed8be..5f1ae3378e 100644 --- a/openpype/hosts/max/plugins/load/load_model.py +++ b/openpype/hosts/max/plugins/load/load_model.py @@ -30,7 +30,7 @@ class ModelAbcLoader(load.LoaderPlugin): rt.AlembicImport.CustomAttributes = True rt.AlembicImport.UVs = True rt.AlembicImport.VertexColors = True - rt.importFile(filepath, rt.name("noPrompt")) + rt.importFile(file_path, rt.name("noPrompt")) abc_after = { c From aab6e19b5ed0f4f76335cea49342f098ed548319 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 1 Jun 2023 15:48:52 +0200 Subject: [PATCH 187/200] skip roots validation for documents only variant of the functions --- openpype/lib/project_backpack.py | 42 +++++++++++++++++--------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/openpype/lib/project_backpack.py b/openpype/lib/project_backpack.py index 07107ec011..674eaa3b91 100644 --- a/openpype/lib/project_backpack.py +++ b/openpype/lib/project_backpack.py @@ -113,26 +113,29 @@ def pack_project( project_name )) - roots = project_doc["config"]["roots"] - # Determine root directory of project - source_root = None - source_root_name = None - for root_name, root_value in roots.items(): - if source_root is not None: - raise ValueError( - "Packaging is supported only for single root projects" - ) - source_root = root_value - source_root_name = root_name + root_path = None + source_root = {} + project_source_path = None + if not only_documents: + roots = project_doc["config"]["roots"] + # Determine root directory of project + source_root_name = None + for root_name, root_value in roots.items(): + if source_root is not None: + raise ValueError( + "Packaging is supported only for single root projects" + ) + source_root = root_value + source_root_name = root_name - root_path = source_root[platform.system().lower()] - print("Using root \"{}\" with path \"{}\"".format( - source_root_name, root_path - )) + root_path = source_root[platform.system().lower()] + print("Using root \"{}\" with path \"{}\"".format( + source_root_name, root_path + )) - project_source_path = os.path.join(root_path, project_name) - if not os.path.exists(project_source_path): - raise ValueError("Didn't find source of project files") + project_source_path = os.path.join(root_path, project_name) + if not os.path.exists(project_source_path): + raise ValueError("Didn't find source of project files") # Determine zip filepath where data will be stored if not destination_dir: @@ -273,8 +276,7 @@ def unpack_project( low_platform = platform.system().lower() project_name = metadata["project_name"] - source_root = metadata["root"] - root_path = source_root[low_platform] + root_path = metadata["root"].get(low_platform) # Drop existing collection replace_project_documents(project_name, docs, database_name) From abc266e9e5868277b47c2dabdd1697ec13d1a024 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 1 Jun 2023 16:14:29 +0200 Subject: [PATCH 188/200] refactor the script to be frame number rather then frame range --- .../startup/frame_setting_for_read_nodes.py | 47 ++++++++++++++++++ .../startup/ops_frame_setting_for_read.py | 49 ------------------- .../defaults/project_settings/nuke.json | 6 +-- 3 files changed, 50 insertions(+), 52 deletions(-) create mode 100644 openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py delete mode 100644 openpype/hosts/nuke/startup/ops_frame_setting_for_read.py diff --git a/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py b/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py new file mode 100644 index 0000000000..f0cbabe20f --- /dev/null +++ b/openpype/hosts/nuke/startup/frame_setting_for_read_nodes.py @@ -0,0 +1,47 @@ +""" OpenPype custom script for resetting read nodes start frame values """ + +import nuke +import nukescripts + + +class FrameSettingsPanel(nukescripts.PythonPanel): + """ Frame Settings Panel """ + def __init__(self): + nukescripts.PythonPanel.__init__(self, "Set Frame Start (Read Node)") + + # create knobs + self.frame = nuke.Int_Knob( + 'frame', 'Frame Number') + self.selected = nuke.Boolean_Knob("selection") + # add knobs to panel + self.addKnob(self.selected) + self.addKnob(self.frame) + + # set values + self.selected.setValue(False) + self.frame.setValue(nuke.root().firstFrame()) + + def process(self): + """ Process the panel values. """ + # get values + frame = self.frame.value() + if self.selected.value(): + # selected nodes processing + if not nuke.selectedNodes(): + return + for rn_ in nuke.selectedNodes(): + if rn_.Class() != "Read": + continue + rn_["frame_mode"].setValue("start_at") + rn_["frame"].setValue(str(frame)) + else: + # all nodes processing + for rn_ in nuke.allNodes(filter="Read"): + rn_["frame_mode"].setValue("start_at") + rn_["frame"].setValue(str(frame)) + + +def main(): + p_ = FrameSettingsPanel() + if p_.showModalDialog(): + print(p_.process()) diff --git a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py b/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py deleted file mode 100644 index bf98ef83f6..0000000000 --- a/openpype/hosts/nuke/startup/ops_frame_setting_for_read.py +++ /dev/null @@ -1,49 +0,0 @@ -import nuke -import nukescripts -import re - - -class FrameSettingsPanel(nukescripts.PythonPanel): - def __init__(self, node): - nukescripts.PythonPanel.__init__(self, 'Frame Range') - self.read_node = node - # CREATE KNOBS - self.range = nuke.String_Knob('fRange', 'Frame Range', '%s-%s' % - (nuke.root().firstFrame(), - nuke.root().lastFrame())) - self.selected = nuke.Boolean_Knob("selection") - self.info = nuke.Help_Knob("Instruction") - # ADD KNOBS - self.addKnob(self.selected) - self.addKnob(self.range) - self.addKnob(self.info) - self.selected.setValue(False) - - def knobChanged(self, knob): - frame_range = self.range.value() - pattern = r"^(?P-?[0-9]+)(?:(?:-+)(?P-?[0-9]+))?$" - match = re.match(pattern, frame_range) - frame_start = int(match.group("start")) - frame_end = int(match.group("end")) - if not self.read_node: - return - for r in self.read_node: - if self.onchecked(): - if not nuke.selectedNodes(): - return - if r in nuke.selectedNodes(): - r["frame_mode"].setValue("start_at") - r["frame"].setValue(frame_range) - r["first"].setValue(frame_start) - r["last"].setValue(frame_end) - else: - r["frame_mode"].setValue("start_at") - r["frame"].setValue(frame_range) - r["first"].setValue(frame_start) - r["last"].setValue(frame_end) - - def onchecked(self): - if self.selected.value(): - return True - else: - return False diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index a0caa40396..3f8be4c872 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -226,9 +226,9 @@ { "type": "action", "sourcetype": "python", - "title": "Set Frame Range (Read Node)", - "command": "import openpype.hosts.nuke.startup.ops_frame_setting_for_read as popup;import nuke;popup.FrameSettingsPanel(nuke.allNodes('Read')).showModalDialog();", - "tooltip": "Set Frame Range for Read Node(s)" + "title": "Set Frame Start (Read Node)", + "command": "from openpype.hosts.nuke.startup.frame_setting_for_read_nodes import main;main();", + "tooltip": "Set frame start for read node(s)" } ] }, From 8356dfac7e1150cbe31cc8a63f26cee0c0fe1dd0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 1 Jun 2023 16:41:13 +0200 Subject: [PATCH 189/200] py2 compatibility --- openpype/hosts/nuke/startup/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 openpype/hosts/nuke/startup/__init__.py diff --git a/openpype/hosts/nuke/startup/__init__.py b/openpype/hosts/nuke/startup/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From d0886e43fe8efc8b675d9da5ccc9c37c459408d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20David?= Date: Thu, 1 Jun 2023 16:42:42 +0200 Subject: [PATCH 190/200] fix doc --- website/docs/dev_blender.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/dev_blender.md b/website/docs/dev_blender.md index 228447fb64..bed0e4a09d 100644 --- a/website/docs/dev_blender.md +++ b/website/docs/dev_blender.md @@ -9,14 +9,14 @@ toc_max_heading_level: 4 In case you need to execute a python script when Blender is started (aka [`-P`](https://docs.blender.org/manual/en/latest/advanced/command_line/arguments.html#python-options)), for example to programmatically modify a blender file for conformation, you can create an OpenPype hook as follows: ```python -from openpype.hosts.blender.hooks.pre_add_run_python_script_arg import AddPythonScriptToLaunchArgs +from openpype.hosts.blender.hooks import pre_add_run_python_script_arg from openpype.lib import PreLaunchHook class MyHook(PreLaunchHook): """Add python script to be executed before Blender launch.""" - order = AddPythonScriptToLaunchArgs.order - 1 + order = pre_add_run_python_script_arg.AddPythonScriptToLaunchArgs.order - 1 app_groups = [ "blender", ] From 47e5d3646046041e1cf55e55e51e0a5818360add Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 1 Jun 2023 17:01:25 +0200 Subject: [PATCH 191/200] :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 192/200] :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 193/200] :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 194/200] :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 195/200] 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 e64779b3450e66f43bf87a43efb97a177d94360c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 2 Jun 2023 15:07:55 +0200 Subject: [PATCH 196/200] fix restart arguments in tray (#5085) --- openpype/tools/tray/pype_tray.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/tools/tray/pype_tray.py b/openpype/tools/tray/pype_tray.py index 2f3b5251f9..fdc0a8094d 100644 --- a/openpype/tools/tray/pype_tray.py +++ b/openpype/tools/tray/pype_tray.py @@ -633,10 +633,10 @@ class TrayManager: # Create a copy of sys.argv additional_args = list(sys.argv) - # Check last argument from `get_openpype_execute_args` - # - when running from code it is the same as first from sys.argv - if args[-1] == additional_args[0]: - additional_args.pop(0) + # Remove first argument from 'sys.argv' + # - when running from code the first argument is 'start.py' + # - when running from build the first argument is executable + additional_args.pop(0) cleanup_additional_args = False if use_expected_version: @@ -663,7 +663,6 @@ class TrayManager: additional_args = _additional_args args.extend(additional_args) - run_detached_process(args, env=envs) self.exit() From 69297d0b687693f0b29e751a4e38b562c336e969 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 2 Jun 2023 14:40:20 +0100 Subject: [PATCH 197/200] cmds.ls returns list --- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index f4a4a44344..74ca27ff3c 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -33,7 +33,7 @@ def preserve_modelpanel_cameras(container, log=None): panel_cameras = {} for panel in cmds.getPanel(type="modelPanel"): cam = cmds.ls(cmds.modelPanel(panel, query=True, camera=True), - long=True) + long=True)[0] # Often but not always maya returns the transform from the # modelPanel as opposed to the camera shape, so we convert it From 4b6059339e251218e4c5817551d44c3d28e7c056 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 3 Jun 2023 03:24:55 +0000 Subject: [PATCH 198/200] [Automated] Bump version --- openpype/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/version.py b/openpype/version.py index dd23138dee..b55ca42244 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.15.9" +__version__ = "3.15.10-nightly.1" From 5b662ecd20e65893bbf4dfe6121e5d9b5c60aa29 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 3 Jun 2023 03:25:35 +0000 Subject: [PATCH 199/200] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index aa5b8decdc..3406ca8b65 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.15.10-nightly.1 - 3.15.9 - 3.15.9-nightly.2 - 3.15.9-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.3-nightly.2 - 3.14.3-nightly.1 - 3.14.2 - - 3.14.2-nightly.5 validations: required: true - type: dropdown From d37ccb4718e3fa9299d098844ddb0a92c1a09552 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 5 Jun 2023 10:26:42 +0200 Subject: [PATCH 200/200] :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(