From 058fcb97a358a972d34a4cabd45773e249e15cf0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 9 Jun 2023 17:51:54 +0800 Subject: [PATCH 001/291] multiple render camera supports for 3dsmax --- openpype/hosts/max/api/lib_rendersettings.py | 11 ++-------- .../hosts/max/plugins/create/create_render.py | 22 ++++++++++++++----- .../max/plugins/publish/collect_render.py | 5 +++++ .../plugins/publish/submit_max_deadline.py | 11 ++++++++++ 4 files changed, 34 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 91e4a5bf9b..db8dee3340 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -35,15 +35,8 @@ class RenderSettings(object): ) def set_render_camera(self, selection): - for sel in selection: - # to avoid Attribute Error from pymxs wrapper - found = False - if rt.classOf(sel) in rt.Camera.classes: - found = True - rt.viewport.setCamera(sel) - break - if not found: - raise RuntimeError("Camera not found") + # to avoid Attribute Error from pymxs wrapper + return rt.viewport.setCamera(selection[0]) def render_output(self, container): folder = rt.maxFilePath diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 5ad895b86e..ec5635b81b 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -14,7 +14,10 @@ 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) + sel_obj = [ + c for c in rt.Objects + if rt.classOf(c) in rt.Camera.classes] + file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename @@ -24,11 +27,20 @@ class CreateRender(plugin.MaxCreator): instance_data, pre_create_data) # type: CreatedInstance container_name = instance.data.get("instance_node") - container = rt.getNodeByName(container_name) # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container - for obj in sel_obj: - obj.parent = container + if self.selected_nodes: + # set viewport camera for + # rendering(mandatory for deadline) + sel_obj = [ + c for c in rt.getCurrentSelection() + if rt.classOf(c) in rt.Camera.classes] + + if not sel_obj: + raise RuntimeError("Please add at least one camera to the scene " + "before creating the render instance") + + RenderSettings().set_render_camera(sel_obj) # for additional work on the node: # instance_node = rt.getNodeByName(instance.get("instance_node")) @@ -37,7 +49,5 @@ class CreateRender(plugin.MaxCreator): # Changing the Render Setup dialog settings should be done # with the actual Render Setup dialog in a closed state. - # set viewport camera for rendering(mandatory for deadline) - RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index db5c84fad9..cbb3a7b4d6 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -48,6 +48,10 @@ class CollectRender(pyblish.api.InstancePlugin): instance.name, asset_id) self.log.debug("version_doc: {0}".format(version_doc)) + sel_obj = [ + c for c in rt.Objects + if rt.classOf(c) in rt.Camera.classes] + version_int = 1 if version_doc: version_int += int(version_doc["name"]) @@ -78,6 +82,7 @@ class CollectRender(pyblish.api.InstancePlugin): "renderer": renderer, "source": filepath, "plugin": "3dsmax", + "cameras": sel_obj, "frameStart": int(rt.rendStart), "frameEnd": int(rt.rendEnd), "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 b6a30e36b7..ff5adc39ad 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -212,6 +212,17 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, plugin_data["RenderOutput"] = beauty_name # as 3dsmax has version with different languages plugin_data["Language"] = "ENU" + render_cameras = instance.data["cameras"] + if render_cameras: + for i, camera in enumerate(render_cameras): + cam_name = "Camera%s" % (i + 1) + plugin_data[cam_name] = camera.name + # set the default camera + plugin_data["Camera"] = render_cameras[0].name + # set empty camera of Camera 0 for the ' + # correct parameter submission + plugin_data["Camera0"] = None + renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] From 5fc037880fa7ddee5c65d63679d1db2810469411 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 19 Jun 2023 17:33:16 +0800 Subject: [PATCH 002/291] refactor the collector and deadline for multiple camera --- openpype/hosts/max/plugins/publish/collect_render.py | 2 +- .../modules/deadline/plugins/publish/submit_max_deadline.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index cbb3a7b4d6..8e39da0fbb 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -49,7 +49,7 @@ class CollectRender(pyblish.api.InstancePlugin): asset_id) self.log.debug("version_doc: {0}".format(version_doc)) sel_obj = [ - c for c in rt.Objects + c.name for c in rt.Objects if rt.classOf(c) in rt.Camera.classes] version_int = 1 diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index ff5adc39ad..365be7b07b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -216,9 +216,9 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if render_cameras: for i, camera in enumerate(render_cameras): cam_name = "Camera%s" % (i + 1) - plugin_data[cam_name] = camera.name - # set the default camera - plugin_data["Camera"] = render_cameras[0].name + plugin_data[cam_name] = camera + # set the default camera + plugin_data["Camera"] = camera # set empty camera of Camera 0 for the ' # correct parameter submission plugin_data["Camera0"] = None From 6b716b8ce92d5e0466ecb92c03aad86e920b8217 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jul 2023 19:16:52 +0800 Subject: [PATCH 003/291] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a1039b9309..45d7d7134d 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -155,8 +155,8 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, start_frame, - end_frame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) elif renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() @@ -169,15 +169,15 @@ class RenderProducts(object): if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_render_elements( - output_file, name, start_frame, - end_frame, 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, start_frame, - end_frame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) elif renderer == "Arnold": @@ -186,7 +186,7 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( - output_file, name, start_frame, end_frame, img_fmt) + output_file, name, start_frame, end_frame, img_fmt) }) elif renderer in [ "V_Ray_6_Hotfix_3", @@ -198,8 +198,8 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, start_frame, - end_frame, img_fmt) # noqa + output_file, name, start_frame, + end_frame, img_fmt) # noqa }) return render_dict From e96bada178f11d8f24d1036746a3f3e519e95b84 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jul 2023 19:23:03 +0800 Subject: [PATCH 004/291] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 44 ++++++++++--------- openpype/hosts/max/api/lib_rendersettings.py | 2 +- .../hosts/max/plugins/create/create_render.py | 2 +- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 45d7d7134d..433214935d 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -33,6 +33,7 @@ class RenderProducts(object): output_file, start_frame, end_frame, img_fmt ) } + def get_multiple_beauty(self, outputs, cameras): beauty_output_frames = dict() for output, camera in zip(outputs, cameras): @@ -68,9 +69,9 @@ class RenderProducts(object): for name in render_name: aovs_frames.update({ f"{camera}_{name}": ( - self.get_expected_render_elements( - filename, name, start_frame, - end_frame, ext) + self.get_expected_render_elements( + filename, name, start_frame, + end_frame, ext) ) }) elif renderer == "Redshift_Renderer": @@ -84,9 +85,9 @@ class RenderProducts(object): if name == "RsCryptomatte": aovs_frames.update({ f"{camera}_{name}": ( - self.get_expected_render_elements( - filename, name, start_frame, - end_frame, ext) + self.get_expected_render_elements( + filename, name, start_frame, + end_frame, ext) ) }) else: @@ -105,9 +106,9 @@ class RenderProducts(object): for name in render_name: aovs_frames.update({ f"{camera}_{name}": ( - self.get_expected_arnold_product( - filename, name, start_frame, - end_frame, ext) + self.get_expected_arnold_product( + filename, name, start_frame, + end_frame, ext) ) }) elif renderer in [ @@ -120,9 +121,9 @@ class RenderProducts(object): for name in render_name: aovs_frames.update({ f"{camera}_{name}": ( - self.get_expected_render_elements( - filename, name, start_frame, - end_frame, ext) + self.get_expected_render_elements( + filename, name, start_frame, + end_frame, ext) ) }) @@ -155,8 +156,8 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, start_frame, - end_frame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) elif renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() @@ -169,15 +170,15 @@ class RenderProducts(object): if name == "RsCryptomatte": render_dict.update({ name: self.get_expected_render_elements( - output_file, name, start_frame, - end_frame, 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, start_frame, - end_frame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) elif renderer == "Arnold": @@ -186,7 +187,8 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_arnold_product( - output_file, name, start_frame, end_frame, img_fmt) + output_file, name, start_frame, + end_frame, img_fmt) }) elif renderer in [ "V_Ray_6_Hotfix_3", @@ -198,8 +200,8 @@ class RenderProducts(object): for name in render_name: render_dict.update({ name: self.get_expected_render_elements( - output_file, name, start_frame, - end_frame, img_fmt) # noqa + output_file, name, start_frame, + end_frame, img_fmt) # noqa }) return render_dict diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 663f610e45..33cfc6dc4a 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -192,7 +192,7 @@ class RenderSettings(object): renderlayer = rt.batchRenderMgr.GetView(layer_no) # use camera name as renderlayer name renderlayer.name = cam - renderlayer.outputFilename ="{0}_{1}..{2}".format( + renderlayer.outputFilename = "{0}_{1}..{2}".format( output, cam, img_fmt) outputs.append(renderlayer.outputFilename) return outputs diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 64e97fb941..23397e1a98 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -17,7 +17,7 @@ class CreateRender(plugin.MaxCreator): file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename - num_of_renderlayer = rt.batchRenderMgr.numViews + num_of_renderlayer = rt.batchRenderMgr.numViews if num_of_renderlayer > 0: rt.batchRenderMgr.DeleteView(num_of_renderlayer) From 9ce0d97e48d4313b3d50d6edab36ab3608a98799 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jul 2023 19:28:08 +0800 Subject: [PATCH 005/291] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 433214935d..d2d5fb08da 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -77,7 +77,7 @@ class RenderProducts(object): elif renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() if render_name: - rs_aov_files = rt.Execute("renderers.current.separateAovFiles") + rs_aov_files = rt.Execute("renderers.current.separateAovFiles") # noqa # this doesn't work, always returns False # rs_AovFiles = rt.RedShift_Renderer().separateAovFiles if ext == "exr" and not rs_aov_files: @@ -97,9 +97,8 @@ class RenderProducts(object): self.get_expected_render_elements( filename, name, start_frame, end_frame, ext) - ) - }) - + ) + }) elif renderer == "Arnold": render_name = self.get_arnold_product_name() if render_name: From b182f29c90dd8810ab130331ad370e58fc21ab38 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jul 2023 19:32:25 +0800 Subject: [PATCH 006/291] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 24 ++++++-------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index d2d5fb08da..03dfcf3345 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -68,11 +68,9 @@ class RenderProducts(object): if render_name: for name in render_name: aovs_frames.update({ - f"{camera}_{name}": ( - self.get_expected_render_elements( + f"{camera}_{name}": self.get_expected_render_elements( filename, name, start_frame, end_frame, ext) - ) }) elif renderer == "Redshift_Renderer": render_name = self.get_render_elements_name() @@ -84,31 +82,25 @@ class RenderProducts(object): for name in render_name: if name == "RsCryptomatte": aovs_frames.update({ - f"{camera}_{name}": ( - self.get_expected_render_elements( + f"{camera}_{name}": self.get_expected_render_elements( filename, name, start_frame, end_frame, ext) - ) }) else: for name in render_name: aovs_frames.update({ - f"{camera}_{name}": ( - self.get_expected_render_elements( + f"{camera}_{name}": self.get_expected_render_elements( filename, name, start_frame, end_frame, ext) - ) }) elif renderer == "Arnold": render_name = self.get_arnold_product_name() if render_name: for name in render_name: aovs_frames.update({ - f"{camera}_{name}": ( - self.get_expected_arnold_product( + f"{camera}_{name}": self.get_expected_arnold_product( filename, name, start_frame, end_frame, ext) - ) }) elif renderer in [ "V_Ray_6_Hotfix_3", @@ -119,11 +111,9 @@ class RenderProducts(object): if render_name: for name in render_name: aovs_frames.update({ - f"{camera}_{name}": ( - self.get_expected_render_elements( - filename, name, start_frame, - end_frame, ext) - ) + f"{camera}_{name}": self.get_expected_render_elements( + filename, name, start_frame, + end_frame, ext) }) return aovs_frames From 4c01e09ef9d3ae8f4fb67b724e9c227e9afaa7f1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jul 2023 19:35:01 +0800 Subject: [PATCH 007/291] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 03dfcf3345..59417a39fa 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -68,7 +68,7 @@ class RenderProducts(object): if render_name: for name in render_name: aovs_frames.update({ - f"{camera}_{name}": self.get_expected_render_elements( + f"{camera}_{name}": self.get_expected_aovs( filename, name, start_frame, end_frame, ext) }) @@ -82,14 +82,14 @@ class RenderProducts(object): for name in render_name: if name == "RsCryptomatte": aovs_frames.update({ - f"{camera}_{name}": self.get_expected_render_elements( + f"{camera}_{name}": self.get_expected_aovs( filename, name, start_frame, end_frame, ext) }) else: for name in render_name: aovs_frames.update({ - f"{camera}_{name}": self.get_expected_render_elements( + f"{camera}_{name}": self.get_expected_aovs( filename, name, start_frame, end_frame, ext) }) @@ -111,9 +111,9 @@ class RenderProducts(object): if render_name: for name in render_name: aovs_frames.update({ - f"{camera}_{name}": self.get_expected_render_elements( - filename, name, start_frame, - end_frame, ext) + f"{camera}_{name}": self.get_expected_aovs( + filename, name, start_frame, + end_frame, ext) }) return aovs_frames @@ -144,7 +144,7 @@ class RenderProducts(object): if render_name: for name in render_name: render_dict.update({ - name: self.get_expected_render_elements( + name: self.get_expected_aovs( output_file, name, start_frame, end_frame, img_fmt) }) @@ -158,14 +158,14 @@ class RenderProducts(object): for name in render_name: if name == "RsCryptomatte": render_dict.update({ - name: self.get_expected_render_elements( + name: self.get_expected_aovs( output_file, name, start_frame, end_frame, img_fmt) }) else: for name in render_name: render_dict.update({ - name: self.get_expected_render_elements( + name: self.get_expected_aovs( output_file, name, start_frame, end_frame, img_fmt) }) @@ -188,7 +188,7 @@ class RenderProducts(object): if render_name: for name in render_name: render_dict.update({ - name: self.get_expected_render_elements( + name: self.get_expected_aovs( output_file, name, start_frame, end_frame, img_fmt) # noqa }) @@ -251,8 +251,8 @@ class RenderProducts(object): return render_name - def get_expected_render_elements(self, folder, name, - start_frame, end_frame, fmt): + def get_expected_aovs(self, folder, name, + start_frame, end_frame, fmt): """Get all the expected render element output files. """ render_elements = [] for f in range(start_frame, end_frame): From 9f90e3a1f15bba7e26303d3fafee5ef5d9bd238e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 12 Jul 2023 19:35:57 +0800 Subject: [PATCH 008/291] hound fix --- 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 59417a39fa..0b8c53dfa0 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -98,7 +98,7 @@ class RenderProducts(object): if render_name: for name in render_name: aovs_frames.update({ - f"{camera}_{name}": self.get_expected_arnold_product( + f"{camera}_{name}": self.get_expected_arnold_product( # noqa filename, name, start_frame, end_frame, ext) }) From c81818d09509b6b85e3259e6bac362717aa1ca1c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 14 Jul 2023 20:25:49 +0800 Subject: [PATCH 009/291] add multi camera options for render creator --- openpype/hosts/max/api/lib_rendersettings.py | 6 +-- .../hosts/max/plugins/create/create_render.py | 19 ++++++++ .../max/plugins/publish/collect_render.py | 46 ++++++++++--------- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 33cfc6dc4a..18160c66a0 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -177,8 +177,8 @@ class RenderSettings(object): render_element_list.append(aov_name) return render_element_list - def create_batch_render_layer(self, container, - output_dir, cameras): + def batch_render_layer(self, container, + output_dir, cameras): outputs = list() output = os.path.join(output_dir, container) img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa @@ -186,7 +186,7 @@ class RenderSettings(object): camera = rt.getNodeByName(cam) layer_no = rt.batchRenderMgr.FindView(cam) renderlayer = None - if layer_no is None: + if layer_no == 0: renderlayer = rt.batchRenderMgr.CreateView(camera) else: renderlayer = rt.batchRenderMgr.GetView(layer_no) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 23397e1a98..617334753a 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.hosts.max.api.lib_rendersettings import RenderSettings @@ -17,6 +18,7 @@ class CreateRender(plugin.MaxCreator): file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename + instance_data["multiCamera"] = pre_create_data.get("multi_cam") num_of_renderlayer = rt.batchRenderMgr.numViews if num_of_renderlayer > 0: rt.batchRenderMgr.DeleteView(num_of_renderlayer) @@ -29,3 +31,20 @@ class CreateRender(plugin.MaxCreator): container_name = instance.data.get("instance_node") # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) + # TODO: create multiple camera options + if self.selected_nodes: + selected_nodes_name = [] + for sel in self.selected_nodes: + name = sel.name + selected_nodes_name.append(name) + RenderSettings().batch_render_layer( + container_name, filename, + selected_nodes_name) + + def get_pre_create_attr_defs(self): + attrs = super(CreateRender, self).get_pre_create_attr_defs() + return attrs + [ + BoolDef("multi_cam", + label="Multiple Cameras Submission", + default=False), + ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 736ffa5865..ca2f2f444f 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -26,22 +26,7 @@ class CollectRender(pyblish.api.InstancePlugin): file = rt.maxFileName current_file = os.path.join(folder, file) filepath = current_file.replace("\\", "/") - container_name = instance.data.get("instance_node") context.data['currentFile'] = current_file - cameras = instance.data.get("members") - sel_cam = [ - c.name for c in cameras - if rt.classOf(c) in rt.Camera.classes] - render_dir = os.path.dirname(rt.rendOutputFilename) - outputs = RenderSettings().create_batch_render_layer( - container_name, render_dir, sel_cam - ) - aov_outputs = RenderSettings().get_batch_render_elements( - container_name, render_dir, sel_cam - ) - files_aov = RenderProducts().get_multiple_beauty(outputs, cameras) - aovs = RenderProducts().get_multiple_aovs(outputs, cameras) - files_aov.update(aovs) asset = get_current_asset_name() files_by_aov = RenderProducts().get_beauty(instance.name) @@ -49,11 +34,33 @@ class CollectRender(pyblish.api.InstancePlugin): aovs = RenderProducts().get_aovs(instance.name) files_by_aov.update(aovs) + if instance.data.get("multiCamera"): + cameras = instance.data.get("members") + if not cameras: + raise RuntimeError("There should be at least" + " one renderable camera in container") + sel_cam = [ + c.name for c in cameras + if rt.classOf(c) in rt.Camera.classes] + container_name = instance.data.get("instance_node") + render_dir = os.path.dirname(rt.rendOutputFilename) + outputs = RenderSettings().batch_render_layer( + container_name, render_dir, sel_cam + ) + + instance.data["cameras"] = sel_cam + + files_by_aov = RenderProducts().get_multiple_beauty( + outputs, sel_cam) + aovs = RenderProducts().get_multiple_aovs( + outputs, sel_cam) + files_by_aov.update(aovs) + if "expectedFiles" not in instance.data: instance.data["expectedFiles"] = list() instance.data["files"] = list() - instance.data["expectedFiles"].append(files_aov) - instance.data["files"].append(files_aov) + instance.data["expectedFiles"].append(files_by_aov) + instance.data["files"].append(files_by_aov) img_format = RenderProducts().image_format() project_name = context.data["projectName"] @@ -94,13 +101,10 @@ class CollectRender(pyblish.api.InstancePlugin): "renderer": renderer, "source": filepath, "plugin": "3dsmax", - "cameras": sel_cam, "frameStart": int(rt.rendStart), "frameEnd": int(rt.rendEnd), "version": version_int, - "farm": True, - "renderoutput": outputs, - "aovoutput": aov_outputs + "farm": True } instance.data.update(data) From 22524c1d8473226b7687626b30b34ab237708fb5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 17 Jul 2023 17:22:09 +0800 Subject: [PATCH 010/291] add option for multiple job and plugin infos --- openpype/hosts/max/api/lib_rendersettings.py | 21 ++-- .../deadline/abstract_submit_deadline.py | 25 ++++ .../plugins/publish/submit_max_deadline.py | 113 +++++++++++++++--- 3 files changed, 136 insertions(+), 23 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 18160c66a0..f2294fbf95 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -160,7 +160,7 @@ class RenderSettings(object): return orig_render_elem def get_batch_render_elements(self, container, - output_dir, cameras): + output_dir, camera): render_element_list = list() output = os.path.join(output_dir, container) render_elem = rt.maxOps.GetCurRenderElementMgr() @@ -168,15 +168,20 @@ class RenderSettings(object): if render_elem_num < 0: return img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - for cam in cameras: - for i in range(render_elem_num): - renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") - aov_name = "{0}_{1}_{2}..{3}".format( - output, cam, renderpass, img_fmt) - render_element_list.append(aov_name) + + for i in range(render_elem_num): + renderlayer_name = render_elem.GetRenderElement(i) + target, renderpass = str(renderlayer_name).split(":") + aov_name = "{0}_{1}_{2}..{3}".format( + output, camera, renderpass, img_fmt) + render_element_list.append(aov_name) return render_element_list + def get_batch_render_output(self, camera): + target_layer_no = rt.batchRenderMgr.FindView(camera) + target_layer = rt.batchRenderMgr.GetView(target_layer_no) + return target_layer.outputFilename + def batch_render_layer(self, container, output_dir, cameras): outputs = list() diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 551a2f7373..de6babf555 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -592,6 +592,31 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, return file_path + def get_job_info_through_camera(self, camera=None): + """Get the job parameters for deadline submission when + multi-camera is enabled. + Args: + infos(dict): a dictionary with job info. + """ + pass + + def get_plugin_info_through_camera(self, camera=None): + """Get the plugin parameters for deadline submission when + multi-camera is enabled. + Args: + infos(dict): a dictionary with plugin info. + """ + pass + + def _use_published_name_for_multiples(self, data): + """Process the parameters submission for deadline when + user enables multi-cameras option. + Args: + job_info_list (list): A list of multiple job infos + plugin_info_list (list): A list of multiple plugin infos + """ + pass + def assemble_payload( self, job_info=None, plugin_info=None, aux_files=None): """Assemble payload data from its various parts. diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 365be7b07b..6bbc956f55 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -56,7 +56,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, cls.priority) cls.chuck_size = settings.get("chunk_size", cls.chunk_size) cls.group = settings.get("group", cls.group) - + # TODO: multiple camera instance, separate job infos def get_job_info(self): job_info = DeadlineJobInfo(Plugin="3dsmax") @@ -73,7 +73,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, src_filepath = context.data["currentFile"] src_filename = os.path.basename(src_filepath) - job_info.Name = "%s - %s" % (src_filename, instance.name) job_info.BatchName = src_filename job_info.Plugin = instance.data["plugin"] @@ -179,9 +178,19 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, } self.log.debug("Submitting 3dsMax render..") - payload = self._use_published_name(payload_data) - job_info, plugin_info = payload - self.submit(self.assemble_payload(job_info, plugin_info)) + #TODO: multiple camera options + if instance.data.get("multiCamera"): + payload = self._use_published_name_for_multiples(payload_data) + job_infos, plugin_infos = payload + for job_info, plugin_info in zip(job_infos, plugin_infos): + self.log.debug(f"job_info: {job_info}") + self.log.debug(f"plugin_info: {plugin_info}") + submission = self.assemble_payload(job_info, plugin_info) + self.submit(submission) + else: + payload = self._use_published_name(payload_data) + job_info, plugin_info = payload + self.submit(self.assemble_payload(job_info, plugin_info)) def _use_published_name(self, data): instance = self._instance @@ -212,16 +221,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, plugin_data["RenderOutput"] = beauty_name # as 3dsmax has version with different languages plugin_data["Language"] = "ENU" - render_cameras = instance.data["cameras"] - if render_cameras: - for i, camera in enumerate(render_cameras): - cam_name = "Camera%s" % (i + 1) - plugin_data[cam_name] = camera - # set the default camera - plugin_data["Camera"] = camera - # set empty camera of Camera 0 for the ' - # correct parameter submission - plugin_data["Camera0"] = None renderer_class = get_current_renderer() @@ -250,6 +249,90 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return job_info, plugin_info + def get_job_info_through_camera(self, camera): + instance = self._instance + context = instance.context + job_info = copy.deepcopy(self.job_info) + exp = instance.data.get("expectedFiles") + + src_filepath = context.data["currentFile"] + src_filename = os.path.basename(src_filepath) + job_info.Name = "%s - %s - %s" % ( + src_filename, instance.name, camera) + for filepath in self._iter_expected_files(exp): + if camera not in filepath: + continue + job_info.OutputDirectory += os.path.dirname(filepath) + job_info.OutputFilename += os.path.basename(filepath) + + return job_info + # set the output filepath with the relative camera + + def get_plugin_info_through_camera(self, camera): + instance = self._instance + # set the target camera + plugin_info = copy.deepcopy(self.plugin_info) + plugin_data = {} + # set the output filepath with the relative camera + files = instance.data.get("expectedFiles") + if not files: + raise RuntimeError("No render elements found") + first_file = next(self._iter_expected_files(files)) + old_output_dir = os.path.dirname(first_file) + rgb_output = RenderSettings().get_batch_render_output(camera) # noqa + rgb_bname = os.path.basename(rgb_output) + dir = os.path.dirname(first_file) + beauty_name = f"{dir}/{rgb_bname}" + beauty_name = beauty_name.replace("\\", "/") + plugin_info["RenderOutput"] = beauty_name + renderer_class = get_current_renderer() + + renderer = str(renderer_class).split(":")[0] + 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 = RenderSettings().get_batch_render_elements( + instance.name, old_output_dir, camera + ) + 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_info["RenderElementOutputFilename%d" % i] = new_elem # noqa + + if camera: + # set the default camera + plugin_data["Camera"] = camera + + plugin_data["Camera0"] = None + + plugin_info.update(plugin_data) + return plugin_info + + def _use_published_name_for_multiples(self, data): + """Process the parameters submission for deadline when + user enables multi-cameras option. + Args: + job_info_list (list): A list of multiple job infos + plugin_info_list (list): A list of multiple plugin infos + """ + job_info_list = [] + plugin_info_list = [] + instance = self._instance + cameras = instance.data.get("cameras", []) + for cam in cameras: + job_info = self.get_job_info_through_camera(cam) + plugin_info = self.get_plugin_info_through_camera(cam) + job_info_list.append(job_info) + plugin_info_list.append(plugin_info) + + return job_info_list, plugin_info_list + def from_published_scene(self, replace_in_path=True): instance = self._instance if instance.data["renderer"] == "Redshift_Renderer": From 5ad81ea6bff0544cffdf7abe04a29eb64d8ec58c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 17 Jul 2023 17:44:24 +0800 Subject: [PATCH 011/291] make sure camera parameters are being collected accurately --- .../plugins/publish/submit_max_deadline.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 6bbc956f55..23a2b4c679 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -131,11 +131,11 @@ 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) + if not instance.data.get("multiCamera"): + 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) return job_info @@ -178,7 +178,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, } self.log.debug("Submitting 3dsMax render..") - #TODO: multiple camera options + if instance.data.get("multiCamera"): payload = self._use_published_name_for_multiples(payload_data) job_infos, plugin_infos = payload @@ -306,9 +306,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, plugin_info["RenderElementOutputFilename%d" % i] = new_elem # noqa if camera: - # set the default camera + # set the default camera and target camera + # (weird parameters from max) plugin_data["Camera"] = camera - + plugin_data["Camera1"] = camera plugin_data["Camera0"] = None plugin_info.update(plugin_data) From f5c8994fa31039b0549eddaa7bbe5ec8e5c3cee8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 17 Jul 2023 20:21:34 +0800 Subject: [PATCH 012/291] get the correct naming for all render outputs --- openpype/hosts/max/api/lib_renderproducts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 5446e4fca3..863dccd99e 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -41,6 +41,8 @@ class RenderProducts(object): beauty_output_frames = dict() for output, camera in zip(outputs, cameras): filename, ext = os.path.splitext(output) + filename = filename.replace(".", "") + ext = ext.replace(".", "") start_frame = int(rt.rendStart) end_frame = int(rt.rendEnd) + 1 new_beauty = self.get_expected_beauty( @@ -57,6 +59,8 @@ class RenderProducts(object): aovs_frames = {} for output, camera in zip(outputs, cameras): filename, ext = os.path.splitext(output) + filename = filename.replace(".", "") + ext = ext.replace(".", "") start_frame = int(rt.rendStart) end_frame = int(rt.rendEnd) + 1 From 523ad0519dcbe52d4821d0981ddc4cc6962aaf60 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 17 Jul 2023 20:44:29 +0800 Subject: [PATCH 013/291] get all beauty as expected files in a correct manner --- openpype/hosts/max/api/lib_renderproducts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 863dccd99e..eaf5015ba8 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -48,9 +48,10 @@ class RenderProducts(object): new_beauty = self.get_expected_beauty( filename, start_frame, end_frame, ext ) - beauty_output_frames = ({ + beauty_output = ({ f"{camera}_beauty": new_beauty }) + beauty_output_frames.update(beauty_output) return beauty_output_frames def get_multiple_aovs(self, outputs, cameras): From 3bb7f871d452fdef0e46db3a92518e8dd8d94afe Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 18 Jul 2023 17:57:04 +0800 Subject: [PATCH 014/291] bug fix camera instance doesn't include anything --- .../deadline/plugins/publish/submit_publish_job.py | 12 ++++++++++-- 1 file changed, 10 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 01a5c55286..1665a05f1e 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -361,7 +361,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) @@ -472,6 +472,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, host_name = self.context.data["hostName"] subset = instance_data["subset"] cameras = instance_data.get("cameras", []) + self.log.info(f"camera: {cameras}") instances = [] # go through aovs in expected files for aov, files in exp_files[0].items(): @@ -497,7 +498,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, task[0].upper(), task[1:], subset[0].upper(), subset[1:]) - cam = [c for c in cameras if c in col.head] + cam = [c for c in cameras if c in cols[0].head] + self.log.debug(f"cam: {cam}") if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) @@ -898,6 +900,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, for v in values: instance_skeleton_data[v] = instance.data.get(v) + # if there are cameras, get the camera + # ]data for instance_skeleton_data + cameras = instance.data.get("cameras") + if cameras: + instance_skeleton_data["cameras"] = cameras + # look into instance data if representations are not having any # which are having tag `publish_on_farm` and include them for repre in instance.data.get("representations", []): From f349e720b09c78e6541eb7b10d6eb31f9020b425 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 20 Jul 2023 23:45:08 +0800 Subject: [PATCH 015/291] exporting camera scene through instanceplugin by subprocess --- openpype/hosts/max/api/lib_rendersettings.py | 18 +++ .../publish/save_scenes_for_cameras.py | 110 ++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 0a6f6569bf..2d453b9712 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -182,6 +182,24 @@ class RenderSettings(object): target_layer = rt.batchRenderMgr.GetView(target_layer_no) return target_layer.outputFilename + def batch_render_elements(self, camera): + target_layer_no = rt.batchRenderMgr.FindView(camera) + target_layer = rt.batchRenderMgr.GetView(target_layer_no) + outputfilename = target_layer.outputFilename + directory = os.path.dirname(outputfilename) + render_elem = rt.maxOps.GetCurRenderElementMgr() + render_elem_num = render_elem.NumRenderElements() + if render_elem_num < 0: + return + ext = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa + + for i in range(render_elem_num): + renderlayer_name = render_elem.GetRenderElement(i) + target, renderpass = str(renderlayer_name).split(":") + aov_name = "{0}_{1}_{2}..{3}".format( + directory, camera, renderpass, ext) + render_elem.SetRenderElementFileName(i, aov_name) + def batch_render_layer(self, container, output_dir, cameras): outputs = list() diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py new file mode 100644 index 0000000000..d3357ba478 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -0,0 +1,110 @@ +import pyblish.api +import os +import sys +import tempfile + +from pymxs import runtime as rt +from openpype.lib import run_subprocess +from openpype.hosts.max.api.lib import get_max_version +from openpype.hosts.max.api.lib_rendersettings import RenderSettings +from openpype.hosts.max.api.lib_renderproducts import RenderProducts + + +class SaveScenesForCamera(pyblish.api.InstancePlugin): + """Save scene files for multiple cameras before + deadline submission + + """ + + label = "Save Scene files for cameras" + order = pyblish.api.ExtractorOrder - 0.48 + hosts = ["max"] + families = ["maxrender", "workfile"] + + def process(self, instance): + if not instance.data.get("multiCamera"): + self.log.debug("Skipping instance...") + return + current_folder = rt.maxFilePath + current_filename = rt.maxFileName + current_filepath = os.path.join(current_folder, current_filename) + camera_scene_files = [] + repres_list = [] + scripts = [] + filename, ext = os.path.splitext(current_filename) + fmt = RenderProducts().image_format() + cameras = instance.data.get("cameras") + if not cameras: + return + new_folder = "{}_{}".format(current_folder, filename) + os.makedirs(new_folder, exist_ok=True) + for camera in cameras: + new_output = RenderSettings().get_batch_render_output(camera) # noqa + new_output = new_output.replace("\\", "/") + new_filename = "{}_{}{}".format( + filename, camera, ext) + new_filepath = os.path.join(new_folder, new_filename) + new_filepath = new_filepath.replace("\\", "/") + camera_scene_files.append(new_filepath) + RenderSettings().batch_render_elements(camera) + rt.rendOutputFilename = new_output + rt.saveMaxFile(current_filepath) + script = (""" +from pymxs import runtime as rt +import os +new_filepath = "{new_filepath}" +new_output = "{new_output}" +camera = "{camera}" +rt.rendOutputFilename = new_output +directory = os.path.dirname(new_output) +render_elem = rt.maxOps.GetCurRenderElementMgr() +render_elem_num = render_elem.NumRenderElements() +if render_elem_num > 0: + ext = "{ext}" + for i in range(render_elem_num): + renderlayer_name = render_elem.GetRenderElement(i) + target, renderpass = str(renderlayer_name).split(":") + aov_name = directory + "_" + camera + "_" + renderpass + "." + "." + ext + render_elem.SetRenderElementFileName(i, aov_name) +rt.saveMaxFile(new_filepath) + """).format(new_filepath=new_filepath, + new_output=new_output, + camera=camera, + ext=fmt) + scripts.append(script) + + max_version = get_max_version() + maxBatch_exe = os.path.join(os.getenv(f"ADSK_3DSMAX_x64_{max_version}"), "3dsmaxbatch") + maxBatch_exe = maxBatch_exe.replace("\\", "/") + if sys.platform == "windows": + maxBatch_exe += ".exe" + maxBatch_exe = os.path.normpath(maxBatch_exe) + with tempfile.TemporaryDirectory() as tmp_dir_name: + tmp_script_path = os.path.join(tmp_dir_name, "extract_scene_files.py") + log_file =os.path.join(tmp_dir_name, "fatal.log") + self.log.info("Using script file: {}".format(tmp_script_path)) + + with open(tmp_script_path, "wt") as tmp: + for script in scripts: + tmp.write(script+"\n") + tmp.write("rt.quitMax(quiet=True)"+"\n") + tmp.write("import time"+"\n") + tmp.write("time.sleep(3)") + + try: + current_filepath = current_filepath.replace("\\","/") + log_file = log_file.replace("\\", "/") + tmp_script_path = tmp_script_path.replace("\\","/") + run_subprocess([maxBatch_exe, tmp_script_path, + "-sceneFile", current_filepath]) + except RuntimeError: + self.log.debug("Checking the scene files existing or not") + + for camera_scene in camera_scene_files: + if not os.path.exists(camera_scene): + self.log.error("Camera scene files not existed yet!") + raise RuntimeError("MaxBatch.exe doesn't run as expected") + self.log.debug(f"Found Camera scene:{camera_scene}") + + if "sceneFiles" not in instance.data: + instance.data["sceneFiles"] = camera_scene_files From f4a864c0d2b0141f5cd81e40edb25b77374b22d7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 20 Jul 2023 23:50:29 +0800 Subject: [PATCH 016/291] cosmetic fix --- .../plugins/publish/save_scenes_for_cameras.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index d3357ba478..2abdb5dba1 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -74,27 +74,27 @@ rt.saveMaxFile(new_filepath) scripts.append(script) max_version = get_max_version() - maxBatch_exe = os.path.join(os.getenv(f"ADSK_3DSMAX_x64_{max_version}"), "3dsmaxbatch") + maxBatch_exe = os.path.join( + os.getenv(f"ADSK_3DSMAX_x64_{max_version}"), "3dsmaxbatch") maxBatch_exe = maxBatch_exe.replace("\\", "/") if sys.platform == "windows": maxBatch_exe += ".exe" maxBatch_exe = os.path.normpath(maxBatch_exe) with tempfile.TemporaryDirectory() as tmp_dir_name: - tmp_script_path = os.path.join(tmp_dir_name, "extract_scene_files.py") - log_file =os.path.join(tmp_dir_name, "fatal.log") + tmp_script_path = os.path.join( + tmp_dir_name, "extract_scene_files.py") self.log.info("Using script file: {}".format(tmp_script_path)) with open(tmp_script_path, "wt") as tmp: for script in scripts: - tmp.write(script+"\n") - tmp.write("rt.quitMax(quiet=True)"+"\n") - tmp.write("import time"+"\n") + tmp.write(script + "\n") + tmp.write("rt.quitMax(quiet=True)" + "\n") + tmp.write("import time" + "\n") tmp.write("time.sleep(3)") try: - current_filepath = current_filepath.replace("\\","/") - log_file = log_file.replace("\\", "/") - tmp_script_path = tmp_script_path.replace("\\","/") + current_filepath = current_filepath.replace("\\", "/") + tmp_script_path = tmp_script_path.replace("\\", "/") run_subprocess([maxBatch_exe, tmp_script_path, "-sceneFile", current_filepath]) except RuntimeError: From 7ae8b86c58ab061daa8b9acefab478adc14651fb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 20 Jul 2023 23:51:21 +0800 Subject: [PATCH 017/291] hound fix --- openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index 2abdb5dba1..af47959f8f 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -64,7 +64,7 @@ if render_elem_num > 0: for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - aov_name = directory + "_" + camera + "_" + renderpass + "." + "." + ext + aov_name = directory + "_" + camera + "_" + renderpass + "." + "." + ext # noqa render_elem.SetRenderElementFileName(i, aov_name) rt.saveMaxFile(new_filepath) """).format(new_filepath=new_filepath, From 3377150fe65342a421107fe71d08f72303cd0151 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jul 2023 16:14:36 +0800 Subject: [PATCH 018/291] clean up the save_scene_camera code --- .../plugins/publish/save_scenes_for_cameras.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index af47959f8f..c6d264de32 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -29,7 +29,6 @@ class SaveScenesForCamera(pyblish.api.InstancePlugin): current_filename = rt.maxFileName current_filepath = os.path.join(current_folder, current_filename) camera_scene_files = [] - repres_list = [] scripts = [] filename, ext = os.path.splitext(current_filename) fmt = RenderProducts().image_format() @@ -88,9 +87,6 @@ rt.saveMaxFile(new_filepath) with open(tmp_script_path, "wt") as tmp: for script in scripts: tmp.write(script + "\n") - tmp.write("rt.quitMax(quiet=True)" + "\n") - tmp.write("import time" + "\n") - tmp.write("time.sleep(3)") try: current_filepath = current_filepath.replace("\\", "/") @@ -100,11 +96,23 @@ rt.saveMaxFile(new_filepath) except RuntimeError: self.log.debug("Checking the scene files existing or not") - for camera_scene in camera_scene_files: + for camera_scene, camera in zip(camera_scene_files, cameras): if not os.path.exists(camera_scene): self.log.error("Camera scene files not existed yet!") raise RuntimeError("MaxBatch.exe doesn't run as expected") self.log.debug(f"Found Camera scene:{camera_scene}") + instance.context.data["currentFile"] = camera_scene + representation = { + "name": "max", + "ext": "max", + "files": os.path.basename(camera_scene), + "stagingDir": new_folder, + "outputName": camera + } + self.log.debug(f"representation: {representation}") + if instance.data.get("representations") is None: + instance.data["representations"] = [] + instance.data["representations"].append(representation) if "sceneFiles" not in instance.data: instance.data["sceneFiles"] = camera_scene_files From 4144ca8e18fc2c5cf1e3b4971748cffa57ff3a9a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jul 2023 16:45:48 +0800 Subject: [PATCH 019/291] clean up the save_scene_camera code --- .../publish/save_scenes_for_cameras.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index c6d264de32..2369cf78a3 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -22,13 +22,11 @@ class SaveScenesForCamera(pyblish.api.InstancePlugin): families = ["maxrender", "workfile"] def process(self, instance): - if not instance.data.get("multiCamera"): - self.log.debug("Skipping instance...") - return current_folder = rt.maxFilePath current_filename = rt.maxFileName current_filepath = os.path.join(current_folder, current_filename) camera_scene_files = [] + repres_list = [] scripts = [] filename, ext = os.path.splitext(current_filename) fmt = RenderProducts().image_format() @@ -94,25 +92,25 @@ rt.saveMaxFile(new_filepath) run_subprocess([maxBatch_exe, tmp_script_path, "-sceneFile", current_filepath]) except RuntimeError: - self.log.debug("Checking the scene files existing or not") + self.log.debug("Checking the scene files existing") for camera_scene, camera in zip(camera_scene_files, cameras): if not os.path.exists(camera_scene): self.log.error("Camera scene files not existed yet!") raise RuntimeError("MaxBatch.exe doesn't run as expected") self.log.debug(f"Found Camera scene:{camera_scene}") - instance.context.data["currentFile"] = camera_scene representation = { - "name": "max", - "ext": "max", - "files": os.path.basename(camera_scene), - "stagingDir": new_folder, - "outputName": camera + "name": camera, + "ext": "max", + "files": os.path.basename(camera_scene), + "stagingDir": os.path.dirname(camera_scene), + "outputName": camera } - self.log.debug(f"representation: {representation}") - if instance.data.get("representations") is None: - instance.data["representations"] = [] - instance.data["representations"].append(representation) + repres_list.append(representation) if "sceneFiles" not in instance.data: instance.data["sceneFiles"] = camera_scene_files + + if instance.data.get("representations") is None: + instance.data["representations"] = [] + instance.data["representations"] = (repres_list) From d62b410efbc3776598d7705c0ee223f672e4a994 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jul 2023 16:57:51 +0800 Subject: [PATCH 020/291] clean up the save_scene_camera code --- openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index 2369cf78a3..3031c0d4cf 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -113,4 +113,4 @@ rt.saveMaxFile(new_filepath) if instance.data.get("representations") is None: instance.data["representations"] = [] - instance.data["representations"] = (repres_list) + instance.data["representations"]= repres_list From 855143d217400b813f128b0804e599332dbe95d6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jul 2023 17:11:26 +0800 Subject: [PATCH 021/291] clean up --- .../plugins/publish/save_scenes_for_cameras.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index 3031c0d4cf..8aff9b58dd 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -11,8 +11,8 @@ from openpype.hosts.max.api.lib_renderproducts import RenderProducts class SaveScenesForCamera(pyblish.api.InstancePlugin): - """Save scene files for multiple cameras before - deadline submission + """Save scene files for multiple cameras without + editing the original scene before deadline submission """ @@ -26,7 +26,6 @@ class SaveScenesForCamera(pyblish.api.InstancePlugin): current_filename = rt.maxFileName current_filepath = os.path.join(current_folder, current_filename) camera_scene_files = [] - repres_list = [] scripts = [] filename, ext = os.path.splitext(current_filename) fmt = RenderProducts().image_format() @@ -99,18 +98,6 @@ rt.saveMaxFile(new_filepath) self.log.error("Camera scene files not existed yet!") raise RuntimeError("MaxBatch.exe doesn't run as expected") self.log.debug(f"Found Camera scene:{camera_scene}") - representation = { - "name": camera, - "ext": "max", - "files": os.path.basename(camera_scene), - "stagingDir": os.path.dirname(camera_scene), - "outputName": camera - } - repres_list.append(representation) if "sceneFiles" not in instance.data: instance.data["sceneFiles"] = camera_scene_files - - if instance.data.get("representations") is None: - instance.data["representations"] = [] - instance.data["representations"]= repres_list From 7ecb5ba056f2e5adf5f6ebbc6af19db16b207d56 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jul 2023 17:14:23 +0800 Subject: [PATCH 022/291] hound fix --- openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index 8aff9b58dd..918b6cda43 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -93,7 +93,7 @@ rt.saveMaxFile(new_filepath) except RuntimeError: self.log.debug("Checking the scene files existing") - for camera_scene, camera in zip(camera_scene_files, cameras): + for camera_scene in camera_scene_files: if not os.path.exists(camera_scene): self.log.error("Camera scene files not existed yet!") raise RuntimeError("MaxBatch.exe doesn't run as expected") From 0a7d24ba5f2f3b8dfaaeb7b0fbf8309c27ce5c38 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jul 2023 18:27:00 +0800 Subject: [PATCH 023/291] use sys.executable to find the directory of maxBatch.exe --- openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index 918b6cda43..0ccc69591a 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -5,7 +5,6 @@ import tempfile from pymxs import runtime as rt from openpype.lib import run_subprocess -from openpype.hosts.max.api.lib import get_max_version from openpype.hosts.max.api.lib_rendersettings import RenderSettings from openpype.hosts.max.api.lib_renderproducts import RenderProducts @@ -69,9 +68,8 @@ rt.saveMaxFile(new_filepath) ext=fmt) scripts.append(script) - max_version = get_max_version() maxBatch_exe = os.path.join( - os.getenv(f"ADSK_3DSMAX_x64_{max_version}"), "3dsmaxbatch") + os.path.dirname(sys.executable), "3dsmaxbatch") maxBatch_exe = maxBatch_exe.replace("\\", "/") if sys.platform == "windows": maxBatch_exe += ".exe" From 7692b95f1c113c8f2da8c0ae335398c0f7740a17 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jul 2023 19:37:00 +0800 Subject: [PATCH 024/291] change the scene_path command in submit 3dsmax renders and make sure render elements have the correct path --- .../publish/save_scenes_for_cameras.py | 19 +++++++++++-------- .../plugins/publish/submit_max_deadline.py | 5 ++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index 0ccc69591a..9561f53996 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -18,7 +18,7 @@ class SaveScenesForCamera(pyblish.api.InstancePlugin): label = "Save Scene files for cameras" order = pyblish.api.ExtractorOrder - 0.48 hosts = ["max"] - families = ["maxrender", "workfile"] + families = ["maxrender"] def process(self, instance): current_folder = rt.maxFilePath @@ -47,11 +47,13 @@ class SaveScenesForCamera(pyblish.api.InstancePlugin): script = (""" from pymxs import runtime as rt import os +filename = "{filename}" new_filepath = "{new_filepath}" new_output = "{new_output}" camera = "{camera}" rt.rendOutputFilename = new_output -directory = os.path.dirname(new_output) +directory = os.path.dirname(rt.rendOutputFilename) +directory = os.path.join(directory, filename) render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num > 0: @@ -62,18 +64,19 @@ if render_elem_num > 0: aov_name = directory + "_" + camera + "_" + renderpass + "." + "." + ext # noqa render_elem.SetRenderElementFileName(i, aov_name) rt.saveMaxFile(new_filepath) - """).format(new_filepath=new_filepath, + """).format(filename=filename, + new_filepath=new_filepath, new_output=new_output, camera=camera, ext=fmt) scripts.append(script) - maxBatch_exe = os.path.join( + maxbatch_exe = os.path.join( os.path.dirname(sys.executable), "3dsmaxbatch") - maxBatch_exe = maxBatch_exe.replace("\\", "/") + maxbatch_exe = maxbatch_exe.replace("\\", "/") if sys.platform == "windows": - maxBatch_exe += ".exe" - maxBatch_exe = os.path.normpath(maxBatch_exe) + maxbatch_exe += ".exe" + maxbatch_exe = os.path.normpath(maxbatch_exe) with tempfile.TemporaryDirectory() as tmp_dir_name: tmp_script_path = os.path.join( tmp_dir_name, "extract_scene_files.py") @@ -86,7 +89,7 @@ rt.saveMaxFile(new_filepath) try: current_filepath = current_filepath.replace("\\", "/") tmp_script_path = tmp_script_path.replace("\\", "/") - run_subprocess([maxBatch_exe, tmp_script_path, + run_subprocess([maxbatch_exe, tmp_script_path, "-sceneFile", current_filepath]) except RuntimeError: self.log.debug("Checking the scene files existing") diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 5fed10a7c5..57f4353dcc 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -12,7 +12,6 @@ from openpype.pipeline import ( legacy_io, OpenPypePyblishPluginMixin ) -from openpype.settings import get_project_settings from openpype.hosts.max.api.lib import ( get_current_renderer, get_multipass_setting @@ -272,8 +271,12 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance # set the target camera plugin_info = copy.deepcopy(self.plugin_info) + plugin_data = {} # set the output filepath with the relative camera + for camera_scene_path in instance.data.get("sceneFiles"): + if camera in camera_scene_path: + plugin_data["SceneFile"] = camera_scene_path files = instance.data.get("expectedFiles") if not files: raise RuntimeError("No render elements found") From b8952629705de15b9fbcc2ca18bba16bd83b7014 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jul 2023 20:51:16 +0800 Subject: [PATCH 025/291] if multiCamera enabled, the deadline submission wont use the published workfiles to render --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 6 +++++- 1 file changed, 5 insertions(+), 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 57f4353dcc..f3d873a1db 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -349,7 +349,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if instance.data["renderer"] == "Redshift_Renderer": self.log.debug("Using Redshift...published scene wont be used..") replace_in_path = False - return replace_in_path + + if instance.data["multiCamera"] == True: + 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 5893675dc0e96dbcef99c5dc89fabc5b9831a783 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 21 Jul 2023 20:53:35 +0800 Subject: [PATCH 026/291] some cosmetic 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 f3d873a1db..904732c4b4 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -350,7 +350,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, self.log.debug("Using Redshift...published scene wont be used..") replace_in_path = False - if instance.data["multiCamera"] == True: + if instance.data.get("multiCamera"): self.log.debug("Using Redshift...published scene wont be used..") replace_in_path = False return replace_in_path From 9801ad52a0e42fbc2a69e0b5c514453d0a6d4954 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 24 Jul 2023 18:30:58 +0800 Subject: [PATCH 027/291] make sure the renderpass subset name is correct when saving scenes for camera in subprocess --- .../max/plugins/publish/save_scenes_for_cameras.py | 2 +- .../deadline/plugins/publish/submit_max_deadline.py | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index 9561f53996..9382a8f4b3 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -64,7 +64,7 @@ if render_elem_num > 0: aov_name = directory + "_" + camera + "_" + renderpass + "." + "." + ext # noqa render_elem.SetRenderElementFileName(i, aov_name) rt.saveMaxFile(new_filepath) - """).format(filename=filename, + """).format(filename=instance.name, new_filepath=new_filepath, new_output=new_output, camera=camera, diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 904732c4b4..822962b23e 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -303,10 +303,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance.name, old_output_dir, camera ) 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_info["RenderElementOutputFilename%d" % i] = new_elem # noqa + if camera in element: + elem_bname = os.path.basename(element) + new_elem = f"{dir}/{elem_bname}" + new_elem = new_elem.replace("/", "\\") + plugin_info["RenderElementOutputFilename%d" % i] = new_elem # noqa if camera: # set the default camera and target camera @@ -349,10 +350,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, if instance.data["renderer"] == "Redshift_Renderer": self.log.debug("Using Redshift...published scene wont be used..") replace_in_path = False - - if instance.data.get("multiCamera"): - self.log.debug("Using Redshift...published scene wont be used..") - replace_in_path = False return replace_in_path @staticmethod From ed55100dd19d2f99d572a78999278b73a78b1c76 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 31 Jul 2023 17:37:21 +0800 Subject: [PATCH 028/291] ondrej's comment --- .../publish/save_scenes_for_cameras.py | 3 --- .../deadline/abstract_submit_deadline.py | 25 ------------------ .../plugins/publish/submit_max_deadline.py | 26 ++++++++++++++++--- 3 files changed, 22 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index 9382a8f4b3..79e9088ac7 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -99,6 +99,3 @@ rt.saveMaxFile(new_filepath) self.log.error("Camera scene files not existed yet!") raise RuntimeError("MaxBatch.exe doesn't run as expected") self.log.debug(f"Found Camera scene:{camera_scene}") - - if "sceneFiles" not in instance.data: - instance.data["sceneFiles"] = camera_scene_files diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index ae568c43e3..3fa427204b 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -531,31 +531,6 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, return replace_with_published_scene_path( self._instance, replace_in_path=replace_in_path) - def get_job_info_through_camera(self, camera=None): - """Get the job parameters for deadline submission when - multi-camera is enabled. - Args: - infos(dict): a dictionary with job info. - """ - pass - - def get_plugin_info_through_camera(self, camera=None): - """Get the plugin parameters for deadline submission when - multi-camera is enabled. - Args: - infos(dict): a dictionary with plugin info. - """ - pass - - def _use_published_name_for_multiples(self, data): - """Process the parameters submission for deadline when - user enables multi-cameras option. - Args: - job_info_list (list): A list of multiple job infos - plugin_info_list (list): A list of multiple plugin infos - """ - pass - def assemble_payload( self, job_info=None, plugin_info=None, aux_files=None): """Assemble payload data from its various parts. diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 822962b23e..78e570a780 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -164,7 +164,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, def process_submission(self): instance = self._instance - filepath = self.scene_path + filepath = instance.context.data["currentFile"] files = instance.data["expectedFiles"] if not files: @@ -184,6 +184,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, self.log.debug("Submitting 3dsMax render..") project_settings = instance.context.data["project_settings"] if instance.data.get("multiCamera"): + self.log.debug("Submitting jobs for multiple cameras..") payload = self._use_published_name_for_multiples( payload_data, project_settings) job_infos, plugin_infos = payload @@ -249,6 +250,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, return job_info, plugin_info def get_job_info_through_camera(self, camera): + """Get the job parameters for deadline submission when + multi-camera is enabled. + Args: + infos(dict): a dictionary with job info. + """ instance = self._instance context = instance.context job_info = copy.deepcopy(self.job_info) @@ -268,15 +274,27 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # set the output filepath with the relative camera def get_plugin_info_through_camera(self, camera): + """Get the plugin parameters for deadline submission when + multi-camera is enabled. + Args: + infos(dict): a dictionary with plugin info. + """ instance = self._instance # set the target camera plugin_info = copy.deepcopy(self.plugin_info) plugin_data = {} # set the output filepath with the relative camera - for camera_scene_path in instance.data.get("sceneFiles"): - if camera in camera_scene_path: - plugin_data["SceneFile"] = camera_scene_path + if instance.data.get("multiCamera"): + scene_filepath = instance.context.data["currentFile"] + scene_filename = os.path.basename(scene_filepath) + scene_directory = os.path.dirname(scene_filepath) + current_filename, ext = os.path.splitext(scene_filename) + camera_scene_name = f"{current_filename}_{camera}{ext}" + camera_scene_filepath = os.path.join( + scene_directory, f"_{current_filename}", camera_scene_name) + plugin_data["SceneFile"] = camera_scene_filepath + files = instance.data.get("expectedFiles") if not files: raise RuntimeError("No render elements found") From 0d9873dd41dc71d603b100469a1faa419688a85c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 31 Jul 2023 22:32:19 +0800 Subject: [PATCH 029/291] ondrej's comment --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 3 ++- 1 file changed, 2 insertions(+), 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 78e570a780..15019c2647 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -12,6 +12,7 @@ from openpype.pipeline import ( legacy_io, OpenPypePyblishPluginMixin ) +from openpype.pipeline.publish import KnownPublishError from openpype.hosts.max.api.lib import ( get_current_renderer, get_multipass_setting @@ -168,7 +169,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, files = instance.data["expectedFiles"] if not files: - raise RuntimeError("No Render Elements found!") + raise KnownPublishError("No Render Elements found!") first_file = next(self._iter_expected_files(files)) output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir From 3e49e6c642b1813d90111bd4007b039f46fded27 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 31 Jul 2023 23:19:31 +0800 Subject: [PATCH 030/291] use KnownPublishError instead of RuntimeError --- .../modules/deadline/plugins/publish/submit_max_deadline.py | 4 ++-- 1 file changed, 2 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 15019c2647..29ce315bdc 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -210,7 +210,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, files = instance.data.get("expectedFiles") if not files: - raise RuntimeError("No render elements found") + raise KnownPublishError("No render elements found") first_file = next(self._iter_expected_files(files)) old_output_dir = os.path.dirname(first_file) output_beauty = RenderSettings().get_render_output(instance.name, @@ -298,7 +298,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, files = instance.data.get("expectedFiles") if not files: - raise RuntimeError("No render elements found") + raise KnownPublishError("No render elements found") first_file = next(self._iter_expected_files(files)) old_output_dir = os.path.dirname(first_file) rgb_output = RenderSettings().get_batch_render_output(camera) # noqa From 1e19bfa6949c7340a7ea146d87d66d1f2bb80aa4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 30 Aug 2023 16:42:58 +0800 Subject: [PATCH 031/291] fix the weird naming of publish render folder --- openpype/pipeline/farm/pyblish_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index fe3ab97de8..61cceb80ad 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -583,7 +583,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, else: # in case of single frame cam = [c for c in cameras if c in col] - if cam: + if cam or subset != "maxrenderMain": if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) else: From 960e7a24bc68cb913ef4a6b6857d01b0006f04cd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 30 Aug 2023 17:32:31 +0800 Subject: [PATCH 032/291] fixing bigroy's comment on camera subset interruption --- openpype/pipeline/farm/pyblish_functions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 61cceb80ad..fb73e46508 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -583,11 +583,15 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, else: # in case of single frame cam = [c for c in cameras if c in col] - if cam or subset != "maxrenderMain": + if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) + if subset == "maxrenderMain": + subset_name = '{}_{}'.format(group_name, aov) else: subset_name = '{}_{}'.format(group_name, cam) + if subset == "maxrenderMain": + subset_name = '{}'.format(group_name) else: if aov: subset_name = '{}_{}'.format(group_name, aov) From 749ca64e58caa9b95df2edc0347c72d44e15c151 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 30 Aug 2023 18:54:14 +0800 Subject: [PATCH 033/291] roy's comment on the subset naming based on cameras in multiple camera rendering --- openpype/pipeline/farm/pyblish_functions.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index fb73e46508..0c5d6a2712 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -579,18 +579,24 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, # if there are multiple cameras, we need to add camera name if isinstance(col, (list, tuple)): - cam = [c for c in cameras if c in col[0]] + cam = next((c for c in cameras if c in col[0]), None) else: # in case of single frame - cam = [c for c in cameras if c in col] + cam = next((cam for cam in cameras if cam in col), None) if cam: if aov: subset_name = '{}_{}_{}'.format(group_name, cam, aov) if subset == "maxrenderMain": + # Max submit scenes by cameras for multiple camera + # submission, it results to include the camera name inside + # the original subset and i.e group_name subset_name = '{}_{}'.format(group_name, aov) else: subset_name = '{}_{}'.format(group_name, cam) if subset == "maxrenderMain": + # Max submit scenes by cameras for multiple camera + # submission, it results to include the camera name inside + # the original subset and i.e group_name subset_name = '{}'.format(group_name) else: if aov: From 1f2e32311f71448d2cc16348871d36ebf0093f04 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 30 Aug 2023 20:34:44 +0800 Subject: [PATCH 034/291] remove camera_name in aov if there is one --- openpype/pipeline/farm/pyblish_functions.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 0c5d6a2712..58ffeb937f 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -585,19 +585,13 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, cam = next((cam for cam in cameras if cam in col), None) if cam: if aov: + # if there is duplicatd camera name found in aov, + # it would be removed + if aov.startswith(cam): + aov = aov.replace(f"{cam}_", "") subset_name = '{}_{}_{}'.format(group_name, cam, aov) - if subset == "maxrenderMain": - # Max submit scenes by cameras for multiple camera - # submission, it results to include the camera name inside - # the original subset and i.e group_name - subset_name = '{}_{}'.format(group_name, aov) else: subset_name = '{}_{}'.format(group_name, cam) - if subset == "maxrenderMain": - # Max submit scenes by cameras for multiple camera - # submission, it results to include the camera name inside - # the original subset and i.e group_name - subset_name = '{}'.format(group_name) else: if aov: subset_name = '{}_{}'.format(group_name, aov) From 30ea0213b72237cdc9bd2920c691fb22cf184338 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 5 Sep 2023 18:40:07 +0800 Subject: [PATCH 035/291] add pattern for clique.assemble function to just iterate through frame ranges --- openpype/pipeline/farm/pyblish_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 58ffeb937f..3f19c3cc95 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -548,7 +548,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, instances = [] # go through AOVs in expected files for aov, files in exp_files[0].items(): - cols, rem = clique.assemble(files) + cols, rem = clique.assemble(files, patterns=[clique.PATTERNS['frames']]) # we shouldn't have any reminders. And if we do, it should # be just one item for single frame renders. if not cols and rem: From 44391391df926316b8c7585e526ebbaffc5ab3d6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 5 Sep 2023 18:41:00 +0800 Subject: [PATCH 036/291] hound --- openpype/pipeline/farm/pyblish_functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 3f19c3cc95..5713c13c4e 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -548,7 +548,8 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, instances = [] # go through AOVs in expected files for aov, files in exp_files[0].items(): - cols, rem = clique.assemble(files, patterns=[clique.PATTERNS['frames']]) + cols, rem = clique.assemble( + files, patterns=[clique.PATTERNS['frames']]) # we shouldn't have any reminders. And if we do, it should # be just one item for single frame renders. if not cols and rem: From 60e5eb74aeee01ad6a621554eac950a5852cdb90 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 5 Sep 2023 18:43:30 +0800 Subject: [PATCH 037/291] big roy's comments on the line 582-583 --- openpype/pipeline/farm/pyblish_functions.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 5713c13c4e..0bdc2b1339 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -579,11 +579,8 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, group_name = subset # if there are multiple cameras, we need to add camera name - if isinstance(col, (list, tuple)): - cam = next((c for c in cameras if c in col[0]), None) - else: - # in case of single frame - cam = next((cam for cam in cameras if cam in col), None) + expected_filepath = col[0] if isinstance(col, (list, tuple)) else col + cam = next((cam for cam in cameras if cam in expected_filepath), None) if cam: if aov: # if there is duplicatd camera name found in aov, From 75d17fcdffb6e10b049110be6defa023b6a5d68e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 5 Sep 2023 18:56:45 +0800 Subject: [PATCH 038/291] restore the pattern for not breaking the submit publish job --- openpype/pipeline/farm/pyblish_functions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 0bdc2b1339..181aad6cb5 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -548,8 +548,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, instances = [] # go through AOVs in expected files for aov, files in exp_files[0].items(): - cols, rem = clique.assemble( - files, patterns=[clique.PATTERNS['frames']]) + cols, rem = clique.assemble(files) # we shouldn't have any reminders. And if we do, it should # be just one item for single frame renders. if not cols and rem: From dadc6fa85747cb4790b226e2279fdacd1744064c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Sep 2023 15:12:21 +0800 Subject: [PATCH 039/291] make sure render outputs with all cameras should be in the render publish folder --- openpype/pipeline/farm/pyblish_functions.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 181aad6cb5..5019d01be3 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -582,11 +582,13 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, cam = next((cam for cam in cameras if cam in expected_filepath), None) if cam: if aov: - # if there is duplicatd camera name found in aov, - # it would be removed - if aov.startswith(cam): - aov = aov.replace(f"{cam}_", "") - subset_name = '{}_{}_{}'.format(group_name, cam, aov) + # Multiple cameras publishing in some hosts such as 3dsMax + # have aov data set to "Camera001_beauty" to differentiate + # the render output files + if not aov.startswith(cam): + subset_name = '{}_{}_{}'.format(group_name, cam, aov) + else: + subset_name = '{}_{}'.format(group_name, aov) else: subset_name = '{}_{}'.format(group_name, cam) else: @@ -594,7 +596,6 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, subset_name = '{}_{}'.format(group_name, aov) else: subset_name = '{}'.format(group_name) - if isinstance(col, (list, tuple)): staging = os.path.dirname(col[0]) else: From 55555e3078e3289e663f13709334e0ed4e038e72 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 22:00:38 +0800 Subject: [PATCH 040/291] iterate the camera list --- openpype/pipeline/farm/pyblish_functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 5019d01be3..1a5cbbf3e2 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -579,7 +579,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, # if there are multiple cameras, we need to add camera name expected_filepath = col[0] if isinstance(col, (list, tuple)) else col - cam = next((cam for cam in cameras if cam in expected_filepath), None) + cam = next(iter(cam for cam in cameras if cam in expected_filepath), None) if cam: if aov: # Multiple cameras publishing in some hosts such as 3dsMax From 810b3259d1e7441bda9a147bdf4942a6a9fd101c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 22:29:41 +0800 Subject: [PATCH 041/291] hound --- openpype/pipeline/farm/pyblish_functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 1a5cbbf3e2..e5530481d2 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -579,7 +579,8 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, # if there are multiple cameras, we need to add camera name expected_filepath = col[0] if isinstance(col, (list, tuple)) else col - cam = next(iter(cam for cam in cameras if cam in expected_filepath), None) + cam = next( + iter(cam for cam in cameras if cam in expected_filepath), None) if cam: if aov: # Multiple cameras publishing in some hosts such as 3dsMax From 824912dba1e1d6d793d872d93188312bb6e43309 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 28 Sep 2023 19:08:09 +0800 Subject: [PATCH 042/291] update camera subset name condition --- openpype/pipeline/farm/pyblish_functions.py | 22 ++++++++++----------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index e5530481d2..f0d330da71 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -579,24 +579,22 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, # if there are multiple cameras, we need to add camera name expected_filepath = col[0] if isinstance(col, (list, tuple)) else col - cam = next( - iter(cam for cam in cameras if cam in expected_filepath), None) - if cam: - if aov: - # Multiple cameras publishing in some hosts such as 3dsMax - # have aov data set to "Camera001_beauty" to differentiate - # the render output files - if not aov.startswith(cam): - subset_name = '{}_{}_{}'.format(group_name, cam, aov) + cams = [cam for cam in cameras if cam in expected_filepath] + if cams: + for cam in cams: + if aov: + if not aov.startswith(cam): + subset_name = '{}_{}_{}'.format(group_name, cam, aov) + else: + subset_name = "{}_{}".format(group_name, aov) else: - subset_name = '{}_{}'.format(group_name, aov) - else: - subset_name = '{}_{}'.format(group_name, cam) + subset_name = '{}_{}'.format(group_name, cam) else: if aov: subset_name = '{}_{}'.format(group_name, aov) else: subset_name = '{}'.format(group_name) + if isinstance(col, (list, tuple)): staging = os.path.dirname(col[0]) else: From 978816fda5f167fbf46f7b68fb19dbff4ea17be9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:09:40 +0200 Subject: [PATCH 043/291] Update old Pype icon to AYON icon --- openpype/resources/icons/folder-favorite3.png | Bin 7957 -> 10232 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openpype/resources/icons/folder-favorite3.png b/openpype/resources/icons/folder-favorite3.png index ce1e6d717181f164f2f459cc1d151c57b282990f..ab0656ad14a5c1351c0090ba280584a786357872 100644 GIT binary patch delta 8662 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 P@MmIZ1}oEddiZ|;-~&O{ delta 6434 zcmbVwc{r5q`?r}fGug?KJy~MLkg=1HLB^KGkSz(LP-bkUl9@@?$}*OeEF)v7lvNsgd7tO!{9McG{v7iRV<_Te zrs*Yh18qaC0Ia^Tl97ov)=*zZ&k&=etAjP!rDMDctEZ)dHPtaRH88sOsSlF%0xm5Y zbAn>(M2b2RcqHnG^^qu2RySNBix;M?K{-Z_B1ax0Q%vzVoJ#~bDEK&OkMcgelZ%pn zbnx*gt>7@FNWwm=EFGA9Rz7@ZZ3GN}X8l5VQg{FQ;l#ruZG*SoON@E>@zv>U1=7{d z#m`RUG4aP}Z>0+v@BkZi9Wp~z59U7yj=O-nly6(lbc?J;31tnsPj zyl}q8mZw_{#m-yWSwito*-@wZU(K?YI+srNBdi|WdhogG!Ox%9UCV1Jevta~Q*!_M4CGiZVGG3Rc`uj;tRek1?`TP0r!2f{% zZ{UBd{$ueEAQk@)`X7^jBLA2l1pNtw{8j&>^PL3+UwFj`5vi&`vBEeQ|Y3qFhI&sns*RaNDo@enS;hcNG!<6-PD*c?=O5JX5G$+e? zDn~U3O?(0?f@?#|PE$SjXT*UPn7CF#X{&}+k-Z$Z$of}G>ihWinJ3#jJJZx8n_2z5 zv6GglW-70_zl^Hn9^wXMRb2!(Z6Mb8crp;BDrrD;Ry95ECOng({w1wef{`kPUgvoa z?OhVh?qUP)`xmoHtuENZjTMZaeYw()x8Cki%gP{LaJeefOjSv^*fo0t(A0`ARTx?u ze<#f`04A<~u0?q4H+$`6P@EX3iT(7ZSpIV#G*+x>Tg*#_{mp|F3w+HxI_I}R5Aq1m zIxLacnLA9~81H0u=KeQkQcKAukIj$c)rK^Q@1UK5u^(63V^R_n8z$UE8eU9_CH>A*;D#cR zfu5|r30*;cFlYt`3Lr|qw!D?ag1;G^+#}!pf`OXb`pmU$K;|w~bsc3XQ ztNn-XR7d&aQst|V*wnnf$1Vpd%?i%HmG{?@!gp}d96@mQ6_Wpj&v0NS7kBgFPR=&~GWM4v;zE5H+|5xh%wsw%jKC8}1gN--XhOUnY>L6t(Y(Ogac%Qu+ULP6;~S;z1o z#8iAW0(EouOUH)trjQ3#rJ#_$eX*}${IJ2ft`V2a(k~T(F-}fMs)F4A65UO~`$zO% zw%Tthu>*_de!Ajc&HeAX`>#NEJtvwug3>K`#aSdv6;eIs@{COGRl%b>SE>b>x}8gQ z44ym^=%TIAKQb>!{afSzP~`t;yrg}t+A)tMnmFA^bO{~}JC{qcUV(VKRz2(=&Wa1e zdvC+y=R!Eaep=NK50TprEuSD~=r`4Jm?b57PVu~`3)PzA4CdH7Q;q) zZ}a9tG->LD(c_Ne50wKr)gH~h9tuFsl|c1bLVgq4&|{G=O$UZEt%Ah3O4X`A z<4FAZ8`U){D4dN!-_eR}H0??GiEj!UcXq>j5F9K=kt613!d0!#Zi)`lv`tL4y}s@0 zmr$qiFkmmRhzcAo&$utpq!2^l=?4Q>_;G4T3(@{N-la}J zpr3z8YY);_WMYdC{0IF5=ivQ%*nnV8Y_kzYXfki`jEuknqzB==bM4R_m9X@xI!Bh< z>OAutnlKZhyhim+gTCV_=Q#znaMH@F&$UZTEyYw_;R7%nOO7#TPD1@iS>Ro^~{yKd`vqowg}2I%iyL(U1Ha5a!cmm0cdgnBjXv0MsOi9!%GNk$ z$c{DMYbirQoux$_8k>1cd5!pvC*4aJXM=u70z~K!2jF27+Fr^-60!kwK3K!6gCX}NRs|^+)lMe{SF&`62&05dedCXS z+w78o(9sVH$1sb7*W;Dsvb7|G1EU}#JXaucm`+0{ASPB~+$B>#FrTJL&)4cGO6ZdC zZ%zj(f~(7mLH9V%zHnSFUkF2iEmT}%f^-M9h&4J)xN3jtb0)~Rcy$_5tF0mQx%F$Q zJP;!j#S-m35v7kn@J;tdq{A zvv?Zr9#8>a5l+_c>ux-49BCUr3>NpLYbmX9S(uK7-2C}UX*87)5vy0^=RnF2^Q$H4 zfJLn*GuqobZkQjQ2mwb2>Csd$FwuZ$)0ESFU_Hy0@S$AZdGPzKXHh0MHbsD9^s3!Nzn94bmc8$_O(4(S_&B8kod*-e( z`{S32Wzd9YD`KuMM7~|ysny=!v_x-&s86rW_p*?qc4qDvkdi+;jvHIC5FeJS^UQfQwS1IKE$obhJ0>)p8?s)$=(wZs_1u&&Gzc~@@VQ_l zbmUA*G)WQR?!8Iz+F9Ts{h-JW;x*Tbx>A>yLV5O4Pxb@OtIRn$QM4Q;3G{W0$g%Er z`R3wT;C0dY+*Gmr4~6nhEZNoJefW3%f7Rg1`JeO$xs~m?jUpF5@35%~U$#(S&XJprJMRIiDp~%> zN1*iF{~^}I@9~?vT>&awSB|ZG)A3DABh;d^HEnZX9ds4TEoOmaLZ$n1jso@8`yOrT z<3bQ;cRQmMoE=rWPQOSwa~9V@-D=krY(g<#+!EgSc5}onVUB;vE>>Ga#TeLCp|EOB z;dAa#_69ZjgCqTm&Glj522i43KzxL+?{ky|cfJhWF>ArFA7svvZZNY1ce!M5-}lp6 znV>!gT_+3xtH7RzRzP)Ko31(r09^V zv6nq44xp1lp4+*i;9(b4-ShWgUCys$;qMP=E4kKSAu(U}d<~wb3f+b_cu9eCC68D^9vXU)l*LrOeEn0@^eY}BE!T7bm++zU z1AcwrS7hi~^snKZxUbZ*meBp{Y%}++5~{ zw+q9z*hKp>LJMeuRKdaXb9O#c8LZqew}rM0@PjrteN-^ef1d7VJ&j9z`)g;5qH0q< zMrPc%Dg}Y@y27L4qavfQJi&#+jaY$uLEX0giEqkN#u!FkhG6Uwh-VwXG!W}|l0Q4V z9rJv~)q$ireiZ*nRchl5wCQ5r3b?-iWP=&Fu^dU2=UxH&06KVR=PjWY)$97rA5^Gb z5fhJK%X;un2>8G;c63lV`!%p~F#;5{jwbw>-yIkXSC;aDY@z}0&jd8--n8vdqZ!N# zkO+yxh-O60Zi<=CiM+{Ef;wV(`Bv|aalCo#J&QM3U#q6S-3BeCO-V2;fcK)?QPf$$kYims>%)AtevEV+JMcB6lqn?^u zEzf|dA^K<0PC-8Ne61WXcjTQEjLrM-;tI%s3)g4^G!fBs{*eLR*TJ0zDU=)Yc^R1Sta^Exl; zfHCblqD!#~k(B6*;sn0laXvMv8??93`z4B#(Ej1OatI_%Y0Z7-*{5KBItN z8==Cyz7Y9^9ePAn$q{S+jOjS|R4=}rh^;jX1S9qOnq|-85P|#i_)aOxRTw1wK?7&$1GTZsdA{rcKMNL!B`7qw11uiCp))lePXIJ); zQ@*+-rs~fvL*+Dj*SK=@dPPI97wbkp>F0{~A}XLfz-*(BRYa8y+k1YEzMS+I=)O-r z%>t4yz%JSoY_aXP>(q`R;fcfNFRsXPoH=45SHVy=Js?#4pf`08t=+)gJ%WS(yY(gz z!TBc9C?2av5ut4FXmL`<*~qS#((_$IS;>m(3#ip7nMHKlD%&pK*GVg8P0=8X0o% zj2&q1?N>dH6^Chx=rW>_32A*BvWUNOEkiuKQ{paR0nhfemHM@wT?lRGs_Y>}aC=1v z80GbWVqnh^zV^2t=@K5KglimzX*VYLS5(|1qCR~H2|choKSlkqB65@~zGv!*_iM-QcANvs`xAa$2JW@juuoK)=j>ol4G-#i z=1;Dh`cr0Z6ow_H%IXmjzIOZOjaKYmUeoh6mir6rdUK+^syZN~+!3*p$916h&fw#*>;|{l^7)=_7a0<@(Pa3*uU0ZT zi4D;bLya^V|9b@fKa>e9iU02lN?XqgBIdp-$z5wLx^bATm;N9`jr_35mTc|MN>vAkZzw0AkPeosJqdxOXr{LXSxbM5~ zs=?X2Q8NbXt+v8BXLZ@1&dG>-F=SgEIeTOdpCzj#b)B$0l{lgRjfELMj&$hPSb}o$ zfn~wFub>mY-32G^gojH(Pr;U`dWJvaczpOUJc@ABWc99CQ%8bIMcJ-(aT<;uKp%l5 zkz!kR4@5T}!I9LsS(lhl9rY8T^cBnfrdvS9+VbV_zkn{}HkfH6hxH>WmLUgWpY5kJ zhU)6@jKWtxXU~iu?55T!4%C_P`ZzXZG8v_<^)cEAsiC#)imv6q#;Bj6EsISK{mHA6 z+(&0(Pv0H6!04v;vWgg`(@PTE(tyKR zFJ>8~N5U1kRcAg~KEkoK8EBKI;PzHjC38vMSaLK)Lv4_}q$+Vt|_yWUt3ilau( z?n|xt5}?lI^AF3fFQjf~=xf}f)zbDu@-J57Y`mPC1XfKcx)gB(#cCD2T0e4P%XR1v zvY3~Kji6-mS^77ga_Dv?g#Fi;4ZE$S+!)aq@&o6M!t+9^WbI?qhLCMyOBV|Gnx*n= zZl4)`eOfJ8Vs^(n(^%54l zx02n(S<*^za@cXMLVM3b!mtlR`IP845i;_Q9kb?C$o)&Lr`uHx68ZZM*C={qHmDPx zF6X|c7JIFn!Qf>T12FV|?`pskJ% From b0af8b1f3d04354eee9f24c6a97adb24c2899aa9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:10:11 +0200 Subject: [PATCH 044/291] Remove unused folder-favorite icons --- openpype/resources/icons/folder-favorite.png | Bin 7008 -> 0 bytes openpype/resources/icons/folder-favorite2.png | Bin 22430 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 openpype/resources/icons/folder-favorite.png delete mode 100644 openpype/resources/icons/folder-favorite2.png diff --git a/openpype/resources/icons/folder-favorite.png b/openpype/resources/icons/folder-favorite.png deleted file mode 100644 index 198b289e9ed39e6f8e13b6debf5aee857039a032..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7008 zcmaJ_XH-*Lus(p1jw1C+vw}!fQKYLhk*Mrn zP>c@d{}ymU+okrNVpIC##aHeHMl#uRDV)Yn5ATQkFKF~hjia{WF1|Zo9P-reO2-FW zs+i=FrlgQeB?)J(<&BF~8m|YHI{d@>K5)3yG}ey&3iv!Ok=n^)xLkiIjC4VA=w(RNOI3UmoSJ-sf+#ZgA!>3L@%^r?No`;GK z2MSNK?RlTX{`i)VEJonR(e{pmg`d-sm9!pmZDt7i6N^7V#_$K7(r8H!=>iw)UWF1n zWBmkSx!1wNuBA2AwITi+KdB1GROGJ&>wR%c#echOrvQ6?u|w9}>CpgQsztBHH

XIfm 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 Date: Sat, 7 Oct 2023 20:11:12 +0200 Subject: [PATCH 045/291] Rename remaining folder-favorite icon to avoid number 3 behind it --- openpype/hosts/nuke/api/utils.py | 2 +- .../{folder-favorite3.png => folder-favorite.png} | Bin 2 files changed, 1 insertion(+), 1 deletion(-) rename openpype/resources/icons/{folder-favorite3.png => folder-favorite.png} (100%) diff --git a/openpype/hosts/nuke/api/utils.py b/openpype/hosts/nuke/api/utils.py index 7b02585892..a7df1dee71 100644 --- a/openpype/hosts/nuke/api/utils.py +++ b/openpype/hosts/nuke/api/utils.py @@ -12,7 +12,7 @@ def set_context_favorites(favorites=None): favorites (dict): couples of {name:path} """ favorites = favorites or {} - icon_path = resources.get_resource("icons", "folder-favorite3.png") + icon_path = resources.get_resource("icons", "folder-favorite.png") for name, path in favorites.items(): nuke.addFavoriteDir( name, diff --git a/openpype/resources/icons/folder-favorite3.png b/openpype/resources/icons/folder-favorite.png similarity index 100% rename from openpype/resources/icons/folder-favorite3.png rename to openpype/resources/icons/folder-favorite.png From c2ef4bd97d6ec554f83c65c95e90d715e3be5ece Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 7 Oct 2023 20:16:41 +0200 Subject: [PATCH 046/291] Match border color to exact icon color --- openpype/resources/icons/folder-favorite.png | Bin 10232 -> 10072 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/openpype/resources/icons/folder-favorite.png b/openpype/resources/icons/folder-favorite.png index ab0656ad14a5c1351c0090ba280584a786357872..65f04d8c8695b6fdf309758cff3d0320a1fae1bf 100644 GIT binary patch delta 8037 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

~J&ak`(to2{TXABf zVr+h`&cS+WM9f98vYL&5+qJzCq@X;mvDkcbWS}U!^5lrNrt(>oa1@~BB4>AxUuAW~ z<3xKkNc0Z9&i~060a>|r*nRl8rn*=ZV&{0Hy%wBR2MgUtV0Sv;u)7em&i&~cg3w`w z1j|HJ_c7N*k2kvD{7>!n29SIK9q>{F?0U$xgY_}&kM8aM-yA4eWi1<`zbvRJF9aZ~ z7y-p53$Cg0fC9@(3M&UevCJm}o4~S^ixZW|gv4z|=LxKVHXyMRRoVFAOCp5<|H(;bf5IW{4luuo&y4@>H;no{JBH#Mw-X*$)fu(eZR)O9$smAVged|T!3h; z96!t$?9AhM0nn9Z?U7j^JqP!Y1vJ)5-I`t>J!dCg=Xapl<5Xy61Z2C080&*x3Qt4v*F2tGbQ6TtJb!5Pl8vt_Z}du6s^ICZa&RM&(l$Z z@pD%h4H04AyOH3{Es7jFr=kybJ1EF-de_huRfj8Zx(LylKJZAJwFe!2--%N6=ptmI zh0ni^Flz^nysh%${H6d}omGpI4G4Kbx3q9p3g$JN2z=?nwxOqH3m8!vdyYTrZZ)NoXz$Y4Y%aLyGqH_Ni|LbfrjCzn?zG|sm;0iWwZfVjTCvQ*sW;?? zlT?Btz&#OakggD&P0H9Rh&31jmmh!A#I7a_f;`CuuVx>f2Zs}qj@&BPfBy&cn!7m( zNs%8Ku7YaSMVut8uW%yuL8CB25i2|mV6?KJ?=C)wqE*?^8L+{t3x^B-G8m@pe+~xk z8IFMV;oxS-fQt$M)%r)k$-nIuy--X&2z$~z?z*RA%s79!Yy8o$aZSt~e@-!Qvr!$gtyHD!X2?HM94&-5 zj+r7q1xfFGpMHR1Trt0zs`>T<<)OW*(OeaiDv`0*T$B4dCe9#(??qvJP#~hABr<=X zBkr9npU=Y4ur6jaKmUuoN~rMiJg$syVsQG4u{cyGQQMZas}b|yzPmhwCEnD#0!wKK z-0UCy1_M&MyYl)k@=ge#3^0s&!&d{>+!BP!yjQ~Sw=yE^A`7q-vKOn{9KLTH1SPsW z(5-fq!CO&0%=dZYgb>OAP4n{WJj=lJn=7&o@SY5gM5GVg=b4P$VJHvTYLb4osfS1} z$4F^ML}!Iv8ofXHa7-F*Os->OsN3|c^lf?w43aGnXOodN7a8i>dVUST!;~SN(;hGL zj}%EQF>i`3sJe&CKswWGoJi*wSl8Nn$IdD_gE#4&MWf1DCBdhok358oM(VmtHVdJ4 z6))Y(-OJnMvz_5B;(1BQ8D!j>rdAXu5N%~RNp`k1j#+}o9as0TUyQ<`CTu~kl8F4i zQ=L@OV%F%rg7*x~ZdTq0`yv{WHvUfTCAcO_NPvwBEp2gTk?i3W7BFE)3#>CBd(x}3D8D&=X@0~gsz@M>!DYQb*@$vrx4s`X< From 5b2e5faccdbd0e425e0d9f438f0e82f96fe15c24 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 18:30:03 +0800 Subject: [PATCH 047/291] remove if condition of cameras --- openpype/pipeline/farm/pyblish_functions.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index f0d330da71..8ae46ae1a1 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -580,20 +580,17 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, # if there are multiple cameras, we need to add camera name expected_filepath = col[0] if isinstance(col, (list, tuple)) else col cams = [cam for cam in cameras if cam in expected_filepath] - if cams: - for cam in cams: - if aov: - if not aov.startswith(cam): - subset_name = '{}_{}_{}'.format(group_name, cam, aov) - else: - subset_name = "{}_{}".format(group_name, aov) - else: - subset_name = '{}_{}'.format(group_name, cam) - else: + for cam in cams: if aov: - subset_name = '{}_{}'.format(group_name, aov) + if aov.startswith(cam): + subset_name = '{}_{}_{}'.format(group_name, cam, aov) + else: + subset_name = "{}_{}".format(group_name, aov) else: - subset_name = '{}'.format(group_name) + if aov.startswith(cam): + subset_name = '{}_{}'.format(group_name, cam) + else: + subset_name = '{}'.format(group_name) if isinstance(col, (list, tuple)): staging = os.path.dirname(col[0]) From 5bb6f304d381938d1effa7cc089971ab85f46352 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 19:26:42 +0800 Subject: [PATCH 048/291] use known publish error instead of runtime error --- openpype/hosts/max/plugins/publish/collect_render.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 78ab02ef5e..01d7cff32d 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -5,6 +5,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name +from openpype.pipeline.publish import KnownPublishError 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_rendersettings import RenderSettings @@ -45,8 +46,8 @@ class CollectRender(pyblish.api.InstancePlugin): if instance.data.get("multiCamera"): cameras = instance.data.get("members") if not cameras: - raise RuntimeError("There should be at least" - " one renderable camera in container") + raise KnownPublishError("There should be at least" + " one renderable camera in container") sel_cam = [ c.name for c in cameras if rt.classOf(c) in rt.Camera.classes] From e3e8770ffefb25a2b89388f70f51d385a10b0cb5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 19 Oct 2023 20:31:06 +0800 Subject: [PATCH 049/291] restore the function code for test --- openpype/pipeline/farm/pyblish_functions.py | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index 3254fbdfda..c9e013a09a 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -578,20 +578,21 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, else: group_name = subset - # if there are multiple cameras, we need to add camera name - expected_filepath = col[0] if isinstance(col, (list, tuple)) else col - cams = [cam for cam in cameras if cam in expected_filepath] - for cam in cams: + if isinstance(col, (list, tuple)): + cam = [c for c in cameras if c in col[0]] + else: + # in case of single frame + cam = [c for c in cameras if c in col] + if cam: if aov: - if aov.startswith(cam): - subset_name = '{}_{}_{}'.format(group_name, cam, aov) - else: - subset_name = "{}_{}".format(group_name, aov) + subset_name = '{}_{}_{}'.format(group_name, cam, aov) else: - if aov.startswith(cam): - subset_name = '{}_{}'.format(group_name, cam) - else: - subset_name = '{}'.format(group_name) + subset_name = '{}_{}'.format(group_name, cam) + else: + if aov: + subset_name = '{}_{}'.format(group_name, aov) + else: + subset_name = '{}'.format(group_name) if isinstance(col, (list, tuple)): staging = os.path.dirname(col[0]) From e9005c66184c5b6145a342e5258256c1929b111a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 31 Oct 2023 12:59:24 +0800 Subject: [PATCH 050/291] make sure subset name conditions are applicable for other hosts such as maya --- openpype/pipeline/farm/pyblish_functions.py | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/farm/pyblish_functions.py b/openpype/pipeline/farm/pyblish_functions.py index c9e013a09a..6265449515 100644 --- a/openpype/pipeline/farm/pyblish_functions.py +++ b/openpype/pipeline/farm/pyblish_functions.py @@ -578,16 +578,18 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, else: group_name = subset - if isinstance(col, (list, tuple)): - cam = [c for c in cameras if c in col[0]] - else: - # in case of single frame - cam = [c for c in cameras if c in col] - if cam: - if aov: - subset_name = '{}_{}_{}'.format(group_name, cam, aov) - else: - subset_name = '{}_{}'.format(group_name, cam) + # if there are multiple cameras, we need to add camera name + expected_filepath = col[0] if isinstance(col, (list, tuple)) else col + cams = [cam for cam in cameras if cam in expected_filepath] + if cams: + for cam in cams: + if aov: + if not aov.startswith(cam): + subset_name = '{}_{}_{}'.format(group_name, cam, aov) + else: + subset_name = "{}_{}".format(group_name, aov) + else: + subset_name = '{}_{}'.format(group_name, cam) else: if aov: subset_name = '{}_{}'.format(group_name, aov) From 6074876adf2ff180108cc24ec94c0a59bb5ea248 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Nov 2023 17:29:12 +0800 Subject: [PATCH 051/291] regenerate UV Tile Preview and reload textures during playblasting --- openpype/hosts/maya/api/lib.py | 9 +++++++++ openpype/hosts/maya/plugins/publish/extract_playblast.py | 6 ++++-- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 ++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 2ecaf87fce..27c61d0af3 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -174,6 +174,15 @@ def maintained_selection(): cmds.select(clear=True) +def regenerate_uv_tile_preview(): + texture_files = cmds.ls(type="file") + if not texture_files: + return + for texture_file in texture_files: + cmds.ogs(regenerateUVTilePreview=texture_file) + cmds.ogs(reloadTextures=True) + + def get_namespace(node): """Return namespace of given node""" node_name = node.rsplit("|", 1)[-1] diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index cfab239da3..8835f288ea 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -43,7 +43,6 @@ class ExtractPlayblast(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) - path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) @@ -125,6 +124,7 @@ class ExtractPlayblast(publish.Extractor): preset["overwrite"] = True cmds.refresh(force=True) + lib.regenerate_uv_tile_preview() refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True)) cmds.currentTime(refreshFrameInt - 1, edit=True) @@ -164,7 +164,8 @@ class ExtractPlayblast(publish.Extractor): "wireframeOnShaded", "xray", "jointXray", - "backfaceCulling" + "backfaceCulling", + "textures" ] viewport_defaults = {} for key in keys: @@ -180,6 +181,7 @@ class ExtractPlayblast(publish.Extractor): capture_preset["Viewport Options"]["override_viewport_options"] ) + self.log.debug("{}".format(instance.data["panel"])) # Force viewer to False in call to capture because we have our own # viewer opening call to allow a signal to trigger between # playblast and viewer diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index c0be3d77db..550243f274 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -101,6 +101,8 @@ class ExtractThumbnail(publish.Extractor): preset["overwrite"] = True cmds.refresh(force=True) + lib.regenerate_uv_tile_preview() + refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True)) cmds.currentTime(refreshFrameInt - 1, edit=True) From 15923ddf9c70a296366241c9cefdae0921e59ee9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Nov 2023 19:02:38 +0800 Subject: [PATCH 052/291] move the regenerate uv_tile_preview code right before the capture --- openpype/hosts/maya/api/lib.py | 6 +++++- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 ++- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 9 ++++----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 27c61d0af3..a2a014caef 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -175,11 +175,15 @@ def maintained_selection(): def regenerate_uv_tile_preview(): + """Regenerate UV Tile Preview during playblast + """ + original_texture_loading = cmds.ogs(query=True, reloadTextures=True) texture_files = cmds.ls(type="file") if not texture_files: return for texture_file in texture_files: - cmds.ogs(regenerateUVTilePreview=texture_file) + if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0: + cmds.ogs(regenerateUVTilePreview=texture_file) cmds.ogs(reloadTextures=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 8835f288ea..5b98fb5fc9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -43,6 +43,8 @@ class ExtractPlayblast(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) + if "textures" in preset["viewport_options"]: + lib.regenerate_uv_tile_preview() path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) @@ -124,7 +126,6 @@ class ExtractPlayblast(publish.Extractor): preset["overwrite"] = True cmds.refresh(force=True) - lib.regenerate_uv_tile_preview() refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True)) cmds.currentTime(refreshFrameInt - 1, edit=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 550243f274..e2dd89836f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -101,8 +101,6 @@ class ExtractThumbnail(publish.Extractor): preset["overwrite"] = True cmds.refresh(force=True) - lib.regenerate_uv_tile_preview() - refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True)) cmds.currentTime(refreshFrameInt - 1, edit=True) @@ -154,9 +152,10 @@ class ExtractThumbnail(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) - - path = capture.capture(**preset) - playblast = self._fix_playblast_output_path(path) + if "textures" in preset["viewport_options"]: + lib.regenerate_uv_tile_preview() + path = capture.capture(**preset) + playblast = self._fix_playblast_output_path(path) _, thumbnail = os.path.split(playblast) From 2b98ac1ef2445280e8d0cc0d4131119f9afc8fb9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Nov 2023 19:04:20 +0800 Subject: [PATCH 053/291] rename regenerateUVTilePreview as reload_textures --- openpype/hosts/maya/api/lib.py | 4 ++-- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index a2a014caef..271b90d878 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -174,8 +174,8 @@ def maintained_selection(): cmds.select(clear=True) -def regenerate_uv_tile_preview(): - """Regenerate UV Tile Preview during playblast +def reload_textures(): + """Reload textures during playblast """ original_texture_loading = cmds.ogs(query=True, reloadTextures=True) texture_files = cmds.ls(type="file") diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 5b98fb5fc9..66ebe2ba0d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -44,7 +44,7 @@ class ExtractPlayblast(publish.Extractor): ) ) if "textures" in preset["viewport_options"]: - lib.regenerate_uv_tile_preview() + lib.reload_textures() path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index e2dd89836f..2b5360efe6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -153,7 +153,7 @@ class ExtractThumbnail(publish.Extractor): ) ) if "textures" in preset["viewport_options"]: - lib.regenerate_uv_tile_preview() + lib.reload_textures() path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From ba83d4cc2f828c8f6c1600f90627d5e86a9c4ec7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Nov 2023 19:05:36 +0800 Subject: [PATCH 054/291] remove unused variables --- openpype/hosts/maya/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 271b90d878..293889ddcc 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -177,7 +177,6 @@ def maintained_selection(): def reload_textures(): """Reload textures during playblast """ - original_texture_loading = cmds.ogs(query=True, reloadTextures=True) texture_files = cmds.ls(type="file") if not texture_files: return From 51f4d8f06f1ff97a86ea20bdcccad16b57210e8f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 23 Nov 2023 17:28:08 +0800 Subject: [PATCH 055/291] make the reload texture being optional and only enabled when the reloadTextures being enabled --- openpype/hosts/maya/api/lib.py | 6 +++++- .../hosts/maya/plugins/publish/extract_playblast.py | 3 +-- .../hosts/maya/plugins/publish/extract_thumbnail.py | 8 ++++---- openpype/settings/defaults/project_settings/maya.json | 1 + .../projects_schema/schemas/schema_maya_capture.json | 11 +++++++++++ .../maya/server/settings/publish_playblast.py | 2 ++ server_addon/maya/server/version.py | 2 +- 7 files changed, 25 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 293889ddcc..4066ee640b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -174,9 +174,13 @@ def maintained_selection(): cmds.select(clear=True) -def reload_textures(): +def reload_textures(preset): """Reload textures during playblast """ + if not preset["viewport_options"]["reloadTextures"]: + self.log.debug("Reload Textures during playblasting is disabled.") + return + texture_files = cmds.ls(type="file") if not texture_files: return diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 66ebe2ba0d..872702e66e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -44,7 +44,7 @@ class ExtractPlayblast(publish.Extractor): ) ) if "textures" in preset["viewport_options"]: - lib.reload_textures() + lib.reload_textures(preset) path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) @@ -182,7 +182,6 @@ class ExtractPlayblast(publish.Extractor): capture_preset["Viewport Options"]["override_viewport_options"] ) - self.log.debug("{}".format(instance.data["panel"])) # Force viewer to False in call to capture because we have our own # viewer opening call to allow a signal to trigger between # playblast and viewer diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 2b5360efe6..27f008652b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -152,10 +152,10 @@ class ExtractThumbnail(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) - if "textures" in preset["viewport_options"]: - lib.reload_textures() - path = capture.capture(**preset) - playblast = self._fix_playblast_output_path(path) + if "textures" in preset["viewport_options"]: + lib.reload_textures(preset) + path = capture.capture(**preset) + playblast = self._fix_playblast_output_path(path) _, thumbnail = os.path.split(playblast) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 7719a5e255..fa2f694747 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1289,6 +1289,7 @@ "twoSidedLighting": true, "lineAAEnable": true, "multiSample": 8, + "reloadTextures": false, "useDefaultMaterial": false, "wireframeOnShaded": false, "xray": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index d90527ac8c..1aa5b3d2e4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -236,6 +236,11 @@ { "type": "splitter" }, + { + "type": "boolean", + "key": "reloadTextures", + "label": "Reload Textures" + }, { "type": "boolean", "key": "useDefaultMaterial", @@ -908,6 +913,12 @@ { "type": "splitter" }, + { + "type": "boolean", + "key": "reloadTextures", + "label": "Reload Textures", + "default": false + }, { "type": "boolean", "key": "useDefaultMaterial", diff --git a/server_addon/maya/server/settings/publish_playblast.py b/server_addon/maya/server/settings/publish_playblast.py index acfcaf5988..205f0eb847 100644 --- a/server_addon/maya/server/settings/publish_playblast.py +++ b/server_addon/maya/server/settings/publish_playblast.py @@ -108,6 +108,7 @@ class ViewportOptionsSetting(BaseSettingsModel): True, title="Enable Anti-Aliasing", section="Anti-Aliasing" ) multiSample: int = Field(8, title="Anti Aliasing Samples") + reloadTextures: bool = Field(False, title="Reload Textures") useDefaultMaterial: bool = Field(False, title="Use Default Material") wireframeOnShaded: bool = Field(False, title="Wireframe On Shaded") xray: bool = Field(False, title="X-Ray") @@ -302,6 +303,7 @@ DEFAULT_PLAYBLAST_SETTING = { "twoSidedLighting": True, "lineAAEnable": True, "multiSample": 8, + "reloadTextures": False, "useDefaultMaterial": False, "wireframeOnShaded": False, "xray": False, diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index 805897cda3..b87834cc35 100644 --- a/server_addon/maya/server/version.py +++ b/server_addon/maya/server/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring addon version.""" -__version__ = "0.1.6" +__version__ = "0.1.7" From 55dab6dc0c01cddf7b920e363e554e0439f78875 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 23 Nov 2023 19:29:39 +0800 Subject: [PATCH 056/291] add loader for redshift proxy family(contributed by big Roy) --- .../plugins/load/load_redshift_proxy.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 openpype/hosts/houdini/plugins/load/load_redshift_proxy.py diff --git a/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py b/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py new file mode 100644 index 0000000000..914154be06 --- /dev/null +++ b/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py @@ -0,0 +1,131 @@ +import os +from openpype.pipeline import ( + load, + get_representation_path, +) +from openpype.hosts.houdini.api import pipeline + +import clique +import hou + + +class RedshiftProxyLoader(load.LoaderPlugin): + """Load Redshift Proxy""" + + families = ["redshiftproxy"] + label = "Load Redshift Proxy" + representations = ["rs"] + order = -10 + icon = "code-fork" + color = "orange" + + def load(self, context, name=None, namespace=None, data=None): + + # Get the root node + obj = hou.node("/obj") + + # Define node name + namespace = namespace if namespace else context["asset"]["name"] + node_name = "{}_{}".format(namespace, name) if namespace else name + + # Create a new geo node + container = obj.createNode("geo", node_name=node_name) + + # Check whether the Redshift parameters exist - if not, then likely + # redshift is not set up or initialized correctly + if not container.parm("RS_objprop_proxy_enable"): + container.destroy() + raise RuntimeError("Unable to initialize geo node with Redshift " + "attributes. Make sure you have the Redshift " + "plug-in set up correctly for Houdini.") + + # Enable by default + container.setParms({ + "RS_objprop_proxy_enable": True, + "RS_objprop_proxy_file": self.format_path(self.fname) + }) + + # Remove the file node, it only loads static meshes + # Houdini 17 has removed the file node from the geo node + file_node = container.node("file1") + if file_node: + file_node.destroy() + + # Add this stub node inside so it previews ok + proxy_sop = container.createNode("redshift_proxySOP", + node_name=node_name) + proxy_sop.setDisplayFlag(True) + + nodes = [container, proxy_sop] + + self[:] = nodes + + return pipeline.containerise( + node_name, + namespace, + nodes, + context, + self.__class__.__name__, + suffix="", + ) + + def update(self, container, representation): + + # Update the file path + file_path = get_representation_path(representation) + + node = container["node"] + node.setParms({ + "RS_objprop_proxy_file": self.format_path(file_path) + }) + + # Update attribute + node.setParms({"representation": str(representation["_id"])}) + + def remove(self, container): + + node = container["node"] + node.destroy() + + def format_path(self, path): + """Format using $F{padding} token if sequence, otherwise just path.""" + + # Find all frames in the folder + ext = ".rs" + folder = os.path.dirname(path) + frames = [f for f in os.listdir(folder) if f.endswith(ext)] + + # Get the collection of frames to detect frame padding + patterns = [clique.PATTERNS["frames"]] + collections, remainder = clique.assemble(frames, + minimum_items=1, + patterns=patterns) + self.log.debug("Detected collections: {}".format(collections)) + self.log.debug("Detected remainder: {}".format(remainder)) + + if not collections and remainder: + if len(remainder) != 1: + raise ValueError("Frames not correctly detected " + "in: {}".format(remainder)) + + # A single frame without frame range detected + return os.path.normpath(path).replace("\\", "/") + + # Frames detected with a valid "frame" number pattern + # Then we don't want to have any remainder files found + assert len(collections) == 1 and not remainder + collection = collections[0] + + num_frames = len(collection.indexes) + if num_frames == 1: + # Return the input path without dynamic $F variable + result = path + else: + # More than a single frame detected - use $F{padding} + fname = "{}$F{}{}".format(collection.head, + collection.padding, + collection.tail) + result = os.path.join(folder, fname) + + # Format file name, Houdini only wants forward slashes + return os.path.normpath(result).replace("\\", "/") From 2d85b5f106d04e2147e73dfb2bb8c4425ba31ba5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 24 Nov 2023 12:32:15 +0800 Subject: [PATCH 057/291] code tweaks on capturing playblast and reloadtexture function --- openpype/hosts/maya/api/lib.py | 15 +++++---------- .../maya/plugins/publish/extract_playblast.py | 7 +++++-- .../maya/plugins/publish/extract_thumbnail.py | 7 +++++-- openpype/settings/lib.py | 2 +- openpype/vendor/python/common/capture.py | 2 ++ 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 4066ee640b..078ed5192b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -174,19 +174,14 @@ def maintained_selection(): cmds.select(clear=True) -def reload_textures(preset): +def reload_textures(): """Reload textures during playblast """ - if not preset["viewport_options"]["reloadTextures"]: - self.log.debug("Reload Textures during playblasting is disabled.") - return - texture_files = cmds.ls(type="file") - if not texture_files: - return - for texture_file in texture_files: - if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0: - cmds.ogs(regenerateUVTilePreview=texture_file) + if texture_files: + for texture_file in texture_files: + if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0: + cmds.ogs(regenerateUVTilePreview=texture_file) cmds.ogs(reloadTextures=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 872702e66e..a3a2f8a5a5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -43,8 +43,11 @@ class ExtractPlayblast(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) - if "textures" in preset["viewport_options"]: - lib.reload_textures(preset) + if ( + preset["viewport_options"].get("reloadTextures") + and "textures" in preset["viewport_options"] + ): + lib.reload_textures() path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 27f008652b..ef843c9df8 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -152,8 +152,11 @@ class ExtractThumbnail(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) - if "textures" in preset["viewport_options"]: - lib.reload_textures(preset) + if ( + preset["viewport_options"].get("reloadTextures") + and "textures" in preset["viewport_options"] + ): + lib.reload_textures() path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index ce62dde43f..d62e50d3c7 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -172,7 +172,7 @@ def save_studio_settings(data): clear_metadata_from_settings(new_data) changes = calculate_changes(old_data, new_data) - modules_manager = ModulesManager(_system_settings=new_data) + modules_manager = ModulesManager(new_data) warnings = [] for module in modules_manager.get_enabled_modules(): diff --git a/openpype/vendor/python/common/capture.py b/openpype/vendor/python/common/capture.py index 224699f916..b6d15ae47a 100644 --- a/openpype/vendor/python/common/capture.py +++ b/openpype/vendor/python/common/capture.py @@ -760,6 +760,8 @@ def _applied_viewport_options(options, panel): # Try to set as much as possible of the state by setting them one by # one. This way we can also report the failing key values explicitly. for key, value in options.items(): + if key == "reloadTextures": + continue try: cmds.modelEditor(panel, edit=True, **{key: value}) except TypeError: From 323d2409d349cbb5339457661d0ba4211f256722 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 24 Nov 2023 17:14:49 +0800 Subject: [PATCH 058/291] unified the code style of the loader with other loaders such as ass and bgeo loader --- .../plugins/load/load_redshift_proxy.py | 64 +++++++------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py b/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py index 914154be06..8a3cc04eab 100644 --- a/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py @@ -5,7 +5,6 @@ from openpype.pipeline import ( ) from openpype.hosts.houdini.api import pipeline -import clique import hou @@ -42,7 +41,8 @@ class RedshiftProxyLoader(load.LoaderPlugin): # Enable by default container.setParms({ "RS_objprop_proxy_enable": True, - "RS_objprop_proxy_file": self.format_path(self.fname) + "RS_objprop_proxy_file": self.format_path( + self.fname, context["representation"]) }) # Remove the file node, it only loads static meshes @@ -76,7 +76,8 @@ class RedshiftProxyLoader(load.LoaderPlugin): node = container["node"] node.setParms({ - "RS_objprop_proxy_file": self.format_path(file_path) + "RS_objprop_proxy_file": self.format_path( + file_path, representation) }) # Update attribute @@ -87,45 +88,24 @@ class RedshiftProxyLoader(load.LoaderPlugin): node = container["node"] node.destroy() - def format_path(self, path): - """Format using $F{padding} token if sequence, otherwise just path.""" + @staticmethod + def format_path(path, representation): + """Format file path correctly for single redshift proxy + or redshift proxy sequence.""" + import re + if not os.path.exists(path): + raise RuntimeError("Path does not exist: %s" % path) - # Find all frames in the folder - ext = ".rs" - folder = os.path.dirname(path) - frames = [f for f in os.listdir(folder) if f.endswith(ext)] - - # Get the collection of frames to detect frame padding - patterns = [clique.PATTERNS["frames"]] - collections, remainder = clique.assemble(frames, - minimum_items=1, - patterns=patterns) - self.log.debug("Detected collections: {}".format(collections)) - self.log.debug("Detected remainder: {}".format(remainder)) - - if not collections and remainder: - if len(remainder) != 1: - raise ValueError("Frames not correctly detected " - "in: {}".format(remainder)) - - # A single frame without frame range detected - return os.path.normpath(path).replace("\\", "/") - - # Frames detected with a valid "frame" number pattern - # Then we don't want to have any remainder files found - assert len(collections) == 1 and not remainder - collection = collections[0] - - num_frames = len(collection.indexes) - if num_frames == 1: - # Return the input path without dynamic $F variable - result = path + is_sequence = bool(representation["context"].get("frame")) + # The path is either a single file or sequence in a folder. + if not is_sequence: + filename = path else: - # More than a single frame detected - use $F{padding} - fname = "{}$F{}{}".format(collection.head, - collection.padding, - collection.tail) - result = os.path.join(folder, fname) + filename = re.sub(r"(.*)\.(\d+)\.(rs.*)", "\\1.$F4.\\3", path) - # Format file name, Houdini only wants forward slashes - return os.path.normpath(result).replace("\\", "/") + filename = os.path.join(path, filename) + + filename = os.path.normpath(filename) + filename = filename.replace("\\", "/") + + return filename From 950581fcd865c43482995bed876ce10977648f70 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 24 Nov 2023 18:14:01 +0800 Subject: [PATCH 059/291] code tweaks on getting texture from the viewport_options dict --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index a3a2f8a5a5..26b2ac7086 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -45,7 +45,7 @@ class ExtractPlayblast(publish.Extractor): ) if ( preset["viewport_options"].get("reloadTextures") - and "textures" in preset["viewport_options"] + and preset["viewport_options"].get("textures") ): lib.reload_textures() path = capture.capture(log=self.log, **preset) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index ef843c9df8..a64d31f6d9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -154,7 +154,7 @@ class ExtractThumbnail(publish.Extractor): ) if ( preset["viewport_options"].get("reloadTextures") - and "textures" in preset["viewport_options"] + and preset["viewport_options"].get("textures") ): lib.reload_textures() path = capture.capture(**preset) From 9ae1e7950915adab798840520455618c32f18cf0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 24 Nov 2023 18:35:07 +0800 Subject: [PATCH 060/291] replace deprecated self.fname by self.filepath_from_context --- .../plugins/load/load_redshift_proxy.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py b/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py index 8a3cc04eab..efd7c6d0ca 100644 --- a/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/houdini/plugins/load/load_redshift_proxy.py @@ -1,9 +1,11 @@ import os +import re from openpype.pipeline import ( load, get_representation_path, ) from openpype.hosts.houdini.api import pipeline +from openpype.pipeline.load import LoadError import hou @@ -34,15 +36,16 @@ class RedshiftProxyLoader(load.LoaderPlugin): # redshift is not set up or initialized correctly if not container.parm("RS_objprop_proxy_enable"): container.destroy() - raise RuntimeError("Unable to initialize geo node with Redshift " - "attributes. Make sure you have the Redshift " - "plug-in set up correctly for Houdini.") + raise LoadError("Unable to initialize geo node with Redshift " + "attributes. Make sure you have the Redshift " + "plug-in set up correctly for Houdini.") # Enable by default container.setParms({ "RS_objprop_proxy_enable": True, "RS_objprop_proxy_file": self.format_path( - self.fname, context["representation"]) + self.filepath_from_context(context), + context["representation"]) }) # Remove the file node, it only loads static meshes @@ -92,18 +95,16 @@ class RedshiftProxyLoader(load.LoaderPlugin): def format_path(path, representation): """Format file path correctly for single redshift proxy or redshift proxy sequence.""" - import re if not os.path.exists(path): raise RuntimeError("Path does not exist: %s" % path) is_sequence = bool(representation["context"].get("frame")) # The path is either a single file or sequence in a folder. - if not is_sequence: - filename = path - else: + if is_sequence: filename = re.sub(r"(.*)\.(\d+)\.(rs.*)", "\\1.$F4.\\3", path) - filename = os.path.join(path, filename) + else: + filename = path filename = os.path.normpath(filename) filename = filename.replace("\\", "/") From 39faa7001e53f5f3dd76064b9ae9d6ddb3daf3be Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 27 Nov 2023 18:09:06 +0800 Subject: [PATCH 061/291] pop the value of reloadTextures before capture --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 ++ openpype/vendor/python/common/capture.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 26b2ac7086..e59309c0fd 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -48,6 +48,8 @@ class ExtractPlayblast(publish.Extractor): and preset["viewport_options"].get("textures") ): lib.reload_textures() + + preset.pop("reloadTextures") # not supported by `capture` path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) diff --git a/openpype/vendor/python/common/capture.py b/openpype/vendor/python/common/capture.py index b6d15ae47a..224699f916 100644 --- a/openpype/vendor/python/common/capture.py +++ b/openpype/vendor/python/common/capture.py @@ -760,8 +760,6 @@ def _applied_viewport_options(options, panel): # Try to set as much as possible of the state by setting them one by # one. This way we can also report the failing key values explicitly. for key, value in options.items(): - if key == "reloadTextures": - continue try: cmds.modelEditor(panel, edit=True, **{key: value}) except TypeError: From 84f58241a6f1347d12fb9b7e1868765a0eeef428 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 27 Nov 2023 18:49:12 +0800 Subject: [PATCH 062/291] pop the reloadvalues from the preset in thumbnail extractor --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index a64d31f6d9..380810d8c0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -157,6 +157,7 @@ class ExtractThumbnail(publish.Extractor): and preset["viewport_options"].get("textures") ): lib.reload_textures() + preset.pop("reloadTextures") # not supported by `capture` path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From 69c45c517f3dd594c379f1172ce739ceeba9cb39 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 27 Nov 2023 23:32:47 +0800 Subject: [PATCH 063/291] make sure reloadtextures is popped when it exists in the preset dict --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 +-- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index e59309c0fd..56113d6a53 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -48,8 +48,7 @@ class ExtractPlayblast(publish.Extractor): and preset["viewport_options"].get("textures") ): lib.reload_textures() - - preset.pop("reloadTextures") # not supported by `capture` + preset.pop("reloadTextures") # not supported by `capture` path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 380810d8c0..aa0a68e4f5 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -157,7 +157,7 @@ class ExtractThumbnail(publish.Extractor): and preset["viewport_options"].get("textures") ): lib.reload_textures() - preset.pop("reloadTextures") # not supported by `capture` + preset.pop("reloadTextures") # not supported by `capture` path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From 899bf8604661efee620a7dd65a44c41e9bd191ab Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Nov 2023 12:13:32 +0800 Subject: [PATCH 064/291] tweak on the preset.pop --- openpype/hosts/max/plugins/publish/extract_thumbnail.py | 7 +++++-- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 02fa75e032..114575cd0e 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -1,10 +1,10 @@ import os import pyblish.api -from openpype.pipeline import publish +from openpype.pipeline import publish, OptionalPyblishPluginMixin from openpype.hosts.max.api.preview_animation import render_preview_animation -class ExtractThumbnail(publish.Extractor): +class ExtractThumbnail(publish.Extractor, OptionalPyblishPluginMixin): """Extract Thumbnail for Review """ @@ -12,8 +12,11 @@ class ExtractThumbnail(publish.Extractor): label = "Extract Thumbnail" hosts = ["max"] families = ["review"] + optional = True def process(self, instance): + if not self.is_active(instance.data): + return ext = instance.data.get("imageFormat") frame = int(instance.data["frameStart"]) staging_dir = self.staging_dir(instance) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 56113d6a53..0e001497bd 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -48,7 +48,7 @@ class ExtractPlayblast(publish.Extractor): and preset["viewport_options"].get("textures") ): lib.reload_textures() - preset.pop("reloadTextures") # not supported by `capture` + preset.pop("reloadTextures", None) # not supported by `capture` path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) From cafd02a8512b6cd24137febcc393443240949f69 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 28 Nov 2023 12:15:26 +0800 Subject: [PATCH 065/291] preset.pop tweaks in thumbnail extractor --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index aa0a68e4f5..67455f60f0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -157,7 +157,7 @@ class ExtractThumbnail(publish.Extractor): and preset["viewport_options"].get("textures") ): lib.reload_textures() - preset.pop("reloadTextures") # not supported by `capture` + preset.pop("reloadTextures", None) # not supported by `capture` path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From b502019d2b7c9bbaab06fde78c41d951c7c39a71 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Dec 2023 15:23:56 +0800 Subject: [PATCH 066/291] bug fix on collector for not being able to collect textures from texture nodes in yeti graph --- openpype/hosts/maya/plugins/publish/collect_yeti_rig.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py index df761cde13..b05bbc7961 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py @@ -128,7 +128,12 @@ class CollectYetiRig(pyblish.api.InstancePlugin): image_search_paths = self._replace_tokens(image_search_paths) # List all related textures - texture_filenames = cmds.pgYetiCommand(node, listTextures=True) + texture_nodes = cmds.pgYetiGraph(node, listNodes=True, type="texture") + texture_filenames = [cmds.pgYetiGraph(node, + node=texture_node, + param="file_name", + getParamValue=True) + for texture_node in texture_nodes] self.log.debug("Found %i texture(s)" % len(texture_filenames)) # Get all reference nodes From caf8a2a2dfdcb259ee80e20c223078e12b1d2342 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Dec 2023 17:41:28 +0800 Subject: [PATCH 067/291] hound --- openpype/hosts/maya/plugins/publish/collect_yeti_rig.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py index b05bbc7961..7a2121742c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py @@ -128,12 +128,13 @@ class CollectYetiRig(pyblish.api.InstancePlugin): image_search_paths = self._replace_tokens(image_search_paths) # List all related textures - texture_nodes = cmds.pgYetiGraph(node, listNodes=True, type="texture") + texture_nodes = cmds.pgYetiGraph( + node, listNodes=True, type="texture") texture_filenames = [cmds.pgYetiGraph(node, node=texture_node, param="file_name", getParamValue=True) - for texture_node in texture_nodes] + for texture_node in texture_nodes] self.log.debug("Found %i texture(s)" % len(texture_filenames)) # Get all reference nodes From 8c615a0db8e1ba251ad09d4d9dbabbdeceeb29cc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Dec 2023 17:42:33 +0800 Subject: [PATCH 068/291] hound --- openpype/hosts/maya/plugins/publish/collect_yeti_rig.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py index 7a2121742c..b2c124d2fd 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py @@ -134,7 +134,8 @@ class CollectYetiRig(pyblish.api.InstancePlugin): node=texture_node, param="file_name", getParamValue=True) - for texture_node in texture_nodes] + for texture_node + in texture_nodes] self.log.debug("Found %i texture(s)" % len(texture_filenames)) # Get all reference nodes From 3fe3b217415ad7cb89641ea74eba00fa78885c07 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Dec 2023 17:44:22 +0800 Subject: [PATCH 069/291] hound --- .../hosts/maya/plugins/publish/collect_yeti_rig.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py index b2c124d2fd..5f40d009c2 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py @@ -130,12 +130,12 @@ class CollectYetiRig(pyblish.api.InstancePlugin): # List all related textures texture_nodes = cmds.pgYetiGraph( node, listNodes=True, type="texture") - texture_filenames = [cmds.pgYetiGraph(node, - node=texture_node, - param="file_name", - getParamValue=True) - for texture_node - in texture_nodes] + texture_filenames = [ + cmds.pgYetiGraph( + node, node=texture_node, + param="file_name", getParamValue=True) + for texture_node in texture_nodes + ] self.log.debug("Found %i texture(s)" % len(texture_filenames)) # Get all reference nodes From 889f86457d9a8c9b94042863fa8ed484d21f920f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Dec 2023 17:46:06 +0800 Subject: [PATCH 070/291] hound --- openpype/hosts/maya/plugins/publish/collect_yeti_rig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py index 5f40d009c2..835934e1bf 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py @@ -132,8 +132,8 @@ class CollectYetiRig(pyblish.api.InstancePlugin): node, listNodes=True, type="texture") texture_filenames = [ cmds.pgYetiGraph( - node, node=texture_node, - param="file_name", getParamValue=True) + node, node=texture_node, + param="file_name", getParamValue=True) for texture_node in texture_nodes ] self.log.debug("Found %i texture(s)" % len(texture_filenames)) From 187b4087ec8486eaa4c4d4ab6d180dec26ef70e4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 4 Dec 2023 15:39:19 +0000 Subject: [PATCH 071/291] Optional preserve references. --- openpype/hosts/maya/plugins/create/create_mayascene.py | 10 ++++++++++ .../maya/plugins/publish/extract_maya_scene_raw.py | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_mayascene.py b/openpype/hosts/maya/plugins/create/create_mayascene.py index b61c97aebf..c6bf1662a1 100644 --- a/openpype/hosts/maya/plugins/create/create_mayascene.py +++ b/openpype/hosts/maya/plugins/create/create_mayascene.py @@ -1,4 +1,5 @@ from openpype.hosts.maya.api import plugin +from openpype.lib import BoolDef class CreateMayaScene(plugin.MayaCreator): @@ -9,3 +10,12 @@ class CreateMayaScene(plugin.MayaCreator): label = "Maya Scene" family = "mayaScene" icon = "file-archive-o" + + def get_instance_attr_defs(self): + return [ + BoolDef( + "preserve_references", + label="Preserve References", + default=True + ) + ] diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index ab170fe48c..bfce0607e1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -70,7 +70,9 @@ class ExtractMayaSceneRaw(publish.Extractor): force=True, typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, - preserveReferences=True, + preserveReferences=instance.data.get( + "creator_attributes", {} + ).get("preserve_references", True), constructionHistory=True, shader=True, constraints=True, From 8bd8dc1af7213b925864c0e3f4ce04f52bc124a5 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 6 Dec 2023 08:05:39 +0000 Subject: [PATCH 072/291] Move to attribute defs --- .../maya/plugins/create/create_mayascene.py | 10 --------- .../plugins/publish/extract_maya_scene_raw.py | 21 ++++++++++++++++--- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_mayascene.py b/openpype/hosts/maya/plugins/create/create_mayascene.py index c6bf1662a1..b61c97aebf 100644 --- a/openpype/hosts/maya/plugins/create/create_mayascene.py +++ b/openpype/hosts/maya/plugins/create/create_mayascene.py @@ -1,5 +1,4 @@ from openpype.hosts.maya.api import plugin -from openpype.lib import BoolDef class CreateMayaScene(plugin.MayaCreator): @@ -10,12 +9,3 @@ class CreateMayaScene(plugin.MayaCreator): label = "Maya Scene" family = "mayaScene" icon = "file-archive-o" - - def get_instance_attr_defs(self): - return [ - BoolDef( - "preserve_references", - label="Preserve References", - default=True - ) - ] diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index bfce0607e1..94b4d6dc5f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -6,6 +6,8 @@ from maya import cmds from openpype.hosts.maya.api.lib import maintained_selection from openpype.pipeline import AVALON_CONTAINER_ID, publish +from openpype.pipeline.publish import OpenPypePyblishPluginMixin +from openpype.lib import BoolDef class ExtractMayaSceneRaw(publish.Extractor): @@ -23,6 +25,16 @@ class ExtractMayaSceneRaw(publish.Extractor): "camerarig"] scene_type = "ma" + @classmethod + def get_attribute_defs(cls): + return [ + BoolDef( + "preserve_references", + label="Preserve References", + default=True + ) + ] + def process(self, instance): """Plugin entry point.""" ext_mapping = ( @@ -64,15 +76,18 @@ class ExtractMayaSceneRaw(publish.Extractor): # Perform extraction self.log.debug("Performing extraction ...") + attribute_values = self.get_attr_values_from_data( + instance.data + ) with maintained_selection(): cmds.select(selection, noExpand=True) cmds.file(path, force=True, typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 exportSelected=True, - preserveReferences=instance.data.get( - "creator_attributes", {} - ).get("preserve_references", True), + preserveReferences=attribute_values[ + "preserve_references" + ], constructionHistory=True, shader=True, constraints=True, From a8d0c4f822d93ba0ebe69636a34198eb0ba8baad Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 6 Dec 2023 08:06:35 +0000 Subject: [PATCH 073/291] Mixin class --- openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index 94b4d6dc5f..183dcd1fb3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -10,7 +10,7 @@ from openpype.pipeline.publish import OpenPypePyblishPluginMixin from openpype.lib import BoolDef -class ExtractMayaSceneRaw(publish.Extractor): +class ExtractMayaSceneRaw(publish.Extractor, OpenPypePyblishPluginMixin): """Extract as Maya Scene (raw). This will preserve all references, construction history, etc. From 07c7b258f5452181c8d10af3bea1b61c6615d685 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 6 Dec 2023 10:01:20 +0000 Subject: [PATCH 074/291] Update openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py Co-authored-by: Roy Nieterau --- .../hosts/maya/plugins/publish/extract_maya_scene_raw.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py index 183dcd1fb3..a4f313bdf9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py +++ b/openpype/hosts/maya/plugins/publish/extract_maya_scene_raw.py @@ -31,6 +31,12 @@ class ExtractMayaSceneRaw(publish.Extractor, OpenPypePyblishPluginMixin): BoolDef( "preserve_references", label="Preserve References", + tooltip=( + "When enabled references will still be references " + "in the published file.\nWhen disabled the references " + "are imported into the published file generating a " + "file without references." + ), default=True ) ] From cbc4c679223a6a3230105b8fe9cae356a4cf3a59 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 7 Dec 2023 21:45:29 +0800 Subject: [PATCH 075/291] make sure the texture can be reloaded --- .../hosts/max/plugins/publish/extract_thumbnail.py | 6 ++---- .../hosts/maya/plugins/publish/extract_playblast.py | 11 ++++++----- .../hosts/maya/plugins/publish/extract_thumbnail.py | 13 +++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 114575cd0e..1b912ac0ec 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -1,10 +1,10 @@ import os import pyblish.api -from openpype.pipeline import publish, OptionalPyblishPluginMixin +from openpype.pipeline import publish from openpype.hosts.max.api.preview_animation import render_preview_animation -class ExtractThumbnail(publish.Extractor, OptionalPyblishPluginMixin): +class ExtractThumbnail(publish.Extractor): """Extract Thumbnail for Review """ @@ -15,8 +15,6 @@ class ExtractThumbnail(publish.Extractor, OptionalPyblishPluginMixin): optional = True def process(self, instance): - if not self.is_active(instance.data): - return ext = instance.data.get("imageFormat") frame = int(instance.data["frameStart"]) staging_dir = self.staging_dir(instance) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 0e001497bd..b885308613 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -43,11 +43,12 @@ class ExtractPlayblast(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) - if ( - preset["viewport_options"].get("reloadTextures") - and preset["viewport_options"].get("textures") - ): - lib.reload_textures() + if "textures" in preset["viewport_options"]: + if "reloadTextures" in preset["viewport_options"]: + lib.reload_textures() + else: + self.log.debug( + "Reload Textures during playblasting is disabled.") preset.pop("reloadTextures", None) # not supported by `capture` path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 67455f60f0..77a538b95d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -152,12 +152,13 @@ class ExtractThumbnail(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) - if ( - preset["viewport_options"].get("reloadTextures") - and preset["viewport_options"].get("textures") - ): - lib.reload_textures() - preset.pop("reloadTextures", None) # not supported by `capture` + if "textures" in preset["viewport_options"]: + if "reloadTextures" in preset["viewport_options"]: + lib.reload_textures() + else: + self.log.debug( + "Reload Textures during playblasting is disabled.") + preset.pop("reloadTextures", None) path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From 7061eabdb52f06abcb12bccc1ff5ef79bced75bb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 7 Dec 2023 21:47:03 +0800 Subject: [PATCH 076/291] restore unnecessary tweaks --- openpype/hosts/max/plugins/publish/extract_thumbnail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/extract_thumbnail.py b/openpype/hosts/max/plugins/publish/extract_thumbnail.py index 1b912ac0ec..02fa75e032 100644 --- a/openpype/hosts/max/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/max/plugins/publish/extract_thumbnail.py @@ -12,7 +12,6 @@ class ExtractThumbnail(publish.Extractor): label = "Extract Thumbnail" hosts = ["max"] families = ["review"] - optional = True def process(self, instance): ext = instance.data.get("imageFormat") From bcdeec3461db61d535721c18d00c0e5c1264956d Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 8 Dec 2023 23:01:41 +0200 Subject: [PATCH 077/291] refactor get_output_parameter --- openpype/hosts/houdini/api/lib.py | 114 ++++++++++-------------------- 1 file changed, 36 insertions(+), 78 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 614052431f..e1a0c4310b 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -121,62 +121,6 @@ def get_id_required_nodes(): return list(nodes) -def get_export_parameter(node): - """Return the export output parameter of the given node - - Example: - root = hou.node("/obj") - my_alembic_node = root.createNode("alembic") - get_output_parameter(my_alembic_node) - # Result: "output" - - Args: - node(hou.Node): node instance - - Returns: - hou.Parm - - """ - node_type = node.type().description() - - # Ensures the proper Take is selected for each ROP to retrieve the correct - # ifd - try: - rop_take = hou.takes.findTake(node.parm("take").eval()) - if rop_take is not None: - hou.takes.setCurrentTake(rop_take) - except AttributeError: - # hou object doesn't always have the 'takes' attribute - pass - - if node_type == "Mantra" and node.parm("soho_outputmode").eval(): - return node.parm("soho_diskfile") - elif node_type == "Alfred": - return node.parm("alf_diskfile") - elif (node_type == "RenderMan" or node_type == "RenderMan RIS"): - pre_ris22 = node.parm("rib_outputmode") and \ - node.parm("rib_outputmode").eval() - ris22 = node.parm("diskfile") and node.parm("diskfile").eval() - if pre_ris22 or ris22: - return node.parm("soho_diskfile") - elif node_type == "Redshift" and node.parm("RS_archive_enable").eval(): - return node.parm("RS_archive_file") - elif node_type == "Wedge" and node.parm("driver").eval(): - return get_export_parameter(node.node(node.parm("driver").eval())) - elif node_type == "Arnold": - return node.parm("ar_ass_file") - elif node_type == "Alembic" and node.parm("use_sop_path").eval(): - return node.parm("sop_path") - elif node_type == "Shotgun Mantra" and node.parm("soho_outputmode").eval(): - return node.parm("sgtk_soho_diskfile") - elif node_type == "Shotgun Alembic" and node.parm("use_sop_path").eval(): - return node.parm("sop_path") - elif node.type().nameWithCategory() == "Driver/vray_renderer": - return node.parm("render_export_filepath") - - raise TypeError("Node type '%s' not supported" % node_type) - - def get_output_parameter(node): """Return the render output parameter of the given node @@ -184,7 +128,7 @@ def get_output_parameter(node): root = hou.node("/obj") my_alembic_node = root.createNode("alembic") get_output_parameter(my_alembic_node) - # Result: "output" + # Result: "filename" Args: node(hou.Node): node instance @@ -192,33 +136,47 @@ def get_output_parameter(node): Returns: hou.Parm + Note 1: + I'm using node.type().name() to get on par with the creators, + Because the return value of `node.type().name()` is the same string value used in creators + e.g. instance_data.update({"node_type": "alembic"}) + + Note 2: + Rop nodes in different network categories have the same output parameter. + So, I took that into consideration as a hint for future development. + """ - node_type = node.type().description() - category = node.type().category().name() + + node_type = node.type().name() # Figure out which type of node is being rendered - if node_type == "Geometry" or node_type == "Filmbox FBX" or \ - (node_type == "ROP Output Driver" and category == "Sop"): - return node.parm("sopoutput") - elif node_type == "Composite": - return node.parm("copoutput") - elif node_type == "opengl": - return node.parm("picture") - elif node_type == "arnold": - if node.evalParm("ar_ass_export_enable"): + if node_type == "alembic" or node_type == "rop_alembic": + return node.parm("filename") + elif node_type == "arnold": + if node_type.evalParm("ar_ass_export_enable"): return node.parm("ar_ass_file") - elif node_type == "Redshift_Proxy_Output": - return node.parm("RS_archive_file") - elif node_type == "ifd": + else: + return node.parm("ar_picture") + elif node_type == "geometry" or node_type == "rop_geometry" or node_type == "filmboxfbx" or node_type == "rop_fbx" : + return node.parm("sopoutput") + elif node_type == "comp": + return node.parm("copoutput") + elif node_type == "karma" or node_type == "opengl": + return node.parm("picture") + elif node_type == "ifd": # Matnra if node.evalParm("soho_outputmode"): return node.parm("soho_diskfile") - elif node_type == "Octane": - return node.parm("HO_img_fileName") - elif node_type == "Fetch": - inner_node = node.node(node.parm("source").eval()) - if inner_node: - return get_output_parameter(inner_node) - elif node.type().nameWithCategory() == "Driver/vray_renderer": + else: + return node.parm("vm_picture") + elif node_type == "Redshift_Proxy_Output": + return node.parm("RS_archive_file") + elif node_type == "Redshift_ROP": + return node.parm("RS_outputFileNamePrefix") + elif node_type == "usd" or node_type == "usd_rop" or node_type == "usdexport": + return node.parm("lopoutput") + elif node_type == "usdrender" or node_type == "usdrender_rop": + return node.parm("outputimage") + elif node_type == "vray_renderer": return node.parm("SettingsOutput_img_file_path") raise TypeError("Node type '%s' not supported" % node_type) From 91e80a1831d81bda0bd5f6de8df94678b08593d7 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 11 Dec 2023 13:05:00 +0200 Subject: [PATCH 078/291] resolve hound - fix lint problems --- openpype/hosts/houdini/api/lib.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index e1a0c4310b..634535604d 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -138,11 +138,13 @@ def get_output_parameter(node): Note 1: I'm using node.type().name() to get on par with the creators, - Because the return value of `node.type().name()` is the same string value used in creators + Because the return value of `node.type().name()` is the same string + value used in creators e.g. instance_data.update({"node_type": "alembic"}) Note 2: - Rop nodes in different network categories have the same output parameter. + Rop nodes in different network categories have + the same output parameter. So, I took that into consideration as a hint for future development. """ @@ -152,12 +154,17 @@ def get_output_parameter(node): # Figure out which type of node is being rendered if node_type == "alembic" or node_type == "rop_alembic": return node.parm("filename") - elif node_type == "arnold": + elif node_type == "arnold": if node_type.evalParm("ar_ass_export_enable"): return node.parm("ar_ass_file") else: return node.parm("ar_picture") - elif node_type == "geometry" or node_type == "rop_geometry" or node_type == "filmboxfbx" or node_type == "rop_fbx" : + elif ( + node_type == "geometry" or + node_type == "rop_geometry" or + node_type == "filmboxfbx" or + node_type == "rop_fbx" + ): return node.parm("sopoutput") elif node_type == "comp": return node.parm("copoutput") @@ -172,7 +179,11 @@ def get_output_parameter(node): return node.parm("RS_archive_file") elif node_type == "Redshift_ROP": return node.parm("RS_outputFileNamePrefix") - elif node_type == "usd" or node_type == "usd_rop" or node_type == "usdexport": + elif ( + node_type == "usd" or + node_type == "usd_rop" or + node_type == "usdexport" + ): return node.parm("lopoutput") elif node_type == "usdrender" or node_type == "usdrender_rop": return node.parm("outputimage") From b73146a538a13c8e5f94a5cb1d3ca5e0c3d4eefe Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 11 Dec 2023 23:47:10 +0800 Subject: [PATCH 079/291] preset pop value should be correct --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 3 ++- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index b885308613..4ce7e19ee2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -49,7 +49,8 @@ class ExtractPlayblast(publish.Extractor): else: self.log.debug( "Reload Textures during playblasting is disabled.") - preset.pop("reloadTextures", None) # not supported by `capture` + # not supported by `capture` + preset["viewport_options"].pop("reloadTextures", None) path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 77a538b95d..bc5f9bc4ed 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -158,7 +158,8 @@ class ExtractThumbnail(publish.Extractor): else: self.log.debug( "Reload Textures during playblasting is disabled.") - preset.pop("reloadTextures", None) + # not supported by `capture` + preset["viewport_options"].pop("reloadTextures", None) path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From 28a62bff59fc12c857c38090b923280a0c2d9ffc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 12 Dec 2023 00:15:25 +0800 Subject: [PATCH 080/291] make sure the material loading mode is parallel --- openpype/hosts/maya/api/lib.py | 12 +++++++++++- .../maya/plugins/publish/extract_playblast.py | 18 ++++++++++-------- .../maya/plugins/publish/extract_thumbnail.py | 8 ++++++-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 2a9defbf2d..817688258b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -180,7 +180,17 @@ def reload_textures(): for texture_file in texture_files: if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0: cmds.ogs(regenerateUVTilePreview=texture_file) - cmds.ogs(reloadTextures=True) + + +@contextlib.contextmanager +def material_loading_mode(mode="immediate"): + """Set material loading mode during context""" + original = cmds.displayPref(query=True, materialLoadingMode=True) + cmds.displayPref(materialLoadingMode=mode) + try: + yield + finally: + cmds.displayPref(materialLoadingMode=original) def get_namespace(node): diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 4ce7e19ee2..b540a2c56d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -43,15 +43,17 @@ class ExtractPlayblast(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) - if "textures" in preset["viewport_options"]: - if "reloadTextures" in preset["viewport_options"]: + if "textures" in preset["viewport_options"] and ( + "reloadTextures" in preset["viewport_options"] + ): + with lib.material_loading_mode(): lib.reload_textures() - else: - self.log.debug( - "Reload Textures during playblasting is disabled.") - # not supported by `capture` - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(log=self.log, **preset) + # not supported by `capture` + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(log=self.log, **preset) + else: + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) def process(self, instance): diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index bc5f9bc4ed..10082436d6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -154,12 +154,16 @@ class ExtractThumbnail(publish.Extractor): ) if "textures" in preset["viewport_options"]: if "reloadTextures" in preset["viewport_options"]: - lib.reload_textures() + with lib.material_loading_mode(): + lib.reload_textures() + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(**preset) else: self.log.debug( "Reload Textures during playblasting is disabled.") + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(**preset) # not supported by `capture` - preset["viewport_options"].pop("reloadTextures", None) path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From cb5fa7faa291b2d237a9ed5c10c0ddf496a05b2d Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 13 Dec 2023 19:02:15 +0200 Subject: [PATCH 081/291] Kuba's comments - Better code and doc string --- openpype/hosts/houdini/api/lib.py | 55 ++++++++++++++----------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 634535604d..39a7006ef4 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -128,64 +128,57 @@ def get_output_parameter(node): root = hou.node("/obj") my_alembic_node = root.createNode("alembic") get_output_parameter(my_alembic_node) - # Result: "filename" + >>> "filename" + + Notes: + I'm using node.type().name() to get on par with the creators, + Because the return value of `node.type().name()` is the + same string value used in creators + e.g. instance_data.update({"node_type": "alembic"}) + + Rop nodes in different network categories have + the same output parameter. + So, I took that into consideration as a hint for + future development. Args: node(hou.Node): node instance Returns: hou.Parm - - Note 1: - I'm using node.type().name() to get on par with the creators, - Because the return value of `node.type().name()` is the same string - value used in creators - e.g. instance_data.update({"node_type": "alembic"}) - - Note 2: - Rop nodes in different network categories have - the same output parameter. - So, I took that into consideration as a hint for future development. - """ node_type = node.type().name() # Figure out which type of node is being rendered - if node_type == "alembic" or node_type == "rop_alembic": + if node_type in {"alembic", "rop_alembic"}: return node.parm("filename") elif node_type == "arnold": if node_type.evalParm("ar_ass_export_enable"): return node.parm("ar_ass_file") - else: - return node.parm("ar_picture") - elif ( - node_type == "geometry" or - node_type == "rop_geometry" or - node_type == "filmboxfbx" or - node_type == "rop_fbx" - ): + return node.parm("ar_picture") + elif node_type in { + "geometry", + "rop_geometry", + "filmboxfbx", + "rop_fbx" + }: return node.parm("sopoutput") elif node_type == "comp": return node.parm("copoutput") - elif node_type == "karma" or node_type == "opengl": + elif node_type in {"karma", "opengl"}: return node.parm("picture") elif node_type == "ifd": # Matnra if node.evalParm("soho_outputmode"): return node.parm("soho_diskfile") - else: - return node.parm("vm_picture") + return node.parm("vm_picture") elif node_type == "Redshift_Proxy_Output": return node.parm("RS_archive_file") elif node_type == "Redshift_ROP": return node.parm("RS_outputFileNamePrefix") - elif ( - node_type == "usd" or - node_type == "usd_rop" or - node_type == "usdexport" - ): + elif node_type in {"usd", "usd_rop", "usdexport"}: return node.parm("lopoutput") - elif node_type == "usdrender" or node_type == "usdrender_rop": + elif node_type in {"usdrender", "usdrender_rop"}: return node.parm("outputimage") elif node_type == "vray_renderer": return node.parm("SettingsOutput_img_file_path") From 58dee9e05e84231d5f2651371f03e036c6ceca05 Mon Sep 17 00:00:00 2001 From: Mustafa Taher Date: Wed, 13 Dec 2023 23:34:48 +0200 Subject: [PATCH 082/291] BigRoy comment - Fix Typo Co-authored-by: Roy Nieterau --- openpype/hosts/houdini/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 39a7006ef4..edd50f10c1 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -168,7 +168,7 @@ def get_output_parameter(node): return node.parm("copoutput") elif node_type in {"karma", "opengl"}: return node.parm("picture") - elif node_type == "ifd": # Matnra + elif node_type == "ifd": # Mantra if node.evalParm("soho_outputmode"): return node.parm("soho_diskfile") return node.parm("vm_picture") From 74daec7f97ba2036ccdcae67fb97103779c406d8 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 14 Dec 2023 00:07:15 +0200 Subject: [PATCH 083/291] fix a bug with render split --- openpype/modules/deadline/abstract_submit_deadline.py | 2 +- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 187feb9b1a..e261800656 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -464,7 +464,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, self.log.info("Submitted job to Deadline: {}.".format(job_id)) # TODO: Find a way that's more generic and not render type specific - if "exportJob" in instance.data: + if instance.data.get("exportJob"): self.log.info("Splitting export and render in two jobs") self.log.info("Export job id: %s", job_id) render_job_info = self.get_job_info(dependency_job_ids=[job_id]) 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 0c75f632cb..6ab8e4b666 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -124,7 +124,7 @@ class HoudiniSubmitDeadline( # Whether Deadline render submission is being split in two # (extract + render) - split_render_job = instance.data["exportJob"] + split_render_job = instance.data.get("exportJob") # If there's some dependency job ids we can assume this is a render job # and not an export job From 9b33073791de81f1f3faf16282c9834f9e98d133 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 14 Dec 2023 00:49:47 +0200 Subject: [PATCH 084/291] rename `exportJob` flag to `split_render` --- .../hosts/houdini/plugins/publish/collect_arnold_rop.py | 6 +++--- .../hosts/houdini/plugins/publish/collect_mantra_rop.py | 6 +++--- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 6 +++--- openpype/modules/deadline/abstract_submit_deadline.py | 2 +- .../plugins/publish/submit_houdini_render_deadline.py | 2 +- 5 files changed, 11 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 c7da8397dc..45d950106e 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -41,11 +41,11 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): render_products = [] # Store whether we are splitting the render job (export + render) - export_job = bool(rop.parm("ar_ass_export_enable").eval()) - instance.data["exportJob"] = export_job + split_render = bool(rop.parm("ar_ass_export_enable").eval()) + instance.data["split_render"] = split_render export_prefix = None export_products = [] - if export_job: + if split_render: export_prefix = evalParmNoFrame( rop, "ar_ass_file", pad_character="0" ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index bc71576174..a28b425057 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -45,11 +45,11 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): render_products = [] # Store whether we are splitting the render job (export + render) - export_job = bool(rop.parm("soho_outputmode").eval()) - instance.data["exportJob"] = export_job + split_render = bool(rop.parm("soho_outputmode").eval()) + instance.data["split_render"] = split_render export_prefix = None export_products = [] - if export_job: + if split_render: export_prefix = evalParmNoFrame( rop, "soho_diskfile", pad_character="0" ) diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index a1f4554726..6e8fe1cc79 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -46,11 +46,11 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): # TODO: add render elements if render element # Store whether we are splitting the render job in an export + render - export_job = rop.parm("render_export_mode").eval() == "2" - instance.data["exportJob"] = export_job + split_render = rop.parm("render_export_mode").eval() == "2" + instance.data["split_render"] = split_render export_prefix = None export_products = [] - if export_job: + if split_render: export_prefix = evalParmNoFrame( rop, "render_export_filepath", pad_character="0" ) diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index e261800656..45aba560ba 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -464,7 +464,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, self.log.info("Submitted job to Deadline: {}.".format(job_id)) # TODO: Find a way that's more generic and not render type specific - if instance.data.get("exportJob"): + if instance.data.get("split_render"): self.log.info("Splitting export and render in two jobs") self.log.info("Export job id: %s", job_id) render_job_info = self.get_job_info(dependency_job_ids=[job_id]) 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 6ab8e4b666..1fb1143e52 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -124,7 +124,7 @@ class HoudiniSubmitDeadline( # Whether Deadline render submission is being split in two # (extract + render) - split_render_job = instance.data.get("exportJob") + split_render_job = instance.data.get("split_render") # If there's some dependency job ids we can assume this is a render job # and not an export job From ed24690f39a1ccdb7c2cd523eb421b626f714e9a Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Thu, 14 Dec 2023 11:27:12 +0200 Subject: [PATCH 085/291] BigRoy's comment - add tags to houdini deadline render jobs --- .../plugins/publish/submit_houdini_render_deadline.py | 5 ++++- 1 file changed, 4 insertions(+), 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 1fb1143e52..b3e29c7e2d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -132,18 +132,21 @@ class HoudiniSubmitDeadline( if dependency_job_ids: is_export_job = False + job_type = "[RENDER]" if split_render_job and not is_export_job: # Convert from family to Deadline plugin name # i.e., arnold_rop -> Arnold plugin = instance.data["family"].replace("_rop", "").capitalize() else: plugin = "Houdini" + if split_render_job: + job_type = "[EXPORT IFD]" job_info = DeadlineJobInfo(Plugin=plugin) filepath = context.data["currentFile"] filename = os.path.basename(filepath) - job_info.Name = "{} - {}".format(filename, instance.name) + job_info.Name = "{} - {} {}".format(filename, instance.name, job_type) job_info.BatchName = filename job_info.UserName = context.data.get( From 464889529132d2a6921dfb8fe80abe7db2f02d38 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Dec 2023 23:09:11 +0800 Subject: [PATCH 086/291] make sure the contextlib.nested used before material loading while it is compatible for both python2 and 3 --- openpype/hosts/maya/api/lib.py | 5 ++- .../maya/plugins/publish/extract_playblast.py | 42 ++++++++++++------- .../maya/plugins/publish/extract_thumbnail.py | 21 +++++----- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 817688258b..41290b805e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -172,8 +172,9 @@ def maintained_selection(): cmds.select(clear=True) -def reload_textures(): - """Reload textures during playblast +def reload_all_udim_tile_previews(): + """Regenerate all UDIM tile preview in texture file + nodes during context """ texture_files = cmds.ls(type="file") if texture_files: diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index b540a2c56d..7bcddf97f1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -43,17 +43,13 @@ class ExtractPlayblast(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) - if "textures" in preset["viewport_options"] and ( - "reloadTextures" in preset["viewport_options"] - ): - with lib.material_loading_mode(): - lib.reload_textures() - # not supported by `capture` - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(log=self.log, **preset) - else: - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(log=self.log, **preset) + + if preset["viewport_options"].get("reloadTextures"): + # Regenerate all UDIM tiles previews + lib.reload_all_udim_tile_previews() + # not supported by `capture` + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) def process(self, instance): @@ -206,11 +202,23 @@ class ExtractPlayblast(publish.Extractor): # TODO: Remove once dropping Python 2. if getattr(contextlib, "nested", None): # Python 3 compatibility. - with contextlib.nested( - lib.maintained_time(), - panel_camera(instance.data["panel"], preset["camera"]) - ): - self._capture(preset) + if preset["viewport_options"].get("textures"): + # If capture includes textures then ensure material + # load mode is set to `immediate` to ensure all + # textures have loaded when playblast starts + with contextlib.nested( + lib.maintained_time(), + panel_camera(instance.data["panel"], preset["camera"]), + lib.material_loading_mode() + ): + self._capture(preset) + + else: + with contextlib.nested( + lib.maintained_time(), + panel_camera(instance.data["panel"], preset["camera"]) + ): + self._capture(preset) else: # Python 2 compatibility. with contextlib.ExitStack() as stack: @@ -218,6 +226,8 @@ class ExtractPlayblast(publish.Extractor): stack.enter_context( panel_camera(instance.data["panel"], preset["camera"]) ) + if preset["viewport_options"].get("textures"): + stack.enter_context(lib.material_loading_mode()) self._capture(preset) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 10082436d6..b24cda8f07 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -152,19 +152,18 @@ class ExtractThumbnail(publish.Extractor): json.dumps(preset, indent=4, sort_keys=True) ) ) + + if "reloadTextures" in preset["viewport_options"]: + lib.reload_all_udim_tile_previews() + + preset["viewport_options"].pop("reloadTextures", None) if "textures" in preset["viewport_options"]: - if "reloadTextures" in preset["viewport_options"]: - with lib.material_loading_mode(): - lib.reload_textures() - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(**preset) - else: - self.log.debug( - "Reload Textures during playblasting is disabled.") - preset["viewport_options"].pop("reloadTextures", None) + with lib.material_loading_mode(): path = capture.capture(**preset) - # not supported by `capture` - path = capture.capture(**preset) + else: + self.log.debug("Reload Textures during playblasting is disabled.") + path = capture.capture(**preset) + playblast = self._fix_playblast_output_path(path) _, thumbnail = os.path.split(playblast) From c62862a773ebc819d1385771a10287c8ae04f9df Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Dec 2023 23:10:21 +0800 Subject: [PATCH 087/291] hound --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index b24cda8f07..3f25a9b17b 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -161,7 +161,8 @@ class ExtractThumbnail(publish.Extractor): with lib.material_loading_mode(): path = capture.capture(**preset) else: - self.log.debug("Reload Textures during playblasting is disabled.") + self.log.debug( + "Reload Textures during playblasting is disabled.") path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From 9f1ab7519fac8f7715ea42c21578fff3ef4225e2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Dec 2023 23:48:05 +0800 Subject: [PATCH 088/291] add exitstack.py into maya api folder & code tweaks --- openpype/hosts/maya/api/exitstack.py | 120 ++++++++++++++++++ .../maya/plugins/publish/extract_playblast.py | 37 ++---- .../maya/plugins/publish/extract_thumbnail.py | 4 +- 3 files changed, 130 insertions(+), 31 deletions(-) create mode 100644 openpype/hosts/maya/api/exitstack.py diff --git a/openpype/hosts/maya/api/exitstack.py b/openpype/hosts/maya/api/exitstack.py new file mode 100644 index 0000000000..dcf7131bd3 --- /dev/null +++ b/openpype/hosts/maya/api/exitstack.py @@ -0,0 +1,120 @@ +import contextlib + # TODO: Remove the entire script once dropping Python 2. +if getattr(contextlib, "nested", None): + from contextlib import ExitStack # noqa +else: + import sys + from collections import deque + + + class ExitStack(object): + """Context manager for dynamic management of a stack of exit callbacks + + For example: + + with ExitStack() as stack: + files = [stack.enter_context(open(fname)) for fname in filenames] + # All opened files will automatically be closed at the end of + # the with statement, even if attempts to open files later + # in the list raise an exception + + """ + def __init__(self): + self._exit_callbacks = deque() + + def pop_all(self): + """Preserve the context stack by transferring it to a new instance""" + new_stack = type(self)() + new_stack._exit_callbacks = self._exit_callbacks + self._exit_callbacks = deque() + return new_stack + + def _push_cm_exit(self, cm, cm_exit): + """Helper to correctly register callbacks to __exit__ methods""" + def _exit_wrapper(*exc_details): + return cm_exit(cm, *exc_details) + _exit_wrapper.__self__ = cm + self.push(_exit_wrapper) + + def push(self, exit): + """Registers a callback with the standard __exit__ method signature + + Can suppress exceptions the same way __exit__ methods can. + + Also accepts any object with an __exit__ method (registering a call + to the method instead of the object itself) + """ + # We use an unbound method rather than a bound method to follow + # the standard lookup behaviour for special methods + _cb_type = type(exit) + try: + exit_method = _cb_type.__exit__ + except AttributeError: + # Not a context manager, so assume its a callable + self._exit_callbacks.append(exit) + else: + self._push_cm_exit(exit, exit_method) + return exit # Allow use as a decorator + + def callback(self, callback, *args, **kwds): + """Registers an arbitrary callback and arguments. + + Cannot suppress exceptions. + """ + def _exit_wrapper(exc_type, exc, tb): + callback(*args, **kwds) + # We changed the signature, so using @wraps is not appropriate, but + # setting __wrapped__ may still help with introspection + _exit_wrapper.__wrapped__ = callback + self.push(_exit_wrapper) + return callback # Allow use as a decorator + + def enter_context(self, cm): + """Enters the supplied context manager + + If successful, also pushes its __exit__ method as a callback and + returns the result of the __enter__ method. + """ + # We look up the special methods on the type to match the with statement + _cm_type = type(cm) + _exit = _cm_type.__exit__ + result = _cm_type.__enter__(cm) + self._push_cm_exit(cm, _exit) + return result + + def close(self): + """Immediately unwind the context stack""" + self.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, *exc_details): + # We manipulate the exception state so it behaves as though + # we were actually nesting multiple with statements + frame_exc = sys.exc_info()[1] + def _fix_exception_context(new_exc, old_exc): + while 1: + exc_context = new_exc.__context__ + if exc_context in (None, frame_exc): + break + new_exc = exc_context + new_exc.__context__ = old_exc + + # Callbacks are invoked in LIFO order to match the behaviour of + # nested context managers + suppressed_exc = False + while self._exit_callbacks: + cb = self._exit_callbacks.pop() + try: + if cb(*exc_details): + suppressed_exc = True + exc_details = (None, None, None) + except: + new_exc_details = sys.exc_info() + # simulate the stack of exceptions by setting the context + _fix_exception_context(new_exc_details[1], exc_details[1]) + if not self._exit_callbacks: + raise + exc_details = new_exc_details + return suppressed_exc diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 7bcddf97f1..2e11a5f26e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -7,6 +7,7 @@ import capture from openpype.pipeline import publish from openpype.hosts.maya.api import lib +from openpype.hosts.maya.api.exitstack import ExitStack from maya import cmds @@ -199,37 +200,15 @@ class ExtractPlayblast(publish.Extractor): preset.update(panel_preset) # Need to ensure Python 2 compatibility. - # TODO: Remove once dropping Python 2. - if getattr(contextlib, "nested", None): - # Python 3 compatibility. + with ExitStack() as stack: + stack.enter_context(lib.maintained_time()) + stack.enter_context( + panel_camera(instance.data["panel"], preset["camera"]) + ) if preset["viewport_options"].get("textures"): - # If capture includes textures then ensure material - # load mode is set to `immediate` to ensure all - # textures have loaded when playblast starts - with contextlib.nested( - lib.maintained_time(), - panel_camera(instance.data["panel"], preset["camera"]), - lib.material_loading_mode() - ): - self._capture(preset) + stack.enter_context(lib.material_loading_mode()) - else: - with contextlib.nested( - lib.maintained_time(), - panel_camera(instance.data["panel"], preset["camera"]) - ): - self._capture(preset) - else: - # Python 2 compatibility. - with contextlib.ExitStack() as stack: - stack.enter_context(lib.maintained_time()) - stack.enter_context( - panel_camera(instance.data["panel"], preset["camera"]) - ) - if preset["viewport_options"].get("textures"): - stack.enter_context(lib.material_loading_mode()) - - self._capture(preset) + self._capture(preset) # Restoring viewport options. if viewport_defaults: diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 3f25a9b17b..b8e7b19bc6 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -153,11 +153,11 @@ class ExtractThumbnail(publish.Extractor): ) ) - if "reloadTextures" in preset["viewport_options"]: + if preset["viewport_options"].get("reloadTextures"): lib.reload_all_udim_tile_previews() preset["viewport_options"].pop("reloadTextures", None) - if "textures" in preset["viewport_options"]: + if preset["viewport_options"].get("textures"): with lib.material_loading_mode(): path = capture.capture(**preset) else: From b7da5708786130ebbfddaaa75d3ff40aea6453c9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Dec 2023 23:53:40 +0800 Subject: [PATCH 089/291] hound --- openpype/hosts/maya/api/exitstack.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/api/exitstack.py b/openpype/hosts/maya/api/exitstack.py index dcf7131bd3..ee32fad56b 100644 --- a/openpype/hosts/maya/api/exitstack.py +++ b/openpype/hosts/maya/api/exitstack.py @@ -1,19 +1,19 @@ import contextlib - # TODO: Remove the entire script once dropping Python 2. +# TODO: Remove the entire script once dropping Python 2. if getattr(contextlib, "nested", None): from contextlib import ExitStack # noqa else: import sys from collections import deque - class ExitStack(object): """Context manager for dynamic management of a stack of exit callbacks For example: with ExitStack() as stack: - files = [stack.enter_context(open(fname)) for fname in filenames] + files = [stack.enter_context(open(fname)) + for fname in filenames] # All opened files will automatically be closed at the end of # the with statement, even if attempts to open files later # in the list raise an exception @@ -30,7 +30,8 @@ else: return new_stack def _push_cm_exit(self, cm, cm_exit): - """Helper to correctly register callbacks to __exit__ methods""" + """Helper to correctly register callbacks + to __exit__ methods""" def _exit_wrapper(*exc_details): return cm_exit(cm, *exc_details) _exit_wrapper.__self__ = cm @@ -54,7 +55,7 @@ else: self._exit_callbacks.append(exit) else: self._push_cm_exit(exit, exit_method) - return exit # Allow use as a decorator + return exit # Allow use as a decorator def callback(self, callback, *args, **kwds): """Registers an arbitrary callback and arguments. @@ -67,7 +68,7 @@ else: # setting __wrapped__ may still help with introspection _exit_wrapper.__wrapped__ = callback self.push(_exit_wrapper) - return callback # Allow use as a decorator + return callback # Allow use as a decorator def enter_context(self, cm): """Enters the supplied context manager @@ -75,7 +76,8 @@ else: If successful, also pushes its __exit__ method as a callback and returns the result of the __enter__ method. """ - # We look up the special methods on the type to match the with statement + # We look up the special methods on the type to + # match the with statement _cm_type = type(cm) _exit = _cm_type.__exit__ result = _cm_type.__enter__(cm) @@ -93,6 +95,7 @@ else: # We manipulate the exception state so it behaves as though # we were actually nesting multiple with statements frame_exc = sys.exc_info()[1] + def _fix_exception_context(new_exc, old_exc): while 1: exc_context = new_exc.__context__ @@ -110,7 +113,7 @@ else: if cb(*exc_details): suppressed_exc = True exc_details = (None, None, None) - except: + except Exception: new_exc_details = sys.exc_info() # simulate the stack of exceptions by setting the context _fix_exception_context(new_exc_details[1], exc_details[1]) From 4e005bfd5780c17ea128ac25c480f339b048f96a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Dec 2023 23:55:46 +0800 Subject: [PATCH 090/291] hound --- openpype/hosts/maya/api/exitstack.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/exitstack.py b/openpype/hosts/maya/api/exitstack.py index ee32fad56b..9b049f1914 100644 --- a/openpype/hosts/maya/api/exitstack.py +++ b/openpype/hosts/maya/api/exitstack.py @@ -5,8 +5,7 @@ if getattr(contextlib, "nested", None): else: import sys from collections import deque - - class ExitStack(object): + class ExitStack(object) """Context manager for dynamic management of a stack of exit callbacks For example: From 7dc19ec7594ce20669b2d4727cf0a2267f119e58 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 15 Dec 2023 23:56:55 +0800 Subject: [PATCH 091/291] hound --- openpype/hosts/maya/api/exitstack.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/exitstack.py b/openpype/hosts/maya/api/exitstack.py index 9b049f1914..cacaa396f0 100644 --- a/openpype/hosts/maya/api/exitstack.py +++ b/openpype/hosts/maya/api/exitstack.py @@ -5,7 +5,9 @@ if getattr(contextlib, "nested", None): else: import sys from collections import deque - class ExitStack(object) + + class ExitStack(object): + """Context manager for dynamic management of a stack of exit callbacks For example: @@ -22,7 +24,8 @@ else: self._exit_callbacks = deque() def pop_all(self): - """Preserve the context stack by transferring it to a new instance""" + """Preserve the context stack by transferring + it to a new instance""" new_stack = type(self)() new_stack._exit_callbacks = self._exit_callbacks self._exit_callbacks = deque() From 57197cc37ab994ab814cbc1fc8b6368f64165ef7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Dec 2023 15:16:10 +0000 Subject: [PATCH 092/291] Use only the final part of folderPath in the instance name --- openpype/hosts/blender/api/plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 568d8f6695..18d2aa5362 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -226,7 +226,7 @@ class BaseCreator(Creator): # Create asset group if AYON_SERVER_ENABLED: - asset_name = instance_data["folderPath"] + asset_name = instance_data["folderPath"].split("/")[-1] else: asset_name = instance_data["asset"] @@ -311,6 +311,8 @@ class BaseCreator(Creator): or asset_name_key in changes.changed_keys ): asset_name = data[asset_name_key] + if AYON_SERVER_ENABLED: + asset_name = asset_name.split("/")[-1] name = prepare_scene_name( asset=asset_name, subset=data["subset"] ) From 73614e08a8de37018b0143edafed558c0cf8e879 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Dec 2023 16:14:10 +0000 Subject: [PATCH 093/291] Add warning if name is too long for Blender --- openpype/hosts/blender/api/plugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 18d2aa5362..d50bfad53d 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -36,6 +36,12 @@ def prepare_scene_name( if namespace: name = f"{name}_{namespace}" name = f"{name}_{subset}" + + # Blender name for a collection or object cannot be longer than 63 + # characters. If the name is longer, it will raise an error. + if len(name) > 63: + raise ValueError(f"Asset name '{name}' is too long.") + return name From 008f78e6a095f9c3c4af3b3f9fac7150705f1670 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 19 Dec 2023 22:04:30 +0800 Subject: [PATCH 094/291] implement the exitstack inside the capture --- .../maya/plugins/publish/extract_playblast.py | 24 +++++++++++-------- .../maya/plugins/publish/extract_thumbnail.py | 8 +++---- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 2e11a5f26e..78d771c250 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -45,13 +45,20 @@ class ExtractPlayblast(publish.Extractor): ) ) - if preset["viewport_options"].get("reloadTextures"): - # Regenerate all UDIM tiles previews - lib.reload_all_udim_tile_previews() - # not supported by `capture` - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(log=self.log, **preset) - self.log.debug("playblast path {}".format(path)) + if preset["viewport_options"].get("textures"): + with ExitStack() as stack: + stack.enter_context(lib.material_loading_mode()) + if preset["viewport_options"].get("reloadTextures"): + # Regenerate all UDIM tiles previews + lib.reload_all_udim_tile_previews() + # not supported by `capture` + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(log=self.log, **preset) + self.log.debug("playblast path {}".format(path)) + else: + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(log=self.log, **preset) + self.log.debug("playblast path {}".format(path)) def process(self, instance): self.log.debug("Extracting capture..") @@ -205,9 +212,6 @@ class ExtractPlayblast(publish.Extractor): stack.enter_context( panel_camera(instance.data["panel"], preset["camera"]) ) - if preset["viewport_options"].get("textures"): - stack.enter_context(lib.material_loading_mode()) - self._capture(preset) # Restoring viewport options. diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index b8e7b19bc6..96c7226db3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -153,16 +153,16 @@ class ExtractThumbnail(publish.Extractor): ) ) - if preset["viewport_options"].get("reloadTextures"): - lib.reload_all_udim_tile_previews() - - preset["viewport_options"].pop("reloadTextures", None) if preset["viewport_options"].get("textures"): with lib.material_loading_mode(): + if preset["viewport_options"].get("reloadTextures"): + lib.reload_all_udim_tile_previews() + preset["viewport_options"].pop("reloadTextures", None) path = capture.capture(**preset) else: self.log.debug( "Reload Textures during playblasting is disabled.") + preset["viewport_options"].pop("reloadTextures", None) path = capture.capture(**preset) playblast = self._fix_playblast_output_path(path) From f9603bb0a5514e5fb28c60d56a50c26a1409e8ec Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 19 Dec 2023 22:44:18 +0800 Subject: [PATCH 095/291] refactor the capture function and move it to lib --- openpype/hosts/maya/api/lib.py | 28 +++++++++++++++++++ .../maya/plugins/publish/extract_playblast.py | 27 ++---------------- .../maya/plugins/publish/extract_thumbnail.py | 22 +-------------- 3 files changed, 31 insertions(+), 46 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 41290b805e..d2a2ab253b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -9,6 +9,8 @@ import re import json import logging import contextlib +import capture +from .exitstack import ExitStack from collections import OrderedDict, defaultdict from math import ceil from six import string_types @@ -183,6 +185,32 @@ def reload_all_udim_tile_previews(): cmds.ogs(regenerateUVTilePreview=texture_file) +def capture_with_preset(preset): + if os.environ.get("OPENPYPE_DEBUG") == "1": + log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) + ) + ) + + if preset["viewport_options"].get("textures"): + with ExitStack() as stack: + stack.enter_context(material_loading_mode()) + if preset["viewport_options"].get("reloadTextures"): + # Regenerate all UDIM tiles previews + reload_all_udim_tile_previews() + # not supported by `capture` + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(log=self.log, **preset) + self.log.debug("playblast path {}".format(path)) + else: + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(log=self.log, **preset) + self.log.debug("playblast path {}".format(path)) + + return path + + @contextlib.contextmanager def material_loading_mode(mode="immediate"): """Set material loading mode during context""" diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 78d771c250..0f1423d63d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -1,5 +1,4 @@ import os -import json import contextlib import clique @@ -9,6 +8,7 @@ from openpype.pipeline import publish from openpype.hosts.maya.api import lib from openpype.hosts.maya.api.exitstack import ExitStack + from maya import cmds @@ -37,29 +37,6 @@ class ExtractPlayblast(publish.Extractor): capture_preset = {} profiles = None - def _capture(self, preset): - if os.environ.get("OPENPYPE_DEBUG") == "1": - self.log.debug( - "Using preset: {}".format( - json.dumps(preset, indent=4, sort_keys=True) - ) - ) - - if preset["viewport_options"].get("textures"): - with ExitStack() as stack: - stack.enter_context(lib.material_loading_mode()) - if preset["viewport_options"].get("reloadTextures"): - # Regenerate all UDIM tiles previews - lib.reload_all_udim_tile_previews() - # not supported by `capture` - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(log=self.log, **preset) - self.log.debug("playblast path {}".format(path)) - else: - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(log=self.log, **preset) - self.log.debug("playblast path {}".format(path)) - def process(self, instance): self.log.debug("Extracting capture..") @@ -212,7 +189,7 @@ class ExtractPlayblast(publish.Extractor): stack.enter_context( panel_camera(instance.data["panel"], preset["camera"]) ) - self._capture(preset) + path = lib.capture_with_preset(preset) # Restoring viewport options. if viewport_defaults: diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 96c7226db3..897383d0cb 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -1,9 +1,6 @@ import os import glob import tempfile -import json - -import capture from openpype.pipeline import publish from openpype.hosts.maya.api import lib @@ -146,24 +143,7 @@ class ExtractThumbnail(publish.Extractor): preset.update(panel_preset) cmds.setFocus(panel) - if os.environ.get("OPENPYPE_DEBUG") == "1": - self.log.debug( - "Using preset: {}".format( - json.dumps(preset, indent=4, sort_keys=True) - ) - ) - - if preset["viewport_options"].get("textures"): - with lib.material_loading_mode(): - if preset["viewport_options"].get("reloadTextures"): - lib.reload_all_udim_tile_previews() - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(**preset) - else: - self.log.debug( - "Reload Textures during playblasting is disabled.") - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(**preset) + path = lib.capture_with_preset(preset) playblast = self._fix_playblast_output_path(path) From 2f03b61c11a6d39dd2e1e3082830d9893ffb6e75 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 20 Dec 2023 17:28:58 +0800 Subject: [PATCH 096/291] refactor the capture and playblast functions and put them into lib.py --- openpype/hosts/maya/api/lib.py | 189 +++++++++++++++++- .../maya/plugins/publish/extract_playblast.py | 158 +-------------- .../maya/plugins/publish/extract_thumbnail.py | 108 +--------- 3 files changed, 194 insertions(+), 261 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index d2a2ab253b..0dd18bb978 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -185,16 +185,46 @@ def reload_all_udim_tile_previews(): cmds.ogs(regenerateUVTilePreview=texture_file) -def capture_with_preset(preset): +@contextlib.contextmanager +def panel_camera(panel, camera): + original_camera = cmds.modelPanel(panel, query=True, camera=True) + try: + cmds.modelPanel(panel, edit=True, camera=camera) + yield + finally: + cmds.modelPanel(panel, edit=True, camera=original_camera) + + +@contextlib.contextmanager +def panel_camera(panel, camera): + original_camera = cmds.modelPanel(panel, query=True, camera=True) + try: + cmds.modelPanel(panel, edit=True, camera=camera) + yield + finally: + cmds.modelPanel(panel, edit=True, camera=original_camera) + + +def capture_with_preset(preset, instance): + """Function for playblast capturing with the preset options + + Args: + preset (dict): preset options + instance (str): instance + + Returns: + _type_: _description_ + """ if os.environ.get("OPENPYPE_DEBUG") == "1": log.debug( "Using preset: {}".format( json.dumps(preset, indent=4, sort_keys=True) ) ) - - if preset["viewport_options"].get("textures"): - with ExitStack() as stack: + with ExitStack() as stack: + stack.enter_context(maintained_time()) + stack.enter_context(panel_camera(instance.data["panel"], preset["camera"])) + if preset["viewport_options"].get("textures"): stack.enter_context(material_loading_mode()) if preset["viewport_options"].get("reloadTextures"): # Regenerate all UDIM tiles previews @@ -203,13 +233,156 @@ def capture_with_preset(preset): preset["viewport_options"].pop("reloadTextures", None) path = capture.capture(log=self.log, **preset) self.log.debug("playblast path {}".format(path)) - else: - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(log=self.log, **preset) - self.log.debug("playblast path {}".format(path)) + else: + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(log=self.log, **preset) + self.log.debug("playblast path {}".format(path)) return path +def get_presets(instance, camera, path, start, end, capture_preset): + """Function for getting all the data of preset options for + playblast capturing + + Args: + instance (str): instance + camera (str): review camera + path (str): filepath + start (int): frameStart + end (int): frameEnd + capture_preset (dict): capture preset + + Returns: + _type_: _description_ + """ + preset = load_capture_preset(data=capture_preset) + + # "isolate_view" will already have been applied at creation, so we'll + # ignore it here. + preset.pop("isolate_view") + + # Set resolution variables from capture presets + width_preset = capture_preset["Resolution"]["width"] + height_preset = capture_preset["Resolution"]["height"] + + # Set resolution variables from asset values + asset_data = instance.data["assetEntity"]["data"] + asset_width = asset_data.get("resolutionWidth") + asset_height = asset_data.get("resolutionHeight") + review_instance_width = instance.data.get("review_width") + review_instance_height = instance.data.get("review_height") + preset["camera"] = camera + + # Tests if project resolution is set, + # if it is a value other than zero, that value is + # used, if not then the asset resolution is + # used + if review_instance_width and review_instance_height: + preset["width"] = review_instance_width + preset["height"] = review_instance_height + elif width_preset and height_preset: + preset["width"] = width_preset + preset["height"] = height_preset + elif asset_width and asset_height: + preset["width"] = asset_width + preset["height"] = asset_height + preset["start_frame"] = start + preset["end_frame"] = end + + # Enforce persisting camera depth of field + camera_options = preset.setdefault("camera_options", {}) + camera_options["depthOfField"] = cmds.getAttr( + "{0}.depthOfField".format(camera)) + + preset["filename"] = path + preset["overwrite"] = True + + cmds.refresh(force=True) + + refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True)) + cmds.currentTime(refreshFrameInt - 1, edit=True) + cmds.currentTime(refreshFrameInt, edit=True) + + # Use displayLights setting from instance + key = "displayLights" + preset["viewport_options"][key] = instance.data[key] + + # Override transparency if requested. + transparency = instance.data.get("transparency", 0) + if transparency != 0: + preset["viewport2_options"]["transparencyAlgorithm"] = transparency + + # Isolate view is requested by having objects in the set besides a + # camera. If there is only 1 member it'll be the camera because we + # validate to have 1 camera only. + if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: + preset["isolate"] = instance.data["setMembers"] + + # Show/Hide image planes on request. + image_plane = instance.data.get("imagePlane", True) + if "viewport_options" in preset: + preset["viewport_options"]["imagePlane"] = image_plane + else: + preset["viewport_options"] = {"imagePlane": image_plane} + + # Disable Pan/Zoom. + pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) + preset.pop("pan_zoom", None) + preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] + + # Need to explicitly enable some viewport changes so the viewport is + # refreshed ahead of playblasting. + keys = [ + "useDefaultMaterial", + "wireframeOnShaded", + "xray", + "jointXray", + "backfaceCulling", + "textures" + ] + viewport_defaults = {} + for key in keys: + viewport_defaults[key] = cmds.modelEditor( + instance.data["panel"], query=True, **{key: True} + ) + if preset["viewport_options"][key]: + cmds.modelEditor( + instance.data["panel"], edit=True, **{key: True} + ) + + override_viewport_options = ( + capture_preset["Viewport Options"]["override_viewport_options"] + ) + + # Force viewer to False in call to capture because we have our own + # viewer opening call to allow a signal to trigger between + # playblast and viewer + preset["viewer"] = False + + # Update preset with current panel setting + # if override_viewport_options is turned off + if not override_viewport_options: + panel_preset = capture.parse_view(instance.data["panel"]) + panel_preset.pop("camera") + preset.update(panel_preset) + + path = capture_with_preset( + preset, instance) + + # Restoring viewport options. + if viewport_defaults: + cmds.modelEditor( + instance.data["panel"], edit=True, **viewport_defaults + ) + + try: + cmds.setAttr( + "{}.panZoomEnabled".format(preset["camera"]), pan_zoom) + except RuntimeError: + self.log.warning("Cannot restore Pan/Zoom settings.") + + return preset + @contextlib.contextmanager def material_loading_mode(mode="immediate"): diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 0f1423d63d..192eb2639d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -1,27 +1,13 @@ import os -import contextlib import clique -import capture from openpype.pipeline import publish from openpype.hosts.maya.api import lib -from openpype.hosts.maya.api.exitstack import ExitStack - from maya import cmds -@contextlib.contextmanager -def panel_camera(panel, camera): - original_camera = cmds.modelPanel(panel, query=True, camera=True) - try: - cmds.modelPanel(panel, edit=True, camera=camera) - yield - finally: - cmds.modelPanel(panel, edit=True, camera=original_camera) - - class ExtractPlayblast(publish.Extractor): """Extract viewport playblast. @@ -53,10 +39,6 @@ class ExtractPlayblast(publish.Extractor): end = cmds.playbackOptions(query=True, animationEndTime=True) self.log.debug("start: {}, end: {}".format(start, end)) - - # get cameras - camera = instance.data["review_camera"] - task_data = instance.data["anatomyData"].get("task", {}) capture_preset = lib.get_capture_preset( task_data.get("name"), @@ -65,143 +47,17 @@ class ExtractPlayblast(publish.Extractor): instance.context.data["project_settings"], self.log ) - - preset = lib.load_capture_preset(data=capture_preset) - - # "isolate_view" will already have been applied at creation, so we'll - # ignore it here. - preset.pop("isolate_view") - - # Set resolution variables from capture presets - width_preset = capture_preset["Resolution"]["width"] - height_preset = capture_preset["Resolution"]["height"] - - # Set resolution variables from asset values - asset_data = instance.data["assetEntity"]["data"] - asset_width = asset_data.get("resolutionWidth") - asset_height = asset_data.get("resolutionHeight") - review_instance_width = instance.data.get("review_width") - review_instance_height = instance.data.get("review_height") - preset["camera"] = camera - - # Tests if project resolution is set, - # if it is a value other than zero, that value is - # used, if not then the asset resolution is - # used - if review_instance_width and review_instance_height: - preset["width"] = review_instance_width - preset["height"] = review_instance_height - elif width_preset and height_preset: - preset["width"] = width_preset - preset["height"] = height_preset - elif asset_width and asset_height: - preset["width"] = asset_width - preset["height"] = asset_height - preset["start_frame"] = start - preset["end_frame"] = end - - # Enforce persisting camera depth of field - camera_options = preset.setdefault("camera_options", {}) - camera_options["depthOfField"] = cmds.getAttr( - "{0}.depthOfField".format(camera)) - stagingdir = self.staging_dir(instance) filename = "{0}".format(instance.name) path = os.path.join(stagingdir, filename) - self.log.debug("Outputting images to %s" % path) - - preset["filename"] = path - preset["overwrite"] = True - - cmds.refresh(force=True) - - refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True)) - cmds.currentTime(refreshFrameInt - 1, edit=True) - cmds.currentTime(refreshFrameInt, edit=True) - - # Use displayLights setting from instance - key = "displayLights" - preset["viewport_options"][key] = instance.data[key] - - # Override transparency if requested. - transparency = instance.data.get("transparency", 0) - if transparency != 0: - preset["viewport2_options"]["transparencyAlgorithm"] = transparency - - # Isolate view is requested by having objects in the set besides a - # camera. If there is only 1 member it'll be the camera because we - # validate to have 1 camera only. - if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: - preset["isolate"] = instance.data["setMembers"] - - # Show/Hide image planes on request. - image_plane = instance.data.get("imagePlane", True) - if "viewport_options" in preset: - preset["viewport_options"]["imagePlane"] = image_plane - else: - preset["viewport_options"] = {"imagePlane": image_plane} - - # Disable Pan/Zoom. - pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) - preset.pop("pan_zoom", None) - preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] - - # Need to explicitly enable some viewport changes so the viewport is - # refreshed ahead of playblasting. - keys = [ - "useDefaultMaterial", - "wireframeOnShaded", - "xray", - "jointXray", - "backfaceCulling", - "textures" - ] - viewport_defaults = {} - for key in keys: - viewport_defaults[key] = cmds.modelEditor( - instance.data["panel"], query=True, **{key: True} - ) - if preset["viewport_options"][key]: - cmds.modelEditor( - instance.data["panel"], edit=True, **{key: True} - ) - - override_viewport_options = ( - capture_preset["Viewport Options"]["override_viewport_options"] - ) - - # Force viewer to False in call to capture because we have our own - # viewer opening call to allow a signal to trigger between - # playblast and viewer - preset["viewer"] = False - - # Update preset with current panel setting - # if override_viewport_options is turned off - if not override_viewport_options: - panel_preset = capture.parse_view(instance.data["panel"]) - panel_preset.pop("camera") - preset.update(panel_preset) - - # Need to ensure Python 2 compatibility. - with ExitStack() as stack: - stack.enter_context(lib.maintained_time()) - stack.enter_context( - panel_camera(instance.data["panel"], preset["camera"]) - ) - path = lib.capture_with_preset(preset) - - # Restoring viewport options. - if viewport_defaults: - cmds.modelEditor( - instance.data["panel"], edit=True, **viewport_defaults - ) - - try: - cmds.setAttr( - "{}.panZoomEnabled".format(preset["camera"]), pan_zoom) - except RuntimeError: - self.log.warning("Cannot restore Pan/Zoom settings.") + # get cameras + camera = instance.data["review_camera"] + preset = lib.get_presets( + instance, camera, path, + start=start, end=end, + capture_preset=capture_preset) + path = lib.capture_with_preset(preset, instance) collected_files = os.listdir(stagingdir) patterns = [clique.PATTERNS["frames"]] diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 897383d0cb..09665a1a58 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -5,8 +5,6 @@ import tempfile from openpype.pipeline import publish from openpype.hosts.maya.api import lib -from maya import cmds - class ExtractThumbnail(publish.Extractor): """Extract viewport thumbnail. @@ -34,54 +32,6 @@ class ExtractThumbnail(publish.Extractor): self.log ) - preset = lib.load_capture_preset(data=capture_preset) - - # "isolate_view" will already have been applied at creation, so we'll - # ignore it here. - preset.pop("isolate_view") - - override_viewport_options = ( - capture_preset["Viewport Options"]["override_viewport_options"] - ) - - preset["camera"] = camera - preset["start_frame"] = instance.data["frameStart"] - preset["end_frame"] = instance.data["frameStart"] - preset["camera_options"] = { - "displayGateMask": False, - "displayResolution": False, - "displayFilmGate": False, - "displayFieldChart": False, - "displaySafeAction": False, - "displaySafeTitle": False, - "displayFilmPivot": False, - "displayFilmOrigin": False, - "overscan": 1.0, - "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), - } - # Set resolution variables from capture presets - width_preset = capture_preset["Resolution"]["width"] - height_preset = capture_preset["Resolution"]["height"] - # Set resolution variables from asset values - asset_data = instance.data["assetEntity"]["data"] - asset_width = asset_data.get("resolutionWidth") - asset_height = asset_data.get("resolutionHeight") - review_instance_width = instance.data.get("review_width") - review_instance_height = instance.data.get("review_height") - # Tests if project resolution is set, - # if it is a value other than zero, that value is - # used, if not then the asset resolution is - # used - if review_instance_width and review_instance_height: - preset["width"] = review_instance_width - preset["height"] = review_instance_height - elif width_preset and height_preset: - preset["width"] = width_preset - preset["height"] = height_preset - elif asset_width and asset_height: - preset["width"] = asset_width - preset["height"] = asset_height - # Create temp directory for thumbnail # - this is to avoid "override" of source file dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") @@ -93,59 +43,13 @@ class ExtractThumbnail(publish.Extractor): path = os.path.join(dst_staging, filename) self.log.debug("Outputting images to %s" % path) + preset = lib.get_presets( + instance, camera, path, + start=1, end=1, + capture_preset=capture_preset) + path = lib.capture_with_preset(preset, instance) - preset["filename"] = path - preset["overwrite"] = True - - cmds.refresh(force=True) - - refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True)) - cmds.currentTime(refreshFrameInt - 1, edit=True) - cmds.currentTime(refreshFrameInt, edit=True) - - # Use displayLights setting from instance - key = "displayLights" - preset["viewport_options"][key] = instance.data[key] - - # Override transparency if requested. - transparency = instance.data.get("transparency", 0) - if transparency != 0: - preset["viewport2_options"]["transparencyAlgorithm"] = transparency - - # Isolate view is requested by having objects in the set besides a - # camera. If there is only 1 member it'll be the camera because we - # validate to have 1 camera only. - if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: - preset["isolate"] = instance.data["setMembers"] - - # Show or Hide Image Plane - image_plane = instance.data.get("imagePlane", True) - if "viewport_options" in preset: - preset["viewport_options"]["imagePlane"] = image_plane - else: - preset["viewport_options"] = {"imagePlane": image_plane} - - # Disable Pan/Zoom. - preset.pop("pan_zoom", None) - preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] - - with lib.maintained_time(): - # Force viewer to False in call to capture because we have our own - # viewer opening call to allow a signal to trigger between - # playblast and viewer - preset["viewer"] = False - - # Update preset with current panel setting - # if override_viewport_options is turned off - panel = cmds.getPanel(withFocus=True) or "" - if not override_viewport_options and "modelPanel" in panel: - panel_preset = capture.parse_active_view() - preset.update(panel_preset) - cmds.setFocus(panel) - - path = lib.capture_with_preset(preset) - - playblast = self._fix_playblast_output_path(path) + playblast = self._fix_playblast_output_path(path) _, thumbnail = os.path.split(playblast) From 25e216b0c419c05067a3eec1cbf8473c9d8d2297 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 20 Dec 2023 17:31:31 +0800 Subject: [PATCH 097/291] hound --- openpype/hosts/maya/api/lib.py | 220 +++++++++++++++++---------------- 1 file changed, 111 insertions(+), 109 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 0dd18bb978..6d30a58506 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -223,7 +223,8 @@ def capture_with_preset(preset, instance): ) with ExitStack() as stack: stack.enter_context(maintained_time()) - stack.enter_context(panel_camera(instance.data["panel"], preset["camera"])) + stack.enter_context(panel_camera( + instance.data["panel"], preset["camera"])) if preset["viewport_options"].get("textures"): stack.enter_context(material_loading_mode()) if preset["viewport_options"].get("reloadTextures"): @@ -240,6 +241,7 @@ def capture_with_preset(preset, instance): return path + def get_presets(instance, camera, path, start, end, capture_preset): """Function for getting all the data of preset options for playblast capturing @@ -255,133 +257,133 @@ def get_presets(instance, camera, path, start, end, capture_preset): Returns: _type_: _description_ """ - preset = load_capture_preset(data=capture_preset) + preset = load_capture_preset(data=capture_preset) - # "isolate_view" will already have been applied at creation, so we'll - # ignore it here. - preset.pop("isolate_view") + # "isolate_view" will already have been applied at creation, so we'll + # ignore it here. + preset.pop("isolate_view") - # Set resolution variables from capture presets - width_preset = capture_preset["Resolution"]["width"] - height_preset = capture_preset["Resolution"]["height"] + # Set resolution variables from capture presets + width_preset = capture_preset["Resolution"]["width"] + height_preset = capture_preset["Resolution"]["height"] - # Set resolution variables from asset values - asset_data = instance.data["assetEntity"]["data"] - asset_width = asset_data.get("resolutionWidth") - asset_height = asset_data.get("resolutionHeight") - review_instance_width = instance.data.get("review_width") - review_instance_height = instance.data.get("review_height") - preset["camera"] = camera + # Set resolution variables from asset values + asset_data = instance.data["assetEntity"]["data"] + asset_width = asset_data.get("resolutionWidth") + asset_height = asset_data.get("resolutionHeight") + review_instance_width = instance.data.get("review_width") + review_instance_height = instance.data.get("review_height") + preset["camera"] = camera - # Tests if project resolution is set, - # if it is a value other than zero, that value is - # used, if not then the asset resolution is - # used - if review_instance_width and review_instance_height: - preset["width"] = review_instance_width - preset["height"] = review_instance_height - elif width_preset and height_preset: - preset["width"] = width_preset - preset["height"] = height_preset - elif asset_width and asset_height: - preset["width"] = asset_width - preset["height"] = asset_height - preset["start_frame"] = start - preset["end_frame"] = end + # Tests if project resolution is set, + # if it is a value other than zero, that value is + # used, if not then the asset resolution is + # used + if review_instance_width and review_instance_height: + preset["width"] = review_instance_width + preset["height"] = review_instance_height + elif width_preset and height_preset: + preset["width"] = width_preset + preset["height"] = height_preset + elif asset_width and asset_height: + preset["width"] = asset_width + preset["height"] = asset_height + preset["start_frame"] = start + preset["end_frame"] = end - # Enforce persisting camera depth of field - camera_options = preset.setdefault("camera_options", {}) - camera_options["depthOfField"] = cmds.getAttr( - "{0}.depthOfField".format(camera)) + # Enforce persisting camera depth of field + camera_options = preset.setdefault("camera_options", {}) + camera_options["depthOfField"] = cmds.getAttr( + "{0}.depthOfField".format(camera)) - preset["filename"] = path - preset["overwrite"] = True + preset["filename"] = path + preset["overwrite"] = True - cmds.refresh(force=True) + cmds.refresh(force=True) - refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True)) - cmds.currentTime(refreshFrameInt - 1, edit=True) - cmds.currentTime(refreshFrameInt, edit=True) + refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True)) + cmds.currentTime(refreshFrameInt - 1, edit=True) + cmds.currentTime(refreshFrameInt, edit=True) - # Use displayLights setting from instance - key = "displayLights" - preset["viewport_options"][key] = instance.data[key] + # Use displayLights setting from instance + key = "displayLights" + preset["viewport_options"][key] = instance.data[key] - # Override transparency if requested. - transparency = instance.data.get("transparency", 0) - if transparency != 0: - preset["viewport2_options"]["transparencyAlgorithm"] = transparency + # Override transparency if requested. + transparency = instance.data.get("transparency", 0) + if transparency != 0: + preset["viewport2_options"]["transparencyAlgorithm"] = transparency - # Isolate view is requested by having objects in the set besides a - # camera. If there is only 1 member it'll be the camera because we - # validate to have 1 camera only. - if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: - preset["isolate"] = instance.data["setMembers"] + # Isolate view is requested by having objects in the set besides a + # camera. If there is only 1 member it'll be the camera because we + # validate to have 1 camera only. + if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: + preset["isolate"] = instance.data["setMembers"] - # Show/Hide image planes on request. - image_plane = instance.data.get("imagePlane", True) - if "viewport_options" in preset: - preset["viewport_options"]["imagePlane"] = image_plane - else: - preset["viewport_options"] = {"imagePlane": image_plane} + # Show/Hide image planes on request. + image_plane = instance.data.get("imagePlane", True) + if "viewport_options" in preset: + preset["viewport_options"]["imagePlane"] = image_plane + else: + preset["viewport_options"] = {"imagePlane": image_plane} - # Disable Pan/Zoom. - pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) - preset.pop("pan_zoom", None) - preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] + # Disable Pan/Zoom. + pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) + preset.pop("pan_zoom", None) + preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] - # Need to explicitly enable some viewport changes so the viewport is - # refreshed ahead of playblasting. - keys = [ - "useDefaultMaterial", - "wireframeOnShaded", - "xray", - "jointXray", - "backfaceCulling", - "textures" - ] - viewport_defaults = {} - for key in keys: - viewport_defaults[key] = cmds.modelEditor( - instance.data["panel"], query=True, **{key: True} + # Need to explicitly enable some viewport changes so the viewport is + # refreshed ahead of playblasting. + keys = [ + "useDefaultMaterial", + "wireframeOnShaded", + "xray", + "jointXray", + "backfaceCulling", + "textures" + ] + viewport_defaults = {} + for key in keys: + viewport_defaults[key] = cmds.modelEditor( + instance.data["panel"], query=True, **{key: True} + ) + if preset["viewport_options"][key]: + cmds.modelEditor( + instance.data["panel"], edit=True, **{key: True} ) - if preset["viewport_options"][key]: - cmds.modelEditor( - instance.data["panel"], edit=True, **{key: True} - ) - override_viewport_options = ( - capture_preset["Viewport Options"]["override_viewport_options"] + override_viewport_options = ( + capture_preset["Viewport Options"]["override_viewport_options"] + ) + + # Force viewer to False in call to capture because we have our own + # viewer opening call to allow a signal to trigger between + # playblast and viewer + preset["viewer"] = False + + # Update preset with current panel setting + # if override_viewport_options is turned off + if not override_viewport_options: + panel_preset = capture.parse_view(instance.data["panel"]) + panel_preset.pop("camera") + preset.update(panel_preset) + + path = capture_with_preset( + preset, instance) + + # Restoring viewport options. + if viewport_defaults: + cmds.modelEditor( + instance.data["panel"], edit=True, **viewport_defaults ) - # Force viewer to False in call to capture because we have our own - # viewer opening call to allow a signal to trigger between - # playblast and viewer - preset["viewer"] = False + try: + cmds.setAttr( + "{}.panZoomEnabled".format(preset["camera"]), pan_zoom) + except RuntimeError: + self.log.warning("Cannot restore Pan/Zoom settings.") - # Update preset with current panel setting - # if override_viewport_options is turned off - if not override_viewport_options: - panel_preset = capture.parse_view(instance.data["panel"]) - panel_preset.pop("camera") - preset.update(panel_preset) - - path = capture_with_preset( - preset, instance) - - # Restoring viewport options. - if viewport_defaults: - cmds.modelEditor( - instance.data["panel"], edit=True, **viewport_defaults - ) - - try: - cmds.setAttr( - "{}.panZoomEnabled".format(preset["camera"]), pan_zoom) - except RuntimeError: - self.log.warning("Cannot restore Pan/Zoom settings.") - - return preset + return preset @contextlib.contextmanager From 29876a496edce570cccf8e0a5e7c4d36cf210de5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 20 Dec 2023 17:32:27 +0800 Subject: [PATCH 098/291] hound --- openpype/hosts/maya/api/lib.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 6d30a58506..8749ac0d6a 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -195,16 +195,6 @@ def panel_camera(panel, camera): cmds.modelPanel(panel, edit=True, camera=original_camera) -@contextlib.contextmanager -def panel_camera(panel, camera): - original_camera = cmds.modelPanel(panel, query=True, camera=True) - try: - cmds.modelPanel(panel, edit=True, camera=camera) - yield - finally: - cmds.modelPanel(panel, edit=True, camera=original_camera) - - def capture_with_preset(preset, instance): """Function for playblast capturing with the preset options From af87fdf657adb7ca5dd6f34bdd41335dc2b9efa7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 20 Dec 2023 21:31:19 +0800 Subject: [PATCH 099/291] the resourceDir for texture also supports without image search path --- .../maya/plugins/publish/collect_yeti_rig.py | 29 ++++++++----------- .../maya/plugins/publish/extract_yeti_rig.py | 16 ++++++---- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py index 835934e1bf..d67c51b895 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py @@ -124,19 +124,19 @@ class CollectYetiRig(pyblish.api.InstancePlugin): image_search_paths = [p for p in image_search_paths.split(os.path.pathsep) if p] - # find all ${TOKEN} tokens and replace them with $TOKEN env. variable - image_search_paths = self._replace_tokens(image_search_paths) + # find all ${TOKEN} tokens and replace them with $TOKEN env. variable + image_search_paths = self._replace_tokens(image_search_paths) - # List all related textures - texture_nodes = cmds.pgYetiGraph( - node, listNodes=True, type="texture") - texture_filenames = [ - cmds.pgYetiGraph( - node, node=texture_node, - param="file_name", getParamValue=True) - for texture_node in texture_nodes - ] - self.log.debug("Found %i texture(s)" % len(texture_filenames)) + # List all related textures + texture_nodes = cmds.pgYetiGraph( + node, listNodes=True, type="texture") + texture_filenames = [ + cmds.pgYetiGraph( + node, node=texture_node, + param="file_name", getParamValue=True) + for texture_node in texture_nodes + ] + self.log.debug("Found %i texture(s)" % len(texture_filenames)) # Get all reference nodes reference_nodes = cmds.pgYetiGraph(node, @@ -144,11 +144,6 @@ class CollectYetiRig(pyblish.api.InstancePlugin): type="reference") self.log.debug("Found %i reference node(s)" % len(reference_nodes)) - if texture_filenames and not image_search_paths: - raise ValueError("pgYetiMaya node '%s' is missing the path to the " - "files in the 'imageSearchPath " - "atttribute'" % node) - # Collect all texture files # find all ${TOKEN} tokens and replace them with $TOKEN env. variable texture_filenames = self._replace_tokens(texture_filenames) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index da67cb911f..a76d15d43e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -142,12 +142,18 @@ class ExtractYetiRig(publish.Extractor): instance.data['transfers'] = [] for resource in instance.data.get('resources', []): - for file in resource['files']: - src = file - dst = os.path.join(image_search_path, os.path.basename(file)) - instance.data['transfers'].append([src, dst]) + if resource["files"]: + for file in resource['files']: + src = file + dst = os.path.join(image_search_path, os.path.basename(file)) + instance.data['transfers'].append([src, dst]) + else: + for file in resource['source']: + src = file if os.path.isabs(file) else os.path.abspath(file) + dst = os.path.join(image_search_path, os.path.basename(file)) + instance.data['transfers'].append([src, dst]) - self.log.debug("adding transfer {} -> {}". format(src, dst)) + self.log.debug("adding transfer {} -> {}". format(src, dst)) # Ensure the imageSearchPath is being remapped to the publish folder attr_value = {"%s.imageSearchPath" % n: str(image_search_path) for From da93e19ca9f2832eb30774b7db6b82aff7f0dfc7 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 20 Dec 2023 21:32:56 +0800 Subject: [PATCH 100/291] hound --- .../hosts/maya/plugins/publish/extract_yeti_rig.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index a76d15d43e..c0c286dcc0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -145,12 +145,16 @@ class ExtractYetiRig(publish.Extractor): if resource["files"]: for file in resource['files']: src = file - dst = os.path.join(image_search_path, os.path.basename(file)) + dst = os.path.join( + image_search_path, os.path.basename(file)) instance.data['transfers'].append([src, dst]) else: for file in resource['source']: - src = file if os.path.isabs(file) else os.path.abspath(file) - dst = os.path.join(image_search_path, os.path.basename(file)) + src = ( + file if os.path.isabs(file) else os.path.abspath(file) + ) + dst = os.path.join( + image_search_path, os.path.basename(file)) instance.data['transfers'].append([src, dst]) self.log.debug("adding transfer {} -> {}". format(src, dst)) From 0ae3ef03d22fbf13ae1dd7f4eee8d5c7cedac11f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 20 Dec 2023 23:41:39 +0800 Subject: [PATCH 101/291] refactor the capture and capture preset function --- openpype/hosts/maya/api/lib.py | 108 +++++++++--------- .../maya/plugins/publish/extract_playblast.py | 4 +- .../maya/plugins/publish/extract_thumbnail.py | 20 +++- 3 files changed, 73 insertions(+), 59 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 8749ac0d6a..c1d1a43d1e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -195,7 +195,7 @@ def panel_camera(panel, camera): cmds.modelPanel(panel, edit=True, camera=original_camera) -def capture_with_preset(preset, instance): +def playblast_capture(preset, instance): """Function for playblast capturing with the preset options Args: @@ -215,24 +215,22 @@ def capture_with_preset(preset, instance): stack.enter_context(maintained_time()) stack.enter_context(panel_camera( instance.data["panel"], preset["camera"])) + stack.enter_context(viewport_default_options(preset, instance)) if preset["viewport_options"].get("textures"): - stack.enter_context(material_loading_mode()) + material_loading_mode() if preset["viewport_options"].get("reloadTextures"): # Regenerate all UDIM tiles previews reload_all_udim_tile_previews() - # not supported by `capture` - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(log=self.log, **preset) - self.log.debug("playblast path {}".format(path)) - else: - preset["viewport_options"].pop("reloadTextures", None) - path = capture.capture(log=self.log, **preset) - self.log.debug("playblast path {}".format(path)) + # not supported by `capture` + preset["viewport_options"].pop("reloadTextures", None) + path = capture.capture(log=self.log, **preset) + self.log.debug("playblast path {}".format(path)) return path -def get_presets(instance, camera, path, start, end, capture_preset): +def generate_capture_preset(instance, camera, path, + start=None, end=None, capture_preset={}): """Function for getting all the data of preset options for playblast capturing @@ -317,35 +315,6 @@ def get_presets(instance, camera, path, start, end, capture_preset): else: preset["viewport_options"] = {"imagePlane": image_plane} - # Disable Pan/Zoom. - pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) - preset.pop("pan_zoom", None) - preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] - - # Need to explicitly enable some viewport changes so the viewport is - # refreshed ahead of playblasting. - keys = [ - "useDefaultMaterial", - "wireframeOnShaded", - "xray", - "jointXray", - "backfaceCulling", - "textures" - ] - viewport_defaults = {} - for key in keys: - viewport_defaults[key] = cmds.modelEditor( - instance.data["panel"], query=True, **{key: True} - ) - if preset["viewport_options"][key]: - cmds.modelEditor( - instance.data["panel"], edit=True, **{key: True} - ) - - override_viewport_options = ( - capture_preset["Viewport Options"]["override_viewport_options"] - ) - # Force viewer to False in call to capture because we have our own # viewer opening call to allow a signal to trigger between # playblast and viewer @@ -353,29 +322,58 @@ def get_presets(instance, camera, path, start, end, capture_preset): # Update preset with current panel setting # if override_viewport_options is turned off + override_viewport_options = ( + capture_preset["Viewport Options"]["override_viewport_options"] + ) if not override_viewport_options: panel_preset = capture.parse_view(instance.data["panel"]) panel_preset.pop("camera") preset.update(panel_preset) - path = capture_with_preset( - preset, instance) - - # Restoring viewport options. - if viewport_defaults: - cmds.modelEditor( - instance.data["panel"], edit=True, **viewport_defaults - ) - - try: - cmds.setAttr( - "{}.panZoomEnabled".format(preset["camera"]), pan_zoom) - except RuntimeError: - self.log.warning("Cannot restore Pan/Zoom settings.") - return preset +@contextlib.contextmanager +def viewport_default_options(preset, instance): + # Disable Pan/Zoom. + pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) + preset.pop("pan_zoom", None) + preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] + + viewport_defaults = {} + # Need to explicitly enable some viewport changes so the viewport is + # refreshed ahead of playblasting. + try: + keys = [ + "useDefaultMaterial", + "wireframeOnShaded", + "xray", + "jointXray", + "backfaceCulling", + "textures" + ] + for key in keys: + viewport_defaults[key] = cmds.modelEditor( + instance.data["panel"], query=True, **{key: True} + ) + if preset["viewport_options"][key]: + cmds.modelEditor( + instance.data["panel"], edit=True, **{key: True} + ) + yield + finally: + # Restoring viewport options. + if viewport_defaults: + cmds.modelEditor( + instance.data["panel"], edit=True, **viewport_defaults + ) + try: + cmds.setAttr( + "{}.panZoomEnabled".format(preset["camera"]), pan_zoom) + except RuntimeError: + self.log.warning("Cannot restore Pan/Zoom settings.") + + @contextlib.contextmanager def material_loading_mode(mode="immediate"): """Set material loading mode during context""" diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 192eb2639d..5a2beaca12 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -53,11 +53,11 @@ class ExtractPlayblast(publish.Extractor): self.log.debug("Outputting images to %s" % path) # get cameras camera = instance.data["review_camera"] - preset = lib.get_presets( + preset = lib.generate_capture_preset( instance, camera, path, start=start, end=end, capture_preset=capture_preset) - path = lib.capture_with_preset(preset, instance) + path = lib.playblast_capture(preset, instance) collected_files = os.listdir(stagingdir) patterns = [clique.PATTERNS["frames"]] diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 09665a1a58..b4931b637f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -4,6 +4,7 @@ import tempfile from openpype.pipeline import publish from openpype.hosts.maya.api import lib +from maya.cmds import cmds class ExtractThumbnail(publish.Extractor): @@ -43,11 +44,26 @@ class ExtractThumbnail(publish.Extractor): path = os.path.join(dst_staging, filename) self.log.debug("Outputting images to %s" % path) - preset = lib.get_presets( + + preset = lib.generate_capture_preset( instance, camera, path, start=1, end=1, capture_preset=capture_preset) - path = lib.capture_with_preset(preset, instance) + + preset["camera_options"].update({ + "displayGateMask": False, + "displayResolution": False, + "displayFilmGate": False, + "displayFieldChart": False, + "displaySafeAction": False, + "displaySafeTitle": False, + "displayFilmPivot": False, + "displayFilmOrigin": False, + "overscan": 1.0, + "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), + } + ) + path = lib.playblast_capture(preset, instance) playblast = self._fix_playblast_output_path(path) From 04f9d4caaa40410995c84ef89821d9a7b525a70f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 20 Dec 2023 23:44:40 +0800 Subject: [PATCH 102/291] hound --- openpype/hosts/maya/api/lib.py | 2 +- .../maya/plugins/publish/extract_thumbnail.py | 21 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index c1d1a43d1e..bc66ec350f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -230,7 +230,7 @@ def playblast_capture(preset, instance): def generate_capture_preset(instance, camera, path, - start=None, end=None, capture_preset={}): + start=None, end=None, capture_preset=None): """Function for getting all the data of preset options for playblast capturing diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index b4931b637f..05fad0025d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -51,16 +51,17 @@ class ExtractThumbnail(publish.Extractor): capture_preset=capture_preset) preset["camera_options"].update({ - "displayGateMask": False, - "displayResolution": False, - "displayFilmGate": False, - "displayFieldChart": False, - "displaySafeAction": False, - "displaySafeTitle": False, - "displayFilmPivot": False, - "displayFilmOrigin": False, - "overscan": 1.0, - "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), + "displayGateMask": False, + "displayResolution": False, + "displayFilmGate": False, + "displayFieldChart": False, + "displaySafeAction": False, + "displaySafeTitle": False, + "displayFilmPivot": False, + "displayFilmOrigin": False, + "overscan": 1.0, + "depthOfField": cmds.getAttr( + "{0}.depthOfField".format(camera)) } ) path = lib.playblast_capture(preset, instance) From 746e34aa559126b7eea2901839366fe6fea06b3d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 20 Dec 2023 23:46:10 +0800 Subject: [PATCH 103/291] hound --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 05fad0025d..6f61515019 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -60,8 +60,7 @@ class ExtractThumbnail(publish.Extractor): "displayFilmPivot": False, "displayFilmOrigin": False, "overscan": 1.0, - "depthOfField": cmds.getAttr( - "{0}.depthOfField".format(camera)) + "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), # noqa } ) path = lib.playblast_capture(preset, instance) From 8ce8d72c0341d1003b6632395fe760fe7098a72e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 20 Dec 2023 23:56:40 +0800 Subject: [PATCH 104/291] add reloadTextures argument back to cmds.ogs --- openpype/hosts/maya/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index bc66ec350f..4db7269d9b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -183,6 +183,7 @@ def reload_all_udim_tile_previews(): for texture_file in texture_files: if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0: cmds.ogs(regenerateUVTilePreview=texture_file) + cmds.ogs(reloadTextures=True) @contextlib.contextmanager From 4b6e5e29dcb2a3ec21ed5437690e5b11a3bfcb31 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 20 Dec 2023 23:59:00 +0800 Subject: [PATCH 105/291] add material_loading_mode into enter_context --- openpype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 4db7269d9b..9892fd0255 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -218,7 +218,7 @@ def playblast_capture(preset, instance): instance.data["panel"], preset["camera"])) stack.enter_context(viewport_default_options(preset, instance)) if preset["viewport_options"].get("textures"): - material_loading_mode() + stack.enter_context(material_loading_mode()) if preset["viewport_options"].get("reloadTextures"): # Regenerate all UDIM tiles previews reload_all_udim_tile_previews() From bf0ad7225ff778dec1eed822f513f04b674756bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 20 Dec 2023 18:38:59 +0100 Subject: [PATCH 106/291] :recycle: remove muster related code --- .../plugins/publish/submit_publish_job.py | 46 ++++++------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index c9019b496b..228aa3ec81 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -89,7 +89,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, """ - label = "Submit image sequence jobs to Deadline or Muster" + label = "Submit Image Publishing job to Deadline" order = pyblish.api.IntegratorOrder + 0.2 icon = "tractor" @@ -582,16 +582,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, ''' - render_job = None - submission_type = "" - if instance.data.get("toBeRenderedOn") == "deadline": - render_job = instance.data.pop("deadlineSubmissionJob", None) - submission_type = "deadline" - - if instance.data.get("toBeRenderedOn") == "muster": - render_job = instance.data.pop("musterSubmissionJob", None) - submission_type = "muster" - + render_job = instance.data.pop("deadlineSubmissionJob", None) if not render_job and instance.data.get("tileRendering") is False: raise AssertionError(("Cannot continue without valid Deadline " "or Muster submission.")) @@ -624,21 +615,19 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "FTRACK_SERVER": os.environ.get("FTRACK_SERVER"), } - deadline_publish_job_id = None - if submission_type == "deadline": - # get default deadline webservice url from deadline module - self.deadline_url = instance.context.data["defaultDeadline"] - # if custom one is set in instance, use that - if instance.data.get("deadlineUrl"): - self.deadline_url = instance.data.get("deadlineUrl") - assert self.deadline_url, "Requires Deadline Webservice URL" + # get default deadline webservice url from deadline module + self.deadline_url = instance.context.data["defaultDeadline"] + # if custom one is set in instance, use that + if instance.data.get("deadlineUrl"): + self.deadline_url = instance.data.get("deadlineUrl") + assert self.deadline_url, "Requires Deadline Webservice URL" - deadline_publish_job_id = \ - self._submit_deadline_post_job(instance, render_job, instances) + 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 + # Inject deadline url to instances. + for inst in instances: + inst["deadlineUrl"] = self.deadline_url # publish job file publish_job = { @@ -664,15 +653,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, if audio_file and os.path.isfile(audio_file): publish_job.update({"audio": audio_file}) - # pass Ftrack credentials in case of Muster - if submission_type == "muster": - ftrack = { - "FTRACK_API_USER": os.environ.get("FTRACK_API_USER"), - "FTRACK_API_KEY": os.environ.get("FTRACK_API_KEY"), - "FTRACK_SERVER": os.environ.get("FTRACK_SERVER"), - } - publish_job.update({"ftrack": ftrack}) - metadata_path, rootless_metadata_path = \ create_metadata_path(instance, anatomy) From 47af46c29f17303f540530dd1869460fca0445a1 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Wed, 20 Dec 2023 22:08:09 +0200 Subject: [PATCH 107/291] =?UTF-8?q?=E2=9C=8F=EF=B8=8F=20=20Kuba's=20commen?= =?UTF-8?q?t=20-=20follow=20pyblish=20key=20naming=20convention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py | 2 +- openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py | 2 +- openpype/hosts/houdini/plugins/publish/collect_vray_rop.py | 2 +- openpype/modules/deadline/abstract_submit_deadline.py | 2 +- .../deadline/plugins/publish/submit_houdini_render_deadline.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py index 45d950106e..ffc2a526a3 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_arnold_rop.py @@ -42,7 +42,7 @@ class CollectArnoldROPRenderProducts(pyblish.api.InstancePlugin): # Store whether we are splitting the render job (export + render) split_render = bool(rop.parm("ar_ass_export_enable").eval()) - instance.data["split_render"] = split_render + instance.data["splitRender"] = split_render export_prefix = None export_products = [] if split_render: diff --git a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py index a28b425057..64ef20f4e7 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_mantra_rop.py @@ -46,7 +46,7 @@ class CollectMantraROPRenderProducts(pyblish.api.InstancePlugin): # Store whether we are splitting the render job (export + render) split_render = bool(rop.parm("soho_outputmode").eval()) - instance.data["split_render"] = split_render + instance.data["splitRender"] = split_render export_prefix = None export_products = [] if split_render: diff --git a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py index 6e8fe1cc79..ad4fdb0da5 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_vray_rop.py @@ -47,7 +47,7 @@ class CollectVrayROPRenderProducts(pyblish.api.InstancePlugin): # Store whether we are splitting the render job in an export + render split_render = rop.parm("render_export_mode").eval() == "2" - instance.data["split_render"] = split_render + instance.data["splitRender"] = split_render export_prefix = None export_products = [] if split_render: diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 45aba560ba..002dfa5992 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -464,7 +464,7 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin, self.log.info("Submitted job to Deadline: {}.".format(job_id)) # TODO: Find a way that's more generic and not render type specific - if instance.data.get("split_render"): + if instance.data.get("splitRender"): self.log.info("Splitting export and render in two jobs") self.log.info("Export job id: %s", job_id) render_job_info = self.get_job_info(dependency_job_ids=[job_id]) 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 b3e29c7e2d..c8960185b2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -124,7 +124,7 @@ class HoudiniSubmitDeadline( # Whether Deadline render submission is being split in two # (extract + render) - split_render_job = instance.data.get("split_render") + split_render_job = instance.data.get("splitRender") # If there's some dependency job ids we can assume this is a render job # and not an export job From 0dd4d7b5059d8e1103a6d9a5edda8c04a578e5bb Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Dec 2023 21:46:23 +0100 Subject: [PATCH 108/291] Code cleanup --- openpype/hosts/maya/api/lib.py | 145 +++++++++++++++++---------------- 1 file changed, 76 insertions(+), 69 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 9892fd0255..5c15bfba26 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -175,9 +175,7 @@ def maintained_selection(): def reload_all_udim_tile_previews(): - """Regenerate all UDIM tile preview in texture file - nodes during context - """ + """Regenerate all UDIM tile preview in texture file""" texture_files = cmds.ls(type="file") if texture_files: for texture_file in texture_files: @@ -188,6 +186,13 @@ def reload_all_udim_tile_previews(): @contextlib.contextmanager def panel_camera(panel, camera): + """Set modelPanel's camera during the context. + + Arguments: + panel (str): modelPanel name. + camera (str): camera name. + + """ original_camera = cmds.modelPanel(panel, query=True, camera=True) try: cmds.modelPanel(panel, edit=True, camera=camera) @@ -196,36 +201,48 @@ def panel_camera(panel, camera): cmds.modelPanel(panel, edit=True, camera=original_camera) -def playblast_capture(preset, instance): - """Function for playblast capturing with the preset options +def render_capture_preset(preset): + """Capture playblast with a preset. + + To generate the preset use `generate_capture_preset`. Args: preset (dict): preset options - instance (str): instance Returns: - _type_: _description_ + str: Output path of `capture.capture` """ + + # Force a refresh at the start of the timeline + # TODO (Question): Why do we need to do this? What bug does it solve? + # Is this for simulations? + cmds.refresh(force=True) + refresh_frame_int = int(cmds.playbackOptions(query=True, minTime=True)) + cmds.currentTime(refresh_frame_int - 1, edit=True) + cmds.currentTime(refresh_frame_int, edit=True) + if os.environ.get("OPENPYPE_DEBUG") == "1": log.debug( "Using preset: {}".format( json.dumps(preset, indent=4, sort_keys=True) ) ) + + # not supported by `capture` so we pop it off of the preset + reload_textures = preset["viewport_options"].pop("reloadTextures", True) + with ExitStack() as stack: stack.enter_context(maintained_time()) - stack.enter_context(panel_camera( - instance.data["panel"], preset["camera"])) - stack.enter_context(viewport_default_options(preset, instance)) + stack.enter_context(panel_camera(preset["panel"], preset["camera"])) + stack.enter_context(viewport_default_options(preset)) if preset["viewport_options"].get("textures"): - stack.enter_context(material_loading_mode()) - if preset["viewport_options"].get("reloadTextures"): + # Force immediate texture loading when to ensure + # all textures have loaded before the playblast starts + stack.enter_context(material_loading_mode("immediate")) + if reload_textures: # Regenerate all UDIM tiles previews reload_all_udim_tile_previews() - # not supported by `capture` - preset["viewport_options"].pop("reloadTextures", None) path = capture.capture(log=self.log, **preset) - self.log.debug("playblast path {}".format(path)) return path @@ -236,7 +253,7 @@ def generate_capture_preset(instance, camera, path, playblast capturing Args: - instance (str): instance + instance (pyblish.api.Instance): instance camera (str): review camera path (str): filepath start (int): frameStart @@ -244,10 +261,21 @@ def generate_capture_preset(instance, camera, path, capture_preset (dict): capture preset Returns: - _type_: _description_ + dict: Resulting preset """ preset = load_capture_preset(data=capture_preset) + preset["camera"] = camera + preset["start_frame"] = start + preset["end_frame"] = end + preset["filename"] = path + preset["overwrite"] = True + preset["panel"] = instance.data["panel"] + + # Disable viewer since we use the rendering logic for publishing + # We don't want to open the generated playblast in a viewer directly. + preset["viewer"] = False + # "isolate_view" will already have been applied at creation, so we'll # ignore it here. preset.pop("isolate_view") @@ -262,7 +290,6 @@ def generate_capture_preset(instance, camera, path, asset_height = asset_data.get("resolutionHeight") review_instance_width = instance.data.get("review_width") review_instance_height = instance.data.get("review_height") - preset["camera"] = camera # Tests if project resolution is set, # if it is a value other than zero, that value is @@ -277,31 +304,6 @@ def generate_capture_preset(instance, camera, path, elif asset_width and asset_height: preset["width"] = asset_width preset["height"] = asset_height - preset["start_frame"] = start - preset["end_frame"] = end - - # Enforce persisting camera depth of field - camera_options = preset.setdefault("camera_options", {}) - camera_options["depthOfField"] = cmds.getAttr( - "{0}.depthOfField".format(camera)) - - preset["filename"] = path - preset["overwrite"] = True - - cmds.refresh(force=True) - - refreshFrameInt = int(cmds.playbackOptions(q=True, minTime=True)) - cmds.currentTime(refreshFrameInt - 1, edit=True) - cmds.currentTime(refreshFrameInt, edit=True) - - # Use displayLights setting from instance - key = "displayLights" - preset["viewport_options"][key] = instance.data[key] - - # Override transparency if requested. - transparency = instance.data.get("transparency", 0) - if transparency != 0: - preset["viewport2_options"]["transparencyAlgorithm"] = transparency # Isolate view is requested by having objects in the set besides a # camera. If there is only 1 member it'll be the camera because we @@ -309,17 +311,26 @@ def generate_capture_preset(instance, camera, path, if instance.data["isolate"] and len(instance.data["setMembers"]) > 1: preset["isolate"] = instance.data["setMembers"] - # Show/Hide image planes on request. - image_plane = instance.data.get("imagePlane", True) - if "viewport_options" in preset: - preset["viewport_options"]["imagePlane"] = image_plane - else: - preset["viewport_options"] = {"imagePlane": image_plane} + # Override camera options + # Enforce persisting camera depth of field + camera_options = preset.setdefault("camera_options", {}) + camera_options["depthOfField"] = cmds.getAttr( + "{0}.depthOfField".format(camera) + ) - # Force viewer to False in call to capture because we have our own - # viewer opening call to allow a signal to trigger between - # playblast and viewer - preset["viewer"] = False + # Use Pan/Zoom from instance data instead of from preset + preset.pop("pan_zoom", None) + preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] + + # Override viewport options by instance data + viewport_options = preset.setdefault("viewport_options", {}) + viewport_options["displayLights"] = instance.data["displayLights"] + viewport_options["imagePlane"] = instance.data.get("imagePlane", True) + + # Override transparency if requested. + transparency = instance.data.get("transparency", 0) + if transparency != 0: + preset["viewport2_options"]["transparencyAlgorithm"] = transparency # Update preset with current panel setting # if override_viewport_options is turned off @@ -335,15 +346,16 @@ def generate_capture_preset(instance, camera, path, @contextlib.contextmanager -def viewport_default_options(preset, instance): - # Disable Pan/Zoom. - pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) - preset.pop("pan_zoom", None) - preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] +def viewport_default_options(preset): + """Context manager used by `render_capture_preset`. + We need to explicitly enable some viewport changes so the viewport is + refreshed ahead of playblasting. + + """ + # TODO: Clarify in the docstring WHY we need to set it ahead of + # playblasting. What issues does it solve? viewport_defaults = {} - # Need to explicitly enable some viewport changes so the viewport is - # refreshed ahead of playblasting. try: keys = [ "useDefaultMaterial", @@ -355,24 +367,19 @@ def viewport_default_options(preset, instance): ] for key in keys: viewport_defaults[key] = cmds.modelEditor( - instance.data["panel"], query=True, **{key: True} + preset["panel"], query=True, **{key: True} ) if preset["viewport_options"][key]: cmds.modelEditor( - instance.data["panel"], edit=True, **{key: True} + preset["panel"], edit=True, **{key: True} ) yield finally: # Restoring viewport options. if viewport_defaults: cmds.modelEditor( - instance.data["panel"], edit=True, **viewport_defaults + preset["panel"], edit=True, **viewport_defaults ) - try: - cmds.setAttr( - "{}.panZoomEnabled".format(preset["camera"]), pan_zoom) - except RuntimeError: - self.log.warning("Cannot restore Pan/Zoom settings.") @contextlib.contextmanager @@ -2891,7 +2898,7 @@ def bake_to_world_space(nodes, return world_space_nodes -def load_capture_preset(data=None): +def load_capture_preset(data): """Convert OpenPype Extract Playblast settings to `capture` arguments Input data is the settings from: From 5a7079c2e4936a393237d3baed592546d09ce6ff Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Dec 2023 21:49:48 +0100 Subject: [PATCH 109/291] Fix calls to refactored function name --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 5a2beaca12..4ec4f733fd 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -57,7 +57,7 @@ class ExtractPlayblast(publish.Extractor): instance, camera, path, start=start, end=end, capture_preset=capture_preset) - path = lib.playblast_capture(preset, instance) + path = lib.render_capture_preset(preset) collected_files = os.listdir(stagingdir) patterns = [clique.PATTERNS["frames"]] diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 6f61515019..d85c00a7da 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -63,7 +63,7 @@ class ExtractThumbnail(publish.Extractor): "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), # noqa } ) - path = lib.playblast_capture(preset, instance) + path = lib.render_capture_preset(preset) playblast = self._fix_playblast_output_path(path) From 9f543f89292d9c30bc05dd20f44f6d076c364835 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Dec 2023 21:51:08 +0100 Subject: [PATCH 110/291] Remove `capture` import that's already imported at top --- openpype/hosts/maya/api/lib.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 5c15bfba26..cf4a6b6b6a 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2912,8 +2912,6 @@ def load_capture_preset(data): """ - import capture - options = dict() viewport_options = dict() viewport2_options = dict() From 82e5e6bcdea325315ff7efcc85314c122b588339 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Dec 2023 23:16:00 +0100 Subject: [PATCH 111/291] Clarify log messages --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 4ec4f733fd..377b609603 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -24,7 +24,7 @@ class ExtractPlayblast(publish.Extractor): profiles = None def process(self, instance): - self.log.debug("Extracting capture..") + self.log.debug("Extracting playblast..") # get scene fps fps = instance.data.get("fps") or instance.context.data.get("fps") diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index d85c00a7da..d15877d603 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -20,7 +20,7 @@ class ExtractThumbnail(publish.Extractor): families = ["review"] def process(self, instance): - self.log.debug("Extracting capture..") + self.log.debug("Extracting thumbnail..") camera = instance.data["review_camera"] From 208e3f16540e5834b9549b82d858eae49acb5c77 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Dec 2023 23:18:19 +0100 Subject: [PATCH 112/291] Depth of field is already preserved from camera by `generate_capture_preset` --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index d15877d603..9bece030a4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -60,7 +60,6 @@ class ExtractThumbnail(publish.Extractor): "displayFilmPivot": False, "displayFilmOrigin": False, "overscan": 1.0, - "depthOfField": cmds.getAttr("{0}.depthOfField".format(camera)), # noqa } ) path = lib.render_capture_preset(preset) From 4796bca514e03e0152534648ed10958f00f5c09d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Dec 2023 23:19:58 +0100 Subject: [PATCH 113/291] Cosmetics, + remove unused import --- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 9bece030a4..0d332d73ea 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -4,7 +4,6 @@ import tempfile from openpype.pipeline import publish from openpype.hosts.maya.api import lib -from maya.cmds import cmds class ExtractThumbnail(publish.Extractor): @@ -60,8 +59,7 @@ class ExtractThumbnail(publish.Extractor): "displayFilmPivot": False, "displayFilmOrigin": False, "overscan": 1.0, - } - ) + }) path = lib.render_capture_preset(preset) playblast = self._fix_playblast_output_path(path) From d880cdd1bb43213fb0c73991ec29eaaa6d433a39 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Dec 2023 23:22:42 +0100 Subject: [PATCH 114/291] Cosmetics - avoid confusion about what `preset.get("filename")` actually is, it's the path passed to the generated preset. Remove unused `path` return value from `lib.render_capture_preset` Match representations logic with other extractors defining the list closer to creation of the representation, match more with ExtractThumbnail --- .../maya/plugins/publish/extract_playblast.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 377b609603..c41cf67fb4 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -57,28 +57,25 @@ class ExtractPlayblast(publish.Extractor): instance, camera, path, start=start, end=end, capture_preset=capture_preset) - path = lib.render_capture_preset(preset) + lib.render_capture_preset(preset) + # Find playblast sequence collected_files = os.listdir(stagingdir) patterns = [clique.PATTERNS["frames"]] collections, remainder = clique.assemble(collected_files, minimum_items=1, patterns=patterns) - filename = preset.get("filename", "%TEMP%") - self.log.debug("filename {}".format(filename)) + self.log.debug("Searching playblast collection for: %s", path) frame_collection = None for collection in collections: filebase = collection.format("{head}").rstrip(".") - self.log.debug("collection head {}".format(filebase)) - if filebase in filename: + self.log.debug("Checking collection head: %s", filebase) + if filebase in path: frame_collection = collection self.log.debug( - "we found collection of interest {}".format( - str(frame_collection))) - - if "representations" not in instance.data: - instance.data["representations"] = [] + "Found playblast collection: %s", frame_collection + ) tags = ["review"] if not instance.data.get("keepImages"): @@ -92,6 +89,9 @@ class ExtractPlayblast(publish.Extractor): if len(collected_files) == 1: collected_files = collected_files[0] + if "representations" not in instance.data: + instance.data["representations"] = [] + representation = { "name": capture_preset["Codec"]["compression"], "ext": capture_preset["Codec"]["compression"], From fa032d5f5519c8ffd6ca7a111447c6fc0f953ffa Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 20 Dec 2023 23:27:20 +0100 Subject: [PATCH 115/291] Cosmetis + improve comment --- openpype/hosts/maya/api/lib.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index cf4a6b6b6a..0a835ebeed 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -291,10 +291,10 @@ def generate_capture_preset(instance, camera, path, review_instance_width = instance.data.get("review_width") review_instance_height = instance.data.get("review_height") - # Tests if project resolution is set, - # if it is a value other than zero, that value is - # used, if not then the asset resolution is - # used + # Use resolution from instance if review width/height is set + # Otherwise use the resolution from preset if it has non-zero values + # Otherwise fall back to asset width x height + # Else define no width, then `capture.capture` will use render resolution if review_instance_width and review_instance_height: preset["width"] = review_instance_width preset["height"] = review_instance_height @@ -320,7 +320,7 @@ def generate_capture_preset(instance, camera, path, # Use Pan/Zoom from instance data instead of from preset preset.pop("pan_zoom", None) - preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] + camera_options["panZoomEnabled"] = instance.data["panZoom"] # Override viewport options by instance data viewport_options = preset.setdefault("viewport_options", {}) @@ -334,10 +334,7 @@ def generate_capture_preset(instance, camera, path, # Update preset with current panel setting # if override_viewport_options is turned off - override_viewport_options = ( - capture_preset["Viewport Options"]["override_viewport_options"] - ) - if not override_viewport_options: + if not capture_preset["Viewport Options"]["override_viewport_options"]: panel_preset = capture.parse_view(instance.data["panel"]) panel_preset.pop("camera") preset.update(panel_preset) From f922d3c8f78be9596f58829960b6889f2d4aacdf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 21 Dec 2023 07:41:39 +0100 Subject: [PATCH 116/291] Update openpype/hosts/maya/api/lib.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- openpype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 0a835ebeed..8acf850782 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -335,7 +335,7 @@ def generate_capture_preset(instance, camera, path, # Update preset with current panel setting # if override_viewport_options is turned off if not capture_preset["Viewport Options"]["override_viewport_options"]: - panel_preset = capture.parse_view(instance.data["panel"]) + panel_preset = capture.parse_view(preset["panel"]) panel_preset.pop("camera") preset.update(panel_preset) From 67a6a1169ebb297d792d7f24f7dadb636ac439b5 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 21 Dec 2023 07:41:47 +0100 Subject: [PATCH 117/291] Update openpype/hosts/maya/api/lib.py Co-authored-by: Kayla Man <64118225+moonyuet@users.noreply.github.com> --- openpype/hosts/maya/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 8acf850782..711b36e746 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -242,6 +242,7 @@ def render_capture_preset(preset): if reload_textures: # Regenerate all UDIM tiles previews reload_all_udim_tile_previews() + preset.pop("panel") path = capture.capture(log=self.log, **preset) return path From fcd605ae7dc99ab5f8965ada71bd317bce0c1a35 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Dec 2023 15:43:08 +0800 Subject: [PATCH 118/291] make sure error will be raised if there is neither texture nor image search path --- .../maya/plugins/publish/collect_yeti_rig.py | 8 ++++---- .../maya/plugins/publish/extract_yeti_rig.py | 18 ++++-------------- openpype/pipeline/publish/lib.py | 2 +- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py index d67c51b895..f82f7b69cd 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_rig.py @@ -6,6 +6,7 @@ from maya import cmds import pyblish.api from openpype.hosts.maya.api import lib +from openpype.pipeline.publish import KnownPublishError SETTINGS = {"renderDensity", @@ -116,7 +117,6 @@ class CollectYetiRig(pyblish.api.InstancePlugin): resources = [] image_search_paths = cmds.getAttr("{}.imageSearchPath".format(node)) - texture_filenames = [] if image_search_paths: # TODO: Somehow this uses OS environment path separator, `:` vs `;` @@ -124,8 +124,8 @@ class CollectYetiRig(pyblish.api.InstancePlugin): image_search_paths = [p for p in image_search_paths.split(os.path.pathsep) if p] - # find all ${TOKEN} tokens and replace them with $TOKEN env. variable - image_search_paths = self._replace_tokens(image_search_paths) + # find all ${TOKEN} tokens and replace them with $TOKEN env. variable + image_search_paths = self._replace_tokens(image_search_paths) # List all related textures texture_nodes = cmds.pgYetiGraph( @@ -163,7 +163,7 @@ class CollectYetiRig(pyblish.api.InstancePlugin): break if not files: - self.log.warning( + raise KnownPublishError( "No texture found for: %s " "(searched: %s)" % (texture, image_search_paths)) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index c0c286dcc0..413961073f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -142,20 +142,10 @@ class ExtractYetiRig(publish.Extractor): instance.data['transfers'] = [] for resource in instance.data.get('resources', []): - if resource["files"]: - for file in resource['files']: - src = file - dst = os.path.join( - image_search_path, os.path.basename(file)) - instance.data['transfers'].append([src, dst]) - else: - for file in resource['source']: - src = ( - file if os.path.isabs(file) else os.path.abspath(file) - ) - dst = os.path.join( - image_search_path, os.path.basename(file)) - instance.data['transfers'].append([src, dst]) + for file in resource['files']: + src = file + dst = os.path.join(image_search_path, os.path.basename(file)) + instance.data['transfers'].append([src, dst]) self.log.debug("adding transfer {} -> {}". format(src, dst)) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 4ea2f932f1..87ca3323cb 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -74,7 +74,7 @@ def get_template_name_profiles( project_settings ["global"] ["publish"] - ["IntegrateAssetNew"] + ["IntegrateHeroVersion"] ["template_name_profiles"] ) if legacy_profiles: From 92107167af33f5617df0add058bfb8399e7f4723 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Dec 2023 15:43:51 +0800 Subject: [PATCH 119/291] restore unnecessary tweaks --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 87ca3323cb..4ea2f932f1 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -74,7 +74,7 @@ def get_template_name_profiles( project_settings ["global"] ["publish"] - ["IntegrateHeroVersion"] + ["IntegrateAssetNew"] ["template_name_profiles"] ) if legacy_profiles: From 186ecff5470c3a26d614d9c587965cf2c70bcef1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Dec 2023 15:45:15 +0800 Subject: [PATCH 120/291] restore unnecessary tweaks --- openpype/hosts/maya/plugins/publish/extract_yeti_rig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py index 413961073f..da67cb911f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py +++ b/openpype/hosts/maya/plugins/publish/extract_yeti_rig.py @@ -147,7 +147,7 @@ class ExtractYetiRig(publish.Extractor): dst = os.path.join(image_search_path, os.path.basename(file)) instance.data['transfers'].append([src, dst]) - self.log.debug("adding transfer {} -> {}". format(src, dst)) + self.log.debug("adding transfer {} -> {}". format(src, dst)) # Ensure the imageSearchPath is being remapped to the publish folder attr_value = {"%s.imageSearchPath" % n: str(image_search_path) for From 55c3bdbd06f0baa06550654342ea7a66bd31c6e4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Dec 2023 17:30:37 +0800 Subject: [PATCH 121/291] add the contextmanager functions for 'reverting lock attributes' --- openpype/hosts/maya/api/lib.py | 139 +++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 60 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index af726409d4..64cd5b6b6d 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2565,6 +2565,66 @@ def bake_to_world_space(nodes, list: The newly created and baked node names. """ + @contextlib.contextmanager + def _revert_lock_attributes(node, new_node): + # Connect all attributes on the node except for transform + # attributes + attrs = _get_attrs(node) + attrs = set(attrs) - transform_attrs if attrs else [] + original_attrs_lock = {} + try: + for attr in attrs: + orig_node_attr = '{0}.{1}'.format(node, attr) + new_node_attr = '{0}.{1}'.format(new_node, attr) + original_attrs_lock[new_node_attr] = ( + cmds.getAttr(new_node_attr, lock=True) + ) + + # unlock to avoid connection errors + cmds.setAttr(new_node_attr, lock=False) + + cmds.connectAttr(orig_node_attr, + new_node_attr, + force=True) + yield + finally: + for attr, lock_state in original_attrs_lock.items(): + cmds.setAttr(attr, lock=lock_state) + + @contextlib.contextmanager + def _revert_lock_shape_attributes(node, new_node, shape): + # If shapes are also baked then connect those keyable attributes + if shape: + children_shapes = cmds.listRelatives(new_node, + children=True, + fullPath=True, + shapes=True) + if children_shapes: + orig_children_shapes = cmds.listRelatives(node, + children=True, + fullPath=True, + shapes=True) + original_shape_lock_attrs = {} + try: + for orig_shape, new_shape in zip(orig_children_shapes, + children_shapes): + attrs = _get_attrs(orig_shape) + for attr in attrs: + orig_node_attr = '{0}.{1}'.format(orig_shape, attr) + new_node_attr = '{0}.{1}'.format(new_shape, attr) + original_shape_lock_attrs[new_node_attr] = ( + cmds.getAttr(new_node_attr, lock=True) + ) + # unlock to avoid connection errors + cmds.setAttr(new_node_attr, lock=False) + + cmds.connectAttr(orig_node_attr, + new_node_attr, + force=True) + yield + finally: + for attr, lock_state in original_shape_lock_attrs.items(): + cmds.setAttr(attr, lock=lock_state) def _get_attrs(node): """Workaround for buggy shape attribute listing with listAttr""" @@ -2599,7 +2659,6 @@ def bake_to_world_space(nodes, world_space_nodes = [] with delete_after() as delete_bin: - # Create the duplicate nodes that are in world-space connected to # the originals for node in nodes: @@ -2610,69 +2669,29 @@ def bake_to_world_space(nodes, new_node = cmds.duplicate(node, name=new_name, renameChildren=True)[0] # noqa + with _revert_lock_attributes(node, new_node): + with _revert_lock_shape_attributes(node, new_node): + # Parent to world + if cmds.listRelatives(new_node, parent=True): + new_node = cmds.parent(new_node, world=True)[0] - # Connect all attributes on the node except for transform - # attributes - attrs = _get_attrs(node) - attrs = set(attrs) - transform_attrs if attrs else [] + # Unlock transform attributes so constraint can be created + for attr in transform_attrs: + cmds.setAttr('{0}.{1}'.format(new_node, attr), lock=False) - for attr in attrs: - orig_node_attr = '{0}.{1}'.format(node, attr) - new_node_attr = '{0}.{1}'.format(new_node, attr) + # Constraints + delete_bin.extend(cmds.parentConstraint(node, new_node, mo=False)) + delete_bin.extend(cmds.scaleConstraint(node, new_node, mo=False)) - # unlock to avoid connection errors - cmds.setAttr(new_node_attr, lock=False) + world_space_nodes.append(new_node) - cmds.connectAttr(orig_node_attr, - new_node_attr, - force=True) - - # If shapes are also baked then connect those keyable attributes - if shape: - children_shapes = cmds.listRelatives(new_node, - children=True, - fullPath=True, - shapes=True) - if children_shapes: - orig_children_shapes = cmds.listRelatives(node, - children=True, - fullPath=True, - shapes=True) - for orig_shape, new_shape in zip(orig_children_shapes, - children_shapes): - attrs = _get_attrs(orig_shape) - for attr in attrs: - orig_node_attr = '{0}.{1}'.format(orig_shape, attr) - new_node_attr = '{0}.{1}'.format(new_shape, attr) - - # unlock to avoid connection errors - cmds.setAttr(new_node_attr, lock=False) - - cmds.connectAttr(orig_node_attr, - new_node_attr, - force=True) - - # Parent to world - if cmds.listRelatives(new_node, parent=True): - new_node = cmds.parent(new_node, world=True)[0] - - # Unlock transform attributes so constraint can be created - for attr in transform_attrs: - cmds.setAttr('{0}.{1}'.format(new_node, attr), lock=False) - - # Constraints - delete_bin.extend(cmds.parentConstraint(node, new_node, mo=False)) - delete_bin.extend(cmds.scaleConstraint(node, new_node, mo=False)) - - world_space_nodes.append(new_node) - - bake(world_space_nodes, - frame_range=frame_range, - step=step, - simulation=simulation, - preserve_outside_keys=preserve_outside_keys, - disable_implicit_control=disable_implicit_control, - shape=shape) + bake(world_space_nodes, + frame_range=frame_range, + step=step, + simulation=simulation, + preserve_outside_keys=preserve_outside_keys, + disable_implicit_control=disable_implicit_control, + shape=shape) return world_space_nodes From ac7b2963dd928f04cce701e8d6ef3b74b26a02ed Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Dec 2023 17:34:07 +0800 Subject: [PATCH 122/291] hound --- openpype/hosts/maya/api/lib.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 64cd5b6b6d..959a615ef9 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2584,8 +2584,8 @@ def bake_to_world_space(nodes, cmds.setAttr(new_node_attr, lock=False) cmds.connectAttr(orig_node_attr, - new_node_attr, - force=True) + new_node_attr, + force=True) yield finally: for attr, lock_state in original_attrs_lock.items(): @@ -2596,18 +2596,18 @@ def bake_to_world_space(nodes, # If shapes are also baked then connect those keyable attributes if shape: children_shapes = cmds.listRelatives(new_node, - children=True, - fullPath=True, - shapes=True) + children=True, + fullPath=True, + shapes=True) if children_shapes: orig_children_shapes = cmds.listRelatives(node, - children=True, - fullPath=True, - shapes=True) + children=True, + fullPath=True, + shapes=True) original_shape_lock_attrs = {} try: for orig_shape, new_shape in zip(orig_children_shapes, - children_shapes): + children_shapes): attrs = _get_attrs(orig_shape) for attr in attrs: orig_node_attr = '{0}.{1}'.format(orig_shape, attr) @@ -2619,8 +2619,8 @@ def bake_to_world_space(nodes, cmds.setAttr(new_node_attr, lock=False) cmds.connectAttr(orig_node_attr, - new_node_attr, - force=True) + new_node_attr, + force=True) yield finally: for attr, lock_state in original_shape_lock_attrs.items(): From a75ff0f71aab21daa86ffd40ce45ff0e8b9aee20 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Dec 2023 17:36:35 +0800 Subject: [PATCH 123/291] hound --- openpype/hosts/maya/api/lib.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 959a615ef9..3bc8b67a81 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2671,27 +2671,30 @@ def bake_to_world_space(nodes, renameChildren=True)[0] # noqa with _revert_lock_attributes(node, new_node): with _revert_lock_shape_attributes(node, new_node): - # Parent to world + # Parent to world if cmds.listRelatives(new_node, parent=True): new_node = cmds.parent(new_node, world=True)[0] # Unlock transform attributes so constraint can be created for attr in transform_attrs: - cmds.setAttr('{0}.{1}'.format(new_node, attr), lock=False) + cmds.setAttr( + '{0}.{1}'.format(new_node, attr), lock=False) # Constraints - delete_bin.extend(cmds.parentConstraint(node, new_node, mo=False)) - delete_bin.extend(cmds.scaleConstraint(node, new_node, mo=False)) + delete_bin.extend( + cmds.parentConstraint(node, new_node, mo=False)) + delete_bin.extend( + cmds.scaleConstraint(node, new_node, mo=False)) world_space_nodes.append(new_node) bake(world_space_nodes, - frame_range=frame_range, - step=step, - simulation=simulation, - preserve_outside_keys=preserve_outside_keys, - disable_implicit_control=disable_implicit_control, - shape=shape) + frame_range=frame_range, + step=step, + simulation=simulation, + preserve_outside_keys=preserve_outside_keys, + disable_implicit_control=disable_implicit_control, + shape=shape) return world_space_nodes From a8b93ec8fe5f30f1f15444df90b9bcb7f0d496f0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 21 Dec 2023 10:15:05 +0000 Subject: [PATCH 124/291] Fixes long library names --- openpype/hosts/blender/plugins/load/load_animation.py | 7 ++++++- openpype/hosts/blender/plugins/load/load_blend.py | 7 ++++++- openpype/hosts/blender/plugins/load/load_blendscene.py | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_animation.py b/openpype/hosts/blender/plugins/load/load_animation.py index 3e7f808903..0f968c75e5 100644 --- a/openpype/hosts/blender/plugins/load/load_animation.py +++ b/openpype/hosts/blender/plugins/load/load_animation.py @@ -61,5 +61,10 @@ class BlendAnimationLoader(plugin.AssetLoader): bpy.data.objects.remove(container) - library = bpy.data.libraries.get(bpy.path.basename(libpath)) + filepath = bpy.path.basename(libpath) + # Blender has a limit of 63 characters for any data name. + # If the filepath is longer, it will be truncated. + if len(filepath) > 63: + filepath = filepath[:63] + library = bpy.data.libraries.get(filepath) bpy.data.libraries.remove(library) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index f437e66795..2d5ac18149 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -106,7 +106,12 @@ class BlendLoader(plugin.AssetLoader): bpy.context.scene.collection.objects.link(obj) # Remove the library from the blend file - library = bpy.data.libraries.get(bpy.path.basename(libpath)) + filepath = bpy.path.basename(libpath) + # Blender has a limit of 63 characters for any data name. + # If the filepath is longer, it will be truncated. + if len(filepath) > 63: + filepath = filepath[:63] + library = bpy.data.libraries.get(filepath) bpy.data.libraries.remove(library) return container, members diff --git a/openpype/hosts/blender/plugins/load/load_blendscene.py b/openpype/hosts/blender/plugins/load/load_blendscene.py index 6cc7f39d03..fba0245af1 100644 --- a/openpype/hosts/blender/plugins/load/load_blendscene.py +++ b/openpype/hosts/blender/plugins/load/load_blendscene.py @@ -60,7 +60,12 @@ class BlendSceneLoader(plugin.AssetLoader): bpy.context.scene.collection.children.link(container) # Remove the library from the blend file - library = bpy.data.libraries.get(bpy.path.basename(libpath)) + filepath = bpy.path.basename(libpath) + # Blender has a limit of 63 characters for any data name. + # If the filepath is longer, it will be truncated. + if len(filepath) > 63: + filepath = filepath[:63] + library = bpy.data.libraries.get(filepath) bpy.data.libraries.remove(library) return container, members From 5f309994c39ae8600f414da8ce1da15fec729408 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Dec 2023 18:30:44 +0800 Subject: [PATCH 125/291] cosmetic tweaks and code clean up --- openpype/hosts/maya/api/exitstack.py | 16 +++++++++++++++- openpype/hosts/maya/api/lib.py | 14 ++++++-------- openpype/pipeline/publish/lib.py | 2 +- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/exitstack.py b/openpype/hosts/maya/api/exitstack.py index cacaa396f0..2460f25f59 100644 --- a/openpype/hosts/maya/api/exitstack.py +++ b/openpype/hosts/maya/api/exitstack.py @@ -1,5 +1,19 @@ +"""Backwards compatible implementation of ExitStack for Python 2. + +ExitStack contextmanager was implemented with Python 3.3. As long as we support +Python 2 hosts we can use this backwards compatible implementation to support both +Python 2 and Python 3. + +Instead of using ExitStack from contextlib, use it from this module: + +>>> from openpype.hosts.maya.api.exitstack import ExitStack + +It will provide the appropriate ExitStack implementation for the current +running Python version. + +""" +# TODO: Remove the entire script once dropping Python 2 support. import contextlib -# TODO: Remove the entire script once dropping Python 2. if getattr(contextlib, "nested", None): from contextlib import ExitStack # noqa else: diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 711b36e746..e763ea6702 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -1,6 +1,7 @@ """Standalone helper functions""" import os +import copy from pprint import pformat import sys import uuid @@ -176,12 +177,9 @@ def maintained_selection(): def reload_all_udim_tile_previews(): """Regenerate all UDIM tile preview in texture file""" - texture_files = cmds.ls(type="file") - if texture_files: - for texture_file in texture_files: - if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0: - cmds.ogs(regenerateUVTilePreview=texture_file) - cmds.ogs(reloadTextures=True) + for texture_file in cmds.ls(type="file"): + if cmds.getAttr("{}.uvTilingMode".format(texture_file)) > 0: + cmds.ogs(regenerateUVTilePreview=texture_file) @contextlib.contextmanager @@ -227,7 +225,7 @@ def render_capture_preset(preset): json.dumps(preset, indent=4, sort_keys=True) ) ) - + preset = copy.deepcopy(preset) # not supported by `capture` so we pop it off of the preset reload_textures = preset["viewport_options"].pop("reloadTextures", True) @@ -235,6 +233,7 @@ def render_capture_preset(preset): stack.enter_context(maintained_time()) stack.enter_context(panel_camera(preset["panel"], preset["camera"])) stack.enter_context(viewport_default_options(preset)) + preset.pop("panel") if preset["viewport_options"].get("textures"): # Force immediate texture loading when to ensure # all textures have loaded before the playblast starts @@ -242,7 +241,6 @@ def render_capture_preset(preset): if reload_textures: # Regenerate all UDIM tiles previews reload_all_udim_tile_previews() - preset.pop("panel") path = capture.capture(log=self.log, **preset) return path diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 4ea2f932f1..87ca3323cb 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -74,7 +74,7 @@ def get_template_name_profiles( project_settings ["global"] ["publish"] - ["IntegrateAssetNew"] + ["IntegrateHeroVersion"] ["template_name_profiles"] ) if legacy_profiles: From 1d4acb78538364cd5ba5d16c82d8d561341eaa06 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Dec 2023 18:33:25 +0800 Subject: [PATCH 126/291] hound --- openpype/hosts/maya/api/exitstack.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/exitstack.py b/openpype/hosts/maya/api/exitstack.py index 2460f25f59..d151ee16d7 100644 --- a/openpype/hosts/maya/api/exitstack.py +++ b/openpype/hosts/maya/api/exitstack.py @@ -1,8 +1,8 @@ """Backwards compatible implementation of ExitStack for Python 2. -ExitStack contextmanager was implemented with Python 3.3. As long as we support -Python 2 hosts we can use this backwards compatible implementation to support both -Python 2 and Python 3. +ExitStack contextmanager was implemented with Python 3.3. +As long as we supportPython 2 hosts we can use this backwards +compatible implementation to support bothPython 2 and Python 3. Instead of using ExitStack from contextlib, use it from this module: From b012e169d413eb632fd347cdd8b52325034e7f50 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 21 Dec 2023 15:23:59 +0000 Subject: [PATCH 127/291] Changed error message Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/blender/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index d50bfad53d..1037854a2d 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -40,7 +40,7 @@ def prepare_scene_name( # Blender name for a collection or object cannot be longer than 63 # characters. If the name is longer, it will raise an error. if len(name) > 63: - raise ValueError(f"Asset name '{name}' is too long.") + raise ValueError(f"Scene name '{name}' would be too long.") return name From a3f93790f1fcae59b2b7db18a5c1fd7a88b8b6ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Dec 2023 00:07:16 +0800 Subject: [PATCH 128/291] repharse the preset pop for panel --- openpype/hosts/maya/api/lib.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index e763ea6702..57deb24a94 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -228,12 +228,11 @@ def render_capture_preset(preset): preset = copy.deepcopy(preset) # not supported by `capture` so we pop it off of the preset reload_textures = preset["viewport_options"].pop("reloadTextures", True) - + panel = preset.pop("panel") with ExitStack() as stack: stack.enter_context(maintained_time()) - stack.enter_context(panel_camera(preset["panel"], preset["camera"])) - stack.enter_context(viewport_default_options(preset)) - preset.pop("panel") + stack.enter_context(panel_camera(panel, preset["camera"])) + stack.enter_context(viewport_default_options(preset, panel)) if preset["viewport_options"].get("textures"): # Force immediate texture loading when to ensure # all textures have loaded before the playblast starts @@ -342,7 +341,7 @@ def generate_capture_preset(instance, camera, path, @contextlib.contextmanager -def viewport_default_options(preset): +def viewport_default_options(preset, panel): """Context manager used by `render_capture_preset`. We need to explicitly enable some viewport changes so the viewport is @@ -363,18 +362,18 @@ def viewport_default_options(preset): ] for key in keys: viewport_defaults[key] = cmds.modelEditor( - preset["panel"], query=True, **{key: True} + panel, query=True, **{key: True} ) if preset["viewport_options"][key]: cmds.modelEditor( - preset["panel"], edit=True, **{key: True} + panel, edit=True, **{key: True} ) yield finally: # Restoring viewport options. if viewport_defaults: cmds.modelEditor( - preset["panel"], edit=True, **viewport_defaults + panel, edit=True, **viewport_defaults ) From 6f5432611fb7020d76a69886a0798e157ede629e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Dec 2023 00:07:56 +0800 Subject: [PATCH 129/291] restore unnecessary tweaks --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 87ca3323cb..4ea2f932f1 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -74,7 +74,7 @@ def get_template_name_profiles( project_settings ["global"] ["publish"] - ["IntegrateHeroVersion"] + ["IntegrateAssetNew"] ["template_name_profiles"] ) if legacy_profiles: From 7acbef93288da4f1925ba4ef29bf43b8cec23d9d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Dec 2023 00:35:21 +0800 Subject: [PATCH 130/291] change the args oder in viewport_default_options --- openpype/hosts/maya/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 57deb24a94..1a8a80f224 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -232,7 +232,7 @@ def render_capture_preset(preset): with ExitStack() as stack: stack.enter_context(maintained_time()) stack.enter_context(panel_camera(panel, preset["camera"])) - stack.enter_context(viewport_default_options(preset, panel)) + stack.enter_context(viewport_default_options(panel, preset)) if preset["viewport_options"].get("textures"): # Force immediate texture loading when to ensure # all textures have loaded before the playblast starts @@ -341,7 +341,7 @@ def generate_capture_preset(instance, camera, path, @contextlib.contextmanager -def viewport_default_options(preset, panel): +def viewport_default_options(panel, preset): """Context manager used by `render_capture_preset`. We need to explicitly enable some viewport changes so the viewport is From 3fd9d47a387d9bc428ca809d473ce038ebac2a6f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Dec 2023 00:45:09 +0800 Subject: [PATCH 131/291] use filename = instance.name --- openpype/hosts/maya/plugins/publish/extract_playblast.py | 2 +- openpype/hosts/maya/plugins/publish/extract_thumbnail.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index c41cf67fb4..507229a7b3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -48,7 +48,7 @@ class ExtractPlayblast(publish.Extractor): self.log ) stagingdir = self.staging_dir(instance) - filename = "{0}".format(instance.name) + filename = instance.name path = os.path.join(stagingdir, filename) self.log.debug("Outputting images to %s" % path) # get cameras diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 0d332d73ea..08f061985e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -39,7 +39,7 @@ class ExtractThumbnail(publish.Extractor): "Create temp directory {} for thumbnail".format(dst_staging) ) # Store new staging to cleanup paths - filename = "{0}".format(instance.name) + filename = instance.name path = os.path.join(dst_staging, filename) self.log.debug("Outputting images to %s" % path) From 1a39a5cc99ca18fcd2354a5e5d8e928ce67bc4df Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 22 Dec 2023 11:09:46 +0100 Subject: [PATCH 132/291] Unlock attributes only during the bake context, make sure attributes are relocked after to preserve the lock state of the original node being baked --- openpype/hosts/maya/api/lib.py | 171 +++++++++++++++++---------------- 1 file changed, 87 insertions(+), 84 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 3bc8b67a81..63f0812785 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2566,68 +2566,36 @@ def bake_to_world_space(nodes, """ @contextlib.contextmanager - def _revert_lock_attributes(node, new_node): - # Connect all attributes on the node except for transform - # attributes - attrs = _get_attrs(node) - attrs = set(attrs) - transform_attrs if attrs else [] - original_attrs_lock = {} + def _unlock_attr(attr): + """Unlock attribute during context if it is locked""" + if not cmds.getAttr(attr, lock=True): + # If not locked, do nothing + yield + return try: - for attr in attrs: - orig_node_attr = '{0}.{1}'.format(node, attr) - new_node_attr = '{0}.{1}'.format(new_node, attr) - original_attrs_lock[new_node_attr] = ( - cmds.getAttr(new_node_attr, lock=True) - ) - - # unlock to avoid connection errors - cmds.setAttr(new_node_attr, lock=False) - - cmds.connectAttr(orig_node_attr, - new_node_attr, - force=True) + cmds.setAttr(attr, lock=False) yield finally: - for attr, lock_state in original_attrs_lock.items(): - cmds.setAttr(attr, lock=lock_state) - - @contextlib.contextmanager - def _revert_lock_shape_attributes(node, new_node, shape): - # If shapes are also baked then connect those keyable attributes - if shape: - children_shapes = cmds.listRelatives(new_node, - children=True, - fullPath=True, - shapes=True) - if children_shapes: - orig_children_shapes = cmds.listRelatives(node, - children=True, - fullPath=True, - shapes=True) - original_shape_lock_attrs = {} - try: - for orig_shape, new_shape in zip(orig_children_shapes, - children_shapes): - attrs = _get_attrs(orig_shape) - for attr in attrs: - orig_node_attr = '{0}.{1}'.format(orig_shape, attr) - new_node_attr = '{0}.{1}'.format(new_shape, attr) - original_shape_lock_attrs[new_node_attr] = ( - cmds.getAttr(new_node_attr, lock=True) - ) - # unlock to avoid connection errors - cmds.setAttr(new_node_attr, lock=False) - - cmds.connectAttr(orig_node_attr, - new_node_attr, - force=True) - yield - finally: - for attr, lock_state in original_shape_lock_attrs.items(): - cmds.setAttr(attr, lock=lock_state) + cmds.setAttr(attr, lock=True) def _get_attrs(node): - """Workaround for buggy shape attribute listing with listAttr""" + """Workaround for buggy shape attribute listing with listAttr + + This will only return keyable settable attributes that have an + incoming connections (those that have a reason to be baked). + + Technically this *may* fail to return attributes driven by complex + expressions for which maya makes no connections, e.g. doing actual + `setAttr` calls in expressions. + + Arguments: + node (str): The node to list attributes for. + + Returns: + list: Keyable attributes with incoming connections. + The attribute may be locked. + + """ attrs = cmds.listAttr(node, write=True, scalar=True, @@ -2652,13 +2620,14 @@ def bake_to_world_space(nodes, return valid_attrs - transform_attrs = set(["t", "r", "s", - "tx", "ty", "tz", - "rx", "ry", "rz", - "sx", "sy", "sz"]) + transform_attrs = {"t", "r", "s", + "tx", "ty", "tz", + "rx", "ry", "rz", + "sx", "sy", "sz"} world_space_nodes = [] - with delete_after() as delete_bin: + with contextlib.ExitStack() as stack: + delete_bin = stack.enter_context(delete_after()) # Create the duplicate nodes that are in world-space connected to # the originals for node in nodes: @@ -2669,32 +2638,66 @@ def bake_to_world_space(nodes, new_node = cmds.duplicate(node, name=new_name, renameChildren=True)[0] # noqa - with _revert_lock_attributes(node, new_node): - with _revert_lock_shape_attributes(node, new_node): - # Parent to world - if cmds.listRelatives(new_node, parent=True): - new_node = cmds.parent(new_node, world=True)[0] - # Unlock transform attributes so constraint can be created - for attr in transform_attrs: - cmds.setAttr( - '{0}.{1}'.format(new_node, attr), lock=False) + # Parent new node to world + if cmds.listRelatives(new_node, parent=True): + new_node = cmds.parent(new_node, world=True)[0] - # Constraints - delete_bin.extend( - cmds.parentConstraint(node, new_node, mo=False)) - delete_bin.extend( - cmds.scaleConstraint(node, new_node, mo=False)) + # Temporarily unlock and passthrough connect all attributes + # so we can bake them over time + # Skip transform attributes because we will constrain them later + attrs = set(_get_attrs(node)) - transform_attrs + for attr in attrs: + orig_node_attr = "{}.{}".format(node, attr) + new_node_attr = "{}.{}".format(new_node, attr) - world_space_nodes.append(new_node) + # unlock during context to avoid connection errors + stack.enter_context(_unlock_attr(new_node_attr)) + cmds.connectAttr(orig_node_attr, + new_node_attr, + force=True) - bake(world_space_nodes, - frame_range=frame_range, - step=step, - simulation=simulation, - preserve_outside_keys=preserve_outside_keys, - disable_implicit_control=disable_implicit_control, - shape=shape) + # If shapes are also baked then also temporarily unlock and + # passthrough connect all shape attributes for baking + if shape: + children_shapes = cmds.listRelatives(new_node, + children=True, + fullPath=True, + shapes=True) + if children_shapes: + orig_children_shapes = cmds.listRelatives(node, + children=True, + fullPath=True, + shapes=True) + for orig_shape, new_shape in zip(orig_children_shapes, + children_shapes): + attrs = _get_attrs(orig_shape) + for attr in attrs: + orig_node_attr = "{}.{}".format(orig_shape, attr) + new_node_attr = "{}.{}".format(new_shape, attr) + + # unlock during context to avoid connection errors + stack.enter_context(_unlock_attr(new_node_attr)) + cmds.connectAttr(orig_node_attr, + new_node_attr, + force=True) + + # Constraint transforms + for attr in transform_attrs: + transform_attr = "{}.{}".format(new_node, attr) + stack.enter_context(_unlock_attr(transform_attr)) + delete_bin.extend(cmds.parentConstraint(node, new_node, mo=False)) + delete_bin.extend(cmds.scaleConstraint(node, new_node, mo=False)) + + world_space_nodes.append(new_node) + + bake(world_space_nodes, + frame_range=frame_range, + step=step, + simulation=simulation, + preserve_outside_keys=preserve_outside_keys, + disable_implicit_control=disable_implicit_control, + shape=shape) return world_space_nodes From eaa0a8f60eeac2e3441d2bf83aa395dca4222326 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Dec 2023 22:21:47 +0800 Subject: [PATCH 133/291] make sure not overindented when calling bake function --- openpype/hosts/maya/api/lib.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 63f0812785..06cefbf793 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2691,13 +2691,13 @@ def bake_to_world_space(nodes, world_space_nodes.append(new_node) - bake(world_space_nodes, - frame_range=frame_range, - step=step, - simulation=simulation, - preserve_outside_keys=preserve_outside_keys, - disable_implicit_control=disable_implicit_control, - shape=shape) + bake(world_space_nodes, + frame_range=frame_range, + step=step, + simulation=simulation, + preserve_outside_keys=preserve_outside_keys, + disable_implicit_control=disable_implicit_control, + shape=shape) return world_space_nodes From fa24804eff3de6c6b39940123adb5417b8d41f12 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 23 Dec 2023 03:25:06 +0000 Subject: [PATCH 134/291] [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 c4ff4dde95..505148da16 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.2-nightly.2" +__version__ = "3.18.2-nightly.3" From 226a2e0b8b0f03e48a47673d91871fe4fb2f5487 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 23 Dec 2023 03:25:38 +0000 Subject: [PATCH 135/291] 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 fd3455ac76..bacdfc3a15 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.18.2-nightly.3 - 3.18.2-nightly.2 - 3.18.2-nightly.1 - 3.18.1 @@ -134,7 +135,6 @@ body: - 3.15.5-nightly.1 - 3.15.4 - 3.15.4-nightly.3 - - 3.15.4-nightly.2 validations: required: true - type: dropdown From 4b6e7beb87f57d34931801c9993fa61f8a135a74 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 23 Dec 2023 21:23:34 +0800 Subject: [PATCH 136/291] make sure extract review intermediate disabled when both deprecrated and current setting diabled --- .../nuke/plugins/publish/extract_review_intermediates.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py index 3ee166eb56..a02a807206 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py @@ -34,6 +34,11 @@ class ExtractReviewIntermediates(publish.Extractor): nuke_publish = project_settings["nuke"]["publish"] deprecated_setting = nuke_publish["ExtractReviewDataMov"] current_setting = nuke_publish.get("ExtractReviewIntermediates") + if not deprecated_setting["enabled"] and ( + not current_setting["enabled"] + ): + cls.enabled = False + if deprecated_setting["enabled"]: # Use deprecated settings if they are still enabled cls.viewer_lut_raw = deprecated_setting["viewer_lut_raw"] From 58172994cfe70afa169aa68dc414cc947a20ab98 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 27 Dec 2023 03:25:47 +0000 Subject: [PATCH 137/291] [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 505148da16..93fdf432b3 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.2-nightly.3" +__version__ = "3.18.2-nightly.4" From 11a3114de8c64bd64854bdccde3d257c746abdad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 Dec 2023 03:26:21 +0000 Subject: [PATCH 138/291] 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 bacdfc3a15..a691e021fd 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.18.2-nightly.4 - 3.18.2-nightly.3 - 3.18.2-nightly.2 - 3.18.2-nightly.1 @@ -134,7 +135,6 @@ body: - 3.15.5-nightly.2 - 3.15.5-nightly.1 - 3.15.4 - - 3.15.4-nightly.3 validations: required: true - type: dropdown From 7f4f32f450807978567360fab2faf59d19c945df Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 12:21:16 +0100 Subject: [PATCH 139/291] add 'create_at' information to report item data --- openpype/tools/publisher/control.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index b02d83e4f6..125d93b6fd 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -4,6 +4,7 @@ import logging import traceback import collections import uuid +import datetime import tempfile import shutil import inspect @@ -285,6 +286,8 @@ class PublishReportMaker: def get_report(self, publish_plugins=None): """Report data with all details of current state.""" + + now = datetime.datetime.now() instances_details = {} for instance in self._all_instances_by_id.values(): instances_details[instance.id] = self._extract_instance_data( @@ -334,7 +337,8 @@ class PublishReportMaker: "context": self._extract_context_data(self._current_context), "crashed_file_paths": crashed_file_paths, "id": uuid.uuid4().hex, - "report_version": "1.0.0" + "created_at": now.strftime("%Y-%m-%d %H:%M:%S"), + "report_version": "1.0.1", } def _extract_context_data(self, context): From 1e78259d3a3c7f7a8ca50fb470957772d6d9fdd8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 12:23:44 +0100 Subject: [PATCH 140/291] added 'created_at' with auto fix for older report items --- .../publisher/publish_report_viewer/window.py | 149 +++++++++++++++--- 1 file changed, 129 insertions(+), 20 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index dc4ad70934..533b9de15f 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -2,6 +2,7 @@ import os import json import six import uuid +import datetime import appdirs from qtpy import QtWidgets, QtCore, QtGui @@ -47,47 +48,79 @@ class PublishReportItem: """Report item representing one file in report directory.""" def __init__(self, content): - item_id = content.get("id") - changed = False - if not item_id: - item_id = str(uuid.uuid4()) - changed = True - content["id"] = item_id + changed = self._fix_content(content) - if not content.get("report_version"): - changed = True - content["report_version"] = "0.0.1" - - report_path = os.path.join(get_reports_dir(), item_id) + report_path = os.path.join(get_reports_dir(), content["id"]) file_modified = None if os.path.exists(report_path): file_modified = os.path.getmtime(report_path) + + created_at_obj = datetime.datetime.strptime( + content["created_at"], "%Y-%m-%d %H:%M:%S" + ) + created_at = self.date_obj_to_timestamp(created_at_obj) + self.content = content self.report_path = report_path self.file_modified = file_modified + self.created_at = float(created_at) self._loaded_label = content.get("label") self._changed = changed self.publish_report = PublishReport(content) @property def version(self): + """Publish report version. + + Returns: + str: Publish report version. + """ return self.content["report_version"] @property def id(self): + """Publish report id. + + Returns: + str: Publish report id. + """ + return self.content["id"] def get_label(self): + """Publish report label. + + Returns: + str: Publish report label showed in UI. + """ + return self.content.get("label") or "Unfilled label" def set_label(self, label): + """Set publish report label. + + Args: + label (str): New publish report label. + """ + if not label: self.content.pop("label", None) self.content["label"] = label label = property(get_label, set_label) + @property + def loaded_label(self): + return self._loaded_label + + def mark_as_changed(self): + """Mark report as changed.""" + + self._changed = True + def save(self): + """Save publish report to file.""" + save = False if ( self._changed @@ -109,6 +142,15 @@ class PublishReportItem: @classmethod def from_filepath(cls, filepath): + """Create report item from file. + + Args: + filepath (str): Path to report file. Content must be json. + + Returns: + PublishReportItem: Report item. + """ + if not os.path.exists(filepath): return None @@ -116,15 +158,25 @@ class PublishReportItem: with open(filepath, "r") as stream: content = json.load(stream) - return cls(content) + file_modified = os.path.getmtime(filepath) + changed = cls._fix_content(content, file_modified=file_modified) + obj = cls(content) + if changed: + obj.mark_as_changed() + return obj + except Exception: return None def remove_file(self): + """Remove report file.""" + if os.path.exists(self.report_path): os.remove(self.report_path) def update_file_content(self): + """Update report content in file.""" + if not os.path.exists(self.report_path): return @@ -148,6 +200,63 @@ class PublishReportItem: self.content = content self.file_modified = file_modified + @staticmethod + def date_obj_to_timestamp(date_obj): + if hasattr(date_obj, "timestamp"): + return date_obj.timestamp() + + # Python 2 support + epoch = datetime.datetime.fromtimestamp(0) + return (date_obj - epoch).total_seconds() + + @classmethod + def _fix_content(cls, content, file_modified=None): + """Fix content for backward compatibility of older report items. + + Args: + content (dict[str, Any]): Report content. + file_modified (Optional[float]): File modification time. + + Returns: + bool: True if content was changed, False otherwise. + """ + + # Fix created_at key + changed = cls._fix_created_at(content, file_modified) + + # NOTE backward compatibility for 'id' and 'report_version' is from + # 28.10.2022 https://github.com/ynput/OpenPype/pull/4040 + # We can probably safely remove it + + # Fix missing 'id' + item_id = content.get("id") + if not item_id: + item_id = str(uuid.uuid4()) + changed = True + content["id"] = item_id + + # Fix missing 'report_version' + if not content.get("report_version"): + changed = True + content["report_version"] = "0.0.1" + return changed + + @classmethod + def _fix_created_at(cls, content, file_modified): + # Key 'create_at' was added in report version 1.0.1 + created_at = content.get("created_at") + if created_at: + return False + + # Auto fix 'created_at', use file modification time if it is not set + # , or current time if modification could not be received. + if file_modified is not None: + created_at_obj = datetime.datetime.fromtimestamp(file_modified) + else: + created_at_obj = datetime.datetime.now() + content["created_at"] = created_at_obj.strftime("%Y-%m-%d %H:%M:%S") + return True + class PublisherReportHandler: """Class handling storing publish report tool.""" @@ -278,16 +387,16 @@ class LoadedFilesModel(QtGui.QStandardItemModel): new_items = [] for normalized_path in filtered_paths: - try: - with open(normalized_path, "r") as stream: - data = json.load(stream) - report_item = PublishReportItem(data) - except Exception: - # TODO handle errors + report_item = PublishReportItem.from_filepath(normalized_path) + if report_item is None: continue - label = data.get("label") - if not label: + # Skip already added report items + # QUESTION: Should we replace existing or skip the item? + if report_item.id in self._items_by_id: + continue + + if not report_item.loaded_label: report_item.label = ( os.path.splitext(os.path.basename(filepath))[0] ) From 5f6d6ca1b3b55011735d468f7839c2076a4c0c8a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 12:24:55 +0100 Subject: [PATCH 141/291] use created at as sorting value --- .../publisher/publish_report_viewer/window.py | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 533b9de15f..0d7042a69c 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -26,6 +26,7 @@ else: ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 +ITEM_CREATED_AT_ROLE = QtCore.Qt.UserRole + 2 def get_reports_dir(): @@ -300,9 +301,16 @@ class PublisherReportHandler: class LoadedFilesModel(QtGui.QStandardItemModel): + header_labels = ("Reports", "Created") + def __init__(self, *args, **kwargs): super(LoadedFilesModel, self).__init__(*args, **kwargs) + # Column count must be set before setting header data + self.setColumnCount(len(self.header_labels)) + for col, label in enumerate(self.header_labels): + self.setHeaderData(col, QtCore.Qt.Horizontal, label) + self._items_by_id = {} self._report_items_by_id = {} @@ -311,10 +319,15 @@ class LoadedFilesModel(QtGui.QStandardItemModel): self._loading_registry = False def refresh(self): - self._handler.reset() + root_item = self.invisibleRootItem() + if root_item.rowCount(): + root_item.removeRows(0, root_item.rowCount()) + self._items_by_id = {} self._report_items_by_id = {} + self._handler.reset() + new_items = [] for report_item in self._handler.list_reports(): item = self._create_item(report_item) @@ -326,26 +339,26 @@ class LoadedFilesModel(QtGui.QStandardItemModel): root_item = self.invisibleRootItem() root_item.appendRows(new_items) - def headerData(self, section, orientation, role): - if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): - if section == 0: - return "Exports" - if section == 1: - return "Modified" - return "" - super(LoadedFilesModel, self).headerData(section, orientation, role) - def data(self, index, role=None): if role is None: role = QtCore.Qt.DisplayRole col = index.column() + if col == 1: + if role in ( + QtCore.Qt.DisplayRole, QtCore.Qt.InitialSortOrderRole + ): + role = ITEM_CREATED_AT_ROLE + if col != 0: index = self.index(index.row(), 0, index.parent()) return super(LoadedFilesModel, self).data(index, role) - def setData(self, index, value, role): + def setData(self, index, value, role=None): + if role is None: + role = QtCore.Qt.EditRole + if role == QtCore.Qt.EditRole: item_id = index.data(ITEM_ID_ROLE) report_item = self._report_items_by_id.get(item_id) @@ -356,6 +369,12 @@ class LoadedFilesModel(QtGui.QStandardItemModel): return super(LoadedFilesModel, self).setData(index, value, role) + def flags(self, index): + # Allow editable flag only for first column + if index.column() > 0: + return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + return super(LoadedFilesModel, self).flags(index) + def _create_item(self, report_item): if report_item.id in self._items_by_id: return None @@ -363,6 +382,7 @@ class LoadedFilesModel(QtGui.QStandardItemModel): item = QtGui.QStandardItem(report_item.label) item.setColumnCount(self.columnCount()) item.setData(report_item.id, ITEM_ID_ROLE) + item.setData(report_item.created_at, ITEM_CREATED_AT_ROLE) return item @@ -444,13 +464,18 @@ class LoadedFilesView(QtWidgets.QTreeView): ) self.setIndentation(0) self.setAlternatingRowColors(True) + self.setSortingEnabled(True) model = LoadedFilesModel() - self.setModel(model) + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setSourceModel(model) + self.setModel(proxy_model) time_delegate = PrettyTimeDelegate() self.setItemDelegateForColumn(1, time_delegate) + self.sortByColumn(1, QtCore.Qt.AscendingOrder) + remove_btn = IconButton(self) remove_icon_path = resources.get_icon_path("delete") loaded_remove_image = QtGui.QImage(remove_icon_path) @@ -465,6 +490,7 @@ class LoadedFilesView(QtWidgets.QTreeView): ) self._model = model + self._proxy_model = proxy_model self._time_delegate = time_delegate self._remove_btn = remove_btn From 8bb497e64b91936e71b5b2263e34e937ac875022 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 12:25:13 +0100 Subject: [PATCH 142/291] renamed 'remove_report_items' tp 'remove_report_item' --- .../tools/publisher/publish_report_viewer/window.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 0d7042a69c..1557f2bd0b 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -290,7 +290,15 @@ class PublisherReportHandler: self._reports_by_id = reports_by_id return reports - def remove_report_items(self, item_id): + def remove_report_item(self, item_id): + """Remove report item by id. + + Remove from cache and also remove the file with the content. + + Args: + item_id (str): Report item id. + """ + item = self._reports_by_id.get(item_id) if item: try: @@ -439,7 +447,7 @@ class LoadedFilesModel(QtGui.QStandardItemModel): if not report_item: return - self._handler.remove_report_items(item_id) + self._handler.remove_report_item(item_id) item = self._items_by_id.get(item_id) parent = self.invisibleRootItem() From 64ef575ccecee9af8715fbcdaf8aca1c10825fcc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 12:25:29 +0100 Subject: [PATCH 143/291] modified docstring --- openpype/tools/publisher/publish_report_viewer/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 1557f2bd0b..ffb2d64be6 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -260,7 +260,7 @@ class PublishReportItem: class PublisherReportHandler: - """Class handling storing publish report tool.""" + """Class handling storing publish report items.""" def __init__(self): self._reports = None From 46f05968bcabf82e05b8f9d693ce23629abbd066 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 12:25:44 +0100 Subject: [PATCH 144/291] add loaded report item only if could be created --- openpype/tools/publisher/publish_report_viewer/window.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index ffb2d64be6..84b25467b4 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -283,8 +283,9 @@ class PublisherReportHandler: continue filepath = os.path.join(report_dir, filename) item = PublishReportItem.from_filepath(filepath) - reports.append(item) - reports_by_id[item.id] = item + if item is not None: + reports.append(item) + reports_by_id[item.id] = item self._reports = reports self._reports_by_id = reports_by_id From 25a0e64a068f50d2f955b6e2a95c743d7d742abc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 15:51:05 +0100 Subject: [PATCH 145/291] removed unnecessary 'date_obj_to_timestamp' --- .../tools/publisher/publish_report_viewer/window.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 84b25467b4..9dbe991201 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -59,7 +59,7 @@ class PublishReportItem: created_at_obj = datetime.datetime.strptime( content["created_at"], "%Y-%m-%d %H:%M:%S" ) - created_at = self.date_obj_to_timestamp(created_at_obj) + created_at = created_at_obj.timestamp() self.content = content self.report_path = report_path @@ -201,15 +201,6 @@ class PublishReportItem: self.content = content self.file_modified = file_modified - @staticmethod - def date_obj_to_timestamp(date_obj): - if hasattr(date_obj, "timestamp"): - return date_obj.timestamp() - - # Python 2 support - epoch = datetime.datetime.fromtimestamp(0) - return (date_obj - epoch).total_seconds() - @classmethod def _fix_content(cls, content, file_modified=None): """Fix content for backward compatibility of older report items. From 6db0dcbb44cc63c83113ef04af56e6e999cd5d41 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 27 Dec 2023 15:55:39 +0100 Subject: [PATCH 146/291] Fix weird comment --- openpype/tools/publisher/publish_report_viewer/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 9dbe991201..7cc7c6366a 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -241,7 +241,7 @@ class PublishReportItem: return False # Auto fix 'created_at', use file modification time if it is not set - # , or current time if modification could not be received. + # or current time if modification could not be received. if file_modified is not None: created_at_obj = datetime.datetime.fromtimestamp(file_modified) else: From f7fba84aeec00977ef893f6696eeb7c2ab8e749d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 16:33:22 +0100 Subject: [PATCH 147/291] fix '_fill_selection' by accessing model method --- openpype/tools/publisher/publish_report_viewer/window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 7cc7c6366a..ea78571223 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -538,7 +538,8 @@ class LoadedFilesView(QtWidgets.QTreeView): if index.isValid(): return - index = self._model.index(0, 0) + model = self.model() + index = model.index(0, 0) if index.isValid(): self.setCurrentIndex(index) From 78460c2d38d4111e3b3dd0403b2148eaa3159e88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 16:43:00 +0100 Subject: [PATCH 148/291] remove items from _report_items_by_id and _items_by_id --- .../tools/publisher/publish_report_viewer/window.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index ea78571223..283d6284d6 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -435,15 +435,13 @@ class LoadedFilesModel(QtGui.QStandardItemModel): root_item.appendRows(new_items) def remove_item_by_id(self, item_id): - report_item = self._report_items_by_id.get(item_id) - if not report_item: - return - self._handler.remove_report_item(item_id) - item = self._items_by_id.get(item_id) - parent = self.invisibleRootItem() - parent.removeRow(item.row()) + self._report_items_by_id.pop(item_id, None) + item = self._items_by_id.pop(item_id) + if item is not None: + parent = self.invisibleRootItem() + parent.removeRow(item.row()) def get_report_by_id(self, item_id): report_item = self._report_items_by_id.get(item_id) From 10125a40784e97e0a46f1e87063833ff98ccd7c2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 16:45:49 +0100 Subject: [PATCH 149/291] avoid usage of 'clear' method --- .../tools/publisher/publish_report_viewer/model.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/model.py b/openpype/tools/publisher/publish_report_viewer/model.py index 663a67ac70..9d3c90d18f 100644 --- a/openpype/tools/publisher/publish_report_viewer/model.py +++ b/openpype/tools/publisher/publish_report_viewer/model.py @@ -26,14 +26,15 @@ class InstancesModel(QtGui.QStandardItemModel): return self._items_by_id def set_report(self, report_item): - self.clear() + root_item = self.invisibleRootItem() + if root_item.rowCount(): + root_item.removeRows(0, root_item.rowCount()) + self._items_by_id.clear() self._plugin_items_by_id.clear() if not report_item: return - root_item = self.invisibleRootItem() - families = set(report_item.instance_items_by_family.keys()) families.remove(None) all_families = list(sorted(families)) @@ -125,14 +126,14 @@ class PluginsModel(QtGui.QStandardItemModel): return self._items_by_id def set_report(self, report_item): - self.clear() + root_item = self.invisibleRootItem() + if root_item.rowCount() > 0: + root_item.removeRows(0, root_item.rowCount()) self._items_by_id.clear() self._plugin_items_by_id.clear() if not report_item: return - root_item = self.invisibleRootItem() - labels_iter = iter(self.order_label_mapping) cur_order, cur_label = next(labels_iter) cur_plugin_items = [] From 79f592aaf375282892d5e5e9e4951073e28bd482 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 16:52:29 +0100 Subject: [PATCH 150/291] fix 'pop' of item by id --- openpype/tools/publisher/publish_report_viewer/window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 283d6284d6..8422f3aeab 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -438,7 +438,7 @@ class LoadedFilesModel(QtGui.QStandardItemModel): self._handler.remove_report_item(item_id) self._report_items_by_id.pop(item_id, None) - item = self._items_by_id.pop(item_id) + item = self._items_by_id.pop(item_id, None) if item is not None: parent = self.invisibleRootItem() parent.removeRow(item.row()) From 3d7e02a550c70ffa07e68c7c4c0950431681707f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 18:21:41 +0100 Subject: [PATCH 151/291] use utc isoformat with help of 'arrow' --- openpype/tools/publisher/control.py | 5 +++-- .../publisher/publish_report_viewer/window.py | 17 +++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 125d93b6fd..23456579ca 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -11,6 +11,7 @@ import inspect from abc import ABCMeta, abstractmethod import six +import arrow import pyblish.api from openpype import AYON_SERVER_ENABLED @@ -287,7 +288,7 @@ class PublishReportMaker: def get_report(self, publish_plugins=None): """Report data with all details of current state.""" - now = datetime.datetime.now() + now = arrow.utcnow().to("local") instances_details = {} for instance in self._all_instances_by_id.values(): instances_details[instance.id] = self._extract_instance_data( @@ -337,7 +338,7 @@ class PublishReportMaker: "context": self._extract_context_data(self._current_context), "crashed_file_paths": crashed_file_paths, "id": uuid.uuid4().hex, - "created_at": now.strftime("%Y-%m-%d %H:%M:%S"), + "created_at": now.isoformat(), "report_version": "1.0.1", } diff --git a/openpype/tools/publisher/publish_report_viewer/window.py b/openpype/tools/publisher/publish_report_viewer/window.py index 8422f3aeab..f9c8c05802 100644 --- a/openpype/tools/publisher/publish_report_viewer/window.py +++ b/openpype/tools/publisher/publish_report_viewer/window.py @@ -2,9 +2,9 @@ import os import json import six import uuid -import datetime import appdirs +import arrow from qtpy import QtWidgets, QtCore, QtGui from openpype import style @@ -56,10 +56,8 @@ class PublishReportItem: if os.path.exists(report_path): file_modified = os.path.getmtime(report_path) - created_at_obj = datetime.datetime.strptime( - content["created_at"], "%Y-%m-%d %H:%M:%S" - ) - created_at = created_at_obj.timestamp() + created_at_obj = arrow.get(content["created_at"]).to("local") + created_at = created_at_obj.float_timestamp self.content = content self.report_path = report_path @@ -243,10 +241,10 @@ class PublishReportItem: # Auto fix 'created_at', use file modification time if it is not set # or current time if modification could not be received. if file_modified is not None: - created_at_obj = datetime.datetime.fromtimestamp(file_modified) + created_at_obj = arrow.Arrow.fromtimestamp(file_modified) else: - created_at_obj = datetime.datetime.now() - content["created_at"] = created_at_obj.strftime("%Y-%m-%d %H:%M:%S") + created_at_obj = arrow.utcnow() + content["created_at"] = created_at_obj.to("local").isoformat() return True @@ -320,9 +318,8 @@ class LoadedFilesModel(QtGui.QStandardItemModel): def refresh(self): root_item = self.invisibleRootItem() - if root_item.rowCount(): + if root_item.rowCount() > 0: root_item.removeRows(0, root_item.rowCount()) - self._items_by_id = {} self._report_items_by_id = {} From 3a22c1dd154fc15409d1a80930861335e5a98b88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 18:35:27 +0100 Subject: [PATCH 152/291] use explicit condition --- openpype/tools/publisher/publish_report_viewer/model.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/tools/publisher/publish_report_viewer/model.py b/openpype/tools/publisher/publish_report_viewer/model.py index 9d3c90d18f..460a269f1a 100644 --- a/openpype/tools/publisher/publish_report_viewer/model.py +++ b/openpype/tools/publisher/publish_report_viewer/model.py @@ -27,9 +27,8 @@ class InstancesModel(QtGui.QStandardItemModel): def set_report(self, report_item): root_item = self.invisibleRootItem() - if root_item.rowCount(): + if root_item.rowCount() > 0: root_item.removeRows(0, root_item.rowCount()) - self._items_by_id.clear() self._plugin_items_by_id.clear() if not report_item: From 53c593d41dae53d25c879ebcb4965c558278737b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Dec 2023 18:53:09 +0100 Subject: [PATCH 153/291] removed unused import --- openpype/tools/publisher/control.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 23456579ca..47e374edf2 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -4,7 +4,6 @@ import logging import traceback import collections import uuid -import datetime import tempfile import shutil import inspect From adf9cb3e2c0054f34b0f7b10ac9cfaa4291916f3 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 30 Dec 2023 03:25:08 +0000 Subject: [PATCH 154/291] [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 93fdf432b3..550bdb70c7 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.2-nightly.4" +__version__ = "3.18.2-nightly.5" From 8ea81950e2341692dc3d76a6acdd5c5c0ddf96b6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 30 Dec 2023 03:25:40 +0000 Subject: [PATCH 155/291] 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 a691e021fd..f345829356 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.18.2-nightly.5 - 3.18.2-nightly.4 - 3.18.2-nightly.3 - 3.18.2-nightly.2 @@ -134,7 +135,6 @@ body: - 3.15.5 - 3.15.5-nightly.2 - 3.15.5-nightly.1 - - 3.15.4 validations: required: true - type: dropdown From ebc4f1467d5c4cef02031ceda4243a683c4c22ed Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Jan 2024 16:20:58 +0800 Subject: [PATCH 156/291] allows users to set up the scene unit scale in Max with OP/AYON settings /refactor fbx extractors --- openpype/hosts/max/api/lib.py | 30 ++++++++++ openpype/hosts/max/api/menu.py | 8 +++ openpype/hosts/max/api/pipeline.py | 58 ++++++++++++++----- .../max/plugins/publish/extract_camera_fbx.py | 55 ------------------ .../{extract_model_fbx.py => extract_fbx.py} | 40 ++++++++++--- .../defaults/project_settings/max.json | 4 ++ .../projects_schema/schema_project_max.json | 31 ++++++++++ server_addon/max/server/settings/main.py | 30 ++++++++++ server_addon/max/server/version.py | 2 +- 9 files changed, 180 insertions(+), 78 deletions(-) delete mode 100644 openpype/hosts/max/plugins/publish/extract_camera_fbx.py rename openpype/hosts/max/plugins/publish/{extract_model_fbx.py => extract_fbx.py} (67%) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8531233bb2..e98d4632ba 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -294,6 +294,35 @@ def reset_frame_range(fps: bool = True): frame_range["frameStartHandle"], frame_range["frameEndHandle"]) +def reset_unit_scale(): + """Apply the unit scale setting to 3dsMax + """ + project_name = get_current_project_name() + settings = get_project_settings(project_name).get("max") + unit_scale_setting = settings.get("unit_scale_settings") + if unit_scale_setting: + scene_scale = unit_scale_setting["scene_unit_scale"] + rt.units.SystemType = rt.Name(scene_scale) + +def convert_unit_scale(): + """Convert system unit scale in 3dsMax + for fbx export + + Returns: + str: unit scale + """ + unit_scale_dict = { + "inches": "in", + "feet": "ft", + "miles": "mi", + "millimeters": "mm", + "centimeters": "cm", + "meters": "m", + "kilometers": "km" + } + current_unit_scale = rt.Execute("units.SystemType as string") + return unit_scale_dict[current_unit_scale] + def set_context_setting(): """Apply the project settings from the project definition @@ -310,6 +339,7 @@ def set_context_setting(): reset_scene_resolution() reset_frame_range() reset_colorspace() + reset_unit_scale() def get_max_version(): diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index caaa3e3730..9bdb6bd7ce 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -124,6 +124,10 @@ class OpenPypeMenu(object): colorspace_action.triggered.connect(self.colorspace_callback) openpype_menu.addAction(colorspace_action) + unit_scale_action = QtWidgets.QAction("Set Unit Scale", openpype_menu) + unit_scale_action.triggered.connect(self.unit_scale_callback) + openpype_menu.addAction(unit_scale_action) + return openpype_menu def load_callback(self): @@ -157,3 +161,7 @@ class OpenPypeMenu(object): def colorspace_callback(self): """Callback to reset colorspace""" return lib.reset_colorspace() + + def unit_scale_callback(self): + """Callback to reset unit scale""" + return lib.reset_unit_scale() diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index d0ae854dc8..ea4ff35557 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -3,6 +3,7 @@ import os import logging from operator import attrgetter +from functools import partial import json @@ -13,6 +14,10 @@ from openpype.pipeline import ( register_loader_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.lib import ( + register_event_callback, + emit_event +) from openpype.hosts.max.api.menu import OpenPypeMenu from openpype.hosts.max.api import lib from openpype.hosts.max.api.plugin import MS_CUSTOM_ATTRIB @@ -46,19 +51,14 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - # self._register_callbacks() + self._register_callbacks() self.menu = OpenPypeMenu() + register_event_callback( + "init", self._deferred_menu_creation) self._has_been_setup = True - - def context_setting(): - return lib.set_context_setting() - - rt.callbacks.addScript(rt.Name('systemPostNew'), - context_setting) - - rt.callbacks.addScript(rt.Name('filePostOpen'), - lib.check_colorspace) + register_event_callback("open", on_open) + register_event_callback("new", on_new) def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? @@ -83,11 +83,28 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return ls() def _register_callbacks(self): - rt.callbacks.removeScripts(id=rt.name("OpenPypeCallbacks")) - - rt.callbacks.addScript( + unique_id = rt.Name("openpype_callbacks") + for handler, event in self._op_events.copy().items(): + if event is None: + continue + try: + rt.callbacks.removeScripts(id=unique_id) + self._op_events[handler] = None + except RuntimeError as exc: + self.log.info(exc) + #self._deferred_menu_creation + self._op_events["init"] = rt.callbacks.addScript( rt.Name("postLoadingMenus"), - self._deferred_menu_creation, id=rt.Name('OpenPypeCallbacks')) + partial(_emit_event_notification_param, "init"), + id=unique_id) + self._op_events["new"] = rt.callbacks.addScript( + rt.Name('systemPostNew'), + partial(_emit_event_notification_param, "new"), + id=unique_id) + self._op_events["open"] = rt.callbacks.addScript( + rt.Name('filePostOpen'), + partial(_emit_event_notification_param, "open"), + id=unique_id) def _deferred_menu_creation(self): self.log.info("Building menu ...") @@ -144,6 +161,19 @@ attributes "OpenPypeContext" rt.saveMaxFile(dst_path) +def _emit_event_notification_param(event): + notification = rt.callbacks.notificationParam() + emit_event(event, {"notificationParam": notification}) + + +def on_open(): + return lib.check_colorspace() + + +def on_new(): + return lib.set_context_setting() + + def ls() -> list: """Get all OpenPype instances.""" objs = rt.objects diff --git a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py b/openpype/hosts/max/plugins/publish/extract_camera_fbx.py deleted file mode 100644 index 4b5631b05f..0000000000 --- a/openpype/hosts/max/plugins/publish/extract_camera_fbx.py +++ /dev/null @@ -1,55 +0,0 @@ -import os - -import pyblish.api -from pymxs import runtime as rt - -from openpype.hosts.max.api import maintained_selection -from openpype.pipeline import OptionalPyblishPluginMixin, publish - - -class ExtractCameraFbx(publish.Extractor, OptionalPyblishPluginMixin): - """Extract Camera with FbxExporter.""" - - order = pyblish.api.ExtractorOrder - 0.2 - label = "Extract Fbx Camera" - hosts = ["max"] - families = ["camera"] - optional = True - - def process(self, instance): - if not self.is_active(instance.data): - return - - stagingdir = self.staging_dir(instance) - filename = "{name}.fbx".format(**instance.data) - - filepath = os.path.join(stagingdir, filename) - 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 - node_list = instance.data["members"] - rt.Select(node_list) - rt.ExportFile( - filepath, - rt.Name("noPrompt"), - selectedOnly=True, - using=rt.FBXEXP, - ) - - self.log.info("Performing Extraction ...") - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - "name": "fbx", - "ext": "fbx", - "files": filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - self.log.info(f"Extracted instance '{instance.name}' to: {filepath}") diff --git a/openpype/hosts/max/plugins/publish/extract_model_fbx.py b/openpype/hosts/max/plugins/publish/extract_fbx.py similarity index 67% rename from openpype/hosts/max/plugins/publish/extract_model_fbx.py rename to openpype/hosts/max/plugins/publish/extract_fbx.py index 6c42fd5364..d41f3e40fc 100644 --- a/openpype/hosts/max/plugins/publish/extract_model_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_fbx.py @@ -3,6 +3,7 @@ import pyblish.api from openpype.pipeline import publish, OptionalPyblishPluginMixin from pymxs import runtime as rt from openpype.hosts.max.api import maintained_selection +from openpype.hosts.max.api.lib import convert_unit_scale class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): @@ -23,14 +24,7 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): stagingdir = self.staging_dir(instance) filename = "{name}.fbx".format(**instance.data) filepath = os.path.join(stagingdir, filename) - - 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) + self._set_fbx_attributes() with maintained_selection(): # select and export @@ -56,3 +50,33 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): self.log.info( "Extracted instance '%s' to: %s" % (instance.name, filepath) ) + + def _set_fbx_attributes(self): + unit_scale = convert_unit_scale() + 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) + if unit_scale: + rt.FBXExporterSetParam("ConvertUnit", unit_scale) + +class ExtractCameraFbx(ExtractModelFbx): + """Extract Camera with FbxExporter.""" + + order = pyblish.api.ExtractorOrder - 0.2 + label = "Extract Fbx Camera" + families = ["camera"] + optional = True + + def _set_fbx_attributes(self): + unit_scale = convert_unit_scale() + rt.FBXExporterSetParam("Animation", True) + rt.FBXExporterSetParam("Cameras", True) + rt.FBXExporterSetParam("AxisConversionMethod", "Animation") + rt.FBXExporterSetParam("UpAxis", "Y") + rt.FBXExporterSetParam("Preserveinstances", True) + if unit_scale: + rt.FBXExporterSetParam("ConvertUnit", unit_scale) diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 19c9d10496..1b574dc4d3 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -1,4 +1,8 @@ { + "unit_scale_settings": { + "enabled": true, + "scene_unit_scale": "Inches" + }, "imageio": { "activate_host_color_management": true, "ocio_config": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 78cca357a3..1df14c04e1 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -5,6 +5,37 @@ "label": "Max", "is_file": true, "children": [ + { + "key": "unit_scale_settings", + "type": "dict", + "label": "Set Unit Scale", + "collapsible": true, + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "key": "scene_unit_scale", + "label": "Scene Unit Scale", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"Inches": "in"}, + {"Feet": "ft"}, + {"Miles": "mi"}, + {"Millimeters": "mm"}, + {"Centimeters": "cm"}, + {"Meters": "m"}, + {"Kilometers": "km"} + ] + } + ] + }, { "key": "imageio", "type": "dict", diff --git a/server_addon/max/server/settings/main.py b/server_addon/max/server/settings/main.py index ea6a11915a..94dee3e55c 100644 --- a/server_addon/max/server/settings/main.py +++ b/server_addon/max/server/settings/main.py @@ -12,6 +12,28 @@ from .publishers import ( ) +def unit_scale_enum(): + """Return enumerator for scene unit scale.""" + return [ + {"label": "in", "value": "Inches"}, + {"label": "ft", "value": "Feet"}, + {"label": "mi", "value": "Miles"}, + {"label": "mm", "value": "Millimeters"}, + {"label": "cm", "value": "Centimeters"}, + {"label": "m", "value": "Meters"}, + {"label": "km", "value": "Kilometers"} + ] + + +class UnitScaleSettings(BaseSettingsModel): + enabled: bool = Field(True, title="Enabled") + scene_unit_scale: str = Field( + "Centimeters", + title="Scene Unit Scale", + enum_resolver=unit_scale_enum + ) + + class PRTAttributesModel(BaseSettingsModel): _layout = "compact" name: str = Field(title="Name") @@ -24,6 +46,10 @@ class PointCloudSettings(BaseSettingsModel): class MaxSettings(BaseSettingsModel): + unit_scale_settings: UnitScaleSettings = Field( + default_factory=UnitScaleSettings, + title="Set Unit Scale" + ) imageio: ImageIOSettings = Field( default_factory=ImageIOSettings, title="Color Management (ImageIO)" @@ -46,6 +72,10 @@ class MaxSettings(BaseSettingsModel): DEFAULT_VALUES = { + "unit_scale_settings": { + "enabled": True, + "scene_unit_scale": "cm" + }, "RenderSettings": DEFAULT_RENDER_SETTINGS, "CreateReview": DEFAULT_CREATE_REVIEW_SETTINGS, "PointCloud": { diff --git a/server_addon/max/server/version.py b/server_addon/max/server/version.py index ae7362549b..bbab0242f6 100644 --- a/server_addon/max/server/version.py +++ b/server_addon/max/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" From 1795290f4d18d7ad259f1bec889bbc878d80b448 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Jan 2024 17:17:40 +0800 Subject: [PATCH 157/291] hound --- openpype/hosts/max/api/lib.py | 1 + openpype/hosts/max/api/pipeline.py | 2 +- openpype/hosts/max/plugins/publish/extract_fbx.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index e98d4632ba..9b3d3fda98 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -304,6 +304,7 @@ def reset_unit_scale(): scene_scale = unit_scale_setting["scene_unit_scale"] rt.units.SystemType = rt.Name(scene_scale) + def convert_unit_scale(): """Convert system unit scale in 3dsMax for fbx export diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index ea4ff35557..98895b858e 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -92,7 +92,7 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): self._op_events[handler] = None except RuntimeError as exc: self.log.info(exc) - #self._deferred_menu_creation + self._op_events["init"] = rt.callbacks.addScript( rt.Name("postLoadingMenus"), partial(_emit_event_notification_param, "init"), diff --git a/openpype/hosts/max/plugins/publish/extract_fbx.py b/openpype/hosts/max/plugins/publish/extract_fbx.py index d41f3e40fc..7454cd08d1 100644 --- a/openpype/hosts/max/plugins/publish/extract_fbx.py +++ b/openpype/hosts/max/plugins/publish/extract_fbx.py @@ -63,6 +63,7 @@ class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin): if unit_scale: rt.FBXExporterSetParam("ConvertUnit", unit_scale) + class ExtractCameraFbx(ExtractModelFbx): """Extract Camera with FbxExporter.""" From e107526bbd8e3daf9cfed36de3d06c5db3cb078b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 2 Jan 2024 11:59:18 +0100 Subject: [PATCH 158/291] fix action to be able to run --- .../ftrack/event_handlers_user/action_djvview.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_djvview.py b/openpype/modules/ftrack/event_handlers_user/action_djvview.py index 334519b4bb..bd9cd62e49 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_djvview.py +++ b/openpype/modules/ftrack/event_handlers_user/action_djvview.py @@ -190,12 +190,12 @@ class DJVViewAction(BaseAction): """Callback method for DJVView action.""" # Launching application - event_data = event["data"] - if "values" not in event_data: + event_values = event["data"].get("value") + if not event_values: return - djv_app_name = event_data["djv_app_name"] - app = self.applicaion_manager.applications.get(djv_app_name) + djv_app_name = event_values["djv_app_name"] + app = self.application_manager.applications.get(djv_app_name) executable = None if app is not None: executable = app.find_executable() @@ -206,11 +206,11 @@ class DJVViewAction(BaseAction): "message": "Couldn't find DJV executable." } - filpath = os.path.normpath(event_data["values"]["path"]) + filpath = os.path.normpath(event_values["path"]) cmd = [ # DJV path - executable, + str(executable), # PATH TO COMPONENT filpath ] From 1e485614bdd01169627b2a05f2005b4d1ef0ade6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 2 Jan 2024 11:59:45 +0100 Subject: [PATCH 159/291] keep process in momery for some time --- .../modules/ftrack/event_handlers_user/action_djvview.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_djvview.py b/openpype/modules/ftrack/event_handlers_user/action_djvview.py index bd9cd62e49..5cc056cd91 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_djvview.py +++ b/openpype/modules/ftrack/event_handlers_user/action_djvview.py @@ -217,7 +217,10 @@ class DJVViewAction(BaseAction): try: # Run DJV with these commands - subprocess.Popen(cmd) + _process = subprocess.Popen(cmd) + # Keep process in memory for some time + time.sleep(0.1) + except FileNotFoundError: return { "success": False, From 58a36c541462eda406f90e5d8d0174a689d3ec3b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 2 Jan 2024 12:00:03 +0100 Subject: [PATCH 160/291] formatting changes --- .../event_handlers_user/action_djvview.py | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_djvview.py b/openpype/modules/ftrack/event_handlers_user/action_djvview.py index 5cc056cd91..5778f0d26e 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_djvview.py +++ b/openpype/modules/ftrack/event_handlers_user/action_djvview.py @@ -60,7 +60,7 @@ class DJVViewAction(BaseAction): return False def interface(self, session, entities, event): - if event['data'].get('values', {}): + if event["data"].get("values", {}): return entity = entities[0] @@ -70,32 +70,32 @@ class DJVViewAction(BaseAction): if entity_type == "assetversion": if ( entity[ - 'components' - ][0]['file_type'][1:] in self.allowed_types + "components" + ][0]["file_type"][1:] in self.allowed_types ): versions.append(entity) else: master_entity = entity if entity_type == "task": - master_entity = entity['parent'] + master_entity = entity["parent"] - for asset in master_entity['assets']: - for version in asset['versions']: + for asset in master_entity["assets"]: + for version in asset["versions"]: # Get only AssetVersion of selected task if ( entity_type == "task" and - version['task']['id'] != entity['id'] + version["task"]["id"] != entity["id"] ): continue # Get only components with allowed type - filetype = version['components'][0]['file_type'] + filetype = version["components"][0]["file_type"] if filetype[1:] in self.allowed_types: versions.append(version) if len(versions) < 1: return { - 'success': False, - 'message': 'There are no Asset Versions to open.' + "success": False, + "message": "There are no Asset Versions to open." } # TODO sort them (somehow?) @@ -134,57 +134,57 @@ class DJVViewAction(BaseAction): last_available = None select_value = None for version in versions: - for component in version['components']: + for component in version["components"]: label = base_label.format( - str(version['version']).zfill(3), - version['asset']['type']['name'], - component['name'] + str(version["version"]).zfill(3), + version["asset"]["type"]["name"], + component["name"] ) try: location = component[ - 'component_locations' - ][0]['location'] + "component_locations" + ][0]["location"] file_path = location.get_filesystem_path(component) except Exception: file_path = component[ - 'component_locations' - ][0]['resource_identifier'] + "component_locations" + ][0]["resource_identifier"] if os.path.isdir(os.path.dirname(file_path)): last_available = file_path - if component['name'] == default_component: + if component["name"] == default_component: select_value = file_path version_items.append( - {'label': label, 'value': file_path} + {"label": label, "value": file_path} ) if len(version_items) == 0: return { - 'success': False, - 'message': ( - 'There are no Asset Versions with accessible path.' + "success": False, + "message": ( + "There are no Asset Versions with accessible path." ) } item = { - 'label': 'Items to view', - 'type': 'enumerator', - 'name': 'path', - 'data': sorted( + "label": "Items to view", + "type": "enumerator", + "name": "path", + "data": sorted( version_items, - key=itemgetter('label'), + key=itemgetter("label"), reverse=True ) } if select_value is not None: - item['value'] = select_value + item["value"] = select_value else: - item['value'] = last_available + item["value"] = last_available items.append(item) - return {'items': items} + return {"items": items} def launch(self, session, entities, event): """Callback method for DJVView action.""" From 480c509f8ea4ac4baa835dcb5ae52cb428ad3cbf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 2 Jan 2024 15:36:51 +0100 Subject: [PATCH 161/291] fix used key --- openpype/modules/ftrack/event_handlers_user/action_djvview.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_djvview.py b/openpype/modules/ftrack/event_handlers_user/action_djvview.py index 5778f0d26e..cc37faacf2 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_djvview.py +++ b/openpype/modules/ftrack/event_handlers_user/action_djvview.py @@ -13,7 +13,7 @@ class DJVViewAction(BaseAction): description = "DJV View Launcher" icon = statics_icon("app_icons", "djvView.png") - type = 'Application' + type = "Application" allowed_types = [ "cin", "dpx", "avi", "dv", "gif", "flv", "mkv", "mov", "mpg", "mpeg", @@ -190,7 +190,7 @@ class DJVViewAction(BaseAction): """Callback method for DJVView action.""" # Launching application - event_values = event["data"].get("value") + event_values = event["data"].get("values") if not event_values: return From cf29a532d2ace421651d39e2104c03014b9a8aff Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Jan 2024 22:51:51 +0800 Subject: [PATCH 162/291] make sure the texture only loaded when the texture is being enabled --- openpype/hosts/maya/api/lib.py | 12 ++++++------ .../settings/defaults/project_settings/maya.json | 2 +- .../projects_schema/schemas/schema_maya_capture.json | 8 ++++---- .../maya/server/settings/publish_playblast.py | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 1a8a80f224..6a0ccbdbfa 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -227,19 +227,19 @@ def render_capture_preset(preset): ) preset = copy.deepcopy(preset) # not supported by `capture` so we pop it off of the preset - reload_textures = preset["viewport_options"].pop("reloadTextures", True) + reload_textures = preset["viewport_options"].get("loadTextures") panel = preset.pop("panel") with ExitStack() as stack: stack.enter_context(maintained_time()) stack.enter_context(panel_camera(panel, preset["camera"])) stack.enter_context(viewport_default_options(panel, preset)) - if preset["viewport_options"].get("textures"): + if reload_textures: # Force immediate texture loading when to ensure # all textures have loaded before the playblast starts - stack.enter_context(material_loading_mode("immediate")) - if reload_textures: - # Regenerate all UDIM tiles previews - reload_all_udim_tile_previews() + stack.enter_context(material_loading_mode(mode="immediate")) + # Regenerate all UDIM tiles previews + reload_all_udim_tile_previews() + preset["viewport_options"].pop("loadTextures") path = capture.capture(log=self.log, **preset) return path diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 1778530311..8136af8c73 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1289,7 +1289,7 @@ "twoSidedLighting": true, "lineAAEnable": true, "multiSample": 8, - "reloadTextures": false, + "loadTextures": false, "useDefaultMaterial": false, "wireframeOnShaded": false, "xray": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 1aa5b3d2e4..76ad9a3ba2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -238,8 +238,8 @@ }, { "type": "boolean", - "key": "reloadTextures", - "label": "Reload Textures" + "key": "loadTextures", + "label": "Load Textures" }, { "type": "boolean", @@ -915,8 +915,8 @@ }, { "type": "boolean", - "key": "reloadTextures", - "label": "Reload Textures", + "key": "loadTextures", + "label": "Load Textures", "default": false }, { diff --git a/server_addon/maya/server/settings/publish_playblast.py b/server_addon/maya/server/settings/publish_playblast.py index 205f0eb847..db92c80db7 100644 --- a/server_addon/maya/server/settings/publish_playblast.py +++ b/server_addon/maya/server/settings/publish_playblast.py @@ -108,7 +108,7 @@ class ViewportOptionsSetting(BaseSettingsModel): True, title="Enable Anti-Aliasing", section="Anti-Aliasing" ) multiSample: int = Field(8, title="Anti Aliasing Samples") - reloadTextures: bool = Field(False, title="Reload Textures") + loadTextures: bool = Field(False, title="Reload Textures") useDefaultMaterial: bool = Field(False, title="Use Default Material") wireframeOnShaded: bool = Field(False, title="Wireframe On Shaded") xray: bool = Field(False, title="X-Ray") @@ -303,7 +303,7 @@ DEFAULT_PLAYBLAST_SETTING = { "twoSidedLighting": True, "lineAAEnable": True, "multiSample": 8, - "reloadTextures": False, + "loadTextures": False, "useDefaultMaterial": False, "wireframeOnShaded": False, "xray": False, From d351e5f1745f9875b496cbf873eec8b3c0e61ab1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 2 Jan 2024 22:54:22 +0800 Subject: [PATCH 163/291] renamed reload Textures to Load Textures --- server_addon/maya/server/settings/publish_playblast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/maya/server/settings/publish_playblast.py b/server_addon/maya/server/settings/publish_playblast.py index db92c80db7..0abc9f7110 100644 --- a/server_addon/maya/server/settings/publish_playblast.py +++ b/server_addon/maya/server/settings/publish_playblast.py @@ -108,7 +108,7 @@ class ViewportOptionsSetting(BaseSettingsModel): True, title="Enable Anti-Aliasing", section="Anti-Aliasing" ) multiSample: int = Field(8, title="Anti Aliasing Samples") - loadTextures: bool = Field(False, title="Reload Textures") + loadTextures: bool = Field(False, title="Load Textures") useDefaultMaterial: bool = Field(False, title="Use Default Material") wireframeOnShaded: bool = Field(False, title="Wireframe On Shaded") xray: bool = Field(False, title="X-Ray") From bea3c78079e2caa27876873beabc83efea9b6d07 Mon Sep 17 00:00:00 2001 From: erictsaivfx Date: Tue, 2 Jan 2024 09:31:10 -0800 Subject: [PATCH 164/291] fix arrow to timezone typo --- openpype/tools/ayon_workfiles/models/workfiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/ayon_workfiles/models/workfiles.py b/openpype/tools/ayon_workfiles/models/workfiles.py index d74a8e164d..f9f910ac8a 100644 --- a/openpype/tools/ayon_workfiles/models/workfiles.py +++ b/openpype/tools/ayon_workfiles/models/workfiles.py @@ -606,7 +606,7 @@ class PublishWorkfilesModel: print("Failed to format workfile path: {}".format(exc)) dirpath, filename = os.path.split(workfile_path) - created_at = arrow.get(repre_entity["createdAt"].to("local")) + created_at = arrow.get(repre_entity["createdAt"]).to("local") return FileItem( dirpath, filename, From c4ee2f785849db86db967c54d4f256946fbb093a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 3 Jan 2024 03:26:06 +0000 Subject: [PATCH 165/291] [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 550bdb70c7..6a8e6f0d95 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.2-nightly.5" +__version__ = "3.18.2-nightly.6" From 58909f2b4d90fc6165142014ad59825bad550eda Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Jan 2024 03:26:39 +0000 Subject: [PATCH 166/291] 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 f345829356..3471c32430 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.18.2-nightly.6 - 3.18.2-nightly.5 - 3.18.2-nightly.4 - 3.18.2-nightly.3 @@ -134,7 +135,6 @@ body: - 3.15.6-nightly.1 - 3.15.5 - 3.15.5-nightly.2 - - 3.15.5-nightly.1 validations: required: true - type: dropdown From 379674d7931d4e479b157443757465850c582976 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 Jan 2024 15:27:09 +0800 Subject: [PATCH 167/291] remove the condition with the deprecated environment variable in AYON --- openpype/hosts/maya/api/lib.py | 15 ++++++--------- .../maya/plugins/publish/extract_thumbnail.py | 2 +- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 6a0ccbdbfa..394f92ed42 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -218,16 +218,14 @@ def render_capture_preset(preset): refresh_frame_int = int(cmds.playbackOptions(query=True, minTime=True)) cmds.currentTime(refresh_frame_int - 1, edit=True) cmds.currentTime(refresh_frame_int, edit=True) - - if os.environ.get("OPENPYPE_DEBUG") == "1": - log.debug( - "Using preset: {}".format( - json.dumps(preset, indent=4, sort_keys=True) - ) + log.debug( + "Using preset: {}".format( + json.dumps(preset, indent=4, sort_keys=True) ) + ) preset = copy.deepcopy(preset) # not supported by `capture` so we pop it off of the preset - reload_textures = preset["viewport_options"].get("loadTextures") + reload_textures = preset["viewport_options"].pop("loadTextures", False) panel = preset.pop("panel") with ExitStack() as stack: stack.enter_context(maintained_time()) @@ -239,7 +237,6 @@ def render_capture_preset(preset): stack.enter_context(material_loading_mode(mode="immediate")) # Regenerate all UDIM tiles previews reload_all_udim_tile_previews() - preset["viewport_options"].pop("loadTextures") path = capture.capture(log=self.log, **preset) return path @@ -364,7 +361,7 @@ def viewport_default_options(panel, preset): viewport_defaults[key] = cmds.modelEditor( panel, query=True, **{key: True} ) - if preset["viewport_options"][key]: + if preset["viewport_options"].get(key): cmds.modelEditor( panel, edit=True, **{key: True} ) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 08f061985e..28362b355c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -34,7 +34,7 @@ class ExtractThumbnail(publish.Extractor): # Create temp directory for thumbnail # - this is to avoid "override" of source file - dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_") + dst_staging = tempfile.mkdtemp(prefix="pyblish_tmp_thumbnail") self.log.debug( "Create temp directory {} for thumbnail".format(dst_staging) ) From 0734682faad9cfb000332af5de7f1b8524c9ae06 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 Jan 2024 15:36:27 +0800 Subject: [PATCH 168/291] code tweaks based on Oscar's comment --- openpype/hosts/max/api/lib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 9b3d3fda98..48b32ad6af 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -299,9 +299,9 @@ def reset_unit_scale(): """ project_name = get_current_project_name() settings = get_project_settings(project_name).get("max") - unit_scale_setting = settings.get("unit_scale_settings") - if unit_scale_setting: - scene_scale = unit_scale_setting["scene_unit_scale"] + scene_scale = settings.get("unit_scale_settings", + {}).get("scene_unit_scale") + if scene_scale: rt.units.SystemType = rt.Name(scene_scale) From e5784320146e1b95af59b1381bae18b08682b5d6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 Jan 2024 17:42:24 +0800 Subject: [PATCH 169/291] using metric types when the unit type enabled instead of using metric types --- openpype/hosts/max/api/lib.py | 11 +++++------ openpype/settings/defaults/project_settings/max.json | 2 +- .../schemas/projects_schema/schema_project_max.json | 3 --- server_addon/max/server/settings/main.py | 5 +---- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 48b32ad6af..74b53d6426 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -302,8 +302,10 @@ def reset_unit_scale(): scene_scale = settings.get("unit_scale_settings", {}).get("scene_unit_scale") if scene_scale: - rt.units.SystemType = rt.Name(scene_scale) - + rt.units.DisplayType = rt.Name("Metric") + rt.units.MetricType = rt.Name(scene_scale) + else: + rt.units.DisplayType = rt.Name("Generic") def convert_unit_scale(): """Convert system unit scale in 3dsMax @@ -313,15 +315,12 @@ def convert_unit_scale(): str: unit scale """ unit_scale_dict = { - "inches": "in", - "feet": "ft", - "miles": "mi", "millimeters": "mm", "centimeters": "cm", "meters": "m", "kilometers": "km" } - current_unit_scale = rt.Execute("units.SystemType as string") + current_unit_scale = rt.Execute("units.MetricType as string") return unit_scale_dict[current_unit_scale] def set_context_setting(): diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 1b574dc4d3..d1610610dc 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -1,7 +1,7 @@ { "unit_scale_settings": { "enabled": true, - "scene_unit_scale": "Inches" + "scene_unit_scale": "Meters" }, "imageio": { "activate_host_color_management": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 1df14c04e1..e4d4d40ce7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -25,9 +25,6 @@ "multiselection": false, "defaults": "exr", "enum_items": [ - {"Inches": "in"}, - {"Feet": "ft"}, - {"Miles": "mi"}, {"Millimeters": "mm"}, {"Centimeters": "cm"}, {"Meters": "m"}, diff --git a/server_addon/max/server/settings/main.py b/server_addon/max/server/settings/main.py index 94dee3e55c..582226eb62 100644 --- a/server_addon/max/server/settings/main.py +++ b/server_addon/max/server/settings/main.py @@ -15,9 +15,6 @@ from .publishers import ( def unit_scale_enum(): """Return enumerator for scene unit scale.""" return [ - {"label": "in", "value": "Inches"}, - {"label": "ft", "value": "Feet"}, - {"label": "mi", "value": "Miles"}, {"label": "mm", "value": "Millimeters"}, {"label": "cm", "value": "Centimeters"}, {"label": "m", "value": "Meters"}, @@ -74,7 +71,7 @@ class MaxSettings(BaseSettingsModel): DEFAULT_VALUES = { "unit_scale_settings": { "enabled": True, - "scene_unit_scale": "cm" + "scene_unit_scale": "m" }, "RenderSettings": DEFAULT_RENDER_SETTINGS, "CreateReview": DEFAULT_CREATE_REVIEW_SETTINGS, From 791ca6782e41261ca63be0454cc3cc53cc54cc71 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 Jan 2024 17:45:43 +0800 Subject: [PATCH 170/291] hound --- openpype/hosts/max/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 74b53d6426..4adb30ab65 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -323,6 +323,7 @@ def convert_unit_scale(): current_unit_scale = rt.Execute("units.MetricType as string") return unit_scale_dict[current_unit_scale] + def set_context_setting(): """Apply the project settings from the project definition From d9f8b9e0f212f61648da3c5aeb444c4f976e6bf3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 Jan 2024 17:50:48 +0800 Subject: [PATCH 171/291] hound --- openpype/hosts/max/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 4adb30ab65..e2d8d9c55f 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -307,6 +307,7 @@ def reset_unit_scale(): else: rt.units.DisplayType = rt.Name("Generic") + def convert_unit_scale(): """Convert system unit scale in 3dsMax for fbx export From e4e6503017c4b1333129df4c16208fd239a7f779 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Wed, 3 Jan 2024 12:07:32 +0100 Subject: [PATCH 172/291] Testing: Release Maya/Deadline job from pending when testing. (#5988) * Release job from pending when testing. * Removed render instance This test was created as simple model and workfile publish, without Deadline rendering. Cleaned up render elements. * Revert changes in submit publish plugin --------- Co-authored-by: kalisp --- .../modules/deadline/plugins/publish/submit_maya_deadline.py | 4 ++-- .../modules/deadline/plugins/publish/submit_publish_job.py | 4 +++- .../work/test_task/test_project_test_asset_test_task_v001.ma | 4 ++-- .../work/test_task/test_project_test_asset_test_task_v002.ma | 4 ++-- .../input/workfile/test_project_test_asset_test_task_v001.ma | 4 ++-- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py index 26a605a744..5591db151a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_maya_deadline.py @@ -231,7 +231,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1" # Adding file dependencies. - if self.asset_dependencies: + if not bool(os.environ.get("IS_TEST")) and self.asset_dependencies: dependencies = instance.context.data["fileDependencies"] for dependency in dependencies: job_info.AssetDependency += dependency @@ -570,7 +570,7 @@ class MayaSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info = copy.deepcopy(self.job_info) - if self.asset_dependencies: + if not bool(os.environ.get("IS_TEST")) and self.asset_dependencies: # Asset dependency to wait for at least the scene file to sync. job_info.AssetDependency += self.scene_path diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 228aa3ec81..04ce2b3433 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -297,7 +297,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, job_index)] = assembly_id # noqa: E501 job_index += 1 elif instance.data.get("bakingSubmissionJobs"): - self.log.info("Adding baking submission jobs as dependencies...") + self.log.info( + "Adding baking submission jobs as dependencies..." + ) job_index = 0 for assembly_id in instance.data["bakingSubmissionJobs"]: payload["JobInfo"]["JobDependency{}".format( diff --git a/tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v001.ma b/tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v001.ma index 2cc87c2f48..8b90e987de 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v001.ma +++ b/tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v001.ma @@ -185,7 +185,7 @@ createNode objectSet -n "modelMain"; addAttr -ci true -sn "attrPrefix" -ln "attrPrefix" -dt "string"; addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string"; addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string"; - addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" + addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" -dt "string"; setAttr ".ihi" 0; setAttr ".cbId" -type "string" "60df31e2be2b48bd3695c056:7364ea6776c9"; @@ -296,7 +296,7 @@ createNode objectSet -n "workfileMain"; addAttr -ci true -sn "task" -ln "task" -dt "string"; addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string"; addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string"; - addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" + addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" -dt "string"; setAttr ".ihi" 0; setAttr ".hio" yes; diff --git a/tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v002.ma b/tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v002.ma index 6bd334466a..f2906058cf 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v002.ma +++ b/tests/integration/hosts/maya/test_publish_in_maya/expected/test_project/test_asset/work/test_task/test_project_test_asset_test_task_v002.ma @@ -185,7 +185,7 @@ createNode objectSet -n "modelMain"; addAttr -ci true -sn "attrPrefix" -ln "attrPrefix" -dt "string"; addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string"; addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string"; - addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" + addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" -dt "string"; setAttr ".ihi" 0; setAttr ".cbId" -type "string" "60df31e2be2b48bd3695c056:7364ea6776c9"; @@ -296,7 +296,7 @@ createNode objectSet -n "workfileMain"; addAttr -ci true -sn "task" -ln "task" -dt "string"; addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string"; addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string"; - addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" + addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" -dt "string"; setAttr ".ihi" 0; setAttr ".hio" yes; diff --git a/tests/integration/hosts/maya/test_publish_in_maya/input/workfile/test_project_test_asset_test_task_v001.ma b/tests/integration/hosts/maya/test_publish_in_maya/input/workfile/test_project_test_asset_test_task_v001.ma index 2cc87c2f48..8b90e987de 100644 --- a/tests/integration/hosts/maya/test_publish_in_maya/input/workfile/test_project_test_asset_test_task_v001.ma +++ b/tests/integration/hosts/maya/test_publish_in_maya/input/workfile/test_project_test_asset_test_task_v001.ma @@ -185,7 +185,7 @@ createNode objectSet -n "modelMain"; addAttr -ci true -sn "attrPrefix" -ln "attrPrefix" -dt "string"; addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string"; addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string"; - addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" + addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" -dt "string"; setAttr ".ihi" 0; setAttr ".cbId" -type "string" "60df31e2be2b48bd3695c056:7364ea6776c9"; @@ -296,7 +296,7 @@ createNode objectSet -n "workfileMain"; addAttr -ci true -sn "task" -ln "task" -dt "string"; addAttr -ci true -sn "publish_attributes" -ln "publish_attributes" -dt "string"; addAttr -ci true -sn "creator_attributes" -ln "creator_attributes" -dt "string"; - addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" + addAttr -ci true -sn "__creator_attributes_keys" -ln "__creator_attributes_keys" -dt "string"; setAttr ".ihi" 0; setAttr ".hio" yes; From 333433057ea2728f1646a20ee602b72c40e5c1ba Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 Jan 2024 21:48:38 +0800 Subject: [PATCH 173/291] cosmetic tweaks regarding to Ondrej's comment --- openpype/hosts/max/api/lib_rendersettings.py | 15 ++++++--------- .../plugins/publish/save_scenes_for_cameras.py | 7 +++---- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 166076f008..be50e296eb 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -80,7 +80,7 @@ class RenderSettings(object): )] except KeyError: aov_separator = "." - output_filename = "{0}..{1}".format(output, img_fmt) + output_filename = f"{output}..{img_fmt}" output_filename = output_filename.replace("{aov_separator}", aov_separator) rt.rendOutputFilename = output_filename @@ -146,13 +146,13 @@ class RenderSettings(object): for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - aov_name = "{0}_{1}..{2}".format(dir, renderpass, ext) + aov_name = f"{dir}_{renderpass}..{ext}" render_elem.SetRenderElementFileName(i, aov_name) def get_render_output(self, container, output_dir): output = os.path.join(output_dir, container) img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - output_filename = "{0}..{1}".format(output, img_fmt) + output_filename = f"{output}..{img_fmt}" return output_filename def get_render_element(self): @@ -181,8 +181,7 @@ class RenderSettings(object): for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - aov_name = "{0}_{1}_{2}..{3}".format( - output, camera, renderpass, img_fmt) + aov_name = f"{output}_{camera}_{renderpass}..{img_fmt}" render_element_list.append(aov_name) return render_element_list @@ -205,8 +204,7 @@ class RenderSettings(object): for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - aov_name = "{0}_{1}_{2}..{3}".format( - directory, camera, renderpass, ext) + aov_name = f"{directory}_{camera}_{renderpass}..{ext}" render_elem.SetRenderElementFileName(i, aov_name) def batch_render_layer(self, container, @@ -224,7 +222,6 @@ class RenderSettings(object): renderlayer = rt.batchRenderMgr.GetView(layer_no) # use camera name as renderlayer name renderlayer.name = cam - renderlayer.outputFilename = "{0}_{1}..{2}".format( - output, cam, img_fmt) + renderlayer.outputFilename = f"{output}_{cam}..{img_fmt}" outputs.append(renderlayer.outputFilename) return outputs diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index 79e9088ac7..c39109417b 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -31,13 +31,12 @@ class SaveScenesForCamera(pyblish.api.InstancePlugin): cameras = instance.data.get("cameras") if not cameras: return - new_folder = "{}_{}".format(current_folder, filename) + new_folder = f"{current_folder}_{filename}" os.makedirs(new_folder, exist_ok=True) for camera in cameras: new_output = RenderSettings().get_batch_render_output(camera) # noqa new_output = new_output.replace("\\", "/") - new_filename = "{}_{}{}".format( - filename, camera, ext) + new_filename = f"{filename}_{camera}{ext}" new_filepath = os.path.join(new_folder, new_filename) new_filepath = new_filepath.replace("\\", "/") camera_scene_files.append(new_filepath) @@ -61,7 +60,7 @@ if render_elem_num > 0: for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - aov_name = directory + "_" + camera + "_" + renderpass + "." + "." + ext # noqa + aov_name = f"{{directory}}_{camera}_{{renderpass}}..{ext}" render_elem.SetRenderElementFileName(i, aov_name) rt.saveMaxFile(new_filepath) """).format(filename=instance.name, From 70062011f7f052f8f918c2a22897a8765cbb1e9e Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 3 Jan 2024 15:14:06 +0000 Subject: [PATCH 174/291] [Automated] Release --- CHANGELOG.md | 317 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 319 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f309d904eb..4a21882008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,323 @@ # Changelog +## [3.18.2](https://github.com/ynput/OpenPype/tree/3.18.2) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.1...3.18.2) + +### **🚀 Enhancements** + + +

+Testing: Release Maya/Deadline job from pending when testing. #5988 + +When testing we wont put the Deadline jobs into pending with dependencies, so the worker can start as soon as possible. + + +___ + +
+ + +
+Max: Tweaks on Extractions for the exporters #5814 + +With this PR +- Suspend Refresh would be introduced in abc & obj extractors for optimization. +- Allow users to choose the custom attributes to be included in abc exports + + +___ + +
+ + +
+Maya: Optional preserve references. #5994 + +Optional preserve references when publishing Maya scenes. + + +___ + +
+ + +
+AYON ftrack: Expect 'ayon' group in custom attributes #6066 + +Expect `ayon` group as one of options to get custom attributes. + + +___ + +
+ + +
+AYON Chore: Remove dependencies related to separated addons #6074 + +Removed dependencies from openpype client pyproject.toml that are already defined by addons which require them. + + +___ + +
+ + +
+Editorial & chore: Stop using pathlib2 #6075 + +Do not use `pathlib2` which is Python 2 backport for `pathlib` module in python 3. + + +___ + +
+ + +
+Traypublisher: Correct validator label #6084 + +Use correct label for Validate filepaths. + + +___ + +
+ + +
+Nuke: Extract Review Intermediate disabled when both Extract Review Mov and Extract Review Intermediate disabled in setting #6089 + +Report in Discord https://discord.com/channels/517362899170230292/563751989075378201/1187874498234556477 + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Bug fix the file from texture node not being collected correctly in Yeti Rig #5990 + +Fix the bug of collect Yeti Rig not being able to get the file parameter(s) from the texture node(s), resulting to the failure of publishing the textures to the resource directory. + + +___ + +
+ + +
+Bug: fix AYON settings for Maya workspace #6069 + +This is changing bug in default AYON setting for Maya workspace, where missing semicolumn caused workspace not being set. This is also syncing default workspace settings to OpenPype + + +___ + +
+ + +
+Refactor colorspace handling in CollectColorspace plugin #6033 + +Traypublisher is now capable set available colorspaces or roles to publishing images sequence or video. This is fix of new implementation where we allowed to use roles in the enumerator selector. + + +___ + +
+ + +
+Bugfix: Houdini render split bugs #6037 + +This PR is a follow up PR to https://github.com/ynput/OpenPype/pull/5420This PR does: +- refactor `get_output_parameter` to what is used to be. +- fix a bug with split render +- rename `exportJob` flag to `split_render` + + +___ + +
+ + +
+Fusion: fix for single frame rendering #6056 + +Fixes publishes of single frame of `render` product type. + + +___ + +
+ + +
+Photoshop: fix layer publish thumbnail missing in loader #6061 + +Thumbnails from any products (either `review` nor separate layer instances) weren't stored in Ayon.This resulted in not showing them in Loader and Server UI. After this PR thumbnails should be shown in the Loader and on the Server (`http://YOUR_AYON_HOSTNAME:5000/projects/YOUR_PROJECT/browser`). + + +___ + +
+ + +
+AYON Chore: Do not use thumbnailSource for thumbnail integration #6063 + +Do not use `thumbnailSource` for thumbnail integration. + + +___ + +
+ + +
+Photoshop: fix creation of .mov #6064 + +Generation of .mov file with 1 frame per published layer was failing. + + +___ + +
+ + +
+Photoshop: fix Collect Color Coded settings #6065 + +Fix for wrong default value for `Collect Color Coded Instances` Settings + + +___ + +
+ + +
+Bug: Fix Publisher parent window in Nuke #6067 + +Fixing issue where publisher parent window wasn't set because wrong use of version constant. + + +___ + +
+ + +
+Python console widget: Save registry fix #6076 + +Do not save registry until there is something to save. + + +___ + +
+ + +
+Ftrack: update asset names for multiple reviewable items #6077 + +Multiple reviewable assetVersion components with better grouping to asset version name. + + +___ + +
+ + +
+Ftrack: DJV action fixes #6098 + +Fix bugs in DJV ftrack action. + + +___ + +
+ + +
+AYON Workfiles tool: Fix arrow to timezone typo #6099 + +Fix parenthesis typo with arrow local timezone function. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Chore: Update folder-favorite icon to ayon icon #5718 + +Updates old "Pype-2.0-era" (from ancient greece times) to AYON logo equivalent.I believe it's only used in Nuke. + + +___ + +
+ +### **Merged pull requests** + + +
+Chore: Maya / Nuke remove publish gui filters from settings #5570 + +- Remove Publish GUI Filters from Nuke settings +- Remove Publish GUI Filters from Maya settings + + +___ + +
+ + +
+Fusion: Project/User option for output format (create_saver) #6045 + +Adds "Output Image Format" option which can be set via project settings and overwritten by users in "Create" menu. This replaces the current behaviour of being hardcoded to "exr". Replacing the need for people to manually edit the saver path if they require a different extension. + + +___ + +
+ + +
+Fusion: Output Image Format Updating Instances (create_saver) #6060 + +Adds the ability to update Saver image output format if changed in the Publish UI.~~Adds an optional validator that compares "Output Image Format" in the Publish menu against the one currently found on the saver. It then offers a repair action to update the output extension on the saver.~~ + + +___ + +
+ + +
+Tests: Fix representation count for AE legacy test #6072 + + +___ + +
+ + + + ## [3.18.1](https://github.com/ynput/OpenPype/tree/3.18.1) diff --git a/openpype/version.py b/openpype/version.py index 6a8e6f0d95..4d7b8f372f 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.2-nightly.6" +__version__ = "3.18.2" diff --git a/pyproject.toml b/pyproject.toml index e64018498f..38236f88bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.18.1" # OpenPype +version = "3.18.2" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From c4dea2c74a2ce25963a796d29a130c8355778718 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 3 Jan 2024 15:15:06 +0000 Subject: [PATCH 175/291] 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 3471c32430..132e960885 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.18.2 - 3.18.2-nightly.6 - 3.18.2-nightly.5 - 3.18.2-nightly.4 @@ -134,7 +135,6 @@ body: - 3.15.6-nightly.2 - 3.15.6-nightly.1 - 3.15.5 - - 3.15.5-nightly.2 validations: required: true - type: dropdown From be3bc7af8e64c727ab51ce834d5232d7d8fc7ce1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 Jan 2024 23:20:17 +0800 Subject: [PATCH 176/291] code changes based on Ondrej's comment --- openpype/hosts/max/api/pipeline.py | 56 +++++++----------------------- 1 file changed, 13 insertions(+), 43 deletions(-) diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 98895b858e..d0ae854dc8 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -3,7 +3,6 @@ import os import logging from operator import attrgetter -from functools import partial import json @@ -14,10 +13,6 @@ from openpype.pipeline import ( register_loader_plugin_path, AVALON_CONTAINER_ID, ) -from openpype.lib import ( - register_event_callback, - emit_event -) from openpype.hosts.max.api.menu import OpenPypeMenu from openpype.hosts.max.api import lib from openpype.hosts.max.api.plugin import MS_CUSTOM_ATTRIB @@ -51,14 +46,19 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_loader_plugin_path(LOAD_PATH) register_creator_plugin_path(CREATE_PATH) - self._register_callbacks() + # self._register_callbacks() self.menu = OpenPypeMenu() - register_event_callback( - "init", self._deferred_menu_creation) self._has_been_setup = True - register_event_callback("open", on_open) - register_event_callback("new", on_new) + + def context_setting(): + return lib.set_context_setting() + + rt.callbacks.addScript(rt.Name('systemPostNew'), + context_setting) + + rt.callbacks.addScript(rt.Name('filePostOpen'), + lib.check_colorspace) def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? @@ -83,28 +83,11 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return ls() def _register_callbacks(self): - unique_id = rt.Name("openpype_callbacks") - for handler, event in self._op_events.copy().items(): - if event is None: - continue - try: - rt.callbacks.removeScripts(id=unique_id) - self._op_events[handler] = None - except RuntimeError as exc: - self.log.info(exc) + rt.callbacks.removeScripts(id=rt.name("OpenPypeCallbacks")) - self._op_events["init"] = rt.callbacks.addScript( + rt.callbacks.addScript( rt.Name("postLoadingMenus"), - partial(_emit_event_notification_param, "init"), - id=unique_id) - self._op_events["new"] = rt.callbacks.addScript( - rt.Name('systemPostNew'), - partial(_emit_event_notification_param, "new"), - id=unique_id) - self._op_events["open"] = rt.callbacks.addScript( - rt.Name('filePostOpen'), - partial(_emit_event_notification_param, "open"), - id=unique_id) + self._deferred_menu_creation, id=rt.Name('OpenPypeCallbacks')) def _deferred_menu_creation(self): self.log.info("Building menu ...") @@ -161,19 +144,6 @@ attributes "OpenPypeContext" rt.saveMaxFile(dst_path) -def _emit_event_notification_param(event): - notification = rt.callbacks.notificationParam() - emit_event(event, {"notificationParam": notification}) - - -def on_open(): - return lib.check_colorspace() - - -def on_new(): - return lib.set_context_setting() - - def ls() -> list: """Get all OpenPype instances.""" objs = rt.objects From 02d41c4cd699d801a16b80fb5867b3e44dee3952 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 3 Jan 2024 23:51:08 +0800 Subject: [PATCH 177/291] small setting bug tweaks on ayon setting --- server_addon/max/server/settings/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/max/server/settings/main.py b/server_addon/max/server/settings/main.py index 582226eb62..cad6024cf7 100644 --- a/server_addon/max/server/settings/main.py +++ b/server_addon/max/server/settings/main.py @@ -71,7 +71,7 @@ class MaxSettings(BaseSettingsModel): DEFAULT_VALUES = { "unit_scale_settings": { "enabled": True, - "scene_unit_scale": "m" + "scene_unit_scale": "Centimeters" }, "RenderSettings": DEFAULT_RENDER_SETTINGS, "CreateReview": DEFAULT_CREATE_REVIEW_SETTINGS, From 9be69c3f304428792a454f1831a6d090ddf86231 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 00:25:34 +0800 Subject: [PATCH 178/291] implement exitstack functions from exitstack.py --- openpype/hosts/maya/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index eb83a44cde..da34896c3f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2839,7 +2839,7 @@ def bake_to_world_space(nodes, "sx", "sy", "sz"} world_space_nodes = [] - with contextlib.ExitStack() as stack: + with ExitStack() as stack: delete_bin = stack.enter_context(delete_after()) # Create the duplicate nodes that are in world-space connected to # the originals From aa9dbf612eda69cf03c21c086f229aaf73b600d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 4 Jan 2024 10:51:18 +0100 Subject: [PATCH 179/291] Chore: Event callbacks can have order (#6080) * EventCallback object have order * callbacks are processed by the callback order * safer approach to get function information * modified docstring a little * added tests for ordered calbacks * fix python 2 support * added support for partial methods * removed unused '_get_func_info' * formatting fix * change test functions docstring * added test for removement of source function when partial is used * Allow order 'None' * implemented 'weakref_partial' to allow partial callbacks * minor tweaks * added support to pass additional arguments to 'wearkref_partial' * modify docstring * added 'weakref_partial' to tests * move public method before prive methods * added required order back but use '100' as default order * fix typo Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- openpype/lib/events.py | 387 +++++++++++++++---- openpype/lib/python_module_tools.py | 2 +- tests/unit/openpype/lib/test_event_system.py | 97 ++++- 3 files changed, 399 insertions(+), 87 deletions(-) diff --git a/openpype/lib/events.py b/openpype/lib/events.py index 496b765a05..774790b80a 100644 --- a/openpype/lib/events.py +++ b/openpype/lib/events.py @@ -16,6 +16,113 @@ class MissingEventSystem(Exception): pass +def _get_func_ref(func): + if inspect.ismethod(func): + return WeakMethod(func) + return weakref.ref(func) + + +def _get_func_info(func): + path = "" + if func is None: + return "", path + + if hasattr(func, "__name__"): + name = func.__name__ + else: + name = str(func) + + # Get path to file and fallback to '' if fails + # NOTE This was added because of 'partial' functions which is handled, + # but who knows what else can cause this to fail? + try: + path = os.path.abspath(inspect.getfile(func)) + except TypeError: + pass + + return name, path + + +class weakref_partial: + """Partial function with weak reference to the wrapped function. + + Can be used as 'functools.partial' but it will store weak reference to + function. That means that the function must be reference counted + to avoid garbage collecting the function itself. + + When the referenced functions is garbage collected then calling the + weakref partial (no matter the args/kwargs passed) will do nothing. + It will fail silently, returning `None`. The `is_valid()` method can + be used to detect whether the reference is still valid. + + Is useful for object methods. In that case the callback is + deregistered when object is destroyed. + + Warnings: + Values passed as *args and **kwargs are stored strongly in memory. + That may "keep alive" objects that should be already destroyed. + It is recommended to pass only immutable objects like 'str', + 'bool', 'int' etc. + + Args: + func (Callable): Function to wrap. + *args: Arguments passed to the wrapped function. + **kwargs: Keyword arguments passed to the wrapped function. + """ + + def __init__(self, func, *args, **kwargs): + self._func_ref = _get_func_ref(func) + self._args = args + self._kwargs = kwargs + + def __call__(self, *args, **kwargs): + func = self._func_ref() + if func is None: + return + + new_args = tuple(list(self._args) + list(args)) + new_kwargs = dict(self._kwargs) + new_kwargs.update(kwargs) + return func(*new_args, **new_kwargs) + + def get_func(self): + """Get wrapped function. + + Returns: + Union[Callable, None]: Wrapped function or None if it was + destroyed. + """ + + return self._func_ref() + + def is_valid(self): + """Check if wrapped function is still valid. + + Returns: + bool: Is wrapped function still valid. + """ + + return self._func_ref() is not None + + def validate_signature(self, *args, **kwargs): + """Validate if passed arguments are supported by wrapped function. + + Returns: + bool: Are passed arguments supported by wrapped function. + """ + + func = self._func_ref() + if func is None: + return False + + new_args = tuple(list(self._args) + list(args)) + new_kwargs = dict(self._kwargs) + new_kwargs.update(kwargs) + return is_func_signature_supported( + func, *new_args, **new_kwargs + ) + + class EventCallback(object): """Callback registered to a topic. @@ -34,20 +141,37 @@ class EventCallback(object): or none arguments. When 1 argument is expected then the processed 'Event' object is passed in. - The registered callbacks don't keep function in memory so it is not - possible to store lambda function as callback. + The callbacks are validated against their reference counter, that is + achieved using 'weakref' module. That means that the callback must + be stored in memory somewhere. e.g. lambda functions are not + supported as valid callback. + + You can use 'weakref_partial' functions. In that case is partial object + stored in the callback object and reference counter is checked for + the wrapped function. Args: - topic(str): Topic which will be listened. - func(func): Callback to a topic. + topic (str): Topic which will be listened. + func (Callable): Callback to a topic. + order (Union[int, None]): Order of callback. Lower number means higher + priority. Raises: TypeError: When passed function is not a callable object. """ - def __init__(self, topic, func): + def __init__(self, topic, func, order): + if not callable(func): + raise TypeError(( + "Registered callback is not callable. \"{}\"" + ).format(str(func))) + + self._validate_order(order) + self._log = None self._topic = topic + self._order = order + self._enabled = True # Replace '*' with any character regex and escape rest of text # - when callback is registered for '*' topic it will receive all # events @@ -63,37 +187,38 @@ class EventCallback(object): topic_regex = re.compile(topic_regex_str) self._topic_regex = topic_regex - # Convert callback into references - # - deleted functions won't cause crashes - if inspect.ismethod(func): - func_ref = WeakMethod(func) - elif callable(func): - func_ref = weakref.ref(func) + # Callback function prep + if isinstance(func, weakref_partial): + partial_func = func + (name, path) = _get_func_info(func.get_func()) + func_ref = None + expect_args = partial_func.validate_signature("fake") + expect_kwargs = partial_func.validate_signature(event="fake") + else: - raise TypeError(( - "Registered callback is not callable. \"{}\"" - ).format(str(func))) + partial_func = None + (name, path) = _get_func_info(func) + # Convert callback into references + # - deleted functions won't cause crashes + func_ref = _get_func_ref(func) - # Collect function name and path to file for logging - func_name = func.__name__ - func_path = os.path.abspath(inspect.getfile(func)) - - # Get expected arguments from function spec - # - positional arguments are always preferred - expect_args = is_func_signature_supported(func, "fake") - expect_kwargs = is_func_signature_supported(func, event="fake") + # Get expected arguments from function spec + # - positional arguments are always preferred + expect_args = is_func_signature_supported(func, "fake") + expect_kwargs = is_func_signature_supported(func, event="fake") self._func_ref = func_ref - self._func_name = func_name - self._func_path = func_path + self._partial_func = partial_func + self._ref_is_valid = True self._expect_args = expect_args self._expect_kwargs = expect_kwargs - self._ref_valid = func_ref is not None - self._enabled = True + + self._name = name + self._path = path def __repr__(self): return "< {} - {} > {}".format( - self.__class__.__name__, self._func_name, self._func_path + self.__class__.__name__, self._name, self._path ) @property @@ -104,32 +229,83 @@ class EventCallback(object): @property def is_ref_valid(self): - return self._ref_valid + """ + + Returns: + bool: Is reference to callback valid. + """ + + self._validate_ref() + return self._ref_is_valid def validate_ref(self): - if not self._ref_valid: - return + """Validate if reference to callback is valid. - callback = self._func_ref() - if not callback: - self._ref_valid = False + Deprecated: + Reference is always live checkd with 'is_ref_valid'. + """ + + # Trigger validate by getting 'is_valid' + _ = self.is_ref_valid @property def enabled(self): - """Is callback enabled.""" + """Is callback enabled. + + Returns: + bool: Is callback enabled. + """ + return self._enabled def set_enabled(self, enabled): - """Change if callback is enabled.""" + """Change if callback is enabled. + + Args: + enabled (bool): Change enabled state of the callback. + """ + self._enabled = enabled def deregister(self): """Calling this function will cause that callback will be removed.""" - # Fake reference - self._ref_valid = False + + self._ref_is_valid = False + self._partial_func = None + self._func_ref = None + + def get_order(self): + """Get callback order. + + Returns: + Union[int, None]: Callback order. + """ + + return self._order + + def set_order(self, order): + """Change callback order. + + Args: + order (Union[int, None]): Order of callback. Lower number means + higher priority. + """ + + self._validate_order(order) + self._order = order + + order = property(get_order, set_order) def topic_matches(self, topic): - """Check if event topic matches callback's topic.""" + """Check if event topic matches callback's topic. + + Args: + topic (str): Topic name. + + Returns: + bool: Topic matches callback's topic. + """ + return self._topic_regex.match(topic) def process_event(self, event): @@ -139,36 +315,69 @@ class EventCallback(object): event(Event): Event that was triggered. """ - # Skip if callback is not enabled or has invalid reference - if not self._ref_valid or not self._enabled: + # Skip if callback is not enabled + if not self._enabled: return - # Get reference - callback = self._func_ref() - # Check if reference is valid or callback's topic matches the event - if not callback: - # Change state if is invalid so the callback is removed - self._ref_valid = False + # Get reference and skip if is not available + callback = self._get_callback() + if callback is None: + return - elif self.topic_matches(event.topic): - # Try execute callback - try: - if self._expect_args: - callback(event) + if not self.topic_matches(event.topic): + return - elif self._expect_kwargs: - callback(event=event) + # Try to execute callback + try: + if self._expect_args: + callback(event) - else: - callback() + elif self._expect_kwargs: + callback(event=event) - except Exception: - self.log.warning( - "Failed to execute event callback {}".format( - str(repr(self)) - ), - exc_info=True - ) + else: + callback() + + except Exception: + self.log.warning( + "Failed to execute event callback {}".format( + str(repr(self)) + ), + exc_info=True + ) + + def _validate_order(self, order): + if isinstance(order, int): + return + + raise TypeError( + "Expected type 'int' got '{}'.".format(str(type(order))) + ) + + def _get_callback(self): + if self._partial_func is not None: + return self._partial_func + + if self._func_ref is not None: + return self._func_ref() + return None + + def _validate_ref(self): + if self._ref_is_valid is False: + return + + if self._func_ref is not None: + self._ref_is_valid = self._func_ref() is not None + + elif self._partial_func is not None: + self._ref_is_valid = self._partial_func.is_valid() + + else: + self._ref_is_valid = False + + if not self._ref_is_valid: + self._func_ref = None + self._partial_func = None # Inherit from 'object' for Python 2 hosts @@ -282,30 +491,39 @@ class Event(object): class EventSystem(object): """Encapsulate event handling into an object. - System wraps registered callbacks and triggered events into single object - so it is possible to create mutltiple independent systems that have their + System wraps registered callbacks and triggered events into single object, + so it is possible to create multiple independent systems that have their topics and callbacks. - + Callbacks are stored by order of their registration, but it is possible to + manually define order of callbacks using 'order' argument within + 'add_callback'. """ + default_order = 100 + def __init__(self): self._registered_callbacks = [] - def add_callback(self, topic, callback): + def add_callback(self, topic, callback, order=None): """Register callback in event system. Args: topic (str): Topic for EventCallback. - callback (Callable): Function or method that will be called - when topic is triggered. + callback (Union[Callable, weakref_partial]): Function or method + that will be called when topic is triggered. + order (Optional[int]): Order of callback. Lower number means + higher priority. Returns: EventCallback: Created callback object which can be used to stop listening. """ - callback = EventCallback(topic, callback) + if order is None: + order = self.default_order + + callback = EventCallback(topic, callback, order) self._registered_callbacks.append(callback) return callback @@ -341,22 +559,6 @@ class EventSystem(object): event.emit() return event - def _process_event(self, event): - """Process event topic and trigger callbacks. - - Args: - event (Event): Prepared event with topic and data. - """ - - invalid_callbacks = [] - for callback in self._registered_callbacks: - callback.process_event(event) - if not callback.is_ref_valid: - invalid_callbacks.append(callback) - - for callback in invalid_callbacks: - self._registered_callbacks.remove(callback) - def emit_event(self, event): """Emit event object. @@ -366,6 +568,21 @@ class EventSystem(object): self._process_event(event) + def _process_event(self, event): + """Process event topic and trigger callbacks. + + Args: + event (Event): Prepared event with topic and data. + """ + + callbacks = tuple(sorted( + self._registered_callbacks, key=lambda x: x.order + )) + for callback in callbacks: + callback.process_event(event) + if not callback.is_ref_valid: + self._registered_callbacks.remove(callback) + class QueuedEventSystem(EventSystem): """Events are automatically processed in queue. diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index bedf19562d..4f9eb7f667 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -269,7 +269,7 @@ def is_func_signature_supported(func, *args, **kwargs): True Args: - func (function): A function where the signature should be tested. + func (Callable): A function where the signature should be tested. *args (Any): Positional arguments for function signature. **kwargs (Any): Keyword arguments for function signature. diff --git a/tests/unit/openpype/lib/test_event_system.py b/tests/unit/openpype/lib/test_event_system.py index aa3f929065..b0a011d83e 100644 --- a/tests/unit/openpype/lib/test_event_system.py +++ b/tests/unit/openpype/lib/test_event_system.py @@ -1,4 +1,9 @@ -from openpype.lib.events import EventSystem, QueuedEventSystem +from functools import partial +from openpype.lib.events import ( + EventSystem, + QueuedEventSystem, + weakref_partial, +) def test_default_event_system(): @@ -81,3 +86,93 @@ def test_manual_event_system_queue(): assert output == expected_output, ( "Callbacks were not called in correct order") + + +def test_unordered_events(): + """ + Validate if callbacks are triggered in order of their register. + """ + + result = [] + + def function_a(): + result.append("A") + + def function_b(): + result.append("B") + + def function_c(): + result.append("C") + + # Without order + event_system = QueuedEventSystem() + event_system.add_callback("test", function_a) + event_system.add_callback("test", function_b) + event_system.add_callback("test", function_c) + event_system.emit("test", {}, "test") + + assert result == ["A", "B", "C"] + + +def test_ordered_events(): + """ + Validate if callbacks are triggered by their order and order + of their register. + """ + result = [] + + def function_a(): + result.append("A") + + def function_b(): + result.append("B") + + def function_c(): + result.append("C") + + def function_d(): + result.append("D") + + def function_e(): + result.append("E") + + def function_f(): + result.append("F") + + # Without order + event_system = QueuedEventSystem() + event_system.add_callback("test", function_a) + event_system.add_callback("test", function_b, order=-10) + event_system.add_callback("test", function_c, order=200) + event_system.add_callback("test", function_d, order=150) + event_system.add_callback("test", function_e) + event_system.add_callback("test", function_f, order=200) + event_system.emit("test", {}, "test") + + assert result == ["B", "A", "E", "D", "C", "F"] + + +def test_events_partial_callbacks(): + """ + Validate if partial callbacks are triggered. + """ + + result = [] + + def function(name): + result.append(name) + + def function_regular(): + result.append("regular") + + event_system = QueuedEventSystem() + event_system.add_callback("test", function_regular) + event_system.add_callback("test", partial(function, "foo")) + event_system.add_callback("test", weakref_partial(function, "bar")) + event_system.emit("test", {}, "test") + + # Delete function should also make partial callbacks invalid + del function + event_system.emit("test", {}, "test") + + assert result == ["regular", "bar", "regular"] From 989ec8beb2947c8abef969c9e2922bea0eec2f8f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 18:03:05 +0800 Subject: [PATCH 180/291] make sure the default shader assigned to the redshift proxy --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index b3fbfb2ed9..0ed09c6007 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -137,6 +137,14 @@ class RedshiftProxyLoader(load.LoaderPlugin): cmds.connectAttr("{}.outMesh".format(rs_mesh), "{}.inMesh".format(mesh_shape)) + # TODO: use the assigned shading group as shaders if existed + # assign default shader to redshift proxy + shader_grp = next( + (shader_group for shader_group in cmds.ls(type="shadingEngine") + if shader_group=="initialShadingGroup") + ) + cmds.sets(mesh_shape, forceElement=shader_grp) + group_node = cmds.group(empty=True, name="{}_GRP".format(name)) mesh_transform = cmds.listRelatives(mesh_shape, parent=True, fullPath=True) From fba52796d61c9968543a5ccaec3b4cf23459535d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 18:04:03 +0800 Subject: [PATCH 181/291] assign the textures when the initial shading group is located --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index 0ed09c6007..340533ccbd 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -143,7 +143,8 @@ class RedshiftProxyLoader(load.LoaderPlugin): (shader_group for shader_group in cmds.ls(type="shadingEngine") if shader_group=="initialShadingGroup") ) - cmds.sets(mesh_shape, forceElement=shader_grp) + if shader_grp: + cmds.sets(mesh_shape, forceElement=shader_grp) group_node = cmds.group(empty=True, name="{}_GRP".format(name)) mesh_transform = cmds.listRelatives(mesh_shape, From a7efe2ea759a6775c42bb0b53a3992201e0f00a3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 18:14:26 +0800 Subject: [PATCH 182/291] hound --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index 340533ccbd..f67fe1c529 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -139,10 +139,9 @@ class RedshiftProxyLoader(load.LoaderPlugin): # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy - shader_grp = next( - (shader_group for shader_group in cmds.ls(type="shadingEngine") - if shader_group=="initialShadingGroup") - ) + shader_grp = next((shader_group for shader_group + in cmds.ls(type="shadingEngine") + if shader_group=="initialShadingGroup")) if shader_grp: cmds.sets(mesh_shape, forceElement=shader_grp) From 3af7795f19e93082824e0162f0436687f13dee51 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 18:18:13 +0800 Subject: [PATCH 183/291] hound --- .../hosts/maya/plugins/load/load_redshift_proxy.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index f67fe1c529..7f88f1b152 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -139,11 +139,11 @@ class RedshiftProxyLoader(load.LoaderPlugin): # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy - shader_grp = next((shader_group for shader_group - in cmds.ls(type="shadingEngine") - if shader_group=="initialShadingGroup")) - if shader_grp: - cmds.sets(mesh_shape, forceElement=shader_grp) + shader_grp = next( + (shader_group for shader_group in cmds.ls(type="shadingEngine") + if shader_group == "initialShadingGroup") + ) + cmds.sets(mesh_shape, forceElement=shader_grp) group_node = cmds.group(empty=True, name="{}_GRP".format(name)) mesh_transform = cmds.listRelatives(mesh_shape, From bcea8967acab10bbc406fdfb0d7db4b12680f121 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 19:18:33 +0800 Subject: [PATCH 184/291] code tweaks on shader_grp's variable --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index 7f88f1b152..7b657f9184 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -140,8 +140,9 @@ class RedshiftProxyLoader(load.LoaderPlugin): # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy shader_grp = next( - (shader_group for shader_group in cmds.ls(type="shadingEngine") - if shader_group == "initialShadingGroup") + (shader_group for shader_group in + cmds.ls("initialShadingGroup", type="shadingEngine") + ) ) cmds.sets(mesh_shape, forceElement=shader_grp) From b40cba08016a9607d4e5f82ac534e0970079bae8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 19:20:43 +0800 Subject: [PATCH 185/291] hound --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index 7b657f9184..33f39ebf38 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -140,8 +140,9 @@ class RedshiftProxyLoader(load.LoaderPlugin): # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy shader_grp = next( - (shader_group for shader_group in - cmds.ls("initialShadingGroup", type="shadingEngine") + ( + shader_group for shader_group in + cmds.ls("initialShadingGroup", type="shadingEngine") ) ) cmds.sets(mesh_shape, forceElement=shader_grp) From 584341dcef9a4d4185ba1d159d60fcba9d0dc755 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 4 Jan 2024 19:30:43 +0800 Subject: [PATCH 186/291] code tweaks on default shader grp --- openpype/hosts/maya/plugins/load/load_redshift_proxy.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py index 33f39ebf38..40385f34d6 100644 --- a/openpype/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/openpype/hosts/maya/plugins/load/load_redshift_proxy.py @@ -139,13 +139,8 @@ class RedshiftProxyLoader(load.LoaderPlugin): # TODO: use the assigned shading group as shaders if existed # assign default shader to redshift proxy - shader_grp = next( - ( - shader_group for shader_group in - cmds.ls("initialShadingGroup", type="shadingEngine") - ) - ) - cmds.sets(mesh_shape, forceElement=shader_grp) + if cmds.ls("initialShadingGroup", type="shadingEngine"): + cmds.sets(mesh_shape, forceElement="initialShadingGroup") group_node = cmds.group(empty=True, name="{}_GRP".format(name)) mesh_transform = cmds.listRelatives(mesh_shape, From 6043d5f7d9a053cbc82936c3978ad590b2053798 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 4 Jan 2024 14:17:45 +0100 Subject: [PATCH 187/291] openpype addon defines runtime dependencies (#6095) --- server_addon/openpype/client/pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server_addon/openpype/client/pyproject.toml b/server_addon/openpype/client/pyproject.toml index 318ceb0185..d8de9d4d96 100644 --- a/server_addon/openpype/client/pyproject.toml +++ b/server_addon/openpype/client/pyproject.toml @@ -16,3 +16,8 @@ pynput = "^1.7.2" # Timers manager - TODO remove "Qt.py" = "^1.3.3" qtawesome = "0.7.3" speedcopy = "^2.1" + +[ayon.runtimeDependencies] +OpenTimelineIO = "0.14.1" +opencolorio = "2.2.1" +Pillow = "9.5.0" From 91a1fb1cdbff63442426b1b0e6bb6768a17aa49e Mon Sep 17 00:00:00 2001 From: kaa Date: Thu, 4 Jan 2024 15:59:26 +0100 Subject: [PATCH 188/291] General: We should keep current subset version when we switch only the representation type (#4629) * keep current subset version when switch repre * added comment and safe switch repres * more clearly variable names and revert last feat * check selected but no change * fix switch hero version --- .../tools/sceneinventory/switch_dialog.py | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index ce2272df57..150e369678 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -1230,12 +1230,12 @@ class SwitchAssetDialog(QtWidgets.QDialog): version_ids = list() - version_docs_by_parent_id = {} + version_docs_by_parent_id_and_name = collections.defaultdict(dict) for version_doc in version_docs: parent_id = version_doc["parent"] - if parent_id not in version_docs_by_parent_id: - version_ids.append(version_doc["_id"]) - version_docs_by_parent_id[parent_id] = version_doc + version_ids.append(version_doc["_id"]) + name = version_doc["name"] + version_docs_by_parent_id_and_name[parent_id][name] = version_doc hero_version_docs_by_parent_id = {} for hero_version_doc in hero_version_docs: @@ -1293,13 +1293,32 @@ class SwitchAssetDialog(QtWidgets.QDialog): repre_doc = _repres.get(container_repre_name) if not repre_doc: - version_doc = version_docs_by_parent_id[subset_id] - version_id = version_doc["_id"] - repres_by_name = repre_docs_by_parent_id_by_name[version_id] - if selected_representation: - repre_doc = repres_by_name[selected_representation] + version_docs_by_name = version_docs_by_parent_id_and_name[ + subset_id + ] + + # If asset or subset are selected for switching, we use latest + # version else we try to keep the current container version. + if ( + selected_asset not in (None, container_asset_name) + or selected_subset not in (None, container_subset_name) + ): + version_name = max(version_docs_by_name) else: - repre_doc = repres_by_name[container_repre_name] + version_name = container_version["name"] + + version_doc = version_docs_by_name[version_name] + version_id = version_doc["_id"] + repres_docs_by_name = repre_docs_by_parent_id_by_name[ + version_id + ] + + if selected_representation: + repres_name = selected_representation + else: + repres_name = container_repre_name + + repre_doc = repres_docs_by_name[repres_name] error = None try: From fd87751c36dc2c1fe1565dc8d782d8dcd786bf3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Sat, 6 Jan 2024 00:01:14 +0100 Subject: [PATCH 189/291] :art: add split export support for redshift --- .../plugins/publish/collect_redshift_rop.py | 19 +++++++++++++++++++ .../publish/submit_houdini_render_deadline.py | 10 ++++++++++ server_addon/deadline/server/version.py | 2 +- server_addon/houdini/server/version.py | 2 +- 4 files changed, 31 insertions(+), 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 0acddab011..cd3bb2bb7a 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -45,6 +45,25 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): beauty_suffix = rop.evalParm("RS_outputBeautyAOVSuffix") render_products = [] + # Store whether we are splitting the render job (export + render) + split_render = bool(rop.parm("RS_archive_enable").eval()) + instance.data["splitRender"] = split_render + export_prefix = None + export_products = [] + if split_render: + export_prefix = evalParmNoFrame( + rop, "RS_archive_file", pad_character="0" + ) + beauty_export_product = self.get_render_product_name( + prefix=export_prefix, + suffix=None) + export_products.append(beauty_export_product) + self.log.debug( + "Found export product: {}".format(beauty_export_product) + ) + instance.data["ifdFile"] = beauty_export_product + instance.data["exportFiles"] = list(export_products) + # Default beauty AOV beauty_product = self.get_render_product_name( prefix=default_prefix, suffix=beauty_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 c8960185b2..0bfb37ee1c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -41,6 +41,11 @@ class VrayRenderPluginInfo(): SeparateFilesPerFrame = attr.ib(default=True) +@attr.s +class RedshiftRenderPluginInfo(): + SceneFile = attr.ib(default=None) + Version = attr.ib(default=None) + class HoudiniSubmitDeadline( abstract_submit_deadline.AbstractSubmitDeadline, OpenPypePyblishPluginMixin @@ -262,6 +267,11 @@ class HoudiniSubmitDeadline( plugin_info = VrayRenderPluginInfo( InputFilename=instance.data["ifdFile"], ) + elif family == "redshift_rop": + plugin_info = RedshiftRenderPluginInfo( + SceneFile=instance.data["ifdFile"], + Version=os.getenv("REDSHIFT_VERSION", "3.5.22"), + ) else: self.log.error( "Family '%s' not supported yet to split render job", diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py index 1276d0254f..0a8da88258 100644 --- a/server_addon/deadline/server/version.py +++ b/server_addon/deadline/server/version.py @@ -1 +1 @@ -__version__ = "0.1.5" +__version__ = "0.1.6" diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index 6232f7ab18..5635676f6b 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.2.10" +__version__ = "0.2.11" From 8f43b87d3628f1eb0ca97f5c017c64e0407bb3bb Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 6 Jan 2024 03:25:10 +0000 Subject: [PATCH 190/291] [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 4d7b8f372f..dba782ded4 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.2" +__version__ = "3.18.3-nightly.1" From 9df4160595cd6678a6501bdc2dc79e40ba4b264b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 6 Jan 2024 03:25:49 +0000 Subject: [PATCH 191/291] 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 132e960885..2e854061d5 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.18.3-nightly.1 - 3.18.2 - 3.18.2-nightly.6 - 3.18.2-nightly.5 @@ -134,7 +135,6 @@ body: - 3.15.6-nightly.3 - 3.15.6-nightly.2 - 3.15.6-nightly.1 - - 3.15.5 validations: required: true - type: dropdown From 18e1f62ba29ff796e35b6e8da6bba3ce1f113e29 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 8 Jan 2024 11:27:43 +0200 Subject: [PATCH 192/291] add split setting in redshift rop creator --- .../plugins/create/create_redshift_rop.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 1b8826a932..d790aaa340 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -15,6 +15,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator): icon = "magic" ext = "exr" + # Default to split export and render jobs + split_render = True + def create(self, subset_name, instance_data, pre_create_data): instance_data.pop("active", None) @@ -76,6 +79,16 @@ class CreateRedshiftROP(plugin.HoudiniCreator): camera = node.path() parms.update({ "RS_renderCamera": camera or ""}) + + if pre_create_data.get("split_render"): + rs_filepath = \ + "{export_dir}{subset_name}/{subset_name}.$F4.rs".format( + export_dir=hou.text.expandString("$HIP/pyblish/rs/"), + subset_name=subset_name, + ) + parms["RS_archive_enable"] = 1 + parms["RS_archive_file"] = rs_filepath + instance_node.setParms(parms) # Lock some Avalon attributes @@ -102,6 +115,9 @@ class CreateRedshiftROP(plugin.HoudiniCreator): BoolDef("farm", label="Submitting to Farm", default=True), + BoolDef("split_render", + label="Split export and render jobs", + default=self.split_render), EnumDef("image_format", image_format_enum, default=self.ext, From 808a2b7031d0ec81516ee170fd3bcef1fa5e3b1c Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 8 Jan 2024 11:43:04 +0200 Subject: [PATCH 193/291] BigRoy's comments --- .../plugins/create/create_redshift_rop.py | 16 +++++++++------- .../plugins/publish/collect_redshift_rop.py | 1 - 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index d790aaa340..097c703283 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -80,14 +80,16 @@ class CreateRedshiftROP(plugin.HoudiniCreator): parms.update({ "RS_renderCamera": camera or ""}) - if pre_create_data.get("split_render"): - rs_filepath = \ - "{export_dir}{subset_name}/{subset_name}.$F4.rs".format( - export_dir=hou.text.expandString("$HIP/pyblish/rs/"), - subset_name=subset_name, - ) + rs_filepath = \ + "{export_dir}{subset_name}/{subset_name}.$F4.rs".format( + export_dir=hou.text.expandString("$HIP/pyblish/rs/"), + subset_name=subset_name, + ) + parms["RS_archive_file"] = rs_filepath + + if pre_create_data.get("split_render", self.split_render): parms["RS_archive_enable"] = 1 - parms["RS_archive_file"] = rs_filepath + instance_node.setParms(parms) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index cd3bb2bb7a..056684b3a3 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -48,7 +48,6 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): # Store whether we are splitting the render job (export + render) split_render = bool(rop.parm("RS_archive_enable").eval()) instance.data["splitRender"] = split_render - export_prefix = None export_products = [] if split_render: export_prefix = evalParmNoFrame( From 938d9126a2bf76df4f9a3e5ccde2231dfd6507c3 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Mon, 8 Jan 2024 11:48:23 +0200 Subject: [PATCH 194/291] BigRoy's comment - stick to code style --- .../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 0bfb37ee1c..8b03f682fc 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -46,6 +46,7 @@ class RedshiftRenderPluginInfo(): SceneFile = attr.ib(default=None) Version = attr.ib(default=None) + class HoudiniSubmitDeadline( abstract_submit_deadline.AbstractSubmitDeadline, OpenPypePyblishPluginMixin From 1e3fad27b0147691a3e535b9473ff330e84cfd51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 8 Jan 2024 10:57:05 +0100 Subject: [PATCH 195/291] :recycle: some code style changes --- .../plugins/create/create_redshift_rop.py | 25 ++++++++----------- .../plugins/publish/collect_redshift_rop.py | 13 ++++------ .../publish/submit_houdini_render_deadline.py | 1 + 3 files changed, 17 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py index 097c703283..b36580f67e 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -39,12 +39,15 @@ class CreateRedshiftROP(plugin.HoudiniCreator): # Also create the linked Redshift IPR Rop try: ipr_rop = instance_node.parent().createNode( - "Redshift_IPR", node_name=basename + "_IPR" + "Redshift_IPR", node_name=f"{basename}_IPR" ) - except hou.OperationFailed: + except hou.OperationFailed as e: raise plugin.OpenPypeCreatorError( - ("Cannot create Redshift node. Is Redshift " - "installed and enabled?")) + ( + "Cannot create Redshift node. Is Redshift " + "installed and enabled?" + ) + ) from e # Move it to directly under the Redshift ROP ipr_rop.setPosition(instance_node.position() + hou.Vector2(0, -1)) @@ -77,22 +80,16 @@ class CreateRedshiftROP(plugin.HoudiniCreator): for node in self.selected_nodes: if node.type().name() == "cam": camera = node.path() - parms.update({ - "RS_renderCamera": camera or ""}) + parms["RS_renderCamera"] = camera or "" - rs_filepath = \ - "{export_dir}{subset_name}/{subset_name}.$F4.rs".format( - export_dir=hou.text.expandString("$HIP/pyblish/rs/"), - subset_name=subset_name, - ) + export_dir=hou.text.expandString("$HIP/pyblish/rs/") + rs_filepath = f"{export_dir}{subset_name}/{subset_name}.$F4.rs" parms["RS_archive_file"] = rs_filepath + instance_node.setParms(parms) if pre_create_data.get("split_render", self.split_render): parms["RS_archive_enable"] = 1 - - instance_node.setParms(parms) - # Lock some Avalon attributes to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) diff --git a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py index 056684b3a3..aec7e07fbc 100644 --- a/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/publish/collect_redshift_rop.py @@ -31,7 +31,6 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): families = ["redshift_rop"] def process(self, instance): - rop = hou.node(instance.data.get("instance_node")) # Collect chunkSize @@ -43,8 +42,6 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): default_prefix = evalParmNoFrame(rop, "RS_outputFileNamePrefix") beauty_suffix = rop.evalParm("RS_outputBeautyAOVSuffix") - render_products = [] - # Store whether we are splitting the render job (export + render) split_render = bool(rop.parm("RS_archive_enable").eval()) instance.data["splitRender"] = split_render @@ -67,7 +64,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): beauty_product = self.get_render_product_name( prefix=default_prefix, suffix=beauty_suffix ) - render_products.append(beauty_product) + render_products = [beauty_product] files_by_aov = { "_": self.generate_expected_files(instance, beauty_product)} @@ -77,11 +74,11 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): i = index + 1 # Skip disabled AOVs - if not rop.evalParm("RS_aovEnable_%s" % i): + if not rop.evalParm(f"RS_aovEnable_{i}"): continue - aov_suffix = rop.evalParm("RS_aovSuffix_%s" % i) - aov_prefix = evalParmNoFrame(rop, "RS_aovCustomPrefix_%s" % i) + aov_suffix = rop.evalParm(f"RS_aovSuffix_{i}") + aov_prefix = evalParmNoFrame(rop, f"RS_aovCustomPrefix_{i}") if not aov_prefix: aov_prefix = default_prefix @@ -103,7 +100,7 @@ class CollectRedshiftROPRenderProducts(pyblish.api.InstancePlugin): instance.data["attachTo"] = [] # stub required data if "expectedFiles" not in instance.data: - instance.data["expectedFiles"] = list() + instance.data["expectedFiles"] = [] instance.data["expectedFiles"].append(files_by_aov) # update the colorspace data 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 8b03f682fc..fd5e789d0e 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -15,6 +15,7 @@ from openpype.lib import ( NumberDef ) + @attr.s class DeadlinePluginInfo(): SceneFile = attr.ib(default=None) From 3e4b8a152217af7473d38e8de4dd6f11ec84170b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 8 Jan 2024 11:16:33 +0100 Subject: [PATCH 196/291] :dog: fix hound --- 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 b36580f67e..151fd26074 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -82,7 +82,7 @@ class CreateRedshiftROP(plugin.HoudiniCreator): camera = node.path() parms["RS_renderCamera"] = camera or "" - export_dir=hou.text.expandString("$HIP/pyblish/rs/") + export_dir = hou.text.expandString("$HIP/pyblish/rs/") rs_filepath = f"{export_dir}{subset_name}/{subset_name}.$F4.rs" parms["RS_archive_file"] = rs_filepath From 5d787d3fc3c77b2977f18b6da1ee7161610e4a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 8 Jan 2024 11:17:12 +0100 Subject: [PATCH 197/291] :recycle: change how redshift version is passed --- .../plugins/publish/submit_houdini_render_deadline.py | 6 ++++-- 1 file changed, 4 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 fd5e789d0e..abcc3378da 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -271,9 +271,11 @@ class HoudiniSubmitDeadline( ) elif family == "redshift_rop": plugin_info = RedshiftRenderPluginInfo( - SceneFile=instance.data["ifdFile"], - Version=os.getenv("REDSHIFT_VERSION", "3.5.22"), + SceneFile=instance.data["ifdFile"] ) + if os.getenv("REDSHIFT_VERSION"): + plugin_info.Version = os.getenv("REDSHIFT_VERSION"), + else: self.log.error( "Family '%s' not supported yet to split render job", From 5f837d6f0b381f0ffc4db488c563bac10127b481 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 8 Jan 2024 11:51:28 +0100 Subject: [PATCH 198/291] AfterEffects: exposing Deadline pools fields in Publisher UI (#6079) * OP-6421 - added render family to families filter As published instances follow product type `render` now, fields wouldn't be shown for them. * OP-6421 - updated documentation * OP-6421 - added hosts filter Limits this with higher precision. --- .../deadline/plugins/publish/collect_pools.py | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index a25b149f11..9ee079b892 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -"""Collect Deadline pools. Choose default one from Settings - -""" import pyblish.api from openpype.lib import TextDef from openpype.pipeline.publish import OpenPypePyblishPluginMixin @@ -9,11 +6,35 @@ from openpype.pipeline.publish import OpenPypePyblishPluginMixin class CollectDeadlinePools(pyblish.api.InstancePlugin, OpenPypePyblishPluginMixin): - """Collect pools from instance if present, from Setting otherwise.""" + """Collect pools from instance or Publisher attributes, from Setting + otherwise. + + Pools are used to control which DL workers could render the job. + + Pools might be set: + - directly on the instance (set directly in DCC) + - from Publisher attributes + - from defaults from Settings. + + Publisher attributes could be shown even for instances that should be + rendered locally as visibility is driven by product type of the instance + (which will be `render` most likely). + (Might be resolved in the future and class attribute 'families' should + be cleaned up.) + + """ order = pyblish.api.CollectorOrder + 0.420 label = "Collect Deadline Pools" - families = ["rendering", + hosts = ["aftereffects", + "fusion", + "harmony" + "nuke", + "maya", + "max"] + + families = ["render", + "rendering", "render.farm", "renderFarm", "renderlayer", @@ -30,7 +51,6 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, cls.secondary_pool = settings.get("secondary_pool", None) def process(self, instance): - attr_values = self.get_attr_values_from_data(instance.data) if not instance.data.get("primaryPool"): instance.data["primaryPool"] = ( @@ -60,8 +80,12 @@ class CollectDeadlinePools(pyblish.api.InstancePlugin, return [ TextDef("primaryPool", label="Primary Pool", - default=cls.primary_pool), + default=cls.primary_pool, + tooltip="Deadline primary pool, " + "applicable for farm rendering"), TextDef("secondaryPool", label="Secondary Pool", - default=cls.secondary_pool) + default=cls.secondary_pool, + tooltip="Deadline secondary pool, " + "applicable for farm rendering") ] From cf17ea8377e21c4820756c83f03cd43f2eafeeeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 8 Jan 2024 12:09:06 +0100 Subject: [PATCH 199/291] :memo: add comment and warning about unspecified RS version --- .../publish/submit_houdini_render_deadline.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 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 abcc3378da..bf7fb45a8b 100644 --- a/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_houdini_render_deadline.py @@ -273,8 +273,20 @@ class HoudiniSubmitDeadline( plugin_info = RedshiftRenderPluginInfo( SceneFile=instance.data["ifdFile"] ) + # Note: To use different versions of Redshift on Deadline + # set the `REDSHIFT_VERSION` env variable in the Tools + # settings in the AYON Application plugin. You will also + # need to set that version in `Redshift.param` file + # of the Redshift Deadline plugin: + # [Redshift_Executable_*] + # where * is the version number. if os.getenv("REDSHIFT_VERSION"): - plugin_info.Version = os.getenv("REDSHIFT_VERSION"), + plugin_info.Version = os.getenv("REDSHIFT_VERSION") + else: + self.log.warning(( + "REDSHIFT_VERSION env variable is not set" + " - using version configured in Deadline" + )) else: self.log.error( From 9d8378502436c1188fdb03be0185552cdfae2a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 8 Jan 2024 12:41:22 +0100 Subject: [PATCH 200/291] :bug: fix render archive enable flag settings --- openpype/hosts/houdini/plugins/create/create_redshift_rop.py | 3 ++- 1 file changed, 2 insertions(+), 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 151fd26074..9d1c7bc90d 100644 --- a/openpype/hosts/houdini/plugins/create/create_redshift_rop.py +++ b/openpype/hosts/houdini/plugins/create/create_redshift_rop.py @@ -86,10 +86,11 @@ class CreateRedshiftROP(plugin.HoudiniCreator): rs_filepath = f"{export_dir}{subset_name}/{subset_name}.$F4.rs" parms["RS_archive_file"] = rs_filepath - instance_node.setParms(parms) if pre_create_data.get("split_render", self.split_render): parms["RS_archive_enable"] = 1 + instance_node.setParms(parms) + # Lock some Avalon attributes to_lock = ["family", "id"] self.lock_parameters(instance_node, to_lock) From 05cbb8b019d2e14fdcde07c38d4a7d80dfc2c3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 8 Jan 2024 13:33:51 +0100 Subject: [PATCH 201/291] :wrench: fix and update pydocstyle configuration --- pyproject.toml | 5 +++++ setup.cfg | 4 ---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 38236f88bc..ee8e8017e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -181,3 +181,8 @@ reportMissingTypeStubs = false [tool.poetry.extras] docs = ["Sphinx", "furo", "sphinxcontrib-napoleon"] + +[tool.pydocstyle] +inherit = false +convetion = "google" +match = "(?!test_).*\\.py" diff --git a/setup.cfg b/setup.cfg index ead9b25164..f0f754fb24 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,10 +16,6 @@ max-complexity = 30 [pylint.'MESSAGES CONTROL'] disable = no-member -[pydocstyle] -convention = google -ignore = D107 - [coverage:run] branch = True omit = /tests From 86cf80027c039a9a353911760eaf13ae78279ea0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jan 2024 11:16:28 +0100 Subject: [PATCH 202/291] Chore: Remove deprecated templates profiles (#6103) * do not use 'IntegrateAssetNew' settings which are not available anymore * don't use 'IntegrateHeroVersion' settings to get hero version template name * remove 'template_name_profiles' from 'IntegrateHeroVersion' * remove unused attribute --- openpype/pipeline/publish/lib.py | 55 +------------------ .../plugins/publish/integrate_hero_version.py | 1 - .../schemas/schema_global_publish.json | 43 --------------- .../core/server/settings/publish_plugins.py | 20 ------- 4 files changed, 2 insertions(+), 117 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 4ea2f932f1..40cb94e2bf 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -58,41 +58,13 @@ def get_template_name_profiles( if not project_settings: project_settings = get_project_settings(project_name) - profiles = ( + return copy.deepcopy( project_settings ["global"] ["tools"] ["publish"] ["template_name_profiles"] ) - if profiles: - return copy.deepcopy(profiles) - - # Use legacy approach for cases new settings are not filled yet for the - # project - legacy_profiles = ( - project_settings - ["global"] - ["publish"] - ["IntegrateAssetNew"] - ["template_name_profiles"] - ) - if legacy_profiles: - if not logger: - logger = Logger.get_logger("get_template_name_profiles") - - logger.warning(( - "Project \"{}\" is using legacy access to publish template." - " It is recommended to move settings to new location" - " 'project_settings/global/tools/publish/template_name_profiles'." - ).format(project_name)) - - # Replace "tasks" key with "task_names" - profiles = [] - for profile in copy.deepcopy(legacy_profiles): - profile["task_names"] = profile.pop("tasks", []) - profiles.append(profile) - return profiles def get_hero_template_name_profiles( @@ -121,36 +93,13 @@ def get_hero_template_name_profiles( if not project_settings: project_settings = get_project_settings(project_name) - profiles = ( + return copy.deepcopy( project_settings ["global"] ["tools"] ["publish"] ["hero_template_name_profiles"] ) - if profiles: - return copy.deepcopy(profiles) - - # Use legacy approach for cases new settings are not filled yet for the - # project - legacy_profiles = copy.deepcopy( - project_settings - ["global"] - ["publish"] - ["IntegrateHeroVersion"] - ["template_name_profiles"] - ) - if legacy_profiles: - if not logger: - logger = Logger.get_logger("get_hero_template_name_profiles") - - logger.warning(( - "Project \"{}\" is using legacy access to hero publish template." - " It is recommended to move settings to new location" - " 'project_settings/global/tools/publish/" - "hero_template_name_profiles'." - ).format(project_name)) - return legacy_profiles def get_publish_template_name( diff --git a/openpype/plugins/publish/integrate_hero_version.py b/openpype/plugins/publish/integrate_hero_version.py index 9f0f7fe7f3..59dc6b5c64 100644 --- a/openpype/plugins/publish/integrate_hero_version.py +++ b/openpype/plugins/publish/integrate_hero_version.py @@ -54,7 +54,6 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): # permissions error on files (files were used or user didn't have perms) # *but all other plugins must be sucessfully completed - template_name_profiles = [] _default_template_name = "hero" def process(self, instance): 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 ac2d9e190d..64f292a140 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 @@ -1023,49 +1023,6 @@ { "type": "label", "label": "NOTE: Hero publish template profiles settings were moved to Tools/Publish/Hero template name profiles. Please move values there." - }, - { - "type": "list", - "key": "template_name_profiles", - "label": "Template name profiles (DEPRECATED)", - "use_label_wrap": true, - "object_type": { - "type": "dict", - "children": [ - { - "key": "families", - "label": "Families", - "type": "list", - "object_type": "text" - }, - { - "type": "hosts-enum", - "key": "hosts", - "label": "Hosts", - "multiselection": true - }, - { - "key": "task_types", - "label": "Task types", - "type": "task-types-enum" - }, - { - "key": "task_names", - "label": "Task names", - "type": "list", - "object_type": "text" - }, - { - "type": "separator" - }, - { - "type": "text", - "key": "template_name", - "label": "Template name", - "tooltip": "Name of template from Anatomy templates" - } - ] - } } ] }, diff --git a/server_addon/core/server/settings/publish_plugins.py b/server_addon/core/server/settings/publish_plugins.py index ef52416369..0c9b9c96ef 100644 --- a/server_addon/core/server/settings/publish_plugins.py +++ b/server_addon/core/server/settings/publish_plugins.py @@ -697,13 +697,6 @@ class IntegrateHeroVersionModel(BaseSettingsModel): optional: bool = Field(False, title="Optional") active: bool = Field(True, title="Active") families: list[str] = Field(default_factory=list, title="Families") - # TODO remove when removed from client code - template_name_profiles: list[IntegrateHeroTemplateNameProfileModel] = ( - Field( - default_factory=list, - title="Template name profiles" - ) - ) class CleanUpModel(BaseSettingsModel): @@ -1049,19 +1042,6 @@ DEFAULT_PUBLISH_VALUES = { "layout", "mayaScene", "simpleUnrealTexture" - ], - "template_name_profiles": [ - { - "product_types": [ - "simpleUnrealTexture" - ], - "hosts": [ - "standalonepublisher" - ], - "task_types": [], - "task_names": [], - "template_name": "simpleUnrealTextureHero" - } ] }, "CleanUp": { From faf22c7c6df76247af2ed7a653bbfe0b84220116 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 10 Jan 2024 03:25:43 +0000 Subject: [PATCH 203/291] [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 dba782ded4..279575d110 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.3-nightly.1" +__version__ = "3.18.3-nightly.2" From 4aec54f577f42b08fb4006c657ff4c94e109fa27 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 10 Jan 2024 03:26:22 +0000 Subject: [PATCH 204/291] 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 2e854061d5..7d6c5650d1 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.18.3-nightly.2 - 3.18.3-nightly.1 - 3.18.2 - 3.18.2-nightly.6 @@ -134,7 +135,6 @@ body: - 3.15.6 - 3.15.6-nightly.3 - 3.15.6-nightly.2 - - 3.15.6-nightly.1 validations: required: true - type: dropdown From 0a6edc648c0b5866b6025d82e091494840b17878 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 Jan 2024 12:03:08 +0000 Subject: [PATCH 205/291] Fix problem with AVALON_CONTAINER collection and workfile instance --- openpype/hosts/blender/api/plugin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 1037854a2d..b1ff3e4a09 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -311,11 +311,13 @@ class BaseCreator(Creator): ) return - # Rename the instance node in the scene if subset or asset changed + # Rename the instance node in the scene if subset or asset changed. + # Do not rename the instance if the family is workfile, as the + # workfile instance is included in the AVALON_CONTAINER collection. if ( "subset" in changes.changed_keys or asset_name_key in changes.changed_keys - ): + ) and created_instance.family != "workfile": asset_name = data[asset_name_key] if AYON_SERVER_ENABLED: asset_name = asset_name.split("/")[-1] From 399bb404c4d0deae30731db123be76dca1de38d8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 10 Jan 2024 17:10:52 +0100 Subject: [PATCH 206/291] Fusion: automatic installation of PySide2 (#6111) * OP-7450 - WIP of new hook to install PySide2 Currently not working yet as subprocess is invoking wrong `pip` which causes issue about missing `dataclasses`. * OP-7450 - updates querying of PySide2 presence Cannot use pip list as wrong pip from .venv is used and it was causing issue about missing dataclass (not in Python3.6). This implementation is simpler and just tries to import PySide2. * OP-7450 - typo * OP-7450 - removed forgotten raise for debugging * OP-7450 - double quotes Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * OP-7450 - return if error Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * OP-7450 - return False Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * OP-7450 - added optionality for InstallPySideToFusion New hook is controllable by Settings. * OP-7450 - updated querying of Qt This approach should be more generic, not tied to specific version of PySide2 * OP-7450 - fix unwanted change * OP-7450 - added settings for legacy OP * OP-7450 - use correct python executable name in Linux Because it is not "expected" python in blender but installed python, I would expect the executable is python3 on linux/macos rather than python. Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * OP-7450 - headless installation in Windows It checks first that it would need admin privileges for installation, if not it installs headlessly. If yes, it will create separate dialog that will ask for admin privileges. * OP-7450 - Hound * Update openpype/hosts/fusion/hooks/pre_pyside_install.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../hosts/fusion/hooks/pre_fusion_setup.py | 3 + .../hosts/fusion/hooks/pre_pyside_install.py | 186 ++++++++++++++++++ .../defaults/project_settings/fusion.json | 5 + .../schema_project_fusion.json | 23 +++ server_addon/fusion/server/settings.py | 49 +++-- server_addon/fusion/server/version.py | 2 +- 6 files changed, 253 insertions(+), 15 deletions(-) create mode 100644 openpype/hosts/fusion/hooks/pre_pyside_install.py diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index 576628e876..3da8968727 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -64,5 +64,8 @@ class FusionPrelaunch(PreLaunchHook): self.launch_context.env[py3_var] = py3_dir + # for hook installing PySide2 + self.data["fusion_python3_home"] = py3_dir + self.log.info(f"Setting OPENPYPE_FUSION: {FUSION_HOST_DIR}") self.launch_context.env["OPENPYPE_FUSION"] = FUSION_HOST_DIR diff --git a/openpype/hosts/fusion/hooks/pre_pyside_install.py b/openpype/hosts/fusion/hooks/pre_pyside_install.py new file mode 100644 index 0000000000..f98aeda233 --- /dev/null +++ b/openpype/hosts/fusion/hooks/pre_pyside_install.py @@ -0,0 +1,186 @@ +import os +import subprocess +import platform +import uuid + +from openpype.lib.applications import PreLaunchHook, LaunchTypes + + +class InstallPySideToFusion(PreLaunchHook): + """Automatically installs Qt binding to fusion's python packages. + + Check if fusion has installed PySide2 and will try to install if not. + + For pipeline implementation is required to have Qt binding installed in + fusion's python packages. + """ + + app_groups = {"fusion"} + order = 2 + launch_types = {LaunchTypes.local} + + def execute(self): + # Prelaunch hook is not crucial + try: + settings = self.data["project_settings"][self.host_name] + if not settings["hooks"]["InstallPySideToFusion"]["enabled"]: + return + self.inner_execute() + except Exception: + self.log.warning( + "Processing of {} crashed.".format(self.__class__.__name__), + exc_info=True + ) + + def inner_execute(self): + self.log.debug("Check for PySide2 installation.") + + fusion_python3_home = self.data.get("fusion_python3_home") + if not fusion_python3_home: + self.log.warning("'fusion_python3_home' was not provided. " + "Installation of PySide2 not possible") + return + + if platform.system().lower() == "windows": + exe_filenames = ["python.exe"] + else: + exe_filenames = ["python3", "python"] + + for exe_filename in exe_filenames: + python_executable = os.path.join(fusion_python3_home, exe_filename) + if os.path.exists(python_executable): + break + + if not os.path.exists(python_executable): + self.log.warning( + "Couldn't find python executable for fusion. {}".format( + python_executable + ) + ) + return + + # Check if PySide2 is installed and skip if yes + if self._is_pyside_installed(python_executable): + self.log.debug("Fusion has already installed PySide2.") + return + + self.log.debug("Installing PySide2.") + # Install PySide2 in fusion's python + if self._windows_require_permissions( + os.path.dirname(python_executable)): + result = self._install_pyside_windows(python_executable) + else: + result = self._install_pyside(python_executable) + + if result: + self.log.info("Successfully installed PySide2 module to fusion.") + else: + self.log.warning("Failed to install PySide2 module to fusion.") + + def _install_pyside_windows(self, python_executable): + """Install PySide2 python module to fusion's python. + + Installation requires administration rights that's why it is required + to use "pywin32" module which can execute command's and ask for + administration rights. + """ + try: + import win32api + import win32con + import win32process + import win32event + import pywintypes + from win32comext.shell.shell import ShellExecuteEx + from win32comext.shell import shellcon + except Exception: + self.log.warning("Couldn't import \"pywin32\" modules") + return False + + try: + # Parameters + # - use "-m pip" as module pip to install PySide2 and argument + # "--ignore-installed" is to force install module to fusion's + # site-packages and make sure it is binary compatible + parameters = "-m pip install --ignore-installed PySide2" + + # Execute command and ask for administrator's rights + process_info = ShellExecuteEx( + nShow=win32con.SW_SHOWNORMAL, + fMask=shellcon.SEE_MASK_NOCLOSEPROCESS, + lpVerb="runas", + lpFile=python_executable, + lpParameters=parameters, + lpDirectory=os.path.dirname(python_executable) + ) + process_handle = process_info["hProcess"] + win32event.WaitForSingleObject(process_handle, + win32event.INFINITE) + returncode = win32process.GetExitCodeProcess(process_handle) + return returncode == 0 + except pywintypes.error: + return False + + def _install_pyside(self, python_executable): + """Install PySide2 python module to fusion's python.""" + try: + # Parameters + # - use "-m pip" as module pip to install PySide2 and argument + # "--ignore-installed" is to force install module to fusion's + # site-packages and make sure it is binary compatible + env = dict(os.environ) + del env['PYTHONPATH'] + args = [ + python_executable, + "-m", + "pip", + "install", + "--ignore-installed", + "PySide2", + ] + process = subprocess.Popen( + args, stdout=subprocess.PIPE, universal_newlines=True, + env=env + ) + process.communicate() + return process.returncode == 0 + except PermissionError: + self.log.warning( + "Permission denied with command:" + "\"{}\".".format(" ".join(args)) + ) + except OSError as error: + self.log.warning(f"OS error has occurred: \"{error}\".") + except subprocess.SubprocessError: + pass + + def _is_pyside_installed(self, python_executable): + """Check if PySide2 module is in fusion's pip list.""" + args = [python_executable, "-c", "from qtpy import QtWidgets"] + process = subprocess.Popen(args, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + _, stderr = process.communicate() + stderr = stderr.decode() + if stderr: + return False + return True + + def _windows_require_permissions(self, dirpath): + if platform.system().lower() != "windows": + return False + + try: + # Attempt to create a temporary file in the folder + temp_file_path = os.path.join(dirpath, uuid.uuid4().hex) + with open(temp_file_path, "w"): + pass + os.remove(temp_file_path) # Clean up temporary file + return False + + except PermissionError: + return True + + except BaseException as exc: + print(("Failed to determine if root requires permissions." + "Unexpected error: {}").format(exc)) + return False diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index 0edcae060a..8579442625 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -15,6 +15,11 @@ "copy_status": false, "force_sync": false }, + "hooks": { + "InstallPySideToFusion": { + "enabled": true + } + }, "create": { "CreateSaver": { "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 5177d8bc7c..fbd856b895 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -41,6 +41,29 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "hooks", + "label": "Hooks", + "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "InstallPySideToFusion", + "label": "Install PySide2", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/server_addon/fusion/server/settings.py b/server_addon/fusion/server/settings.py index 1bc12773d2..21189b390e 100644 --- a/server_addon/fusion/server/settings.py +++ b/server_addon/fusion/server/settings.py @@ -25,16 +25,6 @@ def _create_saver_instance_attributes_enum(): ] -def _image_format_enum(): - return [ - {"value": "exr", "label": "exr"}, - {"value": "tga", "label": "tga"}, - {"value": "png", "label": "png"}, - {"value": "tif", "label": "tif"}, - {"value": "jpg", "label": "jpg"}, - ] - - class CreateSaverPluginModel(BaseSettingsModel): _isGroup = True temp_rendering_path_template: str = Field( @@ -49,9 +39,23 @@ class CreateSaverPluginModel(BaseSettingsModel): enum_resolver=_create_saver_instance_attributes_enum, title="Instance attributes" ) - image_format: str = Field( - enum_resolver=_image_format_enum, - title="Output Image Format" + output_formats: list[str] = Field( + default_factory=list, + title="Output formats" + ) + + +class HookOptionalModel(BaseSettingsModel): + enabled: bool = Field( + True, + title="Enabled" + ) + + +class HooksModel(BaseSettingsModel): + InstallPySideToFusion: HookOptionalModel = Field( + default_factory=HookOptionalModel, + title="Install PySide2" ) @@ -71,6 +75,10 @@ class FusionSettings(BaseSettingsModel): default_factory=CopyFusionSettingsModel, title="Local Fusion profile settings" ) + hooks: HooksModel = Field( + default_factory=HooksModel, + title="Hooks" + ) create: CreatPluginsModel = Field( default_factory=CreatPluginsModel, title="Creator plugins" @@ -93,6 +101,11 @@ DEFAULT_VALUES = { "copy_status": False, "force_sync": False }, + "hooks": { + "InstallPySideToFusion": { + "enabled": True + } + }, "create": { "CreateSaver": { "temp_rendering_path_template": "{workdir}/renders/fusion/{product[name]}/{product[name]}.{frame}.{ext}", @@ -104,7 +117,15 @@ DEFAULT_VALUES = { "reviewable", "farm_rendering" ], - "image_format": "exr" + "output_formats": [ + "exr", + "jpg", + "jpeg", + "jpg", + "tiff", + "png", + "tga" + ] } } } diff --git a/server_addon/fusion/server/version.py b/server_addon/fusion/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/fusion/server/version.py +++ b/server_addon/fusion/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" From aba61718b256ed9cafc7aedcd80ffe369570f0e6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Jan 2024 12:07:35 +0100 Subject: [PATCH 207/291] fix issue with parenting of widgets (#6106) --- openpype/hosts/nuke/api/pipeline.py | 8 ++------ openpype/tools/publisher/window.py | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 12562a6b6f..c2fc684c21 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -259,9 +259,7 @@ def _install_menu(): menu.addCommand( "Create...", lambda: host_tools.show_publisher( - parent=( - main_window if nuke.NUKE_VERSION_MAJOR >= 14 else None - ), + parent=main_window, tab="create" ) ) @@ -270,9 +268,7 @@ def _install_menu(): menu.addCommand( "Publish...", lambda: host_tools.show_publisher( - parent=( - main_window if nuke.NUKE_VERSION_MAJOR >= 14 else None - ), + parent=main_window, tab="publish" ) ) diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index b3138c3f45..20d9884788 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -189,7 +189,7 @@ class PublisherWindow(QtWidgets.QDialog): controller, content_stacked_widget ) - report_widget = ReportPageWidget(controller, parent) + report_widget = ReportPageWidget(controller, content_stacked_widget) # Details - Publish details publish_details_widget = PublishReportViewerWidget( From 4e704b1b0728a158ef824acef1601ede9d4162ee Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:18:58 +0100 Subject: [PATCH 208/291] AYON: OpenPype addon dependencies (#6113) * click is required by openpype addon * removed Qt.py from dependencies * add six to openpype addon dependencies --- server_addon/openpype/client/pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server_addon/openpype/client/pyproject.toml b/server_addon/openpype/client/pyproject.toml index d8de9d4d96..b5978f0498 100644 --- a/server_addon/openpype/client/pyproject.toml +++ b/server_addon/openpype/client/pyproject.toml @@ -7,15 +7,16 @@ python = ">=3.9.1,<3.10" aiohttp_json_rpc = "*" # TVPaint server aiohttp-middlewares = "^2.0.0" wsrpc_aiohttp = "^3.1.1" # websocket server +Click = "^8" clique = "1.6.*" jsonschema = "^2.6.0" pymongo = "^3.11.2" log4mongo = "^1.7" pyblish-base = "^1.8.11" pynput = "^1.7.2" # Timers manager - TODO remove -"Qt.py" = "^1.3.3" -qtawesome = "0.7.3" speedcopy = "^2.1" +six = "^1.15" +qtawesome = "0.7.3" [ayon.runtimeDependencies] OpenTimelineIO = "0.14.1" From adb7e19c232adad1e9fb122ba5cffdd55970916b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Jan 2024 13:21:41 +0100 Subject: [PATCH 209/291] Publisher: Window is not always on top (#6107) * make publisher a window without always on top * put publisher window to the top on process * make sure screenshot window is active * removed unnecessary variable --- .../publisher/widgets/screenshot_widget.py | 8 +++- openpype/tools/publisher/window.py | 38 ++++++++++++++----- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/openpype/tools/publisher/widgets/screenshot_widget.py b/openpype/tools/publisher/widgets/screenshot_widget.py index 3504b419b4..37b958c1c7 100644 --- a/openpype/tools/publisher/widgets/screenshot_widget.py +++ b/openpype/tools/publisher/widgets/screenshot_widget.py @@ -18,10 +18,11 @@ class ScreenMarquee(QtWidgets.QDialog): super(ScreenMarquee, self).__init__(parent=parent) self.setWindowFlags( - QtCore.Qt.FramelessWindowHint + QtCore.Qt.Window + | QtCore.Qt.FramelessWindowHint | QtCore.Qt.WindowStaysOnTopHint | QtCore.Qt.CustomizeWindowHint - | QtCore.Qt.Tool) + ) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.setCursor(QtCore.Qt.CrossCursor) self.setMouseTracking(True) @@ -210,6 +211,9 @@ class ScreenMarquee(QtWidgets.QDialog): """ tool = cls() + # Activate so Escape event is not ignored. + tool.setWindowState(QtCore.Qt.WindowActive) + # Exec dialog and return captured pixmap. tool.exec_() return tool.get_captured_pixmap() diff --git a/openpype/tools/publisher/window.py b/openpype/tools/publisher/window.py index 20d9884788..5dd6998b24 100644 --- a/openpype/tools/publisher/window.py +++ b/openpype/tools/publisher/window.py @@ -42,7 +42,7 @@ from .widgets import ( ) -class PublisherWindow(QtWidgets.QDialog): +class PublisherWindow(QtWidgets.QWidget): """Main window of publisher.""" default_width = 1300 default_height = 800 @@ -50,7 +50,7 @@ class PublisherWindow(QtWidgets.QDialog): publish_footer_spacer = 2 def __init__(self, parent=None, controller=None, reset_on_show=None): - super(PublisherWindow, self).__init__(parent) + super(PublisherWindow, self).__init__() self.setObjectName("PublishWindow") @@ -64,17 +64,12 @@ class PublisherWindow(QtWidgets.QDialog): if reset_on_show is None: reset_on_show = True - if parent is None: - on_top_flag = QtCore.Qt.WindowStaysOnTopHint - else: - on_top_flag = QtCore.Qt.Dialog - self.setWindowFlags( - QtCore.Qt.WindowTitleHint + QtCore.Qt.Window + | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowMaximizeButtonHint | QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowCloseButtonHint - | on_top_flag ) if controller is None: @@ -299,6 +294,12 @@ class PublisherWindow(QtWidgets.QDialog): controller.event_system.add_callback( "publish.process.stopped", self._on_publish_stop ) + controller.event_system.add_callback( + "publish.process.instance.changed", self._on_instance_change + ) + controller.event_system.add_callback( + "publish.process.plugin.changed", self._on_plugin_change + ) controller.event_system.add_callback( "show.card.message", self._on_overlay_message ) @@ -557,6 +558,18 @@ class PublisherWindow(QtWidgets.QDialog): self._reset_on_show = False self.reset() + def _make_sure_on_top(self): + """Raise window to top and activate it. + + This may not work for some DCCs without Qt. + """ + + if not self._window_is_visible: + self.show() + + self.setWindowState(QtCore.Qt.WindowActive) + self.raise_() + def _checks_before_save(self, explicit_save): """Save of changes may trigger some issues. @@ -869,6 +882,12 @@ class PublisherWindow(QtWidgets.QDialog): if self._is_on_create_tab(): self._go_to_publish_tab() + def _on_instance_change(self): + self._make_sure_on_top() + + def _on_plugin_change(self): + self._make_sure_on_top() + def _on_publish_validated_change(self, event): if event["value"]: self._validate_btn.setEnabled(False) @@ -879,6 +898,7 @@ class PublisherWindow(QtWidgets.QDialog): self._comment_input.setText("") def _on_publish_stop(self): + self._make_sure_on_top() self._set_publish_overlay_visibility(False) self._reset_btn.setEnabled(True) self._stop_btn.setEnabled(False) From dbf02e266f106c22e43c457705d0cb10acce5411 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 Jan 2024 21:10:13 +0800 Subject: [PATCH 210/291] create camera node with Camera4 instead of Camera2 in Nuke 14.0 --- openpype/hosts/nuke/api/lib.py | 17 +++++++++++++++++ .../hosts/nuke/plugins/create/create_camera.py | 5 ++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 88c587faf6..785727070d 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3483,3 +3483,20 @@ def get_filenames_without_hash(filename, frame_start, frame_end): new_filename = filename_without_hashes.format(frame) filenames.append(new_filename) return filenames + + +def create_camera_node_by_version(): + """Function to create the camera with the latest node class + For Nuke version 14.0 or later, the Camera4 camera node class + would be used + For the version before, the Camera2 camera node class + would be used + Returns: + Node: camera node + """ + nuke_version = nuke.NUKE_VERSION_STRING + nuke_number_version = next(ver for ver in re.findall("\d+\.\d+", nuke_version)) + if float(nuke_number_version) >= 14.0: + return nuke.createNode("Camera4") + else: + return nuke.createNode("Camera2") diff --git a/openpype/hosts/nuke/plugins/create/create_camera.py b/openpype/hosts/nuke/plugins/create/create_camera.py index b84280b11b..be9c69213e 100644 --- a/openpype/hosts/nuke/plugins/create/create_camera.py +++ b/openpype/hosts/nuke/plugins/create/create_camera.py @@ -4,6 +4,9 @@ from openpype.hosts.nuke.api import ( NukeCreatorError, maintained_selection ) +from openpype.hosts.nuke.api.lib import ( + create_camera_node_by_version +) class CreateCamera(NukeCreator): @@ -32,7 +35,7 @@ class CreateCamera(NukeCreator): "Creator error: Select only camera node type") created_node = self.selected_nodes[0] else: - created_node = nuke.createNode("Camera2") + created_node = create_camera_node_by_version() created_node["tile_color"].setValue( int(self.node_color, 16)) From b51f61796d6cd40417575f14cf89be72bdbb2990 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 Jan 2024 21:17:22 +0800 Subject: [PATCH 211/291] hound shut --- openpype/hosts/nuke/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 785727070d..f408ff1a9d 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3495,7 +3495,8 @@ def create_camera_node_by_version(): Node: camera node """ nuke_version = nuke.NUKE_VERSION_STRING - nuke_number_version = next(ver for ver in re.findall("\d+\.\d+", nuke_version)) + nuke_number_version = next(ver for ver in + re.findall("\d+\.\d+", nuke_version)) if float(nuke_number_version) >= 14.0: return nuke.createNode("Camera4") else: From ee8294824e821c3e8bb45f2f305c8bd9b961b614 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 Jan 2024 21:19:21 +0800 Subject: [PATCH 212/291] hound shut --- openpype/hosts/nuke/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index f408ff1a9d..b879c96c9e 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3495,8 +3495,9 @@ def create_camera_node_by_version(): Node: camera node """ nuke_version = nuke.NUKE_VERSION_STRING - nuke_number_version = next(ver for ver in - re.findall("\d+\.\d+", nuke_version)) + nuke_number_version = next( + ver for ver in re.findall( + r"\d+\.\d+", nuke_version)) if float(nuke_number_version) >= 14.0: return nuke.createNode("Camera4") else: From 199ff9944b3371105933bfaa4756e8f03833f11e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 11 Jan 2024 22:50:29 +0800 Subject: [PATCH 213/291] Jakub's comment - using nuke.NUKE_VERSION_MAJOR for version check instead --- openpype/hosts/nuke/api/lib.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index b879c96c9e..7ba53caead 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3494,11 +3494,8 @@ def create_camera_node_by_version(): Returns: Node: camera node """ - nuke_version = nuke.NUKE_VERSION_STRING - nuke_number_version = next( - ver for ver in re.findall( - r"\d+\.\d+", nuke_version)) - if float(nuke_number_version) >= 14.0: + nuke_number_version = nuke.NUKE_VERSION_MAJOR + if nuke_number_version >= 14: return nuke.createNode("Camera4") else: return nuke.createNode("Camera2") From 629b49c18209652f1c3a931e1f06b791b0180d92 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Jan 2024 16:15:45 +0100 Subject: [PATCH 214/291] Blender: Workfile instance update fix (#6048) * make sure workfile instance has always available 'instance_node' * create CONTAINERS node if does not exist yet --- .../blender/plugins/create/create_workfile.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_workfile.py b/openpype/hosts/blender/plugins/create/create_workfile.py index ceec3e0552..6b168f4c84 100644 --- a/openpype/hosts/blender/plugins/create/create_workfile.py +++ b/openpype/hosts/blender/plugins/create/create_workfile.py @@ -25,7 +25,7 @@ class CreateWorkfile(BaseCreator, AutoCreator): def create(self): """Create workfile instances.""" - existing_instance = next( + workfile_instance = next( ( instance for instance in self.create_context.instances if instance.creator_identifier == self.identifier @@ -39,14 +39,14 @@ class CreateWorkfile(BaseCreator, AutoCreator): host_name = self.create_context.host_name existing_asset_name = None - if existing_instance is not None: + if workfile_instance is not None: if AYON_SERVER_ENABLED: - existing_asset_name = existing_instance.get("folderPath") + existing_asset_name = workfile_instance.get("folderPath") if existing_asset_name is None: - existing_asset_name = existing_instance["asset"] + existing_asset_name = workfile_instance["asset"] - if not existing_instance: + if not workfile_instance: asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( task_name, task_name, asset_doc, project_name, host_name @@ -66,19 +66,18 @@ class CreateWorkfile(BaseCreator, AutoCreator): asset_doc, project_name, host_name, - existing_instance, + workfile_instance, ) ) self.log.info("Auto-creating workfile instance...") - current_instance = CreatedInstance( + workfile_instance = CreatedInstance( self.family, subset_name, data, self ) - instance_node = bpy.data.collections.get(AVALON_CONTAINERS, {}) - current_instance.transient_data["instance_node"] = instance_node - self._add_instance_to_context(current_instance) + self._add_instance_to_context(workfile_instance) + elif ( existing_asset_name != asset_name - or existing_instance["task"] != task_name + or workfile_instance["task"] != task_name ): # Update instance context if it's different asset_doc = get_asset_by_name(project_name, asset_name) @@ -86,12 +85,17 @@ class CreateWorkfile(BaseCreator, AutoCreator): task_name, task_name, asset_doc, project_name, host_name ) if AYON_SERVER_ENABLED: - existing_instance["folderPath"] = asset_name + workfile_instance["folderPath"] = asset_name else: - existing_instance["asset"] = asset_name + workfile_instance["asset"] = asset_name - existing_instance["task"] = task_name - existing_instance["subset"] = subset_name + workfile_instance["task"] = task_name + workfile_instance["subset"] = subset_name + + instance_node = bpy.data.collections.get(AVALON_CONTAINERS) + if not instance_node: + instance_node = bpy.data.collections.new(name=AVALON_CONTAINERS) + workfile_instance.transient_data["instance_node"] = instance_node def collect_instances(self): From 47cf95ed69a4bb4ad4f5a9f2b5b09f145a94bc23 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Jan 2024 10:13:20 +0100 Subject: [PATCH 215/291] Chore: Template data for editorial publishing (#6120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * start with anatomy data without anatomy updates * added ability to fill template data for editorial instances too * do not autofix editorial data in collect resources path * fix childs access --------- Co-authored-by: Jakub Ježek --- .../publish/collect_anatomy_instance_data.py | 211 ++++++++++++++---- .../plugins/publish/collect_resources_path.py | 13 -- 2 files changed, 168 insertions(+), 56 deletions(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index 1b4b44e40e..0a34848166 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -190,47 +190,18 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): project_task_types = project_doc["config"]["tasks"] for instance in context: - asset_doc = instance.data.get("assetEntity") - anatomy_updates = { + anatomy_data = copy.deepcopy(context.data["anatomyData"]) + anatomy_data.update({ "family": instance.data["family"], "subset": instance.data["subset"], - } - if asset_doc: - parents = asset_doc["data"].get("parents") or list() - parent_name = project_doc["name"] - if parents: - parent_name = parents[-1] + }) - hierarchy = "/".join(parents) - anatomy_updates.update({ - "asset": asset_doc["name"], - "hierarchy": hierarchy, - "parent": parent_name, - "folder": { - "name": asset_doc["name"], - }, - }) - - # Task - task_type = None - task_name = instance.data.get("task") - if task_name: - asset_tasks = asset_doc["data"]["tasks"] - task_type = asset_tasks.get(task_name, {}).get("type") - task_code = ( - project_task_types - .get(task_type, {}) - .get("short_name") - ) - anatomy_updates["task"] = { - "name": task_name, - "type": task_type, - "short": task_code - } + self._fill_asset_data(instance, project_doc, anatomy_data) + self._fill_task_data(instance, project_task_types, anatomy_data) # Define version if self.follow_workfile_version: - version_number = context.data('version') + version_number = context.data("version") else: version_number = instance.data.get("version") @@ -242,6 +213,9 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): # If version is not specified for instance or context if version_number is None: + task_data = anatomy_data.get("task") or {} + task_name = task_data.get("name") + task_type = task_data.get("type") version_number = get_versioning_start( context.data["projectName"], instance.context.data["hostName"], @@ -250,29 +224,26 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): family=instance.data["family"], subset=instance.data["subset"] ) - anatomy_updates["version"] = version_number + anatomy_data["version"] = version_number # Additional data resolution_width = instance.data.get("resolutionWidth") if resolution_width: - anatomy_updates["resolution_width"] = resolution_width + anatomy_data["resolution_width"] = resolution_width resolution_height = instance.data.get("resolutionHeight") if resolution_height: - anatomy_updates["resolution_height"] = resolution_height + anatomy_data["resolution_height"] = resolution_height pixel_aspect = instance.data.get("pixelAspect") if pixel_aspect: - anatomy_updates["pixel_aspect"] = float( + anatomy_data["pixel_aspect"] = float( "{:0.2f}".format(float(pixel_aspect)) ) fps = instance.data.get("fps") if fps: - anatomy_updates["fps"] = float("{:0.2f}".format(float(fps))) - - anatomy_data = copy.deepcopy(context.data["anatomyData"]) - anatomy_data.update(anatomy_updates) + anatomy_data["fps"] = float("{:0.2f}".format(float(fps))) # Store anatomy data instance.data["projectEntity"] = project_doc @@ -288,3 +259,157 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): instance_name, json.dumps(anatomy_data, indent=4) )) + + def _fill_asset_data(self, instance, project_doc, anatomy_data): + # QUESTION should we make sure that all asset data are poped if asset + # data cannot be found? + # - 'asset', 'hierarchy', 'parent', 'folder' + asset_doc = instance.data.get("assetEntity") + if asset_doc: + parents = asset_doc["data"].get("parents") or list() + parent_name = project_doc["name"] + if parents: + parent_name = parents[-1] + + hierarchy = "/".join(parents) + anatomy_data.update({ + "asset": asset_doc["name"], + "hierarchy": hierarchy, + "parent": parent_name, + "folder": { + "name": asset_doc["name"], + }, + }) + return + + if instance.data.get("newAssetPublishing"): + hierarchy = instance.data["hierarchy"] + anatomy_data["hierarchy"] = hierarchy + + parent_name = project_doc["name"] + if hierarchy: + parent_name = hierarchy.split("/")[-1] + + asset_name = instance.data["asset"].split("/")[-1] + anatomy_data.update({ + "asset": asset_name, + "hierarchy": hierarchy, + "parent": parent_name, + "folder": { + "name": asset_name, + }, + }) + + def _fill_task_data(self, instance, project_task_types, anatomy_data): + # QUESTION should we make sure that all task data are poped if task + # data cannot be resolved? + # - 'task' + + # Skip if there is no task + task_name = instance.data.get("task") + if not task_name: + return + + # Find task data based on asset entity + asset_doc = instance.data.get("assetEntity") + task_data = self._get_task_data_from_asset( + asset_doc, task_name, project_task_types + ) + if task_data: + # Fill task data + # - if we're in editorial, make sure the task type is filled + if ( + not instance.data.get("newAssetPublishing") + or task_data["type"] + ): + anatomy_data["task"] = task_data + return + + # New hierarchy is not created, so we can only skip rest of the logic + if not instance.data.get("newAssetPublishing"): + return + + # Try to find task data based on hierarchy context and asset name + hierarchy_context = instance.context.data.get("hierarchyContext") + asset_name = instance.data.get("asset") + if not hierarchy_context or not asset_name: + return + + project_name = instance.context.data["projectName"] + # OpenPype approach vs AYON approach + if "/" not in asset_name: + tasks_info = self._find_tasks_info_in_hierarchy( + hierarchy_context, asset_name + ) + else: + current_data = hierarchy_context.get(project_name, {}) + for key in asset_name.split("/"): + if key: + current_data = current_data.get("childs", {}).get(key, {}) + tasks_info = current_data.get("tasks", {}) + + task_info = tasks_info.get(task_name, {}) + task_type = task_info.get("type") + task_code = ( + project_task_types + .get(task_type, {}) + .get("short_name") + ) + anatomy_data["task"] = { + "name": task_name, + "type": task_type, + "short": task_code + } + + def _get_task_data_from_asset( + self, asset_doc, task_name, project_task_types + ): + """ + + Args: + asset_doc (Union[dict[str, Any], None]): Asset document. + task_name (Union[str, None]): Task name. + project_task_types (dict[str, dict[str, Any]]): Project task + types. + + Returns: + Union[dict[str, str], None]: Task data or None if not found. + """ + + if not asset_doc or not task_name: + return None + + asset_tasks = asset_doc["data"]["tasks"] + task_type = asset_tasks.get(task_name, {}).get("type") + task_code = ( + project_task_types + .get(task_type, {}) + .get("short_name") + ) + return { + "name": task_name, + "type": task_type, + "short": task_code + } + + def _find_tasks_info_in_hierarchy(self, hierarchy_context, asset_name): + """Find tasks info for an asset in editorial hierarchy. + + Args: + hierarchy_context (dict[str, Any]): Editorial hierarchy context. + asset_name (str): Asset name. + + Returns: + dict[str, dict[str, Any]]: Tasks info by name. + """ + + hierarchy_queue = collections.deque() + hierarchy_queue.append(hierarchy_context) + while hierarchy_queue: + item = hierarchy_context.popleft() + if asset_name in item: + return item[asset_name].get("tasks") or {} + + for subitem in item.values(): + hierarchy_queue.extend(subitem.get("childs") or []) + return {} diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index c8b67a3d05..6a871124f1 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -79,19 +79,6 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "representation": "TEMP" }) - # Add fill keys for editorial publishing creating new entity - # TODO handle in editorial plugin - if instance.data.get("newAssetPublishing"): - if "hierarchy" not in template_data: - template_data["hierarchy"] = instance.data["hierarchy"] - - if "asset" not in template_data: - asset_name = instance.data["asset"].split("/")[-1] - template_data["asset"] = asset_name - template_data["folder"] = { - "name": asset_name - } - publish_templates = anatomy.templates_obj["publish"] if "folder" in publish_templates: publish_folder = publish_templates["folder"].format_strict( From 046154037bc3514ef30d0448e2c7c1006d56970f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 12 Jan 2024 10:44:01 +0100 Subject: [PATCH 216/291] Site Sync: small fixes in Loader (#6119) * Fix usage of correct values Returned item is dictionary of version_id: links, previous loop was looping through [[]]. * Fix usage of studio icon local and studio have both same provider, local_drive. Both of them should be differentiate by icon though. * Fix - pull only paths from icon_def Icon_def is dictionary with `type` and `path` keys, not directly 'path'. It must be massaged first. * Revert back, fixed in different PR Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * Fix looping Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/client/server/entity_links.py | 23 ++++++++++--------- .../tools/ayon_loader/models/site_sync.py | 15 ++++++++---- .../ayon_sceneinventory/models/site_sync.py | 4 ++-- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/openpype/client/server/entity_links.py b/openpype/client/server/entity_links.py index 368dcdcb9d..7fb9fbde6f 100644 --- a/openpype/client/server/entity_links.py +++ b/openpype/client/server/entity_links.py @@ -124,23 +124,24 @@ def get_linked_representation_id( if not versions_to_check: break - links = con.get_versions_links( + versions_links = con.get_versions_links( project_name, versions_to_check, link_types=link_types, link_direction="out") versions_to_check = set() - for link in links: - # Care only about version links - if link["entityType"] != "version": - continue - entity_id = link["entityId"] - # Skip already found linked version ids - if entity_id in linked_version_ids: - continue - linked_version_ids.add(entity_id) - versions_to_check.add(entity_id) + for links in versions_links.values(): + for link in links: + # Care only about version links + if link["entityType"] != "version": + continue + entity_id = link["entityId"] + # Skip already found linked version ids + if entity_id in linked_version_ids: + continue + linked_version_ids.add(entity_id) + versions_to_check.add(entity_id) linked_version_ids.remove(version_id) if not linked_version_ids: diff --git a/openpype/tools/ayon_loader/models/site_sync.py b/openpype/tools/ayon_loader/models/site_sync.py index 90852b6954..4b7ddee481 100644 --- a/openpype/tools/ayon_loader/models/site_sync.py +++ b/openpype/tools/ayon_loader/models/site_sync.py @@ -140,12 +140,10 @@ class SiteSyncModel: Union[dict[str, Any], None]: Site icon definition. """ - if not project_name: + if not project_name or not self.is_site_sync_enabled(project_name): return None - active_site = self.get_active_site(project_name) - provider = self._get_provider_for_site(project_name, active_site) - return self._get_provider_icon(provider) + return self._get_site_icon_def(project_name, active_site) def get_remote_site_icon_def(self, project_name): """Remote site icon definition. @@ -160,7 +158,14 @@ class SiteSyncModel: if not project_name or not self.is_site_sync_enabled(project_name): return None remote_site = self.get_remote_site(project_name) - provider = self._get_provider_for_site(project_name, remote_site) + return self._get_site_icon_def(project_name, remote_site) + + def _get_site_icon_def(self, project_name, site_name): + # use different icon for studio even if provider is 'local_drive' + if site_name == self._site_sync_addon.DEFAULT_SITE: + provider = "studio" + else: + provider = self._get_provider_for_site(project_name, site_name) return self._get_provider_icon(provider) def get_version_sync_availability(self, project_name, version_ids): diff --git a/openpype/tools/ayon_sceneinventory/models/site_sync.py b/openpype/tools/ayon_sceneinventory/models/site_sync.py index 1297137cb0..0101f6c88e 100644 --- a/openpype/tools/ayon_sceneinventory/models/site_sync.py +++ b/openpype/tools/ayon_sceneinventory/models/site_sync.py @@ -42,8 +42,8 @@ class SiteSyncModel: if not self.is_sync_server_enabled(): return {} - site_sync = self._get_sync_server_module() - return site_sync.get_site_icons() + site_sync_addon = self._get_sync_server_module() + return site_sync_addon.get_site_icons() def get_sites_information(self): return { From df7b8683716ff5bb2ab6b648ab22a62e2a555fd3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Jan 2024 18:32:42 +0800 Subject: [PATCH 217/291] bugfix the thumbnail error when publishing with emissive map & maps without RGB channel --- openpype/hosts/substancepainter/api/lib.py | 50 +++++++++++++++++++ .../publish/collect_textureset_images.py | 20 +++++--- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 1cb480b552..f46426388b 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -643,3 +643,53 @@ def prompt_new_file_with_mesh(mesh_filepath): return return project_mesh + + +def has_rgb_channel_in_texture_set(texture_set_name, map_identifier): + """Function to check whether the texture has RGB channel. + + Args: + texture_set_name (str): Name of Texture Set + map_identifier (str): Map identifier + + Returns: + colorspace_dict: A dictionary which stores the boolean + value of textures having RGB channels + """ + texture_stack = substance_painter.textureset.Stack.from_name(texture_set_name) + # 2D_View is always True as it exports all texture maps + colorspace_dict = {"2D_View": True} + colorspace_dict["BaseColor"] = texture_stack.get_channel( + substance_painter.textureset.ChannelType.BaseColor).is_color() + colorspace_dict["Roughness"] = texture_stack.get_channel( + substance_painter.textureset.ChannelType.Roughness).is_color() + colorspace_dict["Metallic"] = texture_stack.get_channel( + substance_painter.textureset.ChannelType.Metallic).is_color() + colorspace_dict["Height"] = texture_stack.get_channel( + substance_painter.textureset.ChannelType.Height).is_color() + colorspace_dict["Normal"] = texture_stack.get_channel( + substance_painter.textureset.ChannelType.Normal).is_color() + return colorspace_dict.get(map_identifier, False) + + +def texture_set_filtering(texture_set_same, template): + """Function to check whether some specific textures(e.g. Emissive) + are parts of the texture stack in Substance Painter + + Args: + texture_set_same (str): Name of Texture Set + template (str): texture template name + + Returns: + texture_filter: A dictionary which stores the boolean + value of whether the texture exist in the channel. + """ + texture_filter = {} + channel_stack = substance_painter.textureset.Stack.from_name( + texture_set_same) + has_emissive = channel_stack.has_channel( + substance_painter.textureset.ChannelType.Emissive) + map_identifier = strip_template(template) + if map_identifier == "Emissive": + texture_filter[map_identifier] = has_emissive + return texture_filter.get(map_identifier, True) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 316f72509e..4c3398d5b4 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -7,7 +7,9 @@ from openpype.pipeline import publish import substance_painter.textureset from openpype.hosts.substancepainter.api.lib import ( get_parsed_export_maps, - strip_template + strip_template, + has_rgb_channel_in_texture_set, + texture_set_filtering ) from openpype.pipeline.create import get_subset_name from openpype.client import get_asset_by_name @@ -39,11 +41,12 @@ class CollectTextureSet(pyblish.api.InstancePlugin): for (texture_set_name, stack_name), template_maps in maps.items(): self.log.info(f"Processing {texture_set_name}/{stack_name}") for template, outputs in template_maps.items(): - self.log.info(f"Processing {template}") - self.create_image_instance(instance, template, outputs, - asset_doc=asset_doc, - texture_set_name=texture_set_name, - stack_name=stack_name) + if texture_set_filtering(texture_set_name, template): + self.log.info(f"Processing {template}") + self.create_image_instance(instance, template, outputs, + asset_doc=asset_doc, + texture_set_name=texture_set_name, + stack_name=stack_name) def create_image_instance(self, instance, template, outputs, asset_doc, texture_set_name, stack_name): @@ -78,7 +81,6 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Always include the map identifier map_identifier = strip_template(template) suffix += f".{map_identifier}" - image_subset = get_subset_name( # TODO: The family actually isn't 'texture' currently but for now # this is only done so the subset name starts with 'texture' @@ -132,7 +134,9 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Store color space with the instance # Note: The extractor will assign it to the representation colorspace = outputs[0].get("colorSpace") - if colorspace: + has_rgb_channel = has_rgb_channel_in_texture_set( + texture_set_name, map_identifier) + if colorspace and has_rgb_channel: self.log.debug(f"{image_subset} colorspace: {colorspace}") image_instance.data["colorspace"] = colorspace From 72848657af4e52aa6d037fbf1f35c2b4548efeee Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Jan 2024 18:44:18 +0800 Subject: [PATCH 218/291] hound shut --- openpype/hosts/substancepainter/api/lib.py | 6 ++++-- .../plugins/publish/collect_textureset_images.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index f46426388b..67229a75bf 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -656,7 +656,9 @@ def has_rgb_channel_in_texture_set(texture_set_name, map_identifier): colorspace_dict: A dictionary which stores the boolean value of textures having RGB channels """ - texture_stack = substance_painter.textureset.Stack.from_name(texture_set_name) + texture_stack = ( + substance_painter.textureset.Stack.from_name(texture_set_name) + ) # 2D_View is always True as it exports all texture maps colorspace_dict = {"2D_View": True} colorspace_dict["BaseColor"] = texture_stack.get_channel( @@ -686,7 +688,7 @@ def texture_set_filtering(texture_set_same, template): """ texture_filter = {} channel_stack = substance_painter.textureset.Stack.from_name( - texture_set_same) + texture_set_same) has_emissive = channel_stack.has_channel( substance_painter.textureset.ChannelType.Emissive) map_identifier = strip_template(template) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 4c3398d5b4..f535bfa2a6 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -44,9 +44,9 @@ class CollectTextureSet(pyblish.api.InstancePlugin): if texture_set_filtering(texture_set_name, template): self.log.info(f"Processing {template}") self.create_image_instance(instance, template, outputs, - asset_doc=asset_doc, - texture_set_name=texture_set_name, - stack_name=stack_name) + asset_doc=asset_doc, + texture_set_name=texture_set_name, + stack_name=stack_name) def create_image_instance(self, instance, template, outputs, asset_doc, texture_set_name, stack_name): From 6410f381f34625defa91250a8e529e47cb40a8a6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 12 Jan 2024 18:45:40 +0800 Subject: [PATCH 219/291] hound shut --- .../plugins/publish/collect_textureset_images.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index f535bfa2a6..9d6aa06872 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -43,10 +43,11 @@ class CollectTextureSet(pyblish.api.InstancePlugin): for template, outputs in template_maps.items(): if texture_set_filtering(texture_set_name, template): self.log.info(f"Processing {template}") - self.create_image_instance(instance, template, outputs, - asset_doc=asset_doc, - texture_set_name=texture_set_name, - stack_name=stack_name) + self.create_image_instance( + instance, template, outputs, + asset_doc=asset_doc, + texture_set_name=texture_set_name, + stack_name=stack_name) def create_image_instance(self, instance, template, outputs, asset_doc, texture_set_name, stack_name): From 1eb7e59b931e146481253af9fab501120d1d6156 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Jan 2024 12:25:00 +0100 Subject: [PATCH 220/291] Kitsu clear credentials are safe (#6116) --- openpype/modules/kitsu/utils/credentials.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/modules/kitsu/utils/credentials.py b/openpype/modules/kitsu/utils/credentials.py index 941343cc8d..c471b56907 100644 --- a/openpype/modules/kitsu/utils/credentials.py +++ b/openpype/modules/kitsu/utils/credentials.py @@ -64,8 +64,10 @@ def clear_credentials(): user_registry = OpenPypeSecureRegistry("kitsu_user") # Set local settings - user_registry.delete_item("login") - user_registry.delete_item("password") + if user_registry.get_item("login", None) is not None: + user_registry.delete_item("login") + if user_registry.get_item("password", None) is not None: + user_registry.delete_item("password") def save_credentials(login: str, password: str): @@ -92,8 +94,9 @@ def load_credentials() -> Tuple[str, str]: # Get user registry user_registry = OpenPypeSecureRegistry("kitsu_user") - return user_registry.get_item("login", None), user_registry.get_item( - "password", None + return ( + user_registry.get_item("login", None), + user_registry.get_item("password", None) ) From f88ab85cc1d586e71e6a4ccfb975ed7d5c75aef8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Jan 2024 13:50:34 +0100 Subject: [PATCH 221/291] SceneInventory: Fix site sync icon conversion (#6123) * use 'get_qt_icon' to convert icon definition * check if site sync is enabled before getting sites info * convert containers to list * Fix wrong method name --------- Co-authored-by: Petr Kalis --- openpype/tools/ayon_sceneinventory/control.py | 4 ++-- openpype/tools/ayon_sceneinventory/model.py | 5 +++-- openpype/tools/ayon_sceneinventory/models/site_sync.py | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/tools/ayon_sceneinventory/control.py b/openpype/tools/ayon_sceneinventory/control.py index 6111d7e43b..3b063ff72e 100644 --- a/openpype/tools/ayon_sceneinventory/control.py +++ b/openpype/tools/ayon_sceneinventory/control.py @@ -84,9 +84,9 @@ class SceneInventoryController: def get_containers(self): host = self._host if isinstance(host, ILoadHost): - return host.get_containers() + return list(host.get_containers()) elif hasattr(host, "ls"): - return host.ls() + return list(host.ls()) return [] # Site Sync methods diff --git a/openpype/tools/ayon_sceneinventory/model.py b/openpype/tools/ayon_sceneinventory/model.py index 16924b0a7e..f4450f0ac3 100644 --- a/openpype/tools/ayon_sceneinventory/model.py +++ b/openpype/tools/ayon_sceneinventory/model.py @@ -23,6 +23,7 @@ from openpype.pipeline import ( ) from openpype.style import get_default_entity_icon_color from openpype.tools.utils.models import TreeModel, Item +from openpype.tools.ayon_utils.widgets import get_qt_icon def walk_hierarchy(node): @@ -71,8 +72,8 @@ class InventoryModel(TreeModel): site_icons = self._controller.get_site_provider_icons() self._site_icons = { - provider: QtGui.QIcon(icon_path) - for provider, icon_path in site_icons.items() + provider: get_qt_icon(icon_def) + for provider, icon_def in site_icons.items() } def outdated(self, item): diff --git a/openpype/tools/ayon_sceneinventory/models/site_sync.py b/openpype/tools/ayon_sceneinventory/models/site_sync.py index 0101f6c88e..bd65ad1778 100644 --- a/openpype/tools/ayon_sceneinventory/models/site_sync.py +++ b/openpype/tools/ayon_sceneinventory/models/site_sync.py @@ -150,23 +150,23 @@ class SiteSyncModel: return self._remote_site_provider def _cache_sites(self): - site_sync = self._get_sync_server_module() active_site = None remote_site = None active_site_provider = None remote_site_provider = None - if site_sync is not None: + if self.is_sync_server_enabled(): + site_sync = self._get_sync_server_module() project_name = self._controller.get_current_project_name() active_site = site_sync.get_active_site(project_name) remote_site = site_sync.get_remote_site(project_name) active_site_provider = "studio" remote_site_provider = "studio" if active_site != "studio": - active_site_provider = site_sync.get_active_provider( + active_site_provider = site_sync.get_provider_for_site( project_name, active_site ) if remote_site != "studio": - remote_site_provider = site_sync.get_active_provider( + remote_site_provider = site_sync.get_provider_for_site( project_name, remote_site ) From 946b9318b66b96c1606e9d3805024a431e5be2a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Jan 2024 13:51:08 +0100 Subject: [PATCH 222/291] add 'outputName' to thumbnail representation (#6114) --- openpype/plugins/publish/extract_thumbnail_from_source.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_thumbnail_from_source.py b/openpype/plugins/publish/extract_thumbnail_from_source.py index 401a5d615d..33cbf6d9bf 100644 --- a/openpype/plugins/publish/extract_thumbnail_from_source.py +++ b/openpype/plugins/publish/extract_thumbnail_from_source.py @@ -65,7 +65,8 @@ class ExtractThumbnailFromSource(pyblish.api.InstancePlugin): "files": dst_filename, "stagingDir": dst_staging, "thumbnail": True, - "tags": ["thumbnail"] + "tags": ["thumbnail"], + "outputName": "thumbnail", } # adding representation From c506813c345429873e54e10c0cc42fa20517bdab Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 12 Jan 2024 12:59:28 +0000 Subject: [PATCH 223/291] [Automated] Release --- CHANGELOG.md | 256 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 258 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a21882008..7b51fade6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,262 @@ # Changelog +## [3.18.3](https://github.com/ynput/OpenPype/tree/3.18.3) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.2...3.18.3) + +### **🚀 Enhancements** + + +
+Maya: Apply initial viewport shader for Redshift Proxy after loading #6102 + +When the published redshift proxy is being loaded, the shader of the proxy is missing. This is different from the manual load through creating redshift proxy for files. This PR is to assign the default lambert to the redshift proxy, which replicates the same approach when the user manually loads the proxy with filepath. + + +___ + +
+ + +
+General: We should keep current subset version when we switch only the representation type #4629 + +When we switch only the representation type of subsets, we should not get the representation from the last version of the subset. + + +___ + +
+ + +
+Houdini: Add loader for redshift proxy family #5948 + +Loader for Redshift Proxy in Houdini (Thanks for @BigRoy contribution) + + +___ + +
+ + +
+AfterEffects: exposing Deadline pools fields in Publisher UI #6079 + +Deadline pools might be adhoc set by an artist during publishing. AfterEffects implementation wasn't providing this. + + +___ + +
+ + +
+Chore: Event callbacks can have order #6080 + +Event callbacks can have order in which are called, and fixed issue with getting function name and file when using `partial` function as callback. + + +___ + +
+ + +
+AYON: OpenPype addon defines runtime dependencies #6095 + +Moved runtime dependencies from ayon-launcher to openpype addon. + + +___ + +
+ + +
+Max: User's setting for scene unit scale #6097 + +Options for users to set the default scene unit scale for their scenes.AYONLegacy OP + + +___ + +
+ + +
+Chore: Remove deprecated templates profiles #6103 + +Remove deprecated usage of template profiles from settings. + + +___ + +
+ + +
+Publisher: Window is not always on top #6107 + +Goal of this PR is to avoid using `WindowStaysOnTopHint` which causes issues, especially in cases when DCC shows a popup dialog that is behind the window, in that case both Publisher and DCC are frozen and there is nothing to do. + + +___ + +
+ + +
+Houdini: add split job export support for Redshift ROP #6108 + +This is adding support for splitting of export and render jobs for Redshift as is already implemented for Vray, Mantra and Arnold. + + +___ + +
+ + +
+Fusion: automatic installation of PySide2 #6111 + +This PR adds hook which tries to check if PySide2 is installed in Python used by Fusion and if not, it tries to install it automatically. + + +___ + +
+ + +
+AYON: OpenPype addon dependencies #6113 + +Added `click` and `six` to requirements of openpype addon, and removed `Qt.py` requirement, which is not used anywhere. + + +___ + +
+ + +
+Chore: Thumbnail representation has 'outputName' #6114 + +Add thumbnail output name to thumbnail representation to prevent same output filename during integration. + + +___ + +
+ + +
+Kitsu: Clear credentials is safe #6116 + +Do not remove not existing keyring items. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: bug fix the playblast without textures #5942 + +Bug fix the texture not being displayed when users enable texture placement in the OP/AYON setting + + +___ + +
+ + +
+Blender: Workfile instance update fix #6048 + +Make sure workfile instance has always available 'instance_node' in transient data. + + +___ + +
+ + +
+Publisher: Fix issue with parenting of widgets #6106 + +Don't use publisher window parent (usually main DCC window) as parent for report widget. + + +___ + +
+ + +
+:wrench: fix and update pydocstyle configuration #6109 + +Fix pydocstyle configuration and move it to `pyproject.toml` + + +___ + +
+ + +
+Nuke: Create camera node with the latest camera node class in Nuke 14 #6118 + +Creating instance fails for certain cameras, and it seems to only exist in Nuke 14. The reason of causing that contributes to the new camera node class `Camera4` while the camera creator is working with the `Camera2` class. + + +___ + +
+ + +
+Site Sync: small fixes in Loader #6119 + +Resolves issue: +- local and studio icons were same, they should be different +- `TypeError: string indices must be integers` error when downloading/uploading workfiles + + +___ + +
+ + +
+Chore: Template data for editorial publishing #6120 + +Template data for editorial publishing are filled during `CollectInstanceAnatomyData`. The structure for editorial is determined, as it's required for ExtractHierarchy AYON/OpenPype plugins. + + +___ + +
+ + +
+SceneInventory: Fix site sync icon conversion #6123 + +Use 'get_qt_icon' to convert icon definitions from site sync. + + +___ + +
+ + + + ## [3.18.2](https://github.com/ynput/OpenPype/tree/3.18.2) diff --git a/openpype/version.py b/openpype/version.py index 279575d110..c87c143ea3 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.3-nightly.2" +__version__ = "3.18.3" diff --git a/pyproject.toml b/pyproject.toml index ee8e8017e3..bad481c889 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.18.2" # OpenPype +version = "3.18.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From b5b85f7b7fe533e19c2a08ebb5d277ec819d6c2c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 12 Jan 2024 13:00:24 +0000 Subject: [PATCH 224/291] 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 7d6c5650d1..3f762bd2d8 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.18.3 - 3.18.3-nightly.2 - 3.18.3-nightly.1 - 3.18.2 @@ -134,7 +135,6 @@ body: - 3.15.7-nightly.1 - 3.15.6 - 3.15.6-nightly.3 - - 3.15.6-nightly.2 validations: required: true - type: dropdown From e9d38f24f49784021bccd19f18b189fc47e91276 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 12 Jan 2024 15:03:47 +0000 Subject: [PATCH 225/291] Renamed variable --- openpype/hosts/blender/plugins/load/load_animation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_animation.py b/openpype/hosts/blender/plugins/load/load_animation.py index 0f968c75e5..fd087553f0 100644 --- a/openpype/hosts/blender/plugins/load/load_animation.py +++ b/openpype/hosts/blender/plugins/load/load_animation.py @@ -61,10 +61,10 @@ class BlendAnimationLoader(plugin.AssetLoader): bpy.data.objects.remove(container) - filepath = bpy.path.basename(libpath) + filename = bpy.path.basename(libpath) # Blender has a limit of 63 characters for any data name. - # If the filepath is longer, it will be truncated. - if len(filepath) > 63: - filepath = filepath[:63] - library = bpy.data.libraries.get(filepath) + # If the filename is longer, it will be truncated. + if len(filename) > 63: + filename = filename[:63] + library = bpy.data.libraries.get(filename) bpy.data.libraries.remove(library) From 00eb748b4b64339b00799b5fa4c41ad42426f73c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 13 Jan 2024 00:15:46 +0800 Subject: [PATCH 226/291] code tweaks on has_rgb_channel_in_texture_set function & add publish data into the imageinstance --- openpype/hosts/substancepainter/api/lib.py | 48 +++++-------------- .../publish/collect_textureset_images.py | 12 ++--- .../plugins/publish/validate_ouput_maps.py | 1 + 3 files changed, 19 insertions(+), 42 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 67229a75bf..896cca79b0 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -653,45 +653,23 @@ def has_rgb_channel_in_texture_set(texture_set_name, map_identifier): map_identifier (str): Map identifier Returns: - colorspace_dict: A dictionary which stores the boolean - value of textures having RGB channels + bool: Whether the channel type identifier has RGB channel or not + in the texture stack. """ + + # 2D_View is always True as it exports all texture maps texture_stack = ( substance_painter.textureset.Stack.from_name(texture_set_name) ) # 2D_View is always True as it exports all texture maps - colorspace_dict = {"2D_View": True} - colorspace_dict["BaseColor"] = texture_stack.get_channel( - substance_painter.textureset.ChannelType.BaseColor).is_color() - colorspace_dict["Roughness"] = texture_stack.get_channel( - substance_painter.textureset.ChannelType.Roughness).is_color() - colorspace_dict["Metallic"] = texture_stack.get_channel( - substance_painter.textureset.ChannelType.Metallic).is_color() - colorspace_dict["Height"] = texture_stack.get_channel( - substance_painter.textureset.ChannelType.Height).is_color() - colorspace_dict["Normal"] = texture_stack.get_channel( - substance_painter.textureset.ChannelType.Normal).is_color() - return colorspace_dict.get(map_identifier, False) + if map_identifier == "2D_View": + return True + channel_type = getattr( + substance_painter.textureset.ChannelType, map_identifier, None) + if channel_type is None: + return False + if not texture_stack.has_channel(channel_type): + return False -def texture_set_filtering(texture_set_same, template): - """Function to check whether some specific textures(e.g. Emissive) - are parts of the texture stack in Substance Painter - - Args: - texture_set_same (str): Name of Texture Set - template (str): texture template name - - Returns: - texture_filter: A dictionary which stores the boolean - value of whether the texture exist in the channel. - """ - texture_filter = {} - channel_stack = substance_painter.textureset.Stack.from_name( - texture_set_same) - has_emissive = channel_stack.has_channel( - substance_painter.textureset.ChannelType.Emissive) - map_identifier = strip_template(template) - if map_identifier == "Emissive": - texture_filter[map_identifier] = has_emissive - return texture_filter.get(map_identifier, True) + return texture_stack.get_channel(channel_type).is_color() diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 9d6aa06872..4468602392 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -41,13 +41,11 @@ class CollectTextureSet(pyblish.api.InstancePlugin): for (texture_set_name, stack_name), template_maps in maps.items(): self.log.info(f"Processing {texture_set_name}/{stack_name}") for template, outputs in template_maps.items(): - if texture_set_filtering(texture_set_name, template): - self.log.info(f"Processing {template}") - self.create_image_instance( - instance, template, outputs, - asset_doc=asset_doc, - texture_set_name=texture_set_name, - stack_name=stack_name) + self.log.info(f"Processing {template}") + self.create_image_instance(instance, template, outputs, + asset_doc=asset_doc, + texture_set_name=texture_set_name, + stack_name=stack_name) def create_image_instance(self, instance, template, outputs, asset_doc, texture_set_name, stack_name): diff --git a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py index b57cf4c5a2..252683b6c8 100644 --- a/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py +++ b/openpype/hosts/substancepainter/plugins/publish/validate_ouput_maps.py @@ -80,6 +80,7 @@ class ValidateOutputMaps(pyblish.api.InstancePlugin): self.log.warning(f"Disabling texture instance: " f"{image_instance}") image_instance.data["active"] = False + image_instance.data["publish"] = False image_instance.data["integrate"] = False representation.setdefault("tags", []).append("delete") continue From 00ab2bc9f69916ab19ddd15d018dc38cba624991 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 13 Jan 2024 00:19:03 +0800 Subject: [PATCH 227/291] remove unused function --- .../plugins/publish/collect_textureset_images.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 4468602392..370e72f34b 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -8,8 +8,7 @@ import substance_painter.textureset from openpype.hosts.substancepainter.api.lib import ( get_parsed_export_maps, strip_template, - has_rgb_channel_in_texture_set, - texture_set_filtering + has_rgb_channel_in_texture_set ) from openpype.pipeline.create import get_subset_name from openpype.client import get_asset_by_name From 95d2d45e0ffe10186714bf8136f6ac085ef6c202 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 12 Jan 2024 17:07:28 +0000 Subject: [PATCH 228/291] Fix reading image sequences through oiiotool --- openpype/lib/transcoding.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 316dedbd3d..79d24e737a 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -120,6 +120,10 @@ def get_oiio_info_for_input(filepath, logger=None, subimages=False): output = [] for subimage_lines in subimages_lines: + # First line of oiiotool for xml format is "Reading path/to/file.ext". + if not subimage_lines[0].startswith(" Date: Sat, 13 Jan 2024 03:25:27 +0000 Subject: [PATCH 229/291] [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 c87c143ea3..5981cb657a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.3" +__version__ = "3.18.4-nightly.1" From 8afd06233797bb0b949658a323bcbd0aeb3ffbd2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 13 Jan 2024 03:26:00 +0000 Subject: [PATCH 230/291] 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 3f762bd2d8..e9b68a54f1 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.18.4-nightly.1 - 3.18.3 - 3.18.3-nightly.2 - 3.18.3-nightly.1 @@ -134,7 +135,6 @@ body: - 3.15.7-nightly.2 - 3.15.7-nightly.1 - 3.15.6 - - 3.15.6-nightly.3 validations: required: true - type: dropdown From 7d94fb92c23d7ef055329f71d0333ab490a0055c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 15 Jan 2024 10:32:39 +0100 Subject: [PATCH 231/291] Fusion: new creator for image product type (#6057) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduced image product type 'image' product type should result in single frame output, 'render' should be more focused on multiple frames. * Updated logging * Refactor moved generic creaor class to better location * Update openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json Co-authored-by: Jakub Ježek * Change label It might be movie type not only image sequence. * OP-7470 - fix name * OP-7470 - update docstring There were objections for setting up this creator as it seems unnecessary. There is currently no other way how to implement customer requirement but this, but in the future 'alias' product types implementation might solve this. * Implementing changes from #6060 https://github.com/ynput/OpenPype/pull/6060 * Update openpype/settings/defaults/project_settings/fusion.json Co-authored-by: Jakub Ježek * Update server_addon/fusion/server/settings.py Co-authored-by: Jakub Ježek * Update openpype/hosts/fusion/api/plugin.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> * OP-7470 - added explicit frame field Artist can insert specific frame from which `image` instance should be created. * OP-7470 - fix name and logging Prints better message even in debug mode. * OP-7470 - update instance label It contained original frames which was confusing. * Update openpype/hosts/fusion/plugins/create/create_image_saver.py Co-authored-by: Roy Nieterau * OP-7470 - fix documentation * OP-7470 - moved frame range resolution earlier This approach is safer, as frame range is resolved sooner. * OP-7470 - added new validator for single frame * OP-7470 - Hound * OP-7470 - removed unnecessary as label * OP-7470 - use internal class anatomy * OP-7470 - add explicit settings_category to propagete values from Setting correctly apply_settings is replaced by correct value in `settings_category` * OP-7470 - typo * OP-7470 - update docstring * OP-7470 - update formatting data This probably fixes issue with missing product key in intermediate product name. * OP-7470 - moved around only proper fields Some fields (frame and frame_range) are making sense only in specific creator. * OP-7470 - added defaults to Settings * OP-7470 - fixed typo * OP-7470 - bumped up version Settings changed, so addon version should change too. 0.1.2 is in develop * Update openpype/hosts/fusion/plugins/publish/collect_instances.py Co-authored-by: Roy Nieterau * OP-7470 - removed unnecessary variables There was logic intended to use those, deemed not necessary. * OP-7470 - update to error message * OP-7470 - removed unneded method --------- Co-authored-by: Jakub Ježek Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Co-authored-by: Roy Nieterau --- openpype/hosts/fusion/api/plugin.py | 221 +++++++++++++++ .../plugins/create/create_image_saver.py | 64 +++++ .../fusion/plugins/create/create_saver.py | 257 ++---------------- .../fusion/plugins/publish/collect_inputs.py | 2 +- .../plugins/publish/collect_instances.py | 12 + .../fusion/plugins/publish/collect_render.py | 4 +- .../fusion/plugins/publish/save_scene.py | 2 +- .../publish/validate_background_depth.py | 2 +- .../plugins/publish/validate_comp_saved.py | 2 +- .../publish/validate_create_folder_checked.py | 2 +- .../validate_filename_has_extension.py | 2 +- .../plugins/publish/validate_image_frame.py | 27 ++ .../publish/validate_instance_frame_range.py | 4 +- .../publish/validate_saver_has_input.py | 2 +- .../publish/validate_saver_passthrough.py | 2 +- .../publish/validate_saver_resolution.py | 2 +- .../publish/validate_unique_subsets.py | 2 +- .../defaults/project_settings/fusion.json | 12 + .../schema_project_fusion.json | 51 +++- server_addon/fusion/server/settings.py | 67 ++++- server_addon/fusion/server/version.py | 2 +- 21 files changed, 482 insertions(+), 259 deletions(-) create mode 100644 openpype/hosts/fusion/api/plugin.py create mode 100644 openpype/hosts/fusion/plugins/create/create_image_saver.py create mode 100644 openpype/hosts/fusion/plugins/publish/validate_image_frame.py diff --git a/openpype/hosts/fusion/api/plugin.py b/openpype/hosts/fusion/api/plugin.py new file mode 100644 index 0000000000..63a74fbdb5 --- /dev/null +++ b/openpype/hosts/fusion/api/plugin.py @@ -0,0 +1,221 @@ +from copy import deepcopy +import os + +from openpype.hosts.fusion.api import ( + get_current_comp, + comp_lock_and_undo_chunk, +) + +from openpype.lib import ( + BoolDef, + EnumDef, +) +from openpype.pipeline import ( + legacy_io, + Creator, + CreatedInstance +) + + +class GenericCreateSaver(Creator): + default_variants = ["Main", "Mask"] + description = "Fusion Saver to generate image sequence" + icon = "fa5.eye" + + instance_attributes = [ + "reviewable" + ] + + settings_category = "fusion" + + image_format = "exr" + + # TODO: This should be renamed together with Nuke so it is aligned + temp_rendering_path_template = ( + "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}") + + def create(self, subset_name, instance_data, pre_create_data): + self.pass_pre_attributes_to_instance(instance_data, pre_create_data) + + instance = CreatedInstance( + family=self.family, + subset_name=subset_name, + data=instance_data, + creator=self, + ) + data = instance.data_to_store() + comp = get_current_comp() + with comp_lock_and_undo_chunk(comp): + args = (-32768, -32768) # Magical position numbers + saver = comp.AddTool("Saver", *args) + + self._update_tool_with_data(saver, data=data) + + # Register the CreatedInstance + self._imprint(saver, data) + + # Insert the transient data + instance.transient_data["tool"] = saver + + self._add_instance_to_context(instance) + + return instance + + def collect_instances(self): + comp = get_current_comp() + tools = comp.GetToolList(False, "Saver").values() + for tool in tools: + data = self.get_managed_tool_data(tool) + if not data: + continue + + # Add instance + created_instance = CreatedInstance.from_existing(data, self) + + # Collect transient data + created_instance.transient_data["tool"] = tool + + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + for created_inst, _changes in update_list: + new_data = created_inst.data_to_store() + tool = created_inst.transient_data["tool"] + self._update_tool_with_data(tool, new_data) + self._imprint(tool, new_data) + + def remove_instances(self, instances): + for instance in instances: + # Remove the tool from the scene + + tool = instance.transient_data["tool"] + if tool: + tool.Delete() + + # Remove the collected CreatedInstance to remove from UI directly + self._remove_instance_from_context(instance) + + def _imprint(self, tool, data): + # Save all data in a "openpype.{key}" = value data + + # Instance id is the tool's name so we don't need to imprint as data + data.pop("instance_id", None) + + active = data.pop("active", None) + if active is not None: + # Use active value to set the passthrough state + tool.SetAttrs({"TOOLB_PassThrough": not active}) + + for key, value in data.items(): + tool.SetData(f"openpype.{key}", value) + + def _update_tool_with_data(self, tool, data): + """Update tool node name and output path based on subset data""" + if "subset" not in data: + return + + original_subset = tool.GetData("openpype.subset") + original_format = tool.GetData( + "openpype.creator_attributes.image_format" + ) + + subset = data["subset"] + if ( + original_subset != subset + or original_format != data["creator_attributes"]["image_format"] + ): + self._configure_saver_tool(data, tool, subset) + + def _configure_saver_tool(self, data, tool, subset): + formatting_data = deepcopy(data) + + # get frame padding from anatomy templates + frame_padding = self.project_anatomy.templates["frame_padding"] + + # get output format + ext = data["creator_attributes"]["image_format"] + + # Subset change detected + workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) + formatting_data.update({ + "workdir": workdir, + "frame": "0" * frame_padding, + "ext": ext, + "product": { + "name": formatting_data["subset"], + "type": formatting_data["family"], + }, + }) + + # build file path to render + filepath = self.temp_rendering_path_template.format(**formatting_data) + + comp = get_current_comp() + tool["Clip"] = comp.ReverseMapPath(os.path.normpath(filepath)) + + # Rename tool + if tool.Name != subset: + print(f"Renaming {tool.Name} -> {subset}") + tool.SetAttrs({"TOOLS_Name": subset}) + + def get_managed_tool_data(self, tool): + """Return data of the tool if it matches creator identifier""" + data = tool.GetData("openpype") + if not isinstance(data, dict): + return + + required = { + "id": "pyblish.avalon.instance", + "creator_identifier": self.identifier, + } + for key, value in required.items(): + if key not in data or data[key] != value: + return + + # Get active state from the actual tool state + attrs = tool.GetAttrs() + passthrough = attrs["TOOLB_PassThrough"] + data["active"] = not passthrough + + # Override publisher's UUID generation because tool names are + # already unique in Fusion in a comp + data["instance_id"] = tool.Name + + return data + + def get_instance_attr_defs(self): + """Settings for publish page""" + return self.get_pre_create_attr_defs() + + def pass_pre_attributes_to_instance(self, instance_data, pre_create_data): + creator_attrs = instance_data["creator_attributes"] = {} + for pass_key in pre_create_data.keys(): + creator_attrs[pass_key] = pre_create_data[pass_key] + + def _get_render_target_enum(self): + rendering_targets = { + "local": "Local machine rendering", + "frames": "Use existing frames", + } + if "farm_rendering" in self.instance_attributes: + rendering_targets["farm"] = "Farm rendering" + + return EnumDef( + "render_target", items=rendering_targets, label="Render target" + ) + + def _get_reviewable_bool(self): + return BoolDef( + "review", + default=("reviewable" in self.instance_attributes), + label="Review", + ) + + def _get_image_format_enum(self): + image_format_options = ["exr", "tga", "tif", "png", "jpg"] + return EnumDef( + "image_format", + items=image_format_options, + default=self.image_format, + label="Output Image Format", + ) diff --git a/openpype/hosts/fusion/plugins/create/create_image_saver.py b/openpype/hosts/fusion/plugins/create/create_image_saver.py new file mode 100644 index 0000000000..490228d488 --- /dev/null +++ b/openpype/hosts/fusion/plugins/create/create_image_saver.py @@ -0,0 +1,64 @@ +from openpype.lib import NumberDef + +from openpype.hosts.fusion.api.plugin import GenericCreateSaver +from openpype.hosts.fusion.api import get_current_comp + + +class CreateImageSaver(GenericCreateSaver): + """Fusion Saver to generate single image. + + Created to explicitly separate single ('image') or + multi frame('render) outputs. + + This might be temporary creator until 'alias' functionality will be + implemented to limit creation of additional product types with similar, but + not the same workflows. + """ + identifier = "io.openpype.creators.fusion.imagesaver" + label = "Image (saver)" + name = "image" + family = "image" + description = "Fusion Saver to generate image" + + default_frame = 0 + + def get_detail_description(self): + return """Fusion Saver to generate single image. + + This creator is expected for publishing of single frame `image` product + type. + + Artist should provide frame number (integer) to specify which frame + should be published. It must be inside of global timeline frame range. + + Supports local and deadline rendering. + + Supports selection from predefined set of output file extensions: + - exr + - tga + - png + - tif + - jpg + + Created to explicitly separate single frame ('image') or + multi frame ('render') outputs. + """ + + def get_pre_create_attr_defs(self): + """Settings for create page""" + attr_defs = [ + self._get_render_target_enum(), + self._get_reviewable_bool(), + self._get_frame_int(), + self._get_image_format_enum(), + ] + return attr_defs + + def _get_frame_int(self): + return NumberDef( + "frame", + default=self.default_frame, + label="Frame", + tooltip="Set frame to be rendered, must be inside of global " + "timeline range" + ) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 5870828b41..3a8ffe890b 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -1,187 +1,42 @@ -from copy import deepcopy -import os +from openpype.lib import EnumDef -from openpype.hosts.fusion.api import ( - get_current_comp, - comp_lock_and_undo_chunk, -) - -from openpype.lib import ( - BoolDef, - EnumDef, -) -from openpype.pipeline import ( - legacy_io, - Creator as NewCreator, - CreatedInstance, - Anatomy, -) +from openpype.hosts.fusion.api.plugin import GenericCreateSaver -class CreateSaver(NewCreator): +class CreateSaver(GenericCreateSaver): + """Fusion Saver to generate image sequence of 'render' product type. + + Original Saver creator targeted for 'render' product type. It uses + original not to descriptive name because of values in Settings. + """ identifier = "io.openpype.creators.fusion.saver" label = "Render (saver)" name = "render" family = "render" - default_variants = ["Main", "Mask"] description = "Fusion Saver to generate image sequence" - icon = "fa5.eye" - instance_attributes = ["reviewable"] - image_format = "exr" + default_frame_range_option = "asset_db" - # TODO: This should be renamed together with Nuke so it is aligned - temp_rendering_path_template = ( - "{workdir}/renders/fusion/{subset}/{subset}.{frame}.{ext}" - ) + def get_detail_description(self): + return """Fusion Saver to generate image sequence. - def create(self, subset_name, instance_data, pre_create_data): - self.pass_pre_attributes_to_instance(instance_data, pre_create_data) + This creator is expected for publishing of image sequences for 'render' + product type. (But can publish even single frame 'render'.) - instance_data.update( - {"id": "pyblish.avalon.instance", "subset": subset_name} - ) + Select what should be source of render range: + - "Current asset context" - values set on Asset in DB (Ftrack) + - "From render in/out" - from node itself + - "From composition timeline" - from timeline - comp = get_current_comp() - with comp_lock_and_undo_chunk(comp): - args = (-32768, -32768) # Magical position numbers - saver = comp.AddTool("Saver", *args) + Supports local and farm rendering. - self._update_tool_with_data(saver, data=instance_data) - - # Register the CreatedInstance - instance = CreatedInstance( - family=self.family, - subset_name=subset_name, - data=instance_data, - creator=self, - ) - data = instance.data_to_store() - self._imprint(saver, data) - - # Insert the transient data - instance.transient_data["tool"] = saver - - self._add_instance_to_context(instance) - - return instance - - def collect_instances(self): - comp = get_current_comp() - tools = comp.GetToolList(False, "Saver").values() - for tool in tools: - data = self.get_managed_tool_data(tool) - if not data: - continue - - # Add instance - created_instance = CreatedInstance.from_existing(data, self) - - # Collect transient data - created_instance.transient_data["tool"] = tool - - self._add_instance_to_context(created_instance) - - def update_instances(self, update_list): - for created_inst, _changes in update_list: - new_data = created_inst.data_to_store() - tool = created_inst.transient_data["tool"] - self._update_tool_with_data(tool, new_data) - self._imprint(tool, new_data) - - def remove_instances(self, instances): - for instance in instances: - # Remove the tool from the scene - - tool = instance.transient_data["tool"] - if tool: - tool.Delete() - - # Remove the collected CreatedInstance to remove from UI directly - self._remove_instance_from_context(instance) - - def _imprint(self, tool, data): - # Save all data in a "openpype.{key}" = value data - - # Instance id is the tool's name so we don't need to imprint as data - data.pop("instance_id", None) - - active = data.pop("active", None) - if active is not None: - # Use active value to set the passthrough state - tool.SetAttrs({"TOOLB_PassThrough": not active}) - - for key, value in data.items(): - tool.SetData(f"openpype.{key}", value) - - def _update_tool_with_data(self, tool, data): - """Update tool node name and output path based on subset data""" - if "subset" not in data: - return - - original_subset = tool.GetData("openpype.subset") - original_format = tool.GetData( - "openpype.creator_attributes.image_format" - ) - - subset = data["subset"] - if ( - original_subset != subset - or original_format != data["creator_attributes"]["image_format"] - ): - self._configure_saver_tool(data, tool, subset) - - def _configure_saver_tool(self, data, tool, subset): - formatting_data = deepcopy(data) - - # get frame padding from anatomy templates - anatomy = Anatomy() - frame_padding = anatomy.templates["frame_padding"] - - # get output format - ext = data["creator_attributes"]["image_format"] - - # Subset change detected - workdir = os.path.normpath(legacy_io.Session["AVALON_WORKDIR"]) - formatting_data.update( - {"workdir": workdir, "frame": "0" * frame_padding, "ext": ext} - ) - - # build file path to render - filepath = self.temp_rendering_path_template.format(**formatting_data) - - comp = get_current_comp() - tool["Clip"] = comp.ReverseMapPath(os.path.normpath(filepath)) - - # Rename tool - if tool.Name != subset: - print(f"Renaming {tool.Name} -> {subset}") - tool.SetAttrs({"TOOLS_Name": subset}) - - def get_managed_tool_data(self, tool): - """Return data of the tool if it matches creator identifier""" - data = tool.GetData("openpype") - if not isinstance(data, dict): - return - - required = { - "id": "pyblish.avalon.instance", - "creator_identifier": self.identifier, - } - for key, value in required.items(): - if key not in data or data[key] != value: - return - - # Get active state from the actual tool state - attrs = tool.GetAttrs() - passthrough = attrs["TOOLB_PassThrough"] - data["active"] = not passthrough - - # Override publisher's UUID generation because tool names are - # already unique in Fusion in a comp - data["instance_id"] = tool.Name - - return data + Supports selection from predefined set of output file extensions: + - exr + - tga + - png + - tif + - jpg + """ def get_pre_create_attr_defs(self): """Settings for create page""" @@ -193,29 +48,6 @@ class CreateSaver(NewCreator): ] return attr_defs - def get_instance_attr_defs(self): - """Settings for publish page""" - return self.get_pre_create_attr_defs() - - def pass_pre_attributes_to_instance(self, instance_data, pre_create_data): - creator_attrs = instance_data["creator_attributes"] = {} - for pass_key in pre_create_data.keys(): - creator_attrs[pass_key] = pre_create_data[pass_key] - - # These functions below should be moved to another file - # so it can be used by other plugins. plugin.py ? - def _get_render_target_enum(self): - rendering_targets = { - "local": "Local machine rendering", - "frames": "Use existing frames", - } - if "farm_rendering" in self.instance_attributes: - rendering_targets["farm"] = "Farm rendering" - - return EnumDef( - "render_target", items=rendering_targets, label="Render target" - ) - def _get_frame_range_enum(self): frame_range_options = { "asset_db": "Current asset context", @@ -227,42 +59,5 @@ class CreateSaver(NewCreator): "frame_range_source", items=frame_range_options, label="Frame range source", - ) - - def _get_reviewable_bool(self): - return BoolDef( - "review", - default=("reviewable" in self.instance_attributes), - label="Review", - ) - - def _get_image_format_enum(self): - image_format_options = ["exr", "tga", "tif", "png", "jpg"] - return EnumDef( - "image_format", - items=image_format_options, - default=self.image_format, - label="Output Image Format", - ) - - def apply_settings(self, project_settings): - """Method called on initialization of plugin to apply settings.""" - - # plugin settings - plugin_settings = project_settings["fusion"]["create"][ - self.__class__.__name__ - ] - - # individual attributes - self.instance_attributes = plugin_settings.get( - "instance_attributes", self.instance_attributes - ) - self.default_variants = plugin_settings.get( - "default_variants", self.default_variants - ) - self.temp_rendering_path_template = plugin_settings.get( - "temp_rendering_path_template", self.temp_rendering_path_template - ) - self.image_format = plugin_settings.get( - "image_format", self.image_format + default=self.default_frame_range_option ) diff --git a/openpype/hosts/fusion/plugins/publish/collect_inputs.py b/openpype/hosts/fusion/plugins/publish/collect_inputs.py index a6628300db..f23e4d0268 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_inputs.py +++ b/openpype/hosts/fusion/plugins/publish/collect_inputs.py @@ -95,7 +95,7 @@ class CollectUpstreamInputs(pyblish.api.InstancePlugin): label = "Collect Inputs" order = pyblish.api.CollectorOrder + 0.2 hosts = ["fusion"] - families = ["render"] + families = ["render", "image"] def process(self, instance): diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index 4d6da79b77..a0131248e8 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -57,6 +57,18 @@ class CollectInstanceData(pyblish.api.InstancePlugin): start_with_handle = comp_start end_with_handle = comp_end + frame = instance.data["creator_attributes"].get("frame") + # explicitly publishing only single frame + if frame is not None: + frame = int(frame) + + start = frame + end = frame + handle_start = 0 + handle_end = 0 + start_with_handle = frame + end_with_handle = frame + # Include start and end render frame in label subset = instance.data["subset"] label = ( diff --git a/openpype/hosts/fusion/plugins/publish/collect_render.py b/openpype/hosts/fusion/plugins/publish/collect_render.py index a7daa0b64c..366eaa905c 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_render.py +++ b/openpype/hosts/fusion/plugins/publish/collect_render.py @@ -50,7 +50,7 @@ class CollectFusionRender( continue family = inst.data["family"] - if family != "render": + if family not in ["render", "image"]: continue task_name = context.data["task"] @@ -59,7 +59,7 @@ class CollectFusionRender( instance_families = inst.data.get("families", []) subset_name = inst.data["subset"] instance = FusionRenderInstance( - family="render", + family=family, tool=tool, workfileComp=comp, families=instance_families, diff --git a/openpype/hosts/fusion/plugins/publish/save_scene.py b/openpype/hosts/fusion/plugins/publish/save_scene.py index 0798e7c8b7..da9b6ce41f 100644 --- a/openpype/hosts/fusion/plugins/publish/save_scene.py +++ b/openpype/hosts/fusion/plugins/publish/save_scene.py @@ -7,7 +7,7 @@ class FusionSaveComp(pyblish.api.ContextPlugin): label = "Save current file" order = pyblish.api.ExtractorOrder - 0.49 hosts = ["fusion"] - families = ["render", "workfile"] + families = ["render", "image", "workfile"] def process(self, context): diff --git a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py index 6908889eb4..e268f8adec 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_background_depth.py +++ b/openpype/hosts/fusion/plugins/publish/validate_background_depth.py @@ -17,7 +17,7 @@ class ValidateBackgroundDepth( order = pyblish.api.ValidatorOrder label = "Validate Background Depth 32 bit" hosts = ["fusion"] - families = ["render"] + families = ["render", "image"] optional = True actions = [SelectInvalidAction, publish.RepairAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py b/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py index 748047e8cf..6e6d10e09a 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py +++ b/openpype/hosts/fusion/plugins/publish/validate_comp_saved.py @@ -9,7 +9,7 @@ class ValidateFusionCompSaved(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder label = "Validate Comp Saved" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] def process(self, context): diff --git a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py index 35c92163eb..d5c618af58 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py +++ b/openpype/hosts/fusion/plugins/publish/validate_create_folder_checked.py @@ -15,7 +15,7 @@ class ValidateCreateFolderChecked(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Create Folder Checked" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] actions = [RepairAction, SelectInvalidAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py index 537e43c875..38cd578ff2 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py +++ b/openpype/hosts/fusion/plugins/publish/validate_filename_has_extension.py @@ -17,7 +17,7 @@ class ValidateFilenameHasExtension(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Filename Has Extension" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] actions = [SelectInvalidAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_image_frame.py b/openpype/hosts/fusion/plugins/publish/validate_image_frame.py new file mode 100644 index 0000000000..734203f31c --- /dev/null +++ b/openpype/hosts/fusion/plugins/publish/validate_image_frame.py @@ -0,0 +1,27 @@ +import pyblish.api + +from openpype.pipeline import PublishValidationError + + +class ValidateImageFrame(pyblish.api.InstancePlugin): + """Validates that `image` product type contains only single frame.""" + + order = pyblish.api.ValidatorOrder + label = "Validate Image Frame" + families = ["image"] + hosts = ["fusion"] + + def process(self, instance): + render_start = instance.data["frameStartHandle"] + render_end = instance.data["frameEndHandle"] + too_many_frames = (isinstance(instance.data["expectedFiles"], list) + and len(instance.data["expectedFiles"]) > 1) + + if render_end - render_start > 0 or too_many_frames: + desc = ("Trying to render multiple frames. 'image' product type " + "is meant for single frame. Please use 'render' creator.") + raise PublishValidationError( + title="Frame range outside of comp range", + message=desc, + description=desc + ) diff --git a/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py b/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py index 06cd0ca186..edf219e752 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py +++ b/openpype/hosts/fusion/plugins/publish/validate_instance_frame_range.py @@ -7,8 +7,8 @@ class ValidateInstanceFrameRange(pyblish.api.InstancePlugin): """Validate instance frame range is within comp's global render range.""" order = pyblish.api.ValidatorOrder - label = "Validate Filename Has Extension" - families = ["render"] + label = "Validate Frame Range" + families = ["render", "image"] hosts = ["fusion"] def process(self, instance): diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py index faf2102a8b..0103e990fb 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_has_input.py @@ -13,7 +13,7 @@ class ValidateSaverHasInput(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder label = "Validate Saver Has Input" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] actions = [SelectInvalidAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py index 9004976dc5..6019bee93a 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_passthrough.py @@ -9,7 +9,7 @@ class ValidateSaverPassthrough(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder label = "Validate Saver Passthrough" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] actions = [SelectInvalidAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py index efa7295d11..f6aba170c0 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py +++ b/openpype/hosts/fusion/plugins/publish/validate_saver_resolution.py @@ -64,7 +64,7 @@ class ValidateSaverResolution( order = pyblish.api.ValidatorOrder label = "Validate Asset Resolution" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] optional = True actions = [SelectInvalidAction] diff --git a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py index 5b6ceb2fdb..d1693ef3dc 100644 --- a/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -11,7 +11,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): order = pyblish.api.ValidatorOrder label = "Validate Unique Subsets" - families = ["render"] + families = ["render", "image"] hosts = ["fusion"] actions = [SelectInvalidAction] diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index 8579442625..15b6bfc09b 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -32,6 +32,18 @@ "farm_rendering" ], "image_format": "exr" + }, + "CreateImageSaver": { + "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{ext}", + "default_variants": [ + "Main", + "Mask" + ], + "instance_attributes": [ + "reviewable", + "farm_rendering" + ], + "image_format": "exr" } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index fbd856b895..8669842087 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -74,7 +74,56 @@ "type": "dict", "collapsible": true, "key": "CreateSaver", - "label": "Create Saver", + "label": "Create Render Saver", + "is_group": true, + "children": [ + { + "type": "text", + "key": "temp_rendering_path_template", + "label": "Temporary rendering path template" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + }, + { + "key": "instance_attributes", + "label": "Instance attributes", + "type": "enum", + "multiselection": true, + "enum_items": [ + { + "reviewable": "Reviewable" + }, + { + "farm_rendering": "Farm rendering" + } + ] + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselect": false, + "enum_items": [ + {"exr": "exr"}, + {"tga": "tga"}, + {"png": "png"}, + {"tif": "tif"}, + {"jpg": "jpg"} + ] + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "CreateImageSaver", + "label": "Create Image Saver", "is_group": true, "children": [ { diff --git a/server_addon/fusion/server/settings.py b/server_addon/fusion/server/settings.py index 21189b390e..bf295f3064 100644 --- a/server_addon/fusion/server/settings.py +++ b/server_addon/fusion/server/settings.py @@ -25,6 +25,24 @@ def _create_saver_instance_attributes_enum(): ] +def _image_format_enum(): + return [ + {"value": "exr", "label": "exr"}, + {"value": "tga", "label": "tga"}, + {"value": "png", "label": "png"}, + {"value": "tif", "label": "tif"}, + {"value": "jpg", "label": "jpg"}, + ] + + +def _frame_range_options_enum(): + return [ + {"value": "asset_db", "label": "Current asset context"}, + {"value": "render_range", "label": "From render in/out"}, + {"value": "comp_range", "label": "From composition timeline"}, + ] + + class CreateSaverPluginModel(BaseSettingsModel): _isGroup = True temp_rendering_path_template: str = Field( @@ -59,10 +77,29 @@ class HooksModel(BaseSettingsModel): ) +class CreateSaverModel(CreateSaverPluginModel): + default_frame_range_option: str = Field( + default="asset_db", + enum_resolver=_frame_range_options_enum, + title="Default frame range source" + ) + + +class CreateImageSaverModel(CreateSaverPluginModel): + default_frame: int = Field( + 0, + title="Default rendered frame" + ) class CreatPluginsModel(BaseSettingsModel): - CreateSaver: CreateSaverPluginModel = Field( - default_factory=CreateSaverPluginModel, - title="Create Saver" + CreateSaver: CreateSaverModel = Field( + default_factory=CreateSaverModel, + title="Create Saver", + description="Creator for render product type (eg. sequence)" + ) + CreateImageSaver: CreateImageSaverModel = Field( + default_factory=CreateImageSaverModel, + title="Create Image Saver", + description="Creator for image product type (eg. single)" ) @@ -117,15 +154,21 @@ DEFAULT_VALUES = { "reviewable", "farm_rendering" ], - "output_formats": [ - "exr", - "jpg", - "jpeg", - "jpg", - "tiff", - "png", - "tga" - ] + "image_format": "exr", + "default_frame_range_option": "asset_db" + }, + "CreateImageSaver": { + "temp_rendering_path_template": "{workdir}/renders/fusion/{product[name]}/{product[name]}.{ext}", + "default_variants": [ + "Main", + "Mask" + ], + "instance_attributes": [ + "reviewable", + "farm_rendering" + ], + "image_format": "exr", + "default_frame": 0 } } } diff --git a/server_addon/fusion/server/version.py b/server_addon/fusion/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/fusion/server/version.py +++ b/server_addon/fusion/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" From 1ac89d2efa55792e11023419e3ac5914e7b57480 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 15 Jan 2024 19:17:12 +0800 Subject: [PATCH 232/291] restore some of the codes on lib and collect images --- openpype/hosts/substancepainter/api/lib.py | 30 ------------------- .../publish/collect_textureset_images.py | 8 ++--- 2 files changed, 3 insertions(+), 35 deletions(-) diff --git a/openpype/hosts/substancepainter/api/lib.py b/openpype/hosts/substancepainter/api/lib.py index 896cca79b0..1cb480b552 100644 --- a/openpype/hosts/substancepainter/api/lib.py +++ b/openpype/hosts/substancepainter/api/lib.py @@ -643,33 +643,3 @@ def prompt_new_file_with_mesh(mesh_filepath): return return project_mesh - - -def has_rgb_channel_in_texture_set(texture_set_name, map_identifier): - """Function to check whether the texture has RGB channel. - - Args: - texture_set_name (str): Name of Texture Set - map_identifier (str): Map identifier - - Returns: - bool: Whether the channel type identifier has RGB channel or not - in the texture stack. - """ - - # 2D_View is always True as it exports all texture maps - texture_stack = ( - substance_painter.textureset.Stack.from_name(texture_set_name) - ) - # 2D_View is always True as it exports all texture maps - if map_identifier == "2D_View": - return True - - channel_type = getattr( - substance_painter.textureset.ChannelType, map_identifier, None) - if channel_type is None: - return False - if not texture_stack.has_channel(channel_type): - return False - - return texture_stack.get_channel(channel_type).is_color() diff --git a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 370e72f34b..316f72509e 100644 --- a/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/openpype/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -7,8 +7,7 @@ from openpype.pipeline import publish import substance_painter.textureset from openpype.hosts.substancepainter.api.lib import ( get_parsed_export_maps, - strip_template, - has_rgb_channel_in_texture_set + strip_template ) from openpype.pipeline.create import get_subset_name from openpype.client import get_asset_by_name @@ -79,6 +78,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Always include the map identifier map_identifier = strip_template(template) suffix += f".{map_identifier}" + image_subset = get_subset_name( # TODO: The family actually isn't 'texture' currently but for now # this is only done so the subset name starts with 'texture' @@ -132,9 +132,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): # Store color space with the instance # Note: The extractor will assign it to the representation colorspace = outputs[0].get("colorSpace") - has_rgb_channel = has_rgb_channel_in_texture_set( - texture_set_name, map_identifier) - if colorspace and has_rgb_channel: + if colorspace: self.log.debug(f"{image_subset} colorspace: {colorspace}") image_instance.data["colorspace"] = colorspace From 190840fe052d0ee35ad9585d18ebc06eec916808 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jan 2024 12:16:50 +0000 Subject: [PATCH 233/291] Missing nuke family Windows arguments --- .../applications/server/applications.json | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json index 35f1b4cfbb..d2e2d942c6 100644 --- a/server_addon/applications/server/applications.json +++ b/server_addon/applications/server/applications.json @@ -307,7 +307,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--nukeassist"], "darwin": [], "linux": [] }, @@ -329,7 +329,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--nukeassist"], "darwin": [], "linux": [] }, @@ -351,7 +351,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--nukeassist"], "darwin": [], "linux": [] }, @@ -382,7 +382,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--nukex"], "darwin": [], "linux": [] }, @@ -404,7 +404,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--nukex"], "darwin": [], "linux": [] }, @@ -426,7 +426,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--nukex"], "darwin": [], "linux": [] }, @@ -457,7 +457,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--studio"], "darwin": [], "linux": [] }, @@ -479,7 +479,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--studio"], "darwin": [], "linux": [] }, @@ -501,7 +501,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--studio"], "darwin": [], "linux": [] }, @@ -532,7 +532,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--hiero"], "darwin": [], "linux": [] }, @@ -554,7 +554,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--hiero"], "darwin": [], "linux": [] }, @@ -576,7 +576,7 @@ ] }, "arguments": { - "windows": [], + "windows": ["--hiero"], "darwin": [], "linux": [] }, From e9be330393cb5a50fcbc257ecf4ed0c8eb335804 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jan 2024 14:56:29 +0000 Subject: [PATCH 234/291] Add Linux arguments. --- .../applications/server/applications.json | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json index d2e2d942c6..b0b12b2003 100644 --- a/server_addon/applications/server/applications.json +++ b/server_addon/applications/server/applications.json @@ -309,7 +309,7 @@ "arguments": { "windows": ["--nukeassist"], "darwin": [], - "linux": [] + "linux": ["--nukeassist"] }, "environment": "{}", "use_python_2": false @@ -331,7 +331,7 @@ "arguments": { "windows": ["--nukeassist"], "darwin": [], - "linux": [] + "linux": ["--nukeassist"] }, "environment": "{}", "use_python_2": false @@ -353,7 +353,7 @@ "arguments": { "windows": ["--nukeassist"], "darwin": [], - "linux": [] + "linux": ["--nukeassist"] }, "environment": "{}", "use_python_2": false @@ -384,7 +384,7 @@ "arguments": { "windows": ["--nukex"], "darwin": [], - "linux": [] + "linux": ["--nukex"] }, "environment": "{}", "use_python_2": false @@ -406,7 +406,7 @@ "arguments": { "windows": ["--nukex"], "darwin": [], - "linux": [] + "linux": ["--nukex"] }, "environment": "{}", "use_python_2": false @@ -428,7 +428,7 @@ "arguments": { "windows": ["--nukex"], "darwin": [], - "linux": [] + "linux": ["--nukex"] }, "environment": "{}", "use_python_2": false @@ -459,7 +459,7 @@ "arguments": { "windows": ["--studio"], "darwin": [], - "linux": [] + "linux": ["--studio"] }, "environment": "{}", "use_python_2": false @@ -481,7 +481,7 @@ "arguments": { "windows": ["--studio"], "darwin": [], - "linux": [] + "linux": ["--studio"] }, "environment": "{}", "use_python_2": false @@ -503,7 +503,7 @@ "arguments": { "windows": ["--studio"], "darwin": [], - "linux": [] + "linux": ["--studio"] }, "environment": "{}", "use_python_2": false @@ -534,7 +534,7 @@ "arguments": { "windows": ["--hiero"], "darwin": [], - "linux": [] + "linux": ["--hiero"] }, "environment": "{}", "use_python_2": false @@ -556,7 +556,7 @@ "arguments": { "windows": ["--hiero"], "darwin": [], - "linux": [] + "linux": ["--hiero"] }, "environment": "{}", "use_python_2": false @@ -578,7 +578,7 @@ "arguments": { "windows": ["--hiero"], "darwin": [], - "linux": [] + "linux": ["--hiero"] }, "environment": "{}", "use_python_2": false From 7dcdd1b3b0d1799d786054da896af57f13244657 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jan 2024 17:21:40 +0100 Subject: [PATCH 235/291] remove 'template_name_profiles' for 'IntegrateHeroVersion' (#6130) --- openpype/settings/ayon_settings.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index a6d90d1cf0..36f96c9dc7 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1291,12 +1291,6 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): for extract_burnin_def in extract_burnin_defs } - ayon_integrate_hero = ayon_publish["IntegrateHeroVersion"] - for profile in ayon_integrate_hero["template_name_profiles"]: - if "product_types" not in profile: - break - profile["families"] = profile.pop("product_types") - if "IntegrateProductGroup" in ayon_publish: subset_group = ayon_publish.pop("IntegrateProductGroup") subset_group_profiles = subset_group.pop("product_grouping_profiles") From 10167c86a7c25044280df8012ec3193677749677 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Jan 2024 17:33:26 +0100 Subject: [PATCH 236/291] use instance version if context version is not set (#6117) --- openpype/plugins/publish/collect_anatomy_instance_data.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index 0a34848166..34df49ea5b 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -200,9 +200,15 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): self._fill_task_data(instance, project_task_types, anatomy_data) # Define version + version_number = None if self.follow_workfile_version: version_number = context.data("version") - else: + + # Even if 'follow_workfile_version' is enabled, it may not be set + # because workfile version was not collected to 'context.data' + # - that can happen e.g. in 'traypublisher' or other hosts without + # a workfile + if version_number is None: version_number = instance.data.get("version") # use latest version (+1) if already any exist From 17e861f532bbdd6b3e4e9f16f2c69639483c16b2 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 15 Jan 2024 17:42:47 +0000 Subject: [PATCH 237/291] Illicit suggestion --- openpype/lib/transcoding.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 79d24e737a..03fd91bec2 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -110,8 +110,9 @@ def get_oiio_info_for_input(filepath, logger=None, subimages=False): if line == "": subimages_lines.append(lines) lines = [] + xml_started = False - if not xml_started: + if not subimages_lines: raise ValueError( "Failed to read input file \"{}\".\nOutput:\n{}".format( filepath, output @@ -120,10 +121,6 @@ def get_oiio_info_for_input(filepath, logger=None, subimages=False): output = [] for subimage_lines in subimages_lines: - # First line of oiiotool for xml format is "Reading path/to/file.ext". - if not subimage_lines[0].startswith(" Date: Tue, 16 Jan 2024 10:53:40 +0100 Subject: [PATCH 238/291] make sure style object is not garbage collected --- openpype/tools/utils/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 723e71e7aa..365caaafd9 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -91,7 +91,8 @@ def set_style_property(widget, property_name, property_value): if cur_value == property_value: return widget.setProperty(property_name, property_value) - widget.style().polish(widget) + style = widget.style() + style.polish(widget) def paint_image_with_color(image, color): From a6cc0b511e4468be960e14f47aa2300a865e9035 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 16 Jan 2024 10:15:48 +0000 Subject: [PATCH 239/291] Maya: Account and ignore free image planes. (#5993) * Account and ignore for free image planes. * Incorporating BigRoy changes --------- Co-authored-by: Petr Kalis --- .../maya/plugins/publish/extract_camera_mayaScene.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py index 38cf00bbdd..f67e9db14f 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py @@ -265,13 +265,16 @@ def transfer_image_planes(source_cameras, target_cameras, try: for source_camera, target_camera in zip(source_cameras, target_cameras): - image_planes = cmds.listConnections(source_camera, + image_plane_plug = "{}.imagePlane".format(source_camera) + image_planes = cmds.listConnections(image_plane_plug, + source=True, + destination=False, type="imagePlane") or [] # Split of the parent path they are attached - we want - # the image plane node name. + # the image plane node name if attached to a camera. # TODO: Does this still mean the image plane name is unique? - image_planes = [x.split("->", 1)[1] for x in image_planes] + image_planes = [x.split("->", 1)[-1] for x in image_planes] if not image_planes: continue @@ -282,7 +285,7 @@ def transfer_image_planes(source_cameras, target_cameras, if source_camera == target_camera: continue _attach_image_plane(target_camera, image_plane) - else: # explicitly dettaching image planes + else: # explicitly detach image planes cmds.imagePlane(image_plane, edit=True, detach=True) originals[source_camera].append(image_plane) yield From 9d5736aca78dcf9ca449f0bc5b9c73aedf96089c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 16 Jan 2024 13:07:31 +0000 Subject: [PATCH 240/291] [Automated] Release --- CHANGELOG.md | 145 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 147 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b51fade6f..546b2c12ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,151 @@ # Changelog +## [3.18.4](https://github.com/ynput/OpenPype/tree/3.18.4) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.3...3.18.4) + +### **🚀 Enhancements** + + +
+multiple render camera supports for 3dsmax #5124 + +Supports for rendering with multiple cameras in 3dsmax +- [x] Add Batch Render Layers functions +- [x] Rewrite lib.rendersetting and lib.renderproduct +- [x] Add multi-camera options in creator. +- [x] Collector with batch render-layer when multi-camera enabled. +- [x] Add instance plugin for saving scene files with different cameras respectively by using subprocess +- [x] Refactor submit_max_deadline +- [x] Check with metadata.json in submit publish job + + +___ + +
+ + +
+Fusion: new creator for image product type #6057 + +In many DCC `render` product type is expected to be sequence of files. This PR adds new explicit creator for `image` product type which is focused on single frame image. Workflows for both product types might be a bit different, this gives artists more granularity to choose better workflow. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Account and ignore free image planes. #5993 + +Free image planes do not have the `->` path separator, so we need to account for that. + + +___ + +
+ + +
+Blender: Fix long names for instances #6070 + +Changed naming for instances to use only final part of the `folderPath`. + + +___ + +
+ + +
+Traypublisher & Chore: Instance version on follow workfile version #6117 + +If `follow_workfile_version` is enabled but context does not have filled workfile version, a version on instance is used instead. + + +___ + +
+ + +
+Substance Painter: Thumbnail errors with PBR Texture Set #6127 + +When publishing with PBR Metallic Roughness as Output Template, Emissive Map errors out because of the missing channel in the material and the map can't be generated in Substance Painter. This PR is to make sure `imagestance.data["publish"] = False` so that the related "empty" texture instance would be skipped to generate the output. + + +___ + +
+ + +
+Transcoding: Fix reading image sequences through oiiotool #6129 + +When transcoding image sequences, the second image onwards includes the invalid xml line of `Reading path/to/file.exr` of the oiiotool output.This is most likely not the best solution, but it fixes the issue and illustrates the problem.Error: +``` +ERROR:pyblish.plugin:Traceback (most recent call last): + File "C:\Users\tokejepsen\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process + runner(*args) + File "C:\Users\tokejepsen\OpenPype\openpype\plugins\publish\extract_color_transcode.py", line 152, in process + File "C:\Users\tokejepsen\OpenPype\openpype\lib\transcoding.py", line 1136, in convert_colorspace + input_info = get_oiio_info_for_input(input_path, logger=logger) + File "C:\Users\tokejepsen\OpenPype\openpype\lib\transcoding.py", line 124, in get_oiio_info_for_input + output.append(parse_oiio_xml_output(xml_text, logger=logger)) + File "C:\Users\tokejepsen\OpenPype\openpype\lib\transcoding.py", line 276, in parse_oiio_xml_output + tree = xml.etree.ElementTree.fromstring(xml_string) + File "xml\etree\ElementTree.py", line 1347, in XML +xml.etree.ElementTree.ParseError: syntax error: line 1, column 0 +Traceback (most recent call last): + File "C:\Users\tokejepsen\AppData\Local\Ynput\AYON\dependency_packages\ayon_2310271602_windows.zip\dependencies\pyblish\plugin.py", line 527, in __explicit_process + runner(*args) + File "", line 152, in process + File "C:\Users\tokejepsen\OpenPype\openpype\lib\transcoding.py", line 1136, in convert_colorspace + input_info = get_oiio_info_for_input(input_path, logger=logger) + File "C:\Users\tokejepsen\OpenPype\openpype\lib\transcoding.py", line 124, in get_oiio_info_for_input + output.append(parse_oiio_xml_output(xml_text, logger=logger)) + File "C:\Users\tokejepsen\OpenPype\openpype\lib\transcoding.py", line 276, in parse_oiio_xml_output + tree = xml.etree.ElementTree.fromstring(xml_string) + File "xml\etree\ElementTree.py", line 1347, in XML +xml.etree.ElementTree.ParseError: syntax error: line 1, column 0 +``` + + + +___ + +
+ + +
+AYON: Remove 'IntegrateHeroVersion' conversion #6130 + +Remove settings conversion for `IntegrateHeroVersion`. + + +___ + +
+ + +
+Chore tools: Make sure style object is not garbage collected #6136 + +Minor fix in tool utils to make sure style C++ object is not garbage collected when not stored into variable. + + +___ + +
+ + + + ## [3.18.3](https://github.com/ynput/OpenPype/tree/3.18.3) diff --git a/openpype/version.py b/openpype/version.py index 5981cb657a..0b6584dbff 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.4-nightly.1" +__version__ = "3.18.4" diff --git a/pyproject.toml b/pyproject.toml index bad481c889..f9d5381a15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.18.3" # OpenPype +version = "3.18.4" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 4c9b19ed9974559bd63f6eeb593570f010983b85 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 16 Jan 2024 13:08:30 +0000 Subject: [PATCH 241/291] 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 e9b68a54f1..8df948a7c1 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.18.4 - 3.18.4-nightly.1 - 3.18.3 - 3.18.3-nightly.2 @@ -134,7 +135,6 @@ body: - 3.15.7-nightly.3 - 3.15.7-nightly.2 - 3.15.7-nightly.1 - - 3.15.6 validations: required: true - type: dropdown From 5bdad2b37c89272ff29885bc03eabadce907c0be Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 16 Jan 2024 21:36:04 +0800 Subject: [PATCH 242/291] bug fix the limit groups from maya deadline settings unabled to be set by the username and error out --- server_addon/deadline/server/settings/publish_plugins.py | 2 +- server_addon/deadline/server/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index a989f3ad9d..dfb30f9b41 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -87,7 +87,7 @@ class MayaSubmitDeadlineModel(BaseSettingsModel): title="Disable Strict Error Check profiles" ) - @validator("limit", "scene_patches") + @validator("scene_patches") def validate_unique_names(cls, value): ensure_unique_names(value) return value diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py index 0a8da88258..f1380eede2 100644 --- a/server_addon/deadline/server/version.py +++ b/server_addon/deadline/server/version.py @@ -1 +1 @@ -__version__ = "0.1.6" +__version__ = "0.1.7" From b29dee59fadbaa59876d0684eb0c30df5bba0681 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Jan 2024 16:48:35 +0100 Subject: [PATCH 243/291] add addons dir only if exists --- openpype/modules/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1a2513b4b4..41be2998f9 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -542,7 +542,8 @@ def _load_modules(): module_dirs.insert(0, current_dir) addons_dir = os.path.join(os.path.dirname(current_dir), "addons") - module_dirs.append(addons_dir) + if os.path.exists(addons_dir): + module_dirs.append(addons_dir) ignored_host_names = set(IGNORED_HOSTS_IN_AYON) ignored_current_dir_filenames = set(IGNORED_DEFAULT_FILENAMES) From 25b33c60f0e86f7b3303bfdff92b563bc13180b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Jan 2024 16:49:31 +0100 Subject: [PATCH 244/291] add missing '.tif' extension --- openpype/lib/transcoding.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 03fd91bec2..c8ddbde061 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -44,17 +44,17 @@ XML_CHAR_REF_REGEX_HEX = re.compile(r"&#x?[0-9a-fA-F]+;") ARRAY_TYPE_REGEX = re.compile(r"^(int|float|string)\[\d+\]$") IMAGE_EXTENSIONS = { - ".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", ".cal", - ".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", ".fits", - ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", ".icer", - ".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2", - ".jng", ".jpeg", ".jpeg-ls", ".jpeg", ".2000", ".jpg", ".xr", - ".jpeg", ".xt", ".jpeg-hdr", ".kra", ".mng", ".miff", ".nrrd", - ".ora", ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf", - ".pictor", ".png", ".psd", ".psb", ".psp", ".qtvr", ".ras", - ".rgbe", ".logluv", ".tiff", ".sgi", ".tga", ".tiff", ".tiff/ep", - ".tiff/it", ".ufo", ".ufp", ".wbmp", ".webp", ".xbm", ".xcf", - ".xpm", ".xwd" + ".ani", ".anim", ".apng", ".art", ".bmp", ".bpg", ".bsave", + ".cal", ".cin", ".cpc", ".cpt", ".dds", ".dpx", ".ecw", ".exr", + ".fits", ".flic", ".flif", ".fpx", ".gif", ".hdri", ".hevc", + ".icer", ".icns", ".ico", ".cur", ".ics", ".ilbm", ".jbig", ".jbig2", + ".jng", ".jpeg", ".jpeg-ls", ".jpeg-hdr", ".2000", ".jpg", + ".kra", ".logluv", ".mng", ".miff", ".nrrd", ".ora", + ".pam", ".pbm", ".pgm", ".ppm", ".pnm", ".pcx", ".pgf", + ".pictor", ".png", ".psd", ".psb", ".psp", ".qtvr", + ".ras", ".rgbe", ".sgi", ".tga", + ".tif", ".tiff", ".tiff/ep", ".tiff/it", ".ufo", ".ufp", + ".wbmp", ".webp", ".xr", ".xt", ".xbm", ".xcf", ".xpm", ".xwd" } VIDEO_EXTENSIONS = { From b468baa80677aac036472ae484894f4f8330f17b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Jan 2024 16:51:23 +0100 Subject: [PATCH 245/291] use correct variable for error items --- openpype/pipeline/workfile/workfile_template_builder.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/workfile_template_builder.py b/openpype/pipeline/workfile/workfile_template_builder.py index 9dc833061a..3096d22518 100644 --- a/openpype/pipeline/workfile/workfile_template_builder.py +++ b/openpype/pipeline/workfile/workfile_template_builder.py @@ -1971,7 +1971,6 @@ class PlaceholderCreateMixin(object): if not placeholder.data.get("keep_placeholder", True): self.delete_placeholder(placeholder) - def create_failed(self, placeholder, creator_data): if hasattr(placeholder, "create_failed"): placeholder.create_failed(creator_data) @@ -2036,7 +2035,7 @@ class CreatePlaceholderItem(PlaceholderItem): self._failed_created_publish_instances = [] def get_errors(self): - if not self._failed_representations: + if not self._failed_created_publish_instances: return [] message = ( "Failed to create {} instance using Creator {}" From dcf8805cfe2aca6f2615de1a7c18988d0efedae6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 16 Jan 2024 16:53:26 +0000 Subject: [PATCH 246/291] Add settings for effect categories --- .../hiero/server/settings/publish_plugins.py | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/server_addon/hiero/server/settings/publish_plugins.py b/server_addon/hiero/server/settings/publish_plugins.py index a85e62724b..07778b8ebe 100644 --- a/server_addon/hiero/server/settings/publish_plugins.py +++ b/server_addon/hiero/server/settings/publish_plugins.py @@ -1,5 +1,7 @@ -from pydantic import Field -from ayon_server.settings import BaseSettingsModel +from pydantic import Field, validator +from ayon_server.settings import ( + BaseSettingsModel, ensure_unique_names, normalize_name +) class CollectInstanceVersionModel(BaseSettingsModel): @@ -9,6 +11,30 @@ class CollectInstanceVersionModel(BaseSettingsModel): ) +class CollectClipEffectsDefModel(BaseSettingsModel): + _layout = "expanded" + name: str = Field("", title="Name") + effect_classes: list[str] = Field( + default_factory=list, title="Effect Classes" + ) + + @validator("name") + def validate_name(cls, value): + """Ensure name does not contain weird characters""" + return normalize_name(value) + + +class CollectClipEffectsModel(BaseSettingsModel): + effect_categories: list[CollectClipEffectsDefModel] = Field( + default_factory=list, title="Effect Categories" + ) + + @validator("effect_categories") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + class ExtractReviewCutUpVideoModel(BaseSettingsModel): enabled: bool = Field( True, @@ -25,6 +51,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=CollectInstanceVersionModel, title="Collect Instance Version" ) + CollectClipEffects: CollectClipEffectsModel = Field( + default_factory=CollectClipEffectsModel, + title="Collect Clip Effects" + ) """# TODO: enhance settings with host api: Rename class name and plugin name to match title (it makes more sense) From 9c8a4a7b6cccc2fd112b6c41f1e4c10d7f94f6e4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 16 Jan 2024 16:53:51 +0000 Subject: [PATCH 247/291] Categorize effects from settings. --- .../plugins/publish/collect_clip_effects.py | 71 ++++++++++++++----- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py index fcb1ab27a0..89dc66d73c 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py @@ -70,29 +70,62 @@ class CollectClipEffects(pyblish.api.InstancePlugin): subset_split.insert(0, "effect") - name = "".join(subset_split) + effect_categories = { + x["name"]: x["effect_classes"] for x in self.effect_categories + } - # create new instance and inherit data - data = {} - for key, value in instance.data.items(): - if "clipEffectItems" in key: + category_by_effect = {} + for key, values in effect_categories.items(): + for cls in values: + category_by_effect[cls] = key + + effects_categorized = {k: {} for k in effect_categories.keys()} + for key, value in effects.items(): + if key == "assignTo": continue - data[key] = value - # change names - data["subset"] = name - data["family"] = family - data["families"] = [family] - data["name"] = data["subset"] + "_" + data["asset"] - data["label"] = "{} - {}".format( - data['asset'], data["subset"] - ) - data["effects"] = effects + # Some classes can have a number in them. Like Text2. + found_cls = None + for cls in category_by_effect.keys(): + if value["class"].startswith(cls): + found_cls = cls - # create new instance - _instance = instance.context.create_instance(**data) - self.log.info("Created instance `{}`".format(_instance)) - self.log.debug("instance.data `{}`".format(_instance.data)) + if found_cls is None: + continue + + effects_categorized[category_by_effect[found_cls]][key] = value + + if effects_categorized: + for key in effects_categorized.keys(): + effects_categorized[key]["assignTo"] = effects["assignTo"] + else: + effects_categorized[""] = effects + + for category, effects in effects_categorized.items(): + name = "".join(subset_split) + name += category.capitalize() + + # create new instance and inherit data + data = {} + for key, value in instance.data.items(): + if "clipEffectItems" in key: + continue + data[key] = value + + # change names + data["subset"] = name + data["family"] = family + data["families"] = [family] + data["name"] = data["subset"] + "_" + data["asset"] + data["label"] = "{} - {}".format( + data['asset'], data["subset"] + ) + data["effects"] = effects + + # create new instance + _instance = instance.context.create_instance(**data) + self.log.info("Created instance `{}`".format(_instance)) + self.log.debug("instance.data `{}`".format(_instance.data)) def test_overlap(self, effect_t_in, effect_t_out): covering_exp = bool( From 6857082f1e0e9cab399ccb85fe1ec5a41cfc010f Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 17 Jan 2024 03:26:10 +0000 Subject: [PATCH 248/291] [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 0b6584dbff..043b6fbebb 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.4" +__version__ = "3.18.5-nightly.1" From 4cf6ddfea215c5d747fef01ba00ffe18da2b2fb2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 17 Jan 2024 03:26:46 +0000 Subject: [PATCH 249/291] 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 8df948a7c1..1177b9e4fe 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.18.5-nightly.1 - 3.18.4 - 3.18.4-nightly.1 - 3.18.3 @@ -134,7 +135,6 @@ body: - 3.15.7 - 3.15.7-nightly.3 - 3.15.7-nightly.2 - - 3.15.7-nightly.1 validations: required: true - type: dropdown From b7557c754385a8a966c1b7fcce804cbcfa9e8dcf Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jan 2024 09:12:14 +0000 Subject: [PATCH 250/291] Increment version --- server_addon/applications/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/applications/server/version.py b/server_addon/applications/server/version.py index ae7362549b..bbab0242f6 100644 --- a/server_addon/applications/server/version.py +++ b/server_addon/applications/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" From 883eb9f4f86c49bf6e387d38fac1dcc9042c93f3 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jan 2024 09:13:03 +0000 Subject: [PATCH 251/291] Increment version --- server_addon/hiero/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/hiero/server/version.py b/server_addon/hiero/server/version.py index 485f44ac21..3ced3581bb 100644 --- a/server_addon/hiero/server/version.py +++ b/server_addon/hiero/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.2.1" From 7e3e567f002974436badeea5ff25bfa8db9461cf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 17 Jan 2024 10:46:50 +0000 Subject: [PATCH 252/291] Use the new API for override context --- openpype/hosts/blender/api/capture.py | 11 +++--- .../hosts/blender/plugins/load/load_audio.py | 35 ++++++++++--------- .../plugins/publish/extract_abc_animation.py | 14 ++++---- .../plugins/publish/extract_camera_fbx.py | 26 +++++++------- .../blender/plugins/publish/extract_fbx.py | 18 +++++----- .../plugins/publish/extract_fbx_animation.py | 25 +++++++------ .../blender/plugins/publish/extract_layout.py | 23 ++++++------ 7 files changed, 81 insertions(+), 71 deletions(-) diff --git a/openpype/hosts/blender/api/capture.py b/openpype/hosts/blender/api/capture.py index bad6831143..9922140cea 100644 --- a/openpype/hosts/blender/api/capture.py +++ b/openpype/hosts/blender/api/capture.py @@ -127,8 +127,9 @@ def isolate_objects(window, objects): context = create_blender_context(selected=objects, window=window) - bpy.ops.view3d.view_axis(context, type="FRONT") - bpy.ops.view3d.localview(context) + with bpy.context.temp_override(**context): + bpy.ops.view3d.view_axis(type="FRONT") + bpy.ops.view3d.localview() deselect_all() @@ -270,10 +271,12 @@ def _independent_window(): """Create capture-window context.""" context = create_blender_context() current_windows = set(bpy.context.window_manager.windows) - bpy.ops.wm.window_new(context) + with bpy.context.temp_override(**context): + bpy.ops.wm.window_new() window = list(set(bpy.context.window_manager.windows) - current_windows)[0] context["window"] = window try: yield window finally: - bpy.ops.wm.window_close(context) + with bpy.context.temp_override(**context): + bpy.ops.wm.window_close() diff --git a/openpype/hosts/blender/plugins/load/load_audio.py b/openpype/hosts/blender/plugins/load/load_audio.py index 1e5bd39a32..367fff03f0 100644 --- a/openpype/hosts/blender/plugins/load/load_audio.py +++ b/openpype/hosts/blender/plugins/load/load_audio.py @@ -67,7 +67,8 @@ class AudioLoader(plugin.AssetLoader): oc = bpy.context.copy() oc["area"] = window_manager.windows[-1].screen.areas[0] - bpy.ops.sequencer.sound_strip_add(oc, filepath=libpath, frame_start=1) + with bpy.context.temp_override(**oc): + bpy.ops.sequencer.sound_strip_add(filepath=libpath, frame_start=1) window_manager.windows[-1].screen.areas[0].type = old_type @@ -156,17 +157,18 @@ class AudioLoader(plugin.AssetLoader): oc = bpy.context.copy() oc["area"] = window_manager.windows[-1].screen.areas[0] - # We deselect all sequencer strips, and then select the one we - # need to remove. - bpy.ops.sequencer.select_all(oc, action='DESELECT') - scene = bpy.context.scene - scene.sequence_editor.sequences_all[old_audio].select = True + with bpy.context.temp_override(**oc): + # We deselect all sequencer strips, and then select the one we + # need to remove. + bpy.ops.sequencer.select_all(action='DESELECT') + scene = bpy.context.scene + scene.sequence_editor.sequences_all[old_audio].select = True - bpy.ops.sequencer.delete(oc) - bpy.data.sounds.remove(bpy.data.sounds[old_audio]) + bpy.ops.sequencer.delete() + bpy.data.sounds.remove(bpy.data.sounds[old_audio]) - bpy.ops.sequencer.sound_strip_add( - oc, filepath=str(libpath), frame_start=1) + bpy.ops.sequencer.sound_strip_add( + filepath=str(libpath), frame_start=1) window_manager.windows[-1].screen.areas[0].type = old_type @@ -205,12 +207,13 @@ class AudioLoader(plugin.AssetLoader): oc = bpy.context.copy() oc["area"] = window_manager.windows[-1].screen.areas[0] - # We deselect all sequencer strips, and then select the one we - # need to remove. - bpy.ops.sequencer.select_all(oc, action='DESELECT') - bpy.context.scene.sequence_editor.sequences_all[audio].select = True - - bpy.ops.sequencer.delete(oc) + with bpy.context.temp_override(**oc): + # We deselect all sequencer strips, and then select the one we + # need to remove. + bpy.ops.sequencer.select_all(action='DESELECT') + scene = bpy.context.scene + scene.sequence_editor.sequences_all[audio].select = True + bpy.ops.sequencer.delete() window_manager.windows[-1].screen.areas[0].type = old_type diff --git a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py index 6ef9b29693..2bf75aa05e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_abc_animation.py @@ -55,13 +55,13 @@ class ExtractAnimationABC( context = plugin.create_blender_context( active=asset_group, selected=selected) - # We export the abc - bpy.ops.wm.alembic_export( - context, - filepath=filepath, - selected=True, - flatten=False - ) + with bpy.context.temp_override(**context): + # We export the abc + bpy.ops.wm.alembic_export( + filepath=filepath, + selected=True, + flatten=False + ) plugin.deselect_all() diff --git a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py index ee046b7d11..f904b79ddb 100644 --- a/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_camera_fbx.py @@ -50,19 +50,19 @@ class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin): scale_length = bpy.context.scene.unit_settings.scale_length bpy.context.scene.unit_settings.scale_length = 0.01 - # We export the fbx - bpy.ops.export_scene.fbx( - context, - filepath=filepath, - use_active_collection=False, - use_selection=True, - bake_anim_use_nla_strips=False, - bake_anim_use_all_actions=False, - add_leaf_bones=False, - armature_nodetype='ROOT', - object_types={'CAMERA'}, - bake_anim_simplify_factor=0.0 - ) + with bpy.context.temp_override(**context): + # We export the fbx + bpy.ops.export_scene.fbx( + filepath=filepath, + use_active_collection=False, + use_selection=True, + bake_anim_use_nla_strips=False, + bake_anim_use_all_actions=False, + add_leaf_bones=False, + armature_nodetype='ROOT', + object_types={'CAMERA'}, + bake_anim_simplify_factor=0.0 + ) bpy.context.scene.unit_settings.scale_length = scale_length diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index 4ae6501f7d..aed6df1d3d 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -57,15 +57,15 @@ class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin): scale_length = bpy.context.scene.unit_settings.scale_length bpy.context.scene.unit_settings.scale_length = 0.01 - # We export the fbx - bpy.ops.export_scene.fbx( - context, - filepath=filepath, - use_active_collection=False, - use_selection=True, - mesh_smooth_type='FACE', - add_leaf_bones=False - ) + with bpy.context.temp_override(**context): + # We export the fbx + bpy.ops.export_scene.fbx( + filepath=filepath, + use_active_collection=False, + use_selection=True, + mesh_smooth_type='FACE', + add_leaf_bones=False + ) bpy.context.scene.unit_settings.scale_length = scale_length diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 4fc8230a1b..1cb8dac0cf 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -153,17 +153,20 @@ class ExtractAnimationFBX( override = plugin.create_blender_context( active=root, selected=[root, armature]) - bpy.ops.export_scene.fbx( - override, - filepath=filepath, - use_active_collection=False, - use_selection=True, - bake_anim_use_nla_strips=False, - bake_anim_use_all_actions=False, - add_leaf_bones=False, - armature_nodetype='ROOT', - object_types={'EMPTY', 'ARMATURE'} - ) + + with bpy.context.temp_override(**override): + # We export the fbx + bpy.ops.export_scene.fbx( + filepath=filepath, + use_active_collection=False, + use_selection=True, + bake_anim_use_nla_strips=False, + bake_anim_use_all_actions=False, + add_leaf_bones=False, + armature_nodetype='ROOT', + object_types={'EMPTY', 'ARMATURE'} + ) + armature.name = armature_name asset_group.name = asset_group_name root.select_set(True) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 3e8978c8d3..383c3bdcc5 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -80,17 +80,18 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): override = plugin.create_blender_context( active=asset, selected=[asset, obj]) - bpy.ops.export_scene.fbx( - override, - filepath=filepath, - use_active_collection=False, - use_selection=True, - bake_anim_use_nla_strips=False, - bake_anim_use_all_actions=False, - add_leaf_bones=False, - armature_nodetype='ROOT', - object_types={'EMPTY', 'ARMATURE'} - ) + with bpy.context.temp_override(**override): + # We export the fbx + bpy.ops.export_scene.fbx( + filepath=filepath, + use_active_collection=False, + use_selection=True, + bake_anim_use_nla_strips=False, + bake_anim_use_all_actions=False, + add_leaf_bones=False, + armature_nodetype='ROOT', + object_types={'EMPTY', 'ARMATURE'} + ) obj.name = armature_name asset.name = asset_group_name asset.select_set(False) From 38eb8e0ae38169137316b57e90271c640b96420a Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 17 Jan 2024 11:31:29 +0000 Subject: [PATCH 253/291] initial working version --- .../plugins/publish/submit_nuke_deadline.py | 43 ++++++++++++------- .../defaults/project_settings/deadline.json | 2 + .../schema_project_deadline.json | 10 +++++ .../server/settings/publish_plugins.py | 3 ++ 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index d03416ca00..ead22fe302 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -47,6 +47,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, env_allowed_keys = [] env_search_replace_values = {} workfile_dependency = True + use_published_workfile = True @classmethod def get_attribute_defs(cls): @@ -85,8 +86,13 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, ), BoolDef( "workfile_dependency", - default=True, + default=cls.workfile_dependency, label="Workfile Dependency" + ), + BoolDef( + "use_published_workfile", + default=cls.use_published_workfile, + label="Use Published Workfile" ) ] @@ -125,20 +131,27 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, render_path = instance.data['path'] script_path = context.data["currentFile"] - for item_ in context: - if "workfile" in item_.data["family"]: - template_data = item_.data.get("anatomyData") - rep = item_.data.get("representations")[0].get("name") - template_data["representation"] = rep - template_data["ext"] = rep - template_data["comment"] = None - anatomy_filled = context.data["anatomy"].format(template_data) - template_filled = anatomy_filled["publish"]["path"] - script_path = os.path.normpath(template_filled) - - self.log.info( - "Using published scene for render {}".format(script_path) - ) + use_published_workfile = instance.data["attributeValues"].get( + "use_published_workfile", self.use_published_workfile + ) + if use_published_workfile: + for item_ in context: + if "workfile" in item_.data["family"]: + template_data = item_.data.get("anatomyData") + rep = item_.data.get("representations")[0].get("name") + template_data["representation"] = rep + template_data["ext"] = rep + template_data["comment"] = None + anatomy_filled = context.data["anatomy"].format( + template_data + ) + template_filled = anatomy_filled["publish"]["path"] + script_path = os.path.normpath(template_filled) + self.log.info( + "Using published scene for render {}".format( + script_path + ) + ) # only add main rendering job if target is not frames_farm r_job_response_json = None diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index a19464a5c1..b02cfa8207 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -65,6 +65,8 @@ "group": "", "department": "", "use_gpu": true, + "workfile_dependency": true, + "use_published_workfile": true, "env_allowed_keys": [], "env_search_replace_values": {}, "limit_groups": {} diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 1aea778e32..42dea33ef9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -362,6 +362,16 @@ "key": "use_gpu", "label": "Use GPU" }, + { + "type": "boolean", + "key": "workfile_dependency", + "label": "Workfile Dependency" + }, + { + "type": "boolean", + "key": "use_published_workfile", + "label": "Use Published Workfile" + }, { "type": "list", "key": "env_allowed_keys", diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index a989f3ad9d..8e44d8d47f 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -161,6 +161,8 @@ class NukeSubmitDeadlineModel(BaseSettingsModel): group: str = Field(title="Group") department: str = Field(title="Department") use_gpu: bool = Field(title="Use GPU") + workfile_dependency: bool = Field(title="Workfile Dependency") + use_published_workfile: bool = Field(title="Use Published Workfile") env_allowed_keys: list[str] = Field( default_factory=list, @@ -382,6 +384,7 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = { "group": "", "department": "", "use_gpu": True, + "workfile_dependency": True, "env_allowed_keys": [], "env_search_replace_values": [], "limit_groups": [] From acd61bef12f9580f3b1b1ed189b7864ed7623316 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 18 Jan 2024 12:15:10 +0000 Subject: [PATCH 254/291] Increment version --- server_addon/nuke/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py index 9cb17e7976..c49a95c357 100644 --- a/server_addon/nuke/server/version.py +++ b/server_addon/nuke/server/version.py @@ -1 +1 @@ -__version__ = "0.1.8" +__version__ = "0.2.8" From 6591d883e495c7d5b6ac80da1085f556227d1859 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 18 Jan 2024 14:43:20 +0000 Subject: [PATCH 255/291] Correct version increment --- server_addon/deadline/server/version.py | 2 +- server_addon/nuke/server/version.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py index 0a8da88258..01ef12070d 100644 --- a/server_addon/deadline/server/version.py +++ b/server_addon/deadline/server/version.py @@ -1 +1 @@ -__version__ = "0.1.6" +__version__ = "0.2.6" diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py index c49a95c357..9cb17e7976 100644 --- a/server_addon/nuke/server/version.py +++ b/server_addon/nuke/server/version.py @@ -1 +1 @@ -__version__ = "0.2.8" +__version__ = "0.1.8" From 423df8d1fdaaf5742b8aad0a7002e98cdbe1daa9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 18 Jan 2024 14:44:42 +0000 Subject: [PATCH 256/291] Fix settings defaults. --- server_addon/deadline/server/settings/publish_plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index 8e44d8d47f..9fc8d4a6b7 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -385,6 +385,7 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = { "department": "", "use_gpu": True, "workfile_dependency": True, + "use_published_workfile": True, "env_allowed_keys": [], "env_search_replace_values": [], "limit_groups": [] From 5761c4e5d00147eacf77bd275be12c007684912f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Thu, 18 Jan 2024 16:15:31 +0100 Subject: [PATCH 257/291] Update server_addon/deadline/server/version.py --- server_addon/deadline/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py index 6cd38b7465..9cb17e7976 100644 --- a/server_addon/deadline/server/version.py +++ b/server_addon/deadline/server/version.py @@ -1 +1 @@ -__version__ = "0.2.7" +__version__ = "0.1.8" From 1d7f23ab72b83b94fd951426c392035a877fbbce Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 19 Jan 2024 09:47:17 +0200 Subject: [PATCH 258/291] include model product type --- openpype/hosts/houdini/plugins/load/load_fbx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index cac22d62d4..649c2e9995 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -16,7 +16,7 @@ class FbxLoader(load.LoaderPlugin): order = -10 - families = ["staticMesh", "fbx"] + families = ["model", "staticMesh", "fbx"] representations = ["fbx"] def load(self, context, name=None, namespace=None, data=None): From 04c072947c7c7e5f93dc21ad2afd9434c44f2b95 Mon Sep 17 00:00:00 2001 From: MustafaJafar Date: Fri, 19 Jan 2024 10:47:37 +0200 Subject: [PATCH 259/291] BigRoy's Comment - Make it able to load any FBX file --- openpype/hosts/houdini/plugins/load/load_fbx.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/plugins/load/load_fbx.py b/openpype/hosts/houdini/plugins/load/load_fbx.py index 649c2e9995..894ac62b3e 100644 --- a/openpype/hosts/houdini/plugins/load/load_fbx.py +++ b/openpype/hosts/houdini/plugins/load/load_fbx.py @@ -16,8 +16,9 @@ class FbxLoader(load.LoaderPlugin): order = -10 - families = ["model", "staticMesh", "fbx"] - representations = ["fbx"] + families = ["*"] + representations = ["*"] + extensions = {"fbx"} def load(self, context, name=None, namespace=None, data=None): From 5a524b7fd99a7226e450eac2d7a12c2e239cf900 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 19 Jan 2024 16:27:33 +0000 Subject: [PATCH 260/291] Restore actions to objects after update --- .../hosts/blender/plugins/load/load_blend.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 2d5ac18149..4abe4a6afb 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -102,7 +102,6 @@ class BlendLoader(plugin.AssetLoader): # Link all the container children to the collection for obj in container.children_recursive: - print(obj) bpy.context.scene.collection.objects.link(obj) # Remove the library from the blend file @@ -194,8 +193,20 @@ class BlendLoader(plugin.AssetLoader): transform = asset_group.matrix_basis.copy() old_data = dict(asset_group.get(AVALON_PROPERTY)) + old_members = old_data.get("members", []) parent = asset_group.parent + actions = {} + objects_with_anim = [ + obj for obj in asset_group.children_recursive + if obj.animation_data] + for obj in objects_with_anim: + # Check if the object has an action and, if so, add it to a dict + # so we can restore it later. Save and restore the action only + # if it wasn't originally loaded from the current asset. + if obj.animation_data.action not in old_members: + actions[obj.name] = obj.animation_data.action + self.exec_remove(container) asset_group, members = self._process_data(libpath, group_name) @@ -206,6 +217,11 @@ class BlendLoader(plugin.AssetLoader): asset_group.matrix_basis = transform asset_group.parent = parent + # Restore the actions + for obj in asset_group.children_recursive: + if obj.name in actions: + obj.animation_data.action = actions[obj.name] + # Restore the old data, but reset memebers, as they don't exist anymore # This avoids a crash, because the memory addresses of those members # are not valid anymore From 372c74b40674ad254f5665e2b9c0e62234101746 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Jan 2024 18:00:44 +0100 Subject: [PATCH 261/291] use correct variable in queue loop --- openpype/plugins/publish/collect_anatomy_instance_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index 34df49ea5b..8cc1d2b5e3 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -412,7 +412,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): hierarchy_queue = collections.deque() hierarchy_queue.append(hierarchy_context) while hierarchy_queue: - item = hierarchy_context.popleft() + item = hierarchy_queue.popleft() if asset_name in item: return item[asset_name].get("tasks") or {} From 4d1907b94bcd01b916a7ddb70834232ff70af778 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Jan 2024 18:00:59 +0100 Subject: [PATCH 262/291] create copy of hierarchy context before any manipulation --- openpype/plugins/publish/collect_anatomy_instance_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_anatomy_instance_data.py b/openpype/plugins/publish/collect_anatomy_instance_data.py index 8cc1d2b5e3..b1b7ecd138 100644 --- a/openpype/plugins/publish/collect_anatomy_instance_data.py +++ b/openpype/plugins/publish/collect_anatomy_instance_data.py @@ -410,7 +410,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): """ hierarchy_queue = collections.deque() - hierarchy_queue.append(hierarchy_context) + hierarchy_queue.append(copy.deepcopy(hierarchy_context)) while hierarchy_queue: item = hierarchy_queue.popleft() if asset_name in item: From 9a6f518a51bd34ca99c747eb3795b858235a6ce5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 19 Jan 2024 18:01:15 +0100 Subject: [PATCH 263/291] do not store 'hierarchyContext' to variable --- openpype/plugins/publish/extract_hierarchy_to_ayon.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_hierarchy_to_ayon.py b/openpype/plugins/publish/extract_hierarchy_to_ayon.py index b601a3fc29..9e84daca30 100644 --- a/openpype/plugins/publish/extract_hierarchy_to_ayon.py +++ b/openpype/plugins/publish/extract_hierarchy_to_ayon.py @@ -30,8 +30,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): if not AYON_SERVER_ENABLED: return - hierarchy_context = context.data.get("hierarchyContext") - if not hierarchy_context: + if not context.data.get("hierarchyContext"): self.log.debug("Skipping ExtractHierarchyToAYON") return From e0f6497f95006f1190c4a619b566f54386dabf54 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 20 Jan 2024 03:25:30 +0000 Subject: [PATCH 264/291] [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 043b6fbebb..5bb4b4fb0a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.5-nightly.1" +__version__ = "3.18.5-nightly.2" From c3af397348029bf3337262d1fb9f5a469292eb35 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 20 Jan 2024 03:26:05 +0000 Subject: [PATCH 265/291] 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 1177b9e4fe..e831bf3dc1 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.18.5-nightly.2 - 3.18.5-nightly.1 - 3.18.4 - 3.18.4-nightly.1 @@ -134,7 +135,6 @@ body: - 3.15.8-nightly.1 - 3.15.7 - 3.15.7-nightly.3 - - 3.15.7-nightly.2 validations: required: true - type: dropdown From c2ea9303a3a30f491d1984b06f7b91b388f786c1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 22 Jan 2024 11:03:02 +0000 Subject: [PATCH 266/291] Collect non-categorized effects as well --- .../plugins/publish/collect_clip_effects.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py index 89dc66d73c..9c147e88d6 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py @@ -74,32 +74,32 @@ class CollectClipEffects(pyblish.api.InstancePlugin): x["name"]: x["effect_classes"] for x in self.effect_categories } - category_by_effect = {} + category_by_effect = {"": ""} for key, values in effect_categories.items(): for cls in values: category_by_effect[cls] = key effects_categorized = {k: {} for k in effect_categories.keys()} + effects_categorized[""] = {} for key, value in effects.items(): if key == "assignTo": continue # Some classes can have a number in them. Like Text2. - found_cls = None + found_cls = "" for cls in category_by_effect.keys(): if value["class"].startswith(cls): found_cls = cls - if found_cls is None: - continue - effects_categorized[category_by_effect[found_cls]][key] = value - if effects_categorized: - for key in effects_categorized.keys(): - effects_categorized[key]["assignTo"] = effects["assignTo"] - else: - effects_categorized[""] = effects + categories = list(effects_categorized.keys()) + for category in categories: + if not effects_categorized[category]: + effects_categorized.pop(category) + continue + + effects_categorized[category]["assignTo"] = effects["assignTo"] for category, effects in effects_categorized.items(): name = "".join(subset_split) From bfdfa78e57e8c4f3f551569c109ffa5f4c5ed474 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 Jan 2024 10:48:20 +0000 Subject: [PATCH 267/291] Fix problem with independent window --- openpype/hosts/blender/api/capture.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/api/capture.py b/openpype/hosts/blender/api/capture.py index 9922140cea..e16a865607 100644 --- a/openpype/hosts/blender/api/capture.py +++ b/openpype/hosts/blender/api/capture.py @@ -273,10 +273,9 @@ def _independent_window(): current_windows = set(bpy.context.window_manager.windows) with bpy.context.temp_override(**context): bpy.ops.wm.window_new() - window = list(set(bpy.context.window_manager.windows) - current_windows)[0] - context["window"] = window - try: - yield window - finally: - with bpy.context.temp_override(**context): + window = list(set(bpy.context.window_manager.windows) - current_windows)[0] + context["window"] = window + try: + yield window + finally: bpy.ops.wm.window_close() From a6556b4c2cb2c901b1df54460413e77ab278f888 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 23 Jan 2024 10:51:36 +0000 Subject: [PATCH 268/291] Hound fix --- openpype/hosts/blender/api/capture.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/api/capture.py b/openpype/hosts/blender/api/capture.py index e16a865607..e5e6041563 100644 --- a/openpype/hosts/blender/api/capture.py +++ b/openpype/hosts/blender/api/capture.py @@ -273,7 +273,8 @@ def _independent_window(): current_windows = set(bpy.context.window_manager.windows) with bpy.context.temp_override(**context): bpy.ops.wm.window_new() - window = list(set(bpy.context.window_manager.windows) - current_windows)[0] + window = list( + set(bpy.context.window_manager.windows) - current_windows)[0] context["window"] = window try: yield window From 8b9def5b9a281d6f3522af0fb5fe30fad346611c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 Jan 2024 11:41:26 +0000 Subject: [PATCH 269/291] Add default settings. --- server_addon/hiero/server/settings/publish_plugins.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server_addon/hiero/server/settings/publish_plugins.py b/server_addon/hiero/server/settings/publish_plugins.py index 07778b8ebe..f3d1e21fe4 100644 --- a/server_addon/hiero/server/settings/publish_plugins.py +++ b/server_addon/hiero/server/settings/publish_plugins.py @@ -74,5 +74,8 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "tags_addition": [ "review" ] + }, + "CollectClipEffectsModel": { + "effect_categories": [] } } From b0484e6dab9767cc1ea8ffa044ba53a9127bf037 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 Jan 2024 12:02:31 +0000 Subject: [PATCH 270/291] Refactor getting published workfile path --- .../plugins/publish/submit_nuke_deadline.py | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index ead22fe302..746b009255 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -135,23 +135,7 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, "use_published_workfile", self.use_published_workfile ) if use_published_workfile: - for item_ in context: - if "workfile" in item_.data["family"]: - template_data = item_.data.get("anatomyData") - rep = item_.data.get("representations")[0].get("name") - template_data["representation"] = rep - template_data["ext"] = rep - template_data["comment"] = None - anatomy_filled = context.data["anatomy"].format( - template_data - ) - template_filled = anatomy_filled["publish"]["path"] - script_path = os.path.normpath(template_filled) - self.log.info( - "Using published scene for render {}".format( - script_path - ) - ) + script_path = self._get_published_workfile_path(context) # only add main rendering job if target is not frames_farm r_job_response_json = None @@ -210,6 +194,44 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin, families.insert(0, "prerender") instance.data["families"] = families + def _get_published_workfile_path(self, context): + """This method is temporary while the class is not inherited from + AbstractSubmitDeadline""" + for instance in context: + if ( + instance.data["family"] != "workfile" + # Disabled instances won't be integrated + or instance.data("publish") is False + ): + continue + template_data = instance.data["anatomyData"] + # Expect workfile instance has only one representation + representation = instance.data["representations"][0] + # Get workfile extension + repre_file = representation["files"] + self.log.info(repre_file) + ext = os.path.splitext(repre_file)[1].lstrip(".") + + # Fill template data + template_data["representation"] = representation["name"] + template_data["ext"] = ext + template_data["comment"] = None + + anatomy = context.data["anatomy"] + # WARNING Hardcoded template name 'publish' > may not be used + template_obj = anatomy.templates_obj["publish"]["path"] + + template_filled = template_obj.format(template_data) + script_path = os.path.normpath(template_filled) + self.log.info( + "Using published scene for render {}".format( + script_path + ) + ) + return script_path + + return None + def payload_submit( self, instance, From 1d2f97052a527fb379c57518ecae22c6bfe12ca0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 Jan 2024 12:04:59 +0000 Subject: [PATCH 271/291] Add class attribute --- openpype/hosts/hiero/plugins/publish/collect_clip_effects.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py index 9c147e88d6..6647459e8e 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py @@ -9,6 +9,8 @@ class CollectClipEffects(pyblish.api.InstancePlugin): label = "Collect Clip Effects Instances" families = ["clip"] + effect_categories = [] + def process(self, instance): family = "effect" effects = {} From d3276c70dfa8a59a2487e7a6d2feb16c58164652 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 Jan 2024 12:05:05 +0000 Subject: [PATCH 272/291] Fix version --- server_addon/hiero/server/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_addon/hiero/server/version.py b/server_addon/hiero/server/version.py index 3ced3581bb..b3f4756216 100644 --- a/server_addon/hiero/server/version.py +++ b/server_addon/hiero/server/version.py @@ -1 +1 @@ -__version__ = "0.2.1" +__version__ = "0.1.2" From f69c63042f73bfaae4898fbd4ada583f648de5da Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 23 Jan 2024 14:34:44 +0000 Subject: [PATCH 273/291] Update openpype/hosts/hiero/plugins/publish/collect_clip_effects.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- openpype/hosts/hiero/plugins/publish/collect_clip_effects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py index 6647459e8e..d7f646ebc9 100644 --- a/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py +++ b/openpype/hosts/hiero/plugins/publish/collect_clip_effects.py @@ -90,7 +90,7 @@ class CollectClipEffects(pyblish.api.InstancePlugin): # Some classes can have a number in them. Like Text2. found_cls = "" for cls in category_by_effect.keys(): - if value["class"].startswith(cls): + if cls in value["class"]: found_cls = cls effects_categorized[category_by_effect[found_cls]][key] = value From 26409d4fecee3d1dc2b3d94ee545751111aee169 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 Jan 2024 17:36:53 +0000 Subject: [PATCH 274/291] Fix simple instances for Tray Publisher. --- .../plugins/publish/collect_simple_instances.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py index 3fa3c3b8c8..d6e35f4d75 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_simple_instances.py @@ -216,6 +216,11 @@ class CollectSettingsSimpleInstances(pyblish.api.InstancePlugin): instance.data["thumbnailSource"] = first_filepath review_representation["tags"].append("review") + + # Adding "review" to representation name since it can clash with main + # representation if they share the same extension. + review_representation["outputName"] = "review" + self.log.debug("Representation {} was marked for review. {}".format( review_representation["name"], review_path )) From f59e403a3dbfac41eaa8b6debacc0c8f9a746484 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 Jan 2024 17:37:39 +0000 Subject: [PATCH 275/291] Fix edge case for ExtractOIIOTranscode --- openpype/plugins/publish/extract_color_transcode.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index faacb7af2e..052759c10b 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -189,6 +189,13 @@ class ExtractOIIOTranscode(publish.Extractor): if len(new_repre["files"]) == 1: new_repre["files"] = new_repre["files"][0] + # If the source representation has "review" tag, but its not + # part of the output defintion tags, then both the + # representations will be transcoded in ExtractReview and + # their outputs will clash in integration. + if not added_review and "review" in repre.get("tags", []): + added_review = True + new_representations.append(new_repre) added_representations = True From 741eab7140c627b19d71719e4b03424d5481209f Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 23 Jan 2024 17:37:53 +0000 Subject: [PATCH 276/291] Fix thumbnail extraction --- openpype/plugins/publish/extract_thumbnail.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_thumbnail.py b/openpype/plugins/publish/extract_thumbnail.py index 2b4ea0529a..10eb261482 100644 --- a/openpype/plugins/publish/extract_thumbnail.py +++ b/openpype/plugins/publish/extract_thumbnail.py @@ -231,7 +231,10 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "files": jpeg_file, "stagingDir": dst_staging, "thumbnail": True, - "tags": new_repre_tags + "tags": new_repre_tags, + # If source image is jpg then there can be clash when + # integrating to making the output name explicit. + "outputName": "thumbnail" } # adding representation From 97c465c20f57aff7a87e3604ed30849a6feec09d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 24 Jan 2024 03:25:59 +0000 Subject: [PATCH 277/291] [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 5bb4b4fb0a..674ea2cb8a 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.5-nightly.2" +__version__ = "3.18.5-nightly.3" From c8e00d4aa4cfc81d967c8d607bd409af9659b068 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Jan 2024 03:26:36 +0000 Subject: [PATCH 278/291] 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 e831bf3dc1..1816ffe515 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.18.5-nightly.3 - 3.18.5-nightly.2 - 3.18.5-nightly.1 - 3.18.4 @@ -134,7 +135,6 @@ body: - 3.15.8-nightly.2 - 3.15.8-nightly.1 - 3.15.7 - - 3.15.7-nightly.3 validations: required: true - type: dropdown From 24cf858a29f742bf552df7d353052e288e13fee3 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 24 Jan 2024 07:50:38 +0000 Subject: [PATCH 279/291] Remove duplicate plugin --- .../publish/validate_look_members_unique.py | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 openpype/hosts/maya/plugins/publish/validate_look_members_unique.py diff --git a/openpype/hosts/maya/plugins/publish/validate_look_members_unique.py b/openpype/hosts/maya/plugins/publish/validate_look_members_unique.py deleted file mode 100644 index 4e01b55249..0000000000 --- a/openpype/hosts/maya/plugins/publish/validate_look_members_unique.py +++ /dev/null @@ -1,77 +0,0 @@ -from collections import defaultdict - -import pyblish.api - -import openpype.hosts.maya.api.action -from openpype.pipeline.publish import ( - PublishValidationError, ValidatePipelineOrder) - - -class ValidateUniqueRelationshipMembers(pyblish.api.InstancePlugin): - """Validate the relational nodes of the look data to ensure every node is - unique. - - This ensures the all member ids are unique. Every node id must be from - a single node in the scene. - - That means there's only ever one of a specific node inside the look to be - published. For example if you'd have a loaded 3x the same tree and by - accident you're trying to publish them all together in a single look that - would be invalid, because they are the same tree. It should be included - inside the look instance only once. - - """ - - order = ValidatePipelineOrder - label = 'Look members unique' - hosts = ['maya'] - families = ['look'] - - actions = [openpype.hosts.maya.api.action.SelectInvalidAction, - openpype.hosts.maya.api.action.GenerateUUIDsOnInvalidAction] - - def process(self, instance): - """Process all meshes""" - - invalid = self.get_invalid(instance) - if invalid: - raise PublishValidationError( - ("Members found without non-unique IDs: " - "{0}").format(invalid)) - - @staticmethod - def get_invalid(instance): - """ - Check all the relationship members of the objectSets - - Example of the lookData relationships: - {"uuid": 59b2bb27bda2cb2776206dd8:79ab0a63ffdf, - "members":[{"uuid": 59b2bb27bda2cb2776206dd8:1b158cc7496e, - "name": |model_GRP|body_GES|body_GESShape} - ..., - ...]} - - Args: - instance: - - Returns: - - """ - - # Get all members from the sets - id_nodes = defaultdict(set) - relationships = instance.data["lookData"]["relationships"] - - for relationship in relationships.values(): - for member in relationship['members']: - node_id = member["uuid"] - node = member["name"] - id_nodes[node_id].add(node) - - # Check if any id has more than 1 node - invalid = [] - for nodes in id_nodes.values(): - if len(nodes) > 1: - invalid.extend(nodes) - - return invalid From 560698bcd8e600b5ab28ce0d7a2fd6e2fabae15b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 24 Jan 2024 12:19:02 +0000 Subject: [PATCH 280/291] Missing product_names to subsets conversion. --- openpype/settings/ayon_settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 36f96c9dc7..8c6524f423 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1236,6 +1236,8 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): for profile in extract_oiio_transcode_profiles: new_outputs = {} name_counter = {} + if "product_names" in profile: + profile["subsets"] = profile.pop("product_names") for profile_output in profile["outputs"]: if "name" in profile_output: name = profile_output.pop("name") From 277fcd82fb474437d14c8147f4e628b145899b6e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 24 Jan 2024 15:52:35 +0100 Subject: [PATCH 281/291] Extended error message --- .../maya/plugins/publish/validate_scene_set_workspace.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py b/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py index b48d67e416..ad89b7c791 100644 --- a/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py +++ b/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py @@ -44,4 +44,7 @@ class ValidateSceneSetWorkspace(pyblish.api.ContextPlugin): if not is_subdir(scene_name, root_dir): raise PublishValidationError( - "Maya workspace is not set correctly.") + "Maya workspace is not set correctly.\n\n" + f"`{os.path.dirname(scene_name)}` is not in `{root_dir}`.\n\n" + "Please use Workfile app to re-save." + ) From 8ca681109874760d04edc11b3c314b8b736430a0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 25 Jan 2024 16:13:42 +0800 Subject: [PATCH 282/291] bugfix the save scene for camera error out when multi camera option turned off --- .../hosts/max/plugins/publish/save_scenes_for_cameras.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py index c39109417b..f089bf663c 100644 --- a/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py +++ b/openpype/hosts/max/plugins/publish/save_scenes_for_cameras.py @@ -21,6 +21,11 @@ class SaveScenesForCamera(pyblish.api.InstancePlugin): families = ["maxrender"] def process(self, instance): + if not instance.data.get("multiCamera"): + self.log.debug( + "Multi Camera disabled. " + "Skipping to save scene files for cameras") + return current_folder = rt.maxFilePath current_filename = rt.maxFileName current_filepath = os.path.join(current_folder, current_filename) From cb9802e3fe63fa7542b463b0c613bf8480260492 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 25 Jan 2024 11:04:56 +0000 Subject: [PATCH 283/291] Update openpype/plugins/publish/extract_color_transcode.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 052759c10b..922df469fe 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -193,7 +193,7 @@ class ExtractOIIOTranscode(publish.Extractor): # part of the output defintion tags, then both the # representations will be transcoded in ExtractReview and # their outputs will clash in integration. - if not added_review and "review" in repre.get("tags", []): + if "review" in repre.get("tags", []): added_review = True new_representations.append(new_repre) From ac383a5de66e79aec84fb19a2a71ecd55b2ef224 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 25 Jan 2024 11:33:29 +0000 Subject: [PATCH 284/291] Fix missing animation data when updating blend assets --- openpype/hosts/blender/plugins/load/load_blend.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index 4abe4a6afb..1a84f5afbb 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -220,6 +220,8 @@ class BlendLoader(plugin.AssetLoader): # Restore the actions for obj in asset_group.children_recursive: if obj.name in actions: + if not obj.animation_data: + obj.animation_data_create() obj.animation_data.action = actions[obj.name] # Restore the old data, but reset memebers, as they don't exist anymore From 77a12f49f8ad91b2771908b1dcfb9f7f4721fafd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Jan 2024 12:34:35 +0100 Subject: [PATCH 285/291] fix project name duplication in project folders --- openpype/pipeline/project_folders.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/project_folders.py b/openpype/pipeline/project_folders.py index 1bcba5c320..ecdb5cf6d3 100644 --- a/openpype/pipeline/project_folders.py +++ b/openpype/pipeline/project_folders.py @@ -28,13 +28,14 @@ def concatenate_splitted_paths(split_paths, anatomy): # backward compatibility if "__project_root__" in path_items: for root, root_path in anatomy.roots.items(): - if not os.path.exists(str(root_path)): - log.debug("Root {} path path {} not exist on \ - computer!".format(root, root_path)) + if not root_path: continue - clean_items = ["{{root[{}]}}".format(root), - r"{project[name]}"] + clean_items[1:] - output.append(os.path.normpath(os.path.sep.join(clean_items))) + root_items = [ + "{{root[{}]}}".format(root), + "{project[name]}" + ] + root_items.extend(clean_items[1:]) + output.append(os.path.normpath(os.path.sep.join(root_items))) continue output.append(os.path.normpath(os.path.sep.join(clean_items))) From 8f6d78e4eb9985efa689a7c7a1548bcde5231a14 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 25 Jan 2024 12:41:15 +0100 Subject: [PATCH 286/291] skip path exists --- openpype/pipeline/project_folders.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/project_folders.py b/openpype/pipeline/project_folders.py index ecdb5cf6d3..608344ce03 100644 --- a/openpype/pipeline/project_folders.py +++ b/openpype/pipeline/project_folders.py @@ -28,8 +28,14 @@ def concatenate_splitted_paths(split_paths, anatomy): # backward compatibility if "__project_root__" in path_items: for root, root_path in anatomy.roots.items(): - if not root_path: + if not root_path or not os.path.exists(str(root_path)): + log.debug( + "Root {} path path {} not exist on computer!".format( + root, root_path + ) + ) continue + root_items = [ "{{root[{}]}}".format(root), "{project[name]}" From de2d70a8a34e03c27c05a085fdfee408e19a0d79 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Jan 2024 13:30:16 +0100 Subject: [PATCH 287/291] Added settings for Fusion creators to legacy OP (#6162) --- .../defaults/project_settings/fusion.json | 6 ++++-- .../projects_schema/schema_project_fusion.json | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index 15b6bfc09b..f890f94b6f 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -31,7 +31,8 @@ "reviewable", "farm_rendering" ], - "image_format": "exr" + "image_format": "exr", + "default_frame_range_option": "asset_db" }, "CreateImageSaver": { "temp_rendering_path_template": "{workdir}/renders/fusion/{subset}/{subset}.{ext}", @@ -43,7 +44,8 @@ "reviewable", "farm_rendering" ], - "image_format": "exr" + "image_format": "exr", + "default_frame": 0 } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json index 8669842087..84d1efae78 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_fusion.json @@ -116,6 +116,17 @@ {"tif": "tif"}, {"jpg": "jpg"} ] + }, + { + "key": "default_frame_range_option", + "label": "Default frame range source", + "type": "enum", + "multiselect": false, + "enum_items": [ + {"asset_db": "Current asset context"}, + {"render_range": "From render in/out"}, + {"comp_range": "From composition timeline"} + ] } ] }, @@ -165,6 +176,11 @@ {"tif": "tif"}, {"jpg": "jpg"} ] + }, + { + "type": "number", + "key": "default_frame", + "label": "Default rendered frame" } ] } From b783fae8e0262a9c0edc689ad84162d180b8e574 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 25 Jan 2024 13:31:12 +0100 Subject: [PATCH 288/291] Update openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py Co-authored-by: Toke Jepsen --- .../hosts/maya/plugins/publish/validate_scene_set_workspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py b/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py index ad89b7c791..0c780d3a69 100644 --- a/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py +++ b/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py @@ -45,6 +45,6 @@ class ValidateSceneSetWorkspace(pyblish.api.ContextPlugin): if not is_subdir(scene_name, root_dir): raise PublishValidationError( "Maya workspace is not set correctly.\n\n" - f"`{os.path.dirname(scene_name)}` is not in `{root_dir}`.\n\n" + f"Current workfile `{scene_name}` is not inside the current Maya project root directory `{root_dir}`.\n\n" "Please use Workfile app to re-save." ) From a0faa3f130c7a806cec1d045dceceb7ee09ea3e2 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 25 Jan 2024 12:34:53 +0000 Subject: [PATCH 289/291] Update openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py --- .../hosts/maya/plugins/publish/validate_scene_set_workspace.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py b/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py index 0c780d3a69..ddcbab8931 100644 --- a/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py +++ b/openpype/hosts/maya/plugins/publish/validate_scene_set_workspace.py @@ -45,6 +45,7 @@ class ValidateSceneSetWorkspace(pyblish.api.ContextPlugin): if not is_subdir(scene_name, root_dir): raise PublishValidationError( "Maya workspace is not set correctly.\n\n" - f"Current workfile `{scene_name}` is not inside the current Maya project root directory `{root_dir}`.\n\n" + f"Current workfile `{scene_name}` is not inside the " + "current Maya project root directory `{root_dir}`.\n\n" "Please use Workfile app to re-save." ) From 20e089c30ea8834553c8d7888542ca0101787ba7 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 25 Jan 2024 14:04:28 +0000 Subject: [PATCH 290/291] [Automated] Release --- CHANGELOG.md | 215 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 217 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 546b2c12ea..14f0bc469f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,221 @@ # Changelog +## [3.18.5](https://github.com/ynput/OpenPype/tree/3.18.5) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.18.4...3.18.5) + +### **🚀 Enhancements** + + +
+Chore: Add addons dir only if exists #6140 + +Do not add addons directory path for addons discovery if does not exists. + + +___ + +
+ + +
+Hiero: Effect Categories - OP-7397 #6143 + +This PR introduces `Effect Categories` for the Hiero settings. This allows studios to split effect stacks into meaningful subsets. + + +___ + +
+ + +
+Nuke: Render Workfile Attributes #6146 + +`Workfile Dependency` default value can now be controlled from project settings.`Use Published Workfile` makes using published workfiles for rendering optional. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Attributes are locked after publishing if they are locked in Camera Family #6073 + +This PR is to make sure unlock attributes only during the bake context, make sure attributes are relocked after to preserve the lock state of the original node being baked. + + +___ + +
+ + +
+Missing nuke family Windows arguments #6131 + +Default Windows arguments for launching the Nuke family was missing. + + +___ + +
+ + +
+AYON: Fix the bug on the limit group not being set correctly in Maya Deadline Setting #6139 + +This PR is to bug-fix the limit groups from maya deadline settings errored out when the user tries to edit the setting. + + +___ + +
+ + +
+Chore: Transcoding extensions add missing '.tif' extension #6142 + +Image extensions in transcoding helper was missing `.tif` extension and had `.tiff` twice. + + +___ + +
+ + +
+Blender: Use the new API for override context #6145 + +Blender 4.0 disabled the old API to override context. This API updates the code to use the new API. + + +___ + +
+ + +
+BugFix: Include Model in FBX Loader in Houdini #6150 + +A quick bugfig where we can't load fbx exported from blender. The bug was reported here. + + +___ + +
+ + +
+Blender: Restore actions to objects after update #6153 + +Restore the actions assigned to objects after updating assets from blend files. + + +___ + +
+ + +
+Chore: Collect template data with hierarchy context #6154 + +Fixed queue loop where is used wrong variable to pop items from queue. + + +___ + +
+ + +
+OP-6382 - Thumbnail Integration Problem #6156 + +This ticket alerted to 3 different cases of integration issues; +- [x] Using the Tray Publisher with the same image format (extension) for representation and review representation. +- [x] Clash on publish file path from output definitions in `ExtractOIIOTranscode`. +- [x] Clash on publish file from thumbnail in `ExtractThumbnail`There might be an issue with this fix, if a studio does not use the `{output}` token in their `render` anatomy template. But thinking if they have customized it, they will be responsible to maintain these edge cases. + + +___ + +
+ + +
+Max: Bugfix saving camera scene errored out when creating render instance with multi-camera option turned off #6163 + +This PR is to make sure the integrator of saving camera scene turned off and the render submitted successfully when multi-camera options being turned off in 3dsmax + + +___ + +
+ + +
+Chore: Fix duplicated project name on create project structure #6166 + +Small fix in project folders. It is not used same variable name to change values which breaks values on any next loop. + + +___ + +
+ +### **Merged pull requests** + + +
+Maya: Remove duplicate plugin #6157 + +The two plugins below are doing the same work, so we can remove the one focused solely on lookdev.https://github.com/ynput/OpenPype/blob/develop/openpype/hosts/maya/plugins/publish/validate_look_members_unique.pyhttps://github.com/ynput/OpenPype/blob/develop/openpype/hosts/maya/plugins/publish/validate_node_ids_unique.py + + +___ + +
+ + +
+Publish report viewer: Report items sorting #6092 + +Proposal of items sorting in Publish report viewer tool. Items are sorted by report creation time. Creation time is also added to publish report data when saved from publisher tool. + + +___ + +
+ + +
+Maya: Extended error message #6161 + +Added more details to message + + +___ + +
+ + +
+Fusion: Added settings for Fusion creators to legacy OP #6162 + +Added missing OP variant of setting for new Fusion creator. + + +___ + +
+ + + + ## [3.18.4](https://github.com/ynput/OpenPype/tree/3.18.4) diff --git a/openpype/version.py b/openpype/version.py index 674ea2cb8a..ddfb3ebeeb 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.18.5-nightly.3" +__version__ = "3.18.5" diff --git a/pyproject.toml b/pyproject.toml index f9d5381a15..24172aa77f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.18.4" # OpenPype +version = "3.18.5" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 6c83642af6bf7238ae686a1fa22aa95187b42192 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 25 Jan 2024 14:05:21 +0000 Subject: [PATCH 291/291] 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 1816ffe515..fd6999604a 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.18.5 - 3.18.5-nightly.3 - 3.18.5-nightly.2 - 3.18.5-nightly.1 @@ -134,7 +135,6 @@ body: - 3.15.8-nightly.3 - 3.15.8-nightly.2 - 3.15.8-nightly.1 - - 3.15.7 validations: required: true - type: dropdown