From c5b05a95c6799284bf688ef8a0b45e67841dc6b4 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 10 Aug 2023 11:42:08 +0100 Subject: [PATCH 001/186] Basic implementation --- .../blender/plugins/create/create_render.py | 49 +++++ .../blender/plugins/publish/collect_render.py | 81 ++++++++ .../publish/submit_blender_deadline.py | 175 ++++++++++++++++++ .../plugins/publish/submit_publish_job.py | 2 +- 4 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/blender/plugins/create/create_render.py create mode 100644 openpype/hosts/blender/plugins/publish/collect_render.py create mode 100644 openpype/modules/deadline/plugins/publish/submit_blender_deadline.py diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py new file mode 100644 index 0000000000..8323b88cfe --- /dev/null +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -0,0 +1,49 @@ +"""Create render.""" + +import bpy + +from openpype.pipeline import get_current_task_name +from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES + + +class CreateRenderlayer(plugin.Creator): + """Single baked camera""" + + name = "renderingMain" + label = "Render" + family = "renderlayer" + icon = "eye" + + render_settings = {} + + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process) + ops.execute_in_main_thread(mti) + + def _process(self): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + asset = self.data["asset"] + subset = self.data["subset"] + name = plugin.asset_name(asset, subset) + asset_group = bpy.data.collections.new(name=name) + instances.children.link(asset_group) + self.data['task'] = get_current_task_name() + lib.imprint(asset_group, self.data) + + if (self.options or {}).get("useSelection"): + selected = lib.get_selection() + for obj in selected: + asset_group.objects.link(obj) + elif (self.options or {}).get("asset_group"): + obj = (self.options or {}).get("asset_group") + asset_group.objects.link(obj) + + return asset_group diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py new file mode 100644 index 0000000000..c0d314e466 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +"""Collect render data.""" + +import os +import re + +import bpy + +import pyblish.api + + +class CollectBlenderRender(pyblish.api.InstancePlugin): + """Gather all publishable render layers from renderSetup.""" + + order = pyblish.api.CollectorOrder + 0.01 + hosts = ["blender"] + families = ["renderlayer"] + label = "Collect Render Layers" + sync_workfile_version = False + + def process(self, instance): + context = instance.context + + filepath = context.data["currentFile"].replace("\\", "/") + + frame_start = context.data["frameStart"] + frame_end = context.data["frameEnd"] + frame_handle_start = context.data["frameStartHandle"] + frame_handle_end = context.data["frameEndHandle"] + + instance.data.update({ + "frameStart": frame_start, + "frameEnd": frame_end, + "frameStartHandle": frame_handle_start, + "frameEndHandle": frame_handle_end, + "fps": context.data["fps"], + "byFrameStep": bpy.context.scene.frame_step, + "farm": True, + "toBeRenderedOn": "deadline", + }) + + # instance.data["expectedFiles"] = self.generate_expected_files( + # instance, filepath) + + expected_files = [] + + for frame in range( + int(frame_start), + int(frame_end) + 1, + int(bpy.context.scene.frame_step), + ): + frame_str = str(frame).rjust(4, "0") + expected_files.append(f"C:/tmp/{frame_str}.png") + + instance.data["expectedFiles"] = expected_files + + self.log.debug(instance.data["expectedFiles"]) + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py new file mode 100644 index 0000000000..66be306a52 --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +"""Submitting render job to Deadline.""" + +import os +import getpass +import attr +from datetime import datetime + +import bpy + +from openpype.lib import is_running_from_build +from openpype.pipeline import legacy_io +from openpype.pipeline.farm.tools import iter_expected_files +from openpype.tests.lib import is_in_tests + +from openpype_modules.deadline import abstract_submit_deadline +from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo + + +def _validate_deadline_bool_value(instance, attribute, value): + if not isinstance(value, (str, bool)): + raise TypeError(f"Attribute {attribute} must be str or bool.") + if value not in {"1", "0", True, False}: + raise ValueError( + f"Value of {attribute} must be one of '0', '1', True, False") + + +@attr.s +class BlenderPluginInfo(): + SceneFile = attr.ib(default=None) # Input + Version = attr.ib(default=None) # Mandatory for Deadline + + +class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): + label = "Submit Render to Deadline" + hosts = ["blender"] + families = ["renderlayer"] + + priority = 50 + + jobInfo = {} + pluginInfo = {} + group = None + + def get_job_info(self): + job_info = DeadlineJobInfo(Plugin="Blender") + + job_info.update(self.jobInfo) + + instance = self._instance + context = instance.context + + # Always use the original work file name for the Job name even when + # rendering is done from the published Work File. The original work + # file name is clearer because it can also have subversion strings, + # etc. which are stripped for the published file. + src_filepath = context.data["currentFile"] + src_filename = os.path.basename(src_filepath) + + if is_in_tests(): + src_filename += datetime.now().strftime("%d%m%Y%H%M%S") + + job_info.Name = f"{src_filename} - {instance.name}" + job_info.BatchName = src_filename + instance.data.get("blenderRenderPlugin", "Blender") + job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) + + # Deadline requires integers in frame range + frames = "{start}-{end}x{step}".format( + start=int(instance.data["frameStartHandle"]), + end=int(instance.data["frameEndHandle"]), + step=int(instance.data["byFrameStep"]), + ) + job_info.Frames = frames + + job_info.Pool = instance.data.get("primaryPool") + job_info.SecondaryPool = instance.data.get("secondaryPool") + job_info.Comment = context.data.get("comment") + job_info.Priority = instance.data.get("priority", self.priority) + + if self.group != "none" and self.group: + job_info.Group = self.group + + attr_values = self.get_attr_values_from_data(instance.data) + render_globals = instance.data.setdefault("renderGlobals", {}) + machine_list = attr_values.get("machineList", "") + if machine_list: + if attr_values.get("whitelist", True): + machine_list_key = "Whitelist" + else: + machine_list_key = "Blacklist" + render_globals[machine_list_key] = machine_list + + job_info.Priority = attr_values.get("priority") + job_info.ChunkSize = attr_values.get("chunkSize") + + # Add options from RenderGlobals + render_globals = instance.data.get("renderGlobals", {}) + job_info.update(render_globals) + + keys = [ + "FTRACK_API_KEY", + "FTRACK_API_USER", + "FTRACK_SERVER", + "OPENPYPE_SG_USER", + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK", + "AVALON_APP_NAME", + "OPENPYPE_DEV" + "IS_TEST" + ] + + # Add OpenPype version if we are running from build. + if is_running_from_build(): + keys.append("OPENPYPE_VERSION") + + # Add mongo url if it's enabled + if self._instance.context.data.get("deadlinePassMongoUrl"): + keys.append("OPENPYPE_MONGO") + + environment = dict({key: os.environ[key] for key in keys + if key in os.environ}, **legacy_io.Session) + + for key in keys: + value = environment.get(key) + if not value: + continue + job_info.EnvironmentKeyValue[key] = value + + # to recognize job from PYPE for turning Event On/Off + job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" + job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1" + + # Adding file dependencies. + if self.asset_dependencies: + dependencies = instance.context.data["fileDependencies"] + for dependency in dependencies: + job_info.AssetDependency += dependency + + # Add list of expected files to job + # --------------------------------- + exp = instance.data.get("expectedFiles") + for filepath in iter_expected_files(exp): + job_info.OutputDirectory += os.path.dirname(filepath) + job_info.OutputFilename += os.path.basename(filepath) + + return job_info + + def get_plugin_info(self): + instance = self._instance + context = instance.context + + plugin_info = BlenderPluginInfo( + SceneFile=self.scene_path, + Version=bpy.app.version_string, + ) + + plugin_payload = attr.asdict(plugin_info) + + # Patching with pluginInfo from settings + for key, value in self.pluginInfo.items(): + plugin_payload[key] = value + + return plugin_payload + + def process(self, instance): + output_dir = "C:/tmp" + instance.data["outputDir"] = output_dir + + super(BlenderSubmitDeadline, self).process(instance) + + # TODO: Avoid the need for this logic here, needed for submit publish + # Store output dir for unified publisher (filesequence) + # output_dir = os.path.dirname(instance.data["expectedFiles"][0]) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index ec182fcd66..47cb441143 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -95,7 +95,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, targets = ["local"] hosts = ["fusion", "max", "maya", "nuke", "houdini", - "celaction", "aftereffects", "harmony"] + "celaction", "aftereffects", "harmony", "blender"] families = ["render.farm", "prerender.farm", "renderlayer", "imagesequence", From fb76d6348155b6f0db6dee923d74a653921d3707 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 10 Aug 2023 17:41:02 +0100 Subject: [PATCH 002/186] Implemented settings and removed hardcoded paths --- .../blender/plugins/publish/collect_render.py | 150 +++++++++++++----- .../publish/submit_blender_deadline.py | 41 +++-- .../defaults/project_settings/blender.json | 4 + .../defaults/project_settings/deadline.json | 9 ++ .../schema_project_blender.json | 31 ++++ .../schema_project_deadline.json | 44 +++++ 6 files changed, 228 insertions(+), 51 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index c0d314e466..eb37d5b946 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -6,6 +6,12 @@ import re import bpy +from openpype.pipeline import ( + get_current_project_name, +) +from openpype.settings import ( + get_project_settings, +) import pyblish.api @@ -18,16 +24,117 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): label = "Collect Render Layers" sync_workfile_version = False + @staticmethod + def get_default_render_folder(settings): + """Get default render folder from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["default_render_image_folder"]) + + @staticmethod + def get_image_format(settings): + """Get image format from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["image_format"]) + + @staticmethod + def get_render_product(file_path, render_folder, file_name, instance, ext): + output_file = os.path.join( + file_path, render_folder, file_name, instance.name) + + render_product = f"{output_file}.####.{ext}" + render_product = render_product.replace("\\", "/") + + return render_product + + @staticmethod + def generate_expected_files( + render_product, frame_start, frame_end, frame_step + ): + path = os.path.dirname(render_product) + file = os.path.basename(render_product) + + expected_files = [] + + for frame in range(frame_start, frame_end + 1, frame_step): + frame_str = str(frame).rjust(4, "0") + expected_file = os.path.join(path, re.sub("#+", frame_str, file)) + expected_files.append(expected_file.replace("\\", "/")) + + return expected_files + + @staticmethod + def set_render_format(ext): + image_settings = bpy.context.scene.render.image_settings + + if ext == "exr": + image_settings.file_format = "OPEN_EXR" + elif ext == "bmp": + image_settings.file_format = "BMP" + elif ext == "iris": + image_settings.file_format = "IRIS" + elif ext == "png": + image_settings.file_format = "PNG" + elif ext == "jpeg": + image_settings.file_format = "JPEG" + elif ext == "jpeg2000": + image_settings.file_format = "JPEG2000" + elif ext == "tga": + image_settings.file_format = "TARGA" + elif ext == "tga_raw": + image_settings.file_format = "TARGA_RAW" + elif ext == "tiff": + image_settings.file_format = "TIFF" + + @staticmethod + def set_render_camera(instance): + # There should be only one camera in the instance + found = False + for obj in instance: + if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA": + bpy.context.scene.camera = obj + found = True + break + + assert found, "No camera found in the render instance" + def process(self, instance): context = instance.context filepath = context.data["currentFile"].replace("\\", "/") + file_path = os.path.dirname(filepath) + file_name = os.path.basename(filepath) + file_name, _ = os.path.splitext(file_name) + + project = get_current_project_name() + settings = get_project_settings(project) + + render_folder = self.get_default_render_folder(settings) + ext = self.get_image_format(settings) + + render_product = self.get_render_product( + file_path, render_folder, file_name, instance, ext) + + # We set the render path, the format and the camera + bpy.context.scene.render.filepath = render_product + self.set_render_format(ext) + self.set_render_camera(instance) + + # We save the file to save the render settings + bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) frame_start = context.data["frameStart"] frame_end = context.data["frameEnd"] frame_handle_start = context.data["frameStartHandle"] frame_handle_end = context.data["frameEndHandle"] + expected_files = self.generate_expected_files( + render_product, int(frame_start), int(frame_end), + int(bpy.context.scene.frame_step)) + instance.data.update({ "frameStart": frame_start, "frameEnd": frame_end, @@ -36,46 +143,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): "fps": context.data["fps"], "byFrameStep": bpy.context.scene.frame_step, "farm": True, - "toBeRenderedOn": "deadline", + "expectedFiles": expected_files, }) - # instance.data["expectedFiles"] = self.generate_expected_files( - # instance, filepath) - - expected_files = [] - - for frame in range( - int(frame_start), - int(frame_end) + 1, - int(bpy.context.scene.frame_step), - ): - frame_str = str(frame).rjust(4, "0") - expected_files.append(f"C:/tmp/{frame_str}.png") - - instance.data["expectedFiles"] = expected_files - - self.log.debug(instance.data["expectedFiles"]) - - def generate_expected_files(self, instance, path): - """Create expected files in instance data""" - - dir = os.path.dirname(path) - file = os.path.basename(path) - - if "#" in file: - def replace(match): - return "%0{}d".format(len(match.group())) - - file = re.sub("#+", replace, file) - - if "%" not in file: - return path - - expected_files = [] - start = instance.data["frameStart"] - end = instance.data["frameEnd"] - for i in range(int(start), (int(end) + 1)): - expected_files.append( - os.path.join(dir, (file % i)).replace("\\", "/")) - - return expected_files + self.log.info(f"data: {instance.data}") diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 66be306a52..761c5b7b06 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -29,6 +29,7 @@ def _validate_deadline_bool_value(instance, attribute, value): class BlenderPluginInfo(): SceneFile = attr.ib(default=None) # Input Version = attr.ib(default=None) # Mandatory for Deadline + SaveFile = attr.ib(default=True) class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): @@ -36,8 +37,9 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): hosts = ["blender"] families = ["renderlayer"] + use_published = True priority = 50 - + chunk_size = 1 jobInfo = {} pluginInfo = {} group = None @@ -148,12 +150,10 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): return job_info def get_plugin_info(self): - instance = self._instance - context = instance.context - plugin_info = BlenderPluginInfo( SceneFile=self.scene_path, Version=bpy.app.version_string, + SaveFile=True, ) plugin_payload = attr.asdict(plugin_info) @@ -164,12 +164,33 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): return plugin_payload - def process(self, instance): - output_dir = "C:/tmp" + def process_submission(self): + instance = self._instance + + expected_files = instance.data["expectedFiles"] + if not expected_files: + raise RuntimeError("No Render Elements found!") + + output_dir = os.path.dirname(expected_files[0]) instance.data["outputDir"] = output_dir + instance.data["toBeRenderedOn"] = "deadline" - super(BlenderSubmitDeadline, self).process(instance) + file = os.path.basename(bpy.context.scene.render.filepath) + bpy.context.scene.render.filepath = os.path.join(output_dir, file) + bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) - # TODO: Avoid the need for this logic here, needed for submit publish - # Store output dir for unified publisher (filesequence) - # output_dir = os.path.dirname(instance.data["expectedFiles"][0]) + self.log.debug(f"expected_files[0]: {expected_files[0]}") + self.log.debug(f"Output dir: {output_dir}") + + payload = self.assemble_payload() + return self.submit(payload) + + def from_published_scene(self): + """ Do not overwrite expected files. + + Use published is set to True, so rendering will be triggered + from published scene (in 'publish' folder). Default implementation + of abstract class renames expected (eg. rendered) files accordingly + which is not needed here. + """ + return super().from_published_scene(False) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index df865adeba..333a1fed56 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -17,6 +17,10 @@ "rules": {} } }, + "RenderSettings": { + "default_render_image_folder": "renders/blender", + "image_format": "exr" + }, "workfile_builder": { "create_first_version": false, "custom_templates": [] diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 1b8c8397d7..33ea533863 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -99,6 +99,15 @@ "deadline_chunk_size": 10, "deadline_job_delay": "00:00:00:00" }, + "BlenderSubmitDeadline": { + "enabled": true, + "optional": false, + "active": true, + "use_published": true, + "priority": 50, + "chunk_size": 10, + "group": "none" + }, "ProcessSubmittedJobOnFarm": { "enabled": true, "deadline_department": "", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index aeb70dfd8c..787e190de5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -54,6 +54,37 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "RenderSettings", + "label": "Render Settings", + "children": [ + { + "type": "text", + "key": "default_render_image_folder", + "label": "Default render image folder" + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"exr": "exr"}, + {"bmp": "bmp"}, + {"iris": "iris"}, + {"png": "png"}, + {"jpeg": "jpeg"}, + {"jpeg2000": "jpeg2000"}, + {"tga": "tga"}, + {"tga_raw": "tga_raw"}, + {"tiff": "tiff"} + ] + } + ] + }, { "type": "schema_template", "name": "template_workfile_options", 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 6d59b5a92b..596bc30f91 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -531,6 +531,50 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "BlenderSubmitDeadline", + "label": "Blender Submit to Deadline", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "boolean", + "key": "use_published", + "label": "Use Published scene" + }, + { + "type": "number", + "key": "priority", + "label": "Priority" + }, + { + "type": "number", + "key": "chunk_size", + "label": "Frame per Task" + }, + { + "type": "text", + "key": "group", + "label": "Group Name" + } + ] + }, { "type": "dict", "collapsible": true, From a1e8a5eb4c49893291403c4283a60b8a7fad27cf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 10 Aug 2023 17:54:45 +0100 Subject: [PATCH 003/186] Removed some missed leftover code --- .../deadline/plugins/publish/submit_blender_deadline.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 761c5b7b06..b3deb39399 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -175,13 +175,6 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): instance.data["outputDir"] = output_dir instance.data["toBeRenderedOn"] = "deadline" - file = os.path.basename(bpy.context.scene.render.filepath) - bpy.context.scene.render.filepath = os.path.join(output_dir, file) - bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) - - self.log.debug(f"expected_files[0]: {expected_files[0]}") - self.log.debug(f"Output dir: {output_dir}") - payload = self.assemble_payload() return self.submit(payload) From 17b5a86c51c3d1ca3f1a16f8cb249f8c3e8d5ca4 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 11 Aug 2023 11:24:09 +0100 Subject: [PATCH 004/186] Fixed problem with image format --- .../blender/plugins/publish/collect_render.py | 8 +++----- .../projects_schema/schema_project_blender.json | 17 ++++++++--------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index eb37d5b946..11b98c76e6 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -74,19 +74,17 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): image_settings.file_format = "OPEN_EXR" elif ext == "bmp": image_settings.file_format = "BMP" - elif ext == "iris": + elif ext == "rgb": image_settings.file_format = "IRIS" elif ext == "png": image_settings.file_format = "PNG" elif ext == "jpeg": image_settings.file_format = "JPEG" - elif ext == "jpeg2000": + elif ext == "jp2": image_settings.file_format = "JPEG2000" elif ext == "tga": image_settings.file_format = "TARGA" - elif ext == "tga_raw": - image_settings.file_format = "TARGA_RAW" - elif ext == "tiff": + elif ext == "tif": image_settings.file_format = "TIFF" @staticmethod diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 787e190de5..84efec5c0a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -72,15 +72,14 @@ "multiselection": false, "defaults": "exr", "enum_items": [ - {"exr": "exr"}, - {"bmp": "bmp"}, - {"iris": "iris"}, - {"png": "png"}, - {"jpeg": "jpeg"}, - {"jpeg2000": "jpeg2000"}, - {"tga": "tga"}, - {"tga_raw": "tga_raw"}, - {"tiff": "tiff"} + {"exr": "OpenEXR"}, + {"bmp": "BMP"}, + {"rgb": "Iris"}, + {"png": "PNG"}, + {"jpg": "JPEG"}, + {"jp2": "JPEG 2000"}, + {"tga": "Targa"}, + {"tif": "TIFF"} ] } ] From 7d7a41792e96ab3eb93a48e8c641f8bba3ec0f58 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 11 Aug 2023 14:52:15 +0100 Subject: [PATCH 005/186] Added more comments --- .../blender/plugins/publish/collect_render.py | 15 +++++++++++++++ .../plugins/publish/submit_blender_deadline.py | 11 +++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 11b98c76e6..7f060c3b7c 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -42,6 +42,17 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): @staticmethod def get_render_product(file_path, render_folder, file_name, instance, ext): + """ + Generate the path to the render product. Blender interprets the `#` + as the frame number, when it renders. + + Args: + file_path (str): The path to the blender scene. + render_folder (str): The render folder set in settings. + file_name (str): The name of the blender scene. + instance (pyblish.api.Instance): The instance to publish. + ext (str): The image format to render. + """ output_file = os.path.join( file_path, render_folder, file_name, instance.name) @@ -54,6 +65,10 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): def generate_expected_files( render_product, frame_start, frame_end, frame_step ): + """Generate the expected files for the render product. + This returns a list of files that should be rendered. It replaces + the sequence of `#` with the frame number. + """ path = os.path.dirname(render_product) file = os.path.basename(render_product) diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index b3deb39399..7aee087ddc 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -179,11 +179,10 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): return self.submit(payload) def from_published_scene(self): - """ Do not overwrite expected files. - - Use published is set to True, so rendering will be triggered - from published scene (in 'publish' folder). Default implementation - of abstract class renames expected (eg. rendered) files accordingly - which is not needed here. + """ + This is needed to set the correct path for the json metadata. Because + the rendering path is set in the blend file during the collection, + and the path is adjusted to use the published scene, this ensures that + the metadata and the rendered files are in the same location. """ return super().from_published_scene(False) From 94c801ce84fb1bd99ffe57fee9bb0ea127dc03c9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 11 Aug 2023 14:52:28 +0100 Subject: [PATCH 006/186] Removed redundant code --- .../deadline/plugins/publish/submit_blender_deadline.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 7aee087ddc..9bfc4fbb07 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -17,14 +17,6 @@ from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo -def _validate_deadline_bool_value(instance, attribute, value): - if not isinstance(value, (str, bool)): - raise TypeError(f"Attribute {attribute} must be str or bool.") - if value not in {"1", "0", True, False}: - raise ValueError( - f"Value of {attribute} must be one of '0', '1', True, False") - - @attr.s class BlenderPluginInfo(): SceneFile = attr.ib(default=None) # Input From 85b49ec761deb3187ded2ee4262f23efa227c40f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 22 Aug 2023 10:52:02 +0100 Subject: [PATCH 007/186] Basic implementation for AOVs rendering --- .../blender/plugins/publish/collect_render.py | 101 ++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 7f060c3b7c..fafdd9cc2d 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -12,6 +12,10 @@ from openpype.pipeline import ( from openpype.settings import ( get_project_settings, ) +from openpype.hosts.blender.api.ops import ( + MainThreadItem, + execute_in_main_thread +) import pyblish.api @@ -41,7 +45,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): ["image_format"]) @staticmethod - def get_render_product(file_path, render_folder, file_name, instance, ext): + def get_render_product(output_path, instance): """ Generate the path to the render product. Blender interprets the `#` as the frame number, when it renders. @@ -53,10 +57,9 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): instance (pyblish.api.Instance): The instance to publish. ext (str): The image format to render. """ - output_file = os.path.join( - file_path, render_folder, file_name, instance.name) + output_file = os.path.join(output_path, instance.name) - render_product = f"{output_file}.####.{ext}" + render_product = f"{output_file}.####" render_product = render_product.replace("\\", "/") return render_product @@ -83,9 +86,13 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): @staticmethod def set_render_format(ext): + # Set Blender to save the file with the right extension + bpy.context.scene.render.use_file_extension = True + image_settings = bpy.context.scene.render.image_settings if ext == "exr": + # TODO: Check if multilayer option is selected image_settings.file_format = "OPEN_EXR" elif ext == "bmp": image_settings.file_format = "BMP" @@ -102,6 +109,86 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): elif ext == "tif": image_settings.file_format = "TIFF" + def _set_node_tree(self, output_path, instance): + # Set the scene to use the compositor node tree to render + bpy.context.scene.use_nodes = True + + tree = bpy.context.scene.node_tree + + # Get the Render Layers node + rl_node = None + for node in tree.nodes: + if node.bl_idname == "CompositorNodeRLayers": + rl_node = node + break + + # If there's not a Render Layers node, we create it + if not rl_node: + rl_node = tree.nodes.new("CompositorNodeRLayers") + + # Get the enabled output sockets, that are the active passes for the + # render. + # We also exclude some layers. + exclude_sockets = ["Image", "Alpha"] + passes = [ + socket for socket in rl_node.outputs + if socket.enabled and socket.name not in exclude_sockets + ] + + # Remove all output nodes + for node in tree.nodes: + if node.bl_idname == "CompositorNodeOutputFile": + tree.nodes.remove(node) + + # Create a new output node + output = tree.nodes.new("CompositorNodeOutputFile") + + + context = bpy.context.copy() + # context = create_blender_context() + context["node"] = output + + win = bpy.context.window_manager.windows[0] + screen = win.screen + area = screen.areas[0] + region = area.regions[0] + + context["window"] = win + context['screen'] = screen + context['area'] = area + context['region'] = region + + self.log.debug(f"context: {context}") + + # Change area type to node editor, to execute node operators + old_area_type = area.ui_type + area.ui_type = "CompositorNodeTree" + + # Remove the default input socket from the output node + bpy.ops.node.output_file_remove_active_socket(context) + + output.base_path = output_path + image_settings = bpy.context.scene.render.image_settings + output.format.file_format = image_settings.file_format + + # For each active render pass, we add a new socket to the output node + # and link it + for render_pass in passes: + bpy.ops.node.output_file_add_socket( + context, file_path=f"{instance.name}_{render_pass.name}.####") + + node_input = output.inputs[-1] + + tree.links.new(render_pass, node_input) + + # Restore the area type + area.ui_type = old_area_type + + def set_node_tree(self, output_path, instance): + """ Run the creator on Blender main thread""" + mti = MainThreadItem(self._set_node_tree, output_path, instance) + execute_in_main_thread(mti) + @staticmethod def set_render_camera(instance): # There should be only one camera in the instance @@ -128,8 +215,10 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): render_folder = self.get_default_render_folder(settings) ext = self.get_image_format(settings) - render_product = self.get_render_product( - file_path, render_folder, file_name, instance, ext) + output_path = os.path.join(file_path, render_folder, file_name) + + render_product = self.get_render_product(output_path, instance) + self.set_node_tree(output_path, instance) # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product From eb1b5425de4aa546eaaae682f2acf9209dadf359 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 22 Aug 2023 11:54:49 +0100 Subject: [PATCH 008/186] Added support for multilayer EXR --- .../blender/plugins/publish/collect_render.py | 48 ++++++++++++------- .../defaults/project_settings/blender.json | 3 +- .../schema_project_blender.json | 35 ++++++++------ 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index fafdd9cc2d..cd3b922697 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -44,6 +44,14 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): ["RenderSettings"] ["image_format"]) + @staticmethod + def get_multilayer(settings): + """Get multilayer from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["multilayer_exr"]) + @staticmethod def get_render_product(output_path, instance): """ @@ -85,15 +93,15 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): return expected_files @staticmethod - def set_render_format(ext): + def set_render_format(ext, multilayer): # Set Blender to save the file with the right extension bpy.context.scene.render.use_file_extension = True image_settings = bpy.context.scene.render.image_settings if ext == "exr": - # TODO: Check if multilayer option is selected - image_settings.file_format = "OPEN_EXR" + image_settings.file_format = ( + "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR") elif ext == "bmp": image_settings.file_format = "BMP" elif ext == "rgb": @@ -109,6 +117,21 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): elif ext == "tif": image_settings.file_format = "TIFF" + def _create_context(): + context = bpy.context.copy() + + win = bpy.context.window_manager.windows[0] + screen = win.screen + area = screen.areas[0] + region = area.regions[0] + + context["window"] = win + context['screen'] = screen + context['area'] = area + context['region'] = region + + return context + def _set_node_tree(self, output_path, instance): # Set the scene to use the compositor node tree to render bpy.context.scene.use_nodes = True @@ -143,22 +166,10 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # Create a new output node output = tree.nodes.new("CompositorNodeOutputFile") - - context = bpy.context.copy() - # context = create_blender_context() + context = self._create_context() context["node"] = output - win = bpy.context.window_manager.windows[0] - screen = win.screen - area = screen.areas[0] - region = area.regions[0] - - context["window"] = win - context['screen'] = screen - context['area'] = area - context['region'] = region - - self.log.debug(f"context: {context}") + area = context["area"] # Change area type to node editor, to execute node operators old_area_type = area.ui_type @@ -214,6 +225,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): render_folder = self.get_default_render_folder(settings) ext = self.get_image_format(settings) + multilayer = self.get_multilayer(settings) output_path = os.path.join(file_path, render_folder, file_name) @@ -222,7 +234,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product - self.set_render_format(ext) + self.set_render_format(ext, multilayer) self.set_render_camera(instance) # We save the file to save the render settings diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 333a1fed56..c375e550c2 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -19,7 +19,8 @@ }, "RenderSettings": { "default_render_image_folder": "renders/blender", - "image_format": "exr" + "image_format": "exr", + "multilayer_exr": true }, "workfile_builder": { "create_first_version": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 84efec5c0a..d8ef1eee3e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -66,21 +66,26 @@ "label": "Default render image folder" }, { - "key": "image_format", - "label": "Output Image Format", - "type": "enum", - "multiselection": false, - "defaults": "exr", - "enum_items": [ - {"exr": "OpenEXR"}, - {"bmp": "BMP"}, - {"rgb": "Iris"}, - {"png": "PNG"}, - {"jpg": "JPEG"}, - {"jp2": "JPEG 2000"}, - {"tga": "Targa"}, - {"tif": "TIFF"} - ] + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"exr": "OpenEXR"}, + {"bmp": "BMP"}, + {"rgb": "Iris"}, + {"png": "PNG"}, + {"jpg": "JPEG"}, + {"jp2": "JPEG 2000"}, + {"tga": "Targa"}, + {"tif": "TIFF"} + ] + }, + { + "key": "multilayer_exr", + "type": "boolean", + "label": "Multilayer (EXR)" } ] }, From 9f56721334c8a7d42aa88c34ee4ef01befeb153d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 22 Aug 2023 15:56:23 +0100 Subject: [PATCH 009/186] Implemented AOVs rendering in deadline and publishing --- openpype/hosts/blender/api/colorspace.py | 52 +++++++++++ .../blender/plugins/publish/collect_render.py | 92 ++++++++++++++----- .../publish/submit_blender_deadline.py | 3 +- 3 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 openpype/hosts/blender/api/colorspace.py diff --git a/openpype/hosts/blender/api/colorspace.py b/openpype/hosts/blender/api/colorspace.py new file mode 100644 index 0000000000..59deb514f8 --- /dev/null +++ b/openpype/hosts/blender/api/colorspace.py @@ -0,0 +1,52 @@ +import attr + +import bpy + + +@attr.s +class LayerMetadata(object): + """Data class for Render Layer metadata.""" + frameStart = attr.ib() + frameEnd = attr.ib() + + +@attr.s +class RenderProduct(object): + """Getting Colorspace as + Specific Render Product Parameter for submitting + publish job. + """ + colorspace = attr.ib() # colorspace + view = attr.ib() + productName = attr.ib(default=None) + + +class ARenderProduct(object): + + def __init__(self): + """Constructor.""" + # Initialize + self.layer_data = self._get_layer_data() + self.layer_data.products = self.get_colorspace_data() + + def _get_layer_data(self): + scene = bpy.context.scene + + return LayerMetadata( + frameStart=int(scene.frame_start), + frameEnd=int(scene.frame_end), + ) + + def get_colorspace_data(self): + """To be implemented by renderer class. + This should return a list of RenderProducts. + Returns: + list: List of RenderProduct + """ + return [ + RenderProduct( + colorspace="sRGB", + view="ACES 1.0", + productName="" + ) + ] diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index cd3b922697..becb735c21 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -12,10 +12,7 @@ from openpype.pipeline import ( from openpype.settings import ( get_project_settings, ) -from openpype.hosts.blender.api.ops import ( - MainThreadItem, - execute_in_main_thread -) +from openpype.hosts.blender.api import colorspace import pyblish.api @@ -73,12 +70,13 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): return render_product @staticmethod - def generate_expected_files( - render_product, frame_start, frame_end, frame_step + def generate_expected_beauty( + render_product, frame_start, frame_end, frame_step, ext ): - """Generate the expected files for the render product. - This returns a list of files that should be rendered. It replaces - the sequence of `#` with the frame number. + """ + Generate the expected files for the render product for the beauty + render. This returns a list of files that should be rendered. It + replaces the sequence of `#` with the frame number. """ path = os.path.dirname(render_product) file = os.path.basename(render_product) @@ -87,9 +85,39 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): for frame in range(frame_start, frame_end + 1, frame_step): frame_str = str(frame).rjust(4, "0") - expected_file = os.path.join(path, re.sub("#+", frame_str, file)) + filename = re.sub("#+", frame_str, file) + expected_file = f"{os.path.join(path, filename)}.{ext}" expected_files.append(expected_file.replace("\\", "/")) + return { + "beauty": expected_files + } + + @staticmethod + def generate_expected_aovs( + aov_file_product, frame_start, frame_end, frame_step, ext + ): + """ + Generate the expected files for the render product for the beauty + render. This returns a list of files that should be rendered. It + replaces the sequence of `#` with the frame number. + """ + expected_files = {} + + for aov_name, aov_file in aov_file_product: + path = os.path.dirname(aov_file) + file = os.path.basename(aov_file) + + aov_files = [] + + for frame in range(frame_start, frame_end + 1, frame_step): + frame_str = str(frame).rjust(4, "0") + filename = re.sub("#+", frame_str, file) + expected_file = f"{os.path.join(path, filename)}.{ext}" + aov_files.append(expected_file.replace("\\", "/")) + + expected_files[aov_name] = aov_files + return expected_files @staticmethod @@ -117,7 +145,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): elif ext == "tif": image_settings.file_format = "TIFF" - def _create_context(): + def _create_context(self): context = bpy.context.copy() win = bpy.context.window_manager.windows[0] @@ -132,7 +160,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): return context - def _set_node_tree(self, output_path, instance): + def set_node_tree(self, output_path, instance): # Set the scene to use the compositor node tree to render bpy.context.scene.use_nodes = True @@ -153,8 +181,9 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # render. # We also exclude some layers. exclude_sockets = ["Image", "Alpha"] - passes = [ - socket for socket in rl_node.outputs + passes = [ + socket + for socket in rl_node.outputs if socket.enabled and socket.name not in exclude_sockets ] @@ -182,11 +211,16 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): image_settings = bpy.context.scene.render.image_settings output.format.file_format = image_settings.file_format + aov_file_products = [] + # For each active render pass, we add a new socket to the output node # and link it for render_pass in passes: - bpy.ops.node.output_file_add_socket( - context, file_path=f"{instance.name}_{render_pass.name}.####") + filepath = f"{instance.name}_{render_pass.name}.####" + bpy.ops.node.output_file_add_socket(context, file_path=filepath) + + aov_file_products.append( + (render_pass.name, os.path.join(output_path, filepath))) node_input = output.inputs[-1] @@ -195,10 +229,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # Restore the area type area.ui_type = old_area_type - def set_node_tree(self, output_path, instance): - """ Run the creator on Blender main thread""" - mti = MainThreadItem(self._set_node_tree, output_path, instance) - execute_in_main_thread(mti) + return aov_file_products @staticmethod def set_render_camera(instance): @@ -230,7 +261,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): output_path = os.path.join(file_path, render_folder, file_name) render_product = self.get_render_product(output_path, instance) - self.set_node_tree(output_path, instance) + aov_file_product = self.set_node_tree(output_path, instance) # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product @@ -245,9 +276,15 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): frame_handle_start = context.data["frameStartHandle"] frame_handle_end = context.data["frameEndHandle"] - expected_files = self.generate_expected_files( + expected_beauty = self.generate_expected_beauty( render_product, int(frame_start), int(frame_end), - int(bpy.context.scene.frame_step)) + int(bpy.context.scene.frame_step), ext) + + expected_aovs = self.generate_expected_aovs( + aov_file_product, int(frame_start), int(frame_end), + int(bpy.context.scene.frame_step), ext) + + expected_files = expected_beauty | expected_aovs instance.data.update({ "frameStart": frame_start, @@ -257,7 +294,14 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): "fps": context.data["fps"], "byFrameStep": bpy.context.scene.frame_step, "farm": True, - "expectedFiles": expected_files, + "expectedFiles": [expected_files], + # OCIO not currently implemented in Blender, but the following + # settings are required by the schema, so it is hardcoded. + # TODO: Implement OCIO in Blender + "colorspaceConfig": "", + "colorspaceDisplay": "sRGB", + "colorspaceView": "ACES 1.0 SDR-video", + "renderProducts": colorspace.ARenderProduct(), }) self.log.info(f"data: {instance.data}") diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 9bfc4fbb07..ad456c0d13 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -163,7 +163,8 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): if not expected_files: raise RuntimeError("No Render Elements found!") - output_dir = os.path.dirname(expected_files[0]) + first_file = next(iter_expected_files(expected_files)) + output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir instance.data["toBeRenderedOn"] = "deadline" From c9f5a9743257b8ce242660da473d69cb8dba8b52 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 22 Aug 2023 16:33:51 +0100 Subject: [PATCH 010/186] Added setting for aov separator --- .../blender/plugins/publish/collect_render.py | 26 ++++++++++++++++--- .../defaults/project_settings/blender.json | 1 + .../schema_project_blender.json | 12 +++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index becb735c21..b16354460a 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -33,6 +33,23 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): ["RenderSettings"] ["default_render_image_folder"]) + @staticmethod + def get_aov_separator(settings): + """Get aov separator from blender settings.""" + + aov_sep = (settings["blender"] + ["RenderSettings"] + ["aov_separator"]) + + if aov_sep == "dash": + return "-" + elif aov_sep == "underscore": + return "_" + elif aov_sep == "dot": + return "." + else: + raise ValueError(f"Invalid aov separator: {aov_sep}") + @staticmethod def get_image_format(settings): """Get image format from blender settings.""" @@ -160,7 +177,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): return context - def set_node_tree(self, output_path, instance): + def set_node_tree(self, output_path, instance, aov_sep): # Set the scene to use the compositor node tree to render bpy.context.scene.use_nodes = True @@ -181,7 +198,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # render. # We also exclude some layers. exclude_sockets = ["Image", "Alpha"] - passes = [ + passes = [ socket for socket in rl_node.outputs if socket.enabled and socket.name not in exclude_sockets @@ -216,7 +233,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # For each active render pass, we add a new socket to the output node # and link it for render_pass in passes: - filepath = f"{instance.name}_{render_pass.name}.####" + filepath = f"{instance.name}{aov_sep}{render_pass.name}.####" bpy.ops.node.output_file_add_socket(context, file_path=filepath) aov_file_products.append( @@ -255,13 +272,14 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): settings = get_project_settings(project) render_folder = self.get_default_render_folder(settings) + aov_sep = self.get_aov_separator(settings) ext = self.get_image_format(settings) multilayer = self.get_multilayer(settings) output_path = os.path.join(file_path, render_folder, file_name) render_product = self.get_render_product(output_path, instance) - aov_file_product = self.set_node_tree(output_path, instance) + aov_file_product = self.set_node_tree(output_path, instance, aov_sep) # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index c375e550c2..17387f4db6 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -19,6 +19,7 @@ }, "RenderSettings": { "default_render_image_folder": "renders/blender", + "aov_separator": "underscore", "image_format": "exr", "multilayer_exr": true }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index d8ef1eee3e..ecad74b621 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -65,6 +65,18 @@ "key": "default_render_image_folder", "label": "Default render image folder" }, + { + "key": "aov_separator", + "label": "AOV Separator Character", + "type": "enum", + "multiselection": false, + "defaults": "underscore", + "enum_items": [ + {"dash": "- (dash)"}, + {"underscore": "_ (underscore)"}, + {"dot": ". (dot)"} + ] + }, { "key": "image_format", "label": "Output Image Format", From 5f901f2a62d781d6f29dd145fe69bbc26da24651 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 22 Aug 2023 17:38:33 +0100 Subject: [PATCH 011/186] Added setting to set AOVs --- .../blender/plugins/publish/collect_render.py | 25 +++++++++++++++++++ .../defaults/project_settings/blender.json | 3 ++- .../schema_project_blender.json | 23 +++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index b16354460a..309d40d9fd 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -162,6 +162,29 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): elif ext == "tif": image_settings.file_format = "TIFF" + @staticmethod + def set_render_passes(settings): + aov_list = (settings["blender"] + ["RenderSettings"] + ["aov_list"]) + + scene = bpy.context.scene + vl = bpy.context.view_layer + + vl.use_pass_combined = "combined" in aov_list + vl.use_pass_z = "z" in aov_list + vl.use_pass_mist = "mist" in aov_list + vl.use_pass_normal = "normal" in aov_list + vl.use_pass_diffuse_direct = "diffuse_light" in aov_list + vl.use_pass_diffuse_color = "diffuse_color" in aov_list + vl.use_pass_glossy_direct = "specular_light" in aov_list + vl.use_pass_glossy_color = "specular_color" in aov_list + vl.eevee.use_pass_volume_direct = "volume_light" in aov_list + vl.use_pass_emit = "emission" in aov_list + vl.use_pass_environment = "environment" in aov_list + vl.use_pass_shadow = "shadow" in aov_list + vl.use_pass_ambient_occlusion = "ao" in aov_list + def _create_context(self): context = bpy.context.copy() @@ -276,6 +299,8 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): ext = self.get_image_format(settings) multilayer = self.get_multilayer(settings) + self.set_render_passes(settings) + output_path = os.path.join(file_path, render_folder, file_name) render_product = self.get_render_product(output_path, instance) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 17387f4db6..d36fc503dd 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -21,7 +21,8 @@ "default_render_image_folder": "renders/blender", "aov_separator": "underscore", "image_format": "exr", - "multilayer_exr": true + "multilayer_exr": true, + "aov_list": [] }, "workfile_builder": { "create_first_version": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index ecad74b621..b7d61d1d69 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -98,6 +98,29 @@ "key": "multilayer_exr", "type": "boolean", "label": "Multilayer (EXR)" + }, + { + "key": "aov_list", + "label": "AOVs to create", + "type": "enum", + "multiselection": true, + "defaults": "empty", + "enum_items": [ + {"empty": "< empty >"}, + {"combined": "Combined"}, + {"z": "Z"}, + {"mist": "Mist"}, + {"normal": "Normal"}, + {"diffuse_light": "Diffuse Light"}, + {"diffuse_color": "Diffuse Color"}, + {"specular_light": "Specular Light"}, + {"specular_color": "Specular Color"}, + {"volume_light": "Volume Light"}, + {"emission": "Emission"}, + {"environment": "Environment"}, + {"shadow": "Shadow"}, + {"ao": "Ambient Occlusion"} + ] } ] }, From fc1a98b47173d34843b0e2b59ca6e492763700d0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 09:32:32 +0100 Subject: [PATCH 012/186] Added support for custom render passes --- .../blender/plugins/publish/collect_render.py | 15 ++++++++++- .../defaults/project_settings/blender.json | 3 ++- .../schema_project_blender.json | 27 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 309d40d9fd..7c95bb14cf 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -168,7 +168,10 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): ["RenderSettings"] ["aov_list"]) - scene = bpy.context.scene + custom_passes = (settings["blender"] + ["RenderSettings"] + ["custom_passes"]) + vl = bpy.context.view_layer vl.use_pass_combined = "combined" in aov_list @@ -185,6 +188,16 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): vl.use_pass_shadow = "shadow" in aov_list vl.use_pass_ambient_occlusion = "ao" in aov_list + aovs_names = [aov.name for aov in vl.aovs] + for cp in custom_passes: + cp_name = cp[0] + if cp_name not in aovs_names: + aov = vl.aovs.add() + aov.name = cp_name + else: + aov = vl.aovs[cp_name] + aov.type = cp[1].get("type", "VALUE") + def _create_context(self): context = bpy.context.copy() diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index d36fc503dd..8b1d602df0 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -22,7 +22,8 @@ "aov_separator": "underscore", "image_format": "exr", "multilayer_exr": true, - "aov_list": [] + "aov_list": [], + "custom_passes": [] }, "workfile_builder": { "create_first_version": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index b7d61d1d69..8db57f49eb 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -121,6 +121,33 @@ {"shadow": "Shadow"}, {"ao": "Ambient Occlusion"} ] + }, + { + "type": "label", + "label": "Add custom AOVs. They are added to the view layer and in the Compositing Nodetree,\nbut they need to be added manually to the Shader Nodetree." + }, + { + "type": "dict-modifiable", + "store_as_list": true, + "key": "custom_passes", + "label": "Custom Passes", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "type", + "label": "Type", + "type": "enum", + "multiselection": false, + "defaults": "color", + "enum_items": [ + {"COLOR": "Color"}, + {"VALUE": "Value"} + ] + } + ] + } } ] }, From 8f78ebeabdb1e6e7fdf0ab461d8b609669266c29 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 10:51:01 +0100 Subject: [PATCH 013/186] Fixed problem with blender context and multilayer exr --- .../blender/plugins/publish/collect_render.py | 47 +++++-------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 7c95bb14cf..2622c51432 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -198,22 +198,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): aov = vl.aovs[cp_name] aov.type = cp[1].get("type", "VALUE") - def _create_context(self): - context = bpy.context.copy() - - win = bpy.context.window_manager.windows[0] - screen = win.screen - area = screen.areas[0] - region = area.regions[0] - - context["window"] = win - context['screen'] = screen - context['area'] = area - context['region'] = region - - return context - - def set_node_tree(self, output_path, instance, aov_sep): + def set_node_tree(self, output_path, instance, aov_sep, ext, multilayer): # Set the scene to use the compositor node tree to render bpy.context.scene.use_nodes = True @@ -248,17 +233,10 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # Create a new output node output = tree.nodes.new("CompositorNodeOutputFile") - context = self._create_context() - context["node"] = output - - area = context["area"] - - # Change area type to node editor, to execute node operators - old_area_type = area.ui_type - area.ui_type = "CompositorNodeTree" - - # Remove the default input socket from the output node - bpy.ops.node.output_file_remove_active_socket(context) + if ext == "exr" and multilayer: + output.layer_slots.clear() + else: + output.file_slots.clear() output.base_path = output_path image_settings = bpy.context.scene.render.image_settings @@ -270,18 +248,18 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # and link it for render_pass in passes: filepath = f"{instance.name}{aov_sep}{render_pass.name}.####" - bpy.ops.node.output_file_add_socket(context, file_path=filepath) + if ext == "exr" and multilayer: + output.layer_slots.new(render_pass.name) + else: + output.file_slots.new(filepath) - aov_file_products.append( - (render_pass.name, os.path.join(output_path, filepath))) + aov_file_products.append( + (render_pass.name, os.path.join(output_path, filepath))) node_input = output.inputs[-1] tree.links.new(render_pass, node_input) - # Restore the area type - area.ui_type = old_area_type - return aov_file_products @staticmethod @@ -317,7 +295,8 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): output_path = os.path.join(file_path, render_folder, file_name) render_product = self.get_render_product(output_path, instance) - aov_file_product = self.set_node_tree(output_path, instance, aov_sep) + aov_file_product = self.set_node_tree( + output_path, instance, aov_sep, ext, multilayer) # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product From c529abb8ab383ff50e92b8b0284ff2efa2fbff63 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 15:39:31 +0100 Subject: [PATCH 014/186] Moved most of the logic in the creator --- .../blender/plugins/create/create_render.py | 267 +++++++++++++++++- .../blender/plugins/publish/collect_render.py | 235 +-------------- 2 files changed, 266 insertions(+), 236 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 8323b88cfe..49d356ab67 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -1,9 +1,18 @@ """Create render.""" +import os +import re import bpy +from openpype.pipeline import ( + get_current_context, + get_current_project_name, +) +from openpype.settings import ( + get_project_settings, +) from openpype.pipeline import get_current_task_name -from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api import plugin, lib from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -15,14 +24,217 @@ class CreateRenderlayer(plugin.Creator): family = "renderlayer" icon = "eye" - render_settings = {} + @staticmethod + def get_default_render_folder(settings): + """Get default render folder from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["default_render_image_folder"]) + + @staticmethod + def get_aov_separator(settings): + """Get aov separator from blender settings.""" + + aov_sep = (settings["blender"] + ["RenderSettings"] + ["aov_separator"]) + + if aov_sep == "dash": + return "-" + elif aov_sep == "underscore": + return "_" + elif aov_sep == "dot": + return "." + else: + raise ValueError(f"Invalid aov separator: {aov_sep}") + + @staticmethod + def get_image_format(settings): + """Get image format from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["image_format"]) + + @staticmethod + def get_multilayer(settings): + """Get multilayer from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["multilayer_exr"]) + + @staticmethod + def get_render_product(output_path, name): + """ + Generate the path to the render product. Blender interprets the `#` + as the frame number, when it renders. + + Args: + file_path (str): The path to the blender scene. + render_folder (str): The render folder set in settings. + file_name (str): The name of the blender scene. + instance (pyblish.api.Instance): The instance to publish. + ext (str): The image format to render. + """ + output_file = os.path.join(output_path, name) + + render_product = f"{output_file}.####" + render_product = render_product.replace("\\", "/") + + return render_product + + @staticmethod + def set_render_format(ext, multilayer): + # Set Blender to save the file with the right extension + bpy.context.scene.render.use_file_extension = True + + image_settings = bpy.context.scene.render.image_settings + + if ext == "exr": + image_settings.file_format = ( + "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR") + elif ext == "bmp": + image_settings.file_format = "BMP" + elif ext == "rgb": + image_settings.file_format = "IRIS" + elif ext == "png": + image_settings.file_format = "PNG" + elif ext == "jpeg": + image_settings.file_format = "JPEG" + elif ext == "jp2": + image_settings.file_format = "JPEG2000" + elif ext == "tga": + image_settings.file_format = "TARGA" + elif ext == "tif": + image_settings.file_format = "TIFF" + + @staticmethod + def set_render_passes(settings): + aov_list = (settings["blender"] + ["RenderSettings"] + ["aov_list"]) + + custom_passes = (settings["blender"] + ["RenderSettings"] + ["custom_passes"]) + + vl = bpy.context.view_layer + + vl.use_pass_combined = "combined" in aov_list + vl.use_pass_z = "z" in aov_list + vl.use_pass_mist = "mist" in aov_list + vl.use_pass_normal = "normal" in aov_list + vl.use_pass_diffuse_direct = "diffuse_light" in aov_list + vl.use_pass_diffuse_color = "diffuse_color" in aov_list + vl.use_pass_glossy_direct = "specular_light" in aov_list + vl.use_pass_glossy_color = "specular_color" in aov_list + vl.eevee.use_pass_volume_direct = "volume_light" in aov_list + vl.use_pass_emit = "emission" in aov_list + vl.use_pass_environment = "environment" in aov_list + vl.use_pass_shadow = "shadow" in aov_list + vl.use_pass_ambient_occlusion = "ao" in aov_list + + aovs_names = [aov.name for aov in vl.aovs] + for cp in custom_passes: + cp_name = cp[0] + if cp_name not in aovs_names: + aov = vl.aovs.add() + aov.name = cp_name + else: + aov = vl.aovs[cp_name] + aov.type = cp[1].get("type", "VALUE") + + return aov_list, custom_passes + + def set_node_tree(self, output_path, name, aov_sep, ext, multilayer): + # Set the scene to use the compositor node tree to render + bpy.context.scene.use_nodes = True + + tree = bpy.context.scene.node_tree + + # Get the Render Layers node + rl_node = None + for node in tree.nodes: + if node.bl_idname == "CompositorNodeRLayers": + rl_node = node + break + + # If there's not a Render Layers node, we create it + if not rl_node: + rl_node = tree.nodes.new("CompositorNodeRLayers") + + # Get the enabled output sockets, that are the active passes for the + # render. + # We also exclude some layers. + exclude_sockets = ["Image", "Alpha"] + passes = [ + socket + for socket in rl_node.outputs + if socket.enabled and socket.name not in exclude_sockets + ] + + # Remove all output nodes + for node in tree.nodes: + if node.bl_idname == "CompositorNodeOutputFile": + tree.nodes.remove(node) + + # Create a new output node + output = tree.nodes.new("CompositorNodeOutputFile") + + if ext == "exr" and multilayer: + output.layer_slots.clear() + else: + output.file_slots.clear() + + output.base_path = output_path + image_settings = bpy.context.scene.render.image_settings + output.format.file_format = image_settings.file_format + + aov_file_products = [] + + # For each active render pass, we add a new socket to the output node + # and link it + for render_pass in passes: + filepath = f"{name}{aov_sep}{render_pass.name}.####" + if ext == "exr" and multilayer: + output.layer_slots.new(render_pass.name) + else: + output.file_slots.new(filepath) + + aov_file_products.append( + (render_pass.name, os.path.join(output_path, filepath))) + + node_input = output.inputs[-1] + + tree.links.new(render_pass, node_input) + + return aov_file_products + + @staticmethod + def set_render_camera(asset_group): + # There should be only one camera in the instance + found = False + for obj in asset_group.all_objects: + if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA": + bpy.context.scene.camera = obj + found = True + break + + assert found, "No camera found in the render instance" + + @staticmethod + def imprint_render_settings(node, data): + RENDER_DATA = "render_data" + if not node.get(RENDER_DATA): + node[RENDER_DATA] = {} + for key, value in data.items(): + if value is None: + continue + node[RENDER_DATA][key] = value def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) - ops.execute_in_main_thread(mti) - - def _process(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: @@ -46,4 +258,45 @@ class CreateRenderlayer(plugin.Creator): obj = (self.options or {}).get("asset_group") asset_group.objects.link(obj) + filepath = bpy.data.filepath + assert filepath, "Workfile not saved. Please save the file first." + + file_path = os.path.dirname(filepath) + file_name = os.path.basename(filepath) + file_name, _ = os.path.splitext(file_name) + + project = get_current_project_name() + settings = get_project_settings(project) + + render_folder = self.get_default_render_folder(settings) + aov_sep = self.get_aov_separator(settings) + ext = self.get_image_format(settings) + multilayer = self.get_multilayer(settings) + + aov_list, custom_passes = self.set_render_passes(settings) + + output_path = os.path.join(file_path, render_folder, file_name) + + render_product = self.get_render_product(output_path, name) + aov_file_product = self.set_node_tree( + output_path, name, aov_sep, ext, multilayer) + + # We set the render path, the format and the camera + bpy.context.scene.render.filepath = render_product + self.set_render_format(ext, multilayer) + self.set_render_camera(asset_group) + + render_settings = { + "render_folder": render_folder, + "aov_separator": aov_sep, + "image_format": ext, + "multilayer_exr": multilayer, + "aov_list": aov_list, + "custom_passes": custom_passes, + "render_product": render_product, + "aov_file_product": aov_file_product, + } + + self.imprint_render_settings(asset_group, render_settings) + return asset_group diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 2622c51432..557a4c9066 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -6,12 +6,6 @@ import re import bpy -from openpype.pipeline import ( - get_current_project_name, -) -from openpype.settings import ( - get_project_settings, -) from openpype.hosts.blender.api import colorspace import pyblish.api @@ -25,67 +19,6 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): label = "Collect Render Layers" sync_workfile_version = False - @staticmethod - def get_default_render_folder(settings): - """Get default render folder from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["default_render_image_folder"]) - - @staticmethod - def get_aov_separator(settings): - """Get aov separator from blender settings.""" - - aov_sep = (settings["blender"] - ["RenderSettings"] - ["aov_separator"]) - - if aov_sep == "dash": - return "-" - elif aov_sep == "underscore": - return "_" - elif aov_sep == "dot": - return "." - else: - raise ValueError(f"Invalid aov separator: {aov_sep}") - - @staticmethod - def get_image_format(settings): - """Get image format from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["image_format"]) - - @staticmethod - def get_multilayer(settings): - """Get multilayer from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["multilayer_exr"]) - - @staticmethod - def get_render_product(output_path, instance): - """ - Generate the path to the render product. Blender interprets the `#` - as the frame number, when it renders. - - Args: - file_path (str): The path to the blender scene. - render_folder (str): The render folder set in settings. - file_name (str): The name of the blender scene. - instance (pyblish.api.Instance): The instance to publish. - ext (str): The image format to render. - """ - output_file = os.path.join(output_path, instance.name) - - render_product = f"{output_file}.####" - render_product = render_product.replace("\\", "/") - - return render_product - @staticmethod def generate_expected_beauty( render_product, frame_start, frame_end, frame_step, ext @@ -137,174 +70,18 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): return expected_files - @staticmethod - def set_render_format(ext, multilayer): - # Set Blender to save the file with the right extension - bpy.context.scene.render.use_file_extension = True - - image_settings = bpy.context.scene.render.image_settings - - if ext == "exr": - image_settings.file_format = ( - "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR") - elif ext == "bmp": - image_settings.file_format = "BMP" - elif ext == "rgb": - image_settings.file_format = "IRIS" - elif ext == "png": - image_settings.file_format = "PNG" - elif ext == "jpeg": - image_settings.file_format = "JPEG" - elif ext == "jp2": - image_settings.file_format = "JPEG2000" - elif ext == "tga": - image_settings.file_format = "TARGA" - elif ext == "tif": - image_settings.file_format = "TIFF" - - @staticmethod - def set_render_passes(settings): - aov_list = (settings["blender"] - ["RenderSettings"] - ["aov_list"]) - - custom_passes = (settings["blender"] - ["RenderSettings"] - ["custom_passes"]) - - vl = bpy.context.view_layer - - vl.use_pass_combined = "combined" in aov_list - vl.use_pass_z = "z" in aov_list - vl.use_pass_mist = "mist" in aov_list - vl.use_pass_normal = "normal" in aov_list - vl.use_pass_diffuse_direct = "diffuse_light" in aov_list - vl.use_pass_diffuse_color = "diffuse_color" in aov_list - vl.use_pass_glossy_direct = "specular_light" in aov_list - vl.use_pass_glossy_color = "specular_color" in aov_list - vl.eevee.use_pass_volume_direct = "volume_light" in aov_list - vl.use_pass_emit = "emission" in aov_list - vl.use_pass_environment = "environment" in aov_list - vl.use_pass_shadow = "shadow" in aov_list - vl.use_pass_ambient_occlusion = "ao" in aov_list - - aovs_names = [aov.name for aov in vl.aovs] - for cp in custom_passes: - cp_name = cp[0] - if cp_name not in aovs_names: - aov = vl.aovs.add() - aov.name = cp_name - else: - aov = vl.aovs[cp_name] - aov.type = cp[1].get("type", "VALUE") - - def set_node_tree(self, output_path, instance, aov_sep, ext, multilayer): - # Set the scene to use the compositor node tree to render - bpy.context.scene.use_nodes = True - - tree = bpy.context.scene.node_tree - - # Get the Render Layers node - rl_node = None - for node in tree.nodes: - if node.bl_idname == "CompositorNodeRLayers": - rl_node = node - break - - # If there's not a Render Layers node, we create it - if not rl_node: - rl_node = tree.nodes.new("CompositorNodeRLayers") - - # Get the enabled output sockets, that are the active passes for the - # render. - # We also exclude some layers. - exclude_sockets = ["Image", "Alpha"] - passes = [ - socket - for socket in rl_node.outputs - if socket.enabled and socket.name not in exclude_sockets - ] - - # Remove all output nodes - for node in tree.nodes: - if node.bl_idname == "CompositorNodeOutputFile": - tree.nodes.remove(node) - - # Create a new output node - output = tree.nodes.new("CompositorNodeOutputFile") - - if ext == "exr" and multilayer: - output.layer_slots.clear() - else: - output.file_slots.clear() - - output.base_path = output_path - image_settings = bpy.context.scene.render.image_settings - output.format.file_format = image_settings.file_format - - aov_file_products = [] - - # For each active render pass, we add a new socket to the output node - # and link it - for render_pass in passes: - filepath = f"{instance.name}{aov_sep}{render_pass.name}.####" - if ext == "exr" and multilayer: - output.layer_slots.new(render_pass.name) - else: - output.file_slots.new(filepath) - - aov_file_products.append( - (render_pass.name, os.path.join(output_path, filepath))) - - node_input = output.inputs[-1] - - tree.links.new(render_pass, node_input) - - return aov_file_products - - @staticmethod - def set_render_camera(instance): - # There should be only one camera in the instance - found = False - for obj in instance: - if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA": - bpy.context.scene.camera = obj - found = True - break - - assert found, "No camera found in the render instance" - def process(self, instance): context = instance.context - filepath = context.data["currentFile"].replace("\\", "/") - file_path = os.path.dirname(filepath) - file_name = os.path.basename(filepath) - file_name, _ = os.path.splitext(file_name) + render_data = bpy.data.collections[str(instance)].get("render_data") - project = get_current_project_name() - settings = get_project_settings(project) + assert render_data, "No render data found." - render_folder = self.get_default_render_folder(settings) - aov_sep = self.get_aov_separator(settings) - ext = self.get_image_format(settings) - multilayer = self.get_multilayer(settings) + self.log.info(f"render_data: {dict(render_data)}") - self.set_render_passes(settings) - - output_path = os.path.join(file_path, render_folder, file_name) - - render_product = self.get_render_product(output_path, instance) - aov_file_product = self.set_node_tree( - output_path, instance, aov_sep, ext, multilayer) - - # We set the render path, the format and the camera - bpy.context.scene.render.filepath = render_product - self.set_render_format(ext, multilayer) - self.set_render_camera(instance) - - # We save the file to save the render settings - bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) + render_product = render_data.get("render_product") + aov_file_product = render_data.get("aov_file_product") + ext = render_data.get("image_format") frame_start = context.data["frameStart"] frame_end = context.data["frameEnd"] From 259255e7df5bfa6511c859d064aaea0d420b2f88 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 15:40:28 +0100 Subject: [PATCH 015/186] Hound fixes --- openpype/hosts/blender/plugins/create/create_render.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 49d356ab67..53a84ab0b8 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -1,11 +1,9 @@ """Create render.""" import os -import re import bpy from openpype.pipeline import ( - get_current_context, get_current_project_name, ) from openpype.settings import ( From 089909192026a82fde87ebe80e54871c712e32af Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 16:33:02 +0100 Subject: [PATCH 016/186] Fix EXR multilayer output --- openpype/hosts/blender/plugins/create/create_render.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 53a84ab0b8..468e4024e9 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -183,10 +183,12 @@ class CreateRenderlayer(plugin.Creator): if ext == "exr" and multilayer: output.layer_slots.clear() + filepath = f"{name}{aov_sep}AOVs.####" + output.base_path = os.path.join(output_path, filepath) else: output.file_slots.clear() + output.base_path = output_path - output.base_path = output_path image_settings = bpy.context.scene.render.image_settings output.format.file_format = image_settings.file_format @@ -195,10 +197,11 @@ class CreateRenderlayer(plugin.Creator): # For each active render pass, we add a new socket to the output node # and link it for render_pass in passes: - filepath = f"{name}{aov_sep}{render_pass.name}.####" if ext == "exr" and multilayer: output.layer_slots.new(render_pass.name) else: + filepath = f"{name}{aov_sep}{render_pass.name}.####" + output.file_slots.new(filepath) aov_file_products.append( From 028d15fc1efadd93fc963eae52177efa12d66d49 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 16:55:31 +0100 Subject: [PATCH 017/186] Added validator to check if file is saved --- .../plugins/publish/validate_file_saved.py | 20 +++++++++++ .../defaults/project_settings/blender.json | 6 ++++ .../schemas/schema_blender_publish.json | 33 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 openpype/hosts/blender/plugins/publish/validate_file_saved.py diff --git a/openpype/hosts/blender/plugins/publish/validate_file_saved.py b/openpype/hosts/blender/plugins/publish/validate_file_saved.py new file mode 100644 index 0000000000..e191585c55 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/validate_file_saved.py @@ -0,0 +1,20 @@ +import bpy + +import pyblish.api + + +class ValidateFileSaved(pyblish.api.InstancePlugin): + """Validate that the workfile has been saved.""" + + order = pyblish.api.ValidatorOrder - 0.01 + hosts = ["blender"] + label = "Validate File Saved" + optional = False + exclude_families = [] + + def process(self, instance): + if [ef for ef in self.exclude_families + if instance.data["family"] in ef]: + return + if bpy.data.is_dirty: + raise RuntimeError("Workfile is not saved.") diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 8b1d602df0..09ed800ac8 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -35,6 +35,12 @@ "optional": true, "active": true }, + "ValidateFileSaved": { + "enabled": true, + "optional": false, + "active": true, + "exclude_families": [] + }, "ValidateMeshHasUvs": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index 2f0bf0a831..0b694e5b70 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -18,6 +18,39 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateFileSaved", + "label": "Validate File Saved", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "splitter" + }, + { + "key": "exclude_families", + "label": "Exclude Families", + "type": "list", + "object_type": "text" + } + ] + }, { "type": "collapsible-wrap", "label": "Model", From b6e9806258086f260c604ee1ad5a932d93e85b70 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Aug 2023 22:39:13 +0200 Subject: [PATCH 018/186] Allow duplicating publish instance by defining `instance_node` and `instance_id` from `node.path()` instead of parms. --- openpype/hosts/houdini/api/plugin.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 70c837205e..c3fd313a0b 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -187,13 +187,14 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): self.customize_node_look(instance_node) instance_data["instance_node"] = instance_node.path() + instance_data["instance_id"] = instance_node.path() instance = CreatedInstance( self.family, subset_name, instance_data, self) self._add_instance_to_context(instance) - imprint(instance_node, instance.data_to_store()) + self.imprint(instance_node, instance.data_to_store()) return instance except hou.Error as er: @@ -222,25 +223,41 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data[ "houdini_cached_subsets"].get(self.identifier, []): + + node_data = read(instance) + + # Node paths are always the full node path since that is unique + # Because it's the node's path it's not written into attributes + # but explicitly collected + node_path = instance.path() + node_data["instance_id"] = node_path + node_data["instance_node"] = node_path + created_instance = CreatedInstance.from_existing( - read(instance), self + node_data, self ) self._add_instance_to_context(created_instance) def update_instances(self, update_list): for created_inst, changes in update_list: instance_node = hou.node(created_inst.get("instance_node")) - new_values = { key: changes[key].new_value for key in changes.changed_keys } - imprint( + self.imprint( instance_node, new_values, update=True ) + def imprint(self, node, values, update=False): + # Never store instance node and instance id since that data comes + # from the node's path + values.pop("instance_node", None) + values.pop("instance_id", None) + imprint(node, values, update=update) + def remove_instances(self, instances): """Remove specified instance from the scene. From 5ff66afff7b91b1d1583be28c0da99cc088a3300 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Aug 2023 22:40:25 +0200 Subject: [PATCH 019/186] Allow duplicating publish instances in Maya by not storin instance id as an attribute but using the (unique) node's name instead. --- openpype/hosts/maya/api/plugin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 00d6602ef9..00a2c899a2 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -134,6 +134,7 @@ class MayaCreatorBase(object): # We never store the instance_node as value on the node since # it's the node name itself data.pop("instance_node", None) + data.pop("instance_id", None) # We store creator attributes at the root level and assume they # will not clash in names with `subset`, `task`, etc. and other @@ -185,6 +186,7 @@ class MayaCreatorBase(object): # Explicitly re-parse the node name node_data["instance_node"] = node + node_data["instance_id"] = node return node_data From 6da94d4f27be41a974b8f6348c45060510f74032 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 23 Aug 2023 22:41:25 +0200 Subject: [PATCH 020/186] Allow to duplicate publish instances in Fusion, by not relying on `instance_id` data but have the unique identifier be the node's name. --- openpype/hosts/fusion/plugins/create/create_saver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/fusion/plugins/create/create_saver.py b/openpype/hosts/fusion/plugins/create/create_saver.py index 04898d0a45..590a678a3d 100644 --- a/openpype/hosts/fusion/plugins/create/create_saver.py +++ b/openpype/hosts/fusion/plugins/create/create_saver.py @@ -127,6 +127,9 @@ class CreateSaver(NewCreator): 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 @@ -192,6 +195,10 @@ class CreateSaver(NewCreator): 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_pre_create_attr_defs(self): From a0c25edab9e3f209486a53b8caa99b658366f9c7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 24 Aug 2023 11:06:37 +0100 Subject: [PATCH 021/186] Add check to remove instance if any error is triggered during creation --- .../blender/plugins/create/create_render.py | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 468e4024e9..63093b539c 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -235,30 +235,7 @@ class CreateRenderlayer(plugin.Creator): continue node[RENDER_DATA][key] = value - def process(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.collections.new(name=name) - instances.children.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - if (self.options or {}).get("useSelection"): - selected = lib.get_selection() - for obj in selected: - asset_group.objects.link(obj) - elif (self.options or {}).get("asset_group"): - obj = (self.options or {}).get("asset_group") - asset_group.objects.link(obj) - + def prepare_rendering(self, asset_group, name): filepath = bpy.data.filepath assert filepath, "Workfile not saved. Please save the file first." @@ -300,4 +277,28 @@ class CreateRenderlayer(plugin.Creator): self.imprint_render_settings(asset_group, render_settings) + def process(self): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + asset = self.data["asset"] + subset = self.data["subset"] + name = plugin.asset_name(asset, subset) + asset_group = bpy.data.collections.new(name=name) + + try: + instances.children.link(asset_group) + self.data['task'] = get_current_task_name() + lib.imprint(asset_group, self.data) + + self.prepare_rendering(asset_group, name) + except Exception: + # Remove the instance if there was an error + bpy.data.collections.remove(asset_group) + raise + return asset_group From 153a2999011f27901dd05a8953cd91ea82b649c1 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 24 Aug 2023 11:37:52 +0100 Subject: [PATCH 022/186] Removed camera from render instance and added validator to check camera --- .../blender/plugins/create/create_render.py | 13 ---------- .../publish/validate_render_camera_is_set.py | 17 +++++++++++++ .../defaults/project_settings/blender.json | 5 ++++ .../schemas/schema_blender_publish.json | 24 +++++++++++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 63093b539c..2952baafd3 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -213,18 +213,6 @@ class CreateRenderlayer(plugin.Creator): return aov_file_products - @staticmethod - def set_render_camera(asset_group): - # There should be only one camera in the instance - found = False - for obj in asset_group.all_objects: - if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA": - bpy.context.scene.camera = obj - found = True - break - - assert found, "No camera found in the render instance" - @staticmethod def imprint_render_settings(node, data): RENDER_DATA = "render_data" @@ -262,7 +250,6 @@ class CreateRenderlayer(plugin.Creator): # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product self.set_render_format(ext, multilayer) - self.set_render_camera(asset_group) render_settings = { "render_folder": render_folder, diff --git a/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py new file mode 100644 index 0000000000..5a06c1ff0a --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py @@ -0,0 +1,17 @@ +import bpy + +import pyblish.api + + +class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin): + """Validate that there is a camera set as active for rendering.""" + + order = pyblish.api.ValidatorOrder + hosts = ["blender"] + families = ["renderlayer"] + label = "Validate Render Camera Is Set" + optional = False + + def process(self, instance): + if not bpy.context.scene.camera: + raise RuntimeError("No camera is active for rendering.") diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 09ed800ac8..9cbbb49593 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -41,6 +41,11 @@ "active": true, "exclude_families": [] }, + "ValidateRenderCameraIsSet": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateMeshHasUvs": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index 0b694e5b70..05e7f13e70 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -51,6 +51,30 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateRenderCameraIsSet", + "label": "Validate Render Camera Is Set", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + }, { "type": "collapsible-wrap", "label": "Model", From 0f1bf31f69bfb0632eba5f8994e84c6f72d52126 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 24 Aug 2023 15:33:16 +0100 Subject: [PATCH 023/186] Updated settings for Ayon --- server_addon/blender/server/settings/main.py | 7 ++ .../server/settings/publish_plugins.py | 31 +++++ .../server/settings/render_settings.py | 106 ++++++++++++++++++ server_addon/blender/server/version.py | 2 +- .../server/settings/publish_plugins.py | 25 ++++- 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 server_addon/blender/server/settings/render_settings.py diff --git a/server_addon/blender/server/settings/main.py b/server_addon/blender/server/settings/main.py index f6118d39cd..4476ea709b 100644 --- a/server_addon/blender/server/settings/main.py +++ b/server_addon/blender/server/settings/main.py @@ -9,6 +9,10 @@ from .publish_plugins import ( PublishPuginsModel, DEFAULT_BLENDER_PUBLISH_SETTINGS ) +from .render_settings import ( + RenderSettingsModel, + DEFAULT_RENDER_SETTINGS +) class UnitScaleSettingsModel(BaseSettingsModel): @@ -37,6 +41,8 @@ class BlenderSettings(BaseSettingsModel): default_factory=BlenderImageIOModel, title="Color Management (ImageIO)" ) + render_settings: RenderSettingsModel = Field( + default_factory=RenderSettingsModel, title="Render Settings") workfile_builder: TemplateWorkfileBaseOptions = Field( default_factory=TemplateWorkfileBaseOptions, title="Workfile Builder" @@ -55,6 +61,7 @@ DEFAULT_VALUES = { }, "set_frames_startup": True, "set_resolution_startup": True, + "render_settings": DEFAULT_RENDER_SETTINGS, "publish": DEFAULT_BLENDER_PUBLISH_SETTINGS, "workfile_builder": { "create_first_version": False, diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 65dda78411..575bfe9f39 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -26,6 +26,16 @@ class ValidatePluginModel(BaseSettingsModel): active: bool = Field(title="Active") +class ValidateFileSavedModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateFileSaved") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + exclude_families: list[str] = Field( + default_factory=list, + title="Exclude product types" + ) + + class ExtractBlendModel(BaseSettingsModel): enabled: bool = Field(True) optional: bool = Field(title="Optional") @@ -53,6 +63,16 @@ class PublishPuginsModel(BaseSettingsModel): title="Validate Camera Zero Keyframe", section="Validators" ) + ValidateFileSaved: ValidateFileSavedModel = Field( + default_factory=ValidateFileSavedModel, + title="Validate File Saved", + section="Validators" + ) + ValidateRenderCameraIsSet: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Render Camera Is Set", + section="Validators" + ) ValidateMeshHasUvs: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Validate Mesh Has Uvs" @@ -118,6 +138,17 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": True, "active": True }, + "ValidateFileSaved": { + "enabled": True, + "optional": False, + "active": True, + "exclude_families": [] + }, + "ValidateRenderCameraIsSet": { + "enabled": True, + "optional": False, + "active": True + }, "ValidateMeshHasUvs": { "enabled": True, "optional": True, diff --git a/server_addon/blender/server/settings/render_settings.py b/server_addon/blender/server/settings/render_settings.py new file mode 100644 index 0000000000..bef16328d6 --- /dev/null +++ b/server_addon/blender/server/settings/render_settings.py @@ -0,0 +1,106 @@ +"""Providing models and values for Blender Render Settings.""" +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +def aov_separators_enum(): + return [ + {"value": "dash", "label": "- (dash)"}, + {"value": "underscore", "label": "_ (underscore)"}, + {"value": "dot", "label": ". (dot)"} + ] + + +def image_format_enum(): + return [ + {"value": "exr", "label": "OpenEXR"}, + {"value": "bmp", "label": "BMP"}, + {"value": "rgb", "label": "Iris"}, + {"value": "png", "label": "PNG"}, + {"value": "jpg", "label": "JPEG"}, + {"value": "jp2", "label": "JPEG 2000"}, + {"value": "tga", "label": "Targa"}, + {"value": "tif", "label": "TIFF"}, + ] + + +def aov_list_enum(): + return [ + {"value": "empty", "label": "< none >"}, + {"value": "combined", "label": "Combined"}, + {"value": "z", "label": "Z"}, + {"value": "mist", "label": "Mist"}, + {"value": "normal", "label": "Normal"}, + {"value": "diffuse_light", "label": "Diffuse Light"}, + {"value": "diffuse_color", "label": "Diffuse Color"}, + {"value": "specular_light", "label": "Specular Light"}, + {"value": "specular_color", "label": "Specular Color"}, + {"value": "volume_light", "label": "Volume Light"}, + {"value": "emission", "label": "Emission"}, + {"value": "environment", "label": "Environment"}, + {"value": "shadow", "label": "Shadow"}, + {"value": "ao", "label": "Ambient Occlusion"} + ] + + +def custom_passes_types_enum(): + return [ + {"value": "COLOR", "label": "Color"}, + {"value": "VALUE", "label": "Value"}, + ] + + +class CustomPassesModel(BaseSettingsModel): + """Custom Passes""" + _layout = "compact" + + attribute: str = Field("", title="Attribute name") + value: str = Field( + "Color", + title="Type", + enum_resolver=custom_passes_types_enum + ) + + +class RenderSettingsModel(BaseSettingsModel): + default_render_image_folder: str = Field( + title="Default Render Image Folder" + ) + aov_separator: str = Field( + "underscore", + title="AOV Separator Character", + enum_resolver=aov_separators_enum + ) + image_format: str = Field( + "exr", + title="Image Format", + enum_resolver=image_format_enum + ) + multilayer_exr: bool = Field( + title="Multilayer (EXR)" + ) + aov_list: list[str] = Field( + default_factory=list, + enum_resolver=aov_list_enum, + title="AOVs to create" + ) + custom_passes: list[CustomPassesModel] = Field( + default_factory=list, + title="Custom Passes", + description=( + "Add custom AOVs. They are added to the view layer and in the " + "Compositing Nodetree,\nbut they need to be added manually to " + "the Shader Nodetree." + ) + ) + + +DEFAULT_RENDER_SETTINGS = { + "default_render_image_folder": "renders/blender", + "aov_separator": "underscore", + "image_format": "exr", + "multilayer_exr": True, + "aov_list": [], + "custom_passes": [] +} diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index 8d1b667345..a29caa7ba1 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -208,6 +208,16 @@ class CelactionSubmitDeadlineModel(BaseSettingsModel): ) +class BlenderSubmitDeadlineModel(BaseSettingsModel): + enabled: bool = Field(True) + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + use_published: bool = Field(title="Use Published scene") + priority: int = Field(title="Priority") + chunk_size: int = Field(title="Frame per Task") + group: str = Field("", title="Group Name") + + class AOVFilterSubmodel(BaseSettingsModel): _layout = "expanded" name: str = Field(title="Host") @@ -276,8 +286,10 @@ class PublishPluginsModel(BaseSettingsModel): title="After Effects to deadline") CelactionSubmitDeadline: CelactionSubmitDeadlineModel = Field( default_factory=CelactionSubmitDeadlineModel, - title="Celaction Submit Deadline" - ) + title="Celaction Submit Deadline") + BlenderSubmitDeadline: BlenderSubmitDeadlineModel = Field( + default_factory=BlenderSubmitDeadlineModel, + title="Blender Submit Deadline") ProcessSubmittedJobOnFarm: ProcessSubmittedJobOnFarmModel = Field( default_factory=ProcessSubmittedJobOnFarmModel, title="Process submitted job on farm.") @@ -384,6 +396,15 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = { "deadline_chunk_size": 10, "deadline_job_delay": "00:00:00:00" }, + "BlenderSubmitDeadline": { + "enabled": True, + "optional": False, + "active": True, + "use_published": True, + "priority": 50, + "chunk_size": 10, + "group": "none" + }, "ProcessSubmittedJobOnFarm": { "enabled": True, "deadline_department": "", From 1ee944d03c07172d5dd6919eab7b4a974ab52369 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 24 Aug 2023 15:33:53 +0100 Subject: [PATCH 024/186] Increase workfile version after render publish --- .../blender/plugins/publish/increment_workfile_version.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index 27fa4baf28..5f49ad7185 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -9,7 +9,8 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin): label = "Increment Workfile Version" optional = True hosts = ["blender"] - families = ["animation", "model", "rig", "action", "layout", "blendScene"] + families = ["animation", "model", "rig", "action", "layout", "blendScene", + "renderlayer"] def process(self, context): From 481e814858e9c90e4b8308707d341387cb151fd9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 24 Aug 2023 16:39:50 +0100 Subject: [PATCH 025/186] Save the workfile after creating the render instance Blender, by design, doesn't set the file as dirty if modifications happen by script. So, when creating the instance and setting the render settings, the file is not marked as dirty. This means that there is the risk of sending to deadline a file without the right settings. Even the validator to check that the file is saved will detect the file as saved, even if it isn't. The only solution for now it is to force the file to be saved. --- .../hosts/blender/plugins/create/create_render.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 2952baafd3..62700cb55c 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -288,4 +288,15 @@ class CreateRenderlayer(plugin.Creator): bpy.data.collections.remove(asset_group) raise + # TODO: this is undesiderable, but it's the only way to be sure that + # the file is saved before the render starts. + # Blender, by design, doesn't set the file as dirty if modifications + # happen by script. So, when creating the instance and setting the + # render settings, the file is not marked as dirty. This means that + # there is the risk of sending to deadline a file without the right + # settings. Even the validator to check that the file is saved will + # detect the file as saved, even if it isn't. The only solution for + # now it is to force the file to be saved. + bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) + return asset_group From d4ee32a9b78e06ecb25422a44487893bb8d40510 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 25 Aug 2023 12:51:47 +0800 Subject: [PATCH 026/186] pre-hook ocio configuration for max 2024 --- openpype/hooks/pre_ocio_hook.py | 2 +- openpype/hosts/max/api/lib.py | 33 ++++++++++++++++++++++++++++++++- openpype/hosts/max/api/menu.py | 8 ++++++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index 1307ed9f76..4eee48d57c 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -13,7 +13,7 @@ class OCIOEnvHook(PreLaunchHook): "fusion", "blender", "aftereffects", - "max", + "max", "3dsmax", "houdini", "maya", "nuke", diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ccd4cd67e1..d32aa8599a 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- """Library of functions useful for 3dsmax pipeline.""" +import os import contextlib import json from typing import Any, Dict, Union import six +from openpype.pipeline import get_current_project_name +from openpype.settings import get_project_settings from openpype.pipeline.context_tools import ( - get_current_project, get_current_project_asset,) + get_current_project, get_current_project_asset) from pymxs import runtime as rt JSON_PREFIX = "JSON::" @@ -277,6 +280,7 @@ def set_context_setting(): """ reset_scene_resolution() reset_frame_range() + reset_colorspace() def get_max_version(): @@ -312,3 +316,30 @@ def set_timeline(frameStart, frameEnd): """ rt.animationRange = rt.interval(frameStart, frameEnd) return rt.animationRange + + +def reset_colorspace(): + """OCIO Configuration + Supports in 3dsMax 2024+ + + """ + if int(get_max_version) < 2024: + return + project_name = get_current_project_name() + ocio_config_path = os.environ.get("OCIO") + global_imageio = get_project_settings( + project_name)["global"]["imageio"] + if global_imageio["activate_global_color_management"]: + ocio_config = global_imageio["ocio_config"] + ocio_config_path = ocio_config["filepath"][-1] + + max_imageio = get_project_settings( + project_name)["global"]["imageio"] + if max_imageio["activate_global_color_management"]: + ocio_config = max_imageio["ocio_config"] + if ocio_config["override_global_config"]: + ocio_config_path = ocio_config["filepath"][0] + + colorspace_mgr = rt.ColorPipelineMgr + colorspace_mgr.Mode = rt.Name("OCIO_Custom") + colorspace_mgr.OCIOConfigPath = ocio_config_path diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index 066cc90039..aee4568669 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -119,6 +119,10 @@ class OpenPypeMenu(object): frame_action.triggered.connect(self.frame_range_callback) openpype_menu.addAction(frame_action) + colorspace_action = QtWidgets.QAction("Set Colorspace", openpype_menu) + colorspace_action.triggered.connect(self.colospace_setting_callback) + openpype_menu.addAction(colorspace_action) + return openpype_menu def load_callback(self): @@ -148,3 +152,7 @@ class OpenPypeMenu(object): def frame_range_callback(self): """Callback to reset frame range""" return lib.reset_frame_range() + + def colospace_setting_callback(self): + """Callback to reset OCIO colorspace setting""" + return lib.reset_colorspace() \ No newline at end of file From 20df677e48d52b00834348e813017f99d9c2a07d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 29 Aug 2023 22:39:49 +0800 Subject: [PATCH 027/186] add ocio display view transform options in creator settings --- openpype/hosts/max/api/lib.py | 10 +++---- .../hosts/max/plugins/create/create_render.py | 28 ++++++++++++++++++- .../max/plugins/publish/collect_render.py | 17 +++++++++-- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index d32aa8599a..6218fd8351 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -323,9 +323,11 @@ def reset_colorspace(): Supports in 3dsMax 2024+ """ - if int(get_max_version) < 2024: + if int(get_max_version()) < 2024: return project_name = get_current_project_name() + colorspace_mgr = rt.ColorPipelineMgr + colorspace_mgr.Mode = rt.Name("OCIO_Custom") ocio_config_path = os.environ.get("OCIO") global_imageio = get_project_settings( project_name)["global"]["imageio"] @@ -334,12 +336,10 @@ def reset_colorspace(): ocio_config_path = ocio_config["filepath"][-1] max_imageio = get_project_settings( - project_name)["global"]["imageio"] - if max_imageio["activate_global_color_management"]: + project_name)["max"]["imageio"] + if max_imageio["activate_host_color_management"]: ocio_config = max_imageio["ocio_config"] if ocio_config["override_global_config"]: ocio_config_path = ocio_config["filepath"][0] - colorspace_mgr = rt.ColorPipelineMgr - colorspace_mgr.Mode = rt.Name("OCIO_Custom") colorspace_mgr.OCIOConfigPath = ocio_config_path diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 235046684e..f3a4c7d7fa 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -2,7 +2,10 @@ """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin +from openpype.hosts.max.api.lib import get_max_version from openpype.hosts.max.api.lib_rendersettings import RenderSettings +from openpype.lib import EnumDef +from pymxs import runtime as rt class CreateRender(plugin.MaxCreator): @@ -13,11 +16,13 @@ class CreateRender(plugin.MaxCreator): icon = "gear" def create(self, subset_name, instance_data, pre_create_data): - from pymxs import runtime as rt sel_obj = list(rt.selection) file = rt.maxFileName filename, _ = os.path.splitext(file) instance_data["AssetName"] = filename + instance_data["ocio_display_view_transform"] = ( + pre_create_data.get("ocio_display_view_transform") + ) instance = super(CreateRender, self).create( subset_name, @@ -30,3 +35,24 @@ class CreateRender(plugin.MaxCreator): RenderSettings(self.project_settings).set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) + ocio_display = instance.data.get("ocio_display") + if ocio_display: + self.ocio_display = ocio_display + + def get_pre_create_attr_defs(self): + attrs = super(CreateRender, self).get_pre_create_attr_defs() + ocio_display_view_transform_list = [] + colorspace_mgr = rt.ColorPipelineMgr + displays = colorspace_mgr.GetDisplayList() + for display in sorted(displays): + views = colorspace_mgr.GetViewList(display) + for view in sorted(views): + ocio_display_view_transform_list.append({ + "value": "||".join((display, view)) + }) + return attrs + [ + EnumDef("ocio_display_view_transform", + ocio_display_view_transform_list, + default="", + label="OCIO Displays and Views") + ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index db5c84fad9..d003cbac06 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -4,6 +4,7 @@ import os import pyblish.api from pymxs import runtime as rt +from openpype.lib import EnumDef from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace from openpype.hosts.max.api.lib import get_max_version, get_current_renderer @@ -58,9 +59,19 @@ class CollectRender(pyblish.api.InstancePlugin): # most of the 3dsmax renderers # so this is currently hard coded # TODO: add options for redshift/vray ocio config - instance.data["colorspaceConfig"] = "" - instance.data["colorspaceDisplay"] = "sRGB" - instance.data["colorspaceView"] = "ACES 1.0 SDR-video" + if int(get_max_version()) >= 2024: + display_view_transform = instance.data["ocio_display_view_transform"] + display, view_transform = display_view_transform.split("||") + colorspace_mgr = rt.ColorPipelineMgr + instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath + instance.data["colorspaceDisplay"] = display + instance.data["colorspaceView"] = view_transform + + else: + instance.data["colorspaceConfig"] = "" + instance.data["colorspaceDisplay"] = "sRGB" + instance.data["colorspaceView"] = "ACES 1.0 SDR-video" + instance.data["renderProducts"] = colorspace.ARenderProduct() instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] From 1c3764ff5958968b9ef5953521b18c3dcbff5ef2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 29 Aug 2023 22:42:34 +0800 Subject: [PATCH 028/186] hound --- openpype/hosts/max/api/menu.py | 2 +- openpype/hosts/max/plugins/create/create_render.py | 1 - openpype/hosts/max/plugins/publish/collect_render.py | 3 +-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index aee4568669..670270e821 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -155,4 +155,4 @@ class OpenPypeMenu(object): def colospace_setting_callback(self): """Callback to reset OCIO colorspace setting""" - return lib.reset_colorspace() \ No newline at end of file + return lib.reset_colorspace() diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index f3a4c7d7fa..39f95c3b03 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -2,7 +2,6 @@ """Creator plugin for creating camera.""" import os from openpype.hosts.max.api import plugin -from openpype.hosts.max.api.lib import get_max_version from openpype.hosts.max.api.lib_rendersettings import RenderSettings from openpype.lib import EnumDef from pymxs import runtime as rt diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index d003cbac06..12a0ac5487 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -4,7 +4,6 @@ import os import pyblish.api from pymxs import runtime as rt -from openpype.lib import EnumDef from openpype.pipeline import get_current_asset_name from openpype.hosts.max.api import colorspace from openpype.hosts.max.api.lib import get_max_version, get_current_renderer @@ -60,7 +59,7 @@ class CollectRender(pyblish.api.InstancePlugin): # so this is currently hard coded # TODO: add options for redshift/vray ocio config if int(get_max_version()) >= 2024: - display_view_transform = instance.data["ocio_display_view_transform"] + display_view_transform = instance.data["ocio_display_view_transform"] # noqa display, view_transform = display_view_transform.split("||") colorspace_mgr = rt.ColorPipelineMgr instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath From 42033a02946ee2e3480365613eef39ad1b7f3881 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 30 Aug 2023 21:09:12 +0800 Subject: [PATCH 029/186] remove unnecessary codes --- openpype/hosts/max/plugins/create/create_render.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 39f95c3b03..4f97802325 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -35,8 +35,6 @@ class CreateRender(plugin.MaxCreator): # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) ocio_display = instance.data.get("ocio_display") - if ocio_display: - self.ocio_display = ocio_display def get_pre_create_attr_defs(self): attrs = super(CreateRender, self).get_pre_create_attr_defs() From f0436f78c48f887b70b9438d626f4350634517a4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 30 Aug 2023 21:10:29 +0800 Subject: [PATCH 030/186] hound --- openpype/hosts/max/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 4f97802325..7575d297e3 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -34,7 +34,6 @@ class CreateRender(plugin.MaxCreator): RenderSettings(self.project_settings).set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) - ocio_display = instance.data.get("ocio_display") def get_pre_create_attr_defs(self): attrs = super(CreateRender, self).get_pre_create_attr_defs() From fc3598ca2b3e249ae4dc52b606ce33cc2bc2dbab Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 31 Aug 2023 23:27:03 +0800 Subject: [PATCH 031/186] oscar's comment on the code changes --- openpype/hosts/max/api/lib.py | 3 +++ openpype/hosts/max/api/menu.py | 6 +----- openpype/hosts/max/plugins/publish/collect_render.py | 9 ++++----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 6218fd8351..b5fba73c72 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -341,5 +341,8 @@ def reset_colorspace(): ocio_config = max_imageio["ocio_config"] if ocio_config["override_global_config"]: ocio_config_path = ocio_config["filepath"][0] + if not ocio_config_path: + # use the default ocio config path instead + ocio_config_path = ocio_config["filepath"][-1] colorspace_mgr.OCIOConfigPath = ocio_config_path diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index 670270e821..e21ca32712 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -120,7 +120,7 @@ class OpenPypeMenu(object): openpype_menu.addAction(frame_action) colorspace_action = QtWidgets.QAction("Set Colorspace", openpype_menu) - colorspace_action.triggered.connect(self.colospace_setting_callback) + colorspace_action.triggered.connect(lib.reset_colorspace()) openpype_menu.addAction(colorspace_action) return openpype_menu @@ -152,7 +152,3 @@ class OpenPypeMenu(object): def frame_range_callback(self): """Callback to reset frame range""" return lib.reset_frame_range() - - def colospace_setting_callback(self): - """Callback to reset OCIO colorspace setting""" - return lib.reset_colorspace() diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 12a0ac5487..0a745f1772 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -58,6 +58,10 @@ class CollectRender(pyblish.api.InstancePlugin): # most of the 3dsmax renderers # so this is currently hard coded # TODO: add options for redshift/vray ocio config + instance.data["colorspaceConfig"] = "" + instance.data["colorspaceDisplay"] = "sRGB" + instance.data["colorspaceView"] = "ACES 1.0 SDR-video" + if int(get_max_version()) >= 2024: display_view_transform = instance.data["ocio_display_view_transform"] # noqa display, view_transform = display_view_transform.split("||") @@ -66,11 +70,6 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["colorspaceDisplay"] = display instance.data["colorspaceView"] = view_transform - else: - instance.data["colorspaceConfig"] = "" - instance.data["colorspaceDisplay"] = "sRGB" - instance.data["colorspaceView"] = "ACES 1.0 SDR-video" - instance.data["renderProducts"] = colorspace.ARenderProduct() instance.data["publishJobState"] = "Suspended" instance.data["attachTo"] = [] From 8ea97559201e1ea9eb77e9b9109296f903429a53 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 1 Sep 2023 20:28:50 +0800 Subject: [PATCH 032/186] Libor's comment on the ocio settngs in publish tab and wip of the small popup widget for setting ocio config --- openpype/hosts/max/api/lib.py | 42 +++++++++++++++++++ openpype/hosts/max/api/pipeline.py | 3 ++ .../hosts/max/plugins/create/create_render.py | 37 ++++++++-------- .../max/plugins/publish/collect_render.py | 3 +- 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index b5fba73c72..7c8a5c86d4 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -15,6 +15,33 @@ from pymxs import runtime as rt JSON_PREFIX = "JSON::" +class Context: + main_window = None + context_label = None + project_name = os.getenv("AVALON_PROJECT") + # Workfile related code + workfiles_launched = False + workfiles_tool_timer = None + + + +def get_main_window(): + """Acquire Max's main window""" + from qtpy import QtWidgets + if Context.main_window is None: + + top_widgets = QtWidgets.QApplication.topLevelWidgets() + name = "QmaxApplicationWindow" + for widget in top_widgets: + if ( + widget.inherits("QMainWindow") + and widget.metaObject().className() == name + ): + Context.main_window = widget + break + return Context.main_window + + def imprint(node_name: str, data: dict) -> bool: node = rt.GetNodeByName(node_name) if not node: @@ -346,3 +373,18 @@ def reset_colorspace(): ocio_config_path = ocio_config["filepath"][-1] colorspace_mgr.OCIOConfigPath = ocio_config_path + + +def check_colorspace(): + parent = get_main_window() + if int(get_max_version()) >= 2024: + color_mgr = rt.ColorPipelineMgr + if color_mgr.Mode != rt.Name("OCIO_Custom"): + from openpype.widgets import popup + dialog = popup.Popup(parent=parent) + dialog.setWindowTitle("Warning: Wrong OCIO Mode") + dialog.setMessage("This scene has wrong OCIO " + "Mode setting.") + dialog.widgets["button"].setText("Fix") + dialog.on_clicked.connect(reset_colorspace) + dialog.show() diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index 03b85a4066..f7d23236ea 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -56,6 +56,9 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): 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? return True diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 7575d297e3..41b38eeca3 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -3,6 +3,7 @@ import os from openpype.hosts.max.api import plugin from openpype.hosts.max.api.lib_rendersettings import RenderSettings +from openpype.hosts.max.api.lib import get_max_version from openpype.lib import EnumDef from pymxs import runtime as rt @@ -18,11 +19,6 @@ class CreateRender(plugin.MaxCreator): sel_obj = list(rt.selection) file = rt.maxFileName filename, _ = os.path.splitext(file) - instance_data["AssetName"] = filename - instance_data["ocio_display_view_transform"] = ( - pre_create_data.get("ocio_display_view_transform") - ) - instance = super(CreateRender, self).create( subset_name, instance_data, @@ -35,20 +31,27 @@ class CreateRender(plugin.MaxCreator): # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) - def get_pre_create_attr_defs(self): - attrs = super(CreateRender, self).get_pre_create_attr_defs() - ocio_display_view_transform_list = [] - colorspace_mgr = rt.ColorPipelineMgr - displays = colorspace_mgr.GetDisplayList() - for display in sorted(displays): - views = colorspace_mgr.GetViewList(display) - for view in sorted(views): - ocio_display_view_transform_list.append({ - "value": "||".join((display, view)) + def get_instance_attr_defs(self): + ocio_display_view_transform_list = ["sRGB||ACES 1.0 SDR-video"] + if int(get_max_version()) >= 2024: + display_view_default = "" + ocio_display_view_transform_list = [] + colorspace_mgr = rt.ColorPipelineMgr + displays = colorspace_mgr.GetDisplayList() + for display in sorted(displays): + views = colorspace_mgr.GetViewList(display) + for view in sorted(views): + ocio_display_view_transform_list.append({ + "value": "||".join((display, view)) }) - return attrs + [ + if display == "ACES" and view == "sRGB": + display_view_default = "{0}||{1}".format( + display, view + ) + + return [ EnumDef("ocio_display_view_transform", ocio_display_view_transform_list, - default="", + default=display_view_default, label="OCIO Displays and Views") ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 1b8ca30a32..522d20322e 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -66,7 +66,8 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["colorspaceView"] = "ACES 1.0 SDR-video" if int(get_max_version()) >= 2024: - display_view_transform = instance.data["ocio_display_view_transform"] # noqa + creator_attribute = instance.data["creator_attributes"] + display_view_transform = creator_attribute["ocio_display_view_transform"] # noqa display, view_transform = display_view_transform.split("||") colorspace_mgr = rt.ColorPipelineMgr instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath From 3caaf2a5d5800e9b640d7bc678fae54825a71cc5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 1 Sep 2023 21:31:10 +0800 Subject: [PATCH 033/186] hound & fixing the render camera issue --- openpype/hosts/max/api/lib.py | 1 - openpype/hosts/max/plugins/create/create_render.py | 2 +- openpype/hosts/max/plugins/publish/collect_render.py | 2 ++ .../modules/deadline/plugins/publish/submit_max_deadline.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 7c8a5c86d4..7afe14ddcb 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -24,7 +24,6 @@ class Context: workfiles_tool_timer = None - def get_main_window(): """Acquire Max's main window""" from qtpy import QtWidgets diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 41b38eeca3..c12ae8155a 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -43,7 +43,7 @@ class CreateRender(plugin.MaxCreator): for view in sorted(views): ocio_display_view_transform_list.append({ "value": "||".join((display, view)) - }) + }) if display == "ACES" and view == "sRGB": display_view_default = "{0}||{1}".format( display, view diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 522d20322e..675e3b6a57 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -35,6 +35,8 @@ class CollectRender(pyblish.api.InstancePlugin): files_by_aov.update(aovs) camera = rt.viewport.GetCamera() + if instance.data.get("members"): + camera = instance.data["members"][-1] instance.data["cameras"] = [camera.name] if camera else None # noqa if "expectedFiles" not in instance.data: diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 63c6e4a0c7..a9f440668c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -238,7 +238,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, plugin_data["redshift_SeparateAovFiles"] = instance.data.get( "separateAovFiles") if instance.data["cameras"]: - plugin_info["Camera0"] = None + plugin_info["Camera0"] = instance.data["cameras"][0] plugin_info["Camera"] = instance.data["cameras"][0] plugin_info["Camera1"] = instance.data["cameras"][0] self.log.debug("plugin data:{}".format(plugin_data)) From 0cee44306e50e8f012ea559e477ae71770deb5f9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 4 Sep 2023 23:02:02 +0800 Subject: [PATCH 034/186] add colorspace data in collect review --- .../max/plugins/publish/collect_review.py | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 7aeb45f46b..e3ad59ea7d 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -3,7 +3,8 @@ import pyblish.api from pymxs import runtime as rt -from openpype.lib import BoolDef +from openpype.lib import BoolDef, EnumDef +from openpype.hosts.max.api.lib import get_max_version from openpype.pipeline.publish import OpenPypePyblishPluginMixin @@ -43,6 +44,16 @@ class CollectReview(pyblish.api.InstancePlugin, "dspSafeFrame": attr_values.get("dspSafeFrame"), "dspFrameNums": attr_values.get("dspFrameNums") } + + if int(get_max_version()) >= 2024: + display_view_transform = attr_values.get( + "ocio_display_view_transform") + display, view_transform = display_view_transform.split("||") + colorspace_mgr = rt.ColorPipelineMgr + instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath + instance.data["colorspaceDisplay"] = display + instance.data["colorspaceView"] = view_transform + # Enable ftrack functionality instance.data.setdefault("families", []).append('ftrack') @@ -54,8 +65,28 @@ class CollectReview(pyblish.api.InstancePlugin, @classmethod def get_attribute_defs(cls): - + ocio_display_view_transform_list = ["sRGB||ACES 1.0 SDR-video"] + display_view_default = "" + if int(get_max_version()) >= 2024: + display_view_default = "" + ocio_display_view_transform_list = [] + colorspace_mgr = rt.ColorPipelineMgr + displays = colorspace_mgr.GetDisplayList() + for display in sorted(displays): + views = colorspace_mgr.GetViewList(display) + for view in sorted(views): + ocio_display_view_transform_list.append({ + "value": "||".join((display, view)) + }) + if display == "ACES" and view == "sRGB": + display_view_default = "{0}||{1}".format( + display, view + ) return [ + EnumDef("ocio_display_view_transform", + ocio_display_view_transform_list, + default=display_view_default, + label="OCIO Displays and Views"), BoolDef("dspGeometry", label="Geometry", default=True), From c5013305bd1585c057807bd86629f4bdff571a62 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Tue, 5 Sep 2023 21:15:32 +0800 Subject: [PATCH 035/186] Update openpype/hosts/max/api/lib.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/max/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 23116b0365..b909c1c156 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -388,7 +388,7 @@ def check_colorspace(): dialog.setWindowTitle("Warning: Wrong OCIO Mode") dialog.setMessage("This scene has wrong OCIO " "Mode setting.") - dialog.widgets["button"].setText("Fix") + dialog.setButtonText("Fix") dialog.on_clicked.connect(reset_colorspace) dialog.show() From c88b7106fd896d7de2c111e5ffc1a8dbf86191c6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 5 Sep 2023 21:30:59 +0800 Subject: [PATCH 036/186] resolve the qt style issue with jakub's comment --- openpype/hosts/max/api/lib.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index b909c1c156..a2d45fee77 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -11,8 +11,10 @@ from openpype.pipeline import get_current_project_name from openpype.settings import get_project_settings from openpype.pipeline.context_tools import ( get_current_project, get_current_project_asset) +from openpype.style import load_stylesheet from pymxs import runtime as rt + JSON_PREFIX = "JSON::" log = logging.getLogger("openpype.hosts.max") @@ -389,10 +391,10 @@ def check_colorspace(): dialog.setMessage("This scene has wrong OCIO " "Mode setting.") dialog.setButtonText("Fix") + dialog.setStyleSheet(load_stylesheet()) dialog.on_clicked.connect(reset_colorspace) dialog.show() - def unique_namespace(namespace, format="%02d", prefix="", suffix="", con_suffix="CON"): """Return unique namespace From 4aa8f5ac5f333e76894b6fbc3a82e9a43602baed Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 6 Sep 2023 16:35:46 +0800 Subject: [PATCH 037/186] big roy's comments on the code fix --- openpype/hooks/pre_ocio_hook.py | 2 +- openpype/hosts/max/api/lib.py | 67 ++++++++----------- openpype/hosts/max/api/menu.py | 6 +- .../hosts/max/plugins/create/create_render.py | 24 +++---- .../max/plugins/publish/collect_render.py | 8 ++- .../max/plugins/publish/collect_review.py | 18 ++--- .../plugins/publish/submit_max_deadline.py | 7 +- 7 files changed, 63 insertions(+), 69 deletions(-) diff --git a/openpype/hooks/pre_ocio_hook.py b/openpype/hooks/pre_ocio_hook.py index e0877e19bc..e695cf3fe8 100644 --- a/openpype/hooks/pre_ocio_hook.py +++ b/openpype/hooks/pre_ocio_hook.py @@ -13,7 +13,7 @@ class OCIOEnvHook(PreLaunchHook): "fusion", "blender", "aftereffects", - "max", "3dsmax", + "3dsmax", "houdini", "maya", "nuke", diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index a2d45fee77..e5d333c275 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -7,7 +7,7 @@ import json from typing import Any, Dict, Union import six -from openpype.pipeline import get_current_project_name +from openpype.pipeline import get_current_project_name, colorspace from openpype.settings import get_project_settings from openpype.pipeline.context_tools import ( get_current_project, get_current_project_asset) @@ -19,29 +19,18 @@ JSON_PREFIX = "JSON::" log = logging.getLogger("openpype.hosts.max") -class Context: - main_window = None - context_label = None - project_name = os.getenv("AVALON_PROJECT") - # Workfile related code - workfiles_launched = False - workfiles_tool_timer = None - - def get_main_window(): """Acquire Max's main window""" from qtpy import QtWidgets - if Context.main_window is None: - top_widgets = QtWidgets.QApplication.topLevelWidgets() - name = "QmaxApplicationWindow" - for widget in top_widgets: - if ( - widget.inherits("QMainWindow") - and widget.metaObject().className() == name - ): - Context.main_window = widget - break - return Context.main_window + top_widgets = QtWidgets.QApplication.topLevelWidgets() + name = "QmaxApplicationWindow" + for widget in top_widgets: + if ( + widget.inherits("QMainWindow") + and widget.metaObject().className() == name + ): + return widget + raise RuntimeError('Count not find 3dsMax main window.') def imprint(node_name: str, data: dict) -> bool: @@ -356,23 +345,15 @@ def reset_colorspace(): return project_name = get_current_project_name() colorspace_mgr = rt.ColorPipelineMgr - colorspace_mgr.Mode = rt.Name("OCIO_Custom") - ocio_config_path = os.environ.get("OCIO") - global_imageio = get_project_settings( - project_name)["global"]["imageio"] - if global_imageio["activate_global_color_management"]: - ocio_config = global_imageio["ocio_config"] - ocio_config_path = ocio_config["filepath"][-1] + project_settings = get_project_settings(project_name) - max_imageio = get_project_settings( - project_name)["max"]["imageio"] - if max_imageio["activate_host_color_management"]: - ocio_config = max_imageio["ocio_config"] - if ocio_config["override_global_config"]: - ocio_config_path = ocio_config["filepath"][0] - if not ocio_config_path: - # use the default ocio config path instead - ocio_config_path = ocio_config["filepath"][-1] + max_config_data = colorspace.get_imageio_config( + project_name, "max", project_settings) + if max_config_data: + ocio_config_path = max_config_data["path"] + colorspace_mgr = rt.ColorPipelineMgr + colorspace_mgr.Mode = rt.Name("OCIO_Custom") + colorspace_mgr.OCIOConfigPath = ocio_config_path colorspace_mgr.OCIOConfigPath = ocio_config_path @@ -384,12 +365,20 @@ def check_colorspace(): "because Max main window can't be found.") if int(get_max_version()) >= 2024: color_mgr = rt.ColorPipelineMgr - if color_mgr.Mode != rt.Name("OCIO_Custom"): + project_name = get_current_project_name() + project_settings = get_project_settings( + project_name) + global_imageio = project_settings["global"]["imageio"] + max_config_data = colorspace.get_imageio_config( + project_name, "max", project_settings) + config_enabled = global_imageio["activate_global_color_management"] or ( + max_config_data) + if config_enabled and color_mgr.Mode != rt.Name("OCIO_Custom"): from openpype.widgets import popup dialog = popup.Popup(parent=parent) dialog.setWindowTitle("Warning: Wrong OCIO Mode") dialog.setMessage("This scene has wrong OCIO " - "Mode setting.") + "Mode setting.") dialog.setButtonText("Fix") dialog.setStyleSheet(load_stylesheet()) dialog.on_clicked.connect(reset_colorspace) diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index e21ca32712..364f9cd5c5 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -120,7 +120,7 @@ class OpenPypeMenu(object): openpype_menu.addAction(frame_action) colorspace_action = QtWidgets.QAction("Set Colorspace", openpype_menu) - colorspace_action.triggered.connect(lib.reset_colorspace()) + colorspace_action.triggered.connect(self.colorspace_callback) openpype_menu.addAction(colorspace_action) return openpype_menu @@ -152,3 +152,7 @@ class OpenPypeMenu(object): def frame_range_callback(self): """Callback to reset frame range""" return lib.reset_frame_range() + + def colorspace_callback(self): + """Callback to reset colorspace""" + return lib.reset_colorspace() diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index c12ae8155a..6c60f432d3 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" -import os from openpype.hosts.max.api import plugin from openpype.hosts.max.api.lib_rendersettings import RenderSettings from openpype.hosts.max.api.lib import get_max_version @@ -17,8 +16,6 @@ class CreateRender(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): sel_obj = list(rt.selection) - file = rt.maxFileName - filename, _ = os.path.splitext(file) instance = super(CreateRender, self).create( subset_name, instance_data, @@ -32,26 +29,25 @@ class CreateRender(plugin.MaxCreator): RenderSettings().render_output(container_name) def get_instance_attr_defs(self): - ocio_display_view_transform_list = ["sRGB||ACES 1.0 SDR-video"] if int(get_max_version()) >= 2024: - display_view_default = "" - ocio_display_view_transform_list = [] + default_value = "" + display_views = [] colorspace_mgr = rt.ColorPipelineMgr - displays = colorspace_mgr.GetDisplayList() - for display in sorted(displays): - views = colorspace_mgr.GetViewList(display) - for view in sorted(views): - ocio_display_view_transform_list.append({ + for display in sorted(colorspace_mgr.GetDisplayList()): + for view in sorted(colorspace_mgr.GetViewList(display)): + display_views.append({ "value": "||".join((display, view)) }) if display == "ACES" and view == "sRGB": - display_view_default = "{0}||{1}".format( + default_value = "{0}||{1}".format( display, view ) + else: + display_views = ["sRGB||ACES 1.0 SDR-video"] return [ EnumDef("ocio_display_view_transform", - ocio_display_view_transform_list, - default=display_view_default, + display_views, + default=default_value, label="OCIO Displays and Views") ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 675e3b6a57..729a5b173c 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -36,7 +36,11 @@ class CollectRender(pyblish.api.InstancePlugin): camera = rt.viewport.GetCamera() if instance.data.get("members"): - camera = instance.data["members"][-1] + camera_list = [member for member in instance.data["members"] + if rt.ClassOf(member) == rt.Camera.Classes] + if camera_list: + camera = camera_list[-1] + instance.data["cameras"] = [camera.name] if camera else None # noqa if "expectedFiles" not in instance.data: @@ -70,7 +74,7 @@ class CollectRender(pyblish.api.InstancePlugin): if int(get_max_version()) >= 2024: creator_attribute = instance.data["creator_attributes"] display_view_transform = creator_attribute["ocio_display_view_transform"] # noqa - display, view_transform = display_view_transform.split("||") + display, view_transform = display_view_transform.split("||", 1) colorspace_mgr = rt.ColorPipelineMgr instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath instance.data["colorspaceDisplay"] = display diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index e3ad59ea7d..686dc2ed2c 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -48,7 +48,7 @@ class CollectReview(pyblish.api.InstancePlugin, if int(get_max_version()) >= 2024: display_view_transform = attr_values.get( "ocio_display_view_transform") - display, view_transform = display_view_transform.split("||") + display, view_transform = display_view_transform.split("||", 1) colorspace_mgr = rt.ColorPipelineMgr instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath instance.data["colorspaceDisplay"] = display @@ -65,27 +65,27 @@ class CollectReview(pyblish.api.InstancePlugin, @classmethod def get_attribute_defs(cls): - ocio_display_view_transform_list = ["sRGB||ACES 1.0 SDR-video"] - display_view_default = "" + default_value = "" + display_views = [] if int(get_max_version()) >= 2024: - display_view_default = "" - ocio_display_view_transform_list = [] colorspace_mgr = rt.ColorPipelineMgr displays = colorspace_mgr.GetDisplayList() for display in sorted(displays): views = colorspace_mgr.GetViewList(display) for view in sorted(views): - ocio_display_view_transform_list.append({ + display_views.append({ "value": "||".join((display, view)) }) if display == "ACES" and view == "sRGB": - display_view_default = "{0}||{1}".format( + default_value = "{0}||{1}".format( display, view ) + else: + display_views = ["sRGB||ACES 1.0 SDR-video"] return [ EnumDef("ocio_display_view_transform", - ocio_display_view_transform_list, - default=display_view_default, + items=display_views, + default=default_value, label="OCIO Displays and Views"), BoolDef("dspGeometry", label="Geometry", diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index a9f440668c..073da3019a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -238,9 +238,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, plugin_data["redshift_SeparateAovFiles"] = instance.data.get( "separateAovFiles") if instance.data["cameras"]: - plugin_info["Camera0"] = instance.data["cameras"][0] - plugin_info["Camera"] = instance.data["cameras"][0] - plugin_info["Camera1"] = instance.data["cameras"][0] + camera = instance.data["cameras"][0] + plugin_info["Camera0"] = camera + plugin_info["Camera"] = camera + plugin_info["Camera1"] = camera self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) From 1eaddb548553c896563a9e6557188b4f40a2ae89 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 6 Sep 2023 16:47:07 +0800 Subject: [PATCH 038/186] hound --- openpype/hosts/max/api/lib.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index e5d333c275..0549309c0b 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- """Library of functions useful for 3dsmax pipeline.""" -import os import contextlib import logging import json @@ -366,12 +365,11 @@ def check_colorspace(): if int(get_max_version()) >= 2024: color_mgr = rt.ColorPipelineMgr project_name = get_current_project_name() - project_settings = get_project_settings( - project_name) + project_settings = get_project_settings(project_name) global_imageio = project_settings["global"]["imageio"] max_config_data = colorspace.get_imageio_config( - project_name, "max", project_settings) - config_enabled = global_imageio["activate_global_color_management"] or ( + project_name, "max", project_settings) + config_enabled = global_imageio["activate_global_color_management"] or ( # noqa max_config_data) if config_enabled and color_mgr.Mode != rt.Name("OCIO_Custom"): from openpype.widgets import popup From 35b4666043f7fb12368959687138002118dcb527 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 6 Sep 2023 17:12:04 +0800 Subject: [PATCH 039/186] add functions to check if the 3dsmax is in batch mode before popup dialog exists --- openpype/hosts/max/api/lib.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 0549309c0b..0f86e0a07a 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -313,6 +313,14 @@ def get_max_version(): return max_info[7] +def is_HEADLESS(): + """Check if 3dsMax runs in batch mode. + If it returns True, it runs in 3dsbatch.exe + If it returns False, it runs in 3dsmax.exe + """ + return rt.maxops.isInNonInteractiveMode() + + @contextlib.contextmanager def viewport_camera(camera): original = rt.viewport.getCamera() @@ -372,15 +380,16 @@ def check_colorspace(): config_enabled = global_imageio["activate_global_color_management"] or ( # noqa max_config_data) if config_enabled and color_mgr.Mode != rt.Name("OCIO_Custom"): - from openpype.widgets import popup - dialog = popup.Popup(parent=parent) - dialog.setWindowTitle("Warning: Wrong OCIO Mode") - dialog.setMessage("This scene has wrong OCIO " - "Mode setting.") - dialog.setButtonText("Fix") - dialog.setStyleSheet(load_stylesheet()) - dialog.on_clicked.connect(reset_colorspace) - dialog.show() + if not is_HEADLESS: + from openpype.widgets import popup + dialog = popup.Popup(parent=parent) + dialog.setWindowTitle("Warning: Wrong OCIO Mode") + dialog.setMessage("This scene has wrong OCIO " + "Mode setting.") + dialog.setButtonText("Fix") + dialog.setStyleSheet(load_stylesheet()) + dialog.on_clicked.connect(reset_colorspace) + dialog.show() def unique_namespace(namespace, format="%02d", prefix="", suffix="", con_suffix="CON"): From fab390f8f036a2da48807a55fe7b207c177cdc0b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 6 Sep 2023 21:51:47 +0800 Subject: [PATCH 040/186] big roy's comment on check_colorspace function and some fix on IS_HEADLESS() --- openpype/hosts/max/api/lib.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 0f86e0a07a..8cbb807607 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -374,18 +374,15 @@ def check_colorspace(): color_mgr = rt.ColorPipelineMgr project_name = get_current_project_name() project_settings = get_project_settings(project_name) - global_imageio = project_settings["global"]["imageio"] max_config_data = colorspace.get_imageio_config( project_name, "max", project_settings) - config_enabled = global_imageio["activate_global_color_management"] or ( # noqa - max_config_data) - if config_enabled and color_mgr.Mode != rt.Name("OCIO_Custom"): - if not is_HEADLESS: + if max_config_data and color_mgr.Mode != rt.Name("OCIO_Custom"): + if not is_HEADLESS(): from openpype.widgets import popup dialog = popup.Popup(parent=parent) dialog.setWindowTitle("Warning: Wrong OCIO Mode") dialog.setMessage("This scene has wrong OCIO " - "Mode setting.") + "Mode setting.") dialog.setButtonText("Fix") dialog.setStyleSheet(load_stylesheet()) dialog.on_clicked.connect(reset_colorspace) From 6d1407f3462eb09b31d2dbb0daba9d37bddc0d1c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 6 Sep 2023 21:52:35 +0800 Subject: [PATCH 041/186] hound --- openpype/hosts/max/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8cbb807607..6eb328b505 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -382,7 +382,7 @@ def check_colorspace(): dialog = popup.Popup(parent=parent) dialog.setWindowTitle("Warning: Wrong OCIO Mode") dialog.setMessage("This scene has wrong OCIO " - "Mode setting.") + "Mode setting.") dialog.setButtonText("Fix") dialog.setStyleSheet(load_stylesheet()) dialog.on_clicked.connect(reset_colorspace) From da7c47ab2fe6c9a87de6491de2cf71abd7fc98b2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 7 Sep 2023 14:47:22 +0800 Subject: [PATCH 042/186] add fbx extractors and new sets in rig family --- openpype/hosts/maya/api/fbx.py | 4 +- .../hosts/maya/plugins/create/create_rig.py | 22 ++++- .../plugins/publish/collect_rig_for_fbx.py | 45 +++++++++ .../maya/plugins/publish/extract_rig_fbx.py | 92 +++++++++++++++++++ 4 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py create mode 100644 openpype/hosts/maya/plugins/publish/extract_rig_fbx.py diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 260241f5fc..bd0e77e427 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -63,6 +63,7 @@ class FBXExtractor: "embeddedTextures": bool, "inputConnections": bool, "upAxis": str, # x, y or z, + "referencedAssetsContent": bool, "triangulate": bool } @@ -104,7 +105,8 @@ class FBXExtractor: "embeddedTextures": False, "inputConnections": True, "upAxis": "y", - "triangulate": False + "referencedAssetsContent": False, + "triangulate": False, } def __init__(self, log=None): diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 345ab6c00d..9b67c84980 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -1,6 +1,7 @@ from maya import cmds from openpype.hosts.maya.api import plugin +from openpype.lib import BoolDef class CreateRig(plugin.MayaCreator): @@ -12,6 +13,7 @@ class CreateRig(plugin.MayaCreator): icon = "wheelchair" def create(self, subset_name, instance_data, pre_create_data): + instance_data["fbx_enabled"] = pre_create_data.get("fbx_enabled") instance = super(CreateRig, self).create(subset_name, instance_data, @@ -20,6 +22,24 @@ class CreateRig(plugin.MayaCreator): instance_node = instance.get("instance_node") self.log.info("Creating Rig instance set up ...") + # change name controls = cmds.sets(name=subset_name + "_controls_SET", empty=True) + # change name pointcache = cmds.sets(name=subset_name + "_out_SET", empty=True) - cmds.sets([controls, pointcache], forceElement=instance_node) + if pre_create_data.get("fbx_enabled"): + skeleton = cmds.sets(name=subset_name + "_skeleton_SET", empty=True) + skeleton_mesh = cmds.sets(name=subset_name + "_skeletonMesh_SET", empty=True) + cmds.sets([controls, pointcache, + skeleton, skeleton_mesh], forceElement=instance_node) + else: + cmds.sets([controls, pointcache], forceElement=instance_node) + + def get_pre_create_attr_defs(self): + attrs = super(CreateRig, self).get_pre_create_attr_defs() + + return attrs + [ + BoolDef("fbx_enabled", + label="Fbx Export", + default=False), + + ] diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py new file mode 100644 index 0000000000..c57045a052 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +from maya import cmds # noqa +import pyblish.api + + +class CollectRigFbx(pyblish.api.InstancePlugin): + """Collect Unreal Skeletal Mesh.""" + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect rig for fbx" + families = ["rig"] + + def process(self, instance): + if not instance.data.get("fbx_enabled"): + self.log.debug("Skipping collecting rig data for fbx..") + return + + frame = cmds.currentTime(query=True) + instance.data["frameStart"] = frame + instance.data["frameEnd"] = frame + skeleton_sets = [ + i for i in instance[:] + if i.lower().endswith("skeleton_set") + ] + + skeleton_mesh_sets = [ + i for i in instance[:] + if i.lower().endswith("skeletonmesh_set") + ] + if skeleton_sets or skeleton_mesh_sets: + instance.data["families"] += ["fbx"] + instance.data["geometries"] = [] + instance.data["control_rigs"] = [] + instance.data["skeleton_mesh"] = [] + for skeleton_set in skeleton_sets: + skeleton_content = cmds.ls( + cmds.sets(skeleton_set, query=True), long=True) + if skeleton_content: + instance.data["control_rigs"] += skeleton_content + + for skeleton_mesh_set in skeleton_mesh_sets: + skeleton_mesh_content = cmds.ls( + cmds.sets(skeleton_mesh_set, query=True), long=True) + if skeleton_mesh_content: + instance.data["skeleton_mesh"] += skeleton_mesh_content diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py new file mode 100644 index 0000000000..da1a458c9e --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +import os + +from maya import cmds # noqa +import maya.mel as mel # noqa +import pyblish.api + +from openpype.pipeline import publish +from openpype.hosts.maya.api.lib import maintained_selection +from openpype.hosts.maya.api import fbx + + +class ExtractRigFBX(publish.Extractor): + """Extract Rig in FBX format from Maya. + + This extracts the rig in fbx with the constraints + and referenced asset content included. + This also optionally extract animated rig in fbx with + geometries included. + + """ + order = pyblish.api.ExtractorOrder + label = "Extract Rig (FBX)" + families = ["rig"] + + def process(self, instance): + if not instance.data.get("fbx_enabled"): + self.log.debug("fbx extractor has been disable.." + "Skipping the action...") + return + + # Define output path + staging_dir = self.staging_dir(instance) + filename = "{0}.fbx".format(instance.name) + path = os.path.join(staging_dir, filename) + + # The export requires forward slashes because we need + # to format it into a string in a mel expression + path = path.replace('\\', '/') + + self.log.debug("Extracting FBX to: {0}".format(path)) + + control_rigs = instance.data.get("control_rigs",[]) + skeletal_mesh = instance.data.get("skeleton_mesh", []) + members = control_rigs + skeletal_mesh + self._to_extract(instance, path, members) + + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) + + self.log.debug("Extract FBX successful to: {0}".format(path)) + if skeletal_mesh: + self._to_extract(instance, path, skeletal_mesh) + representation = { + 'name': 'fbxanim', + 'ext': 'fbx', + 'files': filename, + "stagingDir": staging_dir, + "outputName": "fbxanim" + } + instance.data["representations"].append(representation) + self.log.debug("Extract animated FBX successful to: {0}".format(path)) + + def _to_extract(self, instance, path, members): + fbx_exporter = fbx.FBXExtractor(log=self.log) + control_rigs = instance.data.get("control_rigs",[]) + skeletal_mesh = instance.data.get("skeleton_mesh", []) + static_sets = control_rigs + skeletal_mesh + if members == static_sets: + instance.data["constraints"] = True + instance.data["referencedAssetsContent"] = True + if members == skeletal_mesh: + instance.data["constraints"] = True + instance.data["referencedAssetsContent"] = True + instance.data["animationOnly"] = True + + fbx_exporter.set_options_from_instance(instance) + + # Export + with maintained_selection(): + fbx_exporter.export(members, path) + cmds.select(members, r=1, noExpand=True) + mel.eval('FBXExport -f "{}" -s'.format(path)) From b54f263d4b7e977442b3087d0565da8a52770784 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 7 Sep 2023 14:55:52 +0800 Subject: [PATCH 043/186] hound --- openpype/hosts/maya/plugins/create/create_rig.py | 6 ++++-- .../hosts/maya/plugins/publish/collect_rig_for_fbx.py | 1 - openpype/hosts/maya/plugins/publish/extract_rig_fbx.py | 8 ++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 9b67c84980..030aa23a22 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -27,8 +27,10 @@ class CreateRig(plugin.MayaCreator): # change name pointcache = cmds.sets(name=subset_name + "_out_SET", empty=True) if pre_create_data.get("fbx_enabled"): - skeleton = cmds.sets(name=subset_name + "_skeleton_SET", empty=True) - skeleton_mesh = cmds.sets(name=subset_name + "_skeletonMesh_SET", empty=True) + skeleton = cmds.sets( + name=subset_name + "_skeleton_SET", empty=True) + skeleton_mesh = cmds.sets( + name=subset_name + "_skeletonMesh_SET", empty=True) cmds.sets([controls, pointcache, skeleton, skeleton_mesh], forceElement=instance_node) else: diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py index c57045a052..bef43aa5f4 100644 --- a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py +++ b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py @@ -28,7 +28,6 @@ class CollectRigFbx(pyblish.api.InstancePlugin): if i.lower().endswith("skeletonmesh_set") ] if skeleton_sets or skeleton_mesh_sets: - instance.data["families"] += ["fbx"] instance.data["geometries"] = [] instance.data["control_rigs"] = [] instance.data["skeleton_mesh"] = [] diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py index da1a458c9e..687b686fb8 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py @@ -40,12 +40,11 @@ class ExtractRigFBX(publish.Extractor): self.log.debug("Extracting FBX to: {0}".format(path)) - control_rigs = instance.data.get("control_rigs",[]) + control_rigs = instance.data.get("control_rigs", []) skeletal_mesh = instance.data.get("skeleton_mesh", []) members = control_rigs + skeletal_mesh self._to_extract(instance, path, members) - if "representations" not in instance.data: instance.data["representations"] = [] @@ -68,11 +67,12 @@ class ExtractRigFBX(publish.Extractor): "outputName": "fbxanim" } instance.data["representations"].append(representation) - self.log.debug("Extract animated FBX successful to: {0}".format(path)) + self.log.debug( + "Extract animated FBX successful to: {0}".format(path)) def _to_extract(self, instance, path, members): fbx_exporter = fbx.FBXExtractor(log=self.log) - control_rigs = instance.data.get("control_rigs",[]) + control_rigs = instance.data.get("control_rigs", []) skeletal_mesh = instance.data.get("skeleton_mesh", []) static_sets = control_rigs + skeletal_mesh if members == static_sets: From d58b5a42f7f792d1a8198cf500fa5fb31752e4f2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 7 Sep 2023 20:46:39 +0800 Subject: [PATCH 044/186] implment fbx extractors in both animation and rig family --- openpype/hosts/maya/api/fbx.py | 12 ++-- .../hosts/maya/plugins/create/create_rig.py | 30 +++------ .../plugins/publish/collect_fbx_animation.py | 27 ++++++++ .../plugins/publish/collect_rig_for_fbx.py | 10 +-- .../plugins/publish/extract_fbx_animation.py | 60 +++++++++++++++++ .../maya/plugins/publish/extract_rig_fbx.py | 56 ++++------------ .../plugins/publish/validate_rig_contents.py | 66 ++++++++++++++++++- .../publish/validate_rig_controllers.py | 8 ++- ...idate_rig_controllers_arnold_attributes.py | 3 +- .../publish/validate_rig_out_set_node_ids.py | 19 +++++- .../publish/validate_rig_output_ids.py | 10 ++- 11 files changed, 213 insertions(+), 88 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/collect_fbx_animation.py create mode 100644 openpype/hosts/maya/plugins/publish/extract_fbx_animation.py diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index bd0e77e427..064ba00f08 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -6,6 +6,7 @@ from pyblish.api import Instance from maya import cmds # noqa import maya.mel as mel # noqa +from openpype.hosts.maya.api.lib import maintained_selection class FBXExtractor: @@ -63,8 +64,8 @@ class FBXExtractor: "embeddedTextures": bool, "inputConnections": bool, "upAxis": str, # x, y or z, - "referencedAssetsContent": bool, - "triangulate": bool + "triangulate": bool, + "exportFileVersion": str } @property @@ -105,8 +106,8 @@ class FBXExtractor: "embeddedTextures": False, "inputConnections": True, "upAxis": "y", - "referencedAssetsContent": False, "triangulate": False, + "exportFileVersion": "FBX201000" } def __init__(self, log=None): @@ -200,5 +201,6 @@ class FBXExtractor: path (str): Path to use for export. """ - cmds.select(members, r=True, noExpand=True) - mel.eval('FBXExport -f "{}" -s'.format(path)) + with maintained_selection(): + cmds.select(members, r=True, noExpand=True) + mel.eval('FBXExport -f "{}" -s'.format(path)) diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 030aa23a22..b4ff6fad07 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -13,7 +13,6 @@ class CreateRig(plugin.MayaCreator): icon = "wheelchair" def create(self, subset_name, instance_data, pre_create_data): - instance_data["fbx_enabled"] = pre_create_data.get("fbx_enabled") instance = super(CreateRig, self).create(subset_name, instance_data, @@ -22,26 +21,13 @@ class CreateRig(plugin.MayaCreator): instance_node = instance.get("instance_node") self.log.info("Creating Rig instance set up ...") - # change name + # change name (_controls_set -> _rigs_SET) controls = cmds.sets(name=subset_name + "_controls_SET", empty=True) - # change name + # change name (_out_SET -> _geo_SET) pointcache = cmds.sets(name=subset_name + "_out_SET", empty=True) - if pre_create_data.get("fbx_enabled"): - skeleton = cmds.sets( - name=subset_name + "_skeleton_SET", empty=True) - skeleton_mesh = cmds.sets( - name=subset_name + "_skeletonMesh_SET", empty=True) - cmds.sets([controls, pointcache, - skeleton, skeleton_mesh], forceElement=instance_node) - else: - cmds.sets([controls, pointcache], forceElement=instance_node) - - def get_pre_create_attr_defs(self): - attrs = super(CreateRig, self).get_pre_create_attr_defs() - - return attrs + [ - BoolDef("fbx_enabled", - label="Fbx Export", - default=False), - - ] + skeleton = cmds.sets( + name=subset_name + "skeletonAnim_SET", empty=True) + skeleton_mesh = cmds.sets( + name=subset_name + "_skeletonMesh_SET", empty=True) + cmds.sets([controls, pointcache, + skeleton, skeleton_mesh], forceElement=instance_node) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py new file mode 100644 index 0000000000..e1b2fc0b7b --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +from maya import cmds # noqa +import pyblish.api + + +class CollectFbxAnimation(pyblish.api.InstancePlugin): + """Collect Unreal Skeletal Mesh.""" + + order = pyblish.api.CollectorOrder + 0.2 + label = "Collect Fbx Animation" + families = ["rig"] + + def process(self, instance): + frame = cmds.currentTime(query=True) + instance.data["frameStart"] = frame + instance.data["frameEnd"] = frame + + skeleton_sets = [ + i for i in instance[:] + if i.lower().endswith("skeletonanim_set") + ] + if skeleton_sets: + for skeleton_set in skeleton_sets: + skeleton_content = cmds.ls( + cmds.sets(skeleton_set, query=True), long=True) + if skeleton_content: + instance.data["animated_skeleton"] += skeleton_content diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py index bef43aa5f4..6ade7451d6 100644 --- a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py +++ b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py @@ -11,16 +11,12 @@ class CollectRigFbx(pyblish.api.InstancePlugin): families = ["rig"] def process(self, instance): - if not instance.data.get("fbx_enabled"): - self.log.debug("Skipping collecting rig data for fbx..") - return - frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame skeleton_sets = [ i for i in instance[:] - if i.lower().endswith("skeleton_set") + if i.lower().endswith("skeletonanim_set") ] skeleton_mesh_sets = [ @@ -28,14 +24,12 @@ class CollectRigFbx(pyblish.api.InstancePlugin): if i.lower().endswith("skeletonmesh_set") ] if skeleton_sets or skeleton_mesh_sets: - instance.data["geometries"] = [] - instance.data["control_rigs"] = [] instance.data["skeleton_mesh"] = [] for skeleton_set in skeleton_sets: skeleton_content = cmds.ls( cmds.sets(skeleton_set, query=True), long=True) if skeleton_content: - instance.data["control_rigs"] += skeleton_content + instance.data["animated_rigs"] += skeleton_content for skeleton_mesh_set in skeleton_mesh_sets: skeleton_mesh_content = cmds.ls( diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py new file mode 100644 index 0000000000..111a202f82 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +import os + +from maya import cmds # noqa +import maya.mel as mel # noqa +import pyblish.api + +from openpype.pipeline import publish +from openpype.pipeline.publish import OptionalPyblishPluginMixin +from openpype.hosts.maya.api import fbx + + +class ExtractRigFBX(publish.Extractor, + OptionalPyblishPluginMixin): + """Extract Rig in FBX format from Maya. + + This extracts the rig in fbx with the constraints + and referenced asset content included. + This also optionally extract animated rig in fbx with + geometries included. + + """ + order = pyblish.api.ExtractorOrder + label = "Extract Animation (FBX)" + families = ["animation"] + + def process(self, instance): + if not self.is_active(instance.data): + return + # Define output path + staging_dir = self.staging_dir(instance) + filename = "{0}.fbx".format(instance.name) + path = os.path.join(staging_dir, filename) + + # The export requires forward slashes because we need + # to format it into a string in a mel expression + fbx_exporter = fbx.FBXExtractor(log=self.log) + out_set = instance.data.get("animated_skeleton", []) + + instance.data["constraints"] = True + instance.data["animationOnly"] = True + + fbx_exporter.set_options_from_instance(instance) + + # Export + fbx_exporter.export(out_set, path) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": staging_dir, + "outputName": "fbxanim" + } + instance.data["representations"].append(representation) + + self.log.debug("Extract animated FBX successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py index 687b686fb8..2aa02a21c3 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py @@ -6,11 +6,12 @@ import maya.mel as mel # noqa import pyblish.api from openpype.pipeline import publish -from openpype.hosts.maya.api.lib import maintained_selection +from openpype.pipeline.publish import OptionalPyblishPluginMixin from openpype.hosts.maya.api import fbx -class ExtractRigFBX(publish.Extractor): +class ExtractRigFBX(publish.Extractor, + OptionalPyblishPluginMixin): """Extract Rig in FBX format from Maya. This extracts the rig in fbx with the constraints @@ -24,11 +25,8 @@ class ExtractRigFBX(publish.Extractor): families = ["rig"] def process(self, instance): - if not instance.data.get("fbx_enabled"): - self.log.debug("fbx extractor has been disable.." - "Skipping the action...") + if not self.is_active(instance.data): return - # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) @@ -36,14 +34,15 @@ class ExtractRigFBX(publish.Extractor): # The export requires forward slashes because we need # to format it into a string in a mel expression - path = path.replace('\\', '/') + fbx_exporter = fbx.FBXExtractor(log=self.log) + out_set = instance.data.get("skeleton_mesh", []) - self.log.debug("Extracting FBX to: {0}".format(path)) + instance.data["constraints"] = True - control_rigs = instance.data.get("control_rigs", []) - skeletal_mesh = instance.data.get("skeleton_mesh", []) - members = control_rigs + skeletal_mesh - self._to_extract(instance, path, members) + fbx_exporter.set_options_from_instance(instance) + + # Export + fbx_exporter.export(out_set, path) if "representations" not in instance.data: instance.data["representations"] = [] @@ -57,36 +56,3 @@ class ExtractRigFBX(publish.Extractor): instance.data["representations"].append(representation) self.log.debug("Extract FBX successful to: {0}".format(path)) - if skeletal_mesh: - self._to_extract(instance, path, skeletal_mesh) - representation = { - 'name': 'fbxanim', - 'ext': 'fbx', - 'files': filename, - "stagingDir": staging_dir, - "outputName": "fbxanim" - } - instance.data["representations"].append(representation) - self.log.debug( - "Extract animated FBX successful to: {0}".format(path)) - - def _to_extract(self, instance, path, members): - fbx_exporter = fbx.FBXExtractor(log=self.log) - control_rigs = instance.data.get("control_rigs", []) - skeletal_mesh = instance.data.get("skeleton_mesh", []) - static_sets = control_rigs + skeletal_mesh - if members == static_sets: - instance.data["constraints"] = True - instance.data["referencedAssetsContent"] = True - if members == skeletal_mesh: - instance.data["constraints"] = True - instance.data["referencedAssetsContent"] = True - instance.data["animationOnly"] = True - - fbx_exporter.set_options_from_instance(instance) - - # Export - with maintained_selection(): - fbx_exporter.export(members, path) - cmds.select(members, r=1, noExpand=True) - mel.eval('FBXExport -f "{}" -s'.format(path)) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index 7b5392f8f9..21d5097fd2 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -80,6 +80,9 @@ class ValidateRigContents(pyblish.api.InstancePlugin): % invalid_geometry) error = True + invalid = self.validate_skeleton_sets(instance) + if invalid: + error = True if error: raise PublishValidationError( "Invalid rig content. See log for details.") @@ -91,7 +94,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): Checks if the node types of the set members valid Args: - set_members: list of nodes of the controls_set + set_members: list of nodes of the controls_SET hierarchy: list of nodes which reside under the root node Returns: @@ -118,7 +121,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): Checks if the node types of the set members valid Args: - set_members: list of nodes of the controls_set + set_members: list of nodes of the controls_SET hierarchy: list of nodes which reside under the root node Returns: @@ -132,3 +135,62 @@ class ValidateRigContents(pyblish.api.InstancePlugin): invalid.append(node) return invalid + + + def validate_skeleton_sets(self, instance): + objectsets = ("skeletonAnim_SET", "skeletonMesh_SET") + missing = [obj for obj in objectsets if obj not in instance] + if missing: + self.log.debug("%s is missing %s" % (instance, missing)) + + # Ensure there are at least some transforms or dag nodes + # in the rig instance + set_members = instance.data['setMembers'] + if not cmds.ls(set_members, type="dagNode", long=True): + self.log.debug("Skipping empty instance...") + return + # Ensure contents in sets and retrieve long path for all objects + output_content = cmds.sets( + "skeletonMesh_SET", query=True) or [] + output_content = cmds.ls(output_content, long=True) + + controls_content = cmds.sets( + "skeletonAnim_SET", query=True) or [] + controls_content = cmds.ls(controls_content, long=True) + + # Validate members are inside the hierarchy from root node + root_node = cmds.ls(set_members, assemblies=True) + hierarchy = cmds.listRelatives(root_node, allDescendents=True, + fullPath=True) + hierarchy = set(hierarchy) + + invalid_hierarchy = [] + if output_content: + for node in output_content: + if node not in hierarchy: + invalid_hierarchy.append(node) + invalid_geometry = self.validate_geometry(output_content) + if controls_content: + for node in controls_content: + if node not in hierarchy: + invalid_hierarchy.append(node) + invalid_controls = self.validate_controls(controls_content) + + error = False + if invalid_hierarchy: + self.log.error("Found nodes which reside outside of root group " + "while they are set up for publishing." + "\n%s" % invalid_hierarchy) + error = True + + if invalid_controls: + self.log.error("Only transforms can be part of the controls_SET." + "\n%s" % invalid_controls) + error = True + + if invalid_geometry: + self.log.error("Only meshes can be part of the out_SET\n%s" + % invalid_geometry) + error = True + + return error diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py index 7bbf4257ab..ae9d9b51d2 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py @@ -61,7 +61,10 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): controllers_sets = [i for i in instance if i == "controls_SET"] controls = cmds.sets(controllers_sets, query=True) assert controls, "Must have 'controls_SET' in rig instance" - + skeletonAnim_sets = [i for i in instance if i == "skeletonAnim_SET"] + if skeletonAnim_sets: + skeleton_controls = cmds.sets(skeletonAnim_sets, query=True) + controls += skeleton_controls # Ensure all controls are within the top group lookup = set(instance[:]) assert all(control in lookup for control in cmds.ls(controls, @@ -184,6 +187,9 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): # Use a single undo chunk with undo_chunk(): controls = cmds.sets("controls_SET", query=True) + anim_skeleton = cmds.sets("skeletonAnim_SET", query=True) + if anim_skeleton: + controls = controls + anim_skeleton for control in controls: # Lock visibility diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py index 842c1de01b..eae75089fc 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py @@ -56,7 +56,8 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - controllers_sets = [i for i in instance if i == "controls_SET"] + controllers_sets = [i for i in instance + if i == "controls_SET"] if not controllers_sets: return [] diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index 39f0941faa..05d7bfad64 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -47,7 +47,21 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): invalid = [] - out_set = next(x for x in instance if x.endswith("out_SET")) + out_set_invalid = cls.get_invalid_not_by_sets(instance) + if out_set_invalid: + invalid += out_set_invalid + + skeletonmesh_invalid = cls.get_invalid_not_by_sets( + instance, set_name="skeletonMesh_SET") + if skeletonmesh_invalid: + invalid += skeletonmesh_invalid + + return invalid + + @classmethod + def get_invalid_not_by_sets(cls, instance, set_name="out_SET"): + invalid = [] + out_set = next(x for x in instance if x.endswith(set_name)) members = cmds.sets(out_set, query=True) shapes = cmds.ls(members, dag=True, @@ -55,7 +69,8 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): shapes=True, long=True, noIntermediate=True) - + if not shapes: + return for shape in shapes: sibling_id = lib.get_id_from_sibling( shape, diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index cbc750bace..9e81b1223a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -40,17 +40,23 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance, compute=False): invalid_matches = cls.get_invalid_matches(instance, compute=compute) + + invalid_skeleton_matches = cls.get_invalid_matches( + instance, compute=compute, set_name="skeletonMesh_SET") + invalid_matches.update(invalid_skeleton_matches) return list(invalid_matches.keys()) @classmethod - def get_invalid_matches(cls, instance, compute=False): + def get_invalid_matches(cls, instance, compute=False, set_name="out_SET"): invalid = {} if compute: - out_set = next(x for x in instance if "out_SET" in x) + out_set = next(x for x in instance if set_name in x) instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True) instance_nodes = cmds.ls(instance_nodes, long=True) + if not instance_nodes: + return for node in instance_nodes: shapes = cmds.listRelatives(node, shapes=True, fullPath=True) if shapes: From 2e36f7fc4723d27bbfcc40021a1e4b55051c3166 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 7 Sep 2023 20:48:19 +0800 Subject: [PATCH 045/186] hound --- openpype/hosts/maya/plugins/create/create_rig.py | 1 - openpype/hosts/maya/plugins/publish/validate_rig_contents.py | 1 - 2 files changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index b4ff6fad07..459fbdab56 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -1,7 +1,6 @@ from maya import cmds from openpype.hosts.maya.api import plugin -from openpype.lib import BoolDef class CreateRig(plugin.MayaCreator): diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index 21d5097fd2..276c22977e 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -136,7 +136,6 @@ class ValidateRigContents(pyblish.api.InstancePlugin): return invalid - def validate_skeleton_sets(self, instance): objectsets = ("skeletonAnim_SET", "skeletonMesh_SET") missing = [obj for obj in objectsets if obj not in instance] From 9bbd457541071b729b048aea6215ce6f35448c0e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Sep 2023 17:16:33 +0800 Subject: [PATCH 046/186] big roy's comment on separating validators of skeleton set from the mandatory set --- .../plugins/publish/validate_rig_contents.py | 67 +----- .../publish/validate_rig_controllers.py | 24 +- ...idate_rig_controllers_arnold_attributes.py | 7 - .../publish/validate_rig_out_set_node_ids.py | 7 +- .../publish/validate_rig_output_ids.py | 19 +- .../publish/validate_skeleton_rig_content.py | 138 +++++++++++ .../validate_skeleton_rig_controller.py | 222 ++++++++++++++++++ .../validate_skeleton_rig_out_set_node_ids.py | 90 +++++++ .../validate_skeleton_rig_output_ids.py | 126 ++++++++++ 9 files changed, 581 insertions(+), 119 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py create mode 100644 openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py create mode 100644 openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py create mode 100644 openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index ad8a0a23c2..23f031a5db 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -96,9 +96,6 @@ class ValidateRigContents(pyblish.api.InstancePlugin): % invalid_geometry) error = True - invalid = self.validate_skeleton_sets(instance) - if invalid: - error = True if error: raise PublishValidationError( "Invalid rig content. See log for details.") @@ -110,7 +107,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): Checks if the node types of the set members valid Args: - set_members: list of nodes of the controls_SET + set_members: list of nodes of the controls_set hierarchy: list of nodes which reside under the root node Returns: @@ -137,7 +134,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): Checks if the node types of the set members valid Args: - set_members: list of nodes of the controls_SET + set_members: list of nodes of the controls_set hierarchy: list of nodes which reside under the root node Returns: @@ -151,63 +148,3 @@ class ValidateRigContents(pyblish.api.InstancePlugin): invalid.append(node) return invalid - - def validate_skeleton_sets(self, instance): - objectsets = ["skeletonAnim_SET", "skeletonMesh_SET"] - missing = [obj for obj in objectsets if obj not in instance] - if missing: - self.log.debug("%s is missing %s" % (instance, missing)) - - controls_set = instance.data["rig_sets"]["skeletonAnim_SET"] - out_set = instance.data["rig_sets"]["skeletonMesh_SET"] - # Ensure there are at least some transforms or dag nodes - # in the rig instance - set_members = instance.data['setMembers'] - if not cmds.ls(set_members, type="dagNode", long=True): - self.log.debug("Skipping empty instance...") - return - # Ensure contents in sets and retrieve long path for all objects - output_content = cmds.sets( - out_set, query=True) or [] - output_content = cmds.ls(output_content, long=True) - - controls_content = cmds.sets( - controls_set, query=True) or [] - controls_content = cmds.ls(controls_content, long=True) - - # Validate members are inside the hierarchy from root node - root_node = cmds.ls(set_members, assemblies=True) - hierarchy = cmds.listRelatives(root_node, allDescendents=True, - fullPath=True) - hierarchy = set(hierarchy) - - invalid_hierarchy = [] - if output_content: - for node in output_content: - if node not in hierarchy: - invalid_hierarchy.append(node) - invalid_geometry = self.validate_geometry(output_content) - if controls_content: - for node in controls_content: - if node not in hierarchy: - invalid_hierarchy.append(node) - invalid_controls = self.validate_controls(controls_content) - - error = False - if invalid_hierarchy: - self.log.error("Found nodes which reside outside of root group " - "while they are set up for publishing." - "\n%s" % invalid_hierarchy) - error = True - - if invalid_controls: - self.log.error("Only transforms can be part of the controls_SET." - "\n%s" % invalid_controls) - error = True - - if invalid_geometry: - self.log.error("Only meshes can be part of the out_SET\n%s" - % invalid_geometry) - error = True - - return error diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py index 266d98a433..a3828f871b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py @@ -76,20 +76,7 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): "All controls must be inside the rig's group." ) return [controls_set] - skeleton_set = instance.data["rig_sets"].get("skeletonAnim_SET") - if not skeleton_set: - cls.log.info( - "No 'skeletonAnim_SET' in rig instance" - ) - skeleton_controls = cmds.sets(skeleton_set, query=True) - if not all(control in lookup for control in cmds.ls(skeleton_controls, - long=True)): - cls.log.error( - "All controls must be inside the rig's group." - ) - return [skeleton_controls] - controls += skeleton_controls # Validate all controls has_connections = list() has_unlocked_visibility = list() @@ -209,19 +196,10 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): "instance: {}".format(instance) ) return - skeleton_set = instance.data["rig_sets"].get("skeletonAnim_SET") - if not skeleton_set: - cls.log.error( - "Unable to repair because no 'skeletonAnim_SET' found in rig " - "instance: {}".format(instance) - ) - return + # Use a single undo chunk with undo_chunk(): controls = cmds.sets(controls_set, query=True) - if skeleton_set: - skeleton_controls = cmds.sets(skeleton_set, query=True) - controls += skeleton_controls for control in controls: # Lock visibility diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py index ec7fecf78a..03f6a5f1ab 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers_arnold_attributes.py @@ -63,13 +63,6 @@ class ValidateRigControllersArnoldAttributes(pyblish.api.InstancePlugin): controls = cmds.sets(controls_set, query=True) or [] if not controls: return [] - skeleton_set = instance.data["rig_sets"].get("skeletonAnim_SET") - if not skeleton_set: - return [] - - skeleton_controls = cmds.sets(skeleton_set, query=True) or [] - if skeleton_controls: - controls += skeleton_controls shapes = cmds.ls(controls, dag=True, diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index a0d477b698..fbd510c683 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -49,10 +49,6 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): out_set = instance.data["rig_sets"].get("out_SET") if not out_set: return [] - skeletonMesh_set = instance.data["rig_sets"].get( - "skeletonMesh_SET") - if skeletonMesh_set: - out_set += skeletonMesh_set invalid = [] members = cmds.sets(out_set, query=True) @@ -62,8 +58,7 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): shapes=True, long=True, noIntermediate=True) - if not shapes: - return + for shape in shapes: sibling_id = lib.get_id_from_sibling( shape, diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index 5b3566a115..24fb36eb8b 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -40,14 +40,10 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance, compute=False): invalid_matches = cls.get_invalid_matches(instance, compute=compute) - - invalid_skeleton_matches = cls.get_invalid_matches( - instance, compute=compute, set_name="skeletonMesh_SET") - invalid_matches.update(invalid_skeleton_matches) return list(invalid_matches.keys()) @classmethod - def get_invalid_matches(cls, instance, compute=False, set_name="out_SET"): + def get_invalid_matches(cls, instance, compute=False): invalid = {} if compute: @@ -57,20 +53,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): return invalid instance_nodes = cmds.sets(out_set, query=True, nodesOnly=True) - - skeletonMesh_set = instance.data["rig_sets"].get( - "skeletonMesh_SET") - if not skeletonMesh_set: - instance.data["mismatched_output_ids"] = invalid - return invalid - else: - skeletonMesh_nodes = cmds.sets( - skeletonMesh_set, query=True, nodesOnly=True) - instance_nodes += skeletonMesh_nodes - instance_nodes = cmds.ls(instance_nodes, long=True) - if not instance_nodes: - return for node in instance_nodes: shapes = cmds.listRelatives(node, shapes=True, fullPath=True) if shapes: diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py new file mode 100644 index 0000000000..8e0a998a1d --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -0,0 +1,138 @@ +import pyblish.api +from maya import cmds + +from openpype.pipeline.publish import ( + PublishValidationError, + ValidateContentsOrder +) + + +class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): + """Ensure skeleton rigs contains pipeline-critical content + + The rigs optionally contain at least two object sets: + "skeletonAnim_SET" - Set of only bone hierarchies + "skeletonMesh_SET" - Set of all cacheable meshes + + """ + + order = ValidateContentsOrder + label = "Rig Contents" + hosts = ["maya"] + families = ["rig"] + + accepted_output = ["mesh", "transform"] + accepted_controllers = ["transform"] + + def process(self, instance): + + objectsets = ["skeletonAnim_SET", "skeletonMesh_SET"] + missing = [obj for obj in objectsets if obj not in instance] + if missing: + self.log.debug("%s is missing %s" % (instance, missing)) + return + + controls_set = instance.data["rig_sets"]["skeletonAnim_SET"] + out_set = instance.data["rig_sets"]["skeletonMesh_SET"] + # Ensure there are at least some transforms or dag nodes + # in the rig instance + set_members = instance.data['setMembers'] + if not cmds.ls(set_members, type="dagNode", long=True): + self.log.debug("Skipping empty instance...") + return + # Ensure contents in sets and retrieve long path for all objects + output_content = cmds.sets( + out_set, query=True) or [] + output_content = cmds.ls(output_content, long=True) + + controls_content = cmds.sets( + controls_set, query=True) or [] + controls_content = cmds.ls(controls_content, long=True) + + # Validate members are inside the hierarchy from root node + root_node = cmds.ls(set_members, assemblies=True) + hierarchy = cmds.listRelatives(root_node, allDescendents=True, + fullPath=True) + hierarchy = set(hierarchy) + + invalid_hierarchy = [] + if output_content: + for node in output_content: + if node not in hierarchy: + invalid_hierarchy.append(node) + invalid_geometry = self.validate_geometry(output_content) + if controls_content: + for node in controls_content: + if node not in hierarchy: + invalid_hierarchy.append(node) + invalid_controls = self.validate_controls(controls_content) + + error = False + if invalid_hierarchy: + self.log.error("Found nodes which reside outside of root group " + "while they are set up for publishing." + "\n%s" % invalid_hierarchy) + error = True + + if invalid_controls: + self.log.error("Only transforms can be part of the skeletonAnim_SET." + "\n%s" % invalid_controls) + error = True + + if invalid_geometry: + self.log.error("Only meshes can be part of the skeletonMesh_SET\n%s" + % invalid_geometry) + error = True + + if error: + raise PublishValidationError( + "Invalid rig content. See log for details.") + + def validate_geometry(self, set_members): + """Check if the out set passes the validations + + Checks if all its set members are within the hierarchy of the root + Checks if the node types of the set members valid + + Args: + set_members: list of nodes of the controls_SET + hierarchy: list of nodes which reside under the root node + + Returns: + errors (list) + """ + + # Validate all shape types + invalid = [] + shapes = cmds.listRelatives(set_members, + allDescendents=True, + shapes=True, + fullPath=True) or [] + all_shapes = cmds.ls(set_members + shapes, long=True, shapes=True) + for shape in all_shapes: + if cmds.nodeType(shape) not in self.accepted_output: + invalid.append(shape) + + return invalid + + def validate_controls(self, set_members): + """Check if the controller set passes the validations + + Checks if all its set members are within the hierarchy of the root + Checks if the node types of the set members valid + + Args: + set_members: list of nodes of the controls_SET + hierarchy: list of nodes which reside under the root node + + Returns: + errors (list) + """ + + # Validate control types + invalid = [] + for node in set_members: + if cmds.nodeType(node) not in self.accepted_controllers: + invalid.append(node) + + return invalid diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py new file mode 100644 index 0000000000..82e0d542ca --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py @@ -0,0 +1,222 @@ +from maya import cmds + +import pyblish.api + +from openpype.pipeline.publish import ( + ValidateContentsOrder, + RepairAction, + PublishValidationError +) +import openpype.hosts.maya.api.action +from openpype.hosts.maya.api.lib import undo_chunk + + +class ValidateSkeletonRigControllers(pyblish.api.InstancePlugin): + """Validate rig controller for skeletonAnim_SET + + Controls must have the transformation attributes on their default + values of translate zero, rotate zero and scale one when they are + unlocked attributes. + + Unlocked keyable attributes may not have any incoming connections. If + these connections are required for the rig then lock the attributes. + + The visibility attribute must be locked. + + Note that `repair` will: + - Lock all visibility attributes + - Reset all default values for translate, rotate, scale + - Break all incoming connections to keyable attributes + + """ + order = ValidateContentsOrder + 0.05 + label = "Rig Controllers" + hosts = ["maya"] + families = ["rig"] + actions = [RepairAction, + openpype.hosts.maya.api.action.SelectInvalidAction] + + # Default controller values + CONTROLLER_DEFAULTS = { + "translateX": 0, + "translateY": 0, + "translateZ": 0, + "rotateX": 0, + "rotateY": 0, + "rotateZ": 0, + "scaleX": 1, + "scaleY": 1, + "scaleZ": 1 + } + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + '{} failed, see log information'.format(self.label) + ) + + @classmethod + def get_invalid(cls, instance): + skeleton_set = instance.data["rig_sets"].get("skeletonAnim_SET") + if not skeleton_set: + cls.log.info( + "No 'skeletonAnim_SET' in rig instance" + ) + return + controls = cmds.sets(skeleton_set, query=True) + lookup = set(instance[:]) + if not all(control in lookup for control in cmds.ls(controls, + long=True)): + cls.log.error( + "All controls must be inside the rig's group." + ) + return [controls] + # Validate all controls + has_connections = list() + has_unlocked_visibility = list() + has_non_default_values = list() + for control in controls: + if cls.get_connected_attributes(control): + has_connections.append(control) + + # check if visibility is locked + attribute = "{}.visibility".format(control) + locked = cmds.getAttr(attribute, lock=True) + if not locked: + has_unlocked_visibility.append(control) + + if cls.get_non_default_attributes(control): + has_non_default_values.append(control) + + if has_connections: + cls.log.error("Controls have input connections: " + "%s" % has_connections) + + if has_non_default_values: + cls.log.error("Controls have non-default values: " + "%s" % has_non_default_values) + + if has_unlocked_visibility: + cls.log.error("Controls have unlocked visibility " + "attribute: %s" % has_unlocked_visibility) + + invalid = [] + if (has_connections or + has_unlocked_visibility or + has_non_default_values): + invalid = set() + invalid.update(has_connections) + invalid.update(has_non_default_values) + invalid.update(has_unlocked_visibility) + invalid = list(invalid) + cls.log.error("Invalid rig controllers. See log for details.") + + return invalid + + @classmethod + def get_non_default_attributes(cls, control): + """Return attribute plugs with non-default values + + Args: + control (str): Name of control node. + + Returns: + list: The invalid plugs + + """ + + invalid = [] + for attr, default in cls.CONTROLLER_DEFAULTS.items(): + if cmds.attributeQuery(attr, node=control, exists=True): + plug = "{}.{}".format(control, attr) + + # Ignore locked attributes + locked = cmds.getAttr(plug, lock=True) + if locked: + continue + + value = cmds.getAttr(plug) + if value != default: + cls.log.warning("Control non-default value: " + "%s = %s" % (plug, value)) + invalid.append(plug) + + return invalid + + @staticmethod + def get_connected_attributes(control): + """Return attribute plugs with incoming connections. + + This will also ensure no (driven) keys on unlocked keyable attributes. + + Args: + control (str): Name of control node. + + Returns: + list: The invalid plugs + + """ + import maya.cmds as mc + + # Support controls without any attributes returning None + attributes = mc.listAttr(control, keyable=True, scalar=True) or [] + invalid = [] + for attr in attributes: + plug = "{}.{}".format(control, attr) + + # Ignore locked attributes + locked = cmds.getAttr(plug, lock=True) + if locked: + continue + + # Ignore proxy connections. + if (cmds.addAttr(plug, query=True, exists=True) and + cmds.addAttr(plug, query=True, usedAsProxy=True)): + continue + + # Check for incoming connections + if cmds.listConnections(plug, source=True, destination=False): + invalid.append(plug) + + return invalid + + @classmethod + def repair(cls, instance): + skeleton_set = instance.data["rig_sets"].get("skeletonAnim_SET") + if not skeleton_set: + cls.log.error( + "Unable to repair because no 'skeletonAnim_SET' found in rig " + "instance: {}".format(instance) + ) + return + # Use a single undo chunk + with undo_chunk(): + controls = cmds.sets(skeleton_set, query=True) + for control in controls: + # Lock visibility + attr = "{}.visibility".format(control) + locked = cmds.getAttr(attr, lock=True) + if not locked: + cls.log.info("Locking visibility for %s" % control) + cmds.setAttr(attr, lock=True) + + # Remove incoming connections + invalid_plugs = cls.get_connected_attributes(control) + if invalid_plugs: + for plug in invalid_plugs: + cls.log.info("Breaking input connection to %s" % plug) + source = cmds.listConnections(plug, + source=True, + destination=False, + plugs=True)[0] + cmds.disconnectAttr(source, plug) + + # Reset non-default values + invalid_plugs = cls.get_non_default_attributes(control) + if invalid_plugs: + for plug in invalid_plugs: + attr = plug.split(".")[-1] + default = cls.CONTROLLER_DEFAULTS[attr] + cls.log.info("Setting %s to %s" % (plug, default)) + cmds.setAttr(plug, default) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py new file mode 100644 index 0000000000..b682c8e953 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py @@ -0,0 +1,90 @@ +import maya.cmds as cmds + +import pyblish.api + +import openpype.hosts.maya.api.action +from openpype.hosts.maya.api import lib +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError +) + + +class ValidateSkeletonRigOutSetNodeIds(pyblish.api.InstancePlugin): + """Validate if deformed shapes have related IDs to the original shapes + from skeleton set. + + When a deformer is applied in the scene on a referenced mesh that already + had deformers then Maya will create a new shape node for the mesh that + does not have the original id. This validator checks whether the ids are + valid on all the shape nodes in the instance. + + """ + + order = ValidateContentsOrder + families = ["rig"] + hosts = ['maya'] + label = 'Rig Out Set Node Ids' + actions = [ + openpype.hosts.maya.api.action.SelectInvalidAction, + RepairAction + ] + allow_history_only = False + + def process(self, instance): + """Process all meshes""" + + # Ensure all nodes have a cbId and a related ID to the original shapes + # if a deformer has been created on the shape + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + "Nodes found with mismatching IDs: {0}".format(invalid) + ) + + @classmethod + def get_invalid(cls, instance): + """Get all nodes which do not match the criteria""" + + skeletonMesh_set = instance.data["rig_sets"].get( + "skeletonMesh_SET") + if not skeletonMesh_set: + return [] + + invalid = [] + members = cmds.sets(skeletonMesh_set, query=True) + shapes = cmds.ls(members, + dag=True, + leaf=True, + shapes=True, + long=True, + noIntermediate=True) + if not shapes: + return + for shape in shapes: + sibling_id = lib.get_id_from_sibling( + shape, + history_only=cls.allow_history_only + ) + if sibling_id: + current_id = lib.get_id(shape) + if current_id != sibling_id: + invalid.append(shape) + + return invalid + + @classmethod + def repair(cls, instance): + + for node in cls.get_invalid(instance): + # Get the original id from sibling + sibling_id = lib.get_id_from_sibling( + node, + history_only=cls.allow_history_only + ) + if not sibling_id: + cls.log.error("Could not find ID in siblings for '%s'", node) + continue + + lib.set_id(node, sibling_id, overwrite=True) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py new file mode 100644 index 0000000000..76f058a94b --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py @@ -0,0 +1,126 @@ +from collections import defaultdict + +from maya import cmds + +import pyblish.api + +import openpype.hosts.maya.api.action +from openpype.hosts.maya.api.lib import get_id, set_id +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError +) + + +def get_basename(node): + """Return node short name without namespace""" + return node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] + + +class ValidateSkeletonRigOutputIds(pyblish.api.InstancePlugin): + """Validate rig output ids from the skeleton sets. + + Ids must share the same id as similarly named nodes in the scene. This is + to ensure the id from the model is preserved through animation. + + """ + order = ValidateContentsOrder + 0.05 + label = "Rig Output Ids" + hosts = ["maya"] + families = ["rig"] + actions = [RepairAction, + openpype.hosts.maya.api.action.SelectInvalidAction] + + def process(self, instance): + invalid = self.get_invalid(instance, compute=True) + if invalid: + raise PublishValidationError("Found nodes with mismatched IDs.") + + @classmethod + def get_invalid(cls, instance, compute=False): + invalid_matches = cls.get_invalid_matches(instance, compute=compute) + + invalid_skeleton_matches = cls.get_invalid_matches( + instance, compute=compute, set_name="skeletonMesh_SET") + invalid_matches.update(invalid_skeleton_matches) + return list(invalid_matches.keys()) + + @classmethod + def get_invalid_matches(cls, instance, compute=False): + invalid = {} + + if compute: + skeletonMesh_set = instance.data["rig_sets"].get( + "skeletonMesh_SET") + if not skeletonMesh_set: + instance.data["mismatched_output_ids"] = invalid + return invalid + + instance_nodes = cmds.sets( + skeletonMesh_set, query=True, nodesOnly=True) + + instance_nodes = cmds.ls(instance_nodes, long=True) + if not instance_nodes: + return + for node in instance_nodes: + shapes = cmds.listRelatives(node, shapes=True, fullPath=True) + if shapes: + instance_nodes.extend(shapes) + + scene_nodes = cmds.ls(type="transform", long=True) + scene_nodes += cmds.ls(type="mesh", long=True) + scene_nodes = set(scene_nodes) - set(instance_nodes) + + scene_nodes_by_basename = defaultdict(list) + for node in scene_nodes: + basename = get_basename(node) + scene_nodes_by_basename[basename].append(node) + + for instance_node in instance_nodes: + basename = get_basename(instance_node) + if basename not in scene_nodes_by_basename: + continue + + matches = scene_nodes_by_basename[basename] + + ids = set(get_id(node) for node in matches) + ids.add(get_id(instance_node)) + + if len(ids) > 1: + cls.log.error( + "\"{}\" id mismatch to: {}".format( + instance_node, matches + ) + ) + invalid[instance_node] = matches + + instance.data["mismatched_output_ids"] = invalid + else: + invalid = instance.data["mismatched_output_ids"] + + return invalid + + @classmethod + def repair(cls, instance): + invalid_matches = cls.get_invalid_matches(instance) + + multiple_ids_match = [] + for instance_node, matches in invalid_matches.items(): + ids = set(get_id(node) for node in matches) + + # If there are multiple scene ids matched, and error needs to be + # raised for manual correction. + if len(ids) > 1: + multiple_ids_match.append({"node": instance_node, + "matches": matches}) + continue + + id_to_set = next(iter(ids)) + set_id(instance_node, id_to_set, overwrite=True) + + if multiple_ids_match: + raise PublishValidationError( + "Multiple matched ids found. Please repair manually: " + "{}".format(multiple_ids_match) + ) From 35e287a57d99bb04aa25ca5f06048b8326cc618e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Sep 2023 17:19:32 +0800 Subject: [PATCH 047/186] big roy's comment --- .../plugins/publish/validate_skeleton_rig_out_set_node_ids.py | 2 +- .../maya/plugins/publish/validate_skeleton_rig_output_ids.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py index b682c8e953..d62bd68b15 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py @@ -61,7 +61,7 @@ class ValidateSkeletonRigOutSetNodeIds(pyblish.api.InstancePlugin): long=True, noIntermediate=True) if not shapes: - return + return [] for shape in shapes: sibling_id = lib.get_id_from_sibling( shape, diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py index 76f058a94b..bea18977f3 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py @@ -62,7 +62,7 @@ class ValidateSkeletonRigOutputIds(pyblish.api.InstancePlugin): instance_nodes = cmds.ls(instance_nodes, long=True) if not instance_nodes: - return + return {} for node in instance_nodes: shapes = cmds.listRelatives(node, shapes=True, fullPath=True) if shapes: From cce6bf2e4299f61118eb5ca479f141e50704be9e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Sep 2023 17:24:01 +0800 Subject: [PATCH 048/186] hound --- .../maya/plugins/publish/validate_skeleton_rig_content.py | 8 ++++---- .../plugins/publish/validate_skeleton_rig_output_ids.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py index 8e0a998a1d..b70d8e6f3f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -75,13 +75,13 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): error = True if invalid_controls: - self.log.error("Only transforms can be part of the skeletonAnim_SET." - "\n%s" % invalid_controls) + self.log.error("Only transforms can be part of the " + "skeletonAnim_SET. \n%s" % invalid_controls) error = True if invalid_geometry: - self.log.error("Only meshes can be part of the skeletonMesh_SET\n%s" - % invalid_geometry) + self.log.error("Only meshes can be part of the " + "skeletonMesh_SET\n%s" % invalid_geometry) error = True if error: diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py index bea18977f3..0b936d35f4 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py @@ -58,7 +58,7 @@ class ValidateSkeletonRigOutputIds(pyblish.api.InstancePlugin): return invalid instance_nodes = cmds.sets( - skeletonMesh_set, query=True, nodesOnly=True) + skeletonMesh_set, query=True, nodesOnly=True) instance_nodes = cmds.ls(instance_nodes, long=True) if not instance_nodes: From cfb4ceb5d41ea59f587555f748da902bae46dc64 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Sep 2023 17:34:45 +0800 Subject: [PATCH 049/186] big roy's comment on rig.fbx families --- .../maya/plugins/publish/collect_fbx_animation.py | 1 + .../maya/plugins/publish/collect_rig_for_fbx.py | 1 + .../maya/plugins/publish/extract_fbx_animation.py | 2 +- .../hosts/maya/plugins/publish/extract_rig_fbx.py | 2 +- .../plugins/publish/validate_skeleton_rig_content.py | 12 ++++++++---- .../publish/validate_skeleton_rig_controller.py | 4 ++-- .../validate_skeleton_rig_out_set_node_ids.py | 4 ++-- .../publish/validate_skeleton_rig_output_ids.py | 4 ++-- openpype/plugins/publish/integrate.py | 1 + 9 files changed, 19 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index e1b2fc0b7b..fb045973b6 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -20,6 +20,7 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin): if i.lower().endswith("skeletonanim_set") ] if skeleton_sets: + instance.data["families"].append("rig.fbx") for skeleton_set in skeleton_sets: skeleton_content = cmds.ls( cmds.sets(skeleton_set, query=True), long=True) diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py index 6ade7451d6..853dcbb259 100644 --- a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py +++ b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py @@ -24,6 +24,7 @@ class CollectRigFbx(pyblish.api.InstancePlugin): if i.lower().endswith("skeletonmesh_set") ] if skeleton_sets or skeleton_mesh_sets: + instance.data["families"].append("rig.fbx") instance.data["skeleton_mesh"] = [] for skeleton_set in skeleton_sets: skeleton_content = cmds.ls( diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 111a202f82..8c540a0101 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -22,7 +22,7 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder label = "Extract Animation (FBX)" - families = ["animation"] + families = ["rig.fbx"] def process(self, instance): if not self.is_active(instance.data): diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py index 2aa02a21c3..ebaf8a83ca 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py @@ -22,7 +22,7 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder label = "Extract Rig (FBX)" - families = ["rig"] + families = ["rig.fbx"] def process(self, instance): if not self.is_active(instance.data): diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py index b70d8e6f3f..0406b00ec6 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -17,9 +17,9 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): """ order = ValidateContentsOrder - label = "Rig Contents" + label = "Skeleton Rig Contents" hosts = ["maya"] - families = ["rig"] + families = ["rig.fbx"] accepted_output = ["mesh", "transform"] accepted_controllers = ["transform"] @@ -27,9 +27,13 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): def process(self, instance): objectsets = ["skeletonAnim_SET", "skeletonMesh_SET"] - missing = [obj for obj in objectsets if obj not in instance] + missing = [ + key for key in objectsets if key not in instance.data["rig_sets"] + ] if missing: - self.log.debug("%s is missing %s" % (instance, missing)) + self.log.debug( + "%s is missing sets: %s" % (instance, ", ".join(missing)) + ) return controls_set = instance.data["rig_sets"]["skeletonAnim_SET"] diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py index 82e0d542ca..a31d13bcec 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py @@ -30,9 +30,9 @@ class ValidateSkeletonRigControllers(pyblish.api.InstancePlugin): """ order = ValidateContentsOrder + 0.05 - label = "Rig Controllers" + label = "Skeleton Rig Controllers" hosts = ["maya"] - families = ["rig"] + families = ["rig.fbx"] actions = [RepairAction, openpype.hosts.maya.api.action.SelectInvalidAction] diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py index d62bd68b15..73ad12f422 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py @@ -23,9 +23,9 @@ class ValidateSkeletonRigOutSetNodeIds(pyblish.api.InstancePlugin): """ order = ValidateContentsOrder - families = ["rig"] + families = ["rig.fbx"] hosts = ['maya'] - label = 'Rig Out Set Node Ids' + label = 'Skeleton Rig Out Set Node Ids' actions = [ openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py index 0b936d35f4..0d1e702749 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py @@ -26,9 +26,9 @@ class ValidateSkeletonRigOutputIds(pyblish.api.InstancePlugin): """ order = ValidateContentsOrder + 0.05 - label = "Rig Output Ids" + label = "Skeleton Rig Output Ids" hosts = ["maya"] - families = ["rig"] + families = ["rig.fbx"] actions = [RepairAction, openpype.hosts.maya.api.action.SelectInvalidAction] diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 7e48155b9e..2e122b652e 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -105,6 +105,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "review", "rendersetup", "rig", + "rig.fbx", "plate", "look", "audio", From 3b6079f74374659a38bfc9b725cabbf26858b05f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 8 Sep 2023 17:58:38 +0800 Subject: [PATCH 050/186] remove rig.fbx --- openpype/plugins/publish/integrate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 2e122b652e..7e48155b9e 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -105,7 +105,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "review", "rendersetup", "rig", - "rig.fbx", "plate", "look", "audio", From 5c3f12d51897b4522e9ce3a364e6aa3c71963a6d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 11 Sep 2023 20:19:40 +0800 Subject: [PATCH 051/186] make the validators optional --- .../defaults/project_settings/maya.json | 20 +++++++++ .../schemas/schema_maya_publish.json | 44 +++++++++++++++++-- .../maya/server/settings/publishers.py | 36 +++++++++++++++ server_addon/maya/server/version.py | 2 +- 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 38f14ec022..2bc226c431 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1140,6 +1140,16 @@ "optional": false, "active": true }, + "ValidateSkeletonRigContents": { + "enabled": false, + "optional": true, + "active": true + }, + "ValidateSkeletonRigControllers": { + "enabled": false, + "optional": true, + "active": true + }, "ValidateSkinclusterDeformerSet": { "enabled": true, "optional": false, @@ -1150,6 +1160,16 @@ "optional": false, "allow_history_only": false }, + "ValidateSkeletonRigOutSetNodeIds": { + "enabled": false, + "optional": false, + "allow_history_only": false + }, + "ValidateSkeletonRigOutputIds": { + "enabled": false, + "optional": true, + "active": true + }, "ValidateCameraAttributes": { "enabled": false, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index b115ee3faa..e8300282d7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -809,9 +809,47 @@ "key": "ValidateSkeletalMeshHierarchy", "label": "Validate Skeletal Mesh Top Node" }, - { + { + "key": "ValidateSkeletonRigContents", + "label": "ValidateSkeleton Rig Contents" + }, + { + "key": "ValidateSkeletonRigControllers", + "label": "Validate Skeleton Rig Controllers" + }, + { "key": "ValidateSkinclusterDeformerSet", "label": "Validate Skincluster Deformer Relationships" + }, + { + "key": "ValidateSkeletonRigOutputIds", + "label": "Validate Skeleton Rig Output Ids" + } + ] + }, + + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "ValidateRigOutSetNodeIds", + "label": "Validate Rig Out Set Node Ids", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "allow_history_only", + "label": "Allow history only" } ] }, @@ -819,8 +857,8 @@ "type": "dict", "collapsible": true, "checkbox_key": "enabled", - "key": "ValidateRigOutSetNodeIds", - "label": "Validate Rig Out Set Node Ids", + "key": "ValidateSkeletonRigOutSetNodeIds", + "label": "Validate Skeleton Rig Out Set Node Ids", "is_group": true, "children": [ { diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index bd7ccdf4d5..6e3179b78e 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -660,14 +660,30 @@ class PublishersModel(BaseSettingsModel): default_factory=BasicValidateModel, title="Validate Skeletal Mesh Top Node", ) + ValidateSkeletonRigContents: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Skeleton Rig Contents" + ) + ValidateSkeletonRigControllers: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Skeleton Rig Controllers" + ) ValidateSkinclusterDeformerSet: BasicValidateModel = Field( default_factory=BasicValidateModel, title="Validate Skincluster Deformer Relationships", ) + ValidateSkeletonRigOutputIds: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Skeleton Rig Output Ids" + ) ValidateRigOutSetNodeIds: ValidateRigOutSetNodeIdsModel = Field( default_factory=ValidateRigOutSetNodeIdsModel, title="Validate Rig Out Set Node Ids", ) + ValidateSkeletonRigOutSetNodeIds: ValidateRigOutSetNodeIdsModel = Field( + default_factory=ValidateRigOutSetNodeIdsModel, + title="Validate Skeleton Rig Out Set Node Ids", + ) # Rig - END ValidateCameraAttributes: BasicValidateModel = Field( default_factory=BasicValidateModel, @@ -1163,6 +1179,16 @@ DEFAULT_PUBLISH_SETTINGS = { "optional": False, "active": True }, + "ValidateSkeletonRigContents": { + "enabled": False, + "optional": True, + "active": True + }, + "ValidateSkeletonRigControllers": { + "enabled": False, + "optional": True, + "active": True + }, "ValidateSkinclusterDeformerSet": { "enabled": True, "optional": False, @@ -1173,6 +1199,16 @@ DEFAULT_PUBLISH_SETTINGS = { "optional": False, "allow_history_only": False }, + "ValidateSkeletonRigOutSetNodeIds": { + "enabled": False, + "optional": False, + "allow_history_only": False + }, + "ValidateSkeletonRigOutputIds": { + "enabled": False, + "optional": True, + "active": True + }, "ValidateCameraAttributes": { "enabled": False, "optional": True, diff --git a/server_addon/maya/server/version.py b/server_addon/maya/server/version.py index e57ad00718..de699158fd 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.3" +__version__ = "0.1.4" From 09e797577b78650eb31e464311c0478564b8f130 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 11 Sep 2023 21:35:58 +0800 Subject: [PATCH 052/186] remove the irrelevant fbx output parameter --- openpype/hosts/maya/api/fbx.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 064ba00f08..000c723d37 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -64,8 +64,7 @@ class FBXExtractor: "embeddedTextures": bool, "inputConnections": bool, "upAxis": str, # x, y or z, - "triangulate": bool, - "exportFileVersion": str + "triangulate": bool } @property @@ -106,8 +105,7 @@ class FBXExtractor: "embeddedTextures": False, "inputConnections": True, "upAxis": "y", - "triangulate": False, - "exportFileVersion": "FBX201000" + "triangulate": False } def __init__(self, log=None): From 5506a89ad644e348be3b8fbb08714496b64f64f2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 11 Sep 2023 21:40:38 +0800 Subject: [PATCH 053/186] wrong family for collect fbx animation --- openpype/hosts/maya/plugins/publish/collect_fbx_animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index fb045973b6..a9a13637f7 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -8,7 +8,7 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.2 label = "Collect Fbx Animation" - families = ["rig"] + families = ["animation"] def process(self, instance): frame = cmds.currentTime(query=True) From 4b27abfae2320e3324cccea63b82a2690097fab5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 11 Sep 2023 15:17:17 +0100 Subject: [PATCH 054/186] Implemented several suggestions from reviews Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Co-authored-by: Oscar Domingo Co-authored-by: Roy Nieterau --- openpype/hosts/blender/api/colorspace.py | 8 ++++---- openpype/hosts/blender/plugins/create/create_render.py | 10 +++------- .../projects_schema/schema_project_blender.json | 2 +- .../blender/server/settings/render_settings.py | 2 +- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/blender/api/colorspace.py b/openpype/hosts/blender/api/colorspace.py index 59deb514f8..0f504a3be0 100644 --- a/openpype/hosts/blender/api/colorspace.py +++ b/openpype/hosts/blender/api/colorspace.py @@ -12,12 +12,12 @@ class LayerMetadata(object): @attr.s class RenderProduct(object): - """Getting Colorspace as - Specific Render Product Parameter for submitting + """ + Getting Colorspace as Specific Render Product Parameter for submitting publish job. """ - colorspace = attr.ib() # colorspace - view = attr.ib() + colorspace = attr.ib() # colorspace + view = attr.ib() # OCIO view transform productName = attr.ib(default=None) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 62700cb55c..fa3cae6cc8 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -3,13 +3,11 @@ import os import bpy +from openpype.settings import get_project_settings from openpype.pipeline import ( get_current_project_name, + get_current_task_name, ) -from openpype.settings import ( - get_project_settings, -) -from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -76,9 +74,7 @@ class CreateRenderlayer(plugin.Creator): instance (pyblish.api.Instance): The instance to publish. ext (str): The image format to render. """ - output_file = os.path.join(output_path, name) - - render_product = f"{output_file}.####" + render_product = f"{os.path.join(output_path, name)}.####" render_product = render_product.replace("\\", "/") return render_product diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 8db57f49eb..a283a2ff5c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -140,7 +140,7 @@ "label": "Type", "type": "enum", "multiselection": false, - "defaults": "color", + "default": "COLOR", "enum_items": [ {"COLOR": "Color"}, {"VALUE": "Value"} diff --git a/server_addon/blender/server/settings/render_settings.py b/server_addon/blender/server/settings/render_settings.py index bef16328d6..7a47095d3c 100644 --- a/server_addon/blender/server/settings/render_settings.py +++ b/server_addon/blender/server/settings/render_settings.py @@ -57,7 +57,7 @@ class CustomPassesModel(BaseSettingsModel): attribute: str = Field("", title="Attribute name") value: str = Field( - "Color", + "COLOR", title="Type", enum_resolver=custom_passes_types_enum ) From 01282f3af797d2c3f879bd91de692965eb25ebdb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 12 Sep 2023 11:47:48 +0100 Subject: [PATCH 055/186] Fix AOVs publish with multilayer EXR --- openpype/hosts/blender/plugins/create/create_render.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index fa3cae6cc8..84387ffb16 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -177,10 +177,15 @@ class CreateRenderlayer(plugin.Creator): # Create a new output node output = tree.nodes.new("CompositorNodeOutputFile") + aov_file_products = [] + if ext == "exr" and multilayer: output.layer_slots.clear() filepath = f"{name}{aov_sep}AOVs.####" output.base_path = os.path.join(output_path, filepath) + + aov_file_products.append( + ("AOVs", os.path.join(output_path, filepath))) else: output.file_slots.clear() output.base_path = output_path @@ -188,8 +193,6 @@ class CreateRenderlayer(plugin.Creator): image_settings = bpy.context.scene.render.image_settings output.format.file_format = image_settings.file_format - aov_file_products = [] - # For each active render pass, we add a new socket to the output node # and link it for render_pass in passes: From 6f23755873fe66e69d549f4551717509a500c75f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 12 Sep 2023 19:48:36 +0800 Subject: [PATCH 056/186] make sure the rig family has published fbx and file version should be 2022 --- openpype/hosts/maya/api/fbx.py | 6 +++-- .../hosts/maya/plugins/create/create_rig.py | 2 +- .../plugins/publish/collect_fbx_animation.py | 8 +++---- .../plugins/publish/collect_rig_for_fbx.py | 23 +++++++++++-------- .../plugins/publish/extract_fbx_animation.py | 8 +++++-- .../maya/plugins/publish/extract_rig_fbx.py | 12 +++++++--- 6 files changed, 37 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 000c723d37..18b28f5154 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -64,7 +64,8 @@ class FBXExtractor: "embeddedTextures": bool, "inputConnections": bool, "upAxis": str, # x, y or z, - "triangulate": bool + "triangulate": bool, + "FileVersion": str } @property @@ -105,7 +106,8 @@ class FBXExtractor: "embeddedTextures": False, "inputConnections": True, "upAxis": "y", - "triangulate": False + "triangulate": False, + "fileVersion": "FBX202000" } def __init__(self, log=None): diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 459fbdab56..69c7787905 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -25,7 +25,7 @@ class CreateRig(plugin.MayaCreator): # change name (_out_SET -> _geo_SET) pointcache = cmds.sets(name=subset_name + "_out_SET", empty=True) skeleton = cmds.sets( - name=subset_name + "skeletonAnim_SET", empty=True) + name=subset_name + "_skeletonAnim_SET", empty=True) skeleton_mesh = cmds.sets( name=subset_name + "_skeletonMesh_SET", empty=True) cmds.sets([controls, pointcache, diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index a9a13637f7..aef838223e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -16,13 +16,13 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin): instance.data["frameEnd"] = frame skeleton_sets = [ - i for i in instance[:] + i for i in instance if i.lower().endswith("skeletonanim_set") ] if skeleton_sets: - instance.data["families"].append("rig.fbx") + instance.data["families"].append("animation.fbx") for skeleton_set in skeleton_sets: - skeleton_content = cmds.ls( - cmds.sets(skeleton_set, query=True), long=True) + skeleton_content = cmds.sets(skeleton_set, query=True) + self.log.debug(f"Collected Animated Skeleton Set: {skeleton_content}") if skeleton_content: instance.data["animated_skeleton"] += skeleton_content diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py index 853dcbb259..fe8d5ca8ef 100644 --- a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py +++ b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py @@ -15,25 +15,28 @@ class CollectRigFbx(pyblish.api.InstancePlugin): instance.data["frameStart"] = frame instance.data["frameEnd"] = frame skeleton_sets = [ - i for i in instance[:] + i for i in instance if i.lower().endswith("skeletonanim_set") ] skeleton_mesh_sets = [ - i for i in instance[:] + i for i in instance if i.lower().endswith("skeletonmesh_set") ] - if skeleton_sets or skeleton_mesh_sets: - instance.data["families"].append("rig.fbx") - instance.data["skeleton_mesh"] = [] + if not skeleton_sets and skeleton_mesh_sets: + self.log.debug("no skeleton_set or skeleton_mesh set was found....") + return + instance.data["skeleton_mesh"] = [] + if skeleton_sets: for skeleton_set in skeleton_sets: - skeleton_content = cmds.ls( - cmds.sets(skeleton_set, query=True), long=True) + skeleton_content = cmds.sets(skeleton_set, query=True) if skeleton_content: instance.data["animated_rigs"] += skeleton_content - + self.log.debug(f"Collected Skeleton Set: {skeleton_content}") + if skeleton_mesh_sets: + instance.data["families"].append("rig.fbx") for skeleton_mesh_set in skeleton_mesh_sets: - skeleton_mesh_content = cmds.ls( - cmds.sets(skeleton_mesh_set, query=True), long=True) + skeleton_mesh_content = cmds.sets(skeleton_mesh_set, query=True) if skeleton_mesh_content: instance.data["skeleton_mesh"] += skeleton_mesh_content + self.log.debug(f"Collected SkeletonMesh Set: {skeleton_mesh_content}") diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 8c540a0101..2ac4734d21 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -2,7 +2,6 @@ import os from maya import cmds # noqa -import maya.mel as mel # noqa import pyblish.api from openpype.pipeline import publish @@ -22,11 +21,16 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder label = "Extract Animation (FBX)" - families = ["rig.fbx"] + families = ["animation"] def process(self, instance): if not self.is_active(instance.data): return + if "animation.fbx" not in instance.data["families"]: + self.log.debug("No object inside skeleton_set..Skipping...") + return + if not cmds.loadPlugin("fbxmaya", query=True): + cmds.loadPlugin("fbxmaya", quiet=True) # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py index ebaf8a83ca..0df602fa29 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py @@ -2,7 +2,6 @@ import os from maya import cmds # noqa -import maya.mel as mel # noqa import pyblish.api from openpype.pipeline import publish @@ -22,12 +21,17 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder label = "Extract Rig (FBX)" - families = ["rig.fbx"] + families = ["rig"] def process(self, instance): if not self.is_active(instance.data): return - # Define output path + if "rig.fbx" not in instance.data["families"]: + self.log.debug("No object inside skeletonMesh_set..Skipping..") + return + if not cmds.loadPlugin("fbxmaya", query=True): + cmds.loadPlugin("fbxmaya", quiet=True) + staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) @@ -46,6 +50,7 @@ class ExtractRigFBX(publish.Extractor, if "representations" not in instance.data: instance.data["representations"] = [] + self.log.debug("Families: {}".format(instance.data["families"])) representation = { 'name': 'fbx', @@ -54,5 +59,6 @@ class ExtractRigFBX(publish.Extractor, "stagingDir": staging_dir, } instance.data["representations"].append(representation) + self.log.debug("Representation: {}".format(representation)) self.log.debug("Extract FBX successful to: {0}".format(path)) From 8b3e2259be7cd7d308804fd964a337d20c73c961 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 12 Sep 2023 21:34:49 +0800 Subject: [PATCH 057/186] hound --- .../maya/plugins/publish/collect_fbx_animation.py | 4 +++- .../maya/plugins/publish/collect_rig_for_fbx.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index aef838223e..72501dc819 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -23,6 +23,8 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin): instance.data["families"].append("animation.fbx") for skeleton_set in skeleton_sets: skeleton_content = cmds.sets(skeleton_set, query=True) - self.log.debug(f"Collected Animated Skeleton Set: {skeleton_content}") + self.log.debug( + "Collected animated " + f"skeleton data: {skeleton_content}") if skeleton_content: instance.data["animated_skeleton"] += skeleton_content diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py index fe8d5ca8ef..d571975438 100644 --- a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py +++ b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py @@ -24,7 +24,8 @@ class CollectRigFbx(pyblish.api.InstancePlugin): if i.lower().endswith("skeletonmesh_set") ] if not skeleton_sets and skeleton_mesh_sets: - self.log.debug("no skeleton_set or skeleton_mesh set was found....") + self.log.debug( + "no skeleton_set or skeleton_mesh set was found....") return instance.data["skeleton_mesh"] = [] if skeleton_sets: @@ -32,11 +33,14 @@ class CollectRigFbx(pyblish.api.InstancePlugin): skeleton_content = cmds.sets(skeleton_set, query=True) if skeleton_content: instance.data["animated_rigs"] += skeleton_content - self.log.debug(f"Collected Skeleton Set: {skeleton_content}") + self.log.debug(f"Collected skeleton data: {skeleton_content}") if skeleton_mesh_sets: instance.data["families"].append("rig.fbx") for skeleton_mesh_set in skeleton_mesh_sets: - skeleton_mesh_content = cmds.sets(skeleton_mesh_set, query=True) + skeleton_mesh_content = cmds.sets( + skeleton_mesh_set, query=True) if skeleton_mesh_content: instance.data["skeleton_mesh"] += skeleton_mesh_content - self.log.debug(f"Collected SkeletonMesh Set: {skeleton_mesh_content}") + self.log.debug( + "Collected skeleton " + f"mesh Set: {skeleton_mesh_content}") From d949041ad9013e4aa4d02fb0f8e4cd6e540019ed Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 12 Sep 2023 15:10:04 +0100 Subject: [PATCH 058/186] Change behaviour for multilayer exr --- .../blender/plugins/create/create_render.py | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 84387ffb16..1c7f883836 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -177,34 +177,29 @@ class CreateRenderlayer(plugin.Creator): # Create a new output node output = tree.nodes.new("CompositorNodeOutputFile") - aov_file_products = [] - - if ext == "exr" and multilayer: - output.layer_slots.clear() - filepath = f"{name}{aov_sep}AOVs.####" - output.base_path = os.path.join(output_path, filepath) - - aov_file_products.append( - ("AOVs", os.path.join(output_path, filepath))) - else: - output.file_slots.clear() - output.base_path = output_path - image_settings = bpy.context.scene.render.image_settings output.format.file_format = image_settings.file_format + # In case of a multilayer exr, we don't need to use the output node, + # because the blender render already outputs a multilayer exr. + if ext == "exr" and multilayer: + output.layer_slots.clear() + return [] + + output.file_slots.clear() + output.base_path = output_path + + aov_file_products = [] + # For each active render pass, we add a new socket to the output node # and link it for render_pass in passes: - if ext == "exr" and multilayer: - output.layer_slots.new(render_pass.name) - else: - filepath = f"{name}{aov_sep}{render_pass.name}.####" + filepath = f"{name}{aov_sep}{render_pass.name}.####" - output.file_slots.new(filepath) + output.file_slots.new(filepath) - aov_file_products.append( - (render_pass.name, os.path.join(output_path, filepath))) + aov_file_products.append( + (render_pass.name, os.path.join(output_path, filepath))) node_input = output.inputs[-1] From e64984d510d84afb238df73296d8320cb3de2f3a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 13 Sep 2023 13:34:44 +0800 Subject: [PATCH 059/186] update fbx param to support skeleton definition exports and add the optional validators to make sure it's always a top group hierarchy of the rig in the sets --- openpype/hosts/maya/api/fbx.py | 8 +-- .../plugins/publish/collect_rig_for_fbx.py | 3 +- .../plugins/publish/extract_fbx_animation.py | 1 + .../maya/plugins/publish/extract_rig_fbx.py | 3 +- .../validate_skeleton_top_group_hierarchy.py | 49 +++++++++++++++++++ .../schemas/schema_maya_publish.json | 4 ++ .../maya/server/settings/publishers.py | 9 ++++ 7 files changed, 71 insertions(+), 6 deletions(-) create mode 100644 openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 18b28f5154..306b7efe0b 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -40,7 +40,7 @@ class FBXExtractor: the option is not included and a warning is logged. """ - + #TODO: add skeletonDefinition return { "cameras": bool, "smoothingGroups": bool, @@ -65,7 +65,8 @@ class FBXExtractor: "inputConnections": bool, "upAxis": str, # x, y or z, "triangulate": bool, - "FileVersion": str + "FileVersion": str, + "skeletonDefinitions": bool } @property @@ -107,7 +108,8 @@ class FBXExtractor: "inputConnections": True, "upAxis": "y", "triangulate": False, - "fileVersion": "FBX202000" + "fileVersion": "FBX202000", + "skeletonDefinitions": False } def __init__(self, log=None): diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py index d571975438..c9f3fea027 100644 --- a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py +++ b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py @@ -33,7 +33,8 @@ class CollectRigFbx(pyblish.api.InstancePlugin): skeleton_content = cmds.sets(skeleton_set, query=True) if skeleton_content: instance.data["animated_rigs"] += skeleton_content - self.log.debug(f"Collected skeleton data: {skeleton_content}") + self.log.debug("Collected skeleton" + f" data: {skeleton_content}") if skeleton_mesh_sets: instance.data["families"].append("rig.fbx") for skeleton_mesh_set in skeleton_mesh_sets: diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 2ac4734d21..b35cfbc271 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -42,6 +42,7 @@ class ExtractRigFBX(publish.Extractor, out_set = instance.data.get("animated_skeleton", []) instance.data["constraints"] = True + instance.data["skeletonDefinitions"] = True instance.data["animationOnly"] = True fbx_exporter.set_options_from_instance(instance) diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py index 0df602fa29..122cfecf3c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py @@ -42,6 +42,7 @@ class ExtractRigFBX(publish.Extractor, out_set = instance.data.get("skeleton_mesh", []) instance.data["constraints"] = True + instance.data["skeletonDefinitions"] = True fbx_exporter.set_options_from_instance(instance) @@ -50,7 +51,6 @@ class ExtractRigFBX(publish.Extractor, if "representations" not in instance.data: instance.data["representations"] = [] - self.log.debug("Families: {}".format(instance.data["families"])) representation = { 'name': 'fbx', @@ -59,6 +59,5 @@ class ExtractRigFBX(publish.Extractor, "stagingDir": staging_dir, } instance.data["representations"].append(representation) - self.log.debug("Representation: {}".format(representation)) self.log.debug("Extract FBX successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py new file mode 100644 index 0000000000..df434f132d --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +"""Plugin for validating naming conventions.""" +from maya import cmds + +import pyblish.api + +from openpype.pipeline.publish import ( + ValidateContentsOrder, + OptionalPyblishPluginMixin, + PublishValidationError +) + + +class ValidateSkeletonTopGroupHierarchy(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates top group hierarchy in the SETs + Make sure the object inside the SETs are always top + group of the hierarchy + + """ + order = ValidateContentsOrder + 0.05 + label = "Top Group Hierarchy" + families = ["rig"] + + def process(self, instance): + invalid = [] + skeleton_data = instance.data.get(("animated_rigs"), []) + skeletonMesh_data = instance.data(("skeleton_mesh"), []) + if skeleton_data: + invalid = self.get_top_hierarchy(skeleton_data) + if invalid: + raise PublishValidationError( + "The set includes the object which " + f"is not at the top hierarchy: {invalid}") + if skeletonMesh_data: + invalid = self.get_top_hierarchy(skeletonMesh_data) + if invalid: + raise PublishValidationError( + "The set includes the object which " + f"is not at the top hierarchy: {invalid}") + + def get_top_hierarchy(self, targets): + non_top_hierarchy_list = [] + for target in targets: + long_names = cmds.ls(target, long=True) + for name in long_names: + if len(name.split["|"]) > 2: + non_top_hierarchy_list.append(name) + return non_top_hierarchy_list diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index e8300282d7..e5fe367e77 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -824,6 +824,10 @@ { "key": "ValidateSkeletonRigOutputIds", "label": "Validate Skeleton Rig Output Ids" + }, + { + "key": "ValidateSkeletonTopGroupHierarchy", + "label": "Validate Skeleton Top Group Hierarchy" } ] }, diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 6e3179b78e..0c733d9cbc 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -676,6 +676,10 @@ class PublishersModel(BaseSettingsModel): default_factory=BasicValidateModel, title="Validate Skeleton Rig Output Ids" ) + ValidateSkeletonTopGroupHierarchy: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Skeleton Top Group Hierarchy", + ) ValidateRigOutSetNodeIds: ValidateRigOutSetNodeIdsModel = Field( default_factory=ValidateRigOutSetNodeIdsModel, title="Validate Rig Out Set Node Ids", @@ -1209,6 +1213,11 @@ DEFAULT_PUBLISH_SETTINGS = { "optional": True, "active": True }, + "ValidateSkeletonTopGroupHierarchy": { + "enabled": False, + "optional": True, + "active": True + }, "ValidateCameraAttributes": { "enabled": False, "optional": True, From c07e741b7a2a35982da423c017485b3f3687bedc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 13 Sep 2023 13:35:55 +0800 Subject: [PATCH 060/186] hound --- openpype/hosts/maya/api/fbx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 306b7efe0b..9092aaec23 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -40,7 +40,7 @@ class FBXExtractor: the option is not included and a warning is logged. """ - #TODO: add skeletonDefinition + return { "cameras": bool, "smoothingGroups": bool, From 4bbb2e0ba35a902a618f97e1ee3d9cdde5de8135 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 13 Sep 2023 13:40:43 +0800 Subject: [PATCH 061/186] add the validator into maya settings --- openpype/settings/defaults/project_settings/maya.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 2bc226c431..022b906c4f 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1170,6 +1170,11 @@ "optional": true, "active": true }, + "ValidateSkeletonTopGroupHierarchy": { + "enabled": false, + "optional": true, + "active": true + }, "ValidateCameraAttributes": { "enabled": false, "optional": true, From b72d241c2fc0e659fb879224a58883d1f5ba4db2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 13 Sep 2023 14:00:04 +0800 Subject: [PATCH 062/186] bigRoy's comment --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 5 +---- openpype/hosts/maya/plugins/publish/extract_rig_fbx.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index b35cfbc271..cf6cb39628 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -21,14 +21,11 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder label = "Extract Animation (FBX)" - families = ["animation"] + families = ["animation.fbx"] def process(self, instance): if not self.is_active(instance.data): return - if "animation.fbx" not in instance.data["families"]: - self.log.debug("No object inside skeleton_set..Skipping...") - return if not cmds.loadPlugin("fbxmaya", query=True): cmds.loadPlugin("fbxmaya", quiet=True) # Define output path diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py index 122cfecf3c..a81e9deaa1 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py @@ -21,14 +21,11 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder label = "Extract Rig (FBX)" - families = ["rig"] + families = ["rig.fbx"] def process(self, instance): if not self.is_active(instance.data): return - if "rig.fbx" not in instance.data["families"]: - self.log.debug("No object inside skeletonMesh_set..Skipping..") - return if not cmds.loadPlugin("fbxmaya", query=True): cmds.loadPlugin("fbxmaya", quiet=True) From 6d411cdbc952271276843041528951d0f228a7de Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 14 Sep 2023 00:36:18 +0800 Subject: [PATCH 063/186] bug fix on Libor's comment --- .../hosts/maya/plugins/publish/collect_rig_for_fbx.py | 1 + .../hosts/maya/plugins/publish/extract_fbx_animation.py | 7 ++++--- openpype/hosts/maya/plugins/publish/extract_rig_fbx.py | 8 ++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py index c9f3fea027..215a2dd6f3 100644 --- a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py +++ b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py @@ -28,6 +28,7 @@ class CollectRigFbx(pyblish.api.InstancePlugin): "no skeleton_set or skeleton_mesh set was found....") return instance.data["skeleton_mesh"] = [] + instance.data["animated_rigs"] = [] if skeleton_sets: for skeleton_set in skeleton_sets: skeleton_content = cmds.sets(skeleton_set, query=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index cf6cb39628..3c2b76c20d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -21,13 +21,14 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder label = "Extract Animation (FBX)" - families = ["animation.fbx"] + families = ["animation"] def process(self, instance): if not self.is_active(instance.data): return - if not cmds.loadPlugin("fbxmaya", query=True): - cmds.loadPlugin("fbxmaya", quiet=True) + if "animation.fbx" not in instance.data["families"]: + self.log.debug("No object inside skeletonAnim_set..Skipping..") + return # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py index a81e9deaa1..570ef2c267 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py @@ -21,14 +21,14 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder label = "Extract Rig (FBX)" - families = ["rig.fbx"] + families = ["rig"] def process(self, instance): if not self.is_active(instance.data): return - if not cmds.loadPlugin("fbxmaya", query=True): - cmds.loadPlugin("fbxmaya", quiet=True) - + if "rig.fbx" not in instance.data["families"]: + self.log.debug("No object inside skeletonMesh_set..Skipping..") + return staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) From 5f67ffdeb035a249a5528bf82256c023b9a7ae90 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 14 Sep 2023 00:39:04 +0800 Subject: [PATCH 064/186] ondrej's comment on the frame range --- openpype/hosts/maya/plugins/publish/collect_fbx_animation.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index 72501dc819..8a4e7360a8 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -11,10 +11,6 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin): families = ["animation"] def process(self, instance): - frame = cmds.currentTime(query=True) - instance.data["frameStart"] = frame - instance.data["frameEnd"] = frame - skeleton_sets = [ i for i in instance if i.lower().endswith("skeletonanim_set") From a2c72e683e2ac3420931a0721a9f3d12f843db96 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 14 Sep 2023 17:10:46 +0800 Subject: [PATCH 065/186] add maya as hosts --- .../hosts/maya/plugins/publish/collect_fbx_animation.py | 1 + openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py | 1 + .../hosts/maya/plugins/publish/extract_fbx_animation.py | 6 ++---- openpype/hosts/maya/plugins/publish/extract_rig_fbx.py | 7 ++----- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index 8a4e7360a8..9749fb4770 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -8,6 +8,7 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.2 label = "Collect Fbx Animation" + hosts = ["maya"] families = ["animation"] def process(self, instance): diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py index 215a2dd6f3..65653b3369 100644 --- a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py +++ b/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py @@ -8,6 +8,7 @@ class CollectRigFbx(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.2 label = "Collect rig for fbx" + hosts = ["maya"] families = ["rig"] def process(self, instance): diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 3c2b76c20d..1b4b63db87 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -21,14 +21,12 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder label = "Extract Animation (FBX)" - families = ["animation"] + hosts = ["maya"] + families = ["animation.fbx"] def process(self, instance): if not self.is_active(instance.data): return - if "animation.fbx" not in instance.data["families"]: - self.log.debug("No object inside skeletonAnim_set..Skipping..") - return # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py index 570ef2c267..9eecde90e9 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py @@ -21,18 +21,15 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder label = "Extract Rig (FBX)" - families = ["rig"] + hosts = ["maya"] + families = ["rig.fbx"] def process(self, instance): if not self.is_active(instance.data): return - if "rig.fbx" not in instance.data["families"]: - self.log.debug("No object inside skeletonMesh_set..Skipping..") - return staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) - # The export requires forward slashes because we need # to format it into a string in a mel expression fbx_exporter = fbx.FBXExtractor(log=self.log) From 96638726a90896673457dccc91f5bec5fd069ae9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 13:09:32 +0100 Subject: [PATCH 066/186] Produce reviews for the beauty render when publishing --- .../hosts/blender/plugins/create/create_render.py | 11 ++++++----- .../hosts/blender/plugins/publish/collect_render.py | 3 +++ .../deadline/plugins/publish/submit_publish_job.py | 1 + .../settings/defaults/project_settings/deadline.json | 3 +++ .../deadline/server/settings/publish_plugins.py | 6 ++++++ 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 1c7f883836..abb04061af 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -62,7 +62,7 @@ class CreateRenderlayer(plugin.Creator): ["multilayer_exr"]) @staticmethod - def get_render_product(output_path, name): + def get_render_product(output_path, name, aov_sep): """ Generate the path to the render product. Blender interprets the `#` as the frame number, when it renders. @@ -74,7 +74,8 @@ class CreateRenderlayer(plugin.Creator): instance (pyblish.api.Instance): The instance to publish. ext (str): The image format to render. """ - render_product = f"{os.path.join(output_path, name)}.####" + filepath = os.path.join(output_path, name) + render_product = f"{filepath}{aov_sep}beauty.####" render_product = render_product.replace("\\", "/") return render_product @@ -233,17 +234,16 @@ class CreateRenderlayer(plugin.Creator): ext = self.get_image_format(settings) multilayer = self.get_multilayer(settings) + self.set_render_format(ext, multilayer) aov_list, custom_passes = self.set_render_passes(settings) output_path = os.path.join(file_path, render_folder, file_name) - render_product = self.get_render_product(output_path, name) + render_product = self.get_render_product(output_path, name, aov_sep) aov_file_product = self.set_node_tree( output_path, name, aov_sep, ext, multilayer) - # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product - self.set_render_format(ext, multilayer) render_settings = { "render_folder": render_folder, @@ -254,6 +254,7 @@ class CreateRenderlayer(plugin.Creator): "custom_passes": custom_passes, "render_product": render_product, "aov_file_product": aov_file_product, + "review": True, } self.imprint_render_settings(asset_group, render_settings) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 557a4c9066..e0fc933241 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -82,6 +82,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): render_product = render_data.get("render_product") aov_file_product = render_data.get("aov_file_product") ext = render_data.get("image_format") + multilayer = render_data.get("multilayer_exr") frame_start = context.data["frameStart"] frame_end = context.data["frameEnd"] @@ -105,6 +106,8 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): "frameEndHandle": frame_handle_end, "fps": context.data["fps"], "byFrameStep": bpy.context.scene.frame_step, + "review": render_data.get("review", False), + "multipartExr": ext == "exr" and multilayer, "farm": True, "expectedFiles": [expected_files], # OCIO not currently implemented in Blender, but the following diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 609bfc3d3b..903b6e42e7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -107,6 +107,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "redshift_rop"] aov_filter = {"maya": [r".*([Bb]eauty).*"], + "blender": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE "harmony": [r".*"], # for everything from AE "celaction": [r".*"], diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 33ea533863..9e88f3b6f2 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -121,6 +121,9 @@ "maya": [ ".*([Bb]eauty).*" ], + "blender": [ + ".*([Bb]eauty).*" + ], "aftereffects": [ ".*" ], diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index a29caa7ba1..32a5d0e353 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -421,6 +421,12 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = { ".*([Bb]eauty).*" ] }, + { + "name": "blender", + "value": [ + ".*([Bb]eauty).*" + ] + }, { "name": "aftereffects", "value": [ From 3c2c33bcea84ada5c9292e12fc53c51e74a73e5a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 17:06:29 +0100 Subject: [PATCH 067/186] Reorganized code and added validator to check the output folder --- openpype/hosts/blender/api/__init__.py | 3 + openpype/hosts/blender/api/render_lib.py | 240 +++++++++++++++++ .../blender/plugins/create/create_render.py | 248 +----------------- .../publish/validate_deadline_publish.py | 47 ++++ 4 files changed, 293 insertions(+), 245 deletions(-) create mode 100644 openpype/hosts/blender/api/render_lib.py create mode 100644 openpype/hosts/blender/plugins/publish/validate_deadline_publish.py diff --git a/openpype/hosts/blender/api/__init__.py b/openpype/hosts/blender/api/__init__.py index 75a11affde..e15f1193a5 100644 --- a/openpype/hosts/blender/api/__init__.py +++ b/openpype/hosts/blender/api/__init__.py @@ -38,6 +38,8 @@ from .lib import ( from .capture import capture +from .render_lib import prepare_rendering + __all__ = [ "install", @@ -66,4 +68,5 @@ __all__ = [ "get_selection", "capture", # "unique_name", + "prepare_rendering", ] diff --git a/openpype/hosts/blender/api/render_lib.py b/openpype/hosts/blender/api/render_lib.py new file mode 100644 index 0000000000..994de43503 --- /dev/null +++ b/openpype/hosts/blender/api/render_lib.py @@ -0,0 +1,240 @@ +import os + +import bpy + +from openpype.settings import get_project_settings +from openpype.pipeline import get_current_project_name + + +def get_default_render_folder(settings): + """Get default render folder from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["default_render_image_folder"]) + +def get_aov_separator(settings): + """Get aov separator from blender settings.""" + + aov_sep = (settings["blender"] + ["RenderSettings"] + ["aov_separator"]) + + if aov_sep == "dash": + return "-" + elif aov_sep == "underscore": + return "_" + elif aov_sep == "dot": + return "." + else: + raise ValueError(f"Invalid aov separator: {aov_sep}") + +def get_image_format(settings): + """Get image format from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["image_format"]) + +def get_multilayer(settings): + """Get multilayer from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["multilayer_exr"]) + +def get_render_product(output_path, name, aov_sep): + """ + Generate the path to the render product. Blender interprets the `#` + as the frame number, when it renders. + + Args: + file_path (str): The path to the blender scene. + render_folder (str): The render folder set in settings. + file_name (str): The name of the blender scene. + instance (pyblish.api.Instance): The instance to publish. + ext (str): The image format to render. + """ + filepath = os.path.join(output_path, name) + render_product = f"{filepath}{aov_sep}beauty.####" + render_product = render_product.replace("\\", "/") + + return render_product + +def set_render_format(ext, multilayer): + # Set Blender to save the file with the right extension + bpy.context.scene.render.use_file_extension = True + + image_settings = bpy.context.scene.render.image_settings + + if ext == "exr": + image_settings.file_format = ( + "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR") + elif ext == "bmp": + image_settings.file_format = "BMP" + elif ext == "rgb": + image_settings.file_format = "IRIS" + elif ext == "png": + image_settings.file_format = "PNG" + elif ext == "jpeg": + image_settings.file_format = "JPEG" + elif ext == "jp2": + image_settings.file_format = "JPEG2000" + elif ext == "tga": + image_settings.file_format = "TARGA" + elif ext == "tif": + image_settings.file_format = "TIFF" + +def set_render_passes(settings): + aov_list = (settings["blender"] + ["RenderSettings"] + ["aov_list"]) + + custom_passes = (settings["blender"] + ["RenderSettings"] + ["custom_passes"]) + + vl = bpy.context.view_layer + + vl.use_pass_combined = "combined" in aov_list + vl.use_pass_z = "z" in aov_list + vl.use_pass_mist = "mist" in aov_list + vl.use_pass_normal = "normal" in aov_list + vl.use_pass_diffuse_direct = "diffuse_light" in aov_list + vl.use_pass_diffuse_color = "diffuse_color" in aov_list + vl.use_pass_glossy_direct = "specular_light" in aov_list + vl.use_pass_glossy_color = "specular_color" in aov_list + vl.eevee.use_pass_volume_direct = "volume_light" in aov_list + vl.use_pass_emit = "emission" in aov_list + vl.use_pass_environment = "environment" in aov_list + vl.use_pass_shadow = "shadow" in aov_list + vl.use_pass_ambient_occlusion = "ao" in aov_list + + aovs_names = [aov.name for aov in vl.aovs] + for cp in custom_passes: + cp_name = cp[0] + if cp_name not in aovs_names: + aov = vl.aovs.add() + aov.name = cp_name + else: + aov = vl.aovs[cp_name] + aov.type = cp[1].get("type", "VALUE") + + return aov_list, custom_passes + +def set_node_tree(output_path, name, aov_sep, ext, multilayer): + # Set the scene to use the compositor node tree to render + bpy.context.scene.use_nodes = True + + tree = bpy.context.scene.node_tree + + # Get the Render Layers node + rl_node = None + for node in tree.nodes: + if node.bl_idname == "CompositorNodeRLayers": + rl_node = node + break + + # If there's not a Render Layers node, we create it + if not rl_node: + rl_node = tree.nodes.new("CompositorNodeRLayers") + + # Get the enabled output sockets, that are the active passes for the + # render. + # We also exclude some layers. + exclude_sockets = ["Image", "Alpha"] + passes = [ + socket + for socket in rl_node.outputs + if socket.enabled and socket.name not in exclude_sockets + ] + + # Remove all output nodes + for node in tree.nodes: + if node.bl_idname == "CompositorNodeOutputFile": + tree.nodes.remove(node) + + # Create a new output node + output = tree.nodes.new("CompositorNodeOutputFile") + + image_settings = bpy.context.scene.render.image_settings + output.format.file_format = image_settings.file_format + + # In case of a multilayer exr, we don't need to use the output node, + # because the blender render already outputs a multilayer exr. + if ext == "exr" and multilayer: + output.layer_slots.clear() + return [] + + output.file_slots.clear() + output.base_path = output_path + + aov_file_products = [] + + # For each active render pass, we add a new socket to the output node + # and link it + for render_pass in passes: + filepath = f"{name}{aov_sep}{render_pass.name}.####" + + output.file_slots.new(filepath) + + aov_file_products.append( + (render_pass.name, os.path.join(output_path, filepath))) + + node_input = output.inputs[-1] + + tree.links.new(render_pass, node_input) + + return aov_file_products + +def imprint_render_settings(node, data): + RENDER_DATA = "render_data" + if not node.get(RENDER_DATA): + node[RENDER_DATA] = {} + for key, value in data.items(): + if value is None: + continue + node[RENDER_DATA][key] = value + +def prepare_rendering(asset_group): + name = asset_group.name + + filepath = bpy.data.filepath + assert filepath, "Workfile not saved. Please save the file first." + + file_path = os.path.dirname(filepath) + file_name = os.path.basename(filepath) + file_name, _ = os.path.splitext(file_name) + + project = get_current_project_name() + settings = get_project_settings(project) + + render_folder = get_default_render_folder(settings) + aov_sep = get_aov_separator(settings) + ext = get_image_format(settings) + multilayer = get_multilayer(settings) + + set_render_format(ext, multilayer) + aov_list, custom_passes = set_render_passes(settings) + + output_path = os.path.join(file_path, render_folder, file_name) + + render_product = get_render_product(output_path, name, aov_sep) + aov_file_product = set_node_tree( + output_path, name, aov_sep, ext, multilayer) + + bpy.context.scene.render.filepath = render_product + + render_settings = { + "render_folder": render_folder, + "aov_separator": aov_sep, + "image_format": ext, + "multilayer_exr": multilayer, + "aov_list": aov_list, + "custom_passes": custom_passes, + "render_product": render_product, + "aov_file_product": aov_file_product, + "review": True, + } + + imprint_render_settings(asset_group, render_settings) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index abb04061af..7a91726a5f 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -3,12 +3,9 @@ import os import bpy -from openpype.settings import get_project_settings -from openpype.pipeline import ( - get_current_project_name, - get_current_task_name, -) +from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib +from openpype.hosts.blender.api.render_lib import prepare_rendering from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,245 +17,6 @@ class CreateRenderlayer(plugin.Creator): family = "renderlayer" icon = "eye" - @staticmethod - def get_default_render_folder(settings): - """Get default render folder from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["default_render_image_folder"]) - - @staticmethod - def get_aov_separator(settings): - """Get aov separator from blender settings.""" - - aov_sep = (settings["blender"] - ["RenderSettings"] - ["aov_separator"]) - - if aov_sep == "dash": - return "-" - elif aov_sep == "underscore": - return "_" - elif aov_sep == "dot": - return "." - else: - raise ValueError(f"Invalid aov separator: {aov_sep}") - - @staticmethod - def get_image_format(settings): - """Get image format from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["image_format"]) - - @staticmethod - def get_multilayer(settings): - """Get multilayer from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["multilayer_exr"]) - - @staticmethod - def get_render_product(output_path, name, aov_sep): - """ - Generate the path to the render product. Blender interprets the `#` - as the frame number, when it renders. - - Args: - file_path (str): The path to the blender scene. - render_folder (str): The render folder set in settings. - file_name (str): The name of the blender scene. - instance (pyblish.api.Instance): The instance to publish. - ext (str): The image format to render. - """ - filepath = os.path.join(output_path, name) - render_product = f"{filepath}{aov_sep}beauty.####" - render_product = render_product.replace("\\", "/") - - return render_product - - @staticmethod - def set_render_format(ext, multilayer): - # Set Blender to save the file with the right extension - bpy.context.scene.render.use_file_extension = True - - image_settings = bpy.context.scene.render.image_settings - - if ext == "exr": - image_settings.file_format = ( - "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR") - elif ext == "bmp": - image_settings.file_format = "BMP" - elif ext == "rgb": - image_settings.file_format = "IRIS" - elif ext == "png": - image_settings.file_format = "PNG" - elif ext == "jpeg": - image_settings.file_format = "JPEG" - elif ext == "jp2": - image_settings.file_format = "JPEG2000" - elif ext == "tga": - image_settings.file_format = "TARGA" - elif ext == "tif": - image_settings.file_format = "TIFF" - - @staticmethod - def set_render_passes(settings): - aov_list = (settings["blender"] - ["RenderSettings"] - ["aov_list"]) - - custom_passes = (settings["blender"] - ["RenderSettings"] - ["custom_passes"]) - - vl = bpy.context.view_layer - - vl.use_pass_combined = "combined" in aov_list - vl.use_pass_z = "z" in aov_list - vl.use_pass_mist = "mist" in aov_list - vl.use_pass_normal = "normal" in aov_list - vl.use_pass_diffuse_direct = "diffuse_light" in aov_list - vl.use_pass_diffuse_color = "diffuse_color" in aov_list - vl.use_pass_glossy_direct = "specular_light" in aov_list - vl.use_pass_glossy_color = "specular_color" in aov_list - vl.eevee.use_pass_volume_direct = "volume_light" in aov_list - vl.use_pass_emit = "emission" in aov_list - vl.use_pass_environment = "environment" in aov_list - vl.use_pass_shadow = "shadow" in aov_list - vl.use_pass_ambient_occlusion = "ao" in aov_list - - aovs_names = [aov.name for aov in vl.aovs] - for cp in custom_passes: - cp_name = cp[0] - if cp_name not in aovs_names: - aov = vl.aovs.add() - aov.name = cp_name - else: - aov = vl.aovs[cp_name] - aov.type = cp[1].get("type", "VALUE") - - return aov_list, custom_passes - - def set_node_tree(self, output_path, name, aov_sep, ext, multilayer): - # Set the scene to use the compositor node tree to render - bpy.context.scene.use_nodes = True - - tree = bpy.context.scene.node_tree - - # Get the Render Layers node - rl_node = None - for node in tree.nodes: - if node.bl_idname == "CompositorNodeRLayers": - rl_node = node - break - - # If there's not a Render Layers node, we create it - if not rl_node: - rl_node = tree.nodes.new("CompositorNodeRLayers") - - # Get the enabled output sockets, that are the active passes for the - # render. - # We also exclude some layers. - exclude_sockets = ["Image", "Alpha"] - passes = [ - socket - for socket in rl_node.outputs - if socket.enabled and socket.name not in exclude_sockets - ] - - # Remove all output nodes - for node in tree.nodes: - if node.bl_idname == "CompositorNodeOutputFile": - tree.nodes.remove(node) - - # Create a new output node - output = tree.nodes.new("CompositorNodeOutputFile") - - image_settings = bpy.context.scene.render.image_settings - output.format.file_format = image_settings.file_format - - # In case of a multilayer exr, we don't need to use the output node, - # because the blender render already outputs a multilayer exr. - if ext == "exr" and multilayer: - output.layer_slots.clear() - return [] - - output.file_slots.clear() - output.base_path = output_path - - aov_file_products = [] - - # For each active render pass, we add a new socket to the output node - # and link it - for render_pass in passes: - filepath = f"{name}{aov_sep}{render_pass.name}.####" - - output.file_slots.new(filepath) - - aov_file_products.append( - (render_pass.name, os.path.join(output_path, filepath))) - - node_input = output.inputs[-1] - - tree.links.new(render_pass, node_input) - - return aov_file_products - - @staticmethod - def imprint_render_settings(node, data): - RENDER_DATA = "render_data" - if not node.get(RENDER_DATA): - node[RENDER_DATA] = {} - for key, value in data.items(): - if value is None: - continue - node[RENDER_DATA][key] = value - - def prepare_rendering(self, asset_group, name): - filepath = bpy.data.filepath - assert filepath, "Workfile not saved. Please save the file first." - - file_path = os.path.dirname(filepath) - file_name = os.path.basename(filepath) - file_name, _ = os.path.splitext(file_name) - - project = get_current_project_name() - settings = get_project_settings(project) - - render_folder = self.get_default_render_folder(settings) - aov_sep = self.get_aov_separator(settings) - ext = self.get_image_format(settings) - multilayer = self.get_multilayer(settings) - - self.set_render_format(ext, multilayer) - aov_list, custom_passes = self.set_render_passes(settings) - - output_path = os.path.join(file_path, render_folder, file_name) - - render_product = self.get_render_product(output_path, name, aov_sep) - aov_file_product = self.set_node_tree( - output_path, name, aov_sep, ext, multilayer) - - bpy.context.scene.render.filepath = render_product - - render_settings = { - "render_folder": render_folder, - "aov_separator": aov_sep, - "image_format": ext, - "multilayer_exr": multilayer, - "aov_list": aov_list, - "custom_passes": custom_passes, - "render_product": render_product, - "aov_file_product": aov_file_product, - "review": True, - } - - self.imprint_render_settings(asset_group, render_settings) - def process(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) @@ -277,7 +35,7 @@ class CreateRenderlayer(plugin.Creator): self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) - self.prepare_rendering(asset_group, name) + prepare_rendering(asset_group) except Exception: # Remove the instance if there was an error bpy.data.collections.remove(asset_group) diff --git a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py new file mode 100644 index 0000000000..54a4442bdb --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py @@ -0,0 +1,47 @@ +import os + +import bpy + +import pyblish.api +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.hosts.blender.api.render_lib import prepare_rendering + + +class ValidateDeadlinePublish(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates Render File Directory is + not the same in every submission + """ + + order = ValidateContentsOrder + families = ["renderlayer"] + hosts = ["blender"] + label = "Validate Render Output for Deadline" + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + filepath = bpy.data.filepath + file = os.path.basename(filepath) + filename, ext = os.path.splitext(file) + if filename not in bpy.context.scene.render.filepath: + raise PublishValidationError( + "Render output folder " + "doesn't match the max scene name! " + "Use Repair action to " + "fix the folder file path.." + ) + + @classmethod + def repair(cls, instance): + container = bpy.data.collections[str(instance)] + prepare_rendering(container) + bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) + cls.log.debug("Reset the render output folder...") From c315ee8f65db1a6a973d6480070c5c098c33327f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 17:20:26 +0100 Subject: [PATCH 068/186] Hound fixes --- openpype/hosts/blender/api/render_lib.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/api/render_lib.py b/openpype/hosts/blender/api/render_lib.py index 994de43503..43560ee6d5 100644 --- a/openpype/hosts/blender/api/render_lib.py +++ b/openpype/hosts/blender/api/render_lib.py @@ -13,12 +13,13 @@ def get_default_render_folder(settings): ["RenderSettings"] ["default_render_image_folder"]) + def get_aov_separator(settings): """Get aov separator from blender settings.""" aov_sep = (settings["blender"] - ["RenderSettings"] - ["aov_separator"]) + ["RenderSettings"] + ["aov_separator"]) if aov_sep == "dash": return "-" @@ -29,6 +30,7 @@ def get_aov_separator(settings): else: raise ValueError(f"Invalid aov separator: {aov_sep}") + def get_image_format(settings): """Get image format from blender settings.""" @@ -36,6 +38,7 @@ def get_image_format(settings): ["RenderSettings"] ["image_format"]) + def get_multilayer(settings): """Get multilayer from blender settings.""" @@ -43,6 +46,7 @@ def get_multilayer(settings): ["RenderSettings"] ["multilayer_exr"]) + def get_render_product(output_path, name, aov_sep): """ Generate the path to the render product. Blender interprets the `#` @@ -61,6 +65,7 @@ def get_render_product(output_path, name, aov_sep): return render_product + def set_render_format(ext, multilayer): # Set Blender to save the file with the right extension bpy.context.scene.render.use_file_extension = True @@ -85,14 +90,15 @@ def set_render_format(ext, multilayer): elif ext == "tif": image_settings.file_format = "TIFF" + def set_render_passes(settings): aov_list = (settings["blender"] ["RenderSettings"] ["aov_list"]) custom_passes = (settings["blender"] - ["RenderSettings"] - ["custom_passes"]) + ["RenderSettings"] + ["custom_passes"]) vl = bpy.context.view_layer @@ -122,6 +128,7 @@ def set_render_passes(settings): return aov_list, custom_passes + def set_node_tree(output_path, name, aov_sep, ext, multilayer): # Set the scene to use the compositor node tree to render bpy.context.scene.use_nodes = True @@ -187,6 +194,7 @@ def set_node_tree(output_path, name, aov_sep, ext, multilayer): return aov_file_products + def imprint_render_settings(node, data): RENDER_DATA = "render_data" if not node.get(RENDER_DATA): @@ -196,6 +204,7 @@ def imprint_render_settings(node, data): continue node[RENDER_DATA][key] = value + def prepare_rendering(asset_group): name = asset_group.name From 11921a9de995794cbc950ce183b5d3e837d78d17 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 17:20:43 +0100 Subject: [PATCH 069/186] Added settings for new validator --- .../defaults/project_settings/blender.json | 5 ++ .../schemas/schema_blender_publish.json | 84 +++++++++++++------ .../server/settings/publish_plugins.py | 10 +++ 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 9cbbb49593..f3eb31174f 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -46,6 +46,11 @@ "optional": false, "active": true }, + "ValidateDeadlinePublish": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateMeshHasUvs": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index 05e7f13e70..7f1a8a915b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -51,30 +51,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "ValidateRenderCameraIsSet", - "label": "Validate Render Camera Is Set", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "boolean", - "key": "optional", - "label": "Optional" - }, - { - "type": "boolean", - "key": "active", - "label": "Active" - } - ] - }, { "type": "collapsible-wrap", "label": "Model", @@ -103,6 +79,66 @@ } ] }, + { + "type": "collapsible-wrap", + "label": "Render", + "children": [ + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "type": "dict", + "collapsible": true, + "key": "ValidateRenderCameraIsSet", + "label": "Validate Render Camera Is Set", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateDeadlinePublish", + "label": "Validate Render Output for Deadline", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + } + ] + } + ] + }, { "type": "splitter" }, diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 575bfe9f39..5e047b7013 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -73,6 +73,11 @@ class PublishPuginsModel(BaseSettingsModel): title="Validate Render Camera Is Set", section="Validators" ) + ValidateDeadlinePublish: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Render Output for Deadline", + section="Validators" + ) ValidateMeshHasUvs: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Validate Mesh Has Uvs" @@ -149,6 +154,11 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": False, "active": True }, + "ValidateDeadlinePublish": { + "enabled": True, + "optional": False, + "active": True + }, "ValidateMeshHasUvs": { "enabled": True, "optional": True, From 2acbf241ad12b32affa6772e87728c31e0b3135c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 18:05:23 +0100 Subject: [PATCH 070/186] Changed family to "render" --- openpype/hosts/blender/plugins/create/create_render.py | 4 +--- openpype/hosts/blender/plugins/publish/collect_render.py | 3 ++- .../blender/plugins/publish/increment_workfile_version.py | 2 +- .../blender/plugins/publish/validate_deadline_publish.py | 2 +- .../blender/plugins/publish/validate_render_camera_is_set.py | 2 +- .../deadline/plugins/publish/submit_blender_deadline.py | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 7a91726a5f..f938a21808 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -1,6 +1,4 @@ """Create render.""" -import os - import bpy from openpype.pipeline import get_current_task_name @@ -14,7 +12,7 @@ class CreateRenderlayer(plugin.Creator): name = "renderingMain" label = "Render" - family = "renderlayer" + family = "render" icon = "eye" def process(self): diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index e0fc933241..92e2473a95 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -15,7 +15,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.01 hosts = ["blender"] - families = ["renderlayer"] + families = ["render"] label = "Collect Render Layers" sync_workfile_version = False @@ -100,6 +100,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): expected_files = expected_beauty | expected_aovs instance.data.update({ + "family": "render.farm", "frameStart": frame_start, "frameEnd": frame_end, "frameStartHandle": frame_handle_start, diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index 5f49ad7185..3d176f9c30 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -10,7 +10,7 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin): optional = True hosts = ["blender"] families = ["animation", "model", "rig", "action", "layout", "blendScene", - "renderlayer"] + "render"] def process(self, context): diff --git a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py index 54a4442bdb..f89a7d3d58 100644 --- a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py @@ -19,7 +19,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, """ order = ValidateContentsOrder - families = ["renderlayer"] + families = ["render.farm"] hosts = ["blender"] label = "Validate Render Output for Deadline" optional = True diff --git a/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py index 5a06c1ff0a..ba3a796f35 100644 --- a/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py +++ b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py @@ -8,7 +8,7 @@ class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder hosts = ["blender"] - families = ["renderlayer"] + families = ["render"] label = "Validate Render Camera Is Set" optional = False diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index ad456c0d13..307fc8b5a2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -27,7 +27,7 @@ class BlenderPluginInfo(): class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): label = "Submit Render to Deadline" hosts = ["blender"] - families = ["renderlayer"] + families = ["render.farm"] use_published = True priority = 50 From c78d496ec948e1108ee23f2babf17878ddaa26fa Mon Sep 17 00:00:00 2001 From: Kayla Date: Mon, 18 Sep 2023 12:17:22 +0800 Subject: [PATCH 071/186] including skeleton sets into animation sets during loading rig & fixing the rig fbx extractor not being displayed --- openpype/hosts/maya/api/lib.py | 16 ++++++++++++++-- .../plugins/publish/collect_fbx_animation.py | 5 +++-- ..._rig_for_fbx.py => collect_skeleton_mesh.py} | 17 +++++------------ .../plugins/publish/extract_fbx_animation.py | 2 +- .../maya/plugins/publish/extract_rig_fbx.py | 14 ++++++++------ 5 files changed, 31 insertions(+), 23 deletions(-) rename openpype/hosts/maya/plugins/publish/{collect_rig_for_fbx.py => collect_skeleton_mesh.py} (67%) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 40b3419e73..2ff4ff42de 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -4109,6 +4109,14 @@ def create_rig_animation_instance( assert output, "No out_SET in rig, this is a bug." assert controls, "No controls_SET in rig, this is a bug." + anim_skeleton = next((node for node in nodes if + node.endswith("skeletonAnim_SET")), None) + if not anim_skeleton: + log.debug("No skeletonAnim_SET in rig") + skeleton_mesh = next((node for node in nodes if + node.endswith("skeletonMesh_SET")), None) + if not skeleton_mesh: + log.debug("No skeletonMesh_SET in rig") # Find the roots amongst the loaded nodes roots = ( cmds.ls(nodes, assemblies=True, long=True) or @@ -4142,10 +4150,14 @@ def create_rig_animation_instance( host = registered_host() create_context = CreateContext(host) - # Create the animation instance + rig_sets = [output, controls] + if anim_skeleton: + rig_sets.append(anim_skeleton) + if skeleton_mesh: + rig_sets.append(skeleton_mesh) with maintained_selection(): - cmds.select([output, controls] + roots, noExpand=True) + cmds.select(rig_sets + roots, noExpand=True) create_context.create( creator_identifier=creator_identifier, variant=namespace, diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index 9749fb4770..75e36e78ce 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -4,7 +4,7 @@ import pyblish.api class CollectFbxAnimation(pyblish.api.InstancePlugin): - """Collect Unreal Skeletal Mesh.""" + """Collect Animated Rig Data for FBX Extractor.""" order = pyblish.api.CollectorOrder + 0.2 label = "Collect Fbx Animation" @@ -17,7 +17,8 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin): if i.lower().endswith("skeletonanim_set") ] if skeleton_sets: - instance.data["families"].append("animation.fbx") + instance.data["families"] += ["animation.fbx"] + instance.data["animated_skeleton"] = [] for skeleton_set in skeleton_sets: skeleton_content = cmds.sets(skeleton_set, query=True) self.log.debug( diff --git a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py similarity index 67% rename from openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py rename to openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py index 65653b3369..ccf65441a2 100644 --- a/openpype/hosts/maya/plugins/publish/collect_rig_for_fbx.py +++ b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py @@ -3,11 +3,11 @@ from maya import cmds # noqa import pyblish.api -class CollectRigFbx(pyblish.api.InstancePlugin): - """Collect Unreal Skeletal Mesh.""" +class CollectSkeletonMesh(pyblish.api.InstancePlugin): + """Collect Static Rig Data for FBX Extractor.""" order = pyblish.api.CollectorOrder + 0.2 - label = "Collect rig for fbx" + label = "Collect Skeleton Mesh" hosts = ["maya"] families = ["rig"] @@ -29,16 +29,9 @@ class CollectRigFbx(pyblish.api.InstancePlugin): "no skeleton_set or skeleton_mesh set was found....") return instance.data["skeleton_mesh"] = [] - instance.data["animated_rigs"] = [] - if skeleton_sets: - for skeleton_set in skeleton_sets: - skeleton_content = cmds.sets(skeleton_set, query=True) - if skeleton_content: - instance.data["animated_rigs"] += skeleton_content - self.log.debug("Collected skeleton" - f" data: {skeleton_content}") + if skeleton_mesh_sets: - instance.data["families"].append("rig.fbx") + instance.data["families"] += ["rig.fbx"] for skeleton_mesh_set in skeleton_mesh_sets: skeleton_mesh_content = cmds.sets( skeleton_mesh_set, query=True) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 1b4b63db87..ef8b22d452 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -44,7 +44,7 @@ class ExtractRigFBX(publish.Extractor, fbx_exporter.set_options_from_instance(instance) # Export - fbx_exporter.export(out_set, path) + fbx_exporter.export(out_set, path.replace("\\", "/")) if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py index 9eecde90e9..c9fe53f0be 100644 --- a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py +++ b/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py @@ -9,8 +9,8 @@ from openpype.pipeline.publish import OptionalPyblishPluginMixin from openpype.hosts.maya.api import fbx -class ExtractRigFBX(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractSkeletonMesh(publish.Extractor, + OptionalPyblishPluginMixin): """Extract Rig in FBX format from Maya. This extracts the rig in fbx with the constraints @@ -20,16 +20,18 @@ class ExtractRigFBX(publish.Extractor, """ order = pyblish.api.ExtractorOrder - label = "Extract Rig (FBX)" + label = "Extract Skeleton Mesh" hosts = ["maya"] families = ["rig.fbx"] def process(self, instance): if not self.is_active(instance.data): return + # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) + # The export requires forward slashes because we need # to format it into a string in a mel expression fbx_exporter = fbx.FBXExtractor(log=self.log) @@ -41,7 +43,7 @@ class ExtractRigFBX(publish.Extractor, fbx_exporter.set_options_from_instance(instance) # Export - fbx_exporter.export(out_set, path) + fbx_exporter.export(out_set, path.replace("\\", "/")) if "representations" not in instance.data: instance.data["representations"] = [] @@ -50,8 +52,8 @@ class ExtractRigFBX(publish.Extractor, 'name': 'fbx', 'ext': 'fbx', 'files': filename, - "stagingDir": staging_dir, + "stagingDir": staging_dir } instance.data["representations"].append(representation) - self.log.debug("Extract FBX successful to: {0}".format(path)) + self.log.debug("Extract animated FBX successful to: {0}".format(path)) From ccf4b7f54781d81d9d6d331f63e3a5a8ab0e0d6f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Sep 2023 10:52:38 +0100 Subject: [PATCH 072/186] Fix error description Co-authored-by: Roy Nieterau --- .../hosts/blender/plugins/publish/validate_deadline_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py index f89a7d3d58..14220b5c9c 100644 --- a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py @@ -34,7 +34,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, if filename not in bpy.context.scene.render.filepath: raise PublishValidationError( "Render output folder " - "doesn't match the max scene name! " + "doesn't match the blender scene name! " "Use Repair action to " "fix the folder file path.." ) From 4737ca8d5962b3d27d93f6799f58884e2c0a47ae Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Sep 2023 11:08:21 +0100 Subject: [PATCH 073/186] Added note on the Multilayer EXR setting --- .../schemas/projects_schema/schema_project_blender.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index a283a2ff5c..4c9405fcd3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -99,6 +99,10 @@ "type": "boolean", "label": "Multilayer (EXR)" }, + { + "type": "label", + "label": "Note: Multilayer EXR is only used when output format type set to EXR." + }, { "key": "aov_list", "label": "AOVs to create", From b5d789e09bdc576c38197d590abcb23999fcb635 Mon Sep 17 00:00:00 2001 From: Kayla Date: Mon, 18 Sep 2023 20:36:00 +0800 Subject: [PATCH 074/186] remove animationOnly parameter as it would convert the joint to transform data --- openpype/hosts/maya/api/fbx.py | 7 ++++--- .../hosts/maya/plugins/publish/extract_fbx_animation.py | 7 +++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 9092aaec23..c06ba12719 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -66,7 +66,8 @@ class FBXExtractor: "upAxis": str, # x, y or z, "triangulate": bool, "FileVersion": str, - "skeletonDefinitions": bool + "skeletonDefinitions": bool, + "referencedAssetsContent": bool } @property @@ -97,7 +98,6 @@ class FBXExtractor: "bakeComplexEnd": end_frame, "bakeComplexStep": 1, "bakeResampleAnimation": True, - "animationOnly": False, "useSceneName": False, "quaternion": "euler", "shapes": True, @@ -109,7 +109,8 @@ class FBXExtractor: "upAxis": "y", "triangulate": False, "fileVersion": "FBX202000", - "skeletonDefinitions": False + "skeletonDefinitions": False, + "referencedAssetsContent": False } def __init__(self, log=None): diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index ef8b22d452..8e96d46344 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -2,6 +2,7 @@ import os from maya import cmds # noqa +import maya.mel as mel import pyblish.api from openpype.pipeline import publish @@ -36,14 +37,12 @@ class ExtractRigFBX(publish.Extractor, # to format it into a string in a mel expression fbx_exporter = fbx.FBXExtractor(log=self.log) out_set = instance.data.get("animated_skeleton", []) - + # Export instance.data["constraints"] = True instance.data["skeletonDefinitions"] = True - instance.data["animationOnly"] = True + instance.data["referencedAssetsContent"] = True fbx_exporter.set_options_from_instance(instance) - - # Export fbx_exporter.export(out_set, path.replace("\\", "/")) if "representations" not in instance.data: From 5429616e1ee894582afdc3422a7b53b079da9495 Mon Sep 17 00:00:00 2001 From: Kayla Date: Mon, 18 Sep 2023 21:50:13 +0800 Subject: [PATCH 075/186] add fbx as representation to the loader and hound fix --- openpype/hosts/maya/api/fbx.py | 1 - openpype/hosts/maya/api/lib.py | 8 ++++---- openpype/hosts/maya/plugins/load/_load_animation.py | 2 +- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- .../hosts/maya/plugins/publish/extract_fbx_animation.py | 4 +--- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index c06ba12719..5bd375362b 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -54,7 +54,6 @@ class FBXExtractor: "bakeComplexEnd": int, "bakeComplexStep": int, "bakeResampleAnimation": bool, - "animationOnly": bool, "useSceneName": bool, "quaternion": str, # "euler" "shapes": bool, diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 2ff4ff42de..2769f05c35 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -4100,14 +4100,14 @@ def create_rig_animation_instance( """ if options is None: options = {} - + name = context["representation"]["name"] output = next((node for node in nodes if node.endswith("out_SET")), None) controls = next((node for node in nodes if node.endswith("controls_SET")), None) - - assert output, "No out_SET in rig, this is a bug." - assert controls, "No controls_SET in rig, this is a bug." + if name != "fbx": + assert output, "No out_SET in rig, this is a bug." + assert controls, "No controls_SET in rig, this is a bug." anim_skeleton = next((node for node in nodes if node.endswith("skeletonAnim_SET")), None) diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py index 981b9ef434..6d67383909 100644 --- a/openpype/hosts/maya/plugins/load/_load_animation.py +++ b/openpype/hosts/maya/plugins/load/_load_animation.py @@ -7,7 +7,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): families = ["animation", "camera", "pointcache"] - representations = ["abc"] + representations = ["abc", "fbx"] label = "Reference animation" order = -10 diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 61f337f501..c9c3fb9786 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -117,7 +117,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): family = context["representation"]["context"]["family"] except ValueError: family = "model" - + print(f"family:{family}") project_name = context["project"]["name"] # True by default to keep legacy behaviours attach_to_root = options.get("attach_to_root", True) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 8e96d46344..142d815a29 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -2,7 +2,6 @@ import os from maya import cmds # noqa -import maya.mel as mel import pyblish.api from openpype.pipeline import publish @@ -52,8 +51,7 @@ class ExtractRigFBX(publish.Extractor, 'name': 'fbx', 'ext': 'fbx', 'files': filename, - "stagingDir": staging_dir, - "outputName": "fbxanim" + "stagingDir": staging_dir } instance.data["representations"].append(representation) From 7252acceb89f7b175bf2b22235bb8ab66d0dbe03 Mon Sep 17 00:00:00 2001 From: Kayla Date: Mon, 18 Sep 2023 21:52:17 +0800 Subject: [PATCH 076/186] hound --- openpype/hosts/maya/api/lib.py | 2 +- openpype/hosts/maya/plugins/load/load_reference.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 2769f05c35..d889fe4b8c 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -4100,7 +4100,7 @@ def create_rig_animation_instance( """ if options is None: options = {} - name = context["representation"]["name"] + name = context["representation"]["name"] output = next((node for node in nodes if node.endswith("out_SET")), None) controls = next((node for node in nodes if diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index c9c3fb9786..61f337f501 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -117,7 +117,7 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): family = context["representation"]["context"]["family"] except ValueError: family = "model" - print(f"family:{family}") + project_name = context["project"]["name"] # True by default to keep legacy behaviours attach_to_root = options.get("attach_to_root", True) From b7995a41a71fc8d0e9593f7c9940a8e7f06de9dc Mon Sep 17 00:00:00 2001 From: Kayla Date: Wed, 20 Sep 2023 21:26:02 +0800 Subject: [PATCH 077/186] enable the skeleton rig content validator and make the fbx animation collector optional and use the asset as both asset_name and asset_type data for custom subset in the loader --- openpype/hosts/maya/api/lib.py | 6 ++- .../plugins/publish/collect_fbx_animation.py | 9 ++++- .../plugins/publish/collect_skeleton_mesh.py | 10 +++++ .../plugins/publish/extract_fbx_animation.py | 6 +-- ...ct_rig_fbx.py => extract_skeleton_mesh.py} | 0 .../publish/validate_skeleton_rig_content.py | 40 +++++++++---------- .../defaults/project_settings/maya.json | 5 ++- .../schemas/schema_maya_publish.json | 14 +++++++ .../maya/server/settings/publishers.py | 13 +++++- 9 files changed, 72 insertions(+), 31 deletions(-) rename openpype/hosts/maya/plugins/publish/{extract_rig_fbx.py => extract_skeleton_mesh.py} (100%) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index d889fe4b8c..fed2887419 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -4113,6 +4113,7 @@ def create_rig_animation_instance( node.endswith("skeletonAnim_SET")), None) if not anim_skeleton: log.debug("No skeletonAnim_SET in rig") + skeleton_mesh = next((node for node in nodes if node.endswith("skeletonMesh_SET")), None) if not skeleton_mesh: @@ -4128,8 +4129,9 @@ def create_rig_animation_instance( if custom_subset: formatting_data = { # TODO remove 'asset_type' and replace 'asset_name' with 'asset' - "asset_name": context['asset']['name'], - "asset_type": context['asset']['type'], + # "asset_name": context['asset']['name'], + # "asset_type": context['asset']['type'], + "asset": context["asset"], "subset": context['subset']['name'], "family": ( context['subset']['data'].get('family') or diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index 75e36e78ce..061619dfb1 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -1,17 +1,22 @@ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api +from openpype.lib import BoolDef +from openpype.pipeline import OptionalPyblishPluginMixin - -class CollectFbxAnimation(pyblish.api.InstancePlugin): +class CollectFbxAnimation(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): """Collect Animated Rig Data for FBX Extractor.""" order = pyblish.api.CollectorOrder + 0.2 label = "Collect Fbx Animation" hosts = ["maya"] families = ["animation"] + optional = True def process(self, instance): + if not self.is_active(instance.data): + return skeleton_sets = [ i for i in instance if i.lower().endswith("skeletonanim_set") diff --git a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py index ccf65441a2..5d894c99a0 100644 --- a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py @@ -29,6 +29,7 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): "no skeleton_set or skeleton_mesh set was found....") return instance.data["skeleton_mesh"] = [] + instance.data["skeleton_rig"] = [] if skeleton_mesh_sets: instance.data["families"] += ["rig.fbx"] @@ -40,3 +41,12 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): self.log.debug( "Collected skeleton " f"mesh Set: {skeleton_mesh_content}") + + if skeleton_sets: + for skeleton_set in skeleton_sets: + skeleton_content = cmds.sets(skeleton_set, query=True) + self.log.debug( + "Collected animated " + f"skeleton data: {skeleton_content}") + if skeleton_content: + instance.data["skeleton_rig"] += skeleton_content diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 142d815a29..1c0a0135d2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -5,12 +5,10 @@ from maya import cmds # noqa import pyblish.api from openpype.pipeline import publish -from openpype.pipeline.publish import OptionalPyblishPluginMixin from openpype.hosts.maya.api import fbx -class ExtractRigFBX(publish.Extractor, - OptionalPyblishPluginMixin): +class ExtractFBXAnimation(publish.Extractor): """Extract Rig in FBX format from Maya. This extracts the rig in fbx with the constraints @@ -25,8 +23,6 @@ class ExtractRigFBX(publish.Extractor, families = ["animation.fbx"] def process(self, instance): - if not self.is_active(instance.data): - return # Define output path staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) diff --git a/openpype/hosts/maya/plugins/publish/extract_rig_fbx.py b/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py similarity index 100% rename from openpype/hosts/maya/plugins/publish/extract_rig_fbx.py rename to openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py index 0406b00ec6..8b8800af17 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -12,7 +12,8 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): The rigs optionally contain at least two object sets: "skeletonAnim_SET" - Set of only bone hierarchies - "skeletonMesh_SET" - Set of all cacheable meshes + "skeletonMesh_SET" - Set of the skinned meshes + with bone hierarchies """ @@ -21,11 +22,10 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): hosts = ["maya"] families = ["rig.fbx"] - accepted_output = ["mesh", "transform"] - accepted_controllers = ["transform"] + accepted_output = ["mesh", "transform", "locator"] + accepted_controllers = ["transform", "locator"] def process(self, instance): - objectsets = ["skeletonAnim_SET", "skeletonMesh_SET"] missing = [ key for key in objectsets if key not in instance.data["rig_sets"] @@ -36,8 +36,8 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): ) return - controls_set = instance.data["rig_sets"]["skeletonAnim_SET"] - out_set = instance.data["rig_sets"]["skeletonMesh_SET"] + skeleton_anim_set = instance.data["rig_sets"]["skeletonAnim_SET"] + skeleton_mesh_set = instance.data["rig_sets"]["skeletonMesh_SET"] # Ensure there are at least some transforms or dag nodes # in the rig instance set_members = instance.data['setMembers'] @@ -45,13 +45,13 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): self.log.debug("Skipping empty instance...") return # Ensure contents in sets and retrieve long path for all objects - output_content = cmds.sets( - out_set, query=True) or [] - output_content = cmds.ls(output_content, long=True) + skeleton_mesh_content = cmds.sets( + skeleton_mesh_set, query=True) or [] + skeleton_mesh_content = cmds.ls(skeleton_mesh_content, long=True) - controls_content = cmds.sets( - controls_set, query=True) or [] - controls_content = cmds.ls(controls_content, long=True) + skeleton_anim_content = cmds.sets( + skeleton_anim_set, query=True) or [] + skeleton_anim_content = cmds.ls(skeleton_anim_content, long=True) # Validate members are inside the hierarchy from root node root_node = cmds.ls(set_members, assemblies=True) @@ -60,16 +60,16 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): hierarchy = set(hierarchy) invalid_hierarchy = [] - if output_content: - for node in output_content: + if skeleton_mesh_content: + for node in skeleton_mesh_content: if node not in hierarchy: invalid_hierarchy.append(node) - invalid_geometry = self.validate_geometry(output_content) - if controls_content: - for node in controls_content: + invalid_geometry = self.validate_geometry(skeleton_mesh_content) + if skeleton_anim_content: + for node in skeleton_anim_content: if node not in hierarchy: invalid_hierarchy.append(node) - invalid_controls = self.validate_controls(controls_content) + invalid_controls = self.validate_controls(skeleton_anim_content) error = False if invalid_hierarchy: @@ -99,7 +99,7 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): Checks if the node types of the set members valid Args: - set_members: list of nodes of the controls_SET + set_members: list of nodes of the skeleton_mesh_set hierarchy: list of nodes which reside under the root node Returns: @@ -126,7 +126,7 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): Checks if the node types of the set members valid Args: - set_members: list of nodes of the controls_SET + set_members: list of nodes of the skeleton_anim_set hierarchy: list of nodes which reside under the root node Returns: diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 022b906c4f..f4fb38ab53 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -707,6 +707,9 @@ "CollectMayaRender": { "sync_workfile_version": false }, + "CollectFbxAnimation": { + "enabled": true + }, "CollectFbxCamera": { "enabled": false }, @@ -1141,7 +1144,7 @@ "active": true }, "ValidateSkeletonRigContents": { - "enabled": false, + "enabled": true, "optional": true, "active": true }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index e5fe367e77..6d81f38aa9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -21,6 +21,20 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "CollectFbxAnimation", + "label": "Collect Fbx Animation", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index 0c733d9cbc..d82daa178c 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -129,6 +129,10 @@ class CollectMayaRenderModel(BaseSettingsModel): ) +class CollectFbxAnimationModel(BaseSettingsModel): + enabled: bool = Field(title="Collect Fbx Animation") + + class CollectFbxCameraModel(BaseSettingsModel): enabled: bool = Field(title="CollectFbxCamera") @@ -364,6 +368,10 @@ class PublishersModel(BaseSettingsModel): title="Collect Render Layers", section="Collectors" ) + CollectFbxAnimation: CollectFbxAnimationModel = Field( + default_factory=CollectFbxAnimationModel, + title="Collect FBX Animation", + ) CollectFbxCamera: CollectFbxCameraModel = Field( default_factory=CollectFbxCameraModel, title="Collect Camera for FBX export", @@ -768,6 +776,9 @@ DEFAULT_PUBLISH_SETTINGS = { "CollectMayaRender": { "sync_workfile_version": False }, + "CollectFbxAnimation": { + "enabled": True + }, "CollectFbxCamera": { "enabled": False }, @@ -1184,7 +1195,7 @@ DEFAULT_PUBLISH_SETTINGS = { "active": True }, "ValidateSkeletonRigContents": { - "enabled": False, + "enabled": True, "optional": True, "active": True }, From 02a1602e30c0f6b27237c4fa5ad7f9bcd7238631 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Sep 2023 18:57:34 +0800 Subject: [PATCH 078/186] grab the data from the colorspace settings instead of allowing the users set colorspace --- .../hosts/max/plugins/create/create_render.py | 28 +---------------- .../max/plugins/publish/collect_render.py | 7 ++--- .../max/plugins/publish/collect_review.py | 30 +++---------------- 3 files changed, 8 insertions(+), 57 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index b22f016c7c..9cc3c8da8a 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" +import os from openpype.hosts.max.api import plugin from openpype.hosts.max.api.lib_rendersettings import RenderSettings -from openpype.hosts.max.api.lib import get_max_version -from openpype.lib import EnumDef -from pymxs import runtime as rt class CreateRender(plugin.MaxCreator): @@ -31,27 +29,3 @@ class CreateRender(plugin.MaxCreator): RenderSettings(self.project_settings).set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) RenderSettings().render_output(container_name) - - def get_instance_attr_defs(self): - if int(get_max_version()) >= 2024: - default_value = "" - display_views = [] - colorspace_mgr = rt.ColorPipelineMgr - for display in sorted(colorspace_mgr.GetDisplayList()): - for view in sorted(colorspace_mgr.GetViewList(display)): - display_views.append({ - "value": "||".join((display, view)) - }) - if display == "ACES" and view == "sRGB": - default_value = "{0}||{1}".format( - display, view - ) - else: - display_views = ["sRGB||ACES 1.0 SDR-video"] - - return [ - EnumDef("ocio_display_view_transform", - display_views, - default=default_value, - label="OCIO Displays and Views") - ] diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 38fa3843ca..1430ab1094 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -71,10 +71,9 @@ class CollectRender(pyblish.api.InstancePlugin): instance.data["colorspaceView"] = "ACES 1.0 SDR-video" if int(get_max_version()) >= 2024: - creator_attribute = instance.data["creator_attributes"] - display_view_transform = creator_attribute["ocio_display_view_transform"] # noqa - display, view_transform = display_view_transform.split("||", 1) - colorspace_mgr = rt.ColorPipelineMgr + colorspace_mgr = rt.ColorPipelineMgr # noqa + display = next((display for display in colorspace_mgr.GetDisplayList())) + view_transform = next((view for view in colorspace_mgr.GetViewList(display))) instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath instance.data["colorspaceDisplay"] = display instance.data["colorspaceView"] = view_transform diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index 686dc2ed2c..bb85b3ba2b 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -3,7 +3,7 @@ import pyblish.api from pymxs import runtime as rt -from openpype.lib import BoolDef, EnumDef +from openpype.lib import BoolDef from openpype.hosts.max.api.lib import get_max_version from openpype.pipeline.publish import OpenPypePyblishPluginMixin @@ -46,10 +46,9 @@ class CollectReview(pyblish.api.InstancePlugin, } if int(get_max_version()) >= 2024: - display_view_transform = attr_values.get( - "ocio_display_view_transform") - display, view_transform = display_view_transform.split("||", 1) - colorspace_mgr = rt.ColorPipelineMgr + colorspace_mgr = rt.ColorPipelineMgr # noqa + display = next((display for display in colorspace_mgr.GetDisplayList())) + view_transform = next((view for view in colorspace_mgr.GetViewList(display))) instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath instance.data["colorspaceDisplay"] = display instance.data["colorspaceView"] = view_transform @@ -65,28 +64,7 @@ class CollectReview(pyblish.api.InstancePlugin, @classmethod def get_attribute_defs(cls): - default_value = "" - display_views = [] - if int(get_max_version()) >= 2024: - colorspace_mgr = rt.ColorPipelineMgr - displays = colorspace_mgr.GetDisplayList() - for display in sorted(displays): - views = colorspace_mgr.GetViewList(display) - for view in sorted(views): - display_views.append({ - "value": "||".join((display, view)) - }) - if display == "ACES" and view == "sRGB": - default_value = "{0}||{1}".format( - display, view - ) - else: - display_views = ["sRGB||ACES 1.0 SDR-video"] return [ - EnumDef("ocio_display_view_transform", - items=display_views, - default=default_value, - label="OCIO Displays and Views"), BoolDef("dspGeometry", label="Geometry", default=True), From e6db06c5763df15829c1c8c152f29684c524e093 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Sep 2023 18:59:17 +0800 Subject: [PATCH 079/186] hound --- openpype/hosts/max/plugins/publish/collect_render.py | 6 ++++-- openpype/hosts/max/plugins/publish/collect_review.py | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 1430ab1094..7d2b080bcc 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -72,8 +72,10 @@ class CollectRender(pyblish.api.InstancePlugin): if int(get_max_version()) >= 2024: colorspace_mgr = rt.ColorPipelineMgr # noqa - display = next((display for display in colorspace_mgr.GetDisplayList())) - view_transform = next((view for view in colorspace_mgr.GetViewList(display))) + display = next( + (display for display in colorspace_mgr.GetDisplayList())) + view_transform = next( + (view for view in colorspace_mgr.GetViewList(display))) instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath instance.data["colorspaceDisplay"] = display instance.data["colorspaceView"] = view_transform diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index bb85b3ba2b..cd6675e483 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -47,8 +47,10 @@ class CollectReview(pyblish.api.InstancePlugin, if int(get_max_version()) >= 2024: colorspace_mgr = rt.ColorPipelineMgr # noqa - display = next((display for display in colorspace_mgr.GetDisplayList())) - view_transform = next((view for view in colorspace_mgr.GetViewList(display))) + display = next( + (display for display in colorspace_mgr.GetDisplayList())) + view_transform = next( + (view for view in colorspace_mgr.GetViewList(display))) instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath instance.data["colorspaceDisplay"] = display instance.data["colorspaceView"] = view_transform From 61e5005da6657571e2ac695feaac424b0351e7b0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 21 Sep 2023 19:11:03 +0800 Subject: [PATCH 080/186] hound --- openpype/hosts/max/plugins/publish/collect_render.py | 2 +- openpype/hosts/max/plugins/publish/collect_review.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 7d2b080bcc..a359e61921 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -72,7 +72,7 @@ class CollectRender(pyblish.api.InstancePlugin): if int(get_max_version()) >= 2024: colorspace_mgr = rt.ColorPipelineMgr # noqa - display = next( + display = next( (display for display in colorspace_mgr.GetDisplayList())) view_transform = next( (view for view in colorspace_mgr.GetViewList(display))) diff --git a/openpype/hosts/max/plugins/publish/collect_review.py b/openpype/hosts/max/plugins/publish/collect_review.py index cd6675e483..8e27a857d7 100644 --- a/openpype/hosts/max/plugins/publish/collect_review.py +++ b/openpype/hosts/max/plugins/publish/collect_review.py @@ -47,7 +47,7 @@ class CollectReview(pyblish.api.InstancePlugin, if int(get_max_version()) >= 2024: colorspace_mgr = rt.ColorPipelineMgr # noqa - display = next( + display = next( (display for display in colorspace_mgr.GetDisplayList())) view_transform = next( (view for view in colorspace_mgr.GetViewList(display))) From 7dd64ff210078716b80271ab33e81a4ba7266993 Mon Sep 17 00:00:00 2001 From: Kayla Date: Sun, 24 Sep 2023 13:07:03 +0800 Subject: [PATCH 081/186] temporarily remove namespace for fbx export and restore namespace after export --- .../plugins/publish/extract_fbx_animation.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 1c0a0135d2..1d683b2eb7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -30,6 +30,8 @@ class ExtractFBXAnimation(publish.Extractor): # The export requires forward slashes because we need # to format it into a string in a mel expression + + fbx_exporter = fbx.FBXExtractor(log=self.log) out_set = instance.data.get("animated_skeleton", []) # Export @@ -38,7 +40,21 @@ class ExtractFBXAnimation(publish.Extractor): instance.data["referencedAssetsContent"] = True fbx_exporter.set_options_from_instance(instance) - fbx_exporter.export(out_set, path.replace("\\", "/")) + + out_set_name = next(out for out in out_set) + # temporarily disable namespace + namespace = out_set_name.split(":")[0] + new_out_set = out_set_name.replace( + f"{namespace}:", "") + cmds.namespace(set=':') + cmds.namespace(set=namespace) + cmds.namespace(rel=True) + + fbx_exporter.export( + new_out_set, path.replace("\\", "/")) + # restore namespace after export + cmds.namespace(set=':') + cmds.namespace(rel=False) if "representations" not in instance.data: instance.data["representations"] = [] From ce104345236e42fa53ba428672a6c810b60007cb Mon Sep 17 00:00:00 2001 From: Kayla Date: Sun, 24 Sep 2023 13:09:04 +0800 Subject: [PATCH 082/186] hound --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 1d683b2eb7..fb7001bb99 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -30,8 +30,6 @@ class ExtractFBXAnimation(publish.Extractor): # The export requires forward slashes because we need # to format it into a string in a mel expression - - fbx_exporter = fbx.FBXExtractor(log=self.log) out_set = instance.data.get("animated_skeleton", []) # Export From 72737702b6fd92a066ea51191752c6d9d10d57f9 Mon Sep 17 00:00:00 2001 From: Kayla Date: Sun, 24 Sep 2023 13:10:08 +0800 Subject: [PATCH 083/186] hound --- openpype/hosts/maya/plugins/publish/collect_fbx_animation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index 061619dfb1..d89236a73c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- from maya import cmds # noqa import pyblish.api -from openpype.lib import BoolDef from openpype.pipeline import OptionalPyblishPluginMixin + class CollectFbxAnimation(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): """Collect Animated Rig Data for FBX Extractor.""" From 6a4ab981ad2a9f5b6f9d3225a260bb43e7d4ac9b Mon Sep 17 00:00:00 2001 From: Kayla Date: Mon, 25 Sep 2023 23:00:25 +0800 Subject: [PATCH 084/186] add validator to make sure all nodes are refernce nodes in skeleton_Anim_SET --- .../publish/validate_animated_reference.py | 31 +++++++++++++++++++ .../defaults/project_settings/maya.json | 5 +++ .../schemas/schema_maya_publish.json | 4 +++ .../maya/server/settings/publishers.py | 9 ++++++ 4 files changed, 49 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/validate_animated_reference.py diff --git a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py new file mode 100644 index 0000000000..8bf9c61d0d --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py @@ -0,0 +1,31 @@ +import pyblish.api +import openpype.hosts.maya.api.action +from openpype.pipeline.publish import ( + PublishValidationError, + ValidateContentsOrder +) +from maya import cmds + + +class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): + """ + Validate all the nodes underneath skeleton_Anim_SET + should be reference nodes + """ + + order = ValidateContentsOrder + hosts = ["maya"] + families = ["animation.fbx"] + label = "Animated Reference Rig" + actions = [openpype.hosts.maya.api.action.SelectInvalidAction] + + def process(self, instance): + animated_sets = instance.data["animated_skeleton"] + for animated_reference in animated_sets: + is_referenced = cmds.referenceQuery( + animated_reference, isNodeReferenced=True) + if not bool(is_referenced): + raise PublishValidationError( + "All the content in skeleton_Anim_SET" + " should be reference nodes" + ) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index f4fb38ab53..d3e01287e5 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1123,6 +1123,11 @@ "optional": true, "active": true }, + "ValidateAnimatedReferenceRig": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateAnimationContent": { "enabled": true, "optional": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index 6d81f38aa9..f2bbc0f70b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -807,6 +807,10 @@ "key": "ValidateRigControllers", "label": "Validate Rig Controllers" }, + { + "key": "ValidateAnimatedReferenceRig", + "label": "Validate Animated Reference Rig" + }, { "key": "ValidateAnimationContent", "label": "Validate Animation Content" diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index d82daa178c..cb3af191a8 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -652,6 +652,10 @@ class PublishersModel(BaseSettingsModel): default_factory=BasicValidateModel, title="Validate Rig Controllers", ) + ValidateAnimatedReferenceRig: BasicValidateModel = Field( + default_factory=BasicValidateModel, + title="Validate Animated Reference Rig", + ) ValidateAnimationContent: BasicValidateModel = Field( default_factory=BasicValidateModel, title="Validate Animation Content", @@ -1174,6 +1178,11 @@ DEFAULT_PUBLISH_SETTINGS = { "optional": True, "active": True }, + "ValidateAnimatedReferenceRig": { + "enabled": True, + "optional": False, + "active": True + }, "ValidateAnimationContent": { "enabled": True, "optional": False, From a352a6468022cf46febfeaf19c19ba93d5e0d59c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 25 Sep 2023 20:16:56 +0300 Subject: [PATCH 085/186] add JOB path houdini setting --- .../defaults/project_settings/houdini.json | 6 ++++ .../schema_project_houdini.json | 4 +++ .../schemas/schema_houdini_general.json | 28 +++++++++++++++++++ .../houdini/server/settings/general.py | 22 +++++++++++++++ server_addon/houdini/server/settings/main.py | 10 ++++++- server_addon/houdini/server/version.py | 2 +- 6 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json create mode 100644 server_addon/houdini/server/settings/general.py diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 5392fc34dd..2b7244ac85 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,4 +1,10 @@ { + "general": { + "job_path": { + "enabled": true, + "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + } + }, "imageio": { "activate_host_color_management": true, "ocio_config": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json index 7f782e3647..d4d0565ec9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_houdini.json @@ -5,6 +5,10 @@ "label": "Houdini", "is_file": true, "children": [ + { + "type": "schema", + "name": "schema_houdini_general" + }, { "key": "imageio", "type": "dict", diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json new file mode 100644 index 0000000000..c275714ac7 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -0,0 +1,28 @@ +{ + "type": "dict", + "key": "general", + "label": "General", + "collapsible": true, + "is_group": true, + "children": [ + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "job_path", + "label": "JOB Path", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "path", + "label": "Path" + } + ] + } + ] +} diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py new file mode 100644 index 0000000000..242093deeb --- /dev/null +++ b/server_addon/houdini/server/settings/general.py @@ -0,0 +1,22 @@ +from pydantic import Field +from ayon_server.settings import BaseSettingsModel + + +class JobPathModel(BaseSettingsModel): + enabled: bool = Field(title="Enabled") + path: str = Field(title="Path") + + +class GeneralSettingsModel(BaseSettingsModel): + JobPath: JobPathModel = Field( + default_factory=JobPathModel, + title="JOB Path" + ) + + +DEFAULT_GENERAL_SETTINGS = { + "JobPath": { + "enabled": True, + "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + } +} diff --git a/server_addon/houdini/server/settings/main.py b/server_addon/houdini/server/settings/main.py index fdb6838f5c..0c2e160c87 100644 --- a/server_addon/houdini/server/settings/main.py +++ b/server_addon/houdini/server/settings/main.py @@ -4,7 +4,10 @@ from ayon_server.settings import ( MultiplatformPathModel, MultiplatformPathListModel, ) - +from .general import ( + GeneralSettingsModel, + DEFAULT_GENERAL_SETTINGS +) from .imageio import HoudiniImageIOModel from .publish_plugins import ( PublishPluginsModel, @@ -52,6 +55,10 @@ class ShelvesModel(BaseSettingsModel): class HoudiniSettings(BaseSettingsModel): + general: GeneralSettingsModel = Field( + default_factory=GeneralSettingsModel, + title="General" + ) imageio: HoudiniImageIOModel = Field( default_factory=HoudiniImageIOModel, title="Color Management (ImageIO)" @@ -73,6 +80,7 @@ class HoudiniSettings(BaseSettingsModel): DEFAULT_VALUES = { + "general": DEFAULT_GENERAL_SETTINGS, "shelves": [], "create": DEFAULT_HOUDINI_CREATE_SETTINGS, "publish": DEFAULT_HOUDINI_PUBLISH_SETTINGS diff --git a/server_addon/houdini/server/version.py b/server_addon/houdini/server/version.py index ae7362549b..bbab0242f6 100644 --- a/server_addon/houdini/server/version.py +++ b/server_addon/houdini/server/version.py @@ -1 +1 @@ -__version__ = "0.1.3" +__version__ = "0.1.4" From fef45ceea24754049ab7acbafe0a5bb655eca497 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 25 Sep 2023 20:31:44 +0300 Subject: [PATCH 086/186] implement get_current_context_template_data function --- openpype/pipeline/context_tools.py | 43 +++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index f567118062..13b14f1296 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -25,7 +25,10 @@ from openpype.tests.lib import is_in_tests from .publish.lib import filter_pyblish_plugins from .anatomy import Anatomy -from .template_data import get_template_data_with_names +from .template_data import ( + get_template_data_with_names, + get_template_data +) from .workfile import ( get_workfile_template_key, get_custom_workfile_template_by_string_context, @@ -658,3 +661,41 @@ def get_process_id(): if _process_id is None: _process_id = str(uuid.uuid4()) return _process_id + + +def get_current_context_template_data(): + """Template data for template fill from current context + + Returns: + Dict[str, str] of the following tokens and their values + - app + - user + - asset + - parent + - hierarchy + - folder[name] + - root[work, ...] + - studio[code, name] + - project[code, name] + - task[type, name, short] + """ + + # pre-prepare get_template_data args + current_context = get_current_context() + project_name = current_context["project_name"] + asset_name = current_context["asset_name"] + anatomy = Anatomy(project_name) + + # prepare get_template_data args + project_doc = get_project(project_name) + asset_doc = get_asset_by_name(project_name, asset_name) + task_name = current_context["task_name"] + host_name = get_current_host_name() + + # get template data + template_data = get_template_data( + project_doc, asset_doc, task_name, host_name + ) + + template_data["root"] = anatomy.roots + return template_data From 59a20fe0fb77d32af3165927d3b0e0fd1d71be81 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 25 Sep 2023 21:58:11 +0300 Subject: [PATCH 087/186] implement validate_job_path and register it in houdini callbacks --- openpype/hosts/houdini/api/lib.py | 35 +++++++++++++++++++++++++- openpype/hosts/houdini/api/pipeline.py | 6 +++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index a3f691e1fc..bdc8e0e973 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -9,9 +9,14 @@ import json import six +from openpype.lib import StringTemplate from openpype.client import get_asset_by_name +from openpype.settings import get_current_project_settings from openpype.pipeline import get_current_project_name, get_current_asset_name -from openpype.pipeline.context_tools import get_current_project_asset +from openpype.pipeline.context_tools import ( + get_current_context_template_data, + get_current_project_asset +) import hou @@ -747,3 +752,31 @@ def get_camera_from_container(container): assert len(cameras) == 1, "Camera instance must have only one camera" return cameras[0] + + +def validate_job_path(): + """Validate job path to ensure it matches the settings.""" + + project_settings = get_current_project_settings() + + if project_settings["houdini"]["general"]["job_path"]["enabled"]: + + # get and resolve job path template + job_path_template = project_settings["houdini"]["general"]["job_path"]["path"] + job_path = StringTemplate.format_template( + job_path_template, get_current_context_template_data() + ) + job_path = job_path.replace("\\","/") + + if job_path == "": + # Set JOB path to HIP path if JOB path is enabled + # and has empty value. + job_path = os.environ["HIP"] + + current_job = hou.hscript("echo -n `$JOB`")[0] + if current_job != job_path: + hou.hscript("set JOB=" + job_path) + os.environ["JOB"] = job_path + print(" - set $JOB to " + job_path) + else: + print(" - JOB Path is disabled, Skipping Check...") diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 6aa65deb89..48cc9e2150 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -300,6 +300,9 @@ def on_save(): log.info("Running callback on save..") + # Validate $JOB value + lib.validate_job_path() + nodes = lib.get_id_required_nodes() for node, new_id in lib.generate_ids(nodes): lib.set_id(node, new_id, overwrite=False) @@ -335,6 +338,9 @@ def on_open(): log.info("Running callback on open..") + # Validate $JOB value + lib.validate_job_path() + # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset lib.validate_fps() From fdea715fe0d7aafd7f3000b8aca7780d432aeacb Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Mon, 25 Sep 2023 22:23:41 +0300 Subject: [PATCH 088/186] resolve hound --- openpype/hosts/houdini/api/lib.py | 5 +++-- server_addon/houdini/server/settings/general.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index bdc8e0e973..876d39b757 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -762,11 +762,12 @@ def validate_job_path(): if project_settings["houdini"]["general"]["job_path"]["enabled"]: # get and resolve job path template - job_path_template = project_settings["houdini"]["general"]["job_path"]["path"] + job_path_template = \ + project_settings["houdini"]["general"]["job_path"]["path"] job_path = StringTemplate.format_template( job_path_template, get_current_context_template_data() ) - job_path = job_path.replace("\\","/") + job_path = job_path.replace("\\", "/") if job_path == "": # Set JOB path to HIP path if JOB path is enabled diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 242093deeb..f5fed1c248 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -17,6 +17,6 @@ class GeneralSettingsModel(BaseSettingsModel): DEFAULT_GENERAL_SETTINGS = { "JobPath": { "enabled": True, - "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa } } From 8fd323fb16a52a2a0dbd3979cc4eaf7d37dca40d Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 26 Sep 2023 12:49:24 +0800 Subject: [PATCH 089/186] add the validation to make sure the skeleton_Anim_SET should be bone hierarchy only --- .../publish/validate_animated_reference.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py index 8bf9c61d0d..0034599976 100644 --- a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py +++ b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py @@ -18,9 +18,15 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): families = ["animation.fbx"] label = "Animated Reference Rig" actions = [openpype.hosts.maya.api.action.SelectInvalidAction] + accepted_controllers = ["transform", "locator"] def process(self, instance): animated_sets = instance.data["animated_skeleton"] + if not animated_sets: + self.log.debug( + "No nodes found in skeleton_Anim_SET..Skipping..") + return + for animated_reference in animated_sets: is_referenced = cmds.referenceQuery( animated_reference, isNodeReferenced=True) @@ -29,3 +35,30 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): "All the content in skeleton_Anim_SET" " should be reference nodes" ) + invalid_controls = self.validate_controls(animated_sets) + if invalid_controls: + raise PublishValidationError( + "All the content in skeleton_Anim_SET" + " should be the transforms" + ) + def validate_controls(self, set_members): + """Check if the controller set passes the validations + + Checks if all its set members are within the hierarchy of the root + Checks if the node types of the set members valid + + Args: + set_members: list of nodes of the skeleton_anim_set + hierarchy: list of nodes which reside under the root node + + Returns: + errors (list) + """ + + # Validate control types + invalid = [] + for node in set_members: + if cmds.nodeType(node) not in self.accepted_controllers: + invalid.append(node) + + return invalid From b33ddb05de211e71151b95f139aae8f99c30e874 Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 26 Sep 2023 12:51:14 +0800 Subject: [PATCH 090/186] hound --- .../maya/plugins/publish/validate_animated_reference.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py index 0034599976..63c0b6958d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py +++ b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py @@ -38,9 +38,10 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): invalid_controls = self.validate_controls(animated_sets) if invalid_controls: raise PublishValidationError( - "All the content in skeleton_Anim_SET" - " should be the transforms" - ) + "All the content in skeleton_Anim_SET" + " should be the transforms" + ) + def validate_controls(self, set_members): """Check if the controller set passes the validations From d1395fe4099bc98dd3bbaf016029fc5d480d0a3c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 26 Sep 2023 16:22:18 +0300 Subject: [PATCH 091/186] update settings names --- openpype/hosts/houdini/api/lib.py | 6 +++--- .../defaults/project_settings/houdini.json | 4 ++-- .../schemas/schema_houdini_general.json | 8 ++++---- server_addon/houdini/server/settings/general.py | 14 +++++++------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 876d39b757..73a6f452d0 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -758,12 +758,12 @@ def validate_job_path(): """Validate job path to ensure it matches the settings.""" project_settings = get_current_project_settings() + project_settings = project_settings["houdini"]["general"]["update_job_var_context"] - if project_settings["houdini"]["general"]["job_path"]["enabled"]: + if project_settings["enabled"]: # get and resolve job path template - job_path_template = \ - project_settings["houdini"]["general"]["job_path"]["path"] + job_path_template = project_settings["job_path"] job_path = StringTemplate.format_template( job_path_template, get_current_context_template_data() ) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 2b7244ac85..5057db1f03 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,8 +1,8 @@ { "general": { - "job_path": { + "update_job_var_context": { "enabled": true, - "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + "job_path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" } }, "imageio": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json index c275714ac7..eecc29592a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -9,8 +9,8 @@ "type": "dict", "collapsible": true, "checkbox_key": "enabled", - "key": "job_path", - "label": "JOB Path", + "key": "update_job_var_context", + "label": "Update $JOB on context change", "children": [ { "type": "boolean", @@ -19,8 +19,8 @@ }, { "type": "text", - "key": "path", - "label": "Path" + "key": "job_path", + "label": "JOB Path" } ] } diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index f5fed1c248..f47fa9c564 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -2,21 +2,21 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel -class JobPathModel(BaseSettingsModel): +class UpdateJobVarcontextModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") - path: str = Field(title="Path") + job_path: str = Field(title="JOB Path") class GeneralSettingsModel(BaseSettingsModel): - JobPath: JobPathModel = Field( - default_factory=JobPathModel, - title="JOB Path" + update_job_var_context: UpdateJobVarcontextModel = Field( + default_factory=UpdateJobVarcontextModel, + title="Update $JOB on context change" ) DEFAULT_GENERAL_SETTINGS = { - "JobPath": { + "update_job_var_context": { "enabled": True, - "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa + "job_path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa } } From 30e2ecb8595213542626b51f8514101054d10fef Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 26 Sep 2023 16:26:14 +0300 Subject: [PATCH 092/186] BigRoy's comment --- openpype/hosts/houdini/api/lib.py | 7 +++---- openpype/hosts/houdini/api/pipeline.py | 4 ++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 73a6f452d0..8624f09289 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -754,11 +754,12 @@ def get_camera_from_container(container): return cameras[0] -def validate_job_path(): +def update_job_var_context(): """Validate job path to ensure it matches the settings.""" project_settings = get_current_project_settings() - project_settings = project_settings["houdini"]["general"]["update_job_var_context"] + project_settings = \ + project_settings["houdini"]["general"]["update_job_var_context"] if project_settings["enabled"]: @@ -779,5 +780,3 @@ def validate_job_path(): hou.hscript("set JOB=" + job_path) os.environ["JOB"] = job_path print(" - set $JOB to " + job_path) - else: - print(" - JOB Path is disabled, Skipping Check...") diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 48cc9e2150..3efbbb12b3 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -301,7 +301,7 @@ def on_save(): log.info("Running callback on save..") # Validate $JOB value - lib.validate_job_path() + lib.update_job_var_context() nodes = lib.get_id_required_nodes() for node, new_id in lib.generate_ids(nodes): @@ -339,7 +339,7 @@ def on_open(): log.info("Running callback on open..") # Validate $JOB value - lib.validate_job_path() + lib.update_job_var_context() # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset From fd8daebed9bbb579c13f193253c00a5d7cf22005 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 26 Sep 2023 20:25:51 +0300 Subject: [PATCH 093/186] update log message --- openpype/hosts/houdini/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 8624f09289..c8211f45d2 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -779,4 +779,5 @@ def update_job_var_context(): if current_job != job_path: hou.hscript("set JOB=" + job_path) os.environ["JOB"] = job_path - print(" - set $JOB to " + job_path) + print(" - Context changed, update $JOB respectively to " + + job_path) From a349733ecf418f353f940969aea1ab0a6e1aaff8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Tue, 26 Sep 2023 20:38:05 +0300 Subject: [PATCH 094/186] sync $JOB and [JOB] --- openpype/hosts/houdini/api/lib.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index c8211f45d2..ac28163144 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -776,6 +776,12 @@ def update_job_var_context(): job_path = os.environ["HIP"] current_job = hou.hscript("echo -n `$JOB`")[0] + + # sync both environment variables. + # because when opening new file $JOB is overridden with + # the value saved in the HIP file but os.environ["JOB"] is not! + os.environ["JOB"] = current_job + if current_job != job_path: hou.hscript("set JOB=" + job_path) os.environ["JOB"] = job_path From f15dcb30b87daf23b3c0a087237cf50e7b26ddf8 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 12:54:02 +0300 Subject: [PATCH 095/186] create JOB folder if not exists --- openpype/hosts/houdini/api/lib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ac28163144..9fe5ac83ce 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -785,5 +785,8 @@ def update_job_var_context(): if current_job != job_path: hou.hscript("set JOB=" + job_path) os.environ["JOB"] = job_path + + os.makedirs(job_path, exist_ok=True) + print(" - Context changed, update $JOB respectively to " + job_path) From edcaa8b62f0be86425da063efe55bc0377fa8c8c Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 16:02:38 +0300 Subject: [PATCH 096/186] update docs --- website/docs/admin_hosts_houdini.md | 19 +++++++++++++++++- .../houdini/update-job-context-change.png | Bin 0 -> 8068 bytes 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 website/docs/assets/houdini/update-job-context-change.png diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 64c54db591..1e82dd97dd 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -3,9 +3,26 @@ id: admin_hosts_houdini title: Houdini sidebar_label: Houdini --- +## General Settings +### JOB Path +you can add your studios preffered JOB Path, JOB value will be checked and updated on file save and open. +Disableing this option will effectivly turn off this feature. + +JOB Path can be: +- Arbitrary hardcoded path +- Openpype template path + > This allows dynamic values for assets or shots.
+ > Using template keys is supported but formatting keys capitalization variants is not, + > e.g. {Asset} and {ASSET} won't work +- empty + > In this case, JOB will be synced to HIP + +![update job on context change](assets/houdini/update-job-context-change.png) + + ## Shelves Manager You can add your custom shelf set into Houdini by setting your shelf sets, shelves and tools in **Houdini -> Shelves Manager**. ![Custom menu definition](assets/houdini-admin_shelvesmanager.png) -The Shelf Set Path is used to load a .shelf file to generate your shelf set. If the path is specified, you don't have to set the shelves and tools. \ No newline at end of file +The Shelf Set Path is used to load a .shelf file to generate your shelf set. If the path is specified, you don't have to set the shelves and tools. diff --git a/website/docs/assets/houdini/update-job-context-change.png b/website/docs/assets/houdini/update-job-context-change.png new file mode 100644 index 0000000000000000000000000000000000000000..0faf317a227291024d95d7a60b9f1572ec57995a GIT binary patch literal 8068 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 literal 0 HcmV?d00001 From ef785e75e39d581a86cbcec76f151cd26cde6ebd Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 16:31:47 +0300 Subject: [PATCH 097/186] update os.makedirs --- openpype/hosts/houdini/api/lib.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 9fe5ac83ce..abc6d5d0f5 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import sys import os +import errno import re import uuid import logging @@ -786,7 +787,14 @@ def update_job_var_context(): hou.hscript("set JOB=" + job_path) os.environ["JOB"] = job_path - os.makedirs(job_path, exist_ok=True) + try: + os.makedirs(job_path) + except OSError as e: + if e.errno != errno.EEXIST: + print( + " - Failed to create JOB dir. Maybe due to " + "insufficient permissions." + ) print(" - Context changed, update $JOB respectively to " + job_path) From a3cb6c445684c5859721a2cdf5961061705d906f Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 16:33:06 +0300 Subject: [PATCH 098/186] resolve hound --- openpype/hosts/houdini/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index abc6d5d0f5..1b04fd692a 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -792,8 +792,8 @@ def update_job_var_context(): except OSError as e: if e.errno != errno.EEXIST: print( - " - Failed to create JOB dir. Maybe due to " - "insufficient permissions." + " - Failed to create JOB dir. Maybe due to " + "insufficient permissions." ) print(" - Context changed, update $JOB respectively to " From 05cef1ed91d1982edd6e0cf8143025f0968371d5 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 16:38:52 +0300 Subject: [PATCH 099/186] Minikiu comments --- openpype/hosts/houdini/api/lib.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 1b04fd692a..1ea71fa2a7 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -759,15 +759,15 @@ def update_job_var_context(): """Validate job path to ensure it matches the settings.""" project_settings = get_current_project_settings() - project_settings = \ + job_var_settings = \ project_settings["houdini"]["general"]["update_job_var_context"] - if project_settings["enabled"]: + if job_var_settings["enabled"]: # get and resolve job path template - job_path_template = project_settings["job_path"] job_path = StringTemplate.format_template( - job_path_template, get_current_context_template_data() + job_var_settings["job_path"], + get_current_context_template_data() ) job_path = job_path.replace("\\", "/") From 260650ea43b7850ead2a2820b02bca05eef8d710 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 16:51:50 +0300 Subject: [PATCH 100/186] update docs --- website/docs/admin_hosts_houdini.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 1e82dd97dd..9c9536a26e 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -5,16 +5,18 @@ sidebar_label: Houdini --- ## General Settings ### JOB Path -you can add your studios preffered JOB Path, JOB value will be checked and updated on file save and open. -Disableing this option will effectivly turn off this feature. +Specify a studio-wide `JOB` path.
+The Houdini `$JOB` path can be customized through project settings with a (dynamic) path that will be updated on context changes, e.g. when switching to another asset or task. + +Disabling this feature will leave `$JOB` var unmanaged and thus no context update changes will occur. JOB Path can be: -- Arbitrary hardcoded path +- Arbitrary path - Openpype template path > This allows dynamic values for assets or shots.
> Using template keys is supported but formatting keys capitalization variants is not, > e.g. {Asset} and {ASSET} won't work -- empty +- Empty > In this case, JOB will be synced to HIP ![update job on context change](assets/houdini/update-job-context-change.png) From 346544df3cb84a3db153945d768dc8175a36957e Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 17:23:33 +0300 Subject: [PATCH 101/186] update docs 2 --- website/docs/admin_hosts_houdini.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 9c9536a26e..2d345d2d76 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -5,9 +5,11 @@ sidebar_label: Houdini --- ## General Settings ### JOB Path -Specify a studio-wide `JOB` path.
+ The Houdini `$JOB` path can be customized through project settings with a (dynamic) path that will be updated on context changes, e.g. when switching to another asset or task. +> If the folder does not exist on the context change it will be created by this feature so that `$JOB` will always try to point to an existing folder. + Disabling this feature will leave `$JOB` var unmanaged and thus no context update changes will occur. JOB Path can be: From 67964bec3aadcf8035b966c6a77ab586a1f797c3 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 17:47:08 +0300 Subject: [PATCH 102/186] fix format --- website/docs/admin_hosts_houdini.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 2d345d2d76..75b0922dac 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -17,7 +17,7 @@ JOB Path can be: - Openpype template path > This allows dynamic values for assets or shots.
> Using template keys is supported but formatting keys capitalization variants is not, - > e.g. {Asset} and {ASSET} won't work + > e.g. `{Asset}` and `{ASSET}` won't work - Empty > In this case, JOB will be synced to HIP From 61ce75f0c9e23f9d7f36d5bd2d7fc10fe5143514 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Wed, 27 Sep 2023 19:12:56 +0300 Subject: [PATCH 103/186] BigRoy's comment --- openpype/hosts/houdini/api/lib.py | 8 +++++--- website/docs/admin_hosts_houdini.md | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 1ea71fa2a7..5302fbea74 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -756,7 +756,10 @@ def get_camera_from_container(container): def update_job_var_context(): - """Validate job path to ensure it matches the settings.""" + """Update $JOB to match current context. + + This will only do something if the setting is enabled in project settings. + """ project_settings = get_current_project_settings() job_var_settings = \ @@ -796,5 +799,4 @@ def update_job_var_context(): "insufficient permissions." ) - print(" - Context changed, update $JOB respectively to " - + job_path) + print(" - Updated $JOB to {}".format(job_path)) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 75b0922dac..ea7991530b 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -8,7 +8,9 @@ sidebar_label: Houdini The Houdini `$JOB` path can be customized through project settings with a (dynamic) path that will be updated on context changes, e.g. when switching to another asset or task. -> If the folder does not exist on the context change it will be created by this feature so that `$JOB` will always try to point to an existing folder. +:::note +If the folder does not exist on the context change it will be created by this feature so that `$JOB` will always try to point to an existing folder. +::: Disabling this feature will leave `$JOB` var unmanaged and thus no context update changes will occur. From 7197134954f4490dcb84032055d17495e4e189a2 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 28 Sep 2023 00:46:47 +0300 Subject: [PATCH 104/186] Allow adding more Houdini vars --- openpype/hosts/houdini/api/lib.py | 74 +++++++++++-------- openpype/hosts/houdini/api/pipeline.py | 8 +- .../defaults/project_settings/houdini.json | 6 +- .../schemas/schema_houdini_general.json | 15 ++-- .../houdini/server/settings/general.py | 29 ++++++-- 5 files changed, 82 insertions(+), 50 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 5302fbea74..f8d17eef07 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -755,48 +755,58 @@ def get_camera_from_container(container): return cameras[0] -def update_job_var_context(): - """Update $JOB to match current context. +def update_houdini_vars_context(): + """Update Houdini vars to match current context. This will only do something if the setting is enabled in project settings. """ project_settings = get_current_project_settings() - job_var_settings = \ - project_settings["houdini"]["general"]["update_job_var_context"] + houdini_vars_settings = \ + project_settings["houdini"]["general"]["update_houdini_var_context"] - if job_var_settings["enabled"]: + if houdini_vars_settings["enabled"]: + houdini_vars = houdini_vars_settings["houdini_vars"] - # get and resolve job path template - job_path = StringTemplate.format_template( - job_var_settings["job_path"], - get_current_context_template_data() - ) - job_path = job_path.replace("\\", "/") + # Remap AYON settings structure to OpenPype settings structure + # It allows me to use the same logic for both AYON and OpenPype + if isinstance(houdini_vars, list): + items = {} + for item in houdini_vars: + items.update({item["var"]: item["path"]}) - if job_path == "": - # Set JOB path to HIP path if JOB path is enabled - # and has empty value. - job_path = os.environ["HIP"] + houdini_vars = items - current_job = hou.hscript("echo -n `$JOB`")[0] + for var, path in houdini_vars.items(): + # get and resolve job path template + path = StringTemplate.format_template( + path, + get_current_context_template_data() + ) + path = path.replace("\\", "/") - # sync both environment variables. - # because when opening new file $JOB is overridden with - # the value saved in the HIP file but os.environ["JOB"] is not! - os.environ["JOB"] = current_job + if var == "JOB" and path == "": + # sync $JOB to $HIP if $JOB is empty + path = os.environ["HIP"] - if current_job != job_path: - hou.hscript("set JOB=" + job_path) - os.environ["JOB"] = job_path + current_path = hou.hscript("echo -n `${}`".format(var))[0] - try: - os.makedirs(job_path) - except OSError as e: - if e.errno != errno.EEXIST: - print( - " - Failed to create JOB dir. Maybe due to " - "insufficient permissions." - ) + # sync both environment variables. + # because houdini doesn't do that by default + # on opening new files + os.environ[var] = current_path - print(" - Updated $JOB to {}".format(job_path)) + if current_path != path: + hou.hscript("set {}={}".format(var, path)) + os.environ[var] = path + + try: + os.makedirs(path) + except OSError as e: + if e.errno != errno.EEXIST: + print( + " - Failed to create {} dir. Maybe due to " + "insufficient permissions.".format(var) + ) + + print(" - Updated ${} to {}".format(var, path)) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 3efbbb12b3..f753d518f0 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -300,8 +300,8 @@ def on_save(): log.info("Running callback on save..") - # Validate $JOB value - lib.update_job_var_context() + # update houdini vars + lib.update_houdini_vars_context() nodes = lib.get_id_required_nodes() for node, new_id in lib.generate_ids(nodes): @@ -338,8 +338,8 @@ def on_open(): log.info("Running callback on open..") - # Validate $JOB value - lib.update_job_var_context() + # update houdini vars + lib.update_houdini_vars_context() # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 5057db1f03..b2fcb708cf 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -1,8 +1,10 @@ { "general": { - "update_job_var_context": { + "update_houdini_var_context": { "enabled": true, - "job_path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + "houdini_vars":{ + "JOB": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" + } } }, "imageio": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json index eecc29592a..127382f4bc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -9,8 +9,8 @@ "type": "dict", "collapsible": true, "checkbox_key": "enabled", - "key": "update_job_var_context", - "label": "Update $JOB on context change", + "key": "update_houdini_var_context", + "label": "Update Houdini Vars on context change", "children": [ { "type": "boolean", @@ -18,9 +18,14 @@ "label": "Enabled" }, { - "type": "text", - "key": "job_path", - "label": "JOB Path" + "type": "dict-modifiable", + "key": "houdini_vars", + "label": "Houdini Vars", + "collapsible": false, + "object_type": { + "type": "path", + "multiplatform": false + } } ] } diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index f47fa9c564..42a071a688 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -2,21 +2,36 @@ from pydantic import Field from ayon_server.settings import BaseSettingsModel -class UpdateJobVarcontextModel(BaseSettingsModel): +class HoudiniVarModel(BaseSettingsModel): + _layout = "expanded" + var: str = Field("", title="Var") + path: str = Field(default_factory="", title="Path") + + +class UpdateHoudiniVarcontextModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") - job_path: str = Field(title="JOB Path") + # TODO this was dynamic dictionary '{var: path}' + houdini_vars: list[HoudiniVarModel] = Field( + default_factory=list, + title="Houdini Vars" + ) class GeneralSettingsModel(BaseSettingsModel): - update_job_var_context: UpdateJobVarcontextModel = Field( - default_factory=UpdateJobVarcontextModel, - title="Update $JOB on context change" + update_houdini_var_context: UpdateHoudiniVarcontextModel = Field( + default_factory=UpdateHoudiniVarcontextModel, + title="Update Houdini Vars on context change" ) DEFAULT_GENERAL_SETTINGS = { - "update_job_var_context": { + "update_houdini_var_context": { "enabled": True, - "job_path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa + "houdini_vars": [ + { + "var": "JOB", + "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa + } + ] } } From ed02bf31114e885a157c8ef13f8474cc86d093a1 Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 17:51:08 +0800 Subject: [PATCH 105/186] remove invalid actions and some code tweaks --- openpype/hosts/maya/plugins/publish/collect_fbx_animation.py | 2 +- .../hosts/maya/plugins/publish/validate_animated_reference.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index d89236a73c..ff7d068d7d 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -22,7 +22,7 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin, if i.lower().endswith("skeletonanim_set") ] if skeleton_sets: - instance.data["families"] += ["animation.fbx"] + instance.data["families"].append("animation.fbx") instance.data["animated_skeleton"] = [] for skeleton_set in skeleton_sets: skeleton_content = cmds.sets(skeleton_set, query=True) diff --git a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py index 63c0b6958d..c1b5a2852d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py +++ b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py @@ -1,5 +1,4 @@ import pyblish.api -import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder @@ -17,7 +16,6 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): hosts = ["maya"] families = ["animation.fbx"] label = "Animated Reference Rig" - actions = [openpype.hosts.maya.api.action.SelectInvalidAction] accepted_controllers = ["transform", "locator"] def process(self, instance): From a433c46e727862a28a0a4835f582135b588e67e6 Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 19:50:44 +0800 Subject: [PATCH 106/186] code tweak on extract fbx animation --- .../hosts/maya/plugins/publish/extract_fbx_animation.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index fb7001bb99..f8b7c18614 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -54,15 +54,12 @@ class ExtractFBXAnimation(publish.Extractor): cmds.namespace(set=':') cmds.namespace(rel=False) - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { + representations = instance.data.setdefault("representations", []) + representations.append({ 'name': 'fbx', 'ext': 'fbx', 'files': filename, "stagingDir": staging_dir - } - instance.data["representations"].append(representation) + }) self.log.debug("Extract animated FBX successful to: {0}".format(path)) From dda932e83ef448c74533ea39f687a6d919a2551c Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 22:37:30 +0800 Subject: [PATCH 107/186] code clean up and tweak on debug mesg --- openpype/hosts/maya/api/lib.py | 11 ---------- .../hosts/maya/plugins/create/create_rig.py | 4 ++-- .../plugins/publish/collect_fbx_animation.py | 22 ++++++++++--------- .../plugins/publish/collect_skeleton_mesh.py | 22 +++++++++---------- .../plugins/publish/extract_fbx_animation.py | 17 +++++++------- .../plugins/publish/extract_skeleton_mesh.py | 12 +++++----- .../publish/validate_animated_reference.py | 15 +++++-------- .../publish/validate_skeleton_rig_content.py | 6 ++--- .../validate_skeleton_top_group_hierarchy.py | 10 ++++----- 9 files changed, 50 insertions(+), 69 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index fed2887419..03a864a1db 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -4111,13 +4111,7 @@ def create_rig_animation_instance( anim_skeleton = next((node for node in nodes if node.endswith("skeletonAnim_SET")), None) - if not anim_skeleton: - log.debug("No skeletonAnim_SET in rig") - skeleton_mesh = next((node for node in nodes if - node.endswith("skeletonMesh_SET")), None) - if not skeleton_mesh: - log.debug("No skeletonMesh_SET in rig") # Find the roots amongst the loaded nodes roots = ( cmds.ls(nodes, assemblies=True, long=True) or @@ -4128,9 +4122,6 @@ def create_rig_animation_instance( custom_subset = options.get("animationSubsetName") if custom_subset: formatting_data = { - # TODO remove 'asset_type' and replace 'asset_name' with 'asset' - # "asset_name": context['asset']['name'], - # "asset_type": context['asset']['type'], "asset": context["asset"], "subset": context['subset']['name'], "family": ( @@ -4156,8 +4147,6 @@ def create_rig_animation_instance( rig_sets = [output, controls] if anim_skeleton: rig_sets.append(anim_skeleton) - if skeleton_mesh: - rig_sets.append(skeleton_mesh) with maintained_selection(): cmds.select(rig_sets + roots, noExpand=True) create_context.create( diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 69c7787905..22a94ed4fd 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -20,9 +20,9 @@ class CreateRig(plugin.MayaCreator): instance_node = instance.get("instance_node") self.log.info("Creating Rig instance set up ...") - # change name (_controls_set -> _rigs_SET) + # TODO:change name (_controls_set -> _rigs_SET) controls = cmds.sets(name=subset_name + "_controls_SET", empty=True) - # change name (_out_SET -> _geo_SET) + # TODO:change name (_out_SET -> _geo_SET) pointcache = cmds.sets(name=subset_name + "_out_SET", empty=True) skeleton = cmds.sets( name=subset_name + "_skeletonAnim_SET", empty=True) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index ff7d068d7d..9347936e63 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -21,13 +21,15 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin, i for i in instance if i.lower().endswith("skeletonanim_set") ] - if skeleton_sets: - instance.data["families"].append("animation.fbx") - instance.data["animated_skeleton"] = [] - for skeleton_set in skeleton_sets: - skeleton_content = cmds.sets(skeleton_set, query=True) - self.log.debug( - "Collected animated " - f"skeleton data: {skeleton_content}") - if skeleton_content: - instance.data["animated_skeleton"] += skeleton_content + if not skeleton_sets: + return + + instance.data["families"].append("animation.fbx") + instance.data["animated_skeleton"] = [] + for skeleton_set in skeleton_sets: + skeleton_content = cmds.sets(skeleton_set, query=True) + self.log.debug( + "Collected animated " + f"skeleton data: {skeleton_content}") + if skeleton_content: + instance.data["animated_skeleton"] += skeleton_content diff --git a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py index 5d894c99a0..73b2103618 100644 --- a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py @@ -12,22 +12,20 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): families = ["rig"] def process(self, instance): + skeleton_sets = instance.data.get("skeletonAnim_SET") + skeleton_mesh_sets = instance.data.get("skeletonMesh_SET") + if not skeleton_mesh_sets: + self.log.debug( + "skeletonMesh_SET found. " + "Skipping collecting of skeleton mesh..." + ) + return + + # Store current frame to ensure single frame export frame = cmds.currentTime(query=True) instance.data["frameStart"] = frame instance.data["frameEnd"] = frame - skeleton_sets = [ - i for i in instance - if i.lower().endswith("skeletonanim_set") - ] - skeleton_mesh_sets = [ - i for i in instance - if i.lower().endswith("skeletonmesh_set") - ] - if not skeleton_sets and skeleton_mesh_sets: - self.log.debug( - "no skeleton_set or skeleton_mesh set was found....") - return instance.data["skeleton_mesh"] = [] instance.data["skeleton_rig"] = [] diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index f8b7c18614..748f30e43d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -6,6 +6,7 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api import fbx +from openpype.hosts.maya.api.lib import namespaced class ExtractFBXAnimation(publish.Extractor): @@ -27,9 +28,8 @@ class ExtractFBXAnimation(publish.Extractor): staging_dir = self.staging_dir(instance) filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) + path = path.replace("\\", "/") - # The export requires forward slashes because we need - # to format it into a string in a mel expression fbx_exporter = fbx.FBXExtractor(log=self.log) out_set = instance.data.get("animated_skeleton", []) # Export @@ -44,12 +44,11 @@ class ExtractFBXAnimation(publish.Extractor): namespace = out_set_name.split(":")[0] new_out_set = out_set_name.replace( f"{namespace}:", "") - cmds.namespace(set=':') - cmds.namespace(set=namespace) - cmds.namespace(rel=True) - - fbx_exporter.export( - new_out_set, path.replace("\\", "/")) + cmds.namespace(set=':' + namespace) + cmds.namespace(relativeNames=True) + with namespaced(":" + namespace, new=False) as namespace: + fbx_exporter.export( + new_out_set, path.replace("\\", "/")) # restore namespace after export cmds.namespace(set=':') cmds.namespace(rel=False) @@ -62,4 +61,4 @@ class ExtractFBXAnimation(publish.Extractor): "stagingDir": staging_dir }) - self.log.debug("Extract animated FBX successful to: {0}".format(path)) + self.log.debug("Extracted Fbx animation successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py b/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py index c9fe53f0be..42cbb33013 100644 --- a/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py @@ -43,17 +43,15 @@ class ExtractSkeletonMesh(publish.Extractor, fbx_exporter.set_options_from_instance(instance) # Export - fbx_exporter.export(out_set, path.replace("\\", "/")) + path = path.replace("\\", "/") + fbx_exporter.export(out_set, path) - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { + representations = instance.data.setdefault("representations", []) + representations.append({ 'name': 'fbx', 'ext': 'fbx', 'files': filename, "stagingDir": staging_dir - } - instance.data["representations"].append(representation) + }) self.log.debug("Extract animated FBX successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py index c1b5a2852d..3dc272d7cc 100644 --- a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py +++ b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py @@ -7,10 +7,7 @@ from maya import cmds class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): - """ - Validate all the nodes underneath skeleton_Anim_SET - should be reference nodes - """ + """Validate all nodes in skeletonAnim_SET are referenced""" order = ValidateContentsOrder hosts = ["maya"] @@ -22,7 +19,7 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): animated_sets = instance.data["animated_skeleton"] if not animated_sets: self.log.debug( - "No nodes found in skeleton_Anim_SET..Skipping..") + "No nodes found in skeletonAnim_SET.Skipping...") return for animated_reference in animated_sets: @@ -30,14 +27,14 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): animated_reference, isNodeReferenced=True) if not bool(is_referenced): raise PublishValidationError( - "All the content in skeleton_Anim_SET" - " should be reference nodes" + "All the content in skeletonAnim_SET" + " should be referenced nodes" ) invalid_controls = self.validate_controls(animated_sets) if invalid_controls: raise PublishValidationError( - "All the content in skeleton_Anim_SET" - " should be the transforms" + "All the content in skeletonAnim_SET" + " should be transforms" ) def validate_controls(self, set_members): diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py index 8b8800af17..c7e724b569 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -45,12 +45,10 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): self.log.debug("Skipping empty instance...") return # Ensure contents in sets and retrieve long path for all objects - skeleton_mesh_content = cmds.sets( - skeleton_mesh_set, query=True) or [] + skeleton_mesh_content = instance.data.get("skeleton_mesh", []) skeleton_mesh_content = cmds.ls(skeleton_mesh_content, long=True) - skeleton_anim_content = cmds.sets( - skeleton_anim_set, query=True) or [] + skeleton_anim_content = instance.data.get("skeleton_rig", []) skeleton_anim_content = cmds.ls(skeleton_anim_content, long=True) # Validate members are inside the hierarchy from root node diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py index df434f132d..1e0d856b4e 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py @@ -25,18 +25,18 @@ class ValidateSkeletonTopGroupHierarchy(pyblish.api.InstancePlugin, def process(self, instance): invalid = [] skeleton_data = instance.data.get(("animated_rigs"), []) - skeletonMesh_data = instance.data(("skeleton_mesh"), []) + skeleton_mesh_data = instance.data(("skeleton_mesh"), []) if skeleton_data: invalid = self.get_top_hierarchy(skeleton_data) if invalid: raise PublishValidationError( - "The set includes the object which " + "The skeletonAnim_SET includes the object which " f"is not at the top hierarchy: {invalid}") - if skeletonMesh_data: - invalid = self.get_top_hierarchy(skeletonMesh_data) + if skeleton_mesh_data: + invalid = self.get_top_hierarchy(skeleton_mesh_data) if invalid: raise PublishValidationError( - "The set includes the object which " + "The skeletonMesh_SET includes the object which " f"is not at the top hierarchy: {invalid}") def get_top_hierarchy(self, targets): From 0829adceda7fb8258b47dc7eb9a94691e789c77b Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 22:41:07 +0800 Subject: [PATCH 108/186] hound --- .../hosts/maya/plugins/publish/extract_fbx_animation.py | 6 ++---- .../maya/plugins/publish/validate_skeleton_rig_content.py | 2 -- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 748f30e43d..e99e7d40bd 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -49,9 +49,6 @@ class ExtractFBXAnimation(publish.Extractor): with namespaced(":" + namespace, new=False) as namespace: fbx_exporter.export( new_out_set, path.replace("\\", "/")) - # restore namespace after export - cmds.namespace(set=':') - cmds.namespace(rel=False) representations = instance.data.setdefault("representations", []) representations.append({ @@ -61,4 +58,5 @@ class ExtractFBXAnimation(publish.Extractor): "stagingDir": staging_dir }) - self.log.debug("Extracted Fbx animation successful to: {0}".format(path)) + self.log.debug( + "Extracted Fbx animation successful to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py index c7e724b569..59595d5a1c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -36,8 +36,6 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): ) return - skeleton_anim_set = instance.data["rig_sets"]["skeletonAnim_SET"] - skeleton_mesh_set = instance.data["rig_sets"]["skeletonMesh_SET"] # Ensure there are at least some transforms or dag nodes # in the rig instance set_members = instance.data['setMembers'] From 276c6a81cd93509fbebc1808955275d9729d936f Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 22:42:51 +0800 Subject: [PATCH 109/186] message tweak --- .../hosts/maya/plugins/publish/validate_skeleton_rig_content.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py index 59595d5a1c..a620c2f631 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -40,7 +40,7 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): # in the rig instance set_members = instance.data['setMembers'] if not cmds.ls(set_members, type="dagNode", long=True): - self.log.debug("Skipping empty instance...") + self.log.debug("Skipping instance without dag nodes...") return # Ensure contents in sets and retrieve long path for all objects skeleton_mesh_content = instance.data.get("skeleton_mesh", []) From 63e294147652c999b1bd0d4325c601830afb9bac Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 22:46:19 +0800 Subject: [PATCH 110/186] remove skeleton_anim_set in collector and validation check on rig content --- .../plugins/publish/collect_skeleton_mesh.py | 11 ------ .../publish/validate_skeleton_rig_content.py | 36 +------------------ 2 files changed, 1 insertion(+), 46 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py index 73b2103618..a22901357b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py @@ -12,7 +12,6 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): families = ["rig"] def process(self, instance): - skeleton_sets = instance.data.get("skeletonAnim_SET") skeleton_mesh_sets = instance.data.get("skeletonMesh_SET") if not skeleton_mesh_sets: self.log.debug( @@ -27,7 +26,6 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): instance.data["frameEnd"] = frame instance.data["skeleton_mesh"] = [] - instance.data["skeleton_rig"] = [] if skeleton_mesh_sets: instance.data["families"] += ["rig.fbx"] @@ -39,12 +37,3 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): self.log.debug( "Collected skeleton " f"mesh Set: {skeleton_mesh_content}") - - if skeleton_sets: - for skeleton_set in skeleton_sets: - skeleton_content = cmds.sets(skeleton_set, query=True) - self.log.debug( - "Collected animated " - f"skeleton data: {skeleton_content}") - if skeleton_content: - instance.data["skeleton_rig"] += skeleton_content diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py index a620c2f631..565295494a 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -26,7 +26,7 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): accepted_controllers = ["transform", "locator"] def process(self, instance): - objectsets = ["skeletonAnim_SET", "skeletonMesh_SET"] + objectsets = ["skeletonMesh_SET"] missing = [ key for key in objectsets if key not in instance.data["rig_sets"] ] @@ -46,8 +46,6 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): skeleton_mesh_content = instance.data.get("skeleton_mesh", []) skeleton_mesh_content = cmds.ls(skeleton_mesh_content, long=True) - skeleton_anim_content = instance.data.get("skeleton_rig", []) - skeleton_anim_content = cmds.ls(skeleton_anim_content, long=True) # Validate members are inside the hierarchy from root node root_node = cmds.ls(set_members, assemblies=True) @@ -61,11 +59,6 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): if node not in hierarchy: invalid_hierarchy.append(node) invalid_geometry = self.validate_geometry(skeleton_mesh_content) - if skeleton_anim_content: - for node in skeleton_anim_content: - if node not in hierarchy: - invalid_hierarchy.append(node) - invalid_controls = self.validate_controls(skeleton_anim_content) error = False if invalid_hierarchy: @@ -74,11 +67,6 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): "\n%s" % invalid_hierarchy) error = True - if invalid_controls: - self.log.error("Only transforms can be part of the " - "skeletonAnim_SET. \n%s" % invalid_controls) - error = True - if invalid_geometry: self.log.error("Only meshes can be part of the " "skeletonMesh_SET\n%s" % invalid_geometry) @@ -114,25 +102,3 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): invalid.append(shape) return invalid - - def validate_controls(self, set_members): - """Check if the controller set passes the validations - - Checks if all its set members are within the hierarchy of the root - Checks if the node types of the set members valid - - Args: - set_members: list of nodes of the skeleton_anim_set - hierarchy: list of nodes which reside under the root node - - Returns: - errors (list) - """ - - # Validate control types - invalid = [] - for node in set_members: - if cmds.nodeType(node) not in self.accepted_controllers: - invalid.append(node) - - return invalid From c5d54a522aad24886adda213c495ea657e9c1567 Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 22:51:53 +0800 Subject: [PATCH 111/186] make sure skeleton_Anim set and skeleton_Mesh set inside the loaded rig_sets --- openpype/hosts/maya/api/lib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 03a864a1db..b246a77512 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -4111,6 +4111,8 @@ def create_rig_animation_instance( anim_skeleton = next((node for node in nodes if node.endswith("skeletonAnim_SET")), None) + skeleton_mesh = next((node for node in nodes if + node.endswith("skeletonMesh_SET")), None) # Find the roots amongst the loaded nodes roots = ( @@ -4147,6 +4149,8 @@ def create_rig_animation_instance( rig_sets = [output, controls] if anim_skeleton: rig_sets.append(anim_skeleton) + if skeleton_mesh: + rig_sets.append(skeleton_mesh) with maintained_selection(): cmds.select(rig_sets + roots, noExpand=True) create_context.create( From 1e7c544e902112fa9c0621fd629f438bf3cd0a36 Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 22:57:30 +0800 Subject: [PATCH 112/186] hound --- .../hosts/maya/plugins/publish/validate_skeleton_rig_content.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py index 565295494a..9be7861309 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -46,7 +46,6 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): skeleton_mesh_content = instance.data.get("skeleton_mesh", []) skeleton_mesh_content = cmds.ls(skeleton_mesh_content, long=True) - # Validate members are inside the hierarchy from root node root_node = cmds.ls(set_members, assemblies=True) hierarchy = cmds.listRelatives(root_node, allDescendents=True, From 5a4ef31f4e06cec2ac49ec9486cf08dc95ceb7ad Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 23:03:10 +0800 Subject: [PATCH 113/186] remove animated_rig instance data in rig family --- .../maya/plugins/publish/validate_skeleton_rig_content.py | 2 -- .../publish/validate_skeleton_top_group_hierarchy.py | 7 ------- 2 files changed, 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py index 9be7861309..09c5bb5bdc 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -11,7 +11,6 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): """Ensure skeleton rigs contains pipeline-critical content The rigs optionally contain at least two object sets: - "skeletonAnim_SET" - Set of only bone hierarchies "skeletonMesh_SET" - Set of the skinned meshes with bone hierarchies @@ -23,7 +22,6 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): families = ["rig.fbx"] accepted_output = ["mesh", "transform", "locator"] - accepted_controllers = ["transform", "locator"] def process(self, instance): objectsets = ["skeletonMesh_SET"] diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py index 1e0d856b4e..541efee9a9 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py @@ -24,14 +24,7 @@ class ValidateSkeletonTopGroupHierarchy(pyblish.api.InstancePlugin, def process(self, instance): invalid = [] - skeleton_data = instance.data.get(("animated_rigs"), []) skeleton_mesh_data = instance.data(("skeleton_mesh"), []) - if skeleton_data: - invalid = self.get_top_hierarchy(skeleton_data) - if invalid: - raise PublishValidationError( - "The skeletonAnim_SET includes the object which " - f"is not at the top hierarchy: {invalid}") if skeleton_mesh_data: invalid = self.get_top_hierarchy(skeleton_mesh_data) if invalid: From a7b99ac0b0e347c74be0d41a15837bc7b0f2b17d Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 23:19:06 +0800 Subject: [PATCH 114/186] make sure the namespace has been restored --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index e99e7d40bd..1647bbdcda 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -50,6 +50,8 @@ class ExtractFBXAnimation(publish.Extractor): fbx_exporter.export( new_out_set, path.replace("\\", "/")) + cmds.namespace(relativeNames=False) + representations = instance.data.setdefault("representations", []) representations.append({ 'name': 'fbx', From ea4ce1b8be7a721124445770a6169ab960145b3a Mon Sep 17 00:00:00 2001 From: Kayla Date: Fri, 29 Sep 2023 15:17:41 +0800 Subject: [PATCH 115/186] make sure the namespace has not been forcily restored --- .../maya/plugins/publish/extract_fbx_animation.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 1647bbdcda..d281e01779 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -44,13 +44,14 @@ class ExtractFBXAnimation(publish.Extractor): namespace = out_set_name.split(":")[0] new_out_set = out_set_name.replace( f"{namespace}:", "") - cmds.namespace(set=':' + namespace) cmds.namespace(relativeNames=True) with namespaced(":" + namespace, new=False) as namespace: - fbx_exporter.export( - new_out_set, path.replace("\\", "/")) - - cmds.namespace(relativeNames=False) + path = path.replace("\\", "/") + fbx_exporter.export(new_out_set, path) + original_relative_names = cmds.namespace( + query=True, relativeNames=True) + if original_relative_names: + cmds.namespace(relativeNames=original_relative_names) representations = instance.data.setdefault("representations", []) representations.append({ From 37cefd892c85218dc3614341866b5332c7384e10 Mon Sep 17 00:00:00 2001 From: Kayla Date: Fri, 29 Sep 2023 18:26:58 +0800 Subject: [PATCH 116/186] abstract namespaced functions for extract fbx animation and add fbx loaders in animatin family --- openpype/hosts/maya/api/fbx.py | 3 + openpype/hosts/maya/api/lib.py | 5 +- .../hosts/maya/plugins/create/create_rig.py | 2 +- .../maya/plugins/load/_load_animation.py | 102 ++++++++++++------ .../plugins/publish/collect_fbx_animation.py | 5 +- .../plugins/publish/collect_skeleton_mesh.py | 5 +- .../plugins/publish/extract_fbx_animation.py | 21 ++-- .../plugins/publish/extract_skeleton_mesh.py | 3 - .../validate_skeleton_rig_output_ids.py | 6 +- .../validate_skeleton_top_group_hierarchy.py | 14 ++- 10 files changed, 102 insertions(+), 64 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 5bd375362b..2dd4f5a73d 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -203,6 +203,9 @@ class FBXExtractor: path (str): Path to use for export. """ + # The export requires forward slashes because we need + # to format it into a string in a mel expression + path = path.replace("\\", "/") with maintained_selection(): cmds.select(members, r=True, noExpand=True) mel.eval('FBXExport -f "{}" -s'.format(path)) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index b246a77512..6019aec37c 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -922,7 +922,7 @@ def no_display_layers(nodes): @contextlib.contextmanager -def namespaced(namespace, new=True): +def namespaced(namespace, new=True, relative_names=None): """Work inside namespace during context Args: @@ -934,6 +934,7 @@ def namespaced(namespace, new=True): """ original = cmds.namespaceInfo(cur=True, absoluteName=True) + original_relative_names = cmds.namespace(query=True, relativeNames=True) if new: namespace = unique_namespace(namespace) cmds.namespace(add=namespace) @@ -943,6 +944,8 @@ def namespaced(namespace, new=True): yield namespace finally: cmds.namespace(set=original) + if relative_names is not None: + cmds.namespace(relativeNames=original_relative_names) @contextlib.contextmanager diff --git a/openpype/hosts/maya/plugins/create/create_rig.py b/openpype/hosts/maya/plugins/create/create_rig.py index 22a94ed4fd..acd5c98f89 100644 --- a/openpype/hosts/maya/plugins/create/create_rig.py +++ b/openpype/hosts/maya/plugins/create/create_rig.py @@ -20,7 +20,7 @@ class CreateRig(plugin.MayaCreator): instance_node = instance.get("instance_node") self.log.info("Creating Rig instance set up ...") - # TODO:change name (_controls_set -> _rigs_SET) + # TODO:change name (_controls_SET -> _rigs_SET) controls = cmds.sets(name=subset_name + "_controls_SET", empty=True) # TODO:change name (_out_SET -> _geo_SET) pointcache = cmds.sets(name=subset_name + "_out_SET", empty=True) diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py index 6d67383909..2432184151 100644 --- a/openpype/hosts/maya/plugins/load/_load_animation.py +++ b/openpype/hosts/maya/plugins/load/_load_animation.py @@ -1,4 +1,46 @@ import openpype.hosts.maya.api.plugin +import maya.cmds as cmds + + +def _process_reference(file_url, name, namespace, options): + """_summary_ + + Args: + file_url (str): fileapth of the objects to be loaded + name (str): subset name + namespace (str): namespace + options (dict): dict of storing the param + + Returns: + list: list of object nodes + """ + from openpype.hosts.maya.api.lib import unique_namespace + # Get name from asset being loaded + # Assuming name is subset name from the animation, we split the number + # suffix from the name to ensure the namespace is unique + name = name.split("_")[0] + ext = file_url.split(".")[-1] + namespace = unique_namespace( + "{}_".format(name), + format="%03d", + suffix="_{}".format(ext) + ) + + attach_to_root = options.get("attach_to_root", True) + group_name = options["group_name"] + + # no group shall be created + if not attach_to_root: + group_name = namespace + + nodes = cmds.file(file_url, + namespace=namespace, + sharedReferenceFile=False, + groupReference=attach_to_root, + groupName=group_name, + reference=True, + returnNewNodes=True) + return nodes class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): @@ -7,7 +49,7 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): families = ["animation", "camera", "pointcache"] - representations = ["abc", "fbx"] + representations = ["abc"] label = "Reference animation" order = -10 @@ -16,44 +58,42 @@ class AbcLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): def process_reference(self, context, name, namespace, options): - import maya.cmds as cmds - from openpype.hosts.maya.api.lib import unique_namespace - cmds.loadPlugin("AbcImport.mll", quiet=True) - # Prevent identical alembic nodes from being shared - # Create unique namespace for the cameras - - # Get name from asset being loaded - # Assuming name is subset name from the animation, we split the number - # suffix from the name to ensure the namespace is unique - name = name.split("_")[0] - namespace = unique_namespace( - "{}_".format(name), - format="%03d", - suffix="_abc" - ) - - attach_to_root = options.get("attach_to_root", True) - group_name = options["group_name"] - - # no group shall be created - if not attach_to_root: - group_name = namespace - # hero_001 (abc) # asset_counter{optional} path = self.filepath_from_context(context) file_url = self.prepare_root_value(path, context["project"]["name"]) - nodes = cmds.file(file_url, - namespace=namespace, - sharedReferenceFile=False, - groupReference=attach_to_root, - groupName=group_name, - reference=True, - returnNewNodes=True) + nodes = _process_reference(file_url, name, namespace, options) # load colorbleed ID attribute self[:] = nodes return nodes + + +class FbxLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): + """Loader to reference an Fbx files""" + + families = ["animation", + "camera"] + representations = ["fbx"] + + label = "Reference animation" + order = -10 + icon = "code-fork" + color = "orange" + + def process_reference(self, context, name, namespace, options): + + cmds.loadPlugin("fbx4maya.mll", quiet=True) + + path = self.filepath_from_context(context) + file_url = self.prepare_root_value(path, + context["project"]["name"]) + + nodes = _process_reference(file_url, name, namespace, options) + + self[:] = nodes + + return nodes diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index 9347936e63..ee5ac741c8 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -29,7 +29,8 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin, for skeleton_set in skeleton_sets: skeleton_content = cmds.sets(skeleton_set, query=True) self.log.debug( - "Collected animated " - f"skeleton data: {skeleton_content}") + "Collected animated skeleton data: {}".format( + skeleton_content + )) if skeleton_content: instance.data["animated_skeleton"] += skeleton_content diff --git a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py index a22901357b..9169e3dc28 100644 --- a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py @@ -35,5 +35,6 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): if skeleton_mesh_content: instance.data["skeleton_mesh"] += skeleton_mesh_content self.log.debug( - "Collected skeleton " - f"mesh Set: {skeleton_mesh_content}") + "Collected skeletonmesh Set: {}".format( + skeleton_mesh_content + )) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index d281e01779..fbc1d5176c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -40,18 +40,15 @@ class ExtractFBXAnimation(publish.Extractor): fbx_exporter.set_options_from_instance(instance) out_set_name = next(out for out in out_set) - # temporarily disable namespace - namespace = out_set_name.split(":")[0] - new_out_set = out_set_name.replace( - f"{namespace}:", "") + # Export from the rig's namespace so that the exported + # FBX does not include the namespace but preserves the node + # names as existing in the rig workfile + namespace, relative_out_set = out_set_name.split(":", 1) cmds.namespace(relativeNames=True) - with namespaced(":" + namespace, new=False) as namespace: - path = path.replace("\\", "/") - fbx_exporter.export(new_out_set, path) - original_relative_names = cmds.namespace( - query=True, relativeNames=True) - if original_relative_names: - cmds.namespace(relativeNames=original_relative_names) + with namespaced( + ":" + namespace, + new=False, relative_names=True) as namespace: + fbx_exporter.export(relative_out_set, path) representations = instance.data.setdefault("representations", []) representations.append({ @@ -62,4 +59,4 @@ class ExtractFBXAnimation(publish.Extractor): }) self.log.debug( - "Extracted Fbx animation successful to: {0}".format(path)) + "Extracted Fbx animation to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py b/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py index 42cbb33013..cecdf282e2 100644 --- a/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py @@ -32,8 +32,6 @@ class ExtractSkeletonMesh(publish.Extractor, filename = "{0}.fbx".format(instance.name) path = os.path.join(staging_dir, filename) - # The export requires forward slashes because we need - # to format it into a string in a mel expression fbx_exporter = fbx.FBXExtractor(log=self.log) out_set = instance.data.get("skeleton_mesh", []) @@ -43,7 +41,6 @@ class ExtractSkeletonMesh(publish.Extractor, fbx_exporter.set_options_from_instance(instance) # Export - path = path.replace("\\", "/") fbx_exporter.export(out_set, path) representations = instance.data.setdefault("representations", []) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py index 0d1e702749..735ca27b39 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py @@ -68,9 +68,7 @@ class ValidateSkeletonRigOutputIds(pyblish.api.InstancePlugin): if shapes: instance_nodes.extend(shapes) - scene_nodes = cmds.ls(type="transform", long=True) - scene_nodes += cmds.ls(type="mesh", long=True) - scene_nodes = set(scene_nodes) - set(instance_nodes) + scene_nodes = cmds.ls(type=("transform", "mesh"), long=True) scene_nodes_by_basename = defaultdict(list) for node in scene_nodes: @@ -109,7 +107,7 @@ class ValidateSkeletonRigOutputIds(pyblish.api.InstancePlugin): for instance_node, matches in invalid_matches.items(): ids = set(get_id(node) for node in matches) - # If there are multiple scene ids matched, and error needs to be + # If there are multiple scene ids matched, an error needs to be # raised for manual correction. if len(ids) > 1: multiple_ids_match.append({"node": instance_node, diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py index 541efee9a9..553618aa50 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py @@ -24,19 +24,17 @@ class ValidateSkeletonTopGroupHierarchy(pyblish.api.InstancePlugin, def process(self, instance): invalid = [] - skeleton_mesh_data = instance.data(("skeleton_mesh"), []) + skeleton_mesh_data = instance.data("skeleton_mesh", []) if skeleton_mesh_data: invalid = self.get_top_hierarchy(skeleton_mesh_data) if invalid: raise PublishValidationError( "The skeletonMesh_SET includes the object which " - f"is not at the top hierarchy: {invalid}") + "is not at the top hierarchy: {}".format(invalid)) def get_top_hierarchy(self, targets): - non_top_hierarchy_list = [] - for target in targets: - long_names = cmds.ls(target, long=True) - for name in long_names: - if len(name.split["|"]) > 2: - non_top_hierarchy_list.append(name) + targets = cmds.ls(targets, long=True) # ensure long names + non_top_hierarchy_list = [ + target for target in targets if target.count("|") > 2 + ] return non_top_hierarchy_list From 08f47c77fd25cee3000bf4e86e21318586f87c43 Mon Sep 17 00:00:00 2001 From: Kayla Date: Fri, 29 Sep 2023 18:29:58 +0800 Subject: [PATCH 117/186] hound --- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index fbc1d5176c..115ba39986 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -45,9 +45,7 @@ class ExtractFBXAnimation(publish.Extractor): # names as existing in the rig workfile namespace, relative_out_set = out_set_name.split(":", 1) cmds.namespace(relativeNames=True) - with namespaced( - ":" + namespace, - new=False, relative_names=True) as namespace: + with namespaced(":" + namespace,new=False, relative_names=True) as namespace: # noqa fbx_exporter.export(relative_out_set, path) representations = instance.data.setdefault("representations", []) From bafd908483136c0e6829e22d2048484bbc161908 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 29 Sep 2023 15:13:17 +0200 Subject: [PATCH 118/186] broken settings fixed by reverting changes form previous PR https://github.com/ynput/OpenPype/pull/5409 --- .../hosts/nuke/plugins/publish/collect_nuke_instance_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py b/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py index b0f69e8ab8..449a1cc935 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py +++ b/openpype/hosts/nuke/plugins/publish/collect_nuke_instance_data.py @@ -2,7 +2,7 @@ import nuke import pyblish.api -class CollectNukeInstanceData(pyblish.api.InstancePlugin): +class CollectInstanceData(pyblish.api.InstancePlugin): """Collect Nuke instance data """ From d8715d59d0d9b095cbd7ba84d72e079ba2e12a4d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 16:57:05 +0300 Subject: [PATCH 119/186] allow values other than paths --- openpype/hosts/houdini/api/lib.py | 62 ++++++++++--------- openpype/pipeline/context_tools.py | 55 ++++++++++++---- .../defaults/project_settings/houdini.json | 10 ++- .../schemas/schema_houdini_general.json | 22 ++++++- .../houdini/server/settings/general.py | 6 +- 5 files changed, 106 insertions(+), 49 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index f8d17eef07..637339f822 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -768,45 +768,49 @@ def update_houdini_vars_context(): if houdini_vars_settings["enabled"]: houdini_vars = houdini_vars_settings["houdini_vars"] - # Remap AYON settings structure to OpenPype settings structure - # It allows me to use the same logic for both AYON and OpenPype - if isinstance(houdini_vars, list): - items = {} - for item in houdini_vars: - items.update({item["var"]: item["path"]}) + # No vars specified - nothing to do + if not houdini_vars: + return - houdini_vars = items + # Get Template data + template_data = get_current_context_template_data() + + # Set Houdini Vars + for item in houdini_vars: + + # For consistency reasons we always force all vars to be uppercase + item["var"] = item["var"].upper() - for var, path in houdini_vars.items(): # get and resolve job path template - path = StringTemplate.format_template( - path, - get_current_context_template_data() + item_value = StringTemplate.format_template( + item["value"], + template_data ) - path = path.replace("\\", "/") - if var == "JOB" and path == "": + if item["is_path"]: + item_value = item_value.replace("\\", "/") + try: + os.makedirs(item_value) + except OSError as e: + if e.errno != errno.EEXIST: + print( + " - Failed to create ${} dir. Maybe due to " + "insufficient permissions.".format(item["var"]) + ) + + if item["var"] == "JOB" and item_value == "": # sync $JOB to $HIP if $JOB is empty - path = os.environ["HIP"] + item_value = os.environ["HIP"] - current_path = hou.hscript("echo -n `${}`".format(var))[0] + current_value = hou.hscript("echo -n `${}`".format(item["var"]))[0] # sync both environment variables. # because houdini doesn't do that by default # on opening new files - os.environ[var] = current_path + os.environ[item["var"]] = current_value - if current_path != path: - hou.hscript("set {}={}".format(var, path)) - os.environ[var] = path + if current_value != item_value: + hou.hscript("set {}={}".format(item["var"], item_value)) + os.environ[item["var"]] = item_value - try: - os.makedirs(path) - except OSError as e: - if e.errno != errno.EEXIST: - print( - " - Failed to create {} dir. Maybe due to " - "insufficient permissions.".format(var) - ) - - print(" - Updated ${} to {}".format(var, path)) + print(" - Updated ${} to {}".format(item["var"], item_value)) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index 13b14f1296..f98132e270 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -667,17 +667,30 @@ def get_current_context_template_data(): """Template data for template fill from current context Returns: - Dict[str, str] of the following tokens and their values - - app - - user - - asset - - parent - - hierarchy - - folder[name] - - root[work, ...] - - studio[code, name] - - project[code, name] - - task[type, name, short] + Dict[str, Any] of the following tokens and their values + Supported Tokens: + - Regular Tokens + - app + - user + - asset + - parent + - hierarchy + - folder[name] + - root[work, ...] + - studio[code, name] + - project[code, name] + - task[type, name, short] + + - Context Specific Tokens + - assetData[frameStart] + - assetData[frameEnd] + - assetData[handleStart] + - assetData[handleEnd] + - assetData[frameStartHandle] + - assetData[frameEndHandle] + - assetData[resolutionHeight] + - assetData[resolutionWidth] + """ # pre-prepare get_template_data args @@ -692,10 +705,28 @@ def get_current_context_template_data(): task_name = current_context["task_name"] host_name = get_current_host_name() - # get template data + # get regular template data template_data = get_template_data( project_doc, asset_doc, task_name, host_name ) template_data["root"] = anatomy.roots + + # get context specific vars + asset_data = asset_doc["data"].copy() + + # compute `frameStartHandle` and `frameEndHandle` + if "frameStart" in asset_data and "handleStart" in asset_data: + asset_data["frameStartHandle"] = ( + asset_data["frameStart"] - asset_data["handleStart"] + ) + + if "frameEnd" in asset_data and "handleEnd" in asset_data: + asset_data["frameEndHandle"] = ( + asset_data["frameEnd"] + asset_data["handleEnd"] + ) + + # add assetData + template_data["assetData"] = asset_data + return template_data diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index b2fcb708cf..3c43e7ae29 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -2,9 +2,13 @@ "general": { "update_houdini_var_context": { "enabled": true, - "houdini_vars":{ - "JOB": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" - } + "houdini_vars":[ + { + "var": "JOB", + "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", + "is_path": true + } + ] } }, "imageio": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json index 127382f4bc..2989d5c5b9 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -18,13 +18,29 @@ "label": "Enabled" }, { - "type": "dict-modifiable", + "type": "list", "key": "houdini_vars", "label": "Houdini Vars", "collapsible": false, "object_type": { - "type": "path", - "multiplatform": false + "type": "dict", + "children": [ + { + "type": "text", + "key": "var", + "label": "Var" + }, + { + "type": "text", + "key": "value", + "label": "Value" + }, + { + "type": "boolean", + "key": "is_path", + "label": "isPath" + } + ] } } ] diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 42a071a688..468c571993 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -5,7 +5,8 @@ from ayon_server.settings import BaseSettingsModel class HoudiniVarModel(BaseSettingsModel): _layout = "expanded" var: str = Field("", title="Var") - path: str = Field(default_factory="", title="Path") + value: str = Field("", title="Value") + is_path: bool = Field(False, title="isPath") class UpdateHoudiniVarcontextModel(BaseSettingsModel): @@ -30,7 +31,8 @@ DEFAULT_GENERAL_SETTINGS = { "houdini_vars": [ { "var": "JOB", - "path": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}" # noqa + "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", # noqa + "is_path": True } ] } From b93da3bd3ddacfcc3770ee44032f51ec9184b6a4 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 16:59:07 +0300 Subject: [PATCH 120/186] resolve hound --- openpype/pipeline/context_tools.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/context_tools.py b/openpype/pipeline/context_tools.py index f98132e270..13630ae7ca 100644 --- a/openpype/pipeline/context_tools.py +++ b/openpype/pipeline/context_tools.py @@ -717,14 +717,12 @@ def get_current_context_template_data(): # compute `frameStartHandle` and `frameEndHandle` if "frameStart" in asset_data and "handleStart" in asset_data: - asset_data["frameStartHandle"] = ( - asset_data["frameStart"] - asset_data["handleStart"] - ) + asset_data["frameStartHandle"] = \ + asset_data["frameStart"] - asset_data["handleStart"] if "frameEnd" in asset_data and "handleEnd" in asset_data: - asset_data["frameEndHandle"] = ( - asset_data["frameEnd"] + asset_data["handleEnd"] - ) + asset_data["frameEndHandle"] = \ + asset_data["frameEnd"] + asset_data["handleEnd"] # add assetData template_data["assetData"] = asset_data From 846bd0fd590688ceb4640b00f8f0f3af2ce6fa65 Mon Sep 17 00:00:00 2001 From: Kayla Date: Fri, 29 Sep 2023 22:12:02 +0800 Subject: [PATCH 121/186] make sure there is a check in relative names in namespace before yield function & add docstring --- openpype/hosts/maya/api/lib.py | 3 ++- openpype/hosts/maya/plugins/load/_load_animation.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 6019aec37c..dc881879ac 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -938,7 +938,8 @@ def namespaced(namespace, new=True, relative_names=None): if new: namespace = unique_namespace(namespace) cmds.namespace(add=namespace) - + if relative_names is not None: + cmds.namespace(relativeNames=relative_names) try: cmds.namespace(set=namespace) yield namespace diff --git a/openpype/hosts/maya/plugins/load/_load_animation.py b/openpype/hosts/maya/plugins/load/_load_animation.py index 2432184151..0781735bc4 100644 --- a/openpype/hosts/maya/plugins/load/_load_animation.py +++ b/openpype/hosts/maya/plugins/load/_load_animation.py @@ -3,7 +3,7 @@ import maya.cmds as cmds def _process_reference(file_url, name, namespace, options): - """_summary_ + """Load files by referencing scene in Maya. Args: file_url (str): fileapth of the objects to be loaded From a4d55b420b53e475e1ba05c45504b87c65bdbe46 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 17:25:04 +0300 Subject: [PATCH 122/186] update docs and rename `isPath` to `is Dir Path` --- openpype/hosts/houdini/api/lib.py | 2 +- .../defaults/project_settings/houdini.json | 2 +- .../schemas/schema_houdini_general.json | 4 +-- .../houdini/server/settings/general.py | 4 +-- website/docs/admin_hosts_houdini.md | 24 ++++++++---------- .../update-houdini-vars-context-change.png | Bin 0 -> 18456 bytes .../houdini/update-job-context-change.png | Bin 8068 -> 0 bytes 7 files changed, 17 insertions(+), 19 deletions(-) create mode 100644 website/docs/assets/houdini/update-houdini-vars-context-change.png delete mode 100644 website/docs/assets/houdini/update-job-context-change.png diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 637339f822..291817bbe9 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -787,7 +787,7 @@ def update_houdini_vars_context(): template_data ) - if item["is_path"]: + if item["is_dir_path"]: item_value = item_value.replace("\\", "/") try: os.makedirs(item_value) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 3c43e7ae29..111ed2b24d 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -6,7 +6,7 @@ { "var": "JOB", "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", - "is_path": true + "is_dir_path": true } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json index 2989d5c5b9..3160e657bf 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -37,8 +37,8 @@ }, { "type": "boolean", - "key": "is_path", - "label": "isPath" + "key": "is_dir_path", + "label": "is Dir Path" } ] } diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 468c571993..7b3b95f978 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -6,7 +6,7 @@ class HoudiniVarModel(BaseSettingsModel): _layout = "expanded" var: str = Field("", title="Var") value: str = Field("", title="Value") - is_path: bool = Field(False, title="isPath") + is_dir_path: bool = Field(False, title="is Dir Path") class UpdateHoudiniVarcontextModel(BaseSettingsModel): @@ -32,7 +32,7 @@ DEFAULT_GENERAL_SETTINGS = { { "var": "JOB", "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", # noqa - "is_path": True + "is_dir_path": True } ] } diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index ea7991530b..749ca43fe2 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -4,26 +4,24 @@ title: Houdini sidebar_label: Houdini --- ## General Settings -### JOB Path +### Houdini Vars + +Allows admins to have a list of vars (e.g. JOB) with (dynamic) values that will be updated on context changes, e.g. when switching to another asset or task. + +Using template keys is supported but formatting keys capitalization variants is not, e.g. `{Asset}` and `{ASSET}` won't work -The Houdini `$JOB` path can be customized through project settings with a (dynamic) path that will be updated on context changes, e.g. when switching to another asset or task. :::note -If the folder does not exist on the context change it will be created by this feature so that `$JOB` will always try to point to an existing folder. +If `is Dir Path` toggle is activated, Openpype will consider the given value is a path of a folder. + +If the folder does not exist on the context change it will be created by this feature so that the path will always try to point to an existing folder. ::: -Disabling this feature will leave `$JOB` var unmanaged and thus no context update changes will occur. +Disabling `Update Houdini vars on context change` feature will leave all Houdini vars unmanaged and thus no context update changes will occur. -JOB Path can be: -- Arbitrary path -- Openpype template path - > This allows dynamic values for assets or shots.
- > Using template keys is supported but formatting keys capitalization variants is not, - > e.g. `{Asset}` and `{ASSET}` won't work -- Empty - > In this case, JOB will be synced to HIP +> If `$JOB` is present in the Houdini var list and has an empty value, OpenPype will set its value to `$HIP` -![update job on context change](assets/houdini/update-job-context-change.png) +![update-houdini-vars-context-change](assets/houdini/update-houdini-vars-context-change.png) diff --git a/website/docs/assets/houdini/update-houdini-vars-context-change.png b/website/docs/assets/houdini/update-houdini-vars-context-change.png new file mode 100644 index 0000000000000000000000000000000000000000..77c67a620dd50923bafbba61f4f5e0f9236a3f27 GIT binary patch literal 18456 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 From b45e8c3131f6c136970afccf7c6ef4b3db251e93 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 17:37:29 +0300 Subject: [PATCH 123/186] update photo --- .../update-houdini-vars-context-change.png | Bin 18456 -> 18904 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/website/docs/assets/houdini/update-houdini-vars-context-change.png b/website/docs/assets/houdini/update-houdini-vars-context-change.png index 77c67a620dd50923bafbba61f4f5e0f9236a3f27..76fb7233214c685cfe632d05e937535280c723f0 100644 GIT binary patch literal 18904 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 literal 18456 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 Date: Fri, 29 Sep 2023 17:44:36 +0300 Subject: [PATCH 124/186] avoid using nested if --- openpype/hosts/houdini/api/lib.py | 78 ++++++++++++++++--------------- 1 file changed, 40 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 291817bbe9..67755c1a72 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -765,52 +765,54 @@ def update_houdini_vars_context(): houdini_vars_settings = \ project_settings["houdini"]["general"]["update_houdini_var_context"] - if houdini_vars_settings["enabled"]: - houdini_vars = houdini_vars_settings["houdini_vars"] + if not houdini_vars_settings["enabled"]: + return - # No vars specified - nothing to do - if not houdini_vars: - return + houdini_vars = houdini_vars_settings["houdini_vars"] - # Get Template data - template_data = get_current_context_template_data() + # No vars specified - nothing to do + if not houdini_vars: + return - # Set Houdini Vars - for item in houdini_vars: + # Get Template data + template_data = get_current_context_template_data() - # For consistency reasons we always force all vars to be uppercase - item["var"] = item["var"].upper() + # Set Houdini Vars + for item in houdini_vars: - # get and resolve job path template - item_value = StringTemplate.format_template( - item["value"], - template_data - ) + # For consistency reasons we always force all vars to be uppercase + item["var"] = item["var"].upper() - if item["is_dir_path"]: - item_value = item_value.replace("\\", "/") - try: - os.makedirs(item_value) - except OSError as e: - if e.errno != errno.EEXIST: - print( - " - Failed to create ${} dir. Maybe due to " - "insufficient permissions.".format(item["var"]) - ) + # get and resolve template in value + item_value = StringTemplate.format_template( + item["value"], + template_data + ) - if item["var"] == "JOB" and item_value == "": - # sync $JOB to $HIP if $JOB is empty - item_value = os.environ["HIP"] + if item["is_dir_path"]: + item_value = item_value.replace("\\", "/") + try: + os.makedirs(item_value) + except OSError as e: + if e.errno != errno.EEXIST: + print( + " - Failed to create ${} dir. Maybe due to " + "insufficient permissions.".format(item["var"]) + ) - current_value = hou.hscript("echo -n `${}`".format(item["var"]))[0] + if item["var"] == "JOB" and item_value == "": + # sync $JOB to $HIP if $JOB is empty + item_value = os.environ["HIP"] - # sync both environment variables. - # because houdini doesn't do that by default - # on opening new files - os.environ[item["var"]] = current_value + current_value = hou.hscript("echo -n `${}`".format(item["var"]))[0] - if current_value != item_value: - hou.hscript("set {}={}".format(item["var"], item_value)) - os.environ[item["var"]] = item_value + # sync both environment variables. + # because houdini doesn't do that by default + # on opening new files + os.environ[item["var"]] = current_value - print(" - Updated ${} to {}".format(item["var"], item_value)) + if current_value != item_value: + hou.hscript("set {}={}".format(item["var"], item_value)) + os.environ[item["var"]] = item_value + + print(" - Updated ${} to {}".format(item["var"], item_value)) From f287144616a5341c6651cb5af5b49416af3b4e30 Mon Sep 17 00:00:00 2001 From: Kayla Date: Fri, 29 Sep 2023 23:28:39 +0800 Subject: [PATCH 125/186] make sure validators for skeleton mesh are in rig.fbx family --- .../maya/plugins/publish/validate_skeleton_rig_content.py | 2 +- .../plugins/publish/validate_skeleton_top_group_hierarchy.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py index 09c5bb5bdc..8b6cc74332 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py @@ -21,7 +21,7 @@ class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): hosts = ["maya"] families = ["rig.fbx"] - accepted_output = ["mesh", "transform", "locator"] + accepted_output = {"mesh", "transform", "locator"} def process(self, instance): objectsets = ["skeletonMesh_SET"] diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py index 553618aa50..1dbe1c454c 100644 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py +++ b/openpype/hosts/maya/plugins/publish/validate_skeleton_top_group_hierarchy.py @@ -19,8 +19,8 @@ class ValidateSkeletonTopGroupHierarchy(pyblish.api.InstancePlugin, """ order = ValidateContentsOrder + 0.05 - label = "Top Group Hierarchy" - families = ["rig"] + label = "Skeleton Rig Top Group Hierarchy" + families = ["rig.fbx"] def process(self, instance): invalid = [] From 82b2bd4b4540c435a76e1aa3bcc911296c887c74 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 19:32:08 +0300 Subject: [PATCH 126/186] update labels and add settings tips --- openpype/hosts/houdini/api/lib.py | 2 +- .../defaults/project_settings/houdini.json | 2 +- .../schemas/schema_houdini_general.json | 8 ++++++-- .../houdini/server/settings/general.py | 10 ++++++++-- .../update-houdini-vars-context-change.png | Bin 18904 -> 23727 bytes 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 67755c1a72..ce89ffe606 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -789,7 +789,7 @@ def update_houdini_vars_context(): template_data ) - if item["is_dir_path"]: + if item["is_directory"]: item_value = item_value.replace("\\", "/") try: os.makedirs(item_value) diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 111ed2b24d..4f57ee52c6 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -6,7 +6,7 @@ { "var": "JOB", "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", - "is_dir_path": true + "is_directory": true } ] } diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json index 3160e657bf..c1e2cae8f0 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -17,6 +17,10 @@ "key": "enabled", "label": "Enabled" }, + { + "type": "label", + "label": "Houdini Vars.
If a value is treated as a directory on update it will be ensured the folder exists" + }, { "type": "list", "key": "houdini_vars", @@ -37,8 +41,8 @@ }, { "type": "boolean", - "key": "is_dir_path", - "label": "is Dir Path" + "key": "is_directory", + "label": "Treat as directory" } ] } diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 7b3b95f978..0109eec63d 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -6,10 +6,16 @@ class HoudiniVarModel(BaseSettingsModel): _layout = "expanded" var: str = Field("", title="Var") value: str = Field("", title="Value") - is_dir_path: bool = Field(False, title="is Dir Path") + is_directory: bool = Field(False, title="Treat as directory") class UpdateHoudiniVarcontextModel(BaseSettingsModel): + """Houdini Vars Note. + + If a value is treated as a directory on update + it will be ensured the folder exists. + """ + enabled: bool = Field(title="Enabled") # TODO this was dynamic dictionary '{var: path}' houdini_vars: list[HoudiniVarModel] = Field( @@ -32,7 +38,7 @@ DEFAULT_GENERAL_SETTINGS = { { "var": "JOB", "value": "{root[work]}/{project[name]}/{hierarchy}/{asset}/work/{task[name]}", # noqa - "is_dir_path": True + "is_directory": True } ] } diff --git a/website/docs/assets/houdini/update-houdini-vars-context-change.png b/website/docs/assets/houdini/update-houdini-vars-context-change.png index 76fb7233214c685cfe632d05e937535280c723f0..74ac8d86c9fb9d7e0520e9922517039a36df20b6 100644 GIT binary patch literal 23727 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 From 7816c0370f705b796dc4f3cafa379b1d3117f681 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 19:34:02 +0300 Subject: [PATCH 127/186] Big Roy's comment --- .../schemas/projects_schema/schemas/schema_houdini_general.json | 2 +- server_addon/houdini/server/settings/general.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json index c1e2cae8f0..de1a0396ec 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_houdini_general.json @@ -19,7 +19,7 @@ }, { "type": "label", - "label": "Houdini Vars.
If a value is treated as a directory on update it will be ensured the folder exists" + "label": "Sync vars with context changes.
If a value is treated as a directory on update it will be ensured the folder exists" }, { "type": "list", diff --git a/server_addon/houdini/server/settings/general.py b/server_addon/houdini/server/settings/general.py index 0109eec63d..21cc4c452c 100644 --- a/server_addon/houdini/server/settings/general.py +++ b/server_addon/houdini/server/settings/general.py @@ -10,7 +10,7 @@ class HoudiniVarModel(BaseSettingsModel): class UpdateHoudiniVarcontextModel(BaseSettingsModel): - """Houdini Vars Note. + """Sync vars with context changes. If a value is treated as a directory on update it will be ensured the folder exists. From e6585e8d9dec22461bcd71e974324b8463558c2d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 19:37:53 +0300 Subject: [PATCH 128/186] update docs --- website/docs/admin_hosts_houdini.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index 749ca43fe2..dd0e92f480 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -12,7 +12,7 @@ Using template keys is supported but formatting keys capitalization variants is :::note -If `is Dir Path` toggle is activated, Openpype will consider the given value is a path of a folder. +If `Treat as directory` toggle is activated, Openpype will consider the given value is a path of a folder. If the folder does not exist on the context change it will be created by this feature so that the path will always try to point to an existing folder. ::: From e75dc71ff7d97410756bc0343774621cd65f6d57 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 29 Sep 2023 19:41:10 +0300 Subject: [PATCH 129/186] update docs --- website/docs/admin_hosts_houdini.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/docs/admin_hosts_houdini.md b/website/docs/admin_hosts_houdini.md index dd0e92f480..18c390e07f 100644 --- a/website/docs/admin_hosts_houdini.md +++ b/website/docs/admin_hosts_houdini.md @@ -21,6 +21,12 @@ Disabling `Update Houdini vars on context change` feature will leave all Houdini > If `$JOB` is present in the Houdini var list and has an empty value, OpenPype will set its value to `$HIP` + +:::note +For consistency reasons we always force all vars to be uppercase. +e.g. `myvar` will be `MYVAR` +::: + ![update-houdini-vars-context-change](assets/houdini/update-houdini-vars-context-change.png) From 6d451ccd09fab8bad13a42c21850605133660d03 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:15:12 +0200 Subject: [PATCH 130/186] Use settings from `apply_settings` --- openpype/hosts/maya/api/plugin.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/plugin.py b/openpype/hosts/maya/api/plugin.py index 79fcf9bc8b..157ce8368f 100644 --- a/openpype/hosts/maya/api/plugin.py +++ b/openpype/hosts/maya/api/plugin.py @@ -601,6 +601,13 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase): class Loader(LoaderPlugin): hosts = ["maya"] + load_settings = {} # defined in settings + + @classmethod + def apply_settings(cls, project_settings, system_settings): + super(Loader, cls).apply_settings(project_settings, system_settings) + cls.load_settings = project_settings['maya']['load'] + def get_custom_namespace_and_group(self, context, options, loader_key): """Queries Settings to get custom template for namespace and group. @@ -613,12 +620,9 @@ class Loader(LoaderPlugin): loader_key (str): key to get separate configuration from Settings ('reference_loader'|'import_loader') """ - options["attach_to_root"] = True - asset = context['asset'] - subset = context['subset'] - settings = get_project_settings(context['project']['name']) - custom_naming = settings['maya']['load'][loader_key] + options["attach_to_root"] = True + custom_naming = self.load_settings[loader_key] if not custom_naming['namespace']: raise LoadError("No namespace specified in " @@ -627,6 +631,8 @@ class Loader(LoaderPlugin): self.log.debug("No custom group_name, no group will be created.") options["attach_to_root"] = False + asset = context['asset'] + subset = context['subset'] formatting_data = { "asset_name": asset['name'], "asset_type": asset['type'], From 28dff4ed3880a008f5a5d3a2cccecb46b16ec4c2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:16:02 +0200 Subject: [PATCH 131/186] Use project settings from context data --- .../plugins/publish/validate_unreal_staticmesh_naming.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index 5ba256f9f5..58fa9d02bd 100644 --- a/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/openpype/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -69,11 +69,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, invalid = [] - project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) collision_prefixes = ( - project_settings + instance.context.data["project_settings"] ["maya"] ["create"] ["CreateUnrealStaticMesh"] From b0a62e3afee78fbad201e1b6c914e8c6241b1d85 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:17:21 +0200 Subject: [PATCH 132/186] Remove unused imports --- openpype/hosts/maya/api/pipeline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 3647ec0b6b..04ff810873 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -28,8 +28,6 @@ from openpype.lib import ( from openpype.pipeline import ( legacy_io, get_current_project_name, - get_current_asset_name, - get_current_task_name, register_loader_plugin_path, register_inventory_action_path, register_creator_plugin_path, From fe786236cddf4cb58ae56f03cf02fcfc29955545 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:18:25 +0200 Subject: [PATCH 133/186] Move Muster module related submitter to Muster module --- .../muster}/plugins/publish/submit_maya_muster.py | 1 + 1 file changed, 1 insertion(+) rename openpype/{hosts/maya => modules/muster}/plugins/publish/submit_maya_muster.py (99%) diff --git a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py b/openpype/modules/muster/plugins/publish/submit_maya_muster.py similarity index 99% rename from openpype/hosts/maya/plugins/publish/submit_maya_muster.py rename to openpype/modules/muster/plugins/publish/submit_maya_muster.py index c174fa7a33..3c3f901f87 100644 --- a/openpype/hosts/maya/plugins/publish/submit_maya_muster.py +++ b/openpype/modules/muster/plugins/publish/submit_maya_muster.py @@ -25,6 +25,7 @@ def _get_template_id(renderer): :rtype: int """ + # TODO: Use setings from context? templates = get_system_settings()["modules"]["muster"]["templates_mapping"] if not templates: raise RuntimeError(("Muster template mapping missing in " From 31e64d0ef819b0f934e94e7ed47e795991625ac3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:25:55 +0200 Subject: [PATCH 134/186] Pass project settings to menu install so it doesn't need to also retrieve it --- openpype/hosts/maya/api/menu.py | 17 +++++++---------- openpype/hosts/maya/api/pipeline.py | 2 +- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 715f54686c..18a4ea0e9a 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -1,14 +1,13 @@ import os import logging +from functools import partial from qtpy import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.settings import get_project_settings from openpype.pipeline import ( - get_current_project_name, get_current_asset_name, get_current_task_name ) @@ -46,12 +45,12 @@ def get_context_label(): ) -def install(): +def install(project_settings): if cmds.about(batch=True): log.info("Skipping openpype.menu initialization in batch mode..") return - def deferred(): + def add_menu(): pyblish_icon = host_tools.get_pyblish_icon() parent_widget = get_main_window() cmds.menu( @@ -191,7 +190,7 @@ def install(): cmds.setParent(MENU_NAME, menu=True) - def add_scripts_menu(): + def add_scripts_menu(project_settings): try: import scriptsmenu.launchformaya as launchformaya except ImportError: @@ -201,9 +200,6 @@ def install(): ) return - # load configuration of custom menu - project_name = get_current_project_name() - project_settings = get_project_settings(project_name) config = project_settings["maya"]["scriptsmenu"]["definition"] _menu = project_settings["maya"]["scriptsmenu"]["name"] @@ -225,8 +221,9 @@ def install(): # so that it only gets called after Maya UI has initialized too. # This is crucial with Maya 2020+ which initializes without UI # first as a QCoreApplication - maya.utils.executeDeferred(deferred) - cmds.evalDeferred(add_scripts_menu, lowestPriority=True) + maya.utils.executeDeferred(add_menu) + cmds.evalDeferred(partial(add_scripts_menu, project_settings), + lowestPriority=True) def uninstall(): diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index 04ff810873..38d7ae08c1 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -106,7 +106,7 @@ class MayaHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): _set_project() self._register_callbacks() - menu.install() + menu.install(project_settings) register_event_callback("save", on_save) register_event_callback("open", on_open) From 00131ffd152cc27236d98a24a065a82a8bf1d566 Mon Sep 17 00:00:00 2001 From: Kayla Date: Sun, 1 Oct 2023 20:15:22 +0800 Subject: [PATCH 135/186] refactor the validators for skeletonMesh and use rig validators as abstract class & minor tweak on collectors and settings --- .../plugins/publish/collect_skeleton_mesh.py | 5 +- .../plugins/publish/validate_rig_contents.py | 117 +++++++-- .../publish/validate_rig_controllers.py | 50 +++- .../publish/validate_rig_out_set_node_ids.py | 33 ++- .../publish/validate_rig_output_ids.py | 25 +- .../publish/validate_skeleton_rig_content.py | 101 -------- .../validate_skeleton_rig_controller.py | 222 ------------------ .../validate_skeleton_rig_out_set_node_ids.py | 90 ------- .../validate_skeleton_rig_output_ids.py | 124 ---------- .../defaults/project_settings/maya.json | 2 +- .../schemas/schema_maya_publish.json | 2 +- .../maya/server/settings/publishers.py | 2 +- 12 files changed, 211 insertions(+), 562 deletions(-) delete mode 100644 openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py delete mode 100644 openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py delete mode 100644 openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py delete mode 100644 openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py diff --git a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py index 9169e3dc28..648029c3fc 100644 --- a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py @@ -12,7 +12,10 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): families = ["rig"] def process(self, instance): - skeleton_mesh_sets = instance.data.get("skeletonMesh_SET") + skeleton_mesh_sets = [ + i for i in instance + if i.lower().endswith("skeletonmesh_set") + ] if not skeleton_mesh_sets: self.log.debug( "skeletonMesh_SET found. " diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index 23f031a5db..5b8faf6cae 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -25,19 +25,26 @@ class ValidateRigContents(pyblish.api.InstancePlugin): accepted_controllers = ["transform"] def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + raise PublishValidationError( + "Invalid rig content. See log for details.") + + @classmethod + def get_invalid(cls, instance): # Find required sets by suffix - required = ["controls_SET", "out_SET"] + required, rig_sets = cls.get_nodes(instance) missing = [ - key for key in required if key not in instance.data["rig_sets"] + key for key in required if key not in rig_sets ] if missing: raise PublishValidationError( "%s is missing sets: %s" % (instance, ", ".join(missing)) ) - controls_set = instance.data["rig_sets"]["controls_SET"] - out_set = instance.data["rig_sets"]["out_SET"] + controls_set = rig_sets["controls_SET"] + out_set = rig_sets["out_SET"] # Ensure there are at least some transforms or dag nodes # in the rig instance @@ -76,31 +83,29 @@ class ValidateRigContents(pyblish.api.InstancePlugin): invalid_hierarchy.append(node) # Additional validations - invalid_geometry = self.validate_geometry(output_content) - invalid_controls = self.validate_controls(controls_content) + invalid_geometry = cls.validate_geometry(output_content) + invalid_controls = cls.validate_controls(controls_content) error = False if invalid_hierarchy: - self.log.error("Found nodes which reside outside of root group " + cls.log.error("Found nodes which reside outside of root group " "while they are set up for publishing." "\n%s" % invalid_hierarchy) error = True if invalid_controls: - self.log.error("Only transforms can be part of the controls_SET." + cls.log.error("Only transforms can be part of the controls_SET." "\n%s" % invalid_controls) error = True if invalid_geometry: - self.log.error("Only meshes can be part of the out_SET\n%s" + cls.log.error("Only meshes can be part of the out_SET\n%s" % invalid_geometry) error = True + return error - if error: - raise PublishValidationError( - "Invalid rig content. See log for details.") - - def validate_geometry(self, set_members): + @classmethod + def validate_geometry(cls, set_members): """Check if the out set passes the validations Checks if all its set members are within the hierarchy of the root @@ -122,12 +127,13 @@ class ValidateRigContents(pyblish.api.InstancePlugin): fullPath=True) or [] all_shapes = cmds.ls(set_members + shapes, long=True, shapes=True) for shape in all_shapes: - if cmds.nodeType(shape) not in self.accepted_output: + if cmds.nodeType(shape) not in cls.accepted_output: invalid.append(shape) return invalid - def validate_controls(self, set_members): + @classmethod + def validate_controls(cls, set_members): """Check if the controller set passes the validations Checks if all its set members are within the hierarchy of the root @@ -144,7 +150,84 @@ class ValidateRigContents(pyblish.api.InstancePlugin): # Validate control types invalid = [] for node in set_members: - if cmds.nodeType(node) not in self.accepted_controllers: + if cmds.nodeType(node) not in cls.accepted_controllers: invalid.append(node) return invalid + + @classmethod + def get_nodes(cls, instance): + objectsets = ["controls_SET", "out_SET"] + rig_sets_nodes = instance.data.get("rig_sets", []) + return objectsets, rig_sets_nodes + + +class ValidateSkeletonRigContents(ValidateRigContents): + """Ensure skeleton rigs contains pipeline-critical content + + The rigs optionally contain at least two object sets: + "skeletonMesh_SET" - Set of the skinned meshes + with bone hierarchies + + """ + + order = ValidateContentsOrder + label = "Skeleton Rig Contents" + hosts = ["maya"] + families = ["rig.fbx"] + + accepted_output = {"mesh", "transform", "locator"} + + @classmethod + def get_invalid(cls, instance): + objectsets, skeleton_mesh_nodes = cls.get_nodes(instance) + missing = [ + key for key in objectsets if key not in instance.data["rig_sets"] + ] + if missing: + cls.log.debug( + "%s is missing sets: %s" % (instance, ", ".join(missing)) + ) + return + + # Ensure there are at least some transforms or dag nodes + # in the rig instance + set_members = instance.data['setMembers'] + if not cmds.ls(set_members, type="dagNode", long=True): + raise PublishValidationError( + "No dag nodes in the pointcache instance. " + "(Empty instance?)" + ) + # Ensure contents in sets and retrieve long path for all objects + output_content = instance.data.get("skeleton_mesh", []) + output_content = cmds.ls(skeleton_mesh_nodes, long=True) + + # Validate members are inside the hierarchy from root node + root_nodes = cmds.ls(set_members, assemblies=True, long=True) + hierarchy = cmds.listRelatives(root_nodes, allDescendents=True, + fullPath=True) + root_nodes + hierarchy = set(hierarchy) + error = False + invalid_hierarchy = [] + if output_content: + for node in output_content: + if node not in hierarchy: + invalid_hierarchy.append(node) + invalid_geometry = cls.validate_geometry(output_content) + if invalid_hierarchy: + cls.log.error("Found nodes which reside outside of root group " + "while they are set up for publishing." + "\n%s" % invalid_hierarchy) + error = True + if invalid_geometry: + cls.log.error("Found nodes which reside outside of root group " + "while they are set up for publishing." + "\n%s" % invalid_hierarchy) + error = True + return error + + @classmethod + def get_nodes(cls, instance): + objectsets = ["skeletonMesh_SET"] + skeleton_mesh_nodes = instance.data.get("skeleton_mesh", []) + return objectsets, skeleton_mesh_nodes diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py index a3828f871b..c1e3d96bae 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py @@ -59,7 +59,7 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): @classmethod def get_invalid(cls, instance): - controls_set = instance.data["rig_sets"].get("controls_SET") + controls_set = cls.get_node(instance) if not controls_set: cls.log.error( "Must have 'controls_SET' in rig instance" @@ -189,7 +189,7 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): @classmethod def repair(cls, instance): - controls_set = instance.data["rig_sets"].get("controls_SET") + controls_set = cls.get_node(instance) if not controls_set: cls.log.error( "Unable to repair because no 'controls_SET' found in rig " @@ -228,3 +228,49 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): default = cls.CONTROLLER_DEFAULTS[attr] cls.log.info("Setting %s to %s" % (plug, default)) cmds.setAttr(plug, default) + @classmethod + def get_node(cls, instance): + return instance.data["rig_sets"].get("controls_SET") + + +class ValidateSkeletonRigControllers(ValidateRigControllers): + """Validate rig controller for skeletonAnim_SET + + Controls must have the transformation attributes on their default + values of translate zero, rotate zero and scale one when they are + unlocked attributes. + + Unlocked keyable attributes may not have any incoming connections. If + these connections are required for the rig then lock the attributes. + + The visibility attribute must be locked. + + Note that `repair` will: + - Lock all visibility attributes + - Reset all default values for translate, rotate, scale + - Break all incoming connections to keyable attributes + + """ + order = ValidateContentsOrder + 0.05 + label = "Skeleton Rig Controllers" + hosts = ["maya"] + families = ["rig.fbx"] + actions = [RepairAction, + openpype.hosts.maya.api.action.SelectInvalidAction] + + # Default controller values + CONTROLLER_DEFAULTS = { + "translateX": 0, + "translateY": 0, + "translateZ": 0, + "rotateX": 0, + "rotateY": 0, + "rotateZ": 0, + "scaleX": 1, + "scaleY": 1, + "scaleZ": 1 + } + + @classmethod + def get_node(cls, instance): + return instance.data["rig_sets"].get("skeletonAnim_SET") diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index fbd510c683..00eca608a1 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -46,7 +46,7 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): def get_invalid(cls, instance): """Get all nodes which do not match the criteria""" - out_set = instance.data["rig_sets"].get("out_SET") + out_set = cls.get_node(instance) if not out_set: return [] @@ -85,3 +85,34 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): continue lib.set_id(node, sibling_id, overwrite=True) + + @classmethod + def get_node(cls, instance): + return instance.data["rig_sets"].get("out_SET") + + +class ValidateSkeletonRigOutSetNodeIds(ValidateRigOutSetNodeIds): + """Validate if deformed shapes have related IDs to the original shapes + from skeleton set. + + When a deformer is applied in the scene on a referenced mesh that already + had deformers then Maya will create a new shape node for the mesh that + does not have the original id. This validator checks whether the ids are + valid on all the shape nodes in the instance. + + """ + + order = ValidateContentsOrder + families = ["rig.fbx"] + hosts = ['maya'] + label = 'Skeleton Rig Out Set Node Ids' + actions = [ + openpype.hosts.maya.api.action.SelectInvalidAction, + RepairAction + ] + allow_history_only = False + + @classmethod + def get_node(cls, instance): + return instance.data["rig_sets"].get( + "skeletonMesh_SET") diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index 24fb36eb8b..e6204902f0 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -47,7 +47,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): invalid = {} if compute: - out_set = instance.data["rig_sets"].get("out_SET") + out_set = cls.get_node(instance) if not out_set: instance.data["mismatched_output_ids"] = invalid return invalid @@ -115,3 +115,26 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): "Multiple matched ids found. Please repair manually: " "{}".format(multiple_ids_match) ) + + @classmethod + def get_node(cls, instance): + return instance.data["rig_sets"].get("out_SET") + + +class ValidateSkeletonRigOutputIds(ValidateRigOutputIds): + """Validate rig output ids from the skeleton sets. + + Ids must share the same id as similarly named nodes in the scene. This is + to ensure the id from the model is preserved through animation. + + """ + order = ValidateContentsOrder + 0.05 + label = "Skeleton Rig Output Ids" + hosts = ["maya"] + families = ["rig.fbx"] + actions = [RepairAction, + openpype.hosts.maya.api.action.SelectInvalidAction] + + @classmethod + def get_node(cls, instance): + return instance.data["rig_sets"].get("skeletonMesh_SET") diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py deleted file mode 100644 index 8b6cc74332..0000000000 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_content.py +++ /dev/null @@ -1,101 +0,0 @@ -import pyblish.api -from maya import cmds - -from openpype.pipeline.publish import ( - PublishValidationError, - ValidateContentsOrder -) - - -class ValidateSkeletonRigContents(pyblish.api.InstancePlugin): - """Ensure skeleton rigs contains pipeline-critical content - - The rigs optionally contain at least two object sets: - "skeletonMesh_SET" - Set of the skinned meshes - with bone hierarchies - - """ - - order = ValidateContentsOrder - label = "Skeleton Rig Contents" - hosts = ["maya"] - families = ["rig.fbx"] - - accepted_output = {"mesh", "transform", "locator"} - - def process(self, instance): - objectsets = ["skeletonMesh_SET"] - missing = [ - key for key in objectsets if key not in instance.data["rig_sets"] - ] - if missing: - self.log.debug( - "%s is missing sets: %s" % (instance, ", ".join(missing)) - ) - return - - # Ensure there are at least some transforms or dag nodes - # in the rig instance - set_members = instance.data['setMembers'] - if not cmds.ls(set_members, type="dagNode", long=True): - self.log.debug("Skipping instance without dag nodes...") - return - # Ensure contents in sets and retrieve long path for all objects - skeleton_mesh_content = instance.data.get("skeleton_mesh", []) - skeleton_mesh_content = cmds.ls(skeleton_mesh_content, long=True) - - # Validate members are inside the hierarchy from root node - root_node = cmds.ls(set_members, assemblies=True) - hierarchy = cmds.listRelatives(root_node, allDescendents=True, - fullPath=True) - hierarchy = set(hierarchy) - - invalid_hierarchy = [] - if skeleton_mesh_content: - for node in skeleton_mesh_content: - if node not in hierarchy: - invalid_hierarchy.append(node) - invalid_geometry = self.validate_geometry(skeleton_mesh_content) - - error = False - if invalid_hierarchy: - self.log.error("Found nodes which reside outside of root group " - "while they are set up for publishing." - "\n%s" % invalid_hierarchy) - error = True - - if invalid_geometry: - self.log.error("Only meshes can be part of the " - "skeletonMesh_SET\n%s" % invalid_geometry) - error = True - - if error: - raise PublishValidationError( - "Invalid rig content. See log for details.") - - def validate_geometry(self, set_members): - """Check if the out set passes the validations - - Checks if all its set members are within the hierarchy of the root - Checks if the node types of the set members valid - - Args: - set_members: list of nodes of the skeleton_mesh_set - hierarchy: list of nodes which reside under the root node - - Returns: - errors (list) - """ - - # Validate all shape types - invalid = [] - shapes = cmds.listRelatives(set_members, - allDescendents=True, - shapes=True, - fullPath=True) or [] - all_shapes = cmds.ls(set_members + shapes, long=True, shapes=True) - for shape in all_shapes: - if cmds.nodeType(shape) not in self.accepted_output: - invalid.append(shape) - - return invalid diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py deleted file mode 100644 index a31d13bcec..0000000000 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_controller.py +++ /dev/null @@ -1,222 +0,0 @@ -from maya import cmds - -import pyblish.api - -from openpype.pipeline.publish import ( - ValidateContentsOrder, - RepairAction, - PublishValidationError -) -import openpype.hosts.maya.api.action -from openpype.hosts.maya.api.lib import undo_chunk - - -class ValidateSkeletonRigControllers(pyblish.api.InstancePlugin): - """Validate rig controller for skeletonAnim_SET - - Controls must have the transformation attributes on their default - values of translate zero, rotate zero and scale one when they are - unlocked attributes. - - Unlocked keyable attributes may not have any incoming connections. If - these connections are required for the rig then lock the attributes. - - The visibility attribute must be locked. - - Note that `repair` will: - - Lock all visibility attributes - - Reset all default values for translate, rotate, scale - - Break all incoming connections to keyable attributes - - """ - order = ValidateContentsOrder + 0.05 - label = "Skeleton Rig Controllers" - hosts = ["maya"] - families = ["rig.fbx"] - actions = [RepairAction, - openpype.hosts.maya.api.action.SelectInvalidAction] - - # Default controller values - CONTROLLER_DEFAULTS = { - "translateX": 0, - "translateY": 0, - "translateZ": 0, - "rotateX": 0, - "rotateY": 0, - "rotateZ": 0, - "scaleX": 1, - "scaleY": 1, - "scaleZ": 1 - } - - def process(self, instance): - invalid = self.get_invalid(instance) - if invalid: - raise PublishValidationError( - '{} failed, see log information'.format(self.label) - ) - - @classmethod - def get_invalid(cls, instance): - skeleton_set = instance.data["rig_sets"].get("skeletonAnim_SET") - if not skeleton_set: - cls.log.info( - "No 'skeletonAnim_SET' in rig instance" - ) - return - controls = cmds.sets(skeleton_set, query=True) - lookup = set(instance[:]) - if not all(control in lookup for control in cmds.ls(controls, - long=True)): - cls.log.error( - "All controls must be inside the rig's group." - ) - return [controls] - # Validate all controls - has_connections = list() - has_unlocked_visibility = list() - has_non_default_values = list() - for control in controls: - if cls.get_connected_attributes(control): - has_connections.append(control) - - # check if visibility is locked - attribute = "{}.visibility".format(control) - locked = cmds.getAttr(attribute, lock=True) - if not locked: - has_unlocked_visibility.append(control) - - if cls.get_non_default_attributes(control): - has_non_default_values.append(control) - - if has_connections: - cls.log.error("Controls have input connections: " - "%s" % has_connections) - - if has_non_default_values: - cls.log.error("Controls have non-default values: " - "%s" % has_non_default_values) - - if has_unlocked_visibility: - cls.log.error("Controls have unlocked visibility " - "attribute: %s" % has_unlocked_visibility) - - invalid = [] - if (has_connections or - has_unlocked_visibility or - has_non_default_values): - invalid = set() - invalid.update(has_connections) - invalid.update(has_non_default_values) - invalid.update(has_unlocked_visibility) - invalid = list(invalid) - cls.log.error("Invalid rig controllers. See log for details.") - - return invalid - - @classmethod - def get_non_default_attributes(cls, control): - """Return attribute plugs with non-default values - - Args: - control (str): Name of control node. - - Returns: - list: The invalid plugs - - """ - - invalid = [] - for attr, default in cls.CONTROLLER_DEFAULTS.items(): - if cmds.attributeQuery(attr, node=control, exists=True): - plug = "{}.{}".format(control, attr) - - # Ignore locked attributes - locked = cmds.getAttr(plug, lock=True) - if locked: - continue - - value = cmds.getAttr(plug) - if value != default: - cls.log.warning("Control non-default value: " - "%s = %s" % (plug, value)) - invalid.append(plug) - - return invalid - - @staticmethod - def get_connected_attributes(control): - """Return attribute plugs with incoming connections. - - This will also ensure no (driven) keys on unlocked keyable attributes. - - Args: - control (str): Name of control node. - - Returns: - list: The invalid plugs - - """ - import maya.cmds as mc - - # Support controls without any attributes returning None - attributes = mc.listAttr(control, keyable=True, scalar=True) or [] - invalid = [] - for attr in attributes: - plug = "{}.{}".format(control, attr) - - # Ignore locked attributes - locked = cmds.getAttr(plug, lock=True) - if locked: - continue - - # Ignore proxy connections. - if (cmds.addAttr(plug, query=True, exists=True) and - cmds.addAttr(plug, query=True, usedAsProxy=True)): - continue - - # Check for incoming connections - if cmds.listConnections(plug, source=True, destination=False): - invalid.append(plug) - - return invalid - - @classmethod - def repair(cls, instance): - skeleton_set = instance.data["rig_sets"].get("skeletonAnim_SET") - if not skeleton_set: - cls.log.error( - "Unable to repair because no 'skeletonAnim_SET' found in rig " - "instance: {}".format(instance) - ) - return - # Use a single undo chunk - with undo_chunk(): - controls = cmds.sets(skeleton_set, query=True) - for control in controls: - # Lock visibility - attr = "{}.visibility".format(control) - locked = cmds.getAttr(attr, lock=True) - if not locked: - cls.log.info("Locking visibility for %s" % control) - cmds.setAttr(attr, lock=True) - - # Remove incoming connections - invalid_plugs = cls.get_connected_attributes(control) - if invalid_plugs: - for plug in invalid_plugs: - cls.log.info("Breaking input connection to %s" % plug) - source = cmds.listConnections(plug, - source=True, - destination=False, - plugs=True)[0] - cmds.disconnectAttr(source, plug) - - # Reset non-default values - invalid_plugs = cls.get_non_default_attributes(control) - if invalid_plugs: - for plug in invalid_plugs: - attr = plug.split(".")[-1] - default = cls.CONTROLLER_DEFAULTS[attr] - cls.log.info("Setting %s to %s" % (plug, default)) - cmds.setAttr(plug, default) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py deleted file mode 100644 index 73ad12f422..0000000000 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_out_set_node_ids.py +++ /dev/null @@ -1,90 +0,0 @@ -import maya.cmds as cmds - -import pyblish.api - -import openpype.hosts.maya.api.action -from openpype.hosts.maya.api import lib -from openpype.pipeline.publish import ( - RepairAction, - ValidateContentsOrder, - PublishValidationError -) - - -class ValidateSkeletonRigOutSetNodeIds(pyblish.api.InstancePlugin): - """Validate if deformed shapes have related IDs to the original shapes - from skeleton set. - - When a deformer is applied in the scene on a referenced mesh that already - had deformers then Maya will create a new shape node for the mesh that - does not have the original id. This validator checks whether the ids are - valid on all the shape nodes in the instance. - - """ - - order = ValidateContentsOrder - families = ["rig.fbx"] - hosts = ['maya'] - label = 'Skeleton Rig Out Set Node Ids' - actions = [ - openpype.hosts.maya.api.action.SelectInvalidAction, - RepairAction - ] - allow_history_only = False - - def process(self, instance): - """Process all meshes""" - - # Ensure all nodes have a cbId and a related ID to the original shapes - # if a deformer has been created on the shape - invalid = self.get_invalid(instance) - if invalid: - raise PublishValidationError( - "Nodes found with mismatching IDs: {0}".format(invalid) - ) - - @classmethod - def get_invalid(cls, instance): - """Get all nodes which do not match the criteria""" - - skeletonMesh_set = instance.data["rig_sets"].get( - "skeletonMesh_SET") - if not skeletonMesh_set: - return [] - - invalid = [] - members = cmds.sets(skeletonMesh_set, query=True) - shapes = cmds.ls(members, - dag=True, - leaf=True, - shapes=True, - long=True, - noIntermediate=True) - if not shapes: - return [] - for shape in shapes: - sibling_id = lib.get_id_from_sibling( - shape, - history_only=cls.allow_history_only - ) - if sibling_id: - current_id = lib.get_id(shape) - if current_id != sibling_id: - invalid.append(shape) - - return invalid - - @classmethod - def repair(cls, instance): - - for node in cls.get_invalid(instance): - # Get the original id from sibling - sibling_id = lib.get_id_from_sibling( - node, - history_only=cls.allow_history_only - ) - if not sibling_id: - cls.log.error("Could not find ID in siblings for '%s'", node) - continue - - lib.set_id(node, sibling_id, overwrite=True) diff --git a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py deleted file mode 100644 index 735ca27b39..0000000000 --- a/openpype/hosts/maya/plugins/publish/validate_skeleton_rig_output_ids.py +++ /dev/null @@ -1,124 +0,0 @@ -from collections import defaultdict - -from maya import cmds - -import pyblish.api - -import openpype.hosts.maya.api.action -from openpype.hosts.maya.api.lib import get_id, set_id -from openpype.pipeline.publish import ( - RepairAction, - ValidateContentsOrder, - PublishValidationError -) - - -def get_basename(node): - """Return node short name without namespace""" - return node.rsplit("|", 1)[-1].rsplit(":", 1)[-1] - - -class ValidateSkeletonRigOutputIds(pyblish.api.InstancePlugin): - """Validate rig output ids from the skeleton sets. - - Ids must share the same id as similarly named nodes in the scene. This is - to ensure the id from the model is preserved through animation. - - """ - order = ValidateContentsOrder + 0.05 - label = "Skeleton Rig Output Ids" - hosts = ["maya"] - families = ["rig.fbx"] - actions = [RepairAction, - openpype.hosts.maya.api.action.SelectInvalidAction] - - def process(self, instance): - invalid = self.get_invalid(instance, compute=True) - if invalid: - raise PublishValidationError("Found nodes with mismatched IDs.") - - @classmethod - def get_invalid(cls, instance, compute=False): - invalid_matches = cls.get_invalid_matches(instance, compute=compute) - - invalid_skeleton_matches = cls.get_invalid_matches( - instance, compute=compute, set_name="skeletonMesh_SET") - invalid_matches.update(invalid_skeleton_matches) - return list(invalid_matches.keys()) - - @classmethod - def get_invalid_matches(cls, instance, compute=False): - invalid = {} - - if compute: - skeletonMesh_set = instance.data["rig_sets"].get( - "skeletonMesh_SET") - if not skeletonMesh_set: - instance.data["mismatched_output_ids"] = invalid - return invalid - - instance_nodes = cmds.sets( - skeletonMesh_set, query=True, nodesOnly=True) - - instance_nodes = cmds.ls(instance_nodes, long=True) - if not instance_nodes: - return {} - for node in instance_nodes: - shapes = cmds.listRelatives(node, shapes=True, fullPath=True) - if shapes: - instance_nodes.extend(shapes) - - scene_nodes = cmds.ls(type=("transform", "mesh"), long=True) - - scene_nodes_by_basename = defaultdict(list) - for node in scene_nodes: - basename = get_basename(node) - scene_nodes_by_basename[basename].append(node) - - for instance_node in instance_nodes: - basename = get_basename(instance_node) - if basename not in scene_nodes_by_basename: - continue - - matches = scene_nodes_by_basename[basename] - - ids = set(get_id(node) for node in matches) - ids.add(get_id(instance_node)) - - if len(ids) > 1: - cls.log.error( - "\"{}\" id mismatch to: {}".format( - instance_node, matches - ) - ) - invalid[instance_node] = matches - - instance.data["mismatched_output_ids"] = invalid - else: - invalid = instance.data["mismatched_output_ids"] - - return invalid - - @classmethod - def repair(cls, instance): - invalid_matches = cls.get_invalid_matches(instance) - - multiple_ids_match = [] - for instance_node, matches in invalid_matches.items(): - ids = set(get_id(node) for node in matches) - - # If there are multiple scene ids matched, an error needs to be - # raised for manual correction. - if len(ids) > 1: - multiple_ids_match.append({"node": instance_node, - "matches": matches}) - continue - - id_to_set = next(iter(ids)) - set_id(instance_node, id_to_set, overwrite=True) - - if multiple_ids_match: - raise PublishValidationError( - "Multiple matched ids found. Please repair manually: " - "{}".format(multiple_ids_match) - ) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index d3e01287e5..5e11227d68 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1179,7 +1179,7 @@ "active": true }, "ValidateSkeletonTopGroupHierarchy": { - "enabled": false, + "enabled": true, "optional": true, "active": true }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json index f2bbc0f70b..f4db51a079 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_publish.json @@ -829,7 +829,7 @@ }, { "key": "ValidateSkeletonRigContents", - "label": "ValidateSkeleton Rig Contents" + "label": "Validate Skeleton Rig Contents" }, { "key": "ValidateSkeletonRigControllers", diff --git a/server_addon/maya/server/settings/publishers.py b/server_addon/maya/server/settings/publishers.py index cb3af191a8..6c5baa3900 100644 --- a/server_addon/maya/server/settings/publishers.py +++ b/server_addon/maya/server/settings/publishers.py @@ -1234,7 +1234,7 @@ DEFAULT_PUBLISH_SETTINGS = { "active": True }, "ValidateSkeletonTopGroupHierarchy": { - "enabled": False, + "enabled": True, "optional": True, "active": True }, From 64f436a74dc69eb5506ecb9c986a426387b894ad Mon Sep 17 00:00:00 2001 From: Kayla Date: Sun, 1 Oct 2023 20:17:21 +0800 Subject: [PATCH 136/186] hound --- .../hosts/maya/plugins/publish/validate_rig_contents.py | 8 ++++---- .../maya/plugins/publish/validate_rig_controllers.py | 1 + .../hosts/maya/plugins/publish/validate_rig_output_ids.py | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index 5b8faf6cae..f3c2231b1f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -216,13 +216,13 @@ class ValidateSkeletonRigContents(ValidateRigContents): invalid_geometry = cls.validate_geometry(output_content) if invalid_hierarchy: cls.log.error("Found nodes which reside outside of root group " - "while they are set up for publishing." - "\n%s" % invalid_hierarchy) + "while they are set up for publishing." + "\n%s" % invalid_hierarchy) error = True if invalid_geometry: cls.log.error("Found nodes which reside outside of root group " - "while they are set up for publishing." - "\n%s" % invalid_hierarchy) + "while they are set up for publishing." + "\n%s" % invalid_hierarchy) error = True return error diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py index c1e3d96bae..4e86e9859f 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py @@ -228,6 +228,7 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): default = cls.CONTROLLER_DEFAULTS[attr] cls.log.info("Setting %s to %s" % (plug, default)) cmds.setAttr(plug, default) + @classmethod def get_node(cls, instance): return instance.data["rig_sets"].get("controls_SET") diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index e6204902f0..cd6ac511e2 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -47,7 +47,7 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): invalid = {} if compute: - out_set = cls.get_node(instance) + out_set = cls.get_node(instance) if not out_set: instance.data["mismatched_output_ids"] = invalid return invalid From 61f7a2039b60567416d80005c046aab7a5e28de2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 00:57:22 +0200 Subject: [PATCH 137/186] Update openpype/modules/muster/plugins/publish/submit_maya_muster.py --- openpype/modules/muster/plugins/publish/submit_maya_muster.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/muster/plugins/publish/submit_maya_muster.py b/openpype/modules/muster/plugins/publish/submit_maya_muster.py index 3c3f901f87..5c95744876 100644 --- a/openpype/modules/muster/plugins/publish/submit_maya_muster.py +++ b/openpype/modules/muster/plugins/publish/submit_maya_muster.py @@ -25,7 +25,7 @@ def _get_template_id(renderer): :rtype: int """ - # TODO: Use setings from context? + # TODO: Use settings from context? templates = get_system_settings()["modules"]["muster"]["templates_mapping"] if not templates: raise RuntimeError(("Muster template mapping missing in " From 0ddf5ffd90a996037bdaa8905e6fda1d37b4e08a Mon Sep 17 00:00:00 2001 From: Kayla Date: Mon, 2 Oct 2023 12:29:41 +0800 Subject: [PATCH 138/186] minor tweak & abstract some codes into functions in rig content --- openpype/hosts/maya/api/fbx.py | 2 +- .../plugins/publish/extract_fbx_animation.py | 4 +- .../plugins/publish/extract_skeleton_mesh.py | 2 +- .../plugins/publish/validate_rig_contents.py | 141 +++++++++++------- .../publish/validate_rig_controllers.py | 18 ++- .../publish/validate_rig_out_set_node_ids.py | 16 ++ .../publish/validate_rig_output_ids.py | 16 ++ 7 files changed, 136 insertions(+), 63 deletions(-) diff --git a/openpype/hosts/maya/api/fbx.py b/openpype/hosts/maya/api/fbx.py index 2dd4f5a73d..dbb3578f08 100644 --- a/openpype/hosts/maya/api/fbx.py +++ b/openpype/hosts/maya/api/fbx.py @@ -64,7 +64,7 @@ class FBXExtractor: "inputConnections": bool, "upAxis": str, # x, y or z, "triangulate": bool, - "FileVersion": str, + "fileVersion": str, "skeletonDefinitions": bool, "referencedAssetsContent": bool } diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 115ba39986..d67fca4e85 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -45,7 +45,7 @@ class ExtractFBXAnimation(publish.Extractor): # names as existing in the rig workfile namespace, relative_out_set = out_set_name.split(":", 1) cmds.namespace(relativeNames=True) - with namespaced(":" + namespace,new=False, relative_names=True) as namespace: # noqa + with namespaced(":" + namespace, new=False, relative_names=True) as namespace: # noqa fbx_exporter.export(relative_out_set, path) representations = instance.data.setdefault("representations", []) @@ -57,4 +57,4 @@ class ExtractFBXAnimation(publish.Extractor): }) self.log.debug( - "Extracted Fbx animation to: {0}".format(path)) + "Extracted FBX animation to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py b/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py index cecdf282e2..50c1fb3bde 100644 --- a/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py +++ b/openpype/hosts/maya/plugins/publish/extract_skeleton_mesh.py @@ -51,4 +51,4 @@ class ExtractSkeletonMesh(publish.Extractor, "stagingDir": staging_dir }) - self.log.debug("Extract animated FBX successful to: {0}".format(path)) + self.log.debug("Extract FBX to: {0}".format(path)) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index f3c2231b1f..c63d0e0a2e 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -35,26 +35,12 @@ class ValidateRigContents(pyblish.api.InstancePlugin): # Find required sets by suffix required, rig_sets = cls.get_nodes(instance) - missing = [ - key for key in required if key not in rig_sets - ] - if missing: - raise PublishValidationError( - "%s is missing sets: %s" % (instance, ", ".join(missing)) - ) + + cls.validate_missing_objectsets(instance, required, rig_sets) controls_set = rig_sets["controls_SET"] out_set = rig_sets["out_SET"] - # Ensure there are at least some transforms or dag nodes - # in the rig instance - set_members = instance.data['setMembers'] - if not cmds.ls(set_members, type="dagNode", long=True): - raise PublishValidationError( - "No dag nodes in the pointcache instance. " - "(Empty instance?)" - ) - # Ensure contents in sets and retrieve long path for all objects output_content = cmds.sets(out_set, query=True) or [] if not output_content: @@ -68,19 +54,8 @@ class ValidateRigContents(pyblish.api.InstancePlugin): ) controls_content = cmds.ls(controls_content, long=True) - # Validate members are inside the hierarchy from root node - root_nodes = cmds.ls(set_members, assemblies=True, long=True) - hierarchy = cmds.listRelatives(root_nodes, allDescendents=True, - fullPath=True) + root_nodes - hierarchy = set(hierarchy) - - invalid_hierarchy = [] - for node in output_content: - if node not in hierarchy: - invalid_hierarchy.append(node) - for node in controls_content: - if node not in hierarchy: - invalid_hierarchy.append(node) + rig_content = output_content + controls_content + invalid_hierarchy = cls.invalid_hierarchy(instance, rig_content) # Additional validations invalid_geometry = cls.validate_geometry(output_content) @@ -104,6 +79,62 @@ class ValidateRigContents(pyblish.api.InstancePlugin): error = True return error + @classmethod + def validate_missing_objectsets(cls, instance, + required_objsets, rig_sets): + """Validate missing objectsets in rig sets + + Args: + instance (str): instance + required_objsets (list): list of objectset names + rig_sets (list): list of rig sets + + Raises: + PublishValidationError: When the error is raised, it will show + which instance has the missing object sets + """ + missing = [ + key for key in required_objsets if key not in rig_sets + ] + if missing: + raise PublishValidationError( + "%s is missing sets: %s" % (instance, ", ".join(missing)) + ) + + @classmethod + def invalid_hierarchy(cls, instance, content): + """_summary_ + + Args: + instance (str): instance + content (list): list of content from rig sets + + Raises: + PublishValidationError: It means no dag nodes in + the rig instance + + Returns: + list: invalid hierarchy + """ + # Ensure there are at least some transforms or dag nodes + # in the rig instance + set_members = instance.data['setMembers'] + if not cmds.ls(set_members, type="dagNode", long=True): + raise PublishValidationError( + "No dag nodes in the rig instance. " + "(Empty instance?)" + ) + # Validate members are inside the hierarchy from root node + root_nodes = cmds.ls(set_members, assemblies=True, long=True) + hierarchy = cmds.listRelatives(root_nodes, allDescendents=True, + fullPath=True) + root_nodes + hierarchy = set(hierarchy) + invalid_hierarchy = [] + for node in content: + if node not in hierarchy: + invalid_hierarchy.append(node) + return invalid_hierarchy + @classmethod def validate_geometry(cls, set_members): """Check if the out set passes the validations @@ -130,8 +161,6 @@ class ValidateRigContents(pyblish.api.InstancePlugin): if cmds.nodeType(shape) not in cls.accepted_output: invalid.append(shape) - return invalid - @classmethod def validate_controls(cls, set_members): """Check if the controller set passes the validations @@ -157,6 +186,14 @@ class ValidateRigContents(pyblish.api.InstancePlugin): @classmethod def get_nodes(cls, instance): + """Get the target objectsets and rig sets nodes + + Args: + instance (str): instance + + Returns: + list: list of objectsets, list of rig sets nodes + """ objectsets = ["controls_SET", "out_SET"] rig_sets_nodes = instance.data.get("rig_sets", []) return objectsets, rig_sets_nodes @@ -181,39 +218,18 @@ class ValidateSkeletonRigContents(ValidateRigContents): @classmethod def get_invalid(cls, instance): objectsets, skeleton_mesh_nodes = cls.get_nodes(instance) - missing = [ - key for key in objectsets if key not in instance.data["rig_sets"] - ] - if missing: - cls.log.debug( - "%s is missing sets: %s" % (instance, ", ".join(missing)) - ) - return + cls.validate_missing_objectsets( + instance, objectsets, instance.data["rig_sets"]) - # Ensure there are at least some transforms or dag nodes - # in the rig instance - set_members = instance.data['setMembers'] - if not cmds.ls(set_members, type="dagNode", long=True): - raise PublishValidationError( - "No dag nodes in the pointcache instance. " - "(Empty instance?)" - ) # Ensure contents in sets and retrieve long path for all objects output_content = instance.data.get("skeleton_mesh", []) output_content = cmds.ls(skeleton_mesh_nodes, long=True) - # Validate members are inside the hierarchy from root node - root_nodes = cmds.ls(set_members, assemblies=True, long=True) - hierarchy = cmds.listRelatives(root_nodes, allDescendents=True, - fullPath=True) + root_nodes - hierarchy = set(hierarchy) + invalid_hierarchy = cls.invalid_hierarchy( + instance, output_content) + invalid_geometry = cls.validate_geometry(output_content) + error = False - invalid_hierarchy = [] - if output_content: - for node in output_content: - if node not in hierarchy: - invalid_hierarchy.append(node) - invalid_geometry = cls.validate_geometry(output_content) if invalid_hierarchy: cls.log.error("Found nodes which reside outside of root group " "while they are set up for publishing." @@ -228,6 +244,15 @@ class ValidateSkeletonRigContents(ValidateRigContents): @classmethod def get_nodes(cls, instance): + """Get the target objectsets and rig sets nodes + + Args: + instance (str): instance + + Returns: + list: list of objectsets, + list of objects node from skeletonMesh_SET + """ objectsets = ["skeletonMesh_SET"] skeleton_mesh_nodes = instance.data.get("skeleton_mesh", []) return objectsets, skeleton_mesh_nodes diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py index 4e86e9859f..a10e2158fa 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py @@ -231,6 +231,14 @@ class ValidateRigControllers(pyblish.api.InstancePlugin): @classmethod def get_node(cls, instance): + """Get target object nodes from controls_SET + + Args: + instance (str): instance + + Returns: + list: list of object nodes from controls_SET + """ return instance.data["rig_sets"].get("controls_SET") @@ -274,4 +282,12 @@ class ValidateSkeletonRigControllers(ValidateRigControllers): @classmethod def get_node(cls, instance): - return instance.data["rig_sets"].get("skeletonAnim_SET") + """Get target object nodes from skeletonMesh_SET + + Args: + instance (str): instance + + Returns: + list: list of object nodes from skeletonMesh_SET + """ + return instance.data["rig_sets"].get("skeletonMesh_SET") diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index 00eca608a1..6f713a3ca1 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -88,6 +88,14 @@ class ValidateRigOutSetNodeIds(pyblish.api.InstancePlugin): @classmethod def get_node(cls, instance): + """Get target object nodes from out_SET + + Args: + instance (str): instance + + Returns: + list: list of object nodes from out_SET + """ return instance.data["rig_sets"].get("out_SET") @@ -114,5 +122,13 @@ class ValidateSkeletonRigOutSetNodeIds(ValidateRigOutSetNodeIds): @classmethod def get_node(cls, instance): + """Get target object nodes from skeletonMesh_SET + + Args: + instance (str): instance + + Returns: + list: list of object nodes from skeletonMesh_SET + """ return instance.data["rig_sets"].get( "skeletonMesh_SET") diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index cd6ac511e2..ec46b2be87 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -118,6 +118,14 @@ class ValidateRigOutputIds(pyblish.api.InstancePlugin): @classmethod def get_node(cls, instance): + """Get target object nodes from out_SET + + Args: + instance (str): instance + + Returns: + list: list of object nodes from out_SET + """ return instance.data["rig_sets"].get("out_SET") @@ -137,4 +145,12 @@ class ValidateSkeletonRigOutputIds(ValidateRigOutputIds): @classmethod def get_node(cls, instance): + """Get target object nodes from skeletonMesh_SET + + Args: + instance (str): instance + + Returns: + list: list of object nodes from skeletonMesh_SET + """ return instance.data["rig_sets"].get("skeletonMesh_SET") From 2850df81b9c73b6d9ffabebf7b8f9a28b5b9c959 Mon Sep 17 00:00:00 2001 From: Kayla Date: Mon, 2 Oct 2023 17:52:34 +0800 Subject: [PATCH 139/186] fix the over-indented of the namespace function under context manager --- openpype/hosts/maya/api/lib.py | 4 ++-- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index c05e375681..fb19cd64a6 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -938,8 +938,8 @@ def namespaced(namespace, new=True, relative_names=None): if new: namespace = unique_namespace(namespace) cmds.namespace(add=namespace) - if relative_names is not None: - cmds.namespace(relativeNames=relative_names) + if relative_names is not None: + cmds.namespace(relativeNames=relative_names) try: cmds.namespace(set=namespace) yield namespace diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index d67fca4e85..f9e696489e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -44,7 +44,6 @@ class ExtractFBXAnimation(publish.Extractor): # FBX does not include the namespace but preserves the node # names as existing in the rig workfile namespace, relative_out_set = out_set_name.split(":", 1) - cmds.namespace(relativeNames=True) with namespaced(":" + namespace, new=False, relative_names=True) as namespace: # noqa fbx_exporter.export(relative_out_set, path) From 6b3aa6a247c895507771d414c653757634628727 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 2 Oct 2023 12:45:18 +0100 Subject: [PATCH 140/186] Added Cycles render passes --- openpype/hosts/blender/api/render_lib.py | 8 +++++++- .../schemas/projects_schema/schema_project_blender.json | 5 ++++- server_addon/blender/server/settings/render_settings.py | 5 ++++- server_addon/blender/server/version.py | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/api/render_lib.py b/openpype/hosts/blender/api/render_lib.py index 43560ee6d5..d564b5ebcb 100644 --- a/openpype/hosts/blender/api/render_lib.py +++ b/openpype/hosts/blender/api/render_lib.py @@ -116,6 +116,12 @@ def set_render_passes(settings): vl.use_pass_shadow = "shadow" in aov_list vl.use_pass_ambient_occlusion = "ao" in aov_list + cycles = vl.cycles + + cycles.denoising_store_passes = "denoising" in aov_list + cycles.use_pass_volume_direct = "volume_direct" in aov_list + cycles.use_pass_volume_indirect = "volume_indirect" in aov_list + aovs_names = [aov.name for aov in vl.aovs] for cp in custom_passes: cp_name = cp[0] @@ -149,7 +155,7 @@ def set_node_tree(output_path, name, aov_sep, ext, multilayer): # Get the enabled output sockets, that are the active passes for the # render. # We also exclude some layers. - exclude_sockets = ["Image", "Alpha"] + exclude_sockets = ["Image", "Alpha", "Noisy Image"] passes = [ socket for socket in rl_node.outputs diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 4c9405fcd3..535d9434a3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -123,7 +123,10 @@ {"emission": "Emission"}, {"environment": "Environment"}, {"shadow": "Shadow"}, - {"ao": "Ambient Occlusion"} + {"ao": "Ambient Occlusion"}, + {"denoising": "Denoising"}, + {"volume_direct": "Direct Volumetric Scattering"}, + {"volume_indirect": "Indirect Volumetric Scattering"} ] }, { diff --git a/server_addon/blender/server/settings/render_settings.py b/server_addon/blender/server/settings/render_settings.py index 7a47095d3c..f62013982e 100644 --- a/server_addon/blender/server/settings/render_settings.py +++ b/server_addon/blender/server/settings/render_settings.py @@ -40,7 +40,10 @@ def aov_list_enum(): {"value": "emission", "label": "Emission"}, {"value": "environment", "label": "Environment"}, {"value": "shadow", "label": "Shadow"}, - {"value": "ao", "label": "Ambient Occlusion"} + {"value": "ao", "label": "Ambient Occlusion"}, + {"value": "denoising", "label": "Denoising"}, + {"value": "volume_direct", "label": "Direct Volumetric Scattering"}, + {"value": "volume_indirect", "label": "Indirect Volumetric Scattering"} ] diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" From 8e1f3beff6e68ec0111ec68754050dc9768754ab Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 2 Oct 2023 12:45:50 +0100 Subject: [PATCH 141/186] Fixed render job environment variables --- .../modules/deadline/plugins/publish/submit_blender_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 307fc8b5a2..4a7497b075 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -123,7 +123,7 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.EnvironmentKeyValue[key] = value # to recognize job from PYPE for turning Event On/Off - job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" + job_info.add_render_job_env_var() job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1" # Adding file dependencies. From 35b3006f29f0c5c197abb2f010a6e51f6d214c95 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 2 Oct 2023 15:34:48 +0100 Subject: [PATCH 142/186] Improved naming for RenderProduct --- openpype/hosts/blender/api/colorspace.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/colorspace.py b/openpype/hosts/blender/api/colorspace.py index 0f504a3be0..4521612b7d 100644 --- a/openpype/hosts/blender/api/colorspace.py +++ b/openpype/hosts/blender/api/colorspace.py @@ -22,12 +22,11 @@ class RenderProduct(object): class ARenderProduct(object): - def __init__(self): """Constructor.""" # Initialize self.layer_data = self._get_layer_data() - self.layer_data.products = self.get_colorspace_data() + self.layer_data.products = self.get_render_products() def _get_layer_data(self): scene = bpy.context.scene @@ -37,7 +36,7 @@ class ARenderProduct(object): frameEnd=int(scene.frame_end), ) - def get_colorspace_data(self): + def get_render_products(self): """To be implemented by renderer class. This should return a list of RenderProducts. Returns: From 3328ba321db903218b2c28d16676cf6dffd34573 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:01:54 +0200 Subject: [PATCH 143/186] AYON Workfiles Tool: Open workfile changes context (#5671) * change context when opening workfile * do not call 'set_context' in blender * removed unused import --- openpype/hosts/blender/api/ops.py | 10 +-- openpype/tools/ayon_workfiles/abstract.py | 6 +- openpype/tools/ayon_workfiles/control.py | 67 +++++++++++++++---- .../ayon_workfiles/widgets/files_widget.py | 31 ++++++--- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 62d7987b47..0eb90eeff9 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -16,6 +16,7 @@ import bpy import bpy.utils.previews from openpype import style +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import get_current_asset_name, get_current_task_name from openpype.tools.utils import host_tools @@ -331,10 +332,11 @@ class LaunchWorkFiles(LaunchQtApp): def execute(self, context): result = super().execute(context) - self._window.set_context({ - "asset": get_current_asset_name(), - "task": get_current_task_name() - }) + if not AYON_SERVER_ENABLED: + self._window.set_context({ + "asset": get_current_asset_name(), + "task": get_current_task_name() + }) return result def before_window_show(self): diff --git a/openpype/tools/ayon_workfiles/abstract.py b/openpype/tools/ayon_workfiles/abstract.py index f511181837..ce399fd4c6 100644 --- a/openpype/tools/ayon_workfiles/abstract.py +++ b/openpype/tools/ayon_workfiles/abstract.py @@ -914,10 +914,12 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): # Controller actions @abstractmethod - def open_workfile(self, filepath): - """Open a workfile. + def open_workfile(self, folder_id, task_id, filepath): + """Open a workfile for context. Args: + folder_id (str): Folder id. + task_id (str): Task id. filepath (str): Workfile path. """ diff --git a/openpype/tools/ayon_workfiles/control.py b/openpype/tools/ayon_workfiles/control.py index 1153a3c01f..3784959caf 100644 --- a/openpype/tools/ayon_workfiles/control.py +++ b/openpype/tools/ayon_workfiles/control.py @@ -452,12 +452,12 @@ class BaseWorkfileController( self._emit_event("controller.refresh.finished") # Controller actions - def open_workfile(self, filepath): + def open_workfile(self, folder_id, task_id, filepath): self._emit_event("open_workfile.started") failed = False try: - self._host_open_workfile(filepath) + self._open_workfile(folder_id, task_id, filepath) except Exception: failed = True @@ -575,6 +575,53 @@ class BaseWorkfileController( self._expected_selection.get_expected_selection_data(), ) + def _get_event_context_data( + self, project_name, folder_id, task_id, folder=None, task=None + ): + if folder is None: + folder = self.get_folder_entity(folder_id) + if task is None: + task = self.get_task_entity(task_id) + # NOTE keys should be OpenPype compatible + return { + "project_name": project_name, + "folder_id": folder_id, + "asset_id": folder_id, + "asset_name": folder["name"], + "task_id": task_id, + "task_name": task["name"], + "host_name": self.get_host_name(), + } + + def _open_workfile(self, folder_id, task_id, filepath): + project_name = self.get_current_project_name() + event_data = self._get_event_context_data( + project_name, folder_id, task_id + ) + event_data["filepath"] = filepath + + emit_event("workfile.open.before", event_data, source="workfiles.tool") + + # Change context + task_name = event_data["task_name"] + if ( + folder_id != self.get_current_folder_id() + or task_name != self.get_current_task_name() + ): + # Use OpenPype asset-like object + asset_doc = get_asset_by_id( + event_data["project_name"], + event_data["folder_id"], + ) + change_current_context( + asset_doc, + event_data["task_name"] + ) + + self._host_open_workfile(filepath) + + emit_event("workfile.open.after", event_data, source="workfiles.tool") + def _save_as_workfile( self, folder_id, @@ -591,18 +638,14 @@ class BaseWorkfileController( task_name = task["name"] # QUESTION should the data be different for 'before' and 'after'? - # NOTE keys should be OpenPype compatible - event_data = { - "project_name": project_name, - "folder_id": folder_id, - "asset_id": folder_id, - "asset_name": folder["name"], - "task_id": task_id, - "task_name": task_name, - "host_name": self.get_host_name(), + event_data = self._get_event_context_data( + project_name, folder_id, task_id, folder, task + ) + event_data.update({ "filename": filename, "workdir_path": workdir, - } + }) + emit_event("workfile.save.before", event_data, source="workfiles.tool") # Create workfiles root folder diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget.py b/openpype/tools/ayon_workfiles/widgets/files_widget.py index fbf4dbc593..656ddf1dd8 100644 --- a/openpype/tools/ayon_workfiles/widgets/files_widget.py +++ b/openpype/tools/ayon_workfiles/widgets/files_widget.py @@ -106,7 +106,8 @@ class FilesWidget(QtWidgets.QWidget): self._on_published_cancel_clicked) self._selected_folder_id = None - self._selected_tak_name = None + self._selected_task_id = None + self._selected_task_name = None self._pre_select_folder_id = None self._pre_select_task_name = None @@ -178,7 +179,7 @@ class FilesWidget(QtWidgets.QWidget): # ------------------------------------------------------------- # Workarea workfiles # ------------------------------------------------------------- - def _open_workfile(self, filepath): + def _open_workfile(self, folder_id, task_name, filepath): if self._controller.has_unsaved_changes(): result = self._save_changes_prompt() if result is None: @@ -186,12 +187,15 @@ class FilesWidget(QtWidgets.QWidget): if result: self._controller.save_current_workfile() - self._controller.open_workfile(filepath) + self._controller.open_workfile(folder_id, task_name, filepath) def _on_workarea_open_clicked(self): path = self._workarea_widget.get_selected_path() - if path: - self._open_workfile(path) + if not path: + return + folder_id = self._selected_folder_id + task_id = self._selected_task_id + self._open_workfile(folder_id, task_id, path) def _on_current_open_requests(self): self._on_workarea_open_clicked() @@ -238,8 +242,12 @@ class FilesWidget(QtWidgets.QWidget): } filepath = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0] - if filepath: - self._open_workfile(filepath) + if not filepath: + return + + folder_id = self._selected_folder_id + task_id = self._selected_task_id + self._open_workfile(folder_id, task_id, filepath) def _on_workarea_save_clicked(self): result = self._exec_save_as_dialog() @@ -279,10 +287,11 @@ class FilesWidget(QtWidgets.QWidget): def _on_task_changed(self, event): self._selected_folder_id = event["folder_id"] - self._selected_tak_name = event["task_name"] + self._selected_task_id = event["task_id"] + self._selected_task_name = event["task_name"] self._valid_selected_context = ( self._selected_folder_id is not None - and self._selected_tak_name is not None + and self._selected_task_id is not None ) self._update_published_btns_state() @@ -311,7 +320,7 @@ class FilesWidget(QtWidgets.QWidget): if enabled: self._pre_select_folder_id = self._selected_folder_id - self._pre_select_task_name = self._selected_tak_name + self._pre_select_task_name = self._selected_task_name else: self._pre_select_folder_id = None self._pre_select_task_name = None @@ -334,7 +343,7 @@ class FilesWidget(QtWidgets.QWidget): return True if self._pre_select_task_name is None: return False - return self._pre_select_task_name != self._selected_tak_name + return self._pre_select_task_name != self._selected_task_name def _on_published_cancel_clicked(self): folder_id = self._pre_select_folder_id From 7206757c13e29d20f74d173b05327274077aa6c8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 18:18:08 +0200 Subject: [PATCH 144/186] Publisher: Refactor Report Maker plugin data storage to be a dict by plugin.id (#5668) * Refactor plugin data storage to be a dict by plugin.id + Fix `_current_plugin_data` type on `__init__` * Avoid plural when the plugin is singular * Refactor `_plugin_data_by_plugin_id` to `_plugin_data_by_id` --- openpype/tools/publisher/control.py | 56 ++++++++++++++--------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index e6b68906fd..a6264303d5 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -176,11 +176,10 @@ class PublishReportMaker: self._create_discover_result = None self._convert_discover_result = None self._publish_discover_result = None - self._plugin_data = [] - self._plugin_data_with_plugin = [] - self._stored_plugins = set() - self._current_plugin_data = [] + self._plugin_data_by_id = {} + self._current_plugin = None + self._current_plugin_data = {} self._all_instances_by_id = {} self._current_context = None @@ -192,9 +191,9 @@ class PublishReportMaker: create_context.convertor_discover_result ) self._publish_discover_result = create_context.publish_discover_result - self._plugin_data = [] - self._plugin_data_with_plugin = [] - self._stored_plugins = set() + + self._plugin_data_by_id = {} + self._current_plugin = None self._current_plugin_data = {} self._all_instances_by_id = {} self._current_context = context @@ -211,18 +210,11 @@ class PublishReportMaker: if self._current_plugin_data: self._current_plugin_data["passed"] = True + self._current_plugin = plugin self._current_plugin_data = self._add_plugin_data_item(plugin) - def _get_plugin_data_item(self, plugin): - store_item = None - for item in self._plugin_data_with_plugin: - if item["plugin"] is plugin: - store_item = item["data"] - break - return store_item - def _add_plugin_data_item(self, plugin): - if plugin in self._stored_plugins: + if plugin.id in self._plugin_data_by_id: # A plugin would be processed more than once. What can cause it: # - there is a bug in controller # - plugin class is imported into multiple files @@ -230,15 +222,9 @@ class PublishReportMaker: raise ValueError( "Plugin '{}' is already stored".format(str(plugin))) - self._stored_plugins.add(plugin) - plugin_data_item = self._create_plugin_data_item(plugin) + self._plugin_data_by_id[plugin.id] = plugin_data_item - self._plugin_data_with_plugin.append({ - "plugin": plugin, - "data": plugin_data_item - }) - self._plugin_data.append(plugin_data_item) return plugin_data_item def _create_plugin_data_item(self, plugin): @@ -279,7 +265,7 @@ class PublishReportMaker: """Add result of single action.""" plugin = result["plugin"] - store_item = self._get_plugin_data_item(plugin) + store_item = self._plugin_data_by_id.get(plugin.id) if store_item is None: store_item = self._add_plugin_data_item(plugin) @@ -301,14 +287,24 @@ class PublishReportMaker: instance, instance in self._current_context ) - plugins_data = copy.deepcopy(self._plugin_data) - if plugins_data and not plugins_data[-1]["passed"]: - plugins_data[-1]["passed"] = True + plugins_data_by_id = copy.deepcopy( + self._plugin_data_by_id + ) + + # Ensure the current plug-in is marked as `passed` in the result + # so that it shows on reports for paused publishes + if self._current_plugin is not None: + current_plugin_data = plugins_data_by_id.get( + self._current_plugin.id + ) + if current_plugin_data and not current_plugin_data["passed"]: + current_plugin_data["passed"] = True if publish_plugins: for plugin in publish_plugins: - if plugin not in self._stored_plugins: - plugins_data.append(self._create_plugin_data_item(plugin)) + if plugin.id not in plugins_data_by_id: + plugins_data_by_id[plugin.id] = \ + self._create_plugin_data_item(plugin) reports = [] if self._create_discover_result is not None: @@ -329,7 +325,7 @@ class PublishReportMaker: ) return { - "plugins_data": plugins_data, + "plugins_data": list(plugins_data_by_id.values()), "instances": instances_details, "context": self._extract_context_data(self._current_context), "crashed_file_paths": crashed_file_paths, From 4330281688177556995c3acac1e72057c8a3bce5 Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 3 Oct 2023 13:53:30 +0800 Subject: [PATCH 145/186] small bugfix on collect skeleton mesh and minor tweak --- openpype/hosts/maya/api/lib.py | 8 ++--- .../plugins/publish/collect_skeleton_mesh.py | 34 ++++++++++--------- .../plugins/publish/extract_fbx_animation.py | 6 +++- .../publish/validate_animated_reference.py | 7 +++- .../plugins/publish/validate_rig_contents.py | 20 ++++++----- .../publish/validate_rig_controllers.py | 2 -- .../publish/validate_rig_out_set_node_ids.py | 5 --- .../publish/validate_rig_output_ids.py | 2 -- 8 files changed, 43 insertions(+), 41 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index fb19cd64a6..f62463420e 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -4150,11 +4150,9 @@ def create_rig_animation_instance( host = registered_host() create_context = CreateContext(host) # Create the animation instance - rig_sets = [output, controls] - if anim_skeleton: - rig_sets.append(anim_skeleton) - if skeleton_mesh: - rig_sets.append(skeleton_mesh) + rig_sets = [output, controls, anim_skeleton, skeleton_mesh] + # Remove sets that this particular rig does not have + rig_sets = [s for s in rig_sets if s is not None] with maintained_selection(): cmds.select(rig_sets + roots, noExpand=True) create_context.create( diff --git a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py index 648029c3fc..b7849238ae 100644 --- a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py @@ -12,13 +12,11 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): families = ["rig"] def process(self, instance): - skeleton_mesh_sets = [ - i for i in instance - if i.lower().endswith("skeletonmesh_set") - ] - if not skeleton_mesh_sets: + skeleton_mesh_set = instance.data["rig_sets"].get( + "skeletonMesh_SET") + if not skeleton_mesh_set: self.log.debug( - "skeletonMesh_SET found. " + "No skeletonMesh_SET found. " "Skipping collecting of skeleton mesh..." ) return @@ -30,14 +28,18 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): instance.data["skeleton_mesh"] = [] - if skeleton_mesh_sets: + if skeleton_mesh_set: + skeleton_mesh_content = cmds.sets( + skeleton_mesh_set, query=True) or [] + if not skeleton_mesh_content: + self.log.debug( + "No object nodes in skeletonMesh_SET. " + "Skipping collecting of skeleton mesh..." + ) + return instance.data["families"] += ["rig.fbx"] - for skeleton_mesh_set in skeleton_mesh_sets: - skeleton_mesh_content = cmds.sets( - skeleton_mesh_set, query=True) - if skeleton_mesh_content: - instance.data["skeleton_mesh"] += skeleton_mesh_content - self.log.debug( - "Collected skeletonmesh Set: {}".format( - skeleton_mesh_content - )) + instance.data["skeleton_mesh"] = skeleton_mesh_content + self.log.debug( + "Collected skeletonMesh_SET members: {}".format( + skeleton_mesh_content + )) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index f9e696489e..20352e1d8a 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -44,7 +44,11 @@ class ExtractFBXAnimation(publish.Extractor): # FBX does not include the namespace but preserves the node # names as existing in the rig workfile namespace, relative_out_set = out_set_name.split(":", 1) - with namespaced(":" + namespace, new=False, relative_names=True) as namespace: # noqa + with namespaced( + ":" + namespace, + new=False, + relative_names=True + ) as namespace: fbx_exporter.export(relative_out_set, path) representations = instance.data.setdefault("representations", []) diff --git a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py index 3dc272d7cc..fe13561048 100644 --- a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py +++ b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py @@ -1,4 +1,5 @@ import pyblish.api +import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder @@ -14,12 +15,15 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): families = ["animation.fbx"] label = "Animated Reference Rig" accepted_controllers = ["transform", "locator"] + actions = [openpype.hosts.maya.api.action.SelectInvalidAction] def process(self, instance): animated_sets = instance.data["animated_skeleton"] if not animated_sets: self.log.debug( - "No nodes found in skeletonAnim_SET.Skipping...") + "No nodes found in skeletonAnim_SET. " + "Skipping validation of animated reference rig..." + ) return for animated_reference in animated_sets: @@ -37,6 +41,7 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): " should be transforms" ) + @classmethod def validate_controls(self, set_members): """Check if the controller set passes the validations diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index c63d0e0a2e..c1a1ce4ffa 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -1,6 +1,6 @@ import pyblish.api from maya import cmds - +import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( PublishValidationError, ValidateContentsOrder @@ -20,6 +20,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): label = "Rig Contents" hosts = ["maya"] families = ["rig"] + action = [openpype.hosts.maya.api.action.SelectInvalidAction ] accepted_output = ["mesh", "transform"] accepted_controllers = ["transform"] @@ -77,7 +78,8 @@ class ValidateRigContents(pyblish.api.InstancePlugin): cls.log.error("Only meshes can be part of the out_SET\n%s" % invalid_geometry) error = True - return error + if error: + return invalid_hierarchy + invalid_controls + invalid_geometry @classmethod def validate_missing_objectsets(cls, instance, @@ -103,7 +105,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): @classmethod def invalid_hierarchy(cls, instance, content): - """_summary_ + """Check if the sets passes the validation Args: instance (str): instance @@ -192,7 +194,8 @@ class ValidateRigContents(pyblish.api.InstancePlugin): instance (str): instance Returns: - list: list of objectsets, list of rig sets nodes + tuple: 2-tuple of list of objectsets, + list of rig sets nodes """ objectsets = ["controls_SET", "out_SET"] rig_sets_nodes = instance.data.get("rig_sets", []) @@ -213,8 +216,6 @@ class ValidateSkeletonRigContents(ValidateRigContents): hosts = ["maya"] families = ["rig.fbx"] - accepted_output = {"mesh", "transform", "locator"} - @classmethod def get_invalid(cls, instance): objectsets, skeleton_mesh_nodes = cls.get_nodes(instance) @@ -240,7 +241,8 @@ class ValidateSkeletonRigContents(ValidateRigContents): "while they are set up for publishing." "\n%s" % invalid_hierarchy) error = True - return error + if error: + return invalid_hierarchy + invalid_geometry @classmethod def get_nodes(cls, instance): @@ -250,8 +252,8 @@ class ValidateSkeletonRigContents(ValidateRigContents): instance (str): instance Returns: - list: list of objectsets, - list of objects node from skeletonMesh_SET + tuple: 2-tuple of list of objectsets, + list of rig sets nodes """ objectsets = ["skeletonMesh_SET"] skeleton_mesh_nodes = instance.data.get("skeleton_mesh", []) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py index a10e2158fa..82248c57b3 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_controllers.py @@ -264,8 +264,6 @@ class ValidateSkeletonRigControllers(ValidateRigControllers): label = "Skeleton Rig Controllers" hosts = ["maya"] families = ["rig.fbx"] - actions = [RepairAction, - openpype.hosts.maya.api.action.SelectInvalidAction] # Default controller values CONTROLLER_DEFAULTS = { diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py index 6f713a3ca1..80ac0f27e6 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_out_set_node_ids.py @@ -114,11 +114,6 @@ class ValidateSkeletonRigOutSetNodeIds(ValidateRigOutSetNodeIds): families = ["rig.fbx"] hosts = ['maya'] label = 'Skeleton Rig Out Set Node Ids' - actions = [ - openpype.hosts.maya.api.action.SelectInvalidAction, - RepairAction - ] - allow_history_only = False @classmethod def get_node(cls, instance): diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py index ec46b2be87..343d8e6924 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_output_ids.py @@ -140,8 +140,6 @@ class ValidateSkeletonRigOutputIds(ValidateRigOutputIds): label = "Skeleton Rig Output Ids" hosts = ["maya"] families = ["rig.fbx"] - actions = [RepairAction, - openpype.hosts.maya.api.action.SelectInvalidAction] @classmethod def get_node(cls, instance): From a49cacc74f4c89a4a70da4bfa8a6d2c0bf458d0a Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 3 Oct 2023 13:54:37 +0800 Subject: [PATCH 146/186] hound --- openpype/hosts/maya/plugins/publish/validate_rig_contents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index c1a1ce4ffa..963ebcea83 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -20,7 +20,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): label = "Rig Contents" hosts = ["maya"] families = ["rig"] - action = [openpype.hosts.maya.api.action.SelectInvalidAction ] + action = [openpype.hosts.maya.api.action.SelectInvalidAction] accepted_output = ["mesh", "transform"] accepted_controllers = ["transform"] From ae1c98d10cc57d24252b373040a904772dc75ba4 Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 3 Oct 2023 13:56:23 +0800 Subject: [PATCH 147/186] docstring edit for invalid hierarchy in validate rig content --- openpype/hosts/maya/plugins/publish/validate_rig_contents.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index 963ebcea83..f55365cc54 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -105,7 +105,8 @@ class ValidateRigContents(pyblish.api.InstancePlugin): @classmethod def invalid_hierarchy(cls, instance, content): - """Check if the sets passes the validation + """Check if the rig sets passes the validation with + correct hierarchy Args: instance (str): instance From 7cbe5e8f6259fae8134a108799a73a64ceb0a61a Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 3 Oct 2023 15:39:48 +0800 Subject: [PATCH 148/186] docstring tweak and some code twek --- .../plugins/publish/collect_fbx_animation.py | 2 +- .../plugins/publish/collect_skeleton_mesh.py | 27 +++++++++---------- .../publish/validate_animated_reference.py | 2 +- .../plugins/publish/validate_rig_contents.py | 13 ++++----- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index ee5ac741c8..03a54af08a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -19,7 +19,7 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin, return skeleton_sets = [ i for i in instance - if i.lower().endswith("skeletonanim_set") + if i.endswith("skeletonAnim_SET") ] if not skeleton_sets: return diff --git a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py index b7849238ae..31f0eca88c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py +++ b/openpype/hosts/maya/plugins/publish/collect_skeleton_mesh.py @@ -28,18 +28,17 @@ class CollectSkeletonMesh(pyblish.api.InstancePlugin): instance.data["skeleton_mesh"] = [] - if skeleton_mesh_set: - skeleton_mesh_content = cmds.sets( - skeleton_mesh_set, query=True) or [] - if not skeleton_mesh_content: - self.log.debug( - "No object nodes in skeletonMesh_SET. " - "Skipping collecting of skeleton mesh..." - ) - return - instance.data["families"] += ["rig.fbx"] - instance.data["skeleton_mesh"] = skeleton_mesh_content + skeleton_mesh_content = cmds.sets( + skeleton_mesh_set, query=True) or [] + if not skeleton_mesh_content: self.log.debug( - "Collected skeletonMesh_SET members: {}".format( - skeleton_mesh_content - )) + "No object nodes in skeletonMesh_SET. " + "Skipping collecting of skeleton mesh..." + ) + return + instance.data["families"] += ["rig.fbx"] + instance.data["skeleton_mesh"] = skeleton_mesh_content + self.log.debug( + "Collected skeletonMesh_SET members: {}".format( + skeleton_mesh_content + )) diff --git a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py index fe13561048..dd606ceaef 100644 --- a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py +++ b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py @@ -43,7 +43,7 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): @classmethod def validate_controls(self, set_members): - """Check if the controller set passes the validations + """Check if the controller set contains only accepted node types. Checks if all its set members are within the hierarchy of the root Checks if the node types of the set members valid diff --git a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py index f55365cc54..106b4024e2 100644 --- a/openpype/hosts/maya/plugins/publish/validate_rig_contents.py +++ b/openpype/hosts/maya/plugins/publish/validate_rig_contents.py @@ -105,8 +105,8 @@ class ValidateRigContents(pyblish.api.InstancePlugin): @classmethod def invalid_hierarchy(cls, instance, content): - """Check if the rig sets passes the validation with - correct hierarchy + """ + Check if all rig set members are within the hierarchy of the rig root Args: instance (str): instance @@ -140,9 +140,7 @@ class ValidateRigContents(pyblish.api.InstancePlugin): @classmethod def validate_geometry(cls, set_members): - """Check if the out set passes the validations - - Checks if all its set members are within the hierarchy of the root + """ Checks if the node types of the set members valid Args: @@ -166,9 +164,8 @@ class ValidateRigContents(pyblish.api.InstancePlugin): @classmethod def validate_controls(cls, set_members): - """Check if the controller set passes the validations - - Checks if all its set members are within the hierarchy of the root + """ + Checks if the control set members are allowed node types. Checks if the node types of the set members valid Args: From 3d2b0172859a8d5b5ab9d5e287bd38e8f6528311 Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 3 Oct 2023 17:32:58 +0800 Subject: [PATCH 149/186] minor tweak --- openpype/hosts/maya/plugins/publish/collect_fbx_animation.py | 2 +- openpype/hosts/maya/plugins/publish/extract_fbx_animation.py | 3 +-- .../hosts/maya/plugins/publish/validate_animated_reference.py | 3 ++- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py index 03a54af08a..aef8765e9c 100644 --- a/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_fbx_animation.py @@ -33,4 +33,4 @@ class CollectFbxAnimation(pyblish.api.InstancePlugin, skeleton_content )) if skeleton_content: - instance.data["animated_skeleton"] += skeleton_content + instance.data["animated_skeleton"] = skeleton_content diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 20352e1d8a..27be724ec0 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -39,11 +39,10 @@ class ExtractFBXAnimation(publish.Extractor): fbx_exporter.set_options_from_instance(instance) - out_set_name = next(out for out in out_set) # Export from the rig's namespace so that the exported # FBX does not include the namespace but preserves the node # names as existing in the rig workfile - namespace, relative_out_set = out_set_name.split(":", 1) + namespace, relative_out_set = out_set[0].split(":", 1) with namespaced( ":" + namespace, new=False, diff --git a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py index dd606ceaef..4537892d6d 100644 --- a/openpype/hosts/maya/plugins/publish/validate_animated_reference.py +++ b/openpype/hosts/maya/plugins/publish/validate_animated_reference.py @@ -18,7 +18,7 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): actions = [openpype.hosts.maya.api.action.SelectInvalidAction] def process(self, instance): - animated_sets = instance.data["animated_skeleton"] + animated_sets = instance.data.get("animated_skeleton", []) if not animated_sets: self.log.debug( "No nodes found in skeletonAnim_SET. " @@ -58,6 +58,7 @@ class ValidateAnimatedReferenceRig(pyblish.api.InstancePlugin): # Validate control types invalid = [] + set_members = cmds.ls(set_members, long=True) for node in set_members: if cmds.nodeType(node) not in self.accepted_controllers: invalid.append(node) From 12be0186b0b634fb60ee87cb0c9250a410d39b02 Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 3 Oct 2023 17:40:38 +0800 Subject: [PATCH 150/186] minor tweak --- .../hosts/maya/plugins/publish/extract_fbx_animation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 27be724ec0..8036c799e7 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -31,7 +31,7 @@ class ExtractFBXAnimation(publish.Extractor): path = path.replace("\\", "/") fbx_exporter = fbx.FBXExtractor(log=self.log) - out_set = instance.data.get("animated_skeleton", []) + out_group = instance.data.get("animated_skeleton", []) # Export instance.data["constraints"] = True instance.data["skeletonDefinitions"] = True @@ -42,13 +42,13 @@ class ExtractFBXAnimation(publish.Extractor): # Export from the rig's namespace so that the exported # FBX does not include the namespace but preserves the node # names as existing in the rig workfile - namespace, relative_out_set = out_set[0].split(":", 1) + namespace, relative_out_group = out_group[0].split(":", 1) with namespaced( ":" + namespace, new=False, relative_names=True ) as namespace: - fbx_exporter.export(relative_out_set, path) + fbx_exporter.export(relative_out_group, path) representations = instance.data.setdefault("representations", []) representations.append({ From 7f5be3d61ad6b09ca29123f4b3cef2496e03787e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 3 Oct 2023 10:42:16 +0100 Subject: [PATCH 151/186] Fix remove/update in new layout instance --- openpype/hosts/blender/plugins/load/load_blend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_blend.py b/openpype/hosts/blender/plugins/load/load_blend.py index fa41f4374b..25d6568889 100644 --- a/openpype/hosts/blender/plugins/load/load_blend.py +++ b/openpype/hosts/blender/plugins/load/load_blend.py @@ -244,7 +244,7 @@ class BlendLoader(plugin.AssetLoader): for parent in parent_containers: parent.get(AVALON_PROPERTY)["members"] = list(filter( lambda i: i not in members, - parent.get(AVALON_PROPERTY)["members"])) + parent.get(AVALON_PROPERTY).get("members", []))) for attr in attrs: for data in getattr(bpy.data, attr): From 71838b05153576235b969e915ac716fac88ce97a Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 3 Oct 2023 20:06:14 +0800 Subject: [PATCH 152/186] abstract relativeNames namesapces into function --- openpype/hosts/maya/api/lib.py | 41 +++++++++++++++++++ .../plugins/publish/extract_fbx_animation.py | 15 ++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index f62463420e..1923a008d5 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -183,6 +183,47 @@ def maintained_selection(): cmds.select(clear=True) +def get_namespace(node): + """Return namespace of given node""" + return node.rsplit("|", 1)[-1].rsplit(":", 1)[0] + + +def strip_namespace(node, namespace): + """Strip given namespace from node path. + + The namespace will only be stripped from names + if it starts with that namespace. If the namespace + occurs within another namespace it's not removed. + + Examples: + >>> strip_namespace("namespace:node", namespace="namespace:") + "node" + >>> strip_namespace("hello:world:node", namespace="hello:world") + "node" + >>> strip_namespace("hello:world:node", namespace="hello") + "world:node" + >>> strip_namespace("hello:world:node", namespace="world") + "hello:world:node" + >>> strip_namespace("ns:group|ns:node", namespace="ns") + "group|node" + + Returns: + str: Node name without given starting namespace. + + """ + + # Ensure namespace ends with `:` + if not namespace.endswith(":"): + namespace = "{}:".format(namespace) + + # The long path for a node can also have the namespace + # in its parents so we need to remove it from each + return "|".join( + name[len(namespace):] if name.startswith(namespace) else name + for name in node.split("|") + ) + + def get_custom_namespace(custom_namespace): """Return unique namespace. diff --git a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py index 8036c799e7..8288bc9329 100644 --- a/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/maya/plugins/publish/extract_fbx_animation.py @@ -6,7 +6,9 @@ import pyblish.api from openpype.pipeline import publish from openpype.hosts.maya.api import fbx -from openpype.hosts.maya.api.lib import namespaced +from openpype.hosts.maya.api.lib import ( + namespaced, get_namespace, strip_namespace +) class ExtractFBXAnimation(publish.Extractor): @@ -31,24 +33,25 @@ class ExtractFBXAnimation(publish.Extractor): path = path.replace("\\", "/") fbx_exporter = fbx.FBXExtractor(log=self.log) - out_group = instance.data.get("animated_skeleton", []) + out_members = instance.data.get("animated_skeleton", []) # Export instance.data["constraints"] = True instance.data["skeletonDefinitions"] = True instance.data["referencedAssetsContent"] = True - fbx_exporter.set_options_from_instance(instance) - # Export from the rig's namespace so that the exported # FBX does not include the namespace but preserves the node # names as existing in the rig workfile - namespace, relative_out_group = out_group[0].split(":", 1) + namespace = get_namespace(out_members[0]) + relative_out_members = [ + strip_namespace(node, namespace) for node in out_members + ] with namespaced( ":" + namespace, new=False, relative_names=True ) as namespace: - fbx_exporter.export(relative_out_group, path) + fbx_exporter.export(relative_out_members, path) representations = instance.data.setdefault("representations", []) representations.append({ From 8b16bacb5315377f7a6f2539b838ea32da0bacf6 Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 3 Oct 2023 20:21:37 +0800 Subject: [PATCH 153/186] make sure the get_namespace won't error out if it doesn't get anything --- openpype/hosts/maya/api/lib.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 1923a008d5..510d4ecc85 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -185,7 +185,11 @@ def maintained_selection(): def get_namespace(node): """Return namespace of given node""" - return node.rsplit("|", 1)[-1].rsplit(":", 1)[0] + node_name = node.rsplit("|", 1)[-1] + if ":" in node_name: + return node_name.rsplit(":", 1)[0] + else: + return "" def strip_namespace(node, namespace): From 56aa22af17be49e18e939f43407eab31338071af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:37:51 +0200 Subject: [PATCH 154/186] :bug: fix variable name overwriting list with string is causing `TypeError: string indices must be integers` in subsequent iterations --- .../plugins/publish/validate_plugin_path_attributes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py b/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py index 9f47bf7a3d..3974150a10 100644 --- a/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py @@ -30,18 +30,18 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin): def get_invalid(cls, instance): invalid = list() - file_attr = cls.attribute - if not file_attr: + file_attrs = cls.attribute + if not file_attrs: return invalid # Consider only valid node types to avoid "Unknown object type" warning all_node_types = set(cmds.allNodeTypes()) - node_types = [key for key in file_attr.keys() if key in all_node_types] + node_types = [key for key in file_attrs.keys() if key in all_node_types] for node, node_type in pairwise(cmds.ls(type=node_types, showType=True)): # get the filepath - file_attr = "{}.{}".format(node, file_attr[node_type]) + file_attr = "{}.{}".format(node, file_attrs[node_type]) filepath = cmds.getAttr(file_attr) if filepath and not os.path.exists(filepath): From 1f265f064a17c4a7befda74ea0cb10ac67b92e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:40:54 +0200 Subject: [PATCH 155/186] :dog: fix hound --- .../maya/plugins/publish/validate_plugin_path_attributes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py b/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py index 3974150a10..cb5c68e4ab 100644 --- a/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py +++ b/openpype/hosts/maya/plugins/publish/validate_plugin_path_attributes.py @@ -36,7 +36,10 @@ class ValidatePluginPathAttributes(pyblish.api.InstancePlugin): # Consider only valid node types to avoid "Unknown object type" warning all_node_types = set(cmds.allNodeTypes()) - node_types = [key for key in file_attrs.keys() if key in all_node_types] + node_types = [ + key for key in file_attrs.keys() + if key in all_node_types + ] for node, node_type in pairwise(cmds.ls(type=node_types, showType=True)): From e78b6065acac274bb3655c60f8a6081372338d8b Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Tue, 3 Oct 2023 18:18:02 +0100 Subject: [PATCH 156/186] Add openpype_mongo command flag for testing. (#5676) * Add openpype_mongo command flag for testing. * Revert back to TEST_OPENPYPE_MONGO TEST_OPENPYPE_MONGO is placeholder used in all source test sip in `input/env_vars/env_var` not a env variable itself * Fix openpype_mongo fixture Fixture decorator was missing. If value passed from command line should be used, it must come first as `env_var` fixture should already contain valid default Mongo uri. * Renamed command line argument to mongo_url --------- Co-authored-by: kalisp --- openpype/cli.py | 8 ++++++-- openpype/pype_commands.py | 16 +++++++++++++++- tests/conftest.py | 10 ++++++++++ tests/lib/testing_classes.py | 4 ++-- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/openpype/cli.py b/openpype/cli.py index 0df277fb0a..7422f32f13 100644 --- a/openpype/cli.py +++ b/openpype/cli.py @@ -290,11 +290,15 @@ def run(script): "--setup_only", help="Only create dbs, do not run tests", default=None) +@click.option("--mongo_url", + help="MongoDB for testing.", + default=None) def runtests(folder, mark, pyargs, test_data_folder, persist, app_variant, - timeout, setup_only): + timeout, setup_only, mongo_url): """Run all automatic tests after proper initialization via start.py""" PypeCommands().run_tests(folder, mark, pyargs, test_data_folder, - persist, app_variant, timeout, setup_only) + persist, app_variant, timeout, setup_only, + mongo_url) @main.command(help="DEPRECATED - run sync server") diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 7f1c3b01e2..7adebbbc97 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -213,7 +213,8 @@ class PypeCommands: pass def run_tests(self, folder, mark, pyargs, - test_data_folder, persist, app_variant, timeout, setup_only): + test_data_folder, persist, app_variant, timeout, setup_only, + mongo_url): """ Runs tests from 'folder' @@ -226,6 +227,10 @@ class PypeCommands: end app_variant (str): variant (eg 2020 for AE), empty if use latest installed version + timeout (int): explicit timeout for single test + setup_only (bool): if only preparation steps should be + triggered, no tests (useful for debugging/development) + mongo_url (str): url to Openpype Mongo database """ print("run_tests") if folder: @@ -264,6 +269,15 @@ class PypeCommands: if setup_only: args.extend(["--setup_only", setup_only]) + if mongo_url: + args.extend(["--mongo_url", mongo_url]) + else: + msg = ( + "Either provide uri to MongoDB through environment variable" + " OPENPYPE_MONGO or the command flag --mongo_url" + ) + assert not os.environ.get("OPENPYPE_MONGO"), msg + print("run_tests args: {}".format(args)) import pytest pytest.main(args) diff --git a/tests/conftest.py b/tests/conftest.py index 4f7c17244b..6e82c9917d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,6 +29,11 @@ def pytest_addoption(parser): help="True - only setup test, do not run any tests" ) + parser.addoption( + "--mongo_url", action="store", default=None, + help="Provide url of the Mongo database." + ) + @pytest.fixture(scope="module") def test_data_folder(request): @@ -55,6 +60,11 @@ def setup_only(request): return request.config.getoption("--setup_only") +@pytest.fixture(scope="module") +def mongo_url(request): + return request.config.getoption("--mongo_url") + + @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object diff --git a/tests/lib/testing_classes.py b/tests/lib/testing_classes.py index 2af4af02de..e82e438e54 100644 --- a/tests/lib/testing_classes.py +++ b/tests/lib/testing_classes.py @@ -147,11 +147,11 @@ class ModuleUnitTest(BaseTest): @pytest.fixture(scope="module") def db_setup(self, download_test_data, env_var, monkeypatch_session, - request): + request, mongo_url): """Restore prepared MongoDB dumps into selected DB.""" backup_dir = os.path.join(download_test_data, "input", "dumps") - uri = os.environ.get("OPENPYPE_MONGO") + uri = mongo_url or os.environ.get("OPENPYPE_MONGO") db_handler = DBHandler(uri) db_handler.setup_from_dump(self.TEST_DB_NAME, backup_dir, overwrite=True, From 02b64a40f1f0181c684fee182a72f723c680bb34 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 4 Oct 2023 03:24:55 +0000 Subject: [PATCH 157/186] [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 8234258f19..399c1404b1 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.2-nightly.1" +__version__ = "3.17.2-nightly.2" From 4a2417d2ca741c6b5a8db81b042e0bfff7a4d12a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 4 Oct 2023 03:25:31 +0000 Subject: [PATCH 158/186] 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 9fb7bbc66c..e3ca8262e5 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.17.2-nightly.2 - 3.17.2-nightly.1 - 3.17.1 - 3.17.1-nightly.3 @@ -134,7 +135,6 @@ body: - 3.14.10-nightly.9 - 3.14.10-nightly.8 - 3.14.10-nightly.7 - - 3.14.10-nightly.6 validations: required: true - type: dropdown From c5bf50a4541a4c5ddfb1d64bd51b1654abf4cbe5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 17:12:28 +0800 Subject: [PATCH 159/186] minor docstring and code tweaks for ExtractReviewMov --- openpype/hosts/nuke/api/lib.py | 12 +++++------- openpype/hosts/nuke/api/plugin.py | 6 +++--- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 07f394ec00..380d9a42d1 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3425,17 +3425,15 @@ def create_viewer_profile_string(viewer, display=None, path_like=False): return "{} ({})".format(viewer, display) -def get_head_filename_without_hashes(original_path, name): - """Function to get the renamed head filename without frame hashes - To avoid the system being confused on finding the filename with - frame hashes if the head of the filename has the hashed symbol +def prepend_name_before_hashed_frame(original_path, name): + """Function to prepend an extra name before the hashed frame numbers Examples: - >>> get_head_filename_without_hashes("render.####.exr", "baking") + >>> prepend_name_before_hashed_frame("render.####.exr", "baking") render.baking.####.exr - >>> get_head_filename_without_hashes("render.%04d.exr", "tag") + >>> prepend_name_before_hashed_frame("render.%04d.exr", "tag") render.tag.%d.exr - >>> get_head_filename_without_hashes("exr.####.exr", "foo") + >>> prepend_name_before_hashed_frame("exr.####.exr", "foo") exr.foo.%04d.exr Args: diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 81841d17be..2f432ad9b6 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -39,7 +39,7 @@ from .lib import ( get_view_process_node, get_viewer_config_from_string, deprecated, - get_head_filename_without_hashes, + prepend_name_before_hashed_frame, get_filenames_without_hash ) from .pipeline import ( @@ -820,12 +820,12 @@ class ExporterReviewMov(ExporterReview): if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: # filename would be with frame hashes if # the file extension is not in video format - filename = get_head_filename_without_hashes( + filename = prepend_name_before_hashed_frame( self.path_in, self.name) self.file = filename # make sure the filename are in # correct image output format - if ".{}".format(self.ext) not in self.file: + if not self.file.endswith(".{}".format(ext)): filename_no_ext, _ = os.path.splitext(filename) self.file = "{}.{}".format(filename_no_ext, self.ext) From 090f1e041b14bfaa6e903f3f1fe836f84acb4253 Mon Sep 17 00:00:00 2001 From: Claudio Hickstein <122775550+spmhickstein@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:17:22 +0200 Subject: [PATCH 160/186] Deadline: handle all valid paths in RenderExecutable (#5694) * handle all valid paths in RenderExecutable * Update openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --------- Co-authored-by: Petr Kalis Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../modules/deadline/repository/custom/plugins/Ayon/Ayon.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py index a29acf9823..2c55e7c951 100644 --- a/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py +++ b/openpype/modules/deadline/repository/custom/plugins/Ayon/Ayon.py @@ -96,7 +96,7 @@ class AyonDeadlinePlugin(DeadlinePlugin): for path in exe_list.split(";"): if path.startswith("~"): path = os.path.expanduser(path) - expanded_paths.append(path) + expanded_paths.append(path) exe = FileUtils.SearchFileList(";".join(expanded_paths)) if exe == "": From aef56b7cd3c89a39bdb0975534d3a78a1c307133 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 18:52:45 +0800 Subject: [PATCH 161/186] remove unnecessary function --- openpype/hosts/nuke/api/lib.py | 26 -------------------------- openpype/hosts/nuke/api/plugin.py | 12 +++++------- 2 files changed, 5 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 380d9a42d1..390545b806 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3425,32 +3425,6 @@ def create_viewer_profile_string(viewer, display=None, path_like=False): return "{} ({})".format(viewer, display) -def prepend_name_before_hashed_frame(original_path, name): - """Function to prepend an extra name before the hashed frame numbers - - Examples: - >>> prepend_name_before_hashed_frame("render.####.exr", "baking") - render.baking.####.exr - >>> prepend_name_before_hashed_frame("render.%04d.exr", "tag") - render.tag.%d.exr - >>> prepend_name_before_hashed_frame("exr.####.exr", "foo") - exr.foo.%04d.exr - - Args: - original_path (str): the filename with frame hashes - name (str): the name of the tags - - Returns: - str: the renamed filename with the tag - """ - filename = os.path.basename(original_path) - - def insert_name(matchobj): - return "{}.{}".format(name, matchobj.group(0)) - - return re.sub(r"(%\d*d)|#+", insert_name, filename) - - def get_filenames_without_hash(filename, frame_start, frame_end): """Get filenames without frame hash i.e. "renderCompositingMain.baking.0001.exr" diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 2f432ad9b6..2ce41f61c7 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -39,7 +39,6 @@ from .lib import ( get_view_process_node, get_viewer_config_from_string, deprecated, - prepend_name_before_hashed_frame, get_filenames_without_hash ) from .pipeline import ( @@ -818,15 +817,14 @@ class ExporterReviewMov(ExporterReview): self.file = self.fhead + self.name + ".{}".format(self.ext) if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: - # filename would be with frame hashes if - # the file extension is not in video format - filename = prepend_name_before_hashed_frame( - self.path_in, self.name) - self.file = filename + filename = os.path.basename(self.path_in) + self.file = self.fhead + self.name + ".{}".format( + filename.split(".", 1)[-1] + ) # make sure the filename are in # correct image output format if not self.file.endswith(".{}".format(ext)): - filename_no_ext, _ = os.path.splitext(filename) + filename_no_ext, _ = os.path.splitext(self.file) self.file = "{}.{}".format(filename_no_ext, self.ext) self.path = os.path.join( From 13b46070fe9fef0b6474a50e9cebb7d7b73eac43 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 19:32:41 +0800 Subject: [PATCH 162/186] use re.sub in the function for review frame sequence name --- openpype/hosts/nuke/api/plugin.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 2ce41f61c7..d54967aa15 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -818,9 +818,8 @@ class ExporterReviewMov(ExporterReview): self.file = self.fhead + self.name + ".{}".format(self.ext) if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: filename = os.path.basename(self.path_in) - self.file = self.fhead + self.name + ".{}".format( - filename.split(".", 1)[-1] - ) + self.file = re.sub( + self.fhead, self.fhead + self.name + ".", filename) # make sure the filename are in # correct image output format if not self.file.endswith(".{}".format(ext)): From 253c895363d2ec1fb0dcdc6d40d4539b120cefd6 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 21:48:44 +0800 Subject: [PATCH 163/186] clean up the code for implementating variable for self.file when the self.ext is image format --- openpype/hosts/nuke/api/plugin.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index d54967aa15..067814679c 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -817,15 +817,11 @@ class ExporterReviewMov(ExporterReview): self.file = self.fhead + self.name + ".{}".format(self.ext) if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: - filename = os.path.basename(self.path_in) - self.file = re.sub( - self.fhead, self.fhead + self.name + ".", filename) - # make sure the filename are in - # correct image output format - if not self.file.endswith(".{}".format(ext)): - filename_no_ext, _ = os.path.splitext(self.file) - self.file = "{}.{}".format(filename_no_ext, self.ext) - + filename_no_ext = os.path.splitext( + os.path.basename(self.path_in))[0] + after_head = filename_no_ext[len(self.fhead):] + self.file = "{}{}.{}.{}".format( + self.fhead, self.name, after_head, self.ext) self.path = os.path.join( self.staging_dir, self.file).replace("\\", "/") From 22ac8e7ac6fcece2aa73b066c02008b34bd6afff Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 22:08:33 +0800 Subject: [PATCH 164/186] use fomrat string for self.file --- openpype/hosts/nuke/api/plugin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 067814679c..0da181908e 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -815,8 +815,10 @@ class ExporterReviewMov(ExporterReview): self.log.info("File info was set...") - self.file = self.fhead + self.name + ".{}".format(self.ext) - if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: + if ".{}".format(self.ext) in VIDEO_EXTENSIONS: + self.file = "{}{}.{}".format( + self.fhead, self.name, self.ext) + else: filename_no_ext = os.path.splitext( os.path.basename(self.path_in))[0] after_head = filename_no_ext[len(self.fhead):] From a75e5d8db6aad7c0462af8f1637080f8c619ca0e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 4 Oct 2023 22:09:24 +0800 Subject: [PATCH 165/186] add comments --- openpype/hosts/nuke/api/plugin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 0da181908e..c39e3c339d 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -819,6 +819,11 @@ class ExporterReviewMov(ExporterReview): self.file = "{}{}.{}".format( self.fhead, self.name, self.ext) else: + # Output is image (or image sequence) + # When the file is an image it's possible it + # has extra information after the `fhead` that + # we want to preserve, e.g. like frame numbers + # or frames hashes like `####` filename_no_ext = os.path.splitext( os.path.basename(self.path_in))[0] after_head = filename_no_ext[len(self.fhead):] From bf15868fc4cd0a75fe6017ddbdf316beb76af303 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Oct 2023 17:48:16 +0200 Subject: [PATCH 166/186] Chore: Refactor Resolve into new style HostBase, IWorkfileHost, ILoadHost --- openpype/hosts/resolve/api/__init__.py | 11 +-- openpype/hosts/resolve/api/menu.py | 9 +- openpype/hosts/resolve/api/pipeline.py | 95 ++++++++++--------- openpype/hosts/resolve/api/utils.py | 10 +- openpype/hosts/resolve/startup.py | 9 +- .../resolve/utility_scripts/OpenPype__Menu.py | 7 +- 6 files changed, 71 insertions(+), 70 deletions(-) diff --git a/openpype/hosts/resolve/api/__init__.py b/openpype/hosts/resolve/api/__init__.py index 2b4546f8d6..dba275e6c4 100644 --- a/openpype/hosts/resolve/api/__init__.py +++ b/openpype/hosts/resolve/api/__init__.py @@ -6,13 +6,10 @@ from .utils import ( ) from .pipeline import ( - install, - uninstall, + ResolveHost, ls, containerise, update_container, - publish, - launch_workfiles_app, maintained_selection, remove_instance, list_instances @@ -76,14 +73,10 @@ __all__ = [ "bmdvf", # pipeline - "install", - "uninstall", + "ResolveHost", "ls", "containerise", "update_container", - "reload_pipeline", - "publish", - "launch_workfiles_app", "maintained_selection", "remove_instance", "list_instances", diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index b3717e01ea..34a63eb89f 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -5,11 +5,6 @@ from qtpy import QtWidgets, QtCore from openpype.tools.utils import host_tools -from .pipeline import ( - publish, - launch_workfiles_app -) - def load_stylesheet(): path = os.path.join(os.path.dirname(__file__), "menu_style.qss") @@ -113,7 +108,7 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_workfile_clicked(self): print("Clicked Workfile") - launch_workfiles_app() + host_tools.show_workfiles() def on_create_clicked(self): print("Clicked Create") @@ -121,7 +116,7 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_publish_clicked(self): print("Clicked Publish") - publish(None) + host_tools.show_publish(parent=None) def on_load_clicked(self): print("Clicked Load") diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 899cb825bb..19c7b13371 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -12,14 +12,24 @@ from openpype.pipeline import ( schema, register_loader_plugin_path, register_creator_plugin_path, - deregister_loader_plugin_path, - deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) -from openpype.tools.utils import host_tools +from openpype.host import ( + HostBase, + IWorkfileHost, + ILoadHost +) from . import lib from .utils import get_resolve_module +from .workio import ( + open_file, + save_file, + file_extensions, + has_unsaved_changes, + work_root, + current_file +) log = Logger.get_logger(__name__) @@ -32,53 +42,59 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") AVALON_CONTAINERS = ":AVALON_CONTAINERS" -def install(): - """Install resolve-specific functionality of avalon-core. +class ResolveHost(HostBase, IWorkfileHost, ILoadHost): + name = "maya" - This is where you install menus and register families, data - and loaders into resolve. + def __init__(self): + super(ResolveHost, self).__init__() - It is called automatically when installing via `api.install(resolve)`. + def install(self): + """Install resolve-specific functionality of avalon-core. - See the Maya equivalent for inspiration on how to implement this. + This is where you install menus and register families, data + and loaders into resolve. - """ + It is called automatically when installing via `api.install(resolve)`. - log.info("openpype.hosts.resolve installed") + See the Maya equivalent for inspiration on how to implement this. - pyblish.register_host("resolve") - pyblish.register_plugin_path(PUBLISH_PATH) - log.info("Registering DaVinci Resovle plug-ins..") + """ - register_loader_plugin_path(LOAD_PATH) - register_creator_plugin_path(CREATE_PATH) + log.info("openpype.hosts.resolve installed") - # register callback for switching publishable - pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) + pyblish.register_host("resolve") + pyblish.register_plugin_path(PUBLISH_PATH) + print("Registering DaVinci Resolve plug-ins..") - get_resolve_module() + register_loader_plugin_path(LOAD_PATH) + register_creator_plugin_path(CREATE_PATH) + # register callback for switching publishable + pyblish.register_callback("instanceToggled", + on_pyblish_instance_toggled) -def uninstall(): - """Uninstall all that was installed + get_resolve_module() - This is where you undo everything that was done in `install()`. - That means, removing menus, deregistering families and data - and everything. It should be as though `install()` was never run, - because odds are calling this function means the user is interested - in re-installing shortly afterwards. If, for example, he has been - modifying the menu or registered families. + def open_workfile(self, filepath): + return open_file(filepath) - """ - pyblish.deregister_host("resolve") - pyblish.deregister_plugin_path(PUBLISH_PATH) - log.info("Deregistering DaVinci Resovle plug-ins..") + def save_workfile(self, filepath=None): + return save_file(filepath) - deregister_loader_plugin_path(LOAD_PATH) - deregister_creator_plugin_path(CREATE_PATH) + def work_root(self, session): + return work_root(session) - # register callback for switching publishable - pyblish.deregister_callback("instanceToggled", on_pyblish_instance_toggled) + def get_current_workfile(self): + return current_file() + + def workfile_has_unsaved_changes(self): + return has_unsaved_changes() + + def get_workfile_extensions(self): + return file_extensions() + + def get_containers(self): + return ls() def containerise(timeline_item, @@ -206,15 +222,6 @@ def update_container(timeline_item, data=None): return bool(lib.set_timeline_item_pype_tag(timeline_item, container)) -def launch_workfiles_app(*args): - host_tools.show_workfiles() - - -def publish(parent): - """Shorthand to publish from within host""" - return host_tools.show_publish() - - @contextlib.contextmanager def maintained_selection(): """Maintain selection during context diff --git a/openpype/hosts/resolve/api/utils.py b/openpype/hosts/resolve/api/utils.py index 871b3af38d..851851a3b3 100644 --- a/openpype/hosts/resolve/api/utils.py +++ b/openpype/hosts/resolve/api/utils.py @@ -17,7 +17,7 @@ def get_resolve_module(): # dont run if already loaded if api.bmdvr: log.info(("resolve module is assigned to " - f"`pype.hosts.resolve.api.bmdvr`: {api.bmdvr}")) + f"`openpype.hosts.resolve.api.bmdvr`: {api.bmdvr}")) return api.bmdvr try: """ @@ -41,6 +41,10 @@ def get_resolve_module(): ) elif sys.platform.startswith("linux"): expected_path = "/opt/resolve/libs/Fusion/Modules" + else: + raise NotImplementedError( + "Unsupported platform: {}".format(sys.platform) + ) # check if the default path has it... print(("Unable to find module DaVinciResolveScript from " @@ -74,6 +78,6 @@ def get_resolve_module(): api.bmdvr = bmdvr api.bmdvf = bmdvf log.info(("Assigning resolve module to " - f"`pype.hosts.resolve.api.bmdvr`: {api.bmdvr}")) + f"`openpype.hosts.resolve.api.bmdvr`: {api.bmdvr}")) log.info(("Assigning resolve module to " - f"`pype.hosts.resolve.api.bmdvf`: {api.bmdvf}")) + f"`openpype.hosts.resolve.api.bmdvf`: {api.bmdvf}")) diff --git a/openpype/hosts/resolve/startup.py b/openpype/hosts/resolve/startup.py index e807a48f5a..5ac3c99524 100644 --- a/openpype/hosts/resolve/startup.py +++ b/openpype/hosts/resolve/startup.py @@ -27,7 +27,8 @@ def ensure_installed_host(): if host: return host - install_host(openpype.hosts.resolve.api) + host = openpype.hosts.resolve.api.ResolveHost() + install_host(host) return registered_host() @@ -37,10 +38,10 @@ def launch_menu(): openpype.hosts.resolve.api.launch_pype_menu() -def open_file(path): +def open_workfile(path): # Avoid the need to "install" the host host = ensure_installed_host() - host.open_file(path) + host.open_workfile(path) def main(): @@ -49,7 +50,7 @@ def main(): if workfile_path and os.path.exists(workfile_path): log.info(f"Opening last workfile: {workfile_path}") - open_file(workfile_path) + open_workfile(workfile_path) else: log.info("No last workfile set to open. Skipping..") diff --git a/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py b/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py index 1087a7b7a0..4f14927074 100644 --- a/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py +++ b/openpype/hosts/resolve/utility_scripts/OpenPype__Menu.py @@ -8,12 +8,13 @@ log = Logger.get_logger(__name__) def main(env): - import openpype.hosts.resolve.api as bmdvr + from openpype.hosts.resolve.api import ResolveHost, launch_pype_menu # activate resolve from openpype - install_host(bmdvr) + host = ResolveHost() + install_host(host) - bmdvr.launch_pype_menu() + launch_pype_menu() if __name__ == "__main__": From 05b487a435f8add93e638c2bb4433d7c8dc15054 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Oct 2023 20:30:45 +0200 Subject: [PATCH 167/186] Apply suggestions from code review by @iLLiCiTiT Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/resolve/api/pipeline.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 19c7b13371..05f556fa5b 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -43,10 +43,7 @@ AVALON_CONTAINERS = ":AVALON_CONTAINERS" class ResolveHost(HostBase, IWorkfileHost, ILoadHost): - name = "maya" - - def __init__(self): - super(ResolveHost, self).__init__() + name = "resolve" def install(self): """Install resolve-specific functionality of avalon-core. @@ -62,7 +59,7 @@ class ResolveHost(HostBase, IWorkfileHost, ILoadHost): log.info("openpype.hosts.resolve installed") - pyblish.register_host("resolve") + pyblish.register_host(self.name) pyblish.register_plugin_path(PUBLISH_PATH) print("Registering DaVinci Resolve plug-ins..") From d26df62e1502beed52522efe3a4b5a6bb9679ee8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Oct 2023 13:00:52 +0200 Subject: [PATCH 168/186] do not crash if task is not filled --- openpype/plugins/actions/open_file_explorer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/plugins/actions/open_file_explorer.py b/openpype/plugins/actions/open_file_explorer.py index e4fbd91143..2eb4ee7f8e 100644 --- a/openpype/plugins/actions/open_file_explorer.py +++ b/openpype/plugins/actions/open_file_explorer.py @@ -83,10 +83,6 @@ class OpenTaskPath(LauncherAction): if os.path.exists(valid_workdir): return valid_workdir - # If task was selected, try to find asset path only to asset - if not task_name: - raise AssertionError("Folder does not exist.") - data.pop("task", None) workdir = anatomy.templates_obj["work"]["folder"].format(data) valid_workdir = self._find_first_filled_path(workdir) From 2c68dbcc72a185e69232dc9646dd0c6eebef1f7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Oct 2023 13:01:02 +0200 Subject: [PATCH 169/186] change an error a little bit --- openpype/plugins/actions/open_file_explorer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/actions/open_file_explorer.py b/openpype/plugins/actions/open_file_explorer.py index 2eb4ee7f8e..1568c41fbd 100644 --- a/openpype/plugins/actions/open_file_explorer.py +++ b/openpype/plugins/actions/open_file_explorer.py @@ -91,7 +91,7 @@ class OpenTaskPath(LauncherAction): valid_workdir = os.path.normpath(valid_workdir) if os.path.exists(valid_workdir): return valid_workdir - raise AssertionError("Folder does not exist.") + raise AssertionError("Folder does not exist yet.") @staticmethod def open_in_explorer(path): From 9c543d12ddb6057c120565099fab20b5a06bd4b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Oct 2023 14:44:13 +0200 Subject: [PATCH 170/186] AYON: Small settings fixes (#5699) * add label to nuke 13-0 variant * make 'ExtractReviewIntermediates' settings backwards compatible * add remaining labels for '13-0' variants --- .../nuke/plugins/publish/extract_review_intermediates.py | 4 +++- openpype/settings/ayon_settings.py | 6 ++++-- server_addon/applications/server/applications.json | 5 +++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py index da060e3157..9730e3b61f 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py @@ -33,11 +33,13 @@ class ExtractReviewIntermediates(publish.Extractor): """ nuke_publish = project_settings["nuke"]["publish"] deprecated_setting = nuke_publish["ExtractReviewDataMov"] - current_setting = nuke_publish["ExtractReviewIntermediates"] + current_setting = nuke_publish.get("ExtractReviewIntermediates") if deprecated_setting["enabled"]: # Use deprecated settings if they are still enabled cls.viewer_lut_raw = deprecated_setting["viewer_lut_raw"] cls.outputs = deprecated_setting["outputs"] + elif current_setting is None: + pass elif current_setting["enabled"]: cls.viewer_lut_raw = current_setting["viewer_lut_raw"] cls.outputs = current_setting["outputs"] diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 68693bb953..d54d71e851 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -748,15 +748,17 @@ def _convert_nuke_project_settings(ayon_settings, output): ) new_review_data_outputs = {} - outputs_settings = None + outputs_settings = [] # Check deprecated ExtractReviewDataMov # settings for backwards compatibility deprecrated_review_settings = ayon_publish["ExtractReviewDataMov"] current_review_settings = ( - ayon_publish["ExtractReviewIntermediates"] + ayon_publish.get("ExtractReviewIntermediates") ) if deprecrated_review_settings["enabled"]: outputs_settings = deprecrated_review_settings["outputs"] + elif current_review_settings is None: + pass elif current_review_settings["enabled"]: outputs_settings = current_review_settings["outputs"] diff --git a/server_addon/applications/server/applications.json b/server_addon/applications/server/applications.json index 8e5b28623e..e40b8d41f6 100644 --- a/server_addon/applications/server/applications.json +++ b/server_addon/applications/server/applications.json @@ -237,6 +237,7 @@ }, { "name": "13-0", + "label": "13.0", "use_python_2": false, "executables": { "windows": [ @@ -319,6 +320,7 @@ }, { "name": "13-0", + "label": "13.0", "use_python_2": false, "executables": { "windows": [ @@ -405,6 +407,7 @@ }, { "name": "13-0", + "label": "13.0", "use_python_2": false, "executables": { "windows": [ @@ -491,6 +494,7 @@ }, { "name": "13-0", + "label": "13.0", "use_python_2": false, "executables": { "windows": [ @@ -577,6 +581,7 @@ }, { "name": "13-0", + "label": "13.0", "use_python_2": false, "executables": { "windows": [ From d7dcc3862f9e558262b6c1a6a74a24ce24f2a160 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 16:03:28 +0300 Subject: [PATCH 171/186] display changes in menu, add menu button --- openpype/hosts/houdini/api/lib.py | 78 +++++++++++++------ openpype/hosts/houdini/api/pipeline.py | 5 +- .../hosts/houdini/startup/MainMenuCommon.xml | 8 ++ 3 files changed, 65 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index ce89ffe606..eea2df7369 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -18,7 +18,7 @@ from openpype.pipeline.context_tools import ( get_current_context_template_data, get_current_project_asset ) - +from openpype.widgets import popup import hou @@ -166,8 +166,6 @@ def validate_fps(): if current_fps != fps: - from openpype.widgets import popup - # Find main window parent = hou.ui.mainQtWindow() if parent is None: @@ -755,31 +753,29 @@ def get_camera_from_container(container): return cameras[0] -def update_houdini_vars_context(): - """Update Houdini vars to match current context. +def get_context_var_changes(): + """get context var changes.""" - This will only do something if the setting is enabled in project settings. - """ + houdini_vars_to_update = {} project_settings = get_current_project_settings() houdini_vars_settings = \ project_settings["houdini"]["general"]["update_houdini_var_context"] if not houdini_vars_settings["enabled"]: - return + return houdini_vars_to_update houdini_vars = houdini_vars_settings["houdini_vars"] # No vars specified - nothing to do if not houdini_vars: - return + return houdini_vars_to_update # Get Template data template_data = get_current_context_template_data() # Set Houdini Vars for item in houdini_vars: - # For consistency reasons we always force all vars to be uppercase item["var"] = item["var"].upper() @@ -789,21 +785,13 @@ def update_houdini_vars_context(): template_data ) - if item["is_directory"]: - item_value = item_value.replace("\\", "/") - try: - os.makedirs(item_value) - except OSError as e: - if e.errno != errno.EEXIST: - print( - " - Failed to create ${} dir. Maybe due to " - "insufficient permissions.".format(item["var"]) - ) - if item["var"] == "JOB" and item_value == "": # sync $JOB to $HIP if $JOB is empty item_value = os.environ["HIP"] + if item["is_directory"]: + item_value = item_value.replace("\\", "/") + current_value = hou.hscript("echo -n `${}`".format(item["var"]))[0] # sync both environment variables. @@ -812,7 +800,49 @@ def update_houdini_vars_context(): os.environ[item["var"]] = current_value if current_value != item_value: - hou.hscript("set {}={}".format(item["var"], item_value)) - os.environ[item["var"]] = item_value + houdini_vars_to_update.update({item["var"]: (current_value, item_value, item["is_directory"])}) - print(" - Updated ${} to {}".format(item["var"], item_value)) + return houdini_vars_to_update + + +def update_houdini_vars_context(): + """Update asset context variables""" + + for var, (old, new, is_directory) in get_context_var_changes().items(): + if is_directory: + try: + os.makedirs(new) + except OSError as e: + if e.errno != errno.EEXIST: + print( + " - Failed to create ${} dir. Maybe due to " + "insufficient permissions.".format(var) + ) + + hou.hscript("set {}={}".format(var, new)) + os.environ[var] = new + print(" - Updated ${} to {}".format(var, new)) + + +def update_houdini_vars_context_dialog(): + """Show pop-up to update asset context variables""" + update_vars = get_context_var_changes() + if not update_vars: + # Nothing to change + return + + message = "\n".join( + "${}: {} -> {}".format(var, old or "None", new) + for var, (old, new, is_directory) in update_vars.items() + ) + parent = hou.ui.mainQtWindow() + dialog = popup.PopupUpdateKeys(parent=parent) + dialog.setModal(True) + dialog.setWindowTitle("Houdini scene has outdated asset variables") + dialog.setMessage(message) + dialog.setButtonText("Fix") + + # on_show is the Fix button clicked callback + dialog.on_clicked_state.connect(lambda: update_houdini_vars_context()) + + dialog.show() diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index f753d518f0..f8db45c56b 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -301,7 +301,7 @@ def on_save(): log.info("Running callback on save..") # update houdini vars - lib.update_houdini_vars_context() + lib.update_houdini_vars_context_dialog() nodes = lib.get_id_required_nodes() for node, new_id in lib.generate_ids(nodes): @@ -339,7 +339,7 @@ def on_open(): log.info("Running callback on open..") # update houdini vars - lib.update_houdini_vars_context() + lib.update_houdini_vars_context_dialog() # Validate FPS after update_task_from_path to # ensure it is using correct FPS for the asset @@ -405,6 +405,7 @@ def _set_context_settings(): """ lib.reset_framerange() + lib.update_houdini_vars_context() def on_pyblish_instance_toggled(instance, new_value, old_value): diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index 5818a117eb..b2e32a70f9 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -86,6 +86,14 @@ openpype.hosts.houdini.api.lib.reset_framerange() ]]> + + + + + From 809b6df22178fda6c3b496cd49edc6799f9c3081 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 16:05:47 +0300 Subject: [PATCH 172/186] resolve hound --- openpype/hosts/houdini/api/lib.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index eea2df7369..68ba4589d9 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -800,7 +800,13 @@ def get_context_var_changes(): os.environ[item["var"]] = current_value if current_value != item_value: - houdini_vars_to_update.update({item["var"]: (current_value, item_value, item["is_directory"])}) + houdini_vars_to_update.update( + { + item["var"]: ( + current_value, item_value, item["is_directory"] + ) + } + ) return houdini_vars_to_update From 35194b567f7599b9480b8ba3e048229a0503faa0 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 16:06:35 +0300 Subject: [PATCH 173/186] resolve hound 2 --- 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 68ba4589d9..fa94ddfeb4 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -814,7 +814,7 @@ def get_context_var_changes(): def update_houdini_vars_context(): """Update asset context variables""" - for var, (old, new, is_directory) in get_context_var_changes().items(): + for var, (_old, new, is_directory) in get_context_var_changes().items(): if is_directory: try: os.makedirs(new) From 0af1b5846c31602944bb78d396d6e8fdd23b23bd Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 16:07:19 +0300 Subject: [PATCH 174/186] resolve hound 3 --- 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 fa94ddfeb4..e4040852b9 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -839,7 +839,7 @@ def update_houdini_vars_context_dialog(): message = "\n".join( "${}: {} -> {}".format(var, old or "None", new) - for var, (old, new, is_directory) in update_vars.items() + for var, (old, new, _is_directory) in update_vars.items() ) parent = hou.ui.mainQtWindow() dialog = popup.PopupUpdateKeys(parent=parent) From 3daa0749d1a40eb0c22214fb69cc5ef76965b65d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Oct 2023 15:08:31 +0200 Subject: [PATCH 175/186] AYON Launcher tool: Fix skip last workfile boolean (#5700) * reverse the boolean to skip last workfile * remove 'start_last_workfile' key to keep logic based on settings * change 'skip_last_workfile' for all variants of DCC * fix context menu on ungrouped items * better sort of action items --- openpype/tools/ayon_launcher/abstract.py | 4 +- openpype/tools/ayon_launcher/control.py | 4 +- .../tools/ayon_launcher/models/actions.py | 10 +++-- .../tools/ayon_launcher/ui/actions_widget.py | 37 +++++++++++++++++-- 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/openpype/tools/ayon_launcher/abstract.py b/openpype/tools/ayon_launcher/abstract.py index 00502fe930..f2ef681c62 100644 --- a/openpype/tools/ayon_launcher/abstract.py +++ b/openpype/tools/ayon_launcher/abstract.py @@ -272,7 +272,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): @abstractmethod def set_application_force_not_open_workfile( - self, project_name, folder_id, task_id, action_id, enabled + self, project_name, folder_id, task_id, action_ids, enabled ): """This is application action related to force not open last workfile. @@ -280,7 +280,7 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): project_name (Union[str, None]): Project name. folder_id (Union[str, None]): Folder id. task_id (Union[str, None]): Task id. - action_id (str): Action identifier. + action_id (Iterable[str]): Action identifiers. enabled (bool): New value of force not open workfile. """ diff --git a/openpype/tools/ayon_launcher/control.py b/openpype/tools/ayon_launcher/control.py index 09e07893c3..a6e528b104 100644 --- a/openpype/tools/ayon_launcher/control.py +++ b/openpype/tools/ayon_launcher/control.py @@ -121,10 +121,10 @@ class BaseLauncherController( project_name, folder_id, task_id) def set_application_force_not_open_workfile( - self, project_name, folder_id, task_id, action_id, enabled + self, project_name, folder_id, task_id, action_ids, enabled ): self._actions_model.set_application_force_not_open_workfile( - project_name, folder_id, task_id, action_id, enabled + project_name, folder_id, task_id, action_ids, enabled ) def trigger_action(self, project_name, folder_id, task_id, identifier): diff --git a/openpype/tools/ayon_launcher/models/actions.py b/openpype/tools/ayon_launcher/models/actions.py index 24fea44db2..93ec115734 100644 --- a/openpype/tools/ayon_launcher/models/actions.py +++ b/openpype/tools/ayon_launcher/models/actions.py @@ -326,13 +326,14 @@ class ActionsModel: return output def set_application_force_not_open_workfile( - self, project_name, folder_id, task_id, action_id, enabled + self, project_name, folder_id, task_id, action_ids, enabled ): no_workfile_reg_data = self._get_no_last_workfile_reg_data() project_data = no_workfile_reg_data.setdefault(project_name, {}) folder_data = project_data.setdefault(folder_id, {}) task_data = folder_data.setdefault(task_id, {}) - task_data[action_id] = enabled + for action_id in action_ids: + task_data[action_id] = enabled self._launcher_tool_reg.set_item( self._not_open_workfile_reg_key, no_workfile_reg_data ) @@ -359,7 +360,10 @@ class ActionsModel: project_name, folder_id, task_id ) force_not_open_workfile = per_action.get(identifier, False) - action.data["start_last_workfile"] = force_not_open_workfile + if force_not_open_workfile: + action.data["start_last_workfile"] = False + else: + action.data.pop("start_last_workfile", None) action.process(session) except Exception as exc: self.log.warning("Action trigger failed.", exc_info=True) diff --git a/openpype/tools/ayon_launcher/ui/actions_widget.py b/openpype/tools/ayon_launcher/ui/actions_widget.py index d04f8f8d24..0630d1d5b5 100644 --- a/openpype/tools/ayon_launcher/ui/actions_widget.py +++ b/openpype/tools/ayon_launcher/ui/actions_widget.py @@ -19,6 +19,21 @@ ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 6 FORCE_NOT_OPEN_WORKFILE_ROLE = QtCore.Qt.UserRole + 7 +def _variant_label_sort_getter(action_item): + """Get variant label value for sorting. + + Make sure the output value is a string. + + Args: + action_item (ActionItem): Action item. + + Returns: + str: Variant label or empty string. + """ + + return action_item.variant_label or "" + + class ActionsQtModel(QtGui.QStandardItemModel): """Qt model for actions. @@ -51,6 +66,7 @@ class ActionsQtModel(QtGui.QStandardItemModel): self._controller = controller self._items_by_id = {} + self._action_items_by_id = {} self._groups_by_id = {} self._selected_project_name = None @@ -72,8 +88,12 @@ class ActionsQtModel(QtGui.QStandardItemModel): def get_item_by_id(self, action_id): return self._items_by_id.get(action_id) + def get_action_item_by_id(self, action_id): + return self._action_items_by_id.get(action_id) + def _clear_items(self): self._items_by_id = {} + self._action_items_by_id = {} self._groups_by_id = {} root = self.invisibleRootItem() root.removeRows(0, root.rowCount()) @@ -101,12 +121,14 @@ class ActionsQtModel(QtGui.QStandardItemModel): groups_by_id = {} for action_items in items_by_label.values(): + action_items.sort(key=_variant_label_sort_getter, reverse=True) first_item = next(iter(action_items)) all_action_items_info.append((first_item, len(action_items) > 1)) groups_by_id[first_item.identifier] = action_items new_items = [] items_by_id = {} + action_items_by_id = {} for action_item_info in all_action_items_info: action_item, is_group = action_item_info icon = get_qt_icon(action_item.icon) @@ -132,6 +154,7 @@ class ActionsQtModel(QtGui.QStandardItemModel): action_item.force_not_open_workfile, FORCE_NOT_OPEN_WORKFILE_ROLE) items_by_id[action_item.identifier] = item + action_items_by_id[action_item.identifier] = action_item if new_items: root_item.appendRows(new_items) @@ -139,10 +162,12 @@ class ActionsQtModel(QtGui.QStandardItemModel): to_remove = set(self._items_by_id.keys()) - set(items_by_id.keys()) for identifier in to_remove: item = self._items_by_id.pop(identifier) + self._action_items_by_id.pop(identifier) root_item.removeRow(item.row()) self._groups_by_id = groups_by_id self._items_by_id = items_by_id + self._action_items_by_id = action_items_by_id self.refreshed.emit() def _on_controller_refresh_finished(self): @@ -387,9 +412,15 @@ class ActionsWidget(QtWidgets.QWidget): checkbox.setChecked(True) action_id = index.data(ACTION_ID_ROLE) + is_group = index.data(ACTION_IS_GROUP_ROLE) + if is_group: + action_items = self._model.get_group_items(action_id) + else: + action_items = [self._model.get_action_item_by_id(action_id)] + action_ids = {action_item.identifier for action_item in action_items} checkbox.stateChanged.connect( lambda: self._on_checkbox_changed( - action_id, checkbox.isChecked() + action_ids, checkbox.isChecked() ) ) action = QtWidgets.QWidgetAction(menu) @@ -402,7 +433,7 @@ class ActionsWidget(QtWidgets.QWidget): menu.exec_(global_point) self._context_menu = None - def _on_checkbox_changed(self, action_id, is_checked): + def _on_checkbox_changed(self, action_ids, is_checked): if self._context_menu is not None: self._context_menu.close() @@ -410,7 +441,7 @@ class ActionsWidget(QtWidgets.QWidget): folder_id = self._model.get_selected_folder_id() task_id = self._model.get_selected_task_id() self._controller.set_application_force_not_open_workfile( - project_name, folder_id, task_id, action_id, is_checked) + project_name, folder_id, task_id, action_ids, is_checked) self._model.refresh() def _on_clicked(self, index): From 31d77932ede38fbc5c5eda29df5fcf920210a0e7 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 16:15:26 +0300 Subject: [PATCH 176/186] print message to user if nothing to change --- openpype/hosts/houdini/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index e4040852b9..1f71481cc6 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -835,6 +835,7 @@ def update_houdini_vars_context_dialog(): update_vars = get_context_var_changes() if not update_vars: # Nothing to change + print(" - Nothing to change, Houdini Vars are up to date.") return message = "\n".join( From e255c20c440211d3578fc7bcc7b350b6756dd859 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Oct 2023 15:37:45 +0200 Subject: [PATCH 177/186] Remove checks for env var (#5696) Env var will be filled in `env_var` fixture, here it is too early to check --- openpype/pype_commands.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index 7adebbbc97..071ecfffd2 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -271,12 +271,6 @@ class PypeCommands: if mongo_url: args.extend(["--mongo_url", mongo_url]) - else: - msg = ( - "Either provide uri to MongoDB through environment variable" - " OPENPYPE_MONGO or the command flag --mongo_url" - ) - assert not os.environ.get("OPENPYPE_MONGO"), msg print("run_tests args: {}".format(args)) import pytest From 52c65c9b6cd194f115f64df850e45764bdf3653a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Oct 2023 16:03:56 +0200 Subject: [PATCH 178/186] Fusion: implement toggle to use Deadline plugin FusionCmd (#5678) * OP-6971 - changed DL plugin to FusionCmd Fusion 17 doesn't work in DL 10.3, but FusionCmd does. It might be probably better option as headless variant. * OP-6971 - added dropdown to Project Settings * OP-6971 - updated settings for Ayon * OP-6971 - added default * OP-6971 - bumped up version * Update openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json Co-authored-by: Roy Nieterau --------- Co-authored-by: Roy Nieterau --- .../plugins/publish/submit_fusion_deadline.py | 4 +++- .../defaults/project_settings/deadline.json | 3 ++- .../schema_project_deadline.json | 9 ++++++++ .../server/settings/publish_plugins.py | 21 +++++++++++++++++++ server_addon/deadline/server/version.py | 2 +- 5 files changed, 36 insertions(+), 3 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index 70aa12956d..c91dd4bd69 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -34,6 +34,8 @@ class FusionSubmitDeadline( targets = ["local"] # presets + plugin = None + priority = 50 chunk_size = 1 concurrent_tasks = 1 @@ -173,7 +175,7 @@ class FusionSubmitDeadline( "SecondaryPool": instance.data.get("secondaryPool"), "Group": self.group, - "Plugin": "Fusion", + "Plugin": self.plugin, "Frames": "{start}-{end}".format( start=int(instance.data["frameStartHandle"]), end=int(instance.data["frameEndHandle"]) diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 9e88f3b6f2..2c5e0dc65d 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -52,7 +52,8 @@ "priority": 50, "chunk_size": 10, "concurrent_tasks": 1, - "group": "" + "group": "", + "plugin": "Fusion" }, "NukeSubmitDeadline": { "enabled": true, 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 596bc30f91..64db852c89 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -289,6 +289,15 @@ "type": "text", "key": "group", "label": "Group Name" + }, + { + "type": "enum", + "key": "plugin", + "label": "Deadline Plugin", + "enum_items": [ + {"Fusion": "Fusion"}, + {"FusionCmd": "FusionCmd"} + ] } ] }, diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index 32a5d0e353..8d48695a9c 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -124,6 +124,24 @@ class LimitGroupsSubmodel(BaseSettingsModel): ) +def fusion_deadline_plugin_enum(): + """Return a list of value/label dicts for the enumerator. + + Returning a list of dicts is used to allow for a custom label to be + displayed in the UI. + """ + return [ + { + "value": "Fusion", + "label": "Fusion" + }, + { + "value": "FusionCmd", + "label": "FusionCmd" + } + ] + + class FusionSubmitDeadlineModel(BaseSettingsModel): enabled: bool = Field(True, title="Enabled") optional: bool = Field(False, title="Optional") @@ -132,6 +150,9 @@ class FusionSubmitDeadlineModel(BaseSettingsModel): chunk_size: int = Field(10, title="Frame per Task") concurrent_tasks: int = Field(1, title="Number of concurrent tasks") group: str = Field("", title="Group Name") + plugin: str = Field("Fusion", + enum_resolver=fusion_deadline_plugin_enum, + title="Deadline Plugin") class NukeSubmitDeadlineModel(BaseSettingsModel): diff --git a/server_addon/deadline/server/version.py b/server_addon/deadline/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/deadline/server/version.py +++ b/server_addon/deadline/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" From c6b370be9aec3b4f6d262e34f911e6dcad0913fd Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 17:23:53 +0300 Subject: [PATCH 179/186] BigRoy's comments --- openpype/hosts/houdini/api/lib.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 1f71481cc6..3b38a6669f 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -777,7 +777,7 @@ def get_context_var_changes(): # Set Houdini Vars for item in houdini_vars: # For consistency reasons we always force all vars to be uppercase - item["var"] = item["var"].upper() + var = item["var"].upper() # get and resolve template in value item_value = StringTemplate.format_template( @@ -785,27 +785,18 @@ def get_context_var_changes(): template_data ) - if item["var"] == "JOB" and item_value == "": + if var == "JOB" and item_value == "": # sync $JOB to $HIP if $JOB is empty item_value = os.environ["HIP"] if item["is_directory"]: item_value = item_value.replace("\\", "/") - current_value = hou.hscript("echo -n `${}`".format(item["var"]))[0] - - # sync both environment variables. - # because houdini doesn't do that by default - # on opening new files - os.environ[item["var"]] = current_value + current_value = hou.hscript("echo -n `${}`".format(var))[0] if current_value != item_value: - houdini_vars_to_update.update( - { - item["var"]: ( - current_value, item_value, item["is_directory"] - ) - } + houdini_vars_to_update[var] = ( + current_value, item_value, item["is_directory"] ) return houdini_vars_to_update @@ -821,13 +812,13 @@ def update_houdini_vars_context(): except OSError as e: if e.errno != errno.EEXIST: print( - " - Failed to create ${} dir. Maybe due to " + "Failed to create ${} dir. Maybe due to " "insufficient permissions.".format(var) ) hou.hscript("set {}={}".format(var, new)) os.environ[var] = new - print(" - Updated ${} to {}".format(var, new)) + print("Updated ${} to {}".format(var, new)) def update_houdini_vars_context_dialog(): @@ -835,7 +826,7 @@ def update_houdini_vars_context_dialog(): update_vars = get_context_var_changes() if not update_vars: # Nothing to change - print(" - Nothing to change, Houdini Vars are up to date.") + print("Nothing to change, Houdini Vars are up to date.") return message = "\n".join( @@ -850,6 +841,6 @@ def update_houdini_vars_context_dialog(): dialog.setButtonText("Fix") # on_show is the Fix button clicked callback - dialog.on_clicked_state.connect(lambda: update_houdini_vars_context()) + dialog.on_clicked_state.connect(update_houdini_vars_context) dialog.show() From 8f0b1827595ef77fa2adcbe2a661c53f99d87513 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 17:26:08 +0300 Subject: [PATCH 180/186] update printed message --- 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 3b38a6669f..44752a3369 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -826,7 +826,7 @@ def update_houdini_vars_context_dialog(): update_vars = get_context_var_changes() if not update_vars: # Nothing to change - print("Nothing to change, Houdini Vars are up to date.") + print("Nothing to change, Houdini vars are already up to date.") return message = "\n".join( From 12f41289018c46ab09eb5336a3dcdea93057183d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Oct 2023 16:46:20 +0200 Subject: [PATCH 181/186] Fusion: added missing env vars to Deadline submission (#5659) * OP-6930 - added missing env vars to Fusion Deadline submission Without this injection of environment variables won't start. * OP-6930 - removed unnecessary env var * OP-6930 - removed unnecessary env var --- .../plugins/publish/submit_fusion_deadline.py | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py index c91dd4bd69..0b97582d2a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_fusion_deadline.py @@ -6,6 +6,7 @@ import requests import pyblish.api +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import legacy_io from openpype.pipeline.publish import ( OpenPypePyblishPluginMixin @@ -218,16 +219,29 @@ class FusionSubmitDeadline( # Include critical variables with submission keys = [ - # TODO: This won't work if the slaves don't have access to - # these paths, such as if slaves are running Linux and the - # submitter is on Windows. - "PYTHONPATH", - "OFX_PLUGIN_PATH", - "FUSION9_MasterPrefs" + "FTRACK_API_KEY", + "FTRACK_API_USER", + "FTRACK_SERVER", + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK", + "AVALON_APP_NAME", + "OPENPYPE_DEV", + "OPENPYPE_LOG_NO_COLORS", + "IS_TEST" ] environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **legacy_io.Session) + # to recognize render jobs + if AYON_SERVER_ENABLED: + environment["AYON_BUNDLE_NAME"] = os.environ["AYON_BUNDLE_NAME"] + render_job_label = "AYON_RENDER_JOB" + else: + render_job_label = "OPENPYPE_RENDER_JOB" + + environment[render_job_label] = "1" + payload["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, From 5b04af7ea138641bb5813ea7894044d03d8285c9 Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Thu, 5 Oct 2023 19:30:44 +0300 Subject: [PATCH 182/186] remove leading and trailing whitespaces from vars --- openpype/hosts/houdini/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 44752a3369..75986c71f5 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -777,7 +777,8 @@ def get_context_var_changes(): # Set Houdini Vars for item in houdini_vars: # For consistency reasons we always force all vars to be uppercase - var = item["var"].upper() + # Also remove any leading, and trailing whitespaces. + var = item["var"].strip().upper() # get and resolve template in value item_value = StringTemplate.format_template( From 2ea8d6530fac1818afb98e04d90484f2456614cc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 6 Oct 2023 10:44:39 +0200 Subject: [PATCH 183/186] AYON Launcher tool: Fix refresh btn (#5685) * rename 'refresh' to 'set_context' in 'TasksModel' * implemented 'refresh' for folders and tasks widgets * propagate refresh to all widgets * don't use 'clear' of 'QStandardItemModel' * change lifetime of folders cache to a minute * added 'refresh_actions' method to launcher to skip clear cache of folders * shorten line * sorting is not case sensitive --- openpype/tools/ayon_launcher/abstract.py | 10 +++++ openpype/tools/ayon_launcher/control.py | 12 ++++++ .../tools/ayon_launcher/ui/actions_widget.py | 14 ++----- .../tools/ayon_launcher/ui/hierarchy_page.py | 4 ++ .../tools/ayon_launcher/ui/projects_widget.py | 13 +++++++ openpype/tools/ayon_launcher/ui/window.py | 39 +++++++++++++------ openpype/tools/ayon_utils/models/hierarchy.py | 13 +++++-- .../ayon_utils/widgets/folders_widget.py | 30 ++++++++++---- .../tools/ayon_utils/widgets/tasks_widget.py | 31 ++++++++++----- 9 files changed, 124 insertions(+), 42 deletions(-) diff --git a/openpype/tools/ayon_launcher/abstract.py b/openpype/tools/ayon_launcher/abstract.py index f2ef681c62..95fe2b2c8d 100644 --- a/openpype/tools/ayon_launcher/abstract.py +++ b/openpype/tools/ayon_launcher/abstract.py @@ -295,3 +295,13 @@ class AbstractLauncherFrontEnd(AbstractLauncherCommon): """ pass + + @abstractmethod + def refresh_actions(self): + """Refresh actions and all related data. + + Triggers 'controller.refresh.actions.started' event at the beginning + and 'controller.refresh.actions.finished' at the end. + """ + + pass diff --git a/openpype/tools/ayon_launcher/control.py b/openpype/tools/ayon_launcher/control.py index a6e528b104..36c0536422 100644 --- a/openpype/tools/ayon_launcher/control.py +++ b/openpype/tools/ayon_launcher/control.py @@ -145,5 +145,17 @@ class BaseLauncherController( self._emit_event("controller.refresh.finished") + def refresh_actions(self): + self._emit_event("controller.refresh.actions.started") + + # Refresh project settings (used for actions discovery) + self._project_settings = {} + # Refresh projects - they define applications + self._projects_model.reset() + # Refresh actions + self._actions_model.refresh() + + self._emit_event("controller.refresh.actions.finished") + def _emit_event(self, topic, data=None): self.emit_event(topic, data, "controller") diff --git a/openpype/tools/ayon_launcher/ui/actions_widget.py b/openpype/tools/ayon_launcher/ui/actions_widget.py index 0630d1d5b5..2a1a06695d 100644 --- a/openpype/tools/ayon_launcher/ui/actions_widget.py +++ b/openpype/tools/ayon_launcher/ui/actions_widget.py @@ -46,10 +46,6 @@ class ActionsQtModel(QtGui.QStandardItemModel): def __init__(self, controller): super(ActionsQtModel, self).__init__() - controller.register_event_callback( - "controller.refresh.finished", - self._on_controller_refresh_finished, - ) controller.register_event_callback( "selection.project.changed", self._on_selection_project_changed, @@ -170,13 +166,6 @@ class ActionsQtModel(QtGui.QStandardItemModel): self._action_items_by_id = action_items_by_id self.refreshed.emit() - def _on_controller_refresh_finished(self): - context = self._controller.get_selected_context() - self._selected_project_name = context["project_name"] - self._selected_folder_id = context["folder_id"] - self._selected_task_id = context["task_id"] - self.refresh() - def _on_selection_project_changed(self, event): self._selected_project_name = event["project_name"] self._selected_folder_id = None @@ -361,6 +350,9 @@ class ActionsWidget(QtWidgets.QWidget): self._set_row_height(1) + def refresh(self): + self._model.refresh() + def _set_row_height(self, rows): self.setMinimumHeight(rows * 75) diff --git a/openpype/tools/ayon_launcher/ui/hierarchy_page.py b/openpype/tools/ayon_launcher/ui/hierarchy_page.py index 5047cdc692..8c546b38ac 100644 --- a/openpype/tools/ayon_launcher/ui/hierarchy_page.py +++ b/openpype/tools/ayon_launcher/ui/hierarchy_page.py @@ -92,6 +92,10 @@ class HierarchyPage(QtWidgets.QWidget): if visible and project_name: self._projects_combobox.set_selection(project_name) + def refresh(self): + self._folders_widget.refresh() + self._tasks_widget.refresh() + def _on_back_clicked(self): self._controller.set_selected_project(None) diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py index baa399d0ed..7dbaec5147 100644 --- a/openpype/tools/ayon_launcher/ui/projects_widget.py +++ b/openpype/tools/ayon_launcher/ui/projects_widget.py @@ -73,6 +73,9 @@ class ProjectIconView(QtWidgets.QListView): class ProjectsWidget(QtWidgets.QWidget): """Projects Page""" + + refreshed = QtCore.Signal() + def __init__(self, controller, parent=None): super(ProjectsWidget, self).__init__(parent=parent) @@ -104,6 +107,7 @@ class ProjectsWidget(QtWidgets.QWidget): main_layout.addWidget(projects_view, 1) projects_view.clicked.connect(self._on_view_clicked) + projects_model.refreshed.connect(self.refreshed) projects_filter_text.textChanged.connect( self._on_project_filter_change) refresh_btn.clicked.connect(self._on_refresh_clicked) @@ -119,6 +123,15 @@ class ProjectsWidget(QtWidgets.QWidget): self._projects_model = projects_model self._projects_proxy_model = projects_proxy_model + def has_content(self): + """Model has at least one project. + + Returns: + bool: True if there is any content in the model. + """ + + return self._projects_model.has_content() + def _on_view_clicked(self, index): if index.isValid(): project_name = index.data(QtCore.Qt.DisplayRole) diff --git a/openpype/tools/ayon_launcher/ui/window.py b/openpype/tools/ayon_launcher/ui/window.py index 139da42a2e..ffc74a2fdc 100644 --- a/openpype/tools/ayon_launcher/ui/window.py +++ b/openpype/tools/ayon_launcher/ui/window.py @@ -99,8 +99,8 @@ class LauncherWindow(QtWidgets.QWidget): message_timer.setInterval(self.message_interval) message_timer.setSingleShot(True) - refresh_timer = QtCore.QTimer() - refresh_timer.setInterval(self.refresh_interval) + actions_refresh_timer = QtCore.QTimer() + actions_refresh_timer.setInterval(self.refresh_interval) page_slide_anim = QtCore.QVariantAnimation(self) page_slide_anim.setDuration(self.page_side_anim_interval) @@ -108,8 +108,10 @@ class LauncherWindow(QtWidgets.QWidget): page_slide_anim.setEndValue(1.0) page_slide_anim.setEasingCurve(QtCore.QEasingCurve.OutQuad) + projects_page.refreshed.connect(self._on_projects_refresh) message_timer.timeout.connect(self._on_message_timeout) - refresh_timer.timeout.connect(self._on_refresh_timeout) + actions_refresh_timer.timeout.connect( + self._on_actions_refresh_timeout) page_slide_anim.valueChanged.connect( self._on_page_slide_value_changed) page_slide_anim.finished.connect(self._on_page_slide_finished) @@ -132,6 +134,7 @@ class LauncherWindow(QtWidgets.QWidget): self._is_on_projects_page = True self._window_is_active = False self._refresh_on_activate = False + self._selected_project_name = None self._pages_widget = pages_widget self._pages_layout = pages_layout @@ -143,7 +146,7 @@ class LauncherWindow(QtWidgets.QWidget): # self._action_history = action_history self._message_timer = message_timer - self._refresh_timer = refresh_timer + self._actions_refresh_timer = actions_refresh_timer self._page_slide_anim = page_slide_anim hierarchy_page.setVisible(not self._is_on_projects_page) @@ -152,14 +155,14 @@ class LauncherWindow(QtWidgets.QWidget): def showEvent(self, event): super(LauncherWindow, self).showEvent(event) self._window_is_active = True - if not self._refresh_timer.isActive(): - self._refresh_timer.start() + if not self._actions_refresh_timer.isActive(): + self._actions_refresh_timer.start() self._controller.refresh() def closeEvent(self, event): super(LauncherWindow, self).closeEvent(event) self._window_is_active = False - self._refresh_timer.stop() + self._actions_refresh_timer.stop() def changeEvent(self, event): if event.type() in ( @@ -170,15 +173,15 @@ class LauncherWindow(QtWidgets.QWidget): self._window_is_active = is_active if is_active and self._refresh_on_activate: self._refresh_on_activate = False - self._on_refresh_timeout() - self._refresh_timer.start() + self._on_actions_refresh_timeout() + self._actions_refresh_timer.start() super(LauncherWindow, self).changeEvent(event) - def _on_refresh_timeout(self): + def _on_actions_refresh_timeout(self): # Stop timer if widget is not visible if self._window_is_active: - self._controller.refresh() + self._controller.refresh_actions() else: self._refresh_on_activate = True @@ -191,12 +194,26 @@ class LauncherWindow(QtWidgets.QWidget): def _on_project_selection_change(self, event): project_name = event["project_name"] + self._selected_project_name = project_name if not project_name: self._go_to_projects_page() elif self._is_on_projects_page: self._go_to_hierarchy_page(project_name) + def _on_projects_refresh(self): + # There is nothing to do, we're on projects page + if self._is_on_projects_page: + return + + # No projects were found -> go back to projects page + if not self._projects_page.has_content(): + self._go_to_projects_page() + return + + self._hierarchy_page.refresh() + self._actions_widget.refresh() + def _on_action_trigger_started(self, event): self._echo("Running action: {}".format(event["full_label"])) diff --git a/openpype/tools/ayon_utils/models/hierarchy.py b/openpype/tools/ayon_utils/models/hierarchy.py index 8e01c557c5..93f4c48d98 100644 --- a/openpype/tools/ayon_utils/models/hierarchy.py +++ b/openpype/tools/ayon_utils/models/hierarchy.py @@ -199,13 +199,18 @@ class HierarchyModel(object): Hierarchy items are folders and tasks. Folders can have as parent another folder or project. Tasks can have as parent only folder. """ + lifetime = 60 # A minute def __init__(self, controller): - self._folders_items = NestedCacheItem(levels=1, default_factory=dict) - self._folders_by_id = NestedCacheItem(levels=2, default_factory=dict) + self._folders_items = NestedCacheItem( + levels=1, default_factory=dict, lifetime=self.lifetime) + self._folders_by_id = NestedCacheItem( + levels=2, default_factory=dict, lifetime=self.lifetime) - self._task_items = NestedCacheItem(levels=2, default_factory=dict) - self._tasks_by_id = NestedCacheItem(levels=2, default_factory=dict) + self._task_items = NestedCacheItem( + levels=2, default_factory=dict, lifetime=self.lifetime) + self._tasks_by_id = NestedCacheItem( + levels=2, default_factory=dict, lifetime=self.lifetime) self._folders_refreshing = set() self._tasks_refreshing = set() diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py index 3fab64f657..4f44881081 100644 --- a/openpype/tools/ayon_utils/widgets/folders_widget.py +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -56,11 +56,21 @@ class FoldersModel(QtGui.QStandardItemModel): return self._has_content - def clear(self): + def refresh(self): + """Refresh folders for last selected project. + + Force to update folders model from controller. This may or may not + trigger query from server, that's based on controller's cache. + """ + + self.set_project_name(self._last_project_name) + + def _clear_items(self): self._items_by_id = {} self._parent_id_by_id = {} self._has_content = False - super(FoldersModel, self).clear() + root_item = self.invisibleRootItem() + root_item.removeRows(0, root_item.rowCount()) def get_index_by_id(self, item_id): """Get index by folder id. @@ -90,7 +100,7 @@ class FoldersModel(QtGui.QStandardItemModel): self._is_refreshing = True if self._last_project_name != project_name: - self.clear() + self._clear_items() self._last_project_name = project_name thread = self._refresh_threads.get(project_name) @@ -135,7 +145,7 @@ class FoldersModel(QtGui.QStandardItemModel): def _fill_items(self, folder_items_by_id): if not folder_items_by_id: if folder_items_by_id is not None: - self.clear() + self._clear_items() self._is_refreshing = False self.refreshed.emit() return @@ -247,6 +257,7 @@ class FoldersWidget(QtWidgets.QWidget): folders_model = FoldersModel(controller) folders_proxy_model = RecursiveSortFilterProxyModel() folders_proxy_model.setSourceModel(folders_model) + folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) folders_view.setModel(folders_proxy_model) @@ -293,6 +304,14 @@ class FoldersWidget(QtWidgets.QWidget): self._folders_proxy_model.setFilterFixedString(name) + def refresh(self): + """Refresh folders model. + + Force to update folders model from controller. + """ + + self._folders_model.refresh() + def _on_project_selection_change(self, event): project_name = event["project_name"] self._set_project_name(project_name) @@ -300,9 +319,6 @@ class FoldersWidget(QtWidgets.QWidget): def _set_project_name(self, project_name): self._folders_model.set_project_name(project_name) - def _clear(self): - self._folders_model.clear() - def _on_folders_refresh_finished(self, event): if event["sender"] != SENDER_NAME: self._set_project_name(event["project_name"]) diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py index 66ebd0b777..0af506863a 100644 --- a/openpype/tools/ayon_utils/widgets/tasks_widget.py +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -44,14 +44,20 @@ class TasksModel(QtGui.QStandardItemModel): # Initial state self._add_invalid_selection_item() - def clear(self): + def _clear_items(self): self._items_by_name = {} self._has_content = False self._remove_invalid_items() - super(TasksModel, self).clear() + root_item = self.invisibleRootItem() + root_item.removeRows(0, root_item.rowCount()) - def refresh(self, project_name, folder_id): - """Refresh tasks for folder. + def refresh(self): + """Refresh tasks for last project and folder.""" + + self._refresh(self._last_project_name, self._last_folder_id) + + def set_context(self, project_name, folder_id): + """Set context for which should be tasks showed. Args: project_name (Union[str]): Name of project. @@ -121,7 +127,7 @@ class TasksModel(QtGui.QStandardItemModel): return self._empty_tasks_item def _add_invalid_item(self, item): - self.clear() + self._clear_items() root_item = self.invisibleRootItem() root_item.appendRow(item) @@ -299,6 +305,7 @@ class TasksWidget(QtWidgets.QWidget): tasks_model = TasksModel(controller) tasks_proxy_model = QtCore.QSortFilterProxyModel() tasks_proxy_model.setSourceModel(tasks_model) + tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) tasks_view.setModel(tasks_proxy_model) @@ -334,8 +341,14 @@ class TasksWidget(QtWidgets.QWidget): self._handle_expected_selection = handle_expected_selection self._expected_selection_data = None - def _clear(self): - self._tasks_model.clear() + def refresh(self): + """Refresh folders for last selected project. + + Force to update folders model from controller. This may or may not + trigger query from server, that's based on controller's cache. + """ + + self._tasks_model.refresh() def _on_tasks_refresh_finished(self, event): """Tasks were refreshed in controller. @@ -353,13 +366,13 @@ class TasksWidget(QtWidgets.QWidget): or event["folder_id"] != self._selected_folder_id ): return - self._tasks_model.refresh( + self._tasks_model.set_context( event["project_name"], self._selected_folder_id ) def _folder_selection_changed(self, event): self._selected_folder_id = event["folder_id"] - self._tasks_model.refresh( + self._tasks_model.set_context( event["project_name"], self._selected_folder_id ) From 7c5d149f56c7aba57c3325fadd43075ec732580d Mon Sep 17 00:00:00 2001 From: Mustafa-Zarkash Date: Fri, 6 Oct 2023 12:11:51 +0300 Subject: [PATCH 184/186] use different popup --- openpype/hosts/houdini/api/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 75986c71f5..3db18ca69a 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -831,17 +831,19 @@ def update_houdini_vars_context_dialog(): return message = "\n".join( - "${}: {} -> {}".format(var, old or "None", new) + "${}: {} -> {}".format(var, old or "None", new or "None") for var, (old, new, _is_directory) in update_vars.items() ) + + # TODO: Use better UI! parent = hou.ui.mainQtWindow() - dialog = popup.PopupUpdateKeys(parent=parent) + dialog = popup.Popup(parent=parent) dialog.setModal(True) dialog.setWindowTitle("Houdini scene has outdated asset variables") dialog.setMessage(message) dialog.setButtonText("Fix") # on_show is the Fix button clicked callback - dialog.on_clicked_state.connect(update_houdini_vars_context) + dialog.on_clicked.connect(update_houdini_vars_context) dialog.show() From 32052551e2c3848da47a3991c3eda985129f9059 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 7 Oct 2023 03:24:54 +0000 Subject: [PATCH 185/186] [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 399c1404b1..01c000e54d 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.2-nightly.2" +__version__ = "3.17.2-nightly.3" From 25c023290f6223e907d0b8a1879931ac528de23d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 7 Oct 2023 03:25:38 +0000 Subject: [PATCH 186/186] 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 e3ca8262e5..78bea3d838 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.17.2-nightly.3 - 3.17.2-nightly.2 - 3.17.2-nightly.1 - 3.17.1 @@ -134,7 +135,6 @@ body: - 3.14.10 - 3.14.10-nightly.9 - 3.14.10-nightly.8 - - 3.14.10-nightly.7 validations: required: true - type: dropdown