From 8e281f4efef4917fbe965f7e49979577e4e6e226 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 10 Jul 2023 13:17:30 +0200 Subject: [PATCH 001/267] simplification of subprocess calls --- openpype/pipeline/colorspace.py | 65 +++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 3f2d4891c1..2ca78f3520 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -206,8 +206,9 @@ def validate_imageio_colorspace_in_config(config_path, colorspace_name): return True +# TODO: remove this in future - backward compatibility def get_data_subprocess(config_path, data_type): - """Get data via subprocess + """[Deprecated] Get data via subprocess Wrapper for Python 2 hosts. @@ -221,7 +222,6 @@ def get_data_subprocess(config_path, data_type): "config", data_type, "--in_path", config_path, "--out_path", tmp_json_path - ] log.info("Executing: {}".format(" ".join(args))) @@ -236,6 +236,45 @@ def get_data_subprocess(config_path, data_type): return json.loads(return_json_data) +def get_wrapped_with_subprocess(command_group, command, **kwargs): + """Get data via subprocess + + Wrapper for Python 2 hosts. + + Args: + command_group (str): command group name + command (str): command name + **kwargs: command arguments + + Returns: + Any[dict, None]: data + """ + with _make_temp_json_file() as tmp_json_path: + # Prepare subprocess arguments + args = [ + "run", get_ocio_config_script_path(), + command_group, command + ] + + for key_, value_ in kwargs.items(): + args.extend(("--{}".format(key_), value_)) + + args.append("--out_path") + args.append(tmp_json_path) + + log.info("Executing: {}".format(" ".join(args))) + + process_kwargs = { + "logger": log + } + + run_openpype_process(*args, **process_kwargs) + + # return all colorspaces + return_json_data = open(tmp_json_path).read() + return json.loads(return_json_data) + + def compatibility_check(): """Making sure PyOpenColorIO is importable""" try: @@ -260,15 +299,18 @@ def get_ocio_config_colorspaces(config_path): if not compatibility_check(): # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess - return get_colorspace_data_subprocess(config_path) + return get_wrapped_with_subprocess( + "config", "get_colorspace", in_path=config_path + ) from openpype.scripts.ocio_wrapper import _get_colorspace_data return _get_colorspace_data(config_path) +# TODO: remove this in future - backward compatibility def get_colorspace_data_subprocess(config_path): - """Get colorspace data via subprocess + """[Deprecated] Get colorspace data via subprocess Wrapper for Python 2 hosts. @@ -278,7 +320,9 @@ def get_colorspace_data_subprocess(config_path): Returns: dict: colorspace and family in couple """ - return get_data_subprocess(config_path, "get_colorspace") + return get_wrapped_with_subprocess( + "config", "get_colorspace", in_path=config_path + ) def get_ocio_config_views(config_path): @@ -296,15 +340,18 @@ def get_ocio_config_views(config_path): if not compatibility_check(): # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess - return get_views_data_subprocess(config_path) + return get_wrapped_with_subprocess( + "config", "get_views", in_path=config_path + ) from openpype.scripts.ocio_wrapper import _get_views_data return _get_views_data(config_path) +# TODO: remove this in future - backward compatibility def get_views_data_subprocess(config_path): - """Get viewers data via subprocess + """[Deprecated] Get viewers data via subprocess Wrapper for Python 2 hosts. @@ -314,7 +361,9 @@ def get_views_data_subprocess(config_path): Returns: dict: `display/viewer` and viewer data """ - return get_data_subprocess(config_path, "get_views") + return get_wrapped_with_subprocess( + "config", "get_views", in_path=config_path + ) def get_imageio_config( From 739c2e15bc58c447cf25ef9a9f9204a0aaf05344 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 10 Jul 2023 13:18:34 +0200 Subject: [PATCH 002/267] adding support for OCIO v2 file rules --- openpype/pipeline/colorspace.py | 31 ++++++++++ openpype/scripts/ocio_wrapper.py | 99 ++++++++++++++++++++++++++++---- 2 files changed, 119 insertions(+), 11 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 2ca78f3520..26e12871f8 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -132,6 +132,37 @@ def get_imageio_colorspace_from_filepath( return colorspace_name +def get_colorspace_from_filepath(config_path, filepath): + """Get colorspace from file path wrapper. + + Wrapper function for getting colorspace from file path. + + Args: + config_path (str): path leading to config.ocio file + filepath (str): path leading to a file + + Returns: + Any[str, None]: matching colorspace name + """ + if not compatibility_check(): + # python environment is not compatible with PyOpenColorIO + # needs to be run in subprocess + result_data = get_wrapped_with_subprocess( + "colorspace", "get_colorspace_from_filepath", + config_path=config_path, + filepath=filepath + ) + if result_data: + return result_data[0] + + from openpype.scripts.ocio_wrapper import _get_colorspace_from_filepath + + result_data = _get_colorspace_from_filepath(config_path, filepath) + + if result_data: + return result_data[0] + + def parse_colorspace_from_filepath( path, host_name, project_name, config_data=None, diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 16558642c6..1c86216347 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -27,7 +27,7 @@ import PyOpenColorIO as ocio @click.group() def main(): - pass + pass # noqa: WPS100 @main.group() @@ -37,7 +37,17 @@ def config(): Example of use: > pyton.exe ./ocio_wrapper.py config *args """ - pass + pass # noqa: WPS100 + + +@main.group() +def colorspace(): + """Colorspace related commands group + + Example of use: + > pyton.exe ./ocio_wrapper.py config *args + """ + pass # noqa: WPS100 @config.command( @@ -70,8 +80,8 @@ def get_colorspace(in_path, out_path): out_data = _get_colorspace_data(in_path) - with open(json_path, "w") as f: - json.dump(out_data, f) + with open(json_path, "w") as f_: + json.dump(out_data, f_) print(f"Colorspace data are saved to '{json_path}'") @@ -97,8 +107,8 @@ def _get_colorspace_data(config_path): config = ocio.Config().CreateFromFile(str(config_path)) return { - c.getName(): c.getFamily() - for c in config.getColorSpaces() + c_.getName(): c_.getFamily() + for c_ in config.getColorSpaces() } @@ -132,8 +142,8 @@ def get_views(in_path, out_path): out_data = _get_views_data(in_path) - with open(json_path, "w") as f: - json.dump(out_data, f) + with open(json_path, "w") as f_: + json.dump(out_data, f_) print(f"Viewer data are saved to '{json_path}'") @@ -157,7 +167,7 @@ def _get_views_data(config_path): config = ocio.Config().CreateFromFile(str(config_path)) - data = {} + data_ = {} for display in config.getDisplays(): for view in config.getViews(display): colorspace = config.getDisplayViewColorSpaceName(display, view) @@ -165,13 +175,80 @@ def _get_views_data(config_path): if colorspace == "": colorspace = display - data[f"{display}/{view}"] = { + data_[f"{display}/{view}"] = { "display": display, "view": view, "colorspace": colorspace } - return data + return data_ + + +@colorspace.command( + name="get_colorspace_from_filepath", + help=( + "return colorspace from filepath " + "--config_path - ocio config file path (input arg is required) " + "--filepath - any file path (input arg is required) " + "--out_path - temp json file path (input arg is required)" + ) +) +@click.option("--config_path", required=True, + help="path where to read ocio config file", + type=click.Path(exists=True)) +@click.option("--filepath", required=True, + help="path to file to get colorspace from", + type=click.Path(exists=True)) +@click.option("--out_path", required=True, + help="path where to write output json file", + type=click.Path()) +def get_colorspace_from_filepath(config_path, filepath, out_path): + """Get colorspace from file path wrapper. + + Python 2 wrapped console command + + Args: + config_path (str): config file path string + filepath (str): path string leading to file + out_path (str): temp json file path string + + Example of use: + > pyton.exe ./ocio_wrapper.py colorspace get_colorspace_from_filepath \ + --config_path= --filepath= --out_path= + """ + json_path = Path(out_path) + + colorspace = _get_colorspace_from_filepath(config_path, filepath) + + with open(json_path, "w") as f_: + json.dump(colorspace, f_) + + print(f"Colorspace name is saved to '{json_path}'") + + +def _get_colorspace_from_filepath(config_path, filepath): + """Return found colorspace data found in v2 file rules. + + Args: + config_path (str): path string leading to config.ocio + filepath (str): path string leading to v2 file rules + + Raises: + IOError: Input config does not exist. + + Returns: + dict: aggregated available colorspaces + """ + config_path = Path(config_path) + + if not config_path.is_file(): + raise IOError( + f"Input path `{config_path}` should be `config.ocio` file") + + config = ocio.Config().CreateFromFile(str(config_path)) + colorspace = config.getColorSpaceFromFilepath(str(filepath)) + + return colorspace if __name__ == '__main__': From 0114993456108652b8995ac87531462e79e065b6 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 10 Jul 2023 13:57:20 +0200 Subject: [PATCH 003/267] compatibility to config version --- openpype/pipeline/colorspace.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 26e12871f8..0b23c2b4e3 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -124,6 +124,10 @@ def get_imageio_colorspace_from_filepath( )) return None + if compatibility_check_config_version(config_data["path"], major=2): + colorspace_name = get_colorspace_from_filepath( + config_data["path"], path) + # validate matching colorspace with config if validate and config_data: validate_imageio_colorspace_in_config( @@ -312,6 +316,32 @@ def compatibility_check(): import PyOpenColorIO # noqa: F401 except ImportError: return False + + # compatible + return True + + +def compatibility_check_config_version(config_path, major=1, minor=None): + """Making sure PyOpenColorIO config version is compatible""" + try: + import PyOpenColorIO as ocio + config = ocio.Config().CreateFromFile(str(config_path)) + + config_version_major = config.getMajorVersion() + config_version_minor = config.getMinorVersion() + print(config_version_major, config_version_minor) + + # check major version + if config_version_major != major: + return False + # check minor version + if minor and config_version_minor != minor: + return False + + except ImportError: + return False + + # compatible return True From 9c18402ac70952a39247d681322777951b1fe97c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 10 Jul 2023 16:42:52 +0200 Subject: [PATCH 004/267] updating config version validation - cashing python version validation so it is overwritable by tests - implementing python compatibility override into tests - adding tests for ocio v2 filerules --- openpype/pipeline/colorspace.py | 52 +++++++++------ openpype/scripts/ocio_wrapper.py | 64 +++++++++++++++++- .../unit/openpype/pipeline/test_colorspace.py | 65 +++++++++++++++++++ 3 files changed, 161 insertions(+), 20 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 0b23c2b4e3..72c140195e 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -19,6 +19,7 @@ log = Logger.get_logger(__name__) class CashedData: remapping = None + python3compatible = None @contextlib.contextmanager @@ -118,16 +119,23 @@ def get_imageio_colorspace_from_filepath( if ext_match and file_match: colorspace_name = file_rule["colorspace"] + # if no file rule matched, try to get colorspace + # from filepath with OCIO v2 way + # QUESTION: should we override file rules from our settings and + # in ocio v2 only focus on file rules set in config file? + if ( + compatibility_check_config_version(config_data["path"], major=2) + and not colorspace_name + ): + colorspace_name = get_colorspace_from_filepath( + config_data["path"], path) + if not colorspace_name: log.info("No imageio file rule matched input path: '{}'".format( path )) return None - if compatibility_check_config_version(config_data["path"], major=2): - colorspace_name = get_colorspace_from_filepath( - config_data["path"], path) - # validate matching colorspace with config if validate and config_data: validate_imageio_colorspace_in_config( @@ -312,33 +320,39 @@ def get_wrapped_with_subprocess(command_group, command, **kwargs): def compatibility_check(): """Making sure PyOpenColorIO is importable""" + if CashedData.python3compatible is not None: + return CashedData.python3compatible + try: import PyOpenColorIO # noqa: F401 + CashedData.python3compatible = True except ImportError: - return False + CashedData.python3compatible = False # compatible - return True + return CashedData.python3compatible def compatibility_check_config_version(config_path, major=1, minor=None): """Making sure PyOpenColorIO config version is compatible""" - try: - import PyOpenColorIO as ocio - config = ocio.Config().CreateFromFile(str(config_path)) - config_version_major = config.getMajorVersion() - config_version_minor = config.getMinorVersion() - print(config_version_major, config_version_minor) + if not compatibility_check(): + # python environment is not compatible with PyOpenColorIO + # needs to be run in subprocess + version_data = get_wrapped_with_subprocess( + "config", "get_version", config_path=config_path + ) - # check major version - if config_version_major != major: - return False - # check minor version - if minor and config_version_minor != minor: - return False + from openpype.scripts.ocio_wrapper import _get_version_data - except ImportError: + version_data = _get_version_data(config_path) + + # check major version + if version_data["major"] != major: + return False + + # check minor version + if minor and version_data["minor"] != minor: return False # compatible diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 1c86216347..4332ea5b01 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -184,6 +184,68 @@ def _get_views_data(config_path): return data_ +@config.command( + name="get_version", + help=( + "return major and minor version from config file " + "--config_path input arg is required" + "--out_path input arg is required" + ) +) +@click.option("--config_path", required=True, + help="path where to read ocio config file", + type=click.Path(exists=True)) +@click.option("--out_path", required=True, + help="path where to write output json file", + type=click.Path()) +def get_version(config_path, out_path): + """Get version of config. + + Python 2 wrapped console command + + Args: + config_path (str): ocio config file path string + out_path (str): temp json file path string + + Example of use: + > pyton.exe ./ocio_wrapper.py config get_version \ + --config_path= --out_path= + """ + json_path = Path(out_path) + + out_data = _get_version_data(config_path) + + with open(json_path, "w") as f_: + json.dump(out_data, f_) + + print(f"Config version data are saved to '{json_path}'") + + +def _get_version_data(config_path): + """Return major and minor version info. + + Args: + config_path (str): path string leading to config.ocio + + Raises: + IOError: Input config does not exist. + + Returns: + dict: minor and major keys with values + """ + config_path = Path(config_path) + + if not config_path.is_file(): + raise IOError("Input path should be `config.ocio` file") + + config = ocio.Config().CreateFromFile(str(config_path)) + + return { + "major": config.getMajorVersion(), + "minor": config.getMinorVersion() + } + + @colorspace.command( name="get_colorspace_from_filepath", help=( @@ -198,7 +260,7 @@ def _get_views_data(config_path): type=click.Path(exists=True)) @click.option("--filepath", required=True, help="path to file to get colorspace from", - type=click.Path(exists=True)) + type=click.Path()) @click.option("--out_path", required=True, help="path where to write output json file", type=click.Path()) diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index c22acee2d4..a6fcc68055 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -185,5 +185,70 @@ class TestPipelineColorspace(TestPipeline): assert expected_hiero == hiero_file_rules, ( f"Not matching file rules {expected_hiero}") + def test_get_imageio_colorspace_from_filepath_p3(self, project_settings): + """Test Colorspace from filepath with python 3 compatibility mode + + Also test ocio v2 file rules + """ + nuke_filepath = "renderCompMain_baking_h264.mp4" + hiero_filepath = "prerenderCompMain.mp4" + + expected_nuke = "Camera Rec.709" + expected_hiero = "Gamma 2.2 Rec.709 - Texture" + + nuke_colorspace = colorspace.get_imageio_colorspace_from_filepath( + nuke_filepath, + "nuke", + "test_project", + project_settings=project_settings + ) + assert expected_nuke == nuke_colorspace, ( + f"Not matching colorspace {expected_nuke}") + + hiero_colorspace = colorspace.get_imageio_colorspace_from_filepath( + hiero_filepath, + "hiero", + "test_project", + project_settings=project_settings + ) + assert expected_hiero == hiero_colorspace, ( + f"Not matching colorspace {expected_hiero}") + + def test_get_imageio_colorspace_from_filepath_python2mode( + self, project_settings): + """Test Colorspace from filepath with python 2 compatibility mode + + Also test ocio v2 file rules + """ + nuke_filepath = "renderCompMain_baking_h264.mp4" + hiero_filepath = "prerenderCompMain.mp4" + + expected_nuke = "Camera Rec.709" + expected_hiero = "Gamma 2.2 Rec.709 - Texture" + + # switch to python 2 compatibility mode + colorspace.CashedData.python3compatible = False + + nuke_colorspace = colorspace.get_imageio_colorspace_from_filepath( + nuke_filepath, + "nuke", + "test_project", + project_settings=project_settings + ) + assert expected_nuke == nuke_colorspace, ( + f"Not matching colorspace {expected_nuke}") + + hiero_colorspace = colorspace.get_imageio_colorspace_from_filepath( + hiero_filepath, + "hiero", + "test_project", + project_settings=project_settings + ) + assert expected_hiero == hiero_colorspace, ( + f"Not matching colorspace {expected_hiero}") + + # return to python 3 compatibility mode + colorspace.CashedData.python3compatible = None + test_case = TestPipelineColorspace() From edc260073b62afb56c2304e747bb739274665630 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 10 Jul 2023 16:49:22 +0200 Subject: [PATCH 005/267] updating testing package gdrive hash --- tests/unit/openpype/pipeline/test_colorspace.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index a6fcc68055..e63ca510f2 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -26,12 +26,12 @@ class TestPipelineColorspace(TestPipeline): Example: cd to OpenPype repo root dir - poetry run python ./start.py runtests ../tests/unit/openpype/pipeline - """ + poetry run python ./start.py runtests /tests/unit/openpype/pipeline/test_colorspace.py + """ # noqa: E501 TEST_FILES = [ ( - "1Lf-mFxev7xiwZCWfImlRcw7Fj8XgNQMh", + "1csqimz8bbNcNgxtEXklLz6GRv91D3KgA", "test_pipeline_colorspace.zip", "" ) From 97477d2049f8b5493ef0c7f03aaceb6e3ea9a034 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 11 Jul 2023 16:11:54 +0200 Subject: [PATCH 006/267] todos for parseColorSpaceFromString --- openpype/pipeline/colorspace.py | 2 ++ openpype/scripts/ocio_wrapper.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 72c140195e..13b235d5dd 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -123,6 +123,8 @@ def get_imageio_colorspace_from_filepath( # from filepath with OCIO v2 way # QUESTION: should we override file rules from our settings and # in ocio v2 only focus on file rules set in config file? + # TODO: do the ocio v compatibility check inside of wrapper script + # because of implementation `parseColorSpaceFromString` if ( compatibility_check_config_version(config_data["path"], major=2) and not colorspace_name diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 4332ea5b01..1feedde627 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -308,6 +308,8 @@ def _get_colorspace_from_filepath(config_path, filepath): f"Input path `{config_path}` should be `config.ocio` file") config = ocio.Config().CreateFromFile(str(config_path)) + + # TODO: use `parseColorSpaceFromString` instead if ocio v1 colorspace = config.getColorSpaceFromFilepath(str(filepath)) return colorspace From c5b05a95c6799284bf688ef8a0b45e67841dc6b4 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 10 Aug 2023 11:42:08 +0100 Subject: [PATCH 007/267] 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 008/267] 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 009/267] 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 010/267] 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 011/267] 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 012/267] 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 013/267] 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 014/267] 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 015/267] 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 016/267] 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 017/267] 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 018/267] 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 019/267] 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 020/267] 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 021/267] 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 022/267] 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 023/267] 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 024/267] 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 025/267] 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 026/267] 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 027/267] 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 028/267] 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 029/267] 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 030/267] 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 031/267] 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 032/267] 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 44d8327aa9c92246c3aec2e588034bad9d83c27c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 12:43:28 +0200 Subject: [PATCH 033/267] adding deprecated decorators --- openpype/pipeline/colorspace.py | 53 ++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 02a6b90f25..c90fb299f9 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -2,9 +2,12 @@ from copy import deepcopy import re import os import json -import platform import contextlib +import functools +import platform import tempfile +import warnings + from openpype import PACKAGE_DIR from openpype.settings import get_project_settings from openpype.lib import ( @@ -22,6 +25,51 @@ class CashedData: python3compatible = None +class DeprecatedWarning(DeprecationWarning): + pass + + +def deprecated(new_destination): + """Mark functions as deprecated. + + It will result in a warning being emitted when the function is used. + """ + + func = None + if callable(new_destination): + func = new_destination + new_destination = None + + def _decorator(decorated_func): + if new_destination is None: + warning_message = ( + " Please check content of deprecated function to figure out" + " possible replacement." + ) + else: + warning_message = " Please replace your usage with '{}'.".format( + new_destination + ) + + @functools.wraps(decorated_func) + def wrapper(*args, **kwargs): + warnings.simplefilter("always", DeprecatedWarning) + warnings.warn( + ( + "Call to deprecated function '{}'" + "\nFunction was moved or removed.{}" + ).format(decorated_func.__name__, warning_message), + category=DeprecatedWarning, + stacklevel=4 + ) + return decorated_func(*args, **kwargs) + return wrapper + + if func is None: + return _decorator + return _decorator(func) + + @contextlib.contextmanager def _make_temp_json_file(): """Wrapping function for json temp file @@ -252,6 +300,7 @@ def validate_imageio_colorspace_in_config(config_path, colorspace_name): # TODO: remove this in future - backward compatibility +@deprecated("get_wrapped_with_subprocess") def get_data_subprocess(config_path, data_type): """[Deprecated] Get data via subprocess @@ -386,6 +435,7 @@ def get_ocio_config_colorspaces(config_path): # TODO: remove this in future - backward compatibility +@deprecated("get_wrapped_with_subprocess") def get_colorspace_data_subprocess(config_path): """[Deprecated] Get colorspace data via subprocess @@ -427,6 +477,7 @@ def get_ocio_config_views(config_path): # TODO: remove this in future - backward compatibility +@deprecated("get_wrapped_with_subprocess") def get_views_data_subprocess(config_path): """[Deprecated] Get viewers data via subprocess From c2212a6cc111f9499b41701dcbad0e1b5809ae07 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 13:21:24 +0200 Subject: [PATCH 034/267] revert unreal submodule --- openpype/hosts/unreal/integration | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/integration b/openpype/hosts/unreal/integration index ff15c70077..63266607ce 160000 --- a/openpype/hosts/unreal/integration +++ b/openpype/hosts/unreal/integration @@ -1 +1 @@ -Subproject commit ff15c700771e719cc5f3d561ac5d6f7590623986 +Subproject commit 63266607ceb972a61484f046634ddfc9eb0b5757 From e10ca74b1b93ddebb9786b04e22dde1d6de137c4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 14:00:52 +0200 Subject: [PATCH 035/267] changing signature of `parse_colorspace_from_filepath` --- openpype/pipeline/colorspace.py | 47 ++++++++++--------- .../unit/openpype/pipeline/test_colorspace.py | 4 +- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index c90fb299f9..9ea9d1f888 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -171,8 +171,6 @@ def get_imageio_colorspace_from_filepath( # from filepath with OCIO v2 way # QUESTION: should we override file rules from our settings and # in ocio v2 only focus on file rules set in config file? - # TODO: do the ocio v compatibility check inside of wrapper script - # because of implementation `parseColorSpaceFromString` if ( compatibility_check_config_version(config_data["path"], major=2) and not colorspace_name @@ -226,51 +224,56 @@ def get_colorspace_from_filepath(config_path, filepath): def parse_colorspace_from_filepath( - path, host_name, project_name, - config_data=None, - project_settings=None + filepath, colorspaces=None, config_path=None ): - """Parse colorspace name from filepath + """Parse colorspace name from list of filepaths An input path can have colorspace name used as part of name or as folder name. + # add example python code block + + Example: + >>> config_path = "path/to/config.ocio" + >>> colorspaces = get_ocio_config_colorspaces(config_path) + >>> colorspace = parse_colorspace_from_filepath( + "path/to/file/acescg/file.exr", + colorspaces=colorspaces + ) + >>> print(colorspace) + acescg + Args: - path (str): path string - host_name (str): host name - project_name (str): project name - config_data (dict, optional): config path and template in dict. - Defaults to None. - project_settings (dict, optional): project settings. Defaults to None. + filepath (str): path string + colorspaces (Optional[dict[str]]): list of colorspaces + config_path (Optional[str]): path to config.ocio file Returns: str: name of colorspace """ - if not config_data: - project_settings = project_settings or get_project_settings( - project_name + if not colorspaces and not config_path: + raise ValueError( + "You need to provide `config_path` if you don't " + "want to provide input `colorspaces`." ) - config_data = get_imageio_config( - project_name, host_name, project_settings) - config_path = config_data["path"] + colorspaces = colorspaces or get_ocio_config_colorspaces(config_path) # match file rule from path colorspace_name = None - colorspaces = get_ocio_config_colorspaces(config_path) for colorspace_key in colorspaces: # check underscored variant of colorspace name # since we are reformatting it in integrate.py - if colorspace_key.replace(" ", "_") in path: + if colorspace_key.replace(" ", "_") in filepath: colorspace_name = colorspace_key break - if colorspace_key in path: + if colorspace_key in filepath: colorspace_name = colorspace_key break if not colorspace_name: log.info("No matching colorspace in config '{}' for path: '{}'".format( - config_path, path + config_path, filepath )) return None diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index e63ca510f2..8ae98f7cf8 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -132,14 +132,14 @@ class TestPipelineColorspace(TestPipeline): path_1 = "renderCompMain_ACES2065-1.####.exr" expected_1 = "ACES2065-1" ret_1 = colorspace.parse_colorspace_from_filepath( - path_1, "nuke", "test_project", project_settings=project_settings + path_1, config_path=config_path_asset ) assert ret_1 == expected_1, f"Not matching colorspace {expected_1}" path_2 = "renderCompMain_BMDFilm_WideGamut_Gen5.mov" expected_2 = "BMDFilm WideGamut Gen5" ret_2 = colorspace.parse_colorspace_from_filepath( - path_2, "nuke", "test_project", project_settings=project_settings + path_2, config_path=config_path_asset ) assert ret_2 == expected_2, f"Not matching colorspace {expected_2}" From c99aa746a2d63b092593fd24f0cda9caa397cd50 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 14:01:36 +0200 Subject: [PATCH 036/267] docstring update --- openpype/pipeline/colorspace.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 9ea9d1f888..e675bdb2e1 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -226,13 +226,11 @@ def get_colorspace_from_filepath(config_path, filepath): def parse_colorspace_from_filepath( filepath, colorspaces=None, config_path=None ): - """Parse colorspace name from list of filepaths + """Parse colorspace name from filepath An input path can have colorspace name used as part of name or as folder name. - # add example python code block - Example: >>> config_path = "path/to/config.ocio" >>> colorspaces = get_ocio_config_colorspaces(config_path) From ba237487a65176f2ee58e08533c65115b5a9c19d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 15:53:36 +0200 Subject: [PATCH 037/267] cashing OCIO config version data --- openpype/pipeline/colorspace.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index e675bdb2e1..30bd685b13 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -23,6 +23,7 @@ log = Logger.get_logger(__name__) class CashedData: remapping = None python3compatible = None + config_version_data = None class DeprecatedWarning(DeprecationWarning): @@ -395,16 +396,17 @@ def compatibility_check_config_version(config_path, major=1, minor=None): "config", "get_version", config_path=config_path ) - from openpype.scripts.ocio_wrapper import _get_version_data + if not CashedData.config_version_data: + from openpype.scripts.ocio_wrapper import _get_version_data - version_data = _get_version_data(config_path) + CashedData.config_version_data = _get_version_data(config_path) # check major version - if version_data["major"] != major: + if CashedData.config_version_data["major"] != major: return False # check minor version - if minor and version_data["minor"] != minor: + if minor and CashedData.config_version_data["minor"] != minor: return False # compatible From 06930318c2762b0f80d60ef07dd3d38a79e7b318 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 16:01:25 +0200 Subject: [PATCH 038/267] fixing logic of the cashing --- openpype/pipeline/colorspace.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 30bd685b13..c1dc47245a 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -389,17 +389,19 @@ def compatibility_check(): def compatibility_check_config_version(config_path, major=1, minor=None): """Making sure PyOpenColorIO config version is compatible""" - if not compatibility_check(): - # python environment is not compatible with PyOpenColorIO - # needs to be run in subprocess - version_data = get_wrapped_with_subprocess( - "config", "get_version", config_path=config_path - ) - if not CashedData.config_version_data: - from openpype.scripts.ocio_wrapper import _get_version_data + if compatibility_check(): + from openpype.scripts.ocio_wrapper import _get_version_data + + CashedData.config_version_data = _get_version_data(config_path) + + else: + # python environment is not compatible with PyOpenColorIO + # needs to be run in subprocess + CashedData.config_version_data = get_wrapped_with_subprocess( + "config", "get_version", config_path=config_path + ) - CashedData.config_version_data = _get_version_data(config_path) # check major version if CashedData.config_version_data["major"] != major: From 6a36d713139ccd4e21b44c0c247a6adda236c863 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 16:26:36 +0200 Subject: [PATCH 039/267] adding colorspace parsing form filepath as fallback --- openpype/pipeline/colorspace.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index c1dc47245a..eb3c4c3b94 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -170,8 +170,6 @@ def get_imageio_colorspace_from_filepath( # if no file rule matched, try to get colorspace # from filepath with OCIO v2 way - # QUESTION: should we override file rules from our settings and - # in ocio v2 only focus on file rules set in config file? if ( compatibility_check_config_version(config_data["path"], major=2) and not colorspace_name @@ -179,6 +177,11 @@ def get_imageio_colorspace_from_filepath( colorspace_name = get_colorspace_from_filepath( config_data["path"], path) + # use parse colorspace from filepath as fallback + colorspace_name = colorspace_name or parse_colorspace_from_filepath( + path, config_path=config_data["path"] + ) + if not colorspace_name: log.info("No imageio file rule matched input path: '{}'".format( path @@ -196,7 +199,8 @@ def get_imageio_colorspace_from_filepath( def get_colorspace_from_filepath(config_path, filepath): """Get colorspace from file path wrapper. - Wrapper function for getting colorspace from file path. + Wrapper function for getting colorspace from file path + with use of OCIO v2 file-rules. Args: config_path (str): path leading to config.ocio file From 7bb23b1bc0d7af7e6260d135a1ef9c77c0f35810 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 16:35:03 +0200 Subject: [PATCH 040/267] utilising Cache for config colorspaces --- openpype/pipeline/colorspace.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index eb3c4c3b94..70b82d6be2 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -24,6 +24,7 @@ class CashedData: remapping = None python3compatible = None config_version_data = None + ocio_config_colorspaces = {} class DeprecatedWarning(DeprecationWarning): @@ -155,7 +156,7 @@ def get_imageio_colorspace_from_filepath( # match file rule from path colorspace_name = None - for _frule_name, file_rule in file_rules.items(): + for _, file_rule in file_rules.items(): pattern = file_rule["pattern"] extension = file_rule["ext"] ext_match = re.match( @@ -431,16 +432,22 @@ def get_ocio_config_colorspaces(config_path): Returns: dict: colorspace and family in couple """ - if not compatibility_check(): - # python environment is not compatible with PyOpenColorIO - # needs to be run in subprocess - return get_wrapped_with_subprocess( - "config", "get_colorspace", in_path=config_path - ) + if not CashedData.ocio_config_colorspaces.get(config_path): + if not compatibility_check(): + # python environment is not compatible with PyOpenColorIO + # needs to be run in subprocess + CashedData.ocio_config_colorspaces[config_path] = \ + get_wrapped_with_subprocess( + "config", "get_colorspace", in_path=config_path + ) + else: + from openpype.scripts.ocio_wrapper import _get_colorspace_data - from openpype.scripts.ocio_wrapper import _get_colorspace_data + CashedData.ocio_config_colorspaces[config_path] = \ + _get_colorspace_data(config_path) + + return CashedData.ocio_config_colorspaces[config_path] - return _get_colorspace_data(config_path) # TODO: remove this in future - backward compatibility From ab872146a9bc52068dd4abb6c009160e1a79120c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 16:36:01 +0200 Subject: [PATCH 041/267] removing line --- openpype/pipeline/colorspace.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 70b82d6be2..8378da5b98 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -449,7 +449,6 @@ def get_ocio_config_colorspaces(config_path): return CashedData.ocio_config_colorspaces[config_path] - # TODO: remove this in future - backward compatibility @deprecated("get_wrapped_with_subprocess") def get_colorspace_data_subprocess(config_path): From b60043e94589b0a1dfdb7853deb6560b5255827b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 28 Aug 2023 16:38:34 +0200 Subject: [PATCH 042/267] hound --- openpype/pipeline/colorspace.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 8378da5b98..1251308eb3 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -407,7 +407,6 @@ def compatibility_check_config_version(config_path, major=1, minor=None): "config", "get_version", config_path=config_path ) - # check major version if CashedData.config_version_data["major"] != major: return False From 20df677e48d52b00834348e813017f99d9c2a07d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 29 Aug 2023 22:39:49 +0800 Subject: [PATCH 043/267] 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 044/267] 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 045/267] 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 046/267] 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 a4e876792a28709a398433a7e047e40c865c4244 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 15:42:05 +0200 Subject: [PATCH 047/267] typo fix --- openpype/pipeline/colorspace.py | 46 ++++++++++++++++----------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 1251308eb3..8025022c59 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -20,7 +20,7 @@ from openpype.pipeline import Anatomy log = Logger.get_logger(__name__) -class CashedData: +class CachedData: remapping = None python3compatible = None config_version_data = None @@ -378,41 +378,41 @@ def get_wrapped_with_subprocess(command_group, command, **kwargs): def compatibility_check(): """Making sure PyOpenColorIO is importable""" - if CashedData.python3compatible is not None: - return CashedData.python3compatible + if CachedData.python3compatible is not None: + return CachedData.python3compatible try: import PyOpenColorIO # noqa: F401 - CashedData.python3compatible = True + CachedData.python3compatible = True except ImportError: - CashedData.python3compatible = False + CachedData.python3compatible = False # compatible - return CashedData.python3compatible + return CachedData.python3compatible def compatibility_check_config_version(config_path, major=1, minor=None): """Making sure PyOpenColorIO config version is compatible""" - if not CashedData.config_version_data: + if not CachedData.config_version_data: if compatibility_check(): from openpype.scripts.ocio_wrapper import _get_version_data - CashedData.config_version_data = _get_version_data(config_path) + CachedData.config_version_data = _get_version_data(config_path) else: # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess - CashedData.config_version_data = get_wrapped_with_subprocess( + CachedData.config_version_data = get_wrapped_with_subprocess( "config", "get_version", config_path=config_path ) # check major version - if CashedData.config_version_data["major"] != major: + if CachedData.config_version_data["major"] != major: return False # check minor version - if minor and CashedData.config_version_data["minor"] != minor: + if minor and CachedData.config_version_data["minor"] != minor: return False # compatible @@ -431,21 +431,21 @@ def get_ocio_config_colorspaces(config_path): Returns: dict: colorspace and family in couple """ - if not CashedData.ocio_config_colorspaces.get(config_path): + if not CachedData.ocio_config_colorspaces.get(config_path): if not compatibility_check(): # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess - CashedData.ocio_config_colorspaces[config_path] = \ + CachedData.ocio_config_colorspaces[config_path] = \ get_wrapped_with_subprocess( "config", "get_colorspace", in_path=config_path ) else: from openpype.scripts.ocio_wrapper import _get_colorspace_data - CashedData.ocio_config_colorspaces[config_path] = \ + CachedData.ocio_config_colorspaces[config_path] = \ _get_colorspace_data(config_path) - return CashedData.ocio_config_colorspaces[config_path] + return CachedData.ocio_config_colorspaces[config_path] # TODO: remove this in future - backward compatibility @@ -730,15 +730,15 @@ def get_remapped_colorspace_to_native( Union[str, None]: native colorspace name defined in remapping or None """ - CashedData.remapping.setdefault(host_name, {}) - if CashedData.remapping[host_name].get("to_native") is None: + CachedData.remapping.setdefault(host_name, {}) + if CachedData.remapping[host_name].get("to_native") is None: remapping_rules = imageio_host_settings["remapping"]["rules"] - CashedData.remapping[host_name]["to_native"] = { + CachedData.remapping[host_name]["to_native"] = { rule["ocio_name"]: rule["host_native_name"] for rule in remapping_rules } - return CashedData.remapping[host_name]["to_native"].get( + return CachedData.remapping[host_name]["to_native"].get( ocio_colorspace_name) @@ -756,15 +756,15 @@ def get_remapped_colorspace_from_native( Union[str, None]: Ocio colorspace name defined in remapping or None. """ - CashedData.remapping.setdefault(host_name, {}) - if CashedData.remapping[host_name].get("from_native") is None: + CachedData.remapping.setdefault(host_name, {}) + if CachedData.remapping[host_name].get("from_native") is None: remapping_rules = imageio_host_settings["remapping"]["rules"] - CashedData.remapping[host_name]["from_native"] = { + CachedData.remapping[host_name]["from_native"] = { rule["host_native_name"]: rule["ocio_name"] for rule in remapping_rules } - return CashedData.remapping[host_name]["from_native"].get( + return CachedData.remapping[host_name]["from_native"].get( host_native_colorspace_name) From 3292114e2109656e3fb92d6443db52838007181c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 15:46:59 +0200 Subject: [PATCH 048/267] better error message --- openpype/pipeline/colorspace.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 8025022c59..9c77723d12 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -257,8 +257,7 @@ def parse_colorspace_from_filepath( """ if not colorspaces and not config_path: raise ValueError( - "You need to provide `config_path` if you don't " - "want to provide input `colorspaces`." + "Must provide `config_path` if `colorspaces` is not provided." ) colorspaces = colorspaces or get_ocio_config_colorspaces(config_path) From c89d384e5a0033d436763751d3be9c85ea639ce2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 16:55:36 +0200 Subject: [PATCH 049/267] optimisation of regex search suggestion from https://github.com/ynput/OpenPype/pull/5273#discussion_r1309372596 --- openpype/pipeline/colorspace.py | 49 +++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 9c77723d12..47227a0e3b 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -255,24 +255,49 @@ def parse_colorspace_from_filepath( Returns: str: name of colorspace """ + def _get_colorspace_match_regex(colorspaces): + """Return a regex patter + + Allows to search a colorspace match in a filename + + Args: + colorspaces (list): List of colorspace names + + Returns: + re.Pattern: regex pattern + """ + pattern = "|".join( + # Allow to match spaces also as underscores because the + # integrator replaces spaces with underscores in filenames + re.escape(colorspace).replace(r"\ ", r"[_ ]") for colorspace in + # Sort by longest first so the regex matches longer matches + # over smaller matches, e.g. matching 'Output - sRGB' over 'sRGB' + sorted(colorspaces, key=len, reverse=True) + ) + return re.compile(pattern) + if not colorspaces and not config_path: raise ValueError( "Must provide `config_path` if `colorspaces` is not provided." ) - colorspaces = colorspaces or get_ocio_config_colorspaces(config_path) - - # match file rule from path colorspace_name = None - for colorspace_key in colorspaces: - # check underscored variant of colorspace name - # since we are reformatting it in integrate.py - if colorspace_key.replace(" ", "_") in filepath: - colorspace_name = colorspace_key - break - if colorspace_key in filepath: - colorspace_name = colorspace_key - break + colorspaces = colorspaces or get_ocio_config_colorspaces(config_path) + underscored_colorspaces = { + key.replace(" ", "_"): key for key in colorspaces + if " " in key + } + + # match colorspace from filepath + regex_pattern = _get_colorspace_match_regex(colorspaces) + match = regex_pattern.search(filepath) + colorspace = match.group(0) if match else None + + if colorspace: + colorspace_name = colorspace + + if colorspace in underscored_colorspaces: + colorspace_name = underscored_colorspaces[colorspace] if not colorspace_name: log.info("No matching colorspace in config '{}' for path: '{}'".format( From 3928e26c45f91b1d2113268c8d0c4a889db93cbf Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 17:02:59 +0200 Subject: [PATCH 050/267] turn public to non-public function --- openpype/pipeline/colorspace.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 47227a0e3b..118466bc92 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -213,7 +213,7 @@ def get_colorspace_from_filepath(config_path, filepath): if not compatibility_check(): # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess - result_data = get_wrapped_with_subprocess( + result_data = _get_wrapped_with_subprocess( "colorspace", "get_colorspace_from_filepath", config_path=config_path, filepath=filepath @@ -331,7 +331,7 @@ def validate_imageio_colorspace_in_config(config_path, colorspace_name): # TODO: remove this in future - backward compatibility -@deprecated("get_wrapped_with_subprocess") +@deprecated("_get_wrapped_with_subprocess") def get_data_subprocess(config_path, data_type): """[Deprecated] Get data via subprocess @@ -361,7 +361,7 @@ def get_data_subprocess(config_path, data_type): return json.loads(return_json_data) -def get_wrapped_with_subprocess(command_group, command, **kwargs): +def _get_wrapped_with_subprocess(command_group, command, **kwargs): """Get data via subprocess Wrapper for Python 2 hosts. @@ -427,7 +427,7 @@ def compatibility_check_config_version(config_path, major=1, minor=None): else: # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess - CachedData.config_version_data = get_wrapped_with_subprocess( + CachedData.config_version_data = _get_wrapped_with_subprocess( "config", "get_version", config_path=config_path ) @@ -460,7 +460,7 @@ def get_ocio_config_colorspaces(config_path): # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess CachedData.ocio_config_colorspaces[config_path] = \ - get_wrapped_with_subprocess( + _get_wrapped_with_subprocess( "config", "get_colorspace", in_path=config_path ) else: @@ -473,7 +473,7 @@ def get_ocio_config_colorspaces(config_path): # TODO: remove this in future - backward compatibility -@deprecated("get_wrapped_with_subprocess") +@deprecated("_get_wrapped_with_subprocess") def get_colorspace_data_subprocess(config_path): """[Deprecated] Get colorspace data via subprocess @@ -485,7 +485,7 @@ def get_colorspace_data_subprocess(config_path): Returns: dict: colorspace and family in couple """ - return get_wrapped_with_subprocess( + return _get_wrapped_with_subprocess( "config", "get_colorspace", in_path=config_path ) @@ -505,7 +505,7 @@ def get_ocio_config_views(config_path): if not compatibility_check(): # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess - return get_wrapped_with_subprocess( + return _get_wrapped_with_subprocess( "config", "get_views", in_path=config_path ) @@ -515,7 +515,7 @@ def get_ocio_config_views(config_path): # TODO: remove this in future - backward compatibility -@deprecated("get_wrapped_with_subprocess") +@deprecated("_get_wrapped_with_subprocess") def get_views_data_subprocess(config_path): """[Deprecated] Get viewers data via subprocess @@ -527,7 +527,7 @@ def get_views_data_subprocess(config_path): Returns: dict: `display/viewer` and viewer data """ - return get_wrapped_with_subprocess( + return _get_wrapped_with_subprocess( "config", "get_views", in_path=config_path ) From 7db4be1a482bbe63e24f1a0176ed35e260cc7bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 30 Aug 2023 17:04:24 +0200 Subject: [PATCH 051/267] Update openpype/pipeline/colorspace.py Co-authored-by: Roy Nieterau --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 118466bc92..fa9f124130 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -156,7 +156,7 @@ def get_imageio_colorspace_from_filepath( # match file rule from path colorspace_name = None - for _, file_rule in file_rules.items(): + for file_rule in file_rules.values(): pattern = file_rule["pattern"] extension = file_rule["ext"] ext_match = re.match( From d570a2bff19fdaa6f0af909c5e450275684f1ab2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 30 Aug 2023 17:05:48 +0200 Subject: [PATCH 052/267] Update openpype/pipeline/colorspace.py Co-authored-by: Roy Nieterau --- openpype/pipeline/colorspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index fa9f124130..20ca60b10e 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -357,8 +357,8 @@ def get_data_subprocess(config_path, data_type): run_openpype_process(*args, **process_kwargs) # return all colorspaces - return_json_data = open(tmp_json_path).read() - return json.loads(return_json_data) + with open(tmp_json_path, "r") as f: + return json.load(f) def _get_wrapped_with_subprocess(command_group, command, **kwargs): From e8c58f13d7a1b394aae3ad60a7ec2bc290b38511 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 17:08:42 +0200 Subject: [PATCH 053/267] suggestion for json return from file --- openpype/pipeline/colorspace.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 20ca60b10e..c7eb778fa2 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -357,8 +357,8 @@ def get_data_subprocess(config_path, data_type): run_openpype_process(*args, **process_kwargs) # return all colorspaces - with open(tmp_json_path, "r") as f: - return json.load(f) + with open(tmp_json_path, "r") as f_: + return json.load(f_) def _get_wrapped_with_subprocess(command_group, command, **kwargs): @@ -396,8 +396,8 @@ def _get_wrapped_with_subprocess(command_group, command, **kwargs): run_openpype_process(*args, **process_kwargs) # return all colorspaces - return_json_data = open(tmp_json_path).read() - return json.loads(return_json_data) + with open(tmp_json_path, "r") as f_: + return json.load(f_) def compatibility_check(): From f1cf4fcda6a205f8c89573f3f5892acb8c5d51e0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 17:23:01 +0200 Subject: [PATCH 054/267] removing deprecated code with backward compatibility --- openpype/pipeline/colorspace.py | 22 +++------------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index c7eb778fa2..9f1c297188 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -340,25 +340,9 @@ def get_data_subprocess(config_path, data_type): Args: config_path (str): path leading to config.ocio file """ - with _make_temp_json_file() as tmp_json_path: - # Prepare subprocess arguments - args = [ - "run", get_ocio_config_script_path(), - "config", data_type, - "--in_path", config_path, - "--out_path", tmp_json_path - ] - log.info("Executing: {}".format(" ".join(args))) - - process_kwargs = { - "logger": log - } - - run_openpype_process(*args, **process_kwargs) - - # return all colorspaces - with open(tmp_json_path, "r") as f_: - return json.load(f_) + return _get_wrapped_with_subprocess( + "config", data_type, in_path=config_path, + ) def _get_wrapped_with_subprocess(command_group, command, **kwargs): From a6b8fdfe33dc5dcf1eb1120a65f5ad264b3b1ef4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 30 Aug 2023 22:43:06 +0200 Subject: [PATCH 055/267] refactor function names and code logic for colorspace from filepath --- openpype/pipeline/colorspace.py | 118 +++++++++++++++++++++---------- openpype/scripts/ocio_wrapper.py | 10 +-- 2 files changed, 84 insertions(+), 44 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 9f1c297188..f1acd18a70 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -115,27 +115,92 @@ def get_ocio_config_script_path(): ) -def get_imageio_colorspace_from_filepath( - path, host_name, project_name, +def get_colorspace_name_from_filepath( + filepath, host_name, project_name, config_data=None, file_rules=None, project_settings=None, validate=True ): """Get colorspace name from filepath + Args: + filepath (str): path string, file rule pattern is tested on it + host_name (str): host name + project_name (str): project name + config_data (Optional[dict]): config path and template in dict. + Defaults to None. + file_rules (Optional[dict]): file rule data from settings. + Defaults to None. + project_settings (Optional[dict]): project settings. Defaults to None. + validate (Optional[bool]): should resulting colorspace be validated + with config file? Defaults to True. + + Returns: + str: name of colorspace + """ + # use ImageIO file rules + colorspace_name = get_imageio_file_rules_colorspace_from_filepath( + filepath, host_name, project_name, + config_data=config_data, file_rules=file_rules, + project_settings=project_settings + ) + + # try to get colorspace from OCIO v2 file rules + if ( + not colorspace_name + and compatibility_check_config_version(config_data["path"], major=2) + ): + colorspace_name = get_config_file_rules_colorspace_from_filepath( + config_data["path"], filepath) + + # use parse colorspace from filepath as fallback + colorspace_name = colorspace_name or parse_colorspace_from_filepath( + filepath, config_path=config_data["path"] + ) + + if not colorspace_name: + log.info("No imageio file rule matched input path: '{}'".format( + filepath + )) + return None + + # validate matching colorspace with config + if validate and config_data: + validate_imageio_colorspace_in_config( + config_data["path"], colorspace_name) + + return colorspace_name + + +# TODO: remove this in future - backward compatibility +@deprecated("get_imageio_file_rules_colorspace_from_filepath") +def get_imageio_colorspace_from_filepath(*args, **kwargs): + return get_imageio_file_rules_colorspace_from_filepath(*args, **kwargs) + +# TODO: remove this in future - backward compatibility +@deprecated("get_imageio_file_rules_colorspace_from_filepath") +def get_colorspace_from_filepath(*args, **kwargs): + return get_imageio_file_rules_colorspace_from_filepath(*args, **kwargs) + + +def get_imageio_file_rules_colorspace_from_filepath( + filepath, host_name, project_name, + config_data=None, file_rules=None, + project_settings=None +): + """Get colorspace name from filepath + ImageIO Settings file rules are tested for matching rule. Args: - path (str): path string, file rule pattern is tested on it + filepath (str): path string, file rule pattern is tested on it host_name (str): host name project_name (str): project name - config_data (dict, optional): config path and template in dict. + config_data (Optional[dict]): config path and template in dict. Defaults to None. - file_rules (dict, optional): file rule data from settings. + file_rules (Optional[dict]): file rule data from settings. Defaults to None. - project_settings (dict, optional): project settings. Defaults to None. - validate (bool, optional): should resulting colorspace be validated - with config file? Defaults to True. + project_settings (Optional[dict]): project settings. Defaults to None. Returns: str: name of colorspace @@ -160,44 +225,19 @@ def get_imageio_colorspace_from_filepath( pattern = file_rule["pattern"] extension = file_rule["ext"] ext_match = re.match( - r".*(?=.{})".format(extension), path + r".*(?=.{})".format(extension), filepath ) file_match = re.search( - pattern, path + pattern, filepath ) if ext_match and file_match: colorspace_name = file_rule["colorspace"] - # if no file rule matched, try to get colorspace - # from filepath with OCIO v2 way - if ( - compatibility_check_config_version(config_data["path"], major=2) - and not colorspace_name - ): - colorspace_name = get_colorspace_from_filepath( - config_data["path"], path) - - # use parse colorspace from filepath as fallback - colorspace_name = colorspace_name or parse_colorspace_from_filepath( - path, config_path=config_data["path"] - ) - - if not colorspace_name: - log.info("No imageio file rule matched input path: '{}'".format( - path - )) - return None - - # validate matching colorspace with config - if validate and config_data: - validate_imageio_colorspace_in_config( - config_data["path"], colorspace_name) - return colorspace_name -def get_colorspace_from_filepath(config_path, filepath): +def get_config_file_rules_colorspace_from_filepath(config_path, filepath): """Get colorspace from file path wrapper. Wrapper function for getting colorspace from file path @@ -214,16 +254,16 @@ def get_colorspace_from_filepath(config_path, filepath): # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess result_data = _get_wrapped_with_subprocess( - "colorspace", "get_colorspace_from_filepath", + "colorspace", "get_config_file_rules_colorspace_from_filepath", config_path=config_path, filepath=filepath ) if result_data: return result_data[0] - from openpype.scripts.ocio_wrapper import _get_colorspace_from_filepath + from openpype.scripts.ocio_wrapper import _get_config_file_rules_colorspace_from_filepath - result_data = _get_colorspace_from_filepath(config_path, filepath) + result_data = _get_config_file_rules_colorspace_from_filepath(config_path, filepath) if result_data: return result_data[0] diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 1feedde627..1515cb4e40 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -247,7 +247,7 @@ def _get_version_data(config_path): @colorspace.command( - name="get_colorspace_from_filepath", + name="get_config_file_rules_colorspace_from_filepath", help=( "return colorspace from filepath " "--config_path - ocio config file path (input arg is required) " @@ -264,7 +264,7 @@ def _get_version_data(config_path): @click.option("--out_path", required=True, help="path where to write output json file", type=click.Path()) -def get_colorspace_from_filepath(config_path, filepath, out_path): +def get_config_file_rules_colorspace_from_filepath(config_path, filepath, out_path): """Get colorspace from file path wrapper. Python 2 wrapped console command @@ -275,12 +275,12 @@ def get_colorspace_from_filepath(config_path, filepath, out_path): out_path (str): temp json file path string Example of use: - > pyton.exe ./ocio_wrapper.py colorspace get_colorspace_from_filepath \ + > pyton.exe ./ocio_wrapper.py colorspace get_config_file_rules_colorspace_from_filepath \ --config_path= --filepath= --out_path= """ json_path = Path(out_path) - colorspace = _get_colorspace_from_filepath(config_path, filepath) + colorspace = _get_config_file_rules_colorspace_from_filepath(config_path, filepath) with open(json_path, "w") as f_: json.dump(colorspace, f_) @@ -288,7 +288,7 @@ def get_colorspace_from_filepath(config_path, filepath, out_path): print(f"Colorspace name is saved to '{json_path}'") -def _get_colorspace_from_filepath(config_path, filepath): +def _get_config_file_rules_colorspace_from_filepath(config_path, filepath): """Return found colorspace data found in v2 file rules. Args: From f5d145ea23eb6b6463e1fea0ed07b4b1e0ffe934 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 10:42:57 +0200 Subject: [PATCH 056/267] hound and todos --- openpype/pipeline/colorspace.py | 11 +++++++++-- openpype/scripts/ocio_wrapper.py | 10 +++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index f1acd18a70..e315633d41 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -261,9 +261,11 @@ def get_config_file_rules_colorspace_from_filepath(config_path, filepath): if result_data: return result_data[0] - from openpype.scripts.ocio_wrapper import _get_config_file_rules_colorspace_from_filepath + # TODO: refactor this so it is not imported but part of this file + from openpype.scripts.ocio_wrapper import _get_config_file_rules_colorspace_from_filepath # noqa: E501 - result_data = _get_config_file_rules_colorspace_from_filepath(config_path, filepath) + result_data = _get_config_file_rules_colorspace_from_filepath( + config_path, filepath) if result_data: return result_data[0] @@ -424,6 +426,7 @@ def _get_wrapped_with_subprocess(command_group, command, **kwargs): return json.load(f_) +# TODO: this should be part of ocio_wrapper.py def compatibility_check(): """Making sure PyOpenColorIO is importable""" if CachedData.python3compatible is not None: @@ -439,11 +442,13 @@ def compatibility_check(): return CachedData.python3compatible +# TODO: this should be part of ocio_wrapper.py def compatibility_check_config_version(config_path, major=1, minor=None): """Making sure PyOpenColorIO config version is compatible""" if not CachedData.config_version_data: if compatibility_check(): + # TODO: refactor this so it is not imported but part of this file from openpype.scripts.ocio_wrapper import _get_version_data CachedData.config_version_data = _get_version_data(config_path) @@ -488,6 +493,7 @@ def get_ocio_config_colorspaces(config_path): "config", "get_colorspace", in_path=config_path ) else: + # TODO: refactor this so it is not imported but part of this file from openpype.scripts.ocio_wrapper import _get_colorspace_data CachedData.ocio_config_colorspaces[config_path] = \ @@ -533,6 +539,7 @@ def get_ocio_config_views(config_path): "config", "get_views", in_path=config_path ) + # TODO: refactor this so it is not imported but part of this file from openpype.scripts.ocio_wrapper import _get_views_data return _get_views_data(config_path) diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 1515cb4e40..56399f10a2 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -264,7 +264,9 @@ def _get_version_data(config_path): @click.option("--out_path", required=True, help="path where to write output json file", type=click.Path()) -def get_config_file_rules_colorspace_from_filepath(config_path, filepath, out_path): +def get_config_file_rules_colorspace_from_filepath( + config_path, filepath, out_path +): """Get colorspace from file path wrapper. Python 2 wrapped console command @@ -275,12 +277,14 @@ def get_config_file_rules_colorspace_from_filepath(config_path, filepath, out_pa out_path (str): temp json file path string Example of use: - > pyton.exe ./ocio_wrapper.py colorspace get_config_file_rules_colorspace_from_filepath \ + > pyton.exe ./ocio_wrapper.py \ + colorspace get_config_file_rules_colorspace_from_filepath \ --config_path= --filepath= --out_path= """ json_path = Path(out_path) - colorspace = _get_config_file_rules_colorspace_from_filepath(config_path, filepath) + colorspace = _get_config_file_rules_colorspace_from_filepath( + config_path, filepath) with open(json_path, "w") as f_: json.dump(colorspace, f_) From abedafaff808a02088ef09ef748fa64e8604abaa Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 10:56:56 +0200 Subject: [PATCH 057/267] fixing name of variable in tests --- tests/unit/openpype/pipeline/test_colorspace.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index 8ae98f7cf8..338627098c 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -227,7 +227,7 @@ class TestPipelineColorspace(TestPipeline): expected_hiero = "Gamma 2.2 Rec.709 - Texture" # switch to python 2 compatibility mode - colorspace.CashedData.python3compatible = False + colorspace.CachedData.python3compatible = False nuke_colorspace = colorspace.get_imageio_colorspace_from_filepath( nuke_filepath, @@ -248,7 +248,7 @@ class TestPipelineColorspace(TestPipeline): f"Not matching colorspace {expected_hiero}") # return to python 3 compatibility mode - colorspace.CashedData.python3compatible = None + colorspace.CachedData.python3compatible = None test_case = TestPipelineColorspace() From f7ce6406f94703dab8c47850c49dc6bd8c2d4920 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 11:46:05 +0200 Subject: [PATCH 058/267] adding contextual settings method to be shared between two functions --- openpype/pipeline/colorspace.py | 52 +++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index e315633d41..a4901b7dfd 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -138,6 +138,16 @@ def get_colorspace_name_from_filepath( Returns: str: name of colorspace """ + project_settings, config_data, file_rules = _get_context_settings( + host_name, project_name, + config_data=config_data, file_rules=file_rules, + project_settings=project_settings + ) + + if not config_data: + # in case global or host color management is not enabled + return None + # use ImageIO file rules colorspace_name = get_imageio_file_rules_colorspace_from_filepath( filepath, host_name, project_name, @@ -183,6 +193,28 @@ def get_colorspace_from_filepath(*args, **kwargs): return get_imageio_file_rules_colorspace_from_filepath(*args, **kwargs) +def _get_context_settings( + host_name, project_name, + config_data=None, file_rules=None, + project_settings=None +): + project_settings = project_settings or get_project_settings( + project_name + ) + + config_data = config_data or get_imageio_config( + project_name, host_name, project_settings) + + # in case host color management is not enabled + if not config_data: + return (None, None, None) + + file_rules = file_rules or get_imageio_file_rules( + project_name, host_name, project_settings) + + return project_settings, config_data, file_rules + + def get_imageio_file_rules_colorspace_from_filepath( filepath, host_name, project_name, config_data=None, file_rules=None, @@ -205,19 +237,15 @@ def get_imageio_file_rules_colorspace_from_filepath( Returns: str: name of colorspace """ - if not any([config_data, file_rules]): - project_settings = project_settings or get_project_settings( - project_name - ) - config_data = get_imageio_config( - project_name, host_name, project_settings) + project_settings, config_data, file_rules = _get_context_settings( + host_name, project_name, + config_data=config_data, file_rules=file_rules, + project_settings=project_settings + ) - # in case host color management is not enabled - if not config_data: - return None - - file_rules = get_imageio_file_rules( - project_name, host_name, project_settings) + if not config_data: + # in case global or host color management is not enabled + return None # match file rule from path colorspace_name = None From 1e6f855e7420a123539b37afa5cc5c580412c09c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 31 Aug 2023 11:46:48 +0200 Subject: [PATCH 059/267] fixing tests --- tests/unit/openpype/pipeline/test_colorspace.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index 338627098c..435ea709ab 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -196,7 +196,7 @@ class TestPipelineColorspace(TestPipeline): expected_nuke = "Camera Rec.709" expected_hiero = "Gamma 2.2 Rec.709 - Texture" - nuke_colorspace = colorspace.get_imageio_colorspace_from_filepath( + nuke_colorspace = colorspace.get_colorspace_name_from_filepath( nuke_filepath, "nuke", "test_project", @@ -205,7 +205,7 @@ class TestPipelineColorspace(TestPipeline): assert expected_nuke == nuke_colorspace, ( f"Not matching colorspace {expected_nuke}") - hiero_colorspace = colorspace.get_imageio_colorspace_from_filepath( + hiero_colorspace = colorspace.get_colorspace_name_from_filepath( hiero_filepath, "hiero", "test_project", @@ -229,7 +229,7 @@ class TestPipelineColorspace(TestPipeline): # switch to python 2 compatibility mode colorspace.CachedData.python3compatible = False - nuke_colorspace = colorspace.get_imageio_colorspace_from_filepath( + nuke_colorspace = colorspace.get_colorspace_name_from_filepath( nuke_filepath, "nuke", "test_project", @@ -238,7 +238,7 @@ class TestPipelineColorspace(TestPipeline): assert expected_nuke == nuke_colorspace, ( f"Not matching colorspace {expected_nuke}") - hiero_colorspace = colorspace.get_imageio_colorspace_from_filepath( + hiero_colorspace = colorspace.get_colorspace_name_from_filepath( hiero_filepath, "hiero", "test_project", From fc3598ca2b3e249ae4dc52b606ce33cc2bc2dbab Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 31 Aug 2023 23:27:03 +0800 Subject: [PATCH 060/267] 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 061/267] 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 062/267] 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 063/267] 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 064/267] 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 065/267] 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 066/267] 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 067/267] 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 068/267] 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 069/267] 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 070/267] 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 071/267] 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 072/267] 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 073/267] 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 074/267] 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 075/267] 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 076/267] 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 077/267] 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 078/267] 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 079/267] 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 080/267] 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 081/267] 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 082/267] 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 083/267] 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 084/267] 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 085/267] 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 086/267] 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 087/267] 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 088/267] 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 089/267] 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 090/267] 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 091/267] 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 092/267] 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 093/267] 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 dbd03c6c5a2b851ca6a8708cf6e290fd498e5192 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 14 Sep 2023 08:38:51 +0100 Subject: [PATCH 094/267] Missing "data" field and enabling of audio --- openpype/hosts/maya/plugins/load/load_audio.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index 265b15f4ae..eaaf81d873 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -61,6 +61,14 @@ class AudioLoader(load.LoaderPlugin): path = get_representation_path(representation) cmds.setAttr("{}.filename".format(audio_node), path, type="string") + + cmds.timeControl( + mel.eval("$tmpVar=$gPlayBackSlider"), + edit=True, + sound=audio_node, + displaySound=True + ) + cmds.setAttr( container["objectName"] + ".representation", str(representation["_id"]), @@ -76,7 +84,7 @@ class AudioLoader(load.LoaderPlugin): project_name, version["parent"], fields=["parent"] ) asset = get_asset_by_id( - project_name, subset["parent"], fields=["parent"] + project_name, subset["parent"], fields=["parent", "data"] ) source_start = 1 - asset["data"]["frameStart"] From a2c72e683e2ac3420931a0721a9f3d12f843db96 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 14 Sep 2023 17:10:46 +0800 Subject: [PATCH 095/267] 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 4d22b6cf4b31465833bc239b16e46ec2b1d1f942 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 14 Sep 2023 11:53:06 +0100 Subject: [PATCH 096/267] Update openpype/hosts/maya/plugins/load/load_audio.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/load/load_audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index eaaf81d873..7114d92daa 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -84,7 +84,7 @@ class AudioLoader(load.LoaderPlugin): project_name, version["parent"], fields=["parent"] ) asset = get_asset_by_id( - project_name, subset["parent"], fields=["parent", "data"] + project_name, subset["parent"], fields=["parent", "data.frameStart", "data.frameEnd"] ) source_start = 1 - asset["data"]["frameStart"] From a029031b58cd95663e018d3e8dd38a23e8475cdb Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 14 Sep 2023 11:53:53 +0100 Subject: [PATCH 097/267] Update openpype/hosts/maya/plugins/load/load_audio.py --- openpype/hosts/maya/plugins/load/load_audio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index 7114d92daa..9c2fdfb6d3 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -84,7 +84,8 @@ class AudioLoader(load.LoaderPlugin): project_name, version["parent"], fields=["parent"] ) asset = get_asset_by_id( - project_name, subset["parent"], fields=["parent", "data.frameStart", "data.frameEnd"] + project_name, subset["parent"], + fields=["parent", "data.frameStart", "data.frameEnd"] ) source_start = 1 - asset["data"]["frameStart"] From 887127828887d0642d1e77c2f92b7f3a441135a7 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 14 Sep 2023 11:54:14 +0100 Subject: [PATCH 098/267] Update openpype/hosts/maya/plugins/load/load_audio.py --- openpype/hosts/maya/plugins/load/load_audio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index 9c2fdfb6d3..17c7d442ae 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -84,7 +84,8 @@ class AudioLoader(load.LoaderPlugin): project_name, version["parent"], fields=["parent"] ) asset = get_asset_by_id( - project_name, subset["parent"], + project_name, + subset["parent"], fields=["parent", "data.frameStart", "data.frameEnd"] ) From 3903071f21f2d2e7ec426d287fe1713c8cf7122b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 14 Sep 2023 11:55:50 +0100 Subject: [PATCH 099/267] Check current sound on timeline --- .../hosts/maya/plugins/load/load_audio.py | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index 17c7d442ae..6e2f2e89bc 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -59,15 +59,23 @@ class AudioLoader(load.LoaderPlugin): assert audio_nodes is not None, "Audio node not found." audio_node = audio_nodes[0] + current_sound = cmds.timeControl( + mel.eval("$tmpVar=$gPlayBackSlider"), + query=True, + sound=True + ) + activate_sound = current_sound == audio_node + path = get_representation_path(representation) cmds.setAttr("{}.filename".format(audio_node), path, type="string") - cmds.timeControl( - mel.eval("$tmpVar=$gPlayBackSlider"), - edit=True, - sound=audio_node, - displaySound=True - ) + if activate_sound: + cmds.timeControl( + mel.eval("$tmpVar=$gPlayBackSlider"), + edit=True, + sound=audio_node, + displaySound=True + ) cmds.setAttr( container["objectName"] + ".representation", From 39dad459d588486a5711cecf79419c24d99524ed Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 14 Sep 2023 15:16:28 +0100 Subject: [PATCH 100/267] Update openpype/hosts/maya/plugins/load/load_audio.py --- openpype/hosts/maya/plugins/load/load_audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index 6e2f2e89bc..fedb985e0b 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -60,7 +60,7 @@ class AudioLoader(load.LoaderPlugin): audio_node = audio_nodes[0] current_sound = cmds.timeControl( - mel.eval("$tmpVar=$gPlayBackSlider"), + mel.eval("$gPlayBackSlider=$gPlayBackSlider"), query=True, sound=True ) From 2c2aef60de3b9e66e3efb79cf2d01578dcf829df Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 14 Sep 2023 15:16:52 +0100 Subject: [PATCH 101/267] Update openpype/hosts/maya/plugins/load/load_audio.py --- openpype/hosts/maya/plugins/load/load_audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index fedb985e0b..7750d41e97 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -71,7 +71,7 @@ class AudioLoader(load.LoaderPlugin): if activate_sound: cmds.timeControl( - mel.eval("$tmpVar=$gPlayBackSlider"), + mel.eval("$gPlayBackSlider=$gPlayBackSlider"), edit=True, sound=audio_node, displaySound=True From a4b1797b2abc2869d15f343f8f00894783fa5270 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 14 Sep 2023 15:18:13 +0100 Subject: [PATCH 102/267] tmpVar > gPlayBackSlider --- openpype/hosts/maya/plugins/load/load_audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index 7750d41e97..d3a670398b 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -30,7 +30,7 @@ class AudioLoader(load.LoaderPlugin): file=context["representation"]["data"]["path"], offset=start_frame ) cmds.timeControl( - mel.eval("$tmpVar=$gPlayBackSlider"), + mel.eval("$gPlayBackSlider=$gPlayBackSlider"), edit=True, sound=sound_node, displaySound=True From 2c2b5a35057ed10d060f5fc645b9ed53f5e5560c Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 14 Sep 2023 15:39:00 +0100 Subject: [PATCH 103/267] Update openpype/hosts/maya/plugins/load/load_audio.py Co-authored-by: Roy Nieterau --- openpype/hosts/maya/plugins/load/load_audio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index d3a670398b..2da5a6f1c2 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -70,6 +70,7 @@ class AudioLoader(load.LoaderPlugin): cmds.setAttr("{}.filename".format(audio_node), path, type="string") if activate_sound: + # maya by default deactivates it from timeline on file change cmds.timeControl( mel.eval("$gPlayBackSlider=$gPlayBackSlider"), edit=True, From 96638726a90896673457dccc91f5bec5fd069ae9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 13:09:32 +0100 Subject: [PATCH 104/267] 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 105/267] 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 106/267] 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 107/267] 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 108/267] 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 109/267] 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 d38cf8258954523f7025015670bfe6b7130c6d08 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 18 Sep 2023 08:49:51 +0100 Subject: [PATCH 110/267] Fix file path fetching from context. --- openpype/hosts/maya/plugins/load/load_audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index 2da5a6f1c2..2ea0b134d1 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -27,7 +27,7 @@ class AudioLoader(load.LoaderPlugin): start_frame = cmds.playbackOptions(query=True, min=True) sound_node = cmds.sound( - file=context["representation"]["data"]["path"], offset=start_frame + file=self.filepath_from_context(context), offset=start_frame ) cmds.timeControl( mel.eval("$gPlayBackSlider=$gPlayBackSlider"), From 7c1d81d62c267ad04c36f33c94ecb1aec3f569aa Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 18 Sep 2023 08:50:11 +0100 Subject: [PATCH 111/267] Improve loader label. --- openpype/hosts/maya/plugins/load/load_audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index 2ea0b134d1..ecf98303d2 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -18,7 +18,7 @@ class AudioLoader(load.LoaderPlugin): """Specific loader of audio.""" families = ["audio"] - label = "Import audio" + label = "Load audio" representations = ["wav"] icon = "volume-up" color = "orange" From ccf4b7f54781d81d9d6d331f63e3a5a8ab0e0d6f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Sep 2023 10:52:38 +0100 Subject: [PATCH 112/267] 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 113/267] 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 114/267] 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 115/267] 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 116/267] 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 411f4bacd16d3d3c5c4ffb29d69f88f383b094dc Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 18 Sep 2023 15:03:45 +0100 Subject: [PATCH 117/267] Support new publisher for colorsets validation. --- .../maya/plugins/publish/validate_color_sets.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/validate_color_sets.py b/openpype/hosts/maya/plugins/publish/validate_color_sets.py index 766124cd9e..173fee4179 100644 --- a/openpype/hosts/maya/plugins/publish/validate_color_sets.py +++ b/openpype/hosts/maya/plugins/publish/validate_color_sets.py @@ -3,9 +3,10 @@ from maya import cmds import pyblish.api import openpype.hosts.maya.api.action from openpype.pipeline.publish import ( - RepairAction, ValidateMeshOrder, - OptionalPyblishPluginMixin + OptionalPyblishPluginMixin, + PublishValidationError, + RepairAction ) @@ -22,8 +23,9 @@ class ValidateColorSets(pyblish.api.Validator, hosts = ['maya'] families = ['model'] label = 'Mesh ColorSets' - actions = [openpype.hosts.maya.api.action.SelectInvalidAction, - RepairAction] + actions = [ + openpype.hosts.maya.api.action.SelectInvalidAction, RepairAction + ] optional = True @staticmethod @@ -48,8 +50,9 @@ class ValidateColorSets(pyblish.api.Validator, invalid = self.get_invalid(instance) if invalid: - raise ValueError("Meshes found with " - "Color Sets: {0}".format(invalid)) + raise PublishValidationError( + message="Meshes found with Color Sets: {0}".format(invalid) + ) @classmethod def repair(cls, instance): From c03326f5d8d0c45427d5cb24187c390d2b8c5113 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 19 Sep 2023 19:19:40 +0800 Subject: [PATCH 118/267] Jakub's comment on the review plugin --- openpype/hosts/nuke/api/plugin.py | 5 + .../publish/extract_review_data_mov.py | 2 +- openpype/settings/ayon_settings.py | 21 ++- .../defaults/project_settings/nuke.json | 54 +++++++ .../schemas/schema_nuke_publish.json | 145 ++++++++++++++++++ .../nuke/server/settings/publish_plugins.py | 10 +- .../settings_project_global.md | 12 +- website/docs/pype2/admin_presets_plugins.md | 3 +- 8 files changed, 236 insertions(+), 16 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index a0e1525cd0..adbe43e481 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -21,6 +21,9 @@ from openpype.pipeline import ( CreatedInstance, get_current_task_name ) +from openpype.lib.transcoding import ( + VIDEO_EXTENSIONS +) from .lib import ( INSTANCE_DATA_KNOB, Knobby, @@ -801,6 +804,8 @@ class ExporterReviewMov(ExporterReview): self.log.info("File info was set...") self.file = self.fhead + self.name + ".{}".format(self.ext) + if self.ext != VIDEO_EXTENSIONS: + self.file = os.path.basename(self.path_in) self.path = os.path.join( self.staging_dir, self.file).replace("\\", "/") diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py index 956d1a54a3..1568a2de9b 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py @@ -8,7 +8,7 @@ from openpype.hosts.nuke.api import plugin from openpype.hosts.nuke.api.lib import maintained_selection -class ExtractReviewDataMov(publish.Extractor): +class ExtractReviewDataBakingStreams(publish.Extractor): """Extracts movie and thumbnail with baked in luts must be run after extract_render_local.py diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 9a4f0607e0..b7fcaa1216 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -748,7 +748,19 @@ def _convert_nuke_project_settings(ayon_settings, output): ) new_review_data_outputs = {} - for item in ayon_publish["ExtractReviewDataMov"]["outputs"]: + outputs_settings = None + # just in case that the users having old presets in outputs setting + deprecrated_review_settings = ayon_publish["ExtractReviewDataMov"] + current_review_settings = ( + ayon_publish["ExtractReviewDataBakingStreams"] + ) + if deprecrated_review_settings["outputs"] == ( + current_review_settings["outputs"]): + outputs_settings = current_review_settings["outputs"] + else: + outputs_settings = deprecrated_review_settings["outputs"] + + for item in outputs_settings: item_filter = item["filter"] if "product_names" in item_filter: item_filter["subsets"] = item_filter.pop("product_names") @@ -767,7 +779,12 @@ def _convert_nuke_project_settings(ayon_settings, output): name = item.pop("name") new_review_data_outputs[name] = item - ayon_publish["ExtractReviewDataMov"]["outputs"] = new_review_data_outputs + + if deprecrated_review_settings["outputs"] == ( + current_review_settings["outputs"]): + current_review_settings["outputs"] = new_review_data_outputs + else: + deprecrated_review_settings["outputs"] = new_review_data_outputs collect_instance_data = ayon_publish["CollectInstanceData"] if "sync_workfile_version_on_product_types" in collect_instance_data: diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 7961e77113..fac78dbcd5 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -501,6 +501,60 @@ } } }, + "ExtractReviewDataBakingStreams": { + "enabled": true, + "viewer_lut_raw": false, + "outputs": { + "baking": { + "filter": { + "task_types": [], + "families": [], + "subsets": [] + }, + "read_raw": false, + "viewer_process_override": "", + "bake_viewer_process": true, + "bake_viewer_input_process": true, + "reformat_nodes_config": { + "enabled": false, + "reposition_nodes": [ + { + "node_class": "Reformat", + "knobs": [ + { + "type": "text", + "name": "type", + "value": "to format" + }, + { + "type": "text", + "name": "format", + "value": "HD_1080" + }, + { + "type": "text", + "name": "filter", + "value": "Lanczos6" + }, + { + "type": "bool", + "name": "black_outside", + "value": true + }, + { + "type": "bool", + "name": "pbb", + "value": false + } + ] + } + ] + }, + "extension": "mov", + "add_custom_tags": [] + } + } + }, "ExtractSlateFrame": { "viewer_lut_raw": false, "key_value_mapping": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index f006392bef..0f366d55ba 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -371,6 +371,151 @@ ] }, + { + "type": "label", + "label": "^ Settings and for ExtractReviewDataMov is deprecated and will be soon removed.
Please use ExtractReviewDataBakingStreams instead." + }, + { + "type": "dict", + "collapsible": true, + "checkbox_key": "enabled", + "key": "ExtractReviewDataBakingStreams", + "label": "ExtractReviewDataBakingStreams", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "viewer_lut_raw", + "label": "Viewer LUT raw" + }, + { + "key": "outputs", + "label": "Output Definitions", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "type": "dict", + "collapsible": false, + "key": "filter", + "label": "Filtering", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subsets", + "type": "list", + "object_type": "text" + } + ] + }, + { + "type": "separator" + }, + { + "type": "boolean", + "key": "read_raw", + "label": "Read colorspace RAW", + "default": false + }, + { + "type": "text", + "key": "viewer_process_override", + "label": "Viewer Process colorspace profile override" + }, + { + "type": "boolean", + "key": "bake_viewer_process", + "label": "Bake Viewer Process" + }, + { + "type": "boolean", + "key": "bake_viewer_input_process", + "label": "Bake Viewer Input Process (LUTs)" + }, + { + "type": "separator" + }, + { + "key": "reformat_nodes_config", + "type": "dict", + "label": "Reformat Nodes", + "collapsible": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "label", + "label": "Reposition knobs supported only.
You can add multiple reformat nodes
and set their knobs. Order of reformat
nodes is important. First reformat node
will be applied first and last reformat
node will be applied last." + }, + { + "key": "reposition_nodes", + "type": "list", + "label": "Reposition nodes", + "object_type": { + "type": "dict", + "children": [ + { + "key": "node_class", + "label": "Node class", + "type": "text" + }, + { + "type": "schema_template", + "name": "template_nuke_knob_inputs", + "template_data": [ + { + "label": "Node knobs", + "key": "knobs" + } + ] + } + ] + } + } + ] + }, + { + "type": "separator" + }, + { + "type": "text", + "key": "extension", + "label": "Write node file type" + }, + { + "key": "add_custom_tags", + "label": "Add custom tags", + "type": "list", + "object_type": "text" + } + ] + } + } + + ] + }, { "type": "dict", "collapsible": true, diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index c78685534f..423448219d 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -165,7 +165,7 @@ class BakingStreamModel(BaseSettingsModel): title="Custom tags", default_factory=list) -class ExtractReviewDataMovModel(BaseSettingsModel): +class ExtractReviewBakingStreamsModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") viewer_lut_raw: bool = Field(title="Viewer lut raw") outputs: list[BakingStreamModel] = Field( @@ -266,9 +266,9 @@ class PublishPuginsModel(BaseSettingsModel): title="Extract Review Data Lut", default_factory=ExtractReviewDataLutModel ) - ExtractReviewDataMov: ExtractReviewDataMovModel = Field( - title="Extract Review Data Mov", - default_factory=ExtractReviewDataMovModel + ExtractReviewDataBakingStreams: ExtractReviewBakingStreamsModel = Field( + title="Extract Review Data Baking Streams", + default_factory=ExtractReviewBakingStreamsModel ) ExtractSlateFrame: ExtractSlateFrameModel = Field( title="Extract Slate Frame", @@ -410,7 +410,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "ExtractReviewDataLut": { "enabled": False }, - "ExtractReviewDataMov": { + "ExtractReviewDataBakingStreams": { "enabled": True, "viewer_lut_raw": False, "outputs": [ diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 5ddf247d98..9092ccdcdf 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -189,10 +189,10 @@ A profile may generate multiple outputs from a single input. Each output must de - Profile filtering defines which group of output definitions is used but output definitions may require more specific filters on their own. - They may filter by subset name (regex can be used) or publish families. Publish families are more complex as are based on knowing code base. - Filtering by custom tags -> this is used for targeting to output definitions from other extractors using settings (at this moment only Nuke bake extractor can target using custom tags). - - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewDataMov/outputs/baking/add_custom_tags` + - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewDataBakingStreams/outputs/baking/add_custom_tags` - Filtering by input length. Input may be video, sequence or single image. It is possible that `.mp4` should be created only when input is video or sequence and to create review `.png` when input is single frame. In some cases the output should be created even if it's single frame or multi frame input. - + ### Extract Burnin Plugin is responsible for adding burnins into review representations. @@ -226,13 +226,13 @@ A burnin profile may set multiple burnin outputs from one input. The burnin's na | **Bottom Centered** | Bottom center content. | str | "{username}" | | **Bottom Right** | Bottom right corner content. | str | "{frame_start}-{current_frame}-{frame_end}" | -Each burnin profile can be configured with additional family filtering and can -add additional tags to the burnin representation, these can be configured under +Each burnin profile can be configured with additional family filtering and can +add additional tags to the burnin representation, these can be configured under the profile's **Additional filtering** section. :::note Filename suffix -The filename suffix is appended to filename of the source representation. For -example, if the source representation has suffix **"h264"** and the burnin +The filename suffix is appended to filename of the source representation. For +example, if the source representation has suffix **"h264"** and the burnin suffix is **"client"** then the final suffix is **"h264_client"**. ::: diff --git a/website/docs/pype2/admin_presets_plugins.md b/website/docs/pype2/admin_presets_plugins.md index 6a057f4bb4..a869ead819 100644 --- a/website/docs/pype2/admin_presets_plugins.md +++ b/website/docs/pype2/admin_presets_plugins.md @@ -534,8 +534,7 @@ Plugin responsible for generating thumbnails with colorspace controlled by Nuke. } ``` -### `ExtractReviewDataMov` - +### `ExtractReviewDataBakingStreams` `viewer_lut_raw` **true** will publish the baked mov file without any colorspace conversion. It will be baked with the workfile workspace. This can happen in case the Viewer input process uses baked screen space luts. #### baking with controlled colorspace From e0fba9713d37c4cf73210c2c37179f9403b2399a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 19 Sep 2023 19:22:52 +0800 Subject: [PATCH 119/267] hound --- openpype/settings/ayon_settings.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index b7fcaa1216..0b72d267f7 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -755,7 +755,8 @@ def _convert_nuke_project_settings(ayon_settings, output): ayon_publish["ExtractReviewDataBakingStreams"] ) if deprecrated_review_settings["outputs"] == ( - current_review_settings["outputs"]): + current_review_settings["outputs"] + ): outputs_settings = current_review_settings["outputs"] else: outputs_settings = deprecrated_review_settings["outputs"] @@ -781,7 +782,8 @@ def _convert_nuke_project_settings(ayon_settings, output): new_review_data_outputs[name] = item if deprecrated_review_settings["outputs"] == ( - current_review_settings["outputs"]): + current_review_settings["outputs"] + ): current_review_settings["outputs"] = new_review_data_outputs else: deprecrated_review_settings["outputs"] = new_review_data_outputs From 5b2e9da304697aea73af8cac3616d895e4bdd0d0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Sep 2023 14:52:19 +0200 Subject: [PATCH 120/267] (nuke): fix set colorspace on writes --- openpype/hosts/nuke/api/lib.py | 48 +++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 41e6a27cef..8626151beb 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2320,23 +2320,51 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. # get data from avalon knob avalon_knob_data = read_avalon_data(node) + node_data = get_node_data(node, INSTANCE_DATA_KNOB) if avalon_knob_data.get("id") != "pyblish.avalon.instance": + + if ( + # backward compatibility + # TODO: remove this once old avalon data api will be removed + avalon_knob_data + and avalon_knob_data.get("id") != "pyblish.avalon.instance" + ): + continue + elif ( + node_data + and node_data.get("id") != "pyblish.avalon.instance" + ): continue - if "creator" not in avalon_knob_data: + if ( + # backward compatibility + # TODO: remove this once old avalon data api will be removed + avalon_knob_data + and "creator" not in avalon_knob_data + ): + continue + elif ( + node_data + and "creator_identifier" not in node_data + ): continue - # establish families - families = [avalon_knob_data["family"]] - if avalon_knob_data.get("families"): - families.append(avalon_knob_data.get("families")) - nuke_imageio_writes = get_imageio_node_setting( - node_class=avalon_knob_data["families"], - plugin_name=avalon_knob_data["creator"], - subset=avalon_knob_data["subset"] - ) + nuke_imageio_writes = None + if avalon_knob_data: + # establish families + families = [avalon_knob_data["family"]] + if avalon_knob_data.get("families"): + families.append(avalon_knob_data.get("families")) + + nuke_imageio_writes = get_imageio_node_setting( + node_class=avalon_knob_data["families"], + plugin_name=avalon_knob_data["creator"], + subset=avalon_knob_data["subset"] + ) + elif node_data: + nuke_imageio_writes = get_write_node_template_attr(node) log.debug("nuke_imageio_writes: `{}`".format(nuke_imageio_writes)) From 961013e9afd5074b768ee73fe0a10464bdb62cd0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Sep 2023 15:03:18 +0200 Subject: [PATCH 121/267] Nuke: adding print of name of node which is processed --- openpype/hosts/nuke/api/lib.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 8626151beb..fb2b5d0f45 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2316,14 +2316,13 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. ''' Adds correct colorspace to write node dict ''' - for node in nuke.allNodes(filter="Group"): + for node in nuke.allNodes(filter="Group", group=self._root_node): + log.info("Setting colorspace to `{}`".format(node.name())) # get data from avalon knob avalon_knob_data = read_avalon_data(node) node_data = get_node_data(node, INSTANCE_DATA_KNOB) - if avalon_knob_data.get("id") != "pyblish.avalon.instance": - if ( # backward compatibility # TODO: remove this once old avalon data api will be removed @@ -2350,7 +2349,6 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. ): continue - nuke_imageio_writes = None if avalon_knob_data: # establish families From 3d7479b65ec4a5b862633536791ac0773d5cdd67 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 19 Sep 2023 15:43:26 +0200 Subject: [PATCH 122/267] nuke: extract review data mov read node with expression --- 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 a0e1525cd0..1e318e17cf 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -869,6 +869,11 @@ class ExporterReviewMov(ExporterReview): r_node["origlast"].setValue(self.last_frame) r_node["colorspace"].setValue(self.write_colorspace) + # do not rely on defaults, set explicitly + # to be sure it is set correctly + r_node["frame_mode"].setValue("expression") + r_node["frame"].setValue("") + if read_raw: r_node["raw"].setValue(1) From b7995a41a71fc8d0e9593f7c9940a8e7f06de9dc Mon Sep 17 00:00:00 2001 From: Kayla Date: Wed, 20 Sep 2023 21:26:02 +0800 Subject: [PATCH 123/267] 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 124/267] 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 125/267] 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 126/267] 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 b8e054a50cac66c4e671da0261093be68db555b2 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Sep 2023 17:01:07 +0200 Subject: [PATCH 127/267] renaming variable to make more sense --- openpype/pipeline/colorspace.py | 12 ++++++------ tests/unit/openpype/pipeline/test_colorspace.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index ae16c13635..454c23a55e 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -24,7 +24,7 @@ log = Logger.get_logger(__name__) class CachedData: remapping = None - python3compatible = None + has_compatible_ocio_package = None config_version_data = None ocio_config_colorspaces = {} @@ -459,17 +459,17 @@ def _get_wrapped_with_subprocess(command_group, command, **kwargs): # TODO: this should be part of ocio_wrapper.py def compatibility_check(): """Making sure PyOpenColorIO is importable""" - if CachedData.python3compatible is not None: - return CachedData.python3compatible + if CachedData.has_compatible_ocio_package is not None: + return CachedData.has_compatible_ocio_package try: import PyOpenColorIO # noqa: F401 - CachedData.python3compatible = True + CachedData.has_compatible_ocio_package = True except ImportError: - CachedData.python3compatible = False + CachedData.has_compatible_ocio_package = False # compatible - return CachedData.python3compatible + return CachedData.has_compatible_ocio_package # TODO: this should be part of ocio_wrapper.py diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index 435ea709ab..493be786a3 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -227,7 +227,7 @@ class TestPipelineColorspace(TestPipeline): expected_hiero = "Gamma 2.2 Rec.709 - Texture" # switch to python 2 compatibility mode - colorspace.CachedData.python3compatible = False + colorspace.CachedData.has_compatible_ocio_package = False nuke_colorspace = colorspace.get_colorspace_name_from_filepath( nuke_filepath, From da7ffb84fba2041f7415537e5031485f5835f8e8 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Sep 2023 17:10:27 +0200 Subject: [PATCH 128/267] improving regex patter for underscored pattern --- openpype/pipeline/colorspace.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 454c23a55e..677656c02f 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -341,7 +341,7 @@ def parse_colorspace_from_filepath( pattern = "|".join( # Allow to match spaces also as underscores because the # integrator replaces spaces with underscores in filenames - re.escape(colorspace).replace(r"\ ", r"[_ ]") for colorspace in + re.escape(colorspace) for colorspace in # Sort by longest first so the regex matches longer matches # over smaller matches, e.g. matching 'Output - sRGB' over 'sRGB' sorted(colorspaces, key=len, reverse=True) @@ -355,22 +355,20 @@ def parse_colorspace_from_filepath( colorspace_name = None colorspaces = colorspaces or get_ocio_config_colorspaces(config_path) - underscored_colorspaces = { - key.replace(" ", "_"): key for key in colorspaces + underscored_colorspaces = list({ + key.replace(" ", "_") for key in colorspaces if " " in key - } + }) # match colorspace from filepath - regex_pattern = _get_colorspace_match_regex(colorspaces) + regex_pattern = _get_colorspace_match_regex( + colorspaces + underscored_colorspaces) match = regex_pattern.search(filepath) colorspace = match.group(0) if match else None if colorspace: colorspace_name = colorspace - if colorspace in underscored_colorspaces: - colorspace_name = underscored_colorspaces[colorspace] - if not colorspace_name: log.info("No matching colorspace in config '{}' for path: '{}'".format( config_path, filepath From 6e7cde73be7c3309deb78f10324d8779e354f5cc Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Sep 2023 17:17:45 +0200 Subject: [PATCH 129/267] second part of previous commit --- openpype/pipeline/colorspace.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 677656c02f..1bb4624537 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -353,29 +353,28 @@ def parse_colorspace_from_filepath( "Must provide `config_path` if `colorspaces` is not provided." ) - colorspace_name = None colorspaces = colorspaces or get_ocio_config_colorspaces(config_path) - underscored_colorspaces = list({ - key.replace(" ", "_") for key in colorspaces + underscored_colorspaces = { + key.replace(" ", "_"): key for key in colorspaces if " " in key - }) + } # match colorspace from filepath regex_pattern = _get_colorspace_match_regex( - colorspaces + underscored_colorspaces) + colorspaces + underscored_colorspaces.keys()) match = regex_pattern.search(filepath) colorspace = match.group(0) if match else None if colorspace: - colorspace_name = colorspace + return colorspace - if not colorspace_name: - log.info("No matching colorspace in config '{}' for path: '{}'".format( - config_path, filepath - )) - return None + if colorspace in underscored_colorspaces: + return underscored_colorspaces[colorspace] - return colorspace_name + log.info("No matching colorspace in config '{}' for path: '{}'".format( + config_path, filepath + )) + return None def validate_imageio_colorspace_in_config(config_path, colorspace_name): From d045b8322304dbcbf86e63d7c767561c5f7cd800 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Sep 2023 17:20:57 +0200 Subject: [PATCH 130/267] reversing order for underscored first --- openpype/pipeline/colorspace.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 1bb4624537..4ece96cfff 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -365,12 +365,12 @@ def parse_colorspace_from_filepath( match = regex_pattern.search(filepath) colorspace = match.group(0) if match else None - if colorspace: - return colorspace - if colorspace in underscored_colorspaces: return underscored_colorspaces[colorspace] + if colorspace: + return colorspace + log.info("No matching colorspace in config '{}' for path: '{}'".format( config_path, filepath )) From 49cfebb1639389447d0e4973b0f1d42630beec7d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Sep 2023 17:23:34 +0200 Subject: [PATCH 131/267] log can be added as explicit arg --- openpype/pipeline/colorspace.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 4ece96cfff..a1e86dbd64 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -442,11 +442,7 @@ def _get_wrapped_with_subprocess(command_group, command, **kwargs): log.info("Executing: {}".format(" ".join(args))) - process_kwargs = { - "logger": log - } - - run_openpype_process(*args, **process_kwargs) + run_openpype_process(*args, logger=log) # return all colorspaces with open(tmp_json_path, "r") as f_: From aefbc7ef47e46ff91cd85df045801ad7ad6349bd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Sep 2023 17:24:36 +0200 Subject: [PATCH 132/267] removing extra space --- openpype/pipeline/colorspace.py | 8 ++++---- tests/unit/openpype/pipeline/test_colorspace.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index a1e86dbd64..0cc2f35a49 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -24,7 +24,7 @@ log = Logger.get_logger(__name__) class CachedData: remapping = None - has_compatible_ocio_package = None + has_compatible_ocio_package = None config_version_data = None ocio_config_colorspaces = {} @@ -452,14 +452,14 @@ def _get_wrapped_with_subprocess(command_group, command, **kwargs): # TODO: this should be part of ocio_wrapper.py def compatibility_check(): """Making sure PyOpenColorIO is importable""" - if CachedData.has_compatible_ocio_package is not None: + if CachedData.has_compatible_ocio_package is not None: return CachedData.has_compatible_ocio_package try: import PyOpenColorIO # noqa: F401 - CachedData.has_compatible_ocio_package = True + CachedData.has_compatible_ocio_package = True except ImportError: - CachedData.has_compatible_ocio_package = False + CachedData.has_compatible_ocio_package = False # compatible return CachedData.has_compatible_ocio_package diff --git a/tests/unit/openpype/pipeline/test_colorspace.py b/tests/unit/openpype/pipeline/test_colorspace.py index 493be786a3..85faa8ff5d 100644 --- a/tests/unit/openpype/pipeline/test_colorspace.py +++ b/tests/unit/openpype/pipeline/test_colorspace.py @@ -227,7 +227,7 @@ class TestPipelineColorspace(TestPipeline): expected_hiero = "Gamma 2.2 Rec.709 - Texture" # switch to python 2 compatibility mode - colorspace.CachedData.has_compatible_ocio_package = False + colorspace.CachedData.has_compatible_ocio_package = False nuke_colorspace = colorspace.get_colorspace_name_from_filepath( nuke_filepath, From 89d3a3ae228c8ed4f8a1e9b7e5e6cea2c6b65539 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Sep 2023 17:28:27 +0200 Subject: [PATCH 133/267] caching per config path compatibility check --- openpype/pipeline/colorspace.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 0cc2f35a49..446849b76e 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -25,7 +25,7 @@ log = Logger.get_logger(__name__) class CachedData: remapping = None has_compatible_ocio_package = None - config_version_data = None + config_version_data = {} ocio_config_colorspaces = {} @@ -469,26 +469,26 @@ def compatibility_check(): def compatibility_check_config_version(config_path, major=1, minor=None): """Making sure PyOpenColorIO config version is compatible""" - if not CachedData.config_version_data: + if not CachedData.config_version_data.get(config_path): if compatibility_check(): # TODO: refactor this so it is not imported but part of this file from openpype.scripts.ocio_wrapper import _get_version_data - CachedData.config_version_data = _get_version_data(config_path) + CachedData.config_version_data[config_path] = _get_version_data(config_path) else: # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess - CachedData.config_version_data = _get_wrapped_with_subprocess( + CachedData.config_version_data[config_path] = _get_wrapped_with_subprocess( "config", "get_version", config_path=config_path ) # check major version - if CachedData.config_version_data["major"] != major: + if CachedData.config_version_data[config_path]["major"] != major: return False # check minor version - if minor and CachedData.config_version_data["minor"] != minor: + if minor and CachedData.config_version_data[config_path]["minor"] != minor: return False # compatible From 2f89cadd8a82fb1590a2bd064c24ddc27e42e893 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Sep 2023 17:29:54 +0200 Subject: [PATCH 134/267] hound --- openpype/pipeline/colorspace.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 446849b76e..2dd618a1f2 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -474,14 +474,16 @@ def compatibility_check_config_version(config_path, major=1, minor=None): # TODO: refactor this so it is not imported but part of this file from openpype.scripts.ocio_wrapper import _get_version_data - CachedData.config_version_data[config_path] = _get_version_data(config_path) + CachedData.config_version_data[config_path] = \ + _get_version_data(config_path) else: # python environment is not compatible with PyOpenColorIO # needs to be run in subprocess - CachedData.config_version_data[config_path] = _get_wrapped_with_subprocess( - "config", "get_version", config_path=config_path - ) + CachedData.config_version_data[config_path] = \ + _get_wrapped_with_subprocess( + "config", "get_version", config_path=config_path + ) # check major version if CachedData.config_version_data[config_path]["major"] != major: From da1d62f8931d38b2cae119e655575b67db69dba7 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 21 Sep 2023 17:31:21 +0200 Subject: [PATCH 135/267] hound --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 2dd618a1f2..44cff34c67 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -483,7 +483,7 @@ def compatibility_check_config_version(config_path, major=1, minor=None): CachedData.config_version_data[config_path] = \ _get_wrapped_with_subprocess( "config", "get_version", config_path=config_path - ) + ) # check major version if CachedData.config_version_data[config_path]["major"] != major: From 174ef45b0b068ccfc9382a254af64ea0d4c5b009 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Sep 2023 15:07:58 +0800 Subject: [PATCH 136/267] jakub's comment on apply_settings and fix the bug of not being extracted the review --- openpype/hosts/nuke/api/plugin.py | 11 ++- ...ov.py => extract_review_baking_streams.py} | 30 +++++++- .../nuke/server/settings/publish_plugins.py | 71 +++++++++++++++++++ server_addon/nuke/server/version.py | 2 +- 4 files changed, 109 insertions(+), 5 deletions(-) rename openpype/hosts/nuke/plugins/publish/{extract_review_data_mov.py => extract_review_baking_streams.py} (82%) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index adbe43e481..a814615164 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -804,8 +804,13 @@ class ExporterReviewMov(ExporterReview): self.log.info("File info was set...") self.file = self.fhead + self.name + ".{}".format(self.ext) - if self.ext != VIDEO_EXTENSIONS: - self.file = os.path.basename(self.path_in) + if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: + filename = os.path.basename(self.path_in) + self.file = filename + if ".{}".format(self.ext) not in self.file: + wrg_ext = filename.split(".")[-1] + self.file = filename.replace(wrg_ext, self.ext) + self.path = os.path.join( self.staging_dir, self.file).replace("\\", "/") @@ -926,7 +931,7 @@ class ExporterReviewMov(ExporterReview): self.log.debug("Path: {}".format(self.path)) write_node["file"].setValue(str(self.path)) write_node["file_type"].setValue(str(self.ext)) - + self.log.debug("{0}".format(self.ext)) # Knobs `meta_codec` and `mov64_codec` are not available on centos. # TODO shouldn't this come from settings on outputs? try: diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py b/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py similarity index 82% rename from openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py rename to openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py index 1568a2de9b..59a3f659c9 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data_mov.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py @@ -16,7 +16,7 @@ class ExtractReviewDataBakingStreams(publish.Extractor): """ order = pyblish.api.ExtractorOrder + 0.01 - label = "Extract Review Data Mov" + label = "Extract Review Data Baking Streams" families = ["review"] hosts = ["nuke"] @@ -25,6 +25,34 @@ class ExtractReviewDataBakingStreams(publish.Extractor): viewer_lut_raw = None outputs = {} + @classmethod + def apply_settings(cls, project_settings): + """just in case there are some old presets + in deprecrated ExtractReviewDataMov Plugins + """ + nuke_publish = project_settings["nuke"]["publish"] + deprecrated_review_settings = nuke_publish["ExtractReviewDataMov"] + current_review_settings = ( + nuke_publish["ExtractReviewDataBakingStreams"] + ) + if deprecrated_review_settings["viewer_lut_raw"] == ( + current_review_settings["viewer_lut_raw"] + ): + cls.viewer_lut_raw = ( + current_review_settings["viewer_lut_raw"] + ) + else: + cls.viewer_lut_raw = ( + deprecrated_review_settings["viewer_lut_raw"] + ) + + if deprecrated_review_settings["outputs"] == ( + current_review_settings["outputs"] + ): + cls.outputs = current_review_settings["outputs"] + else: + cls.outputs = deprecrated_review_settings["outputs"] + def process(self, instance): families = set(instance.data["families"]) diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 423448219d..6459dd7225 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -165,6 +165,18 @@ class BakingStreamModel(BaseSettingsModel): title="Custom tags", default_factory=list) +class ExtractReviewDataMovModel(BaseSettingsModel): + """[deprecated] use Extract Review Data Baking + Streams instead. + """ + enabled: bool = Field(title="Enabled") + viewer_lut_raw: bool = Field(title="Viewer lut raw") + outputs: list[BakingStreamModel] = Field( + default_factory=list, + title="Baking streams" + ) + + class ExtractReviewBakingStreamsModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") viewer_lut_raw: bool = Field(title="Viewer lut raw") @@ -266,6 +278,10 @@ class PublishPuginsModel(BaseSettingsModel): title="Extract Review Data Lut", default_factory=ExtractReviewDataLutModel ) + ExtractReviewDataMov: ExtractReviewDataMovModel = Field( + title="Extract Review Data Mov", + default_factory=ExtractReviewDataMovModel + ) ExtractReviewDataBakingStreams: ExtractReviewBakingStreamsModel = Field( title="Extract Review Data Baking Streams", default_factory=ExtractReviewBakingStreamsModel @@ -410,6 +426,61 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { "ExtractReviewDataLut": { "enabled": False }, + "ExtractReviewDataMov": { + "enabled": True, + "viewer_lut_raw": False, + "outputs": [ + { + "name": "baking", + "filter": { + "task_types": [], + "product_types": [], + "product_names": [] + }, + "read_raw": False, + "viewer_process_override": "", + "bake_viewer_process": True, + "bake_viewer_input_process": True, + "reformat_nodes_config": { + "enabled": False, + "reposition_nodes": [ + { + "node_class": "Reformat", + "knobs": [ + { + "type": "text", + "name": "type", + "text": "to format" + }, + { + "type": "text", + "name": "format", + "text": "HD_1080" + }, + { + "type": "text", + "name": "filter", + "text": "Lanczos6" + }, + { + "type": "bool", + "name": "black_outside", + "boolean": True + }, + { + "type": "bool", + "name": "pbb", + "boolean": False + } + ] + } + ] + }, + "extension": "mov", + "add_custom_tags": [] + } + ] + }, "ExtractReviewDataBakingStreams": { "enabled": True, "viewer_lut_raw": False, diff --git a/server_addon/nuke/server/version.py b/server_addon/nuke/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/nuke/server/version.py +++ b/server_addon/nuke/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" From 3cf203e46580d88c842bd931177ebdb159690f89 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Sep 2023 10:48:31 +0200 Subject: [PATCH 137/267] AYON settings: Extract OIIO transcode settings (#5639) * added name to ExtractOIIOTranscode output definition * convert outputs of 'ExtractOIIOTranscode' to 'dict' --- openpype/settings/ayon_settings.py | 23 ++++++++++++++++++- .../core/server/settings/publish_plugins.py | 7 ++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index 9a4f0607e0..3be8ac8ae5 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -1102,7 +1102,7 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): "studio_name", "studio_code", ): - ayon_core.pop(key) + ayon_core.pop(key, None) # Publish conversion ayon_publish = ayon_core["publish"] @@ -1140,6 +1140,27 @@ def _convert_global_project_settings(ayon_settings, output, default_settings): profile["outputs"] = new_outputs + # ExtractOIIOTranscode plugin + extract_oiio_transcode = ayon_publish["ExtractOIIOTranscode"] + extract_oiio_transcode_profiles = extract_oiio_transcode["profiles"] + for profile in extract_oiio_transcode_profiles: + new_outputs = {} + name_counter = {} + for output in profile["outputs"]: + if "name" in output: + name = output.pop("name") + else: + # Backwards compatibility for setting without 'name' in model + name = output["extension"] + if name in new_outputs: + name_counter[name] += 1 + name = "{}_{}".format(name, name_counter[name]) + else: + name_counter[name] = 0 + + new_outputs[name] = output + profile["outputs"] = new_outputs + # Extract Burnin plugin extract_burnin = ayon_publish["ExtractBurnin"] extract_burnin_options = extract_burnin["options"] diff --git a/server_addon/core/server/settings/publish_plugins.py b/server_addon/core/server/settings/publish_plugins.py index c012312579..69a759465e 100644 --- a/server_addon/core/server/settings/publish_plugins.py +++ b/server_addon/core/server/settings/publish_plugins.py @@ -116,6 +116,8 @@ class OIIOToolArgumentsModel(BaseSettingsModel): class ExtractOIIOTranscodeOutputModel(BaseSettingsModel): + _layout = "expanded" + name: str = Field("", title="Name") extension: str = Field("", title="Extension") transcoding_type: str = Field( "colorspace", @@ -164,6 +166,11 @@ class ExtractOIIOTranscodeProfileModel(BaseSettingsModel): title="Output Definitions", ) + @validator("outputs") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + class ExtractOIIOTranscodeModel(BaseSettingsModel): enabled: bool = Field(True) From 87ed2f960daa97d4d94b73403c5076519bf6b20c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Sep 2023 11:47:39 +0200 Subject: [PATCH 138/267] Launcher tool: Refactor launcher tool (for AYON) (#5612) * added helper classes to utils * implemented base of ayon utils * initial commit for launcher tool * use image for extender * actions are shown and can be triggered * fix actions on finished refresh * refresh automatically * fix re-refreshing of projects model * added page slide animation * updated abstrack classes * change how icon is prepared * fix actions sorting * show messages like in launcher tool * do not clear items on refresh * stop refresh timer only on close event * use Ynput/AYON for local settings json * register default actions in launcher action module * change register naming * move 'SquareButton' to utils widgets * removed duplicated method * removed unused variable * removed unused import * don't use lambda * swap default name for 'OpenPypeSettingsRegistry' * Change support version --- openpype/lib/local_settings.py | 14 +- openpype/modules/launcher_action.py | 73 ++- openpype/pipeline/actions.py | 8 +- openpype/tools/ayon_launcher/abstract.py | 297 ++++++++++ openpype/tools/ayon_launcher/control.py | 149 ++++++ .../tools/ayon_launcher/models/__init__.py | 8 + .../tools/ayon_launcher/models/actions.py | 505 ++++++++++++++++++ .../tools/ayon_launcher/models/selection.py | 72 +++ openpype/tools/ayon_launcher/ui/__init__.py | 6 + .../tools/ayon_launcher/ui/actions_widget.py | 453 ++++++++++++++++ .../tools/ayon_launcher/ui/hierarchy_page.py | 102 ++++ .../tools/ayon_launcher/ui/projects_widget.py | 135 +++++ .../ayon_launcher/ui/resources/__init__.py | 7 + .../ayon_launcher/ui/resources/options.png | Bin 0 -> 1772 bytes openpype/tools/ayon_launcher/ui/window.py | 295 ++++++++++ openpype/tools/ayon_utils/models/__init__.py | 29 + openpype/tools/ayon_utils/models/cache.py | 196 +++++++ openpype/tools/ayon_utils/models/hierarchy.py | 340 ++++++++++++ openpype/tools/ayon_utils/models/projects.py | 145 +++++ openpype/tools/ayon_utils/widgets/__init__.py | 37 ++ .../ayon_utils/widgets/folders_widget.py | 364 +++++++++++++ .../ayon_utils/widgets/projects_widget.py | 325 +++++++++++ .../tools/ayon_utils/widgets/tasks_widget.py | 436 +++++++++++++++ openpype/tools/ayon_utils/widgets/utils.py | 98 ++++ openpype/tools/launcher/actions.py | 44 +- openpype/tools/utils/__init__.py | 9 + openpype/tools/utils/widgets.py | 79 ++- 27 files changed, 4158 insertions(+), 68 deletions(-) create mode 100644 openpype/tools/ayon_launcher/abstract.py create mode 100644 openpype/tools/ayon_launcher/control.py create mode 100644 openpype/tools/ayon_launcher/models/__init__.py create mode 100644 openpype/tools/ayon_launcher/models/actions.py create mode 100644 openpype/tools/ayon_launcher/models/selection.py create mode 100644 openpype/tools/ayon_launcher/ui/__init__.py create mode 100644 openpype/tools/ayon_launcher/ui/actions_widget.py create mode 100644 openpype/tools/ayon_launcher/ui/hierarchy_page.py create mode 100644 openpype/tools/ayon_launcher/ui/projects_widget.py create mode 100644 openpype/tools/ayon_launcher/ui/resources/__init__.py create mode 100644 openpype/tools/ayon_launcher/ui/resources/options.png create mode 100644 openpype/tools/ayon_launcher/ui/window.py create mode 100644 openpype/tools/ayon_utils/models/__init__.py create mode 100644 openpype/tools/ayon_utils/models/cache.py create mode 100644 openpype/tools/ayon_utils/models/hierarchy.py create mode 100644 openpype/tools/ayon_utils/models/projects.py create mode 100644 openpype/tools/ayon_utils/widgets/__init__.py create mode 100644 openpype/tools/ayon_utils/widgets/folders_widget.py create mode 100644 openpype/tools/ayon_utils/widgets/projects_widget.py create mode 100644 openpype/tools/ayon_utils/widgets/tasks_widget.py create mode 100644 openpype/tools/ayon_utils/widgets/utils.py diff --git a/openpype/lib/local_settings.py b/openpype/lib/local_settings.py index 3fb35a7e7b..dae6e074af 100644 --- a/openpype/lib/local_settings.py +++ b/openpype/lib/local_settings.py @@ -494,10 +494,18 @@ class OpenPypeSettingsRegistry(JSONSettingRegistry): """ def __init__(self, name=None): - self.vendor = "pypeclub" - self.product = "openpype" + if AYON_SERVER_ENABLED: + vendor = "Ynput" + product = "AYON" + default_name = "AYON_settings" + else: + vendor = "pypeclub" + product = "openpype" + default_name = "openpype_settings" + self.vendor = vendor + self.product = product if not name: - name = "openpype_settings" + name = default_name path = appdirs.user_data_dir(self.product, self.vendor) super(OpenPypeSettingsRegistry, self).__init__(name, path) diff --git a/openpype/modules/launcher_action.py b/openpype/modules/launcher_action.py index c4331b6094..5e14f25f76 100644 --- a/openpype/modules/launcher_action.py +++ b/openpype/modules/launcher_action.py @@ -1,3 +1,6 @@ +import os + +from openpype import PLUGINS_DIR, AYON_SERVER_ENABLED from openpype.modules import ( OpenPypeModule, ITrayAction, @@ -13,36 +16,66 @@ class LauncherAction(OpenPypeModule, ITrayAction): self.enabled = True # Tray attributes - self.window = None + self._window = None def tray_init(self): - self.create_window() + self._create_window() - self.add_doubleclick_callback(self.show_launcher) + self.add_doubleclick_callback(self._show_launcher) def tray_start(self): return def connect_with_modules(self, enabled_modules): # Register actions - if self.tray_initialized: - from openpype.tools.launcher import actions - actions.register_config_actions() - actions_paths = self.manager.collect_plugin_paths()["actions"] - actions.register_actions_from_paths(actions_paths) - actions.register_environment_actions() - - def create_window(self): - if self.window: + if not self.tray_initialized: return - from openpype.tools.launcher import LauncherWindow - self.window = LauncherWindow() + + from openpype.pipeline.actions import register_launcher_action_path + + actions_dir = os.path.join(PLUGINS_DIR, "actions") + if os.path.exists(actions_dir): + register_launcher_action_path(actions_dir) + + actions_paths = self.manager.collect_plugin_paths()["actions"] + for path in actions_paths: + if path and os.path.exists(path): + register_launcher_action_path(actions_dir) + + paths_str = os.environ.get("AVALON_ACTIONS") or "" + if paths_str: + self.log.warning( + "WARNING: 'AVALON_ACTIONS' is deprecated. Support of this" + " environment variable will be removed in future versions." + " Please consider using 'OpenPypeModule' to define custom" + " action paths. Planned version to drop the support" + " is 3.17.2 or 3.18.0 ." + ) + + for path in paths_str.split(os.pathsep): + if path and os.path.exists(path): + register_launcher_action_path(path) def on_action_trigger(self): - self.show_launcher() + """Implementation for ITrayAction interface. - def show_launcher(self): - if self.window: - self.window.show() - self.window.raise_() - self.window.activateWindow() + Show launcher tool on action trigger. + """ + + self._show_launcher() + + def _create_window(self): + if self._window: + return + if AYON_SERVER_ENABLED: + from openpype.tools.ayon_launcher.ui import LauncherWindow + else: + from openpype.tools.launcher import LauncherWindow + self._window = LauncherWindow() + + def _show_launcher(self): + if self._window is None: + return + self._window.show() + self._window.raise_() + self._window.activateWindow() diff --git a/openpype/pipeline/actions.py b/openpype/pipeline/actions.py index b488fe3e1f..feb1bd05d2 100644 --- a/openpype/pipeline/actions.py +++ b/openpype/pipeline/actions.py @@ -20,7 +20,13 @@ class LauncherAction(object): log.propagate = True def is_compatible(self, session): - """Return whether the class is compatible with the Session.""" + """Return whether the class is compatible with the Session. + + Args: + session (dict[str, Union[str, None]]): Session data with + AVALON_PROJECT, AVALON_ASSET and AVALON_TASK. + """ + return True def process(self, session, **kwargs): diff --git a/openpype/tools/ayon_launcher/abstract.py b/openpype/tools/ayon_launcher/abstract.py new file mode 100644 index 0000000000..00502fe930 --- /dev/null +++ b/openpype/tools/ayon_launcher/abstract.py @@ -0,0 +1,297 @@ +from abc import ABCMeta, abstractmethod + +import six + + +@six.add_metaclass(ABCMeta) +class AbstractLauncherCommon(object): + @abstractmethod + def register_event_callback(self, topic, callback): + """Register event callback. + + Listen for events with given topic. + + Args: + topic (str): Name of topic. + callback (Callable): Callback that will be called when event + is triggered. + """ + + pass + + +class AbstractLauncherBackend(AbstractLauncherCommon): + @abstractmethod + def emit_event(self, topic, data=None, source=None): + """Emit event. + + Args: + topic (str): Event topic used for callbacks filtering. + data (Optional[dict[str, Any]]): Event data. + source (Optional[str]): Event source. + """ + + pass + + @abstractmethod + def get_project_settings(self, project_name): + """Project settings for current project. + + Args: + project_name (Union[str, None]): Project name. + + Returns: + dict[str, Any]: Project settings. + """ + + pass + + @abstractmethod + def get_project_entity(self, project_name): + """Get project entity by name. + + Args: + project_name (str): Project name. + + Returns: + dict[str, Any]: Project entity data. + """ + + pass + + @abstractmethod + def get_folder_entity(self, project_name, folder_id): + """Get folder entity by id. + + Args: + project_name (str): Project name. + folder_id (str): Folder id. + + Returns: + dict[str, Any]: Folder entity data. + """ + + pass + + @abstractmethod + def get_task_entity(self, project_name, task_id): + """Get task entity by id. + + Args: + project_name (str): Project name. + task_id (str): Task id. + + Returns: + dict[str, Any]: Task entity data. + """ + + pass + + +class AbstractLauncherFrontEnd(AbstractLauncherCommon): + # Entity items for UI + @abstractmethod + def get_project_items(self, sender=None): + """Project items for all projects. + + This function may trigger events 'projects.refresh.started' and + 'projects.refresh.finished' which will contain 'sender' value in data. + That may help to avoid re-refresh of project items in UI elements. + + Args: + sender (str): Who requested folder items. + + Returns: + list[ProjectItem]: Minimum possible information needed + for visualisation of folder hierarchy. + """ + + pass + + @abstractmethod + def get_folder_items(self, project_name, sender=None): + """Folder items to visualize project hierarchy. + + This function may trigger events 'folders.refresh.started' and + 'folders.refresh.finished' which will contain 'sender' value in data. + That may help to avoid re-refresh of folder items in UI elements. + + Args: + project_name (str): Project name. + sender (str): Who requested folder items. + + Returns: + list[FolderItem]: Minimum possible information needed + for visualisation of folder hierarchy. + """ + + pass + + @abstractmethod + def get_task_items(self, project_name, folder_id, sender=None): + """Task items. + + This function may trigger events 'tasks.refresh.started' and + 'tasks.refresh.finished' which will contain 'sender' value in data. + That may help to avoid re-refresh of task items in UI elements. + + Args: + project_name (str): Project name. + folder_id (str): Folder ID for which are tasks requested. + sender (str): Who requested folder items. + + Returns: + list[TaskItem]: Minimum possible information needed + for visualisation of tasks. + """ + + pass + + @abstractmethod + def get_selected_project_name(self): + """Selected project name. + + Returns: + Union[str, None]: Selected project name. + """ + + pass + + @abstractmethod + def get_selected_folder_id(self): + """Selected folder id. + + Returns: + Union[str, None]: Selected folder id. + """ + + pass + + @abstractmethod + def get_selected_task_id(self): + """Selected task id. + + Returns: + Union[str, None]: Selected task id. + """ + + pass + + @abstractmethod + def get_selected_task_name(self): + """Selected task name. + + Returns: + Union[str, None]: Selected task name. + """ + + pass + + @abstractmethod + def get_selected_context(self): + """Get whole selected context. + + Example: + { + "project_name": self.get_selected_project_name(), + "folder_id": self.get_selected_folder_id(), + "task_id": self.get_selected_task_id(), + "task_name": self.get_selected_task_name(), + } + + Returns: + dict[str, Union[str, None]]: Selected context. + """ + + pass + + @abstractmethod + def set_selected_project(self, project_name): + """Change selected folder. + + Args: + project_name (Union[str, None]): Project nameor None if no project + is selected. + """ + + pass + + @abstractmethod + def set_selected_folder(self, folder_id): + """Change selected folder. + + Args: + folder_id (Union[str, None]): Folder id or None if no folder + is selected. + """ + + pass + + @abstractmethod + def set_selected_task(self, task_id, task_name): + """Change selected task. + + Args: + task_id (Union[str, None]): Task id or None if no task + is selected. + task_name (Union[str, None]): Task name or None if no task + is selected. + """ + + pass + + # Actions + @abstractmethod + def get_action_items(self, project_name, folder_id, task_id): + """Get action items for given context. + + Args: + project_name (Union[str, None]): Project name. + folder_id (Union[str, None]): Folder id. + task_id (Union[str, None]): Task id. + + Returns: + list[ActionItem]: List of action items that should be shown + for given context. + """ + + pass + + @abstractmethod + def trigger_action(self, project_name, folder_id, task_id, action_id): + """Trigger action on given context. + + Args: + 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. + """ + + pass + + @abstractmethod + def set_application_force_not_open_workfile( + self, project_name, folder_id, task_id, action_id, enabled + ): + """This is application action related to force not open last workfile. + + Args: + 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. + enabled (bool): New value of force not open workfile. + """ + + pass + + @abstractmethod + def refresh(self): + """Refresh everything, models, ui etc. + + Triggers 'controller.refresh.started' event at the beginning and + 'controller.refresh.finished' at the end. + """ + + pass diff --git a/openpype/tools/ayon_launcher/control.py b/openpype/tools/ayon_launcher/control.py new file mode 100644 index 0000000000..09e07893c3 --- /dev/null +++ b/openpype/tools/ayon_launcher/control.py @@ -0,0 +1,149 @@ +from openpype.lib import Logger +from openpype.lib.events import QueuedEventSystem +from openpype.settings import get_project_settings +from openpype.tools.ayon_utils.models import ProjectsModel, HierarchyModel + +from .abstract import AbstractLauncherFrontEnd, AbstractLauncherBackend +from .models import LauncherSelectionModel, ActionsModel + + +class BaseLauncherController( + AbstractLauncherFrontEnd, AbstractLauncherBackend +): + def __init__(self): + self._project_settings = {} + self._event_system = None + self._log = None + + self._selection_model = LauncherSelectionModel(self) + self._projects_model = ProjectsModel(self) + self._hierarchy_model = HierarchyModel(self) + self._actions_model = ActionsModel(self) + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + @property + def event_system(self): + """Inner event system for workfiles tool controller. + + Is used for communication with UI. Event system is created on demand. + + Returns: + QueuedEventSystem: Event system which can trigger callbacks + for topics. + """ + + if self._event_system is None: + self._event_system = QueuedEventSystem() + return self._event_system + + # --------------------------------- + # Implementation of abstract methods + # --------------------------------- + # Events system + def emit_event(self, topic, data=None, source=None): + """Use implemented event system to trigger event.""" + + if data is None: + data = {} + self.event_system.emit(topic, data, source) + + def register_event_callback(self, topic, callback): + self.event_system.add_callback(topic, callback) + + # Entity items for UI + def get_project_items(self, sender=None): + return self._projects_model.get_project_items(sender) + + def get_folder_items(self, project_name, sender=None): + return self._hierarchy_model.get_folder_items(project_name, sender) + + def get_task_items(self, project_name, folder_id, sender=None): + return self._hierarchy_model.get_task_items( + project_name, folder_id, sender) + + # Project settings for applications actions + def get_project_settings(self, project_name): + if project_name in self._project_settings: + return self._project_settings[project_name] + settings = get_project_settings(project_name) + self._project_settings[project_name] = settings + return settings + + # Entity for backend + def get_project_entity(self, project_name): + return self._projects_model.get_project_entity(project_name) + + def get_folder_entity(self, project_name, folder_id): + return self._hierarchy_model.get_folder_entity( + project_name, folder_id) + + def get_task_entity(self, project_name, task_id): + return self._hierarchy_model.get_task_entity(project_name, task_id) + + # Selection methods + def get_selected_project_name(self): + return self._selection_model.get_selected_project_name() + + def set_selected_project(self, project_name): + self._selection_model.set_selected_project(project_name) + + def get_selected_folder_id(self): + return self._selection_model.get_selected_folder_id() + + def set_selected_folder(self, folder_id): + self._selection_model.set_selected_folder(folder_id) + + def get_selected_task_id(self): + return self._selection_model.get_selected_task_id() + + def get_selected_task_name(self): + return self._selection_model.get_selected_task_name() + + def set_selected_task(self, task_id, task_name): + self._selection_model.set_selected_task(task_id, task_name) + + def get_selected_context(self): + return { + "project_name": self.get_selected_project_name(), + "folder_id": self.get_selected_folder_id(), + "task_id": self.get_selected_task_id(), + "task_name": self.get_selected_task_name(), + } + + # Actions + def get_action_items(self, project_name, folder_id, task_id): + return self._actions_model.get_action_items( + project_name, folder_id, task_id) + + def set_application_force_not_open_workfile( + self, project_name, folder_id, task_id, action_id, enabled + ): + self._actions_model.set_application_force_not_open_workfile( + project_name, folder_id, task_id, action_id, enabled + ) + + def trigger_action(self, project_name, folder_id, task_id, identifier): + self._actions_model.trigger_action( + project_name, folder_id, task_id, identifier) + + # General methods + def refresh(self): + self._emit_event("controller.refresh.started") + + self._project_settings = {} + + self._projects_model.reset() + self._hierarchy_model.reset() + + self._actions_model.refresh() + self._projects_model.refresh() + + self._emit_event("controller.refresh.finished") + + def _emit_event(self, topic, data=None): + self.emit_event(topic, data, "controller") diff --git a/openpype/tools/ayon_launcher/models/__init__.py b/openpype/tools/ayon_launcher/models/__init__.py new file mode 100644 index 0000000000..1bc60c85f0 --- /dev/null +++ b/openpype/tools/ayon_launcher/models/__init__.py @@ -0,0 +1,8 @@ +from .actions import ActionsModel +from .selection import LauncherSelectionModel + + +__all__ = ( + "ActionsModel", + "LauncherSelectionModel", +) diff --git a/openpype/tools/ayon_launcher/models/actions.py b/openpype/tools/ayon_launcher/models/actions.py new file mode 100644 index 0000000000..24fea44db2 --- /dev/null +++ b/openpype/tools/ayon_launcher/models/actions.py @@ -0,0 +1,505 @@ +import os + +from openpype import resources +from openpype.lib import Logger, OpenPypeSettingsRegistry +from openpype.pipeline.actions import ( + discover_launcher_actions, + LauncherAction, +) + + +# class Action: +# def __init__(self, label, icon=None, identifier=None): +# self._label = label +# self._icon = icon +# self._callbacks = [] +# self._identifier = identifier or uuid.uuid4().hex +# self._checked = True +# self._checkable = False +# +# def set_checked(self, checked): +# self._checked = checked +# +# def set_checkable(self, checkable): +# self._checkable = checkable +# +# def set_label(self, label): +# self._label = label +# +# def add_callback(self, callback): +# self._callbacks = callback +# +# +# class Menu: +# def __init__(self, label, icon=None): +# self.label = label +# self.icon = icon +# self._actions = [] +# +# def add_action(self, action): +# self._actions.append(action) + + +class ApplicationAction(LauncherAction): + """Action to launch an application. + + Application action based on 'ApplicationManager' system. + + Handling of applications in launcher is not ideal and should be completely + redone from scratch. This is just a temporary solution to keep backwards + compatibility with OpenPype launcher. + + Todos: + Move handling of errors to frontend. + """ + + # Application object + application = None + # Action attributes + name = None + label = None + label_variant = None + group = None + icon = None + color = None + order = 0 + data = {} + project_settings = {} + project_entities = {} + + _log = None + required_session_keys = ( + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK" + ) + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + def is_compatible(self, session): + for key in self.required_session_keys: + if not session.get(key): + return False + + project_name = session["AVALON_PROJECT"] + project_entity = self.project_entities[project_name] + apps = project_entity["attrib"].get("applications") + if not apps or self.application.full_name not in apps: + return False + + project_settings = self.project_settings[project_name] + only_available = project_settings["applications"]["only_available"] + if only_available and not self.application.find_executable(): + return False + return True + + def _show_message_box(self, title, message, details=None): + from qtpy import QtWidgets, QtGui + from openpype import style + + dialog = QtWidgets.QMessageBox() + icon = QtGui.QIcon(resources.get_openpype_icon_filepath()) + dialog.setWindowIcon(icon) + dialog.setStyleSheet(style.load_stylesheet()) + dialog.setWindowTitle(title) + dialog.setText(message) + if details: + dialog.setDetailedText(details) + dialog.exec_() + + def process(self, session, **kwargs): + """Process the full Application action""" + + from openpype.lib import ( + ApplictionExecutableNotFound, + ApplicationLaunchFailed, + ) + + project_name = session["AVALON_PROJECT"] + asset_name = session["AVALON_ASSET"] + task_name = session["AVALON_TASK"] + try: + self.application.launch( + project_name=project_name, + asset_name=asset_name, + task_name=task_name, + **self.data + ) + + except ApplictionExecutableNotFound as exc: + details = exc.details + msg = exc.msg + log_msg = str(msg) + if details: + log_msg += "\n" + details + self.log.warning(log_msg) + self._show_message_box( + "Application executable not found", msg, details + ) + + except ApplicationLaunchFailed as exc: + msg = str(exc) + self.log.warning(msg, exc_info=True) + self._show_message_box("Application launch failed", msg) + + +class ActionItem: + """Item representing single action to trigger. + + Todos: + Get rid of application specific logic. + + Args: + identifier (str): Unique identifier of action item. + label (str): Action label. + variant_label (Union[str, None]): Variant label, full label is + concatenated with space. Actions are grouped under single + action if it has same 'label' and have set 'variant_label'. + icon (dict[str, str]): Icon definition. + order (int): Action ordering. + is_application (bool): Is action application action. + force_not_open_workfile (bool): Force not open workfile. Application + related. + full_label (Optional[str]): Full label, if not set it is generated + from 'label' and 'variant_label'. + """ + + def __init__( + self, + identifier, + label, + variant_label, + icon, + order, + is_application, + force_not_open_workfile, + full_label=None + ): + self.identifier = identifier + self.label = label + self.variant_label = variant_label + self.icon = icon + self.order = order + self.is_application = is_application + self.force_not_open_workfile = force_not_open_workfile + self._full_label = full_label + + def copy(self): + return self.from_data(self.to_data()) + + @property + def full_label(self): + if self._full_label is None: + if self.variant_label: + self._full_label = " ".join([self.label, self.variant_label]) + else: + self._full_label = self.label + return self._full_label + + def to_data(self): + return { + "identifier": self.identifier, + "label": self.label, + "variant_label": self.variant_label, + "icon": self.icon, + "order": self.order, + "is_application": self.is_application, + "force_not_open_workfile": self.force_not_open_workfile, + "full_label": self._full_label, + } + + @classmethod + def from_data(cls, data): + return cls(**data) + + +def get_action_icon(action): + """Get action icon info. + + Args: + action (LacunherAction): Action instance. + + Returns: + dict[str, str]: Icon info. + """ + + icon = action.icon + if not icon: + return { + "type": "awesome-font", + "name": "fa.cube", + "color": "white" + } + + if isinstance(icon, dict): + return icon + + icon_path = resources.get_resource(icon) + if not os.path.exists(icon_path): + try: + icon_path = icon.format(resources.RESOURCES_DIR) + except Exception: + pass + + if os.path.exists(icon_path): + return { + "type": "path", + "path": icon_path, + } + + return { + "type": "awesome-font", + "name": icon, + "color": action.color or "white" + } + + +class ActionsModel: + """Actions model. + + Args: + controller (AbstractLauncherBackend): Controller instance. + """ + + _not_open_workfile_reg_key = "force_not_open_workfile" + + def __init__(self, controller): + self._controller = controller + + self._log = None + + self._discovered_actions = None + self._actions = None + self._action_items = {} + + self._launcher_tool_reg = OpenPypeSettingsRegistry("launcher_tool") + + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + + def refresh(self): + self._discovered_actions = None + self._actions = None + self._action_items = {} + + self._controller.emit_event("actions.refresh.started") + self._get_action_objects() + self._controller.emit_event("actions.refresh.finished") + + def get_action_items(self, project_name, folder_id, task_id): + """Get actions for project. + + Args: + project_name (Union[str, None]): Project name. + folder_id (Union[str, None]): Folder id. + task_id (Union[str, None]): Task id. + + Returns: + list[ActionItem]: List of actions. + """ + + not_open_workfile_actions = self._get_no_last_workfile_for_context( + project_name, folder_id, task_id) + session = self._prepare_session(project_name, folder_id, task_id) + output = [] + action_items = self._get_action_items(project_name) + for identifier, action in self._get_action_objects().items(): + if not action.is_compatible(session): + continue + + action_item = action_items[identifier] + # Handling of 'force_not_open_workfile' for applications + if action_item.is_application: + action_item = action_item.copy() + action_item.force_not_open_workfile = ( + not_open_workfile_actions.get(identifier, False) + ) + + output.append(action_item) + return output + + def set_application_force_not_open_workfile( + self, project_name, folder_id, task_id, action_id, 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 + self._launcher_tool_reg.set_item( + self._not_open_workfile_reg_key, no_workfile_reg_data + ) + + def trigger_action(self, project_name, folder_id, task_id, identifier): + session = self._prepare_session(project_name, folder_id, task_id) + failed = False + error_message = None + action_label = identifier + action_items = self._get_action_items(project_name) + try: + action = self._actions[identifier] + action_item = action_items[identifier] + action_label = action_item.full_label + self._controller.emit_event( + "action.trigger.started", + { + "identifier": identifier, + "full_label": action_label, + } + ) + if isinstance(action, ApplicationAction): + per_action = self._get_no_last_workfile_for_context( + project_name, folder_id, task_id + ) + force_not_open_workfile = per_action.get(identifier, False) + action.data["start_last_workfile"] = force_not_open_workfile + action.process(session) + except Exception as exc: + self.log.warning("Action trigger failed.", exc_info=True) + failed = True + error_message = str(exc) + + self._controller.emit_event( + "action.trigger.finished", + { + "identifier": identifier, + "failed": failed, + "error_message": error_message, + "full_label": action_label, + } + ) + + def _get_no_last_workfile_reg_data(self): + try: + no_workfile_reg_data = self._launcher_tool_reg.get_item( + self._not_open_workfile_reg_key) + except ValueError: + no_workfile_reg_data = {} + self._launcher_tool_reg.set_item( + self._not_open_workfile_reg_key, no_workfile_reg_data) + return no_workfile_reg_data + + def _get_no_last_workfile_for_context( + self, project_name, folder_id, task_id + ): + not_open_workfile_reg_data = self._get_no_last_workfile_reg_data() + return ( + not_open_workfile_reg_data + .get(project_name, {}) + .get(folder_id, {}) + .get(task_id, {}) + ) + + def _prepare_session(self, project_name, folder_id, task_id): + folder_name = None + if folder_id: + folder = self._controller.get_folder_entity( + project_name, folder_id) + if folder: + folder_name = folder["name"] + + task_name = None + if task_id: + task = self._controller.get_task_entity(project_name, task_id) + if task: + task_name = task["name"] + + return { + "AVALON_PROJECT": project_name, + "AVALON_ASSET": folder_name, + "AVALON_TASK": task_name, + } + + def _get_discovered_action_classes(self): + if self._discovered_actions is None: + self._discovered_actions = ( + discover_launcher_actions() + + self._get_applications_action_classes() + ) + return self._discovered_actions + + def _get_action_objects(self): + if self._actions is None: + actions = {} + for cls in self._get_discovered_action_classes(): + obj = cls() + identifier = getattr(obj, "identifier", None) + if identifier is None: + identifier = cls.__name__ + actions[identifier] = obj + self._actions = actions + return self._actions + + def _get_action_items(self, project_name): + action_items = self._action_items.get(project_name) + if action_items is not None: + return action_items + + project_entity = None + if project_name: + project_entity = self._controller.get_project_entity(project_name) + project_settings = self._controller.get_project_settings(project_name) + + action_items = {} + for identifier, action in self._get_action_objects().items(): + is_application = isinstance(action, ApplicationAction) + if is_application: + action.project_entities[project_name] = project_entity + action.project_settings[project_name] = project_settings + label = action.label or identifier + variant_label = getattr(action, "label_variant", None) + icon = get_action_icon(action) + item = ActionItem( + identifier, + label, + variant_label, + icon, + action.order, + is_application, + False + ) + action_items[identifier] = item + self._action_items[project_name] = action_items + return action_items + + def _get_applications_action_classes(self): + from openpype.lib.applications import ( + CUSTOM_LAUNCH_APP_GROUPS, + ApplicationManager, + ) + + actions = [] + + manager = ApplicationManager() + for full_name, application in manager.applications.items(): + if ( + application.group.name in CUSTOM_LAUNCH_APP_GROUPS + or not application.enabled + ): + continue + + action = type( + "app_{}".format(full_name), + (ApplicationAction,), + { + "identifier": "application.{}".format(full_name), + "application": application, + "name": application.name, + "label": application.group.label, + "label_variant": application.label, + "group": None, + "icon": application.icon, + "color": getattr(application, "color", None), + "order": getattr(application, "order", None) or 0, + "data": {} + } + ) + actions.append(action) + return actions diff --git a/openpype/tools/ayon_launcher/models/selection.py b/openpype/tools/ayon_launcher/models/selection.py new file mode 100644 index 0000000000..b156d2084c --- /dev/null +++ b/openpype/tools/ayon_launcher/models/selection.py @@ -0,0 +1,72 @@ +class LauncherSelectionModel(object): + """Model handling selection changes. + + Triggering events: + - "selection.project.changed" + - "selection.folder.changed" + - "selection.task.changed" + """ + + event_source = "launcher.selection.model" + + def __init__(self, controller): + self._controller = controller + + self._project_name = None + self._folder_id = None + self._task_name = None + self._task_id = None + + def get_selected_project_name(self): + return self._project_name + + def set_selected_project(self, project_name): + if project_name == self._project_name: + return + + self._project_name = project_name + self._controller.emit_event( + "selection.project.changed", + {"project_name": project_name}, + self.event_source + ) + + def get_selected_folder_id(self): + return self._folder_id + + def set_selected_folder(self, folder_id): + if folder_id == self._folder_id: + return + + self._folder_id = folder_id + self._controller.emit_event( + "selection.folder.changed", + { + "project_name": self._project_name, + "folder_id": folder_id, + }, + self.event_source + ) + + def get_selected_task_name(self): + return self._task_name + + def get_selected_task_id(self): + return self._task_id + + def set_selected_task(self, task_id, task_name): + if task_id == self._task_id: + return + + self._task_name = task_name + self._task_id = task_id + self._controller.emit_event( + "selection.task.changed", + { + "project_name": self._project_name, + "folder_id": self._folder_id, + "task_name": task_name, + "task_id": task_id, + }, + self.event_source + ) diff --git a/openpype/tools/ayon_launcher/ui/__init__.py b/openpype/tools/ayon_launcher/ui/__init__.py new file mode 100644 index 0000000000..da30c84656 --- /dev/null +++ b/openpype/tools/ayon_launcher/ui/__init__.py @@ -0,0 +1,6 @@ +from .window import LauncherWindow + + +__all__ = ( + "LauncherWindow", +) diff --git a/openpype/tools/ayon_launcher/ui/actions_widget.py b/openpype/tools/ayon_launcher/ui/actions_widget.py new file mode 100644 index 0000000000..d04f8f8d24 --- /dev/null +++ b/openpype/tools/ayon_launcher/ui/actions_widget.py @@ -0,0 +1,453 @@ +import time +import collections + +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.tools.flickcharm import FlickCharm +from openpype.tools.ayon_utils.widgets import get_qt_icon + +from .resources import get_options_image_path + +ANIMATION_LEN = 7 + +ACTION_ID_ROLE = QtCore.Qt.UserRole + 1 +ACTION_IS_APPLICATION_ROLE = QtCore.Qt.UserRole + 2 +ACTION_IS_GROUP_ROLE = QtCore.Qt.UserRole + 3 +ACTION_SORT_ROLE = QtCore.Qt.UserRole + 4 +ANIMATION_START_ROLE = QtCore.Qt.UserRole + 5 +ANIMATION_STATE_ROLE = QtCore.Qt.UserRole + 6 +FORCE_NOT_OPEN_WORKFILE_ROLE = QtCore.Qt.UserRole + 7 + + +class ActionsQtModel(QtGui.QStandardItemModel): + """Qt model for actions. + + Args: + controller (AbstractLauncherFrontEnd): Controller instance. + """ + + refreshed = QtCore.Signal() + + 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, + ) + controller.register_event_callback( + "selection.folder.changed", + self._on_selection_folder_changed, + ) + controller.register_event_callback( + "selection.task.changed", + self._on_selection_task_changed, + ) + + self._controller = controller + + self._items_by_id = {} + self._groups_by_id = {} + + self._selected_project_name = None + self._selected_folder_id = None + self._selected_task_id = None + + def get_selected_project_name(self): + return self._selected_project_name + + def get_selected_folder_id(self): + return self._selected_folder_id + + def get_selected_task_id(self): + return self._selected_task_id + + def get_group_items(self, action_id): + return self._groups_by_id[action_id] + + def get_item_by_id(self, action_id): + return self._items_by_id.get(action_id) + + def _clear_items(self): + self._items_by_id = {} + self._groups_by_id = {} + root = self.invisibleRootItem() + root.removeRows(0, root.rowCount()) + + def refresh(self): + items = self._controller.get_action_items( + self._selected_project_name, + self._selected_folder_id, + self._selected_task_id, + ) + if not items: + self._clear_items() + self.refreshed.emit() + return + + root_item = self.invisibleRootItem() + + all_action_items_info = [] + items_by_label = collections.defaultdict(list) + for item in items: + if not item.variant_label: + all_action_items_info.append((item, False)) + else: + items_by_label[item.label].append(item) + + groups_by_id = {} + for action_items in items_by_label.values(): + 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 = {} + for action_item_info in all_action_items_info: + action_item, is_group = action_item_info + icon = get_qt_icon(action_item.icon) + if is_group: + label = action_item.label + else: + label = action_item.full_label + + item = self._items_by_id.get(action_item.identifier) + if item is None: + item = QtGui.QStandardItem() + item.setData(action_item.identifier, ACTION_ID_ROLE) + new_items.append(item) + + item.setFlags(QtCore.Qt.ItemIsEnabled) + item.setData(label, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setData(is_group, ACTION_IS_GROUP_ROLE) + item.setData(action_item.order, ACTION_SORT_ROLE) + item.setData( + action_item.is_application, ACTION_IS_APPLICATION_ROLE) + item.setData( + action_item.force_not_open_workfile, + FORCE_NOT_OPEN_WORKFILE_ROLE) + items_by_id[action_item.identifier] = item + + if new_items: + root_item.appendRows(new_items) + + 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) + root_item.removeRow(item.row()) + + self._groups_by_id = groups_by_id + self._items_by_id = 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 + self._selected_task_id = None + self.refresh() + + def _on_selection_folder_changed(self, event): + self._selected_project_name = event["project_name"] + self._selected_folder_id = event["folder_id"] + self._selected_task_id = None + self.refresh() + + def _on_selection_task_changed(self, event): + self._selected_project_name = event["project_name"] + self._selected_folder_id = event["folder_id"] + self._selected_task_id = event["task_id"] + self.refresh() + + +class ActionDelegate(QtWidgets.QStyledItemDelegate): + _cached_extender = {} + + def __init__(self, *args, **kwargs): + super(ActionDelegate, self).__init__(*args, **kwargs) + self._anim_start_color = QtGui.QColor(178, 255, 246) + self._anim_end_color = QtGui.QColor(5, 44, 50) + + def _draw_animation(self, painter, option, index): + grid_size = option.widget.gridSize() + x_offset = int( + (grid_size.width() / 2) + - (option.rect.width() / 2) + ) + item_x = option.rect.x() - x_offset + rect_offset = grid_size.width() / 20 + size = grid_size.width() - (rect_offset * 2) + anim_rect = QtCore.QRect( + item_x + rect_offset, + option.rect.y() + rect_offset, + size, + size + ) + + painter.save() + + painter.setBrush(QtCore.Qt.transparent) + + gradient = QtGui.QConicalGradient() + gradient.setCenter(QtCore.QPointF(anim_rect.center())) + gradient.setColorAt(0, self._anim_start_color) + gradient.setColorAt(1, self._anim_end_color) + + time_diff = time.time() - index.data(ANIMATION_START_ROLE) + + # Repeat 4 times + part_anim = 2.5 + part_time = time_diff % part_anim + offset = (part_time / part_anim) * 360 + angle = (offset + 90) % 360 + + gradient.setAngle(-angle) + + pen = QtGui.QPen(QtGui.QBrush(gradient), rect_offset) + pen.setCapStyle(QtCore.Qt.RoundCap) + painter.setPen(pen) + painter.drawArc( + anim_rect, + -16 * (angle + 10), + -16 * offset + ) + + painter.restore() + + @classmethod + def _get_extender_pixmap(cls, size): + pix = cls._cached_extender.get(size) + if pix is not None: + return pix + pix = QtGui.QPixmap(get_options_image_path()).scaled( + size, size, + QtCore.Qt.KeepAspectRatio, + QtCore.Qt.SmoothTransformation + ) + cls._cached_extender[size] = pix + return pix + + def paint(self, painter, option, index): + painter.setRenderHints( + QtGui.QPainter.Antialiasing + | QtGui.QPainter.SmoothPixmapTransform + ) + + if index.data(ANIMATION_STATE_ROLE): + self._draw_animation(painter, option, index) + + super(ActionDelegate, self).paint(painter, option, index) + + if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): + rect = QtCore.QRectF( + option.rect.x(), option.rect.height(), 5, 5) + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(QtGui.QColor(200, 0, 0)) + painter.drawEllipse(rect) + + if not index.data(ACTION_IS_GROUP_ROLE): + return + + grid_size = option.widget.gridSize() + x_offset = int( + (grid_size.width() / 2) + - (option.rect.width() / 2) + ) + item_x = option.rect.x() - x_offset + + tenth_size = int(grid_size.width() / 10) + extender_size = int(tenth_size * 2.4) + + extender_x = item_x + tenth_size + extender_y = option.rect.y() + tenth_size + + pix = self._get_extender_pixmap(extender_size) + painter.drawPixmap(extender_x, extender_y, pix) + + +class ActionsWidget(QtWidgets.QWidget): + def __init__(self, controller, parent): + super(ActionsWidget, self).__init__(parent) + + self._controller = controller + + view = QtWidgets.QListView(self) + view.setProperty("mode", "icon") + view.setObjectName("IconView") + view.setViewMode(QtWidgets.QListView.IconMode) + view.setResizeMode(QtWidgets.QListView.Adjust) + view.setSelectionMode(QtWidgets.QListView.NoSelection) + view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + view.setWrapping(True) + view.setGridSize(QtCore.QSize(70, 75)) + view.setIconSize(QtCore.QSize(30, 30)) + view.setSpacing(0) + view.setWordWrap(True) + + # Make view flickable + flick = FlickCharm(parent=view) + flick.activateOn(view) + + model = ActionsQtModel(controller) + + proxy_model = QtCore.QSortFilterProxyModel() + proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + proxy_model.setSortRole(ACTION_SORT_ROLE) + + proxy_model.setSourceModel(model) + view.setModel(proxy_model) + + delegate = ActionDelegate(self) + view.setItemDelegate(delegate) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.addWidget(view) + + animation_timer = QtCore.QTimer() + animation_timer.setInterval(40) + animation_timer.timeout.connect(self._on_animation) + + view.clicked.connect(self._on_clicked) + view.customContextMenuRequested.connect(self._on_context_menu) + model.refreshed.connect(self._on_model_refresh) + + self._animated_items = set() + self._animation_timer = animation_timer + + self._context_menu = None + + self._flick = flick + self._view = view + self._model = model + self._proxy_model = proxy_model + + self._set_row_height(1) + + def _set_row_height(self, rows): + self.setMinimumHeight(rows * 75) + + def _on_model_refresh(self): + self._proxy_model.sort(0) + + def _on_animation(self): + time_now = time.time() + for action_id in tuple(self._animated_items): + item = self._model.get_item_by_id(action_id) + if item is None: + self._animated_items.discard(action_id) + continue + + start_time = item.data(ANIMATION_START_ROLE) + if start_time is None or (time_now - start_time) > ANIMATION_LEN: + item.setData(0, ANIMATION_STATE_ROLE) + self._animated_items.discard(action_id) + + if not self._animated_items: + self._animation_timer.stop() + + self.update() + + def _start_animation(self, index): + # Offset refresh timout + model_index = self._proxy_model.mapToSource(index) + if not model_index.isValid(): + return + action_id = model_index.data(ACTION_ID_ROLE) + self._model.setData(model_index, time.time(), ANIMATION_START_ROLE) + self._model.setData(model_index, 1, ANIMATION_STATE_ROLE) + self._animated_items.add(action_id) + self._animation_timer.start() + + def _on_context_menu(self, point): + """Creates menu to force skip opening last workfile.""" + index = self._view.indexAt(point) + if not index.isValid(): + return + + if not index.data(ACTION_IS_APPLICATION_ROLE): + return + + menu = QtWidgets.QMenu(self._view) + checkbox = QtWidgets.QCheckBox( + "Skip opening last workfile.", menu) + if index.data(FORCE_NOT_OPEN_WORKFILE_ROLE): + checkbox.setChecked(True) + + action_id = index.data(ACTION_ID_ROLE) + checkbox.stateChanged.connect( + lambda: self._on_checkbox_changed( + action_id, checkbox.isChecked() + ) + ) + action = QtWidgets.QWidgetAction(menu) + action.setDefaultWidget(checkbox) + + menu.addAction(action) + + self._context_menu = menu + global_point = self.mapToGlobal(point) + menu.exec_(global_point) + self._context_menu = None + + def _on_checkbox_changed(self, action_id, is_checked): + if self._context_menu is not None: + self._context_menu.close() + + project_name = self._model.get_selected_project_name() + 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) + self._model.refresh() + + def _on_clicked(self, index): + if not index or not index.isValid(): + return + + is_group = index.data(ACTION_IS_GROUP_ROLE) + action_id = index.data(ACTION_ID_ROLE) + + project_name = self._model.get_selected_project_name() + folder_id = self._model.get_selected_folder_id() + task_id = self._model.get_selected_task_id() + + if not is_group: + self._controller.trigger_action( + project_name, folder_id, task_id, action_id + ) + self._start_animation(index) + return + + action_items = self._model.get_group_items(action_id) + + menu = QtWidgets.QMenu(self) + actions_mapping = {} + + for action_item in action_items: + menu_action = QtWidgets.QAction(action_item.full_label) + menu.addAction(menu_action) + actions_mapping[menu_action] = action_item + + result = menu.exec_(QtGui.QCursor.pos()) + if not result: + return + + action_item = actions_mapping[result] + + self._controller.trigger_action( + project_name, folder_id, task_id, action_item.identifier + ) + self._start_animation(index) diff --git a/openpype/tools/ayon_launcher/ui/hierarchy_page.py b/openpype/tools/ayon_launcher/ui/hierarchy_page.py new file mode 100644 index 0000000000..5047cdc692 --- /dev/null +++ b/openpype/tools/ayon_launcher/ui/hierarchy_page.py @@ -0,0 +1,102 @@ +import qtawesome +from qtpy import QtWidgets, QtCore + +from openpype.tools.utils import ( + PlaceholderLineEdit, + SquareButton, + RefreshButton, +) +from openpype.tools.ayon_utils.widgets import ( + ProjectsCombobox, + FoldersWidget, + TasksWidget, +) + + +class HierarchyPage(QtWidgets.QWidget): + def __init__(self, controller, parent): + super(HierarchyPage, self).__init__(parent) + + # Header + header_widget = QtWidgets.QWidget(self) + + btn_back_icon = qtawesome.icon("fa.angle-left", color="white") + btn_back = SquareButton(header_widget) + btn_back.setIcon(btn_back_icon) + + projects_combobox = ProjectsCombobox(controller, header_widget) + + refresh_btn = RefreshButton(header_widget) + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(btn_back, 0) + header_layout.addWidget(projects_combobox, 1) + header_layout.addWidget(refresh_btn, 0) + + # Body - Folders + Tasks selection + content_body = QtWidgets.QSplitter(self) + content_body.setContentsMargins(0, 0, 0, 0) + content_body.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) + content_body.setOrientation(QtCore.Qt.Horizontal) + + # - Folders widget with filter + folders_wrapper = QtWidgets.QWidget(content_body) + + folders_filter_text = PlaceholderLineEdit(folders_wrapper) + folders_filter_text.setPlaceholderText("Filter folders...") + + folders_widget = FoldersWidget(controller, folders_wrapper) + + folders_wrapper_layout = QtWidgets.QVBoxLayout(folders_wrapper) + folders_wrapper_layout.setContentsMargins(0, 0, 0, 0) + folders_wrapper_layout.addWidget(folders_filter_text, 0) + folders_wrapper_layout.addWidget(folders_widget, 1) + + # - Tasks widget + tasks_widget = TasksWidget(controller, content_body) + + content_body.addWidget(folders_wrapper) + content_body.addWidget(tasks_widget) + content_body.setStretchFactor(0, 100) + content_body.setStretchFactor(1, 65) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(header_widget, 0) + main_layout.addWidget(content_body, 1) + + btn_back.clicked.connect(self._on_back_clicked) + refresh_btn.clicked.connect(self._on_refreh_clicked) + folders_filter_text.textChanged.connect(self._on_filter_text_changed) + + self._is_visible = False + self._controller = controller + + self._btn_back = btn_back + self._projects_combobox = projects_combobox + self._folders_widget = folders_widget + self._tasks_widget = tasks_widget + + # Post init + projects_combobox.set_listen_to_selection_change(self._is_visible) + + def set_page_visible(self, visible, project_name=None): + if self._is_visible == visible: + return + self._is_visible = visible + self._projects_combobox.set_listen_to_selection_change(visible) + if visible and project_name: + self._projects_combobox.set_selection(project_name) + + def _on_back_clicked(self): + self._controller.set_selected_project(None) + + def _on_refreh_clicked(self): + self._controller.refresh() + + def _on_filter_text_changed(self, text): + self._folders_widget.set_name_filer(text) diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py new file mode 100644 index 0000000000..baa399d0ed --- /dev/null +++ b/openpype/tools/ayon_launcher/ui/projects_widget.py @@ -0,0 +1,135 @@ +from qtpy import QtWidgets, QtCore + +from openpype.tools.flickcharm import FlickCharm +from openpype.tools.utils import PlaceholderLineEdit, RefreshButton +from openpype.tools.ayon_utils.widgets import ( + ProjectsModel, + ProjectSortFilterProxy, +) +from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER + + +class ProjectIconView(QtWidgets.QListView): + """Styled ListView that allows to toggle between icon and list mode. + + Toggling between the two modes is done by Right Mouse Click. + """ + + IconMode = 0 + ListMode = 1 + + def __init__(self, parent=None, mode=ListMode): + super(ProjectIconView, self).__init__(parent=parent) + + # Workaround for scrolling being super slow or fast when + # toggling between the two visual modes + self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) + self.setObjectName("IconView") + + self._mode = None + self.set_mode(mode) + + def set_mode(self, mode): + if mode == self._mode: + return + + self._mode = mode + + if mode == self.IconMode: + self.setViewMode(QtWidgets.QListView.IconMode) + self.setResizeMode(QtWidgets.QListView.Adjust) + self.setWrapping(True) + self.setWordWrap(True) + self.setGridSize(QtCore.QSize(151, 90)) + self.setIconSize(QtCore.QSize(50, 50)) + self.setSpacing(0) + self.setAlternatingRowColors(False) + + self.setProperty("mode", "icon") + self.style().polish(self) + + self.verticalScrollBar().setSingleStep(30) + + elif self.ListMode: + self.setProperty("mode", "list") + self.style().polish(self) + + self.setViewMode(QtWidgets.QListView.ListMode) + self.setResizeMode(QtWidgets.QListView.Adjust) + self.setWrapping(False) + self.setWordWrap(False) + self.setIconSize(QtCore.QSize(20, 20)) + self.setGridSize(QtCore.QSize(100, 25)) + self.setSpacing(0) + self.setAlternatingRowColors(False) + + self.verticalScrollBar().setSingleStep(34) + + def mousePressEvent(self, event): + if event.button() == QtCore.Qt.RightButton: + self.set_mode(int(not self._mode)) + return super(ProjectIconView, self).mousePressEvent(event) + + +class ProjectsWidget(QtWidgets.QWidget): + """Projects Page""" + def __init__(self, controller, parent=None): + super(ProjectsWidget, self).__init__(parent=parent) + + header_widget = QtWidgets.QWidget(self) + + projects_filter_text = PlaceholderLineEdit(header_widget) + projects_filter_text.setPlaceholderText("Filter projects...") + + refresh_btn = RefreshButton(header_widget) + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(projects_filter_text, 1) + header_layout.addWidget(refresh_btn, 0) + + projects_view = ProjectIconView(parent=self) + projects_view.setSelectionMode(QtWidgets.QListView.NoSelection) + flick = FlickCharm(parent=self) + flick.activateOn(projects_view) + projects_model = ProjectsModel(controller) + projects_proxy_model = ProjectSortFilterProxy() + projects_proxy_model.setSourceModel(projects_model) + + projects_view.setModel(projects_proxy_model) + + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(header_widget, 0) + main_layout.addWidget(projects_view, 1) + + projects_view.clicked.connect(self._on_view_clicked) + projects_filter_text.textChanged.connect( + self._on_project_filter_change) + refresh_btn.clicked.connect(self._on_refresh_clicked) + + controller.register_event_callback( + "projects.refresh.finished", + self._on_projects_refresh_finished + ) + + self._controller = controller + + self._projects_view = projects_view + self._projects_model = projects_model + self._projects_proxy_model = projects_proxy_model + + def _on_view_clicked(self, index): + if index.isValid(): + project_name = index.data(QtCore.Qt.DisplayRole) + self._controller.set_selected_project(project_name) + + def _on_project_filter_change(self, text): + self._projects_proxy_model.setFilterFixedString(text) + + def _on_refresh_clicked(self): + self._controller.refresh() + + def _on_projects_refresh_finished(self, event): + if event["sender"] != PROJECTS_MODEL_SENDER: + self._projects_model.refresh() diff --git a/openpype/tools/ayon_launcher/ui/resources/__init__.py b/openpype/tools/ayon_launcher/ui/resources/__init__.py new file mode 100644 index 0000000000..27c59af2ba --- /dev/null +++ b/openpype/tools/ayon_launcher/ui/resources/__init__.py @@ -0,0 +1,7 @@ +import os + +RESOURCES_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def get_options_image_path(): + return os.path.join(RESOURCES_DIR, "options.png") diff --git a/openpype/tools/ayon_launcher/ui/resources/options.png b/openpype/tools/ayon_launcher/ui/resources/options.png new file mode 100644 index 0000000000000000000000000000000000000000..a9617d0d1914634835f388c01479c672a8c8ffd7 GIT binary patch literal 1772 zcmb7^`B%~j7skJcd*X`HFm9u1j=6?t%;Iwb7j zwT|b(sB<3ljDb1wl4oJgfJ(AW&7Bpv&K*RI{SGm)zYu0jaC8|wa}(8OdA=SqaOrGq zQl&JypY*{sIl^1ofVAgks@SbOwLH%}dUorXyCcm^p8M0qpzTd|xz-DUnY^dOKTPeH z@AJ-CoGxRX%>y<5sp+n7Xk3?Ib+6{^?Cg%=d&rthvwyAl5BkShe>;4{WaeF*=5}tm ztt=TDXzceIC%r2%j9B($%Mh%fdW+^DZJMF}7T%OTA(YA8((QYh7f_5?|N5vQJC}2i zC0r;RJ>ErHqPC^l%s{3;^f+m96LRwULUx{B(Jk=u8K5KQK(F2Y*hE<*nGwM+6L<1iX zlK}t@mfr$Is`1VO02RGJA5=K0d~t%GY(ju`e_V2|K}V)SS?KCKM{H&_P7Y3Crp5mD z(`Z|5OJ5s)n{5lU+FfOFe`n~y1vEYAMcpmGYIrBiNw-)XnP|0HU<^ZOl*}9xr(R_s z-ktQ)Rp*UwVBUH#819d^g7kQOs&9HGq+ox_gXKk_@&4ncL<1u#E}<#6%&D0{)?q>@ z?Mq-Gs6s7mESn~{WhJteFd6)`mZ1U=UOLUm;q`|VcDE-YaVZ;Q~Te%Sofmu0UYVg!&X;RuJAt9igF;-A4LhWK?gT_dOlF*OOzD^7K zM0(b%SCz|j<0nL|yHR9|tdKubbevo`S#A{YvDG=|<+k;omrnFfC=_onNlmF}*P%aQ zz^g7hN^R`Pd;|)6L$)FXQese2SGyOAY{6{}Y=kB?Kv#n$PhZdf2ky zC~`-vdo>j`0wm``3CPJbVh^l!T^XN!WuMskOu14otdtzM{FZ(Ri1&Fi@A@7tB8dK~ zde}ZsK7UHkRttC-w4~8JO&(yw0aNwZM}4Ljqg`wPwrbtjQMQjA+>o+2e`K*~z`}^8 z$vMbbLM0Q2PC(c15PkyL_|q$ttu02DN;O3I7xQtiX;sVJ+ymk-oAE{EL?t$|+05dC zI@|D&m@;_t3WNN^wQ{8^-@~D`hWrV+gGfW9Ct4Myk1m@@3?wv$+$1JoCUMX~gcLy% zh~XxkM&oi1d}qIrrDlMz02j145nuS1%F=x$|b>QE}Uw_gWZT>QLRG z(V=0dJffRQo$`Clt5srtbzj?s=cCa_lk?tP8IX-f>!Ws5L@ck1<(p=DzydJQUDGyaDo3`;n#I_7#p1q zhXiA8A329}vHO`;fO84CeMF#Wsx?to(^2@VC=8r2mai3L)W}`t7f{yg}pV5{4$m~-H5NUJkTQN6t*Ssmw zEK7QvnhYK8CMbrSija`c4|B|61&Y%ub36cVDVz$Q7|_Ra`j1sDOF{i9-4hK;j#?7m zg4b(STtTXty_=b>KL8LX)j~j)sZmySrC>jPN|p?KUt2o6^)Rm87RP5K{aekI3Hu$u zbFte3N7JA8*R$kRE2H|HiFjp)Tf?0gH~)_%?0Q*P#>> cache = NestedCacheItem(levels=2) + >>> cache["a"]["b"].is_valid + False + >>> cache["a"]["b"].get_data() + None + >>> cache["a"]["b"] = 1 + >>> cache["a"]["b"].is_valid + True + >>> cache["a"]["b"].get_data() + 1 + >>> cache.reset() + >>> cache["a"]["b"].is_valid + False + + Args: + levels (int): Number of nested levels where read cache is stored. + default_factory (Optional[callable]): Function that returns default + value used on init and on reset. + lifetime (Optional[int]): Lifetime of the cache data in seconds. + _init_info (Optional[InitInfo]): Private argument. Init info for + nested cache where created from parent item. + """ + + def __init__( + self, levels=1, default_factory=None, lifetime=None, _init_info=None + ): + if levels < 1: + raise ValueError("Nested levels must be greater than 0") + self._data_by_key = {} + if _init_info is None: + _init_info = InitInfo(default_factory, lifetime) + self._init_info = _init_info + self._levels = levels + + def __getitem__(self, key): + """Get cached data. + + Args: + key (str): Key of the cache item. + + Returns: + Union[NestedCacheItem, CacheItem]: Cache item. + """ + + cache = self._data_by_key.get(key) + if cache is None: + if self._levels > 1: + cache = NestedCacheItem( + levels=self._levels - 1, + _init_info=self._init_info + ) + else: + cache = CacheItem( + self._init_info.default_factory, + self._init_info.lifetime + ) + self._data_by_key[key] = cache + return cache + + def __setitem__(self, key, value): + """Update cached data. + + Args: + key (str): Key of the cache item. + value (Any): Any data that are cached. + """ + + if self._levels > 1: + raise AttributeError(( + "{} does not support '__setitem__'. Lower nested level by {}" + ).format(self.__class__.__name__, self._levels - 1)) + cache = self[key] + cache.update_data(value) + + def get(self, key): + """Get cached data. + + Args: + key (str): Key of the cache item. + + Returns: + Union[NestedCacheItem, CacheItem]: Cache item. + """ + + return self[key] + + def reset(self): + """Reset cache.""" + + self._data_by_key = {} + + def set_lifetime(self, lifetime): + """Change lifetime of all children cache items. + + Args: + lifetime (int): Lifetime of the cache data in seconds. + """ + + self._init_info.lifetime = lifetime + for cache in self._data_by_key.values(): + cache.set_lifetime(lifetime) + + @property + def is_valid(self): + """Raise reasonable error when called on wront level. + + Raises: + AttributeError: If called on nested cache item. + """ + + raise AttributeError(( + "{} does not support 'is_valid'. Lower nested level by '{}'" + ).format(self.__class__.__name__, self._levels)) diff --git a/openpype/tools/ayon_utils/models/hierarchy.py b/openpype/tools/ayon_utils/models/hierarchy.py new file mode 100644 index 0000000000..8e01c557c5 --- /dev/null +++ b/openpype/tools/ayon_utils/models/hierarchy.py @@ -0,0 +1,340 @@ +import collections +import contextlib +from abc import ABCMeta, abstractmethod + +import ayon_api +import six + +from openpype.style import get_default_entity_icon_color + +from .cache import NestedCacheItem + +HIERARCHY_MODEL_SENDER = "hierarchy.model" + + +@six.add_metaclass(ABCMeta) +class AbstractHierarchyController: + @abstractmethod + def emit_event(self, topic, data, source): + pass + + +class FolderItem: + """Item representing folder entity on a server. + + Folder can be a child of another folder or a project. + + Args: + entity_id (str): Folder id. + parent_id (Union[str, None]): Parent folder id. If 'None' then project + is parent. + name (str): Name of folder. + label (str): Folder label. + icon_name (str): Name of icon from font awesome. + icon_color (str): Hex color string that will be used for icon. + """ + + def __init__( + self, entity_id, parent_id, name, label, icon + ): + self.entity_id = entity_id + self.parent_id = parent_id + self.name = name + if not icon: + icon = { + "type": "awesome-font", + "name": "fa.folder", + "color": get_default_entity_icon_color() + } + self.icon = icon + self.label = label or name + + def to_data(self): + """Converts folder item to data. + + Returns: + dict[str, Any]: Folder item data. + """ + + return { + "entity_id": self.entity_id, + "parent_id": self.parent_id, + "name": self.name, + "label": self.label, + "icon": self.icon, + } + + @classmethod + def from_data(cls, data): + """Re-creates folder item from data. + + Args: + data (dict[str, Any]): Folder item data. + + Returns: + FolderItem: Folder item. + """ + + return cls(**data) + + +class TaskItem: + """Task item representing task entity on a server. + + Task is child of a folder. + + Task item has label that is used for display in UI. The label is by + default using task name and type. + + Args: + task_id (str): Task id. + name (str): Name of task. + task_type (str): Type of task. + parent_id (str): Parent folder id. + icon_name (str): Name of icon from font awesome. + icon_color (str): Hex color string that will be used for icon. + """ + + def __init__( + self, task_id, name, task_type, parent_id, icon + ): + self.task_id = task_id + self.name = name + self.task_type = task_type + self.parent_id = parent_id + if icon is None: + icon = { + "type": "awesome-font", + "name": "fa.male", + "color": get_default_entity_icon_color() + } + self.icon = icon + + self._label = None + + @property + def id(self): + """Alias for task_id. + + Returns: + str: Task id. + """ + + return self.task_id + + @property + def label(self): + """Label of task item for UI. + + Returns: + str: Label of task item. + """ + + if self._label is None: + self._label = "{} ({})".format(self.name, self.task_type) + return self._label + + def to_data(self): + """Converts task item to data. + + Returns: + dict[str, Any]: Task item data. + """ + + return { + "task_id": self.task_id, + "name": self.name, + "parent_id": self.parent_id, + "task_type": self.task_type, + "icon": self.icon, + } + + @classmethod + def from_data(cls, data): + """Re-create task item from data. + + Args: + data (dict[str, Any]): Task item data. + + Returns: + TaskItem: Task item. + """ + + return cls(**data) + + +def _get_task_items_from_tasks(tasks): + """ + + Returns: + TaskItem: Task item. + """ + + output = [] + for task in tasks: + folder_id = task["folderId"] + output.append(TaskItem( + task["id"], + task["name"], + task["type"], + folder_id, + None + )) + return output + + +def _get_folder_item_from_hierarchy_item(item): + return FolderItem( + item["id"], + item["parentId"], + item["name"], + item["label"], + None + ) + + +class HierarchyModel(object): + """Model for project hierarchy items. + + Hierarchy items are folders and tasks. Folders can have as parent another + folder or project. Tasks can have as parent only folder. + """ + + def __init__(self, controller): + self._folders_items = NestedCacheItem(levels=1, default_factory=dict) + self._folders_by_id = NestedCacheItem(levels=2, default_factory=dict) + + self._task_items = NestedCacheItem(levels=2, default_factory=dict) + self._tasks_by_id = NestedCacheItem(levels=2, default_factory=dict) + + self._folders_refreshing = set() + self._tasks_refreshing = set() + self._controller = controller + + def reset(self): + self._folders_items.reset() + self._folders_by_id.reset() + + self._task_items.reset() + self._tasks_by_id.reset() + + def refresh_project(self, project_name): + self._refresh_folders_cache(project_name) + + def get_folder_items(self, project_name, sender): + if not self._folders_items[project_name].is_valid: + self._refresh_folders_cache(project_name, sender) + return self._folders_items[project_name].get_data() + + def get_task_items(self, project_name, folder_id, sender): + if not project_name or not folder_id: + return [] + + task_cache = self._task_items[project_name][folder_id] + if not task_cache.is_valid: + self._refresh_tasks_cache(project_name, folder_id, sender) + return task_cache.get_data() + + def get_folder_entity(self, project_name, folder_id): + cache = self._folders_by_id[project_name][folder_id] + if not cache.is_valid: + entity = None + if folder_id: + entity = ayon_api.get_folder_by_id(project_name, folder_id) + cache.update_data(entity) + return cache.get_data() + + def get_task_entity(self, project_name, task_id): + cache = self._tasks_by_id[project_name][task_id] + if not cache.is_valid: + entity = None + if task_id: + entity = ayon_api.get_task_by_id(project_name, task_id) + cache.update_data(entity) + return cache.get_data() + + @contextlib.contextmanager + def _folder_refresh_event_manager(self, project_name, sender): + self._folders_refreshing.add(project_name) + self._controller.emit_event( + "folders.refresh.started", + {"project_name": project_name, "sender": sender}, + HIERARCHY_MODEL_SENDER + ) + try: + yield + + finally: + self._controller.emit_event( + "folders.refresh.finished", + {"project_name": project_name, "sender": sender}, + HIERARCHY_MODEL_SENDER + ) + self._folders_refreshing.remove(project_name) + + @contextlib.contextmanager + def _task_refresh_event_manager( + self, project_name, folder_id, sender + ): + self._tasks_refreshing.add(folder_id) + self._controller.emit_event( + "tasks.refresh.started", + { + "project_name": project_name, + "folder_id": folder_id, + "sender": sender, + }, + HIERARCHY_MODEL_SENDER + ) + try: + yield + + finally: + self._controller.emit_event( + "tasks.refresh.finished", + { + "project_name": project_name, + "folder_id": folder_id, + "sender": sender, + }, + HIERARCHY_MODEL_SENDER + ) + self._tasks_refreshing.discard(folder_id) + + def _refresh_folders_cache(self, project_name, sender=None): + if project_name in self._folders_refreshing: + return + + with self._folder_refresh_event_manager(project_name, sender): + folder_items = self._query_folders(project_name) + self._folders_items[project_name].update_data(folder_items) + + def _query_folders(self, project_name): + hierarchy = ayon_api.get_folders_hierarchy(project_name) + + folder_items = {} + hierachy_queue = collections.deque(hierarchy["hierarchy"]) + while hierachy_queue: + item = hierachy_queue.popleft() + folder_item = _get_folder_item_from_hierarchy_item(item) + folder_items[folder_item.entity_id] = folder_item + hierachy_queue.extend(item["children"] or []) + return folder_items + + def _refresh_tasks_cache(self, project_name, folder_id, sender=None): + if folder_id in self._tasks_refreshing: + return + + with self._task_refresh_event_manager( + project_name, folder_id, sender + ): + task_items = self._query_tasks(project_name, folder_id) + self._task_items[project_name][folder_id] = task_items + + def _query_tasks(self, project_name, folder_id): + tasks = list(ayon_api.get_tasks( + project_name, + folder_ids=[folder_id], + fields={"id", "name", "label", "folderId", "type"} + )) + return _get_task_items_from_tasks(tasks) diff --git a/openpype/tools/ayon_utils/models/projects.py b/openpype/tools/ayon_utils/models/projects.py new file mode 100644 index 0000000000..ae3eeecea4 --- /dev/null +++ b/openpype/tools/ayon_utils/models/projects.py @@ -0,0 +1,145 @@ +import contextlib +from abc import ABCMeta, abstractmethod + +import ayon_api +import six + +from openpype.style import get_default_entity_icon_color + +from .cache import CacheItem + +PROJECTS_MODEL_SENDER = "projects.model" + + +@six.add_metaclass(ABCMeta) +class AbstractHierarchyController: + @abstractmethod + def emit_event(self, topic, data, source): + pass + + +class ProjectItem: + """Item representing folder entity on a server. + + Folder can be a child of another folder or a project. + + Args: + name (str): Project name. + active (Union[str, None]): Parent folder id. If 'None' then project + is parent. + """ + + def __init__(self, name, active, icon=None): + self.name = name + self.active = active + if icon is None: + icon = { + "type": "awesome-font", + "name": "fa.map", + "color": get_default_entity_icon_color(), + } + self.icon = icon + + def to_data(self): + """Converts folder item to data. + + Returns: + dict[str, Any]: Folder item data. + """ + + return { + "name": self.name, + "active": self.active, + "icon": self.icon, + } + + @classmethod + def from_data(cls, data): + """Re-creates folder item from data. + + Args: + data (dict[str, Any]): Folder item data. + + Returns: + FolderItem: Folder item. + """ + + return cls(**data) + + +def _get_project_items_from_entitiy(projects): + """ + + Args: + projects (list[dict[str, Any]]): List of projects. + + Returns: + ProjectItem: Project item. + """ + + return [ + ProjectItem(project["name"], project["active"]) + for project in projects + ] + + +class ProjectsModel(object): + def __init__(self, controller): + self._projects_cache = CacheItem(default_factory=dict) + self._project_items_by_name = {} + self._projects_by_name = {} + + self._is_refreshing = False + self._controller = controller + + def reset(self): + self._projects_cache.reset() + self._project_items_by_name = {} + self._projects_by_name = {} + + def refresh(self): + self._refresh_projects_cache() + + def get_project_items(self, sender): + if not self._projects_cache.is_valid: + self._refresh_projects_cache(sender) + return self._projects_cache.get_data() + + def get_project_entity(self, project_name): + if project_name not in self._projects_by_name: + entity = None + if project_name: + entity = ayon_api.get_project(project_name) + self._projects_by_name[project_name] = entity + return self._projects_by_name[project_name] + + @contextlib.contextmanager + def _project_refresh_event_manager(self, sender): + self._is_refreshing = True + self._controller.emit_event( + "projects.refresh.started", + {"sender": sender}, + PROJECTS_MODEL_SENDER + ) + try: + yield + + finally: + self._controller.emit_event( + "projects.refresh.finished", + {"sender": sender}, + PROJECTS_MODEL_SENDER + ) + self._is_refreshing = False + + def _refresh_projects_cache(self, sender=None): + if self._is_refreshing: + return + + with self._project_refresh_event_manager(sender): + project_items = self._query_projects() + self._projects_cache.update_data(project_items) + + def _query_projects(self): + projects = ayon_api.get_projects(fields=["name", "active"]) + return _get_project_items_from_entitiy(projects) diff --git a/openpype/tools/ayon_utils/widgets/__init__.py b/openpype/tools/ayon_utils/widgets/__init__.py new file mode 100644 index 0000000000..59aef98faf --- /dev/null +++ b/openpype/tools/ayon_utils/widgets/__init__.py @@ -0,0 +1,37 @@ +from .projects_widget import ( + # ProjectsWidget, + ProjectsCombobox, + ProjectsModel, + ProjectSortFilterProxy, +) + +from .folders_widget import ( + FoldersWidget, + FoldersModel, +) + +from .tasks_widget import ( + TasksWidget, + TasksModel, +) +from .utils import ( + get_qt_icon, + RefreshThread, +) + + +__all__ = ( + # "ProjectsWidget", + "ProjectsCombobox", + "ProjectsModel", + "ProjectSortFilterProxy", + + "FoldersWidget", + "FoldersModel", + + "TasksWidget", + "TasksModel", + + "get_qt_icon", + "RefreshThread", +) diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py new file mode 100644 index 0000000000..3fab64f657 --- /dev/null +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -0,0 +1,364 @@ +import collections + +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.tools.utils import ( + RecursiveSortFilterProxyModel, + DeselectableTreeView, +) + +from .utils import RefreshThread, get_qt_icon + +SENDER_NAME = "qt_folders_model" +ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 +ITEM_NAME_ROLE = QtCore.Qt.UserRole + 2 + + +class FoldersModel(QtGui.QStandardItemModel): + """Folders model which cares about refresh of folders. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(FoldersModel, self).__init__() + + self._controller = controller + self._items_by_id = {} + self._parent_id_by_id = {} + + self._refresh_threads = {} + self._current_refresh_thread = None + self._last_project_name = None + + self._has_content = False + self._is_refreshing = False + + @property + def is_refreshing(self): + """Model is refreshing. + + Returns: + bool: True if model is refreshing. + """ + return self._is_refreshing + + @property + def has_content(self): + """Has at least one folder. + + Returns: + bool: True if model has at least one folder. + """ + + return self._has_content + + def clear(self): + self._items_by_id = {} + self._parent_id_by_id = {} + self._has_content = False + super(FoldersModel, self).clear() + + def get_index_by_id(self, item_id): + """Get index by folder id. + + Returns: + QtCore.QModelIndex: Index of the folder. Can be invalid if folder + is not available. + """ + item = self._items_by_id.get(item_id) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def set_project_name(self, project_name): + """Refresh folders items. + + Refresh start thread because it can cause that controller can + start query from database if folders are not cached. + """ + + if not project_name: + self._last_project_name = project_name + self._current_refresh_thread = None + self._fill_items({}) + return + + self._is_refreshing = True + + if self._last_project_name != project_name: + self.clear() + self._last_project_name = project_name + + thread = self._refresh_threads.get(project_name) + if thread is not None: + self._current_refresh_thread = thread + return + + thread = RefreshThread( + project_name, + self._controller.get_folder_items, + project_name, + SENDER_NAME + ) + self._current_refresh_thread = thread + self._refresh_threads[thread.id] = thread + thread.refresh_finished.connect(self._on_refresh_thread) + thread.start() + + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Folders are stored by id. + + Args: + thread_id (str): Thread id. + """ + + # Make sure to remove thread from '_refresh_threads' dict + thread = self._refresh_threads.pop(thread_id) + if ( + self._current_refresh_thread is None + or thread_id != self._current_refresh_thread.id + ): + return + + self._fill_items(thread.get_result()) + + 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._is_refreshing = False + self.refreshed.emit() + return + + self._has_content = True + + folder_ids = set(folder_items_by_id) + ids_to_remove = set(self._items_by_id) - folder_ids + + folder_items_by_parent = collections.defaultdict(dict) + for folder_item in folder_items_by_id.values(): + ( + folder_items_by_parent + [folder_item.parent_id] + [folder_item.entity_id] + ) = folder_item + + hierarchy_queue = collections.deque() + hierarchy_queue.append((self.invisibleRootItem(), None)) + + # Keep pointers to removed items until the refresh finishes + # - some children of the items could be moved and reused elsewhere + removed_items = [] + while hierarchy_queue: + item = hierarchy_queue.popleft() + parent_item, parent_id = item + folder_items = folder_items_by_parent[parent_id] + + items_by_id = {} + folder_ids_to_add = set(folder_items) + for row_idx in reversed(range(parent_item.rowCount())): + child_item = parent_item.child(row_idx) + child_id = child_item.data(ITEM_ID_ROLE) + if child_id in ids_to_remove: + removed_items.append(parent_item.takeRow(row_idx)) + else: + items_by_id[child_id] = child_item + + new_items = [] + for item_id in folder_ids_to_add: + folder_item = folder_items[item_id] + item = items_by_id.get(item_id) + if item is None: + is_new = True + item = QtGui.QStandardItem() + item.setEditable(False) + else: + is_new = self._parent_id_by_id[item_id] != parent_id + + icon = get_qt_icon(folder_item.icon) + item.setData(item_id, ITEM_ID_ROLE) + item.setData(folder_item.name, ITEM_NAME_ROLE) + item.setData(folder_item.label, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) + if is_new: + new_items.append(item) + self._items_by_id[item_id] = item + self._parent_id_by_id[item_id] = parent_id + + hierarchy_queue.append((item, item_id)) + + if new_items: + parent_item.appendRows(new_items) + + for item_id in ids_to_remove: + self._items_by_id.pop(item_id) + self._parent_id_by_id.pop(item_id) + + self._is_refreshing = False + self.refreshed.emit() + + +class FoldersWidget(QtWidgets.QWidget): + """Folders widget. + + Widget that handles folders view, model and selection. + + Expected selection handling is disabled by default. If enabled, the + widget will handle the expected in predefined way. Widget is listening + to event 'expected_selection_changed' with expected event data below, + the same data must be available when called method + 'get_expected_selection_data' on controller. + + { + "folder": { + "current": bool, # Folder is what should be set now + "folder_id": Union[str, None], # Folder id that should be selected + }, + ... + } + + Selection is confirmed by calling method 'expected_folder_selected' on + controller. + + + Args: + controller (AbstractWorkfilesFrontend): The control object. + parent (QtWidgets.QWidget): The parent widget. + handle_expected_selection (bool): If True, the widget will handle + the expected selection. Defaults to False. + """ + + def __init__(self, controller, parent, handle_expected_selection=False): + super(FoldersWidget, self).__init__(parent) + + folders_view = DeselectableTreeView(self) + folders_view.setHeaderHidden(True) + + folders_model = FoldersModel(controller) + folders_proxy_model = RecursiveSortFilterProxyModel() + folders_proxy_model.setSourceModel(folders_model) + + folders_view.setModel(folders_proxy_model) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(folders_view, 1) + + controller.register_event_callback( + "selection.project.changed", + self._on_project_selection_change, + ) + controller.register_event_callback( + "folders.refresh.finished", + self._on_folders_refresh_finished + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh + ) + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + selection_model = folders_view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + folders_model.refreshed.connect(self._on_model_refresh) + + self._controller = controller + self._folders_view = folders_view + self._folders_model = folders_model + self._folders_proxy_model = folders_proxy_model + + self._handle_expected_selection = handle_expected_selection + self._expected_selection = None + + def set_name_filer(self, name): + """Set filter of folder name. + + Args: + name (str): The string filter. + """ + + self._folders_proxy_model.setFilterFixedString(name) + + def _on_project_selection_change(self, event): + project_name = event["project_name"] + self._set_project_name(project_name) + + 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"]) + + def _on_controller_refresh(self): + self._update_expected_selection() + + def _on_model_refresh(self): + if self._expected_selection: + self._set_expected_selection() + self._folders_proxy_model.sort(0) + + def _get_selected_item_id(self): + selection_model = self._folders_view.selectionModel() + for index in selection_model.selectedIndexes(): + item_id = index.data(ITEM_ID_ROLE) + if item_id is not None: + return item_id + return None + + def _on_selection_change(self): + item_id = self._get_selected_item_id() + self._controller.set_selected_folder(item_id) + + # Expected selection handling + def _on_expected_selection_change(self, event): + self._update_expected_selection(event.data) + + def _update_expected_selection(self, expected_data=None): + if not self._handle_expected_selection: + return + + if expected_data is None: + expected_data = self._controller.get_expected_selection_data() + + folder_data = expected_data.get("folder") + if not folder_data or not folder_data["current"]: + return + + folder_id = folder_data["id"] + self._expected_selection = folder_id + if not self._folders_model.is_refreshing: + self._set_expected_selection() + + def _set_expected_selection(self): + if not self._handle_expected_selection: + return + + folder_id = self._expected_selection + self._expected_selection = None + if ( + folder_id is not None + and folder_id != self._get_selected_item_id() + ): + index = self._folders_model.get_index_by_id(folder_id) + if index.isValid(): + proxy_index = self._folders_proxy_model.mapFromSource(index) + self._folders_view.setCurrentIndex(proxy_index) + self._controller.expected_folder_selected(folder_id) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py new file mode 100644 index 0000000000..818d574910 --- /dev/null +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -0,0 +1,325 @@ +from qtpy import QtWidgets, QtCore, QtGui + +from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER +from .utils import RefreshThread, get_qt_icon + +PROJECT_NAME_ROLE = QtCore.Qt.UserRole + 1 +PROJECT_IS_ACTIVE_ROLE = QtCore.Qt.UserRole + 2 + + +class ProjectsModel(QtGui.QStandardItemModel): + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(ProjectsModel, self).__init__() + self._controller = controller + + self._project_items = {} + + self._empty_item = None + self._empty_item_added = False + + self._is_refreshing = False + self._refresh_thread = None + + @property + def is_refreshing(self): + return self._is_refreshing + + def refresh(self): + self._refresh() + + def has_content(self): + return len(self._project_items) > 0 + + def _add_empty_item(self): + item = self._get_empty_item() + if not self._empty_item_added: + root_item = self.invisibleRootItem() + root_item.appendRow(item) + self._empty_item_added = True + + def _remove_empty_item(self): + if not self._empty_item_added: + return + + root_item = self.invisibleRootItem() + item = self._get_empty_item() + root_item.takeRow(item.row()) + self._empty_item_added = False + + def _get_empty_item(self): + if self._empty_item is None: + item = QtGui.QStandardItem("< No projects >") + item.setFlags(QtCore.Qt.NoItemFlags) + self._empty_item = item + return self._empty_item + + def _refresh(self): + if self._is_refreshing: + return + self._is_refreshing = True + refresh_thread = RefreshThread( + "projects", self._query_project_items + ) + refresh_thread.refresh_finished.connect(self._refresh_finished) + refresh_thread.start() + self._refresh_thread = refresh_thread + + def _query_project_items(self): + return self._controller.get_project_items() + + def _refresh_finished(self): + # TODO check if failed + result = self._refresh_thread.get_result() + self._refresh_thread = None + + self._fill_items(result) + + self._is_refreshing = False + self.refreshed.emit() + + def _fill_items(self, project_items): + items_to_remove = set(self._project_items.keys()) + new_items = [] + for project_item in project_items: + project_name = project_item.name + items_to_remove.discard(project_name) + item = self._project_items.get(project_name) + if item is None: + item = QtGui.QStandardItem() + new_items.append(item) + icon = get_qt_icon(project_item.icon) + item.setData(project_name, QtCore.Qt.DisplayRole) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(project_item.active, PROJECT_IS_ACTIVE_ROLE) + self._project_items[project_name] = item + + root_item = self.invisibleRootItem() + if new_items: + root_item.appendRows(new_items) + + for project_name in items_to_remove: + item = self._project_items.pop(project_name) + root_item.removeRow(item.row()) + + if self.has_content(): + self._remove_empty_item() + else: + self._add_empty_item() + + +class ProjectSortFilterProxy(QtCore.QSortFilterProxyModel): + def __init__(self, *args, **kwargs): + super(ProjectSortFilterProxy, self).__init__(*args, **kwargs) + self._filter_inactive = True + # Disable case sensitivity + self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) + + def lessThan(self, left_index, right_index): + if left_index.data(PROJECT_NAME_ROLE) is None: + return True + + if right_index.data(PROJECT_NAME_ROLE) is None: + return False + + left_is_active = left_index.data(PROJECT_IS_ACTIVE_ROLE) + right_is_active = right_index.data(PROJECT_IS_ACTIVE_ROLE) + if right_is_active == left_is_active: + return super(ProjectSortFilterProxy, self).lessThan( + left_index, right_index + ) + + if left_is_active: + return True + return False + + def filterAcceptsRow(self, source_row, source_parent): + index = self.sourceModel().index(source_row, 0, source_parent) + string_pattern = self.filterRegularExpression().pattern() + if ( + self._filter_inactive + and not index.data(PROJECT_IS_ACTIVE_ROLE) + ): + return False + + if string_pattern: + project_name = index.data(PROJECT_IS_ACTIVE_ROLE) + if project_name is not None: + return string_pattern.lower() in project_name.lower() + + return super(ProjectSortFilterProxy, self).filterAcceptsRow( + source_row, source_parent + ) + + def _custom_index_filter(self, index): + return bool(index.data(PROJECT_IS_ACTIVE_ROLE)) + + def is_active_filter_enabled(self): + return self._filter_inactive + + def set_active_filter_enabled(self, value): + if self._filter_inactive == value: + return + self._filter_inactive = value + self.invalidateFilter() + + +class ProjectsCombobox(QtWidgets.QWidget): + def __init__(self, controller, parent, handle_expected_selection=False): + super(ProjectsCombobox, self).__init__(parent) + + projects_combobox = QtWidgets.QComboBox(self) + combobox_delegate = QtWidgets.QStyledItemDelegate(projects_combobox) + projects_combobox.setItemDelegate(combobox_delegate) + projects_model = ProjectsModel(controller) + projects_proxy_model = ProjectSortFilterProxy() + projects_proxy_model.setSourceModel(projects_model) + projects_combobox.setModel(projects_proxy_model) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(projects_combobox, 1) + + projects_model.refreshed.connect(self._on_model_refresh) + + controller.register_event_callback( + "projects.refresh.finished", + self._on_projects_refresh_finished + ) + controller.register_event_callback( + "controller.refresh.finished", + self._on_controller_refresh + ) + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + projects_combobox.currentIndexChanged.connect( + self._on_current_index_changed + ) + + self._controller = controller + self._listen_selection_change = True + + self._handle_expected_selection = handle_expected_selection + self._expected_selection = None + + self._projects_combobox = projects_combobox + self._projects_model = projects_model + self._projects_proxy_model = projects_proxy_model + self._combobox_delegate = combobox_delegate + + def refresh(self): + self._projects_model.refresh() + + def set_selection(self, project_name): + """Set selection to a given project. + + Selection change is ignored if project is not found. + + Args: + project_name (str): Name of project. + + Returns: + bool: True if selection was changed, False otherwise. NOTE: + Selection may not be changed if project is not found, or if + project is already selected. + """ + + idx = self._projects_combobox.findData( + project_name, PROJECT_NAME_ROLE) + if idx < 0: + return False + if idx != self._projects_combobox.currentIndex(): + self._projects_combobox.setCurrentIndex(idx) + return True + return False + + def set_listen_to_selection_change(self, listen): + """Disable listening to changes of the selection. + + Because combobox is triggering selection change when it's model + is refreshed, it's necessary to disable listening to selection for + some cases, e.g. when is on a different page of UI and should be just + refreshed. + + Args: + listen (bool): Enable or disable listening to selection changes. + """ + + self._listen_selection_change = listen + + def get_current_project_name(self): + """Name of selected project. + + Returns: + Union[str, None]: Name of selected project, or None if no project + """ + + idx = self._projects_combobox.currentIndex() + if idx < 0: + return None + return self._projects_combobox.itemData(idx, PROJECT_NAME_ROLE) + + def _on_current_index_changed(self, idx): + if not self._listen_selection_change: + return + project_name = self._projects_combobox.itemData( + idx, PROJECT_NAME_ROLE) + self._controller.set_selected_project(project_name) + + def _on_model_refresh(self): + self._projects_proxy_model.sort(0) + if self._expected_selection: + self._set_expected_selection() + + def _on_projects_refresh_finished(self, event): + if event["sender"] != PROJECTS_MODEL_SENDER: + self._projects_model.refresh() + + def _on_controller_refresh(self): + self._update_expected_selection() + + # Expected selection handling + def _on_expected_selection_change(self, event): + self._update_expected_selection(event.data) + + def _set_expected_selection(self): + if not self._handle_expected_selection: + return + project_name = self._expected_selection + if project_name is not None: + if project_name != self.get_current_project_name(): + self.set_selection(project_name) + else: + # Fake project change + self._on_current_index_changed( + self._projects_combobox.currentIndex() + ) + + self._controller.expected_project_selected(project_name) + + def _update_expected_selection(self, expected_data=None): + if not self._handle_expected_selection: + return + if expected_data is None: + expected_data = self._controller.get_expected_selection_data() + + project_data = expected_data.get("project") + if ( + not project_data + or not project_data["current"] + or project_data["selected"] + ): + return + self._expected_selection = project_data["name"] + if not self._projects_model.is_refreshing: + self._set_expected_selection() + + +class ProjectsWidget(QtWidgets.QWidget): + # TODO implement + pass diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py new file mode 100644 index 0000000000..66ebd0b777 --- /dev/null +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -0,0 +1,436 @@ +from qtpy import QtWidgets, QtGui, QtCore + +from openpype.style import get_disabled_entity_icon_color +from openpype.tools.utils import DeselectableTreeView + +from .utils import RefreshThread, get_qt_icon + +SENDER_NAME = "qt_tasks_model" +ITEM_ID_ROLE = QtCore.Qt.UserRole + 1 +PARENT_ID_ROLE = QtCore.Qt.UserRole + 2 +ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3 +TASK_TYPE_ROLE = QtCore.Qt.UserRole + 4 + + +class TasksModel(QtGui.QStandardItemModel): + """Tasks model which cares about refresh of tasks by folder id. + + Args: + controller (AbstractWorkfilesFrontend): The control object. + """ + + refreshed = QtCore.Signal() + + def __init__(self, controller): + super(TasksModel, self).__init__() + + self._controller = controller + + self._items_by_name = {} + self._has_content = False + self._is_refreshing = False + + self._invalid_selection_item_used = False + self._invalid_selection_item = None + self._empty_tasks_item_used = False + self._empty_tasks_item = None + + self._last_project_name = None + self._last_folder_id = None + + self._refresh_threads = {} + self._current_refresh_thread = None + + # Initial state + self._add_invalid_selection_item() + + def clear(self): + self._items_by_name = {} + self._has_content = False + self._remove_invalid_items() + super(TasksModel, self).clear() + + def refresh(self, project_name, folder_id): + """Refresh tasks for folder. + + Args: + project_name (Union[str]): Name of project. + folder_id (Union[str, None]): Folder id. + """ + + self._refresh(project_name, folder_id) + + def get_index_by_name(self, task_name): + """Find item by name and return its index. + + Returns: + QtCore.QModelIndex: Index of item. Is invalid if task is not + found by name. + """ + + item = self._items_by_name.get(task_name) + if item is None: + return QtCore.QModelIndex() + return self.indexFromItem(item) + + def get_last_project_name(self): + """Get last refreshed project name. + + Returns: + Union[str, None]: Project name. + """ + + return self._last_project_name + + def get_last_folder_id(self): + """Get last refreshed folder id. + + Returns: + Union[str, None]: Folder id. + """ + + return self._last_folder_id + + def set_selected_project(self, project_name): + self._selected_project_name = project_name + + def _get_invalid_selection_item(self): + if self._invalid_selection_item is None: + item = QtGui.QStandardItem("Select a folder") + item.setFlags(QtCore.Qt.NoItemFlags) + icon = get_qt_icon({ + "type": "awesome-font", + "name": "fa.times", + "color": get_disabled_entity_icon_color(), + }) + item.setData(icon, QtCore.Qt.DecorationRole) + self._invalid_selection_item = item + return self._invalid_selection_item + + def _get_empty_task_item(self): + if self._empty_tasks_item is None: + item = QtGui.QStandardItem("No task") + icon = get_qt_icon({ + "type": "awesome-font", + "name": "fa.exclamation-circle", + "color": get_disabled_entity_icon_color(), + }) + item.setData(icon, QtCore.Qt.DecorationRole) + item.setFlags(QtCore.Qt.NoItemFlags) + self._empty_tasks_item = item + return self._empty_tasks_item + + def _add_invalid_item(self, item): + self.clear() + root_item = self.invisibleRootItem() + root_item.appendRow(item) + + def _remove_invalid_item(self, item): + root_item = self.invisibleRootItem() + root_item.takeRow(item.row()) + + def _remove_invalid_items(self): + self._remove_invalid_selection_item() + self._remove_empty_task_item() + + def _add_invalid_selection_item(self): + if not self._invalid_selection_item_used: + self._add_invalid_item(self._get_invalid_selection_item()) + self._invalid_selection_item_used = True + + def _remove_invalid_selection_item(self): + if self._invalid_selection_item: + self._remove_invalid_item(self._get_invalid_selection_item()) + self._invalid_selection_item_used = False + + def _add_empty_task_item(self): + if not self._empty_tasks_item_used: + self._add_invalid_item(self._get_empty_task_item()) + self._empty_tasks_item_used = True + + def _remove_empty_task_item(self): + if self._empty_tasks_item_used: + self._remove_invalid_item(self._get_empty_task_item()) + self._empty_tasks_item_used = False + + def _refresh(self, project_name, folder_id): + self._is_refreshing = True + self._last_project_name = project_name + self._last_folder_id = folder_id + if not folder_id: + self._add_invalid_selection_item() + self._current_refresh_thread = None + self._is_refreshing = False + self.refreshed.emit() + return + + thread = self._refresh_threads.get(folder_id) + if thread is not None: + self._current_refresh_thread = thread + return + thread = RefreshThread( + folder_id, + self._controller.get_task_items, + project_name, + folder_id + ) + self._current_refresh_thread = thread + self._refresh_threads[thread.id] = thread + thread.refresh_finished.connect(self._on_refresh_thread) + thread.start() + + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Tasks are stored by name, so if a folder has same task name as + previously selected folder it keeps the selection. + + Args: + thread_id (str): Thread id. + """ + + # Make sure to remove thread from '_refresh_threads' dict + thread = self._refresh_threads.pop(thread_id) + if ( + self._current_refresh_thread is None + or thread_id != self._current_refresh_thread.id + ): + return + + task_items = thread.get_result() + # Task items are refreshed + if task_items is None: + return + + # No tasks are available on folder + if not task_items: + self._add_empty_task_item() + return + self._remove_invalid_items() + + new_items = [] + new_names = set() + for task_item in task_items: + name = task_item.name + new_names.add(name) + item = self._items_by_name.get(name) + if item is None: + item = QtGui.QStandardItem() + item.setEditable(False) + new_items.append(item) + self._items_by_name[name] = item + + # TODO cache locally + icon = get_qt_icon(task_item.icon) + item.setData(task_item.label, QtCore.Qt.DisplayRole) + item.setData(name, ITEM_NAME_ROLE) + item.setData(task_item.id, ITEM_ID_ROLE) + item.setData(task_item.parent_id, PARENT_ID_ROLE) + item.setData(icon, QtCore.Qt.DecorationRole) + + root_item = self.invisibleRootItem() + + for name in set(self._items_by_name) - new_names: + item = self._items_by_name.pop(name) + root_item.removeRow(item.row()) + + if new_items: + root_item.appendRows(new_items) + + self._has_content = root_item.rowCount() > 0 + self._is_refreshing = False + self.refreshed.emit() + + @property + def is_refreshing(self): + """Model is refreshing. + + Returns: + bool: Model is refreshing + """ + + return self._is_refreshing + + @property + def has_content(self): + """Model has content. + + Returns: + bools: Have at least one task. + """ + + return self._has_content + + def headerData(self, section, orientation, role): + # Show nice labels in the header + if ( + role == QtCore.Qt.DisplayRole + and orientation == QtCore.Qt.Horizontal + ): + if section == 0: + return "Tasks" + + return super(TasksModel, self).headerData( + section, orientation, role + ) + + +class TasksWidget(QtWidgets.QWidget): + """Tasks widget. + + Widget that handles tasks view, model and selection. + + Args: + controller (AbstractWorkfilesFrontend): Workfiles controller. + parent (QtWidgets.QWidget): Parent widget. + handle_expected_selection (Optional[bool]): Handle expected selection. + """ + + def __init__(self, controller, parent, handle_expected_selection=False): + super(TasksWidget, self).__init__(parent) + + tasks_view = DeselectableTreeView(self) + tasks_view.setIndentation(0) + + tasks_model = TasksModel(controller) + tasks_proxy_model = QtCore.QSortFilterProxyModel() + tasks_proxy_model.setSourceModel(tasks_model) + + tasks_view.setModel(tasks_proxy_model) + + main_layout = QtWidgets.QHBoxLayout(self) + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(tasks_view, 1) + + controller.register_event_callback( + "tasks.refresh.finished", + self._on_tasks_refresh_finished + ) + controller.register_event_callback( + "selection.folder.changed", + self._folder_selection_changed + ) + controller.register_event_callback( + "expected_selection_changed", + self._on_expected_selection_change + ) + + selection_model = tasks_view.selectionModel() + selection_model.selectionChanged.connect(self._on_selection_change) + + tasks_model.refreshed.connect(self._on_tasks_model_refresh) + + self._controller = controller + self._tasks_view = tasks_view + self._tasks_model = tasks_model + self._tasks_proxy_model = tasks_proxy_model + + self._selected_folder_id = None + + self._handle_expected_selection = handle_expected_selection + self._expected_selection_data = None + + def _clear(self): + self._tasks_model.clear() + + def _on_tasks_refresh_finished(self, event): + """Tasks were refreshed in controller. + + Ignore if refresh was triggered by tasks model, or refreshed folder is + not the same as currently selected folder. + + Args: + event (Event): Event object. + """ + + # Refresh only if current folder id is the same + if ( + event["sender"] == SENDER_NAME + or event["folder_id"] != self._selected_folder_id + ): + return + self._tasks_model.refresh( + event["project_name"], self._selected_folder_id + ) + + def _folder_selection_changed(self, event): + self._selected_folder_id = event["folder_id"] + self._tasks_model.refresh( + event["project_name"], self._selected_folder_id + ) + + def _on_tasks_model_refresh(self): + if not self._set_expected_selection(): + self._on_selection_change() + self._tasks_proxy_model.sort(0) + + def _get_selected_item_ids(self): + selection_model = self._tasks_view.selectionModel() + for index in selection_model.selectedIndexes(): + task_id = index.data(ITEM_ID_ROLE) + task_name = index.data(ITEM_NAME_ROLE) + parent_id = index.data(PARENT_ID_ROLE) + if task_name is not None: + return parent_id, task_id, task_name + return self._selected_folder_id, None, None + + def _on_selection_change(self): + # Don't trigger task change during refresh + # - a task was deselected if that happens + # - can cause crash triggered during tasks refreshing + if self._tasks_model.is_refreshing: + return + + parent_id, task_id, task_name = self._get_selected_item_ids() + self._controller.set_selected_task(task_id, task_name) + + # Expected selection handling + def _on_expected_selection_change(self, event): + self._update_expected_selection(event.data) + + def _set_expected_selection(self): + if not self._handle_expected_selection: + return False + + if self._expected_selection_data is None: + return False + folder_id = self._expected_selection_data["folder_id"] + task_name = self._expected_selection_data["task_name"] + self._expected_selection_data = None + model_folder_id = self._tasks_model.get_last_folder_id() + if folder_id != model_folder_id: + return False + if task_name is not None: + index = self._tasks_model.get_index_by_name(task_name) + if index.isValid(): + proxy_index = self._tasks_proxy_model.mapFromSource(index) + self._tasks_view.setCurrentIndex(proxy_index) + self._controller.expected_task_selected(folder_id, task_name) + return True + + def _update_expected_selection(self, expected_data=None): + if not self._handle_expected_selection: + return + if expected_data is None: + expected_data = self._controller.get_expected_selection_data() + folder_data = expected_data.get("folder") + task_data = expected_data.get("task") + if ( + not folder_data + or not task_data + or not task_data["current"] + ): + return + folder_id = folder_data["id"] + self._expected_selection_data = { + "task_name": task_data["name"], + "folder_id": folder_id, + } + model_folder_id = self._tasks_model.get_last_folder_id() + if folder_id != model_folder_id or self._tasks_model.is_refreshing: + return + self._set_expected_selection() diff --git a/openpype/tools/ayon_utils/widgets/utils.py b/openpype/tools/ayon_utils/widgets/utils.py new file mode 100644 index 0000000000..8bc3b1ea9b --- /dev/null +++ b/openpype/tools/ayon_utils/widgets/utils.py @@ -0,0 +1,98 @@ +import os +from functools import partial + +from qtpy import QtCore, QtGui + +from openpype.tools.utils.lib import get_qta_icon_by_name_and_color + + +class RefreshThread(QtCore.QThread): + refresh_finished = QtCore.Signal(str) + + def __init__(self, thread_id, func, *args, **kwargs): + super(RefreshThread, self).__init__() + self._id = thread_id + self._callback = partial(func, *args, **kwargs) + self._exception = None + self._result = None + + @property + def id(self): + return self._id + + @property + def failed(self): + return self._exception is not None + + def run(self): + try: + self._result = self._callback() + except Exception as exc: + self._exception = exc + self.refresh_finished.emit(self.id) + + def get_result(self): + return self._result + + +class _IconsCache: + """Cache for icons.""" + + _cache = {} + _default = None + + @classmethod + def _get_cache_key(cls, icon_def): + parts = [] + icon_type = icon_def["type"] + if icon_type == "path": + parts = [icon_type, icon_def["path"]] + + elif icon_type == "awesome-font": + parts = [icon_type, icon_def["name"], icon_def["color"]] + return "|".join(parts) + + @classmethod + def get_icon(cls, icon_def): + icon_type = icon_def["type"] + cache_key = cls._get_cache_key(icon_def) + cache = cls._cache.get(cache_key) + if cache is not None: + return cache + + icon = None + if icon_type == "path": + path = icon_def["path"] + if os.path.exists(path): + icon = QtGui.QIcon(path) + + elif icon_type == "awesome-font": + icon_name = icon_def["name"] + icon_color = icon_def["color"] + icon = get_qta_icon_by_name_and_color(icon_name, icon_color) + if icon is None: + icon = get_qta_icon_by_name_and_color( + "fa.{}".format(icon_name), icon_color) + if icon is None: + icon = cls.get_default() + cls._cache[cache_key] = icon + return icon + + @classmethod + def get_default(cls): + pix = QtGui.QPixmap(1, 1) + pix.fill(QtCore.Qt.transparent) + return QtGui.QIcon(pix) + + +def get_qt_icon(icon_def): + """Returns icon from cache or creates new one. + + Args: + icon_def (dict[str, Any]): Icon definition. + + Returns: + QtGui.QIcon: Icon. + """ + + return _IconsCache.get_icon(icon_def) diff --git a/openpype/tools/launcher/actions.py b/openpype/tools/launcher/actions.py index 61660ee9b7..285b5d04ca 100644 --- a/openpype/tools/launcher/actions.py +++ b/openpype/tools/launcher/actions.py @@ -1,8 +1,5 @@ -import os - from qtpy import QtWidgets, QtGui -from openpype import PLUGINS_DIR from openpype import style from openpype import resources from openpype.lib import ( @@ -10,46 +7,7 @@ from openpype.lib import ( ApplictionExecutableNotFound, ApplicationLaunchFailed ) -from openpype.pipeline import ( - LauncherAction, - register_launcher_action_path, -) - - -def register_actions_from_paths(paths): - if not paths: - return - - for path in paths: - if not path: - continue - - if path.startswith("."): - print(( - "BUG: Relative paths are not allowed for security reasons. {}" - ).format(path)) - continue - - if not os.path.exists(path): - print("Path was not found: {}".format(path)) - continue - - register_launcher_action_path(path) - - -def register_config_actions(): - """Register actions from the configuration for Launcher""" - - actions_dir = os.path.join(PLUGINS_DIR, "actions") - if os.path.exists(actions_dir): - register_actions_from_paths([actions_dir]) - - -def register_environment_actions(): - """Register actions from AVALON_ACTIONS for Launcher.""" - - paths_str = os.environ.get("AVALON_ACTIONS") or "" - register_actions_from_paths(paths_str.split(os.pathsep)) +from openpype.pipeline import LauncherAction # TODO move to 'openpype.pipeline.actions' diff --git a/openpype/tools/utils/__init__.py b/openpype/tools/utils/__init__.py index d343353112..018088e916 100644 --- a/openpype/tools/utils/__init__.py +++ b/openpype/tools/utils/__init__.py @@ -15,6 +15,10 @@ from .widgets import ( IconButton, PixmapButton, SeparatorWidget, + VerticalExpandButton, + SquareButton, + RefreshButton, + GoToCurrentButton, ) from .views import DeselectableTreeView from .error_dialog import ErrorMessageBox @@ -60,6 +64,11 @@ __all__ = ( "PixmapButton", "SeparatorWidget", + "VerticalExpandButton", + "SquareButton", + "RefreshButton", + "GoToCurrentButton", + "DeselectableTreeView", "ErrorMessageBox", diff --git a/openpype/tools/utils/widgets.py b/openpype/tools/utils/widgets.py index a70437cc65..9223afecaa 100644 --- a/openpype/tools/utils/widgets.py +++ b/openpype/tools/utils/widgets.py @@ -6,10 +6,13 @@ import qtawesome from openpype.style import ( get_objected_colors, - get_style_image_path + get_style_image_path, + get_default_tools_icon_color, ) from openpype.lib.attribute_definitions import AbstractAttrDef +from .lib import get_qta_icon_by_name_and_color + log = logging.getLogger(__name__) @@ -777,3 +780,77 @@ class SeparatorWidget(QtWidgets.QFrame): self._orientation = orientation self._set_size(self._size) + + +def get_refresh_icon(): + return get_qta_icon_by_name_and_color( + "fa.refresh", get_default_tools_icon_color() + ) + + +def get_go_to_current_icon(): + return get_qta_icon_by_name_and_color( + "fa.arrow-down", get_default_tools_icon_color() + ) + + +class VerticalExpandButton(QtWidgets.QPushButton): + """Button which is expanding vertically. + + By default, button is a little bit smaller than other widgets like + QLineEdit. This button is expanding vertically to match size of + other widgets, next to it. + """ + + def __init__(self, parent=None): + super(VerticalExpandButton, self).__init__(parent) + + sp = self.sizePolicy() + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) + self.setSizePolicy(sp) + + +class SquareButton(QtWidgets.QPushButton): + """Make button square shape. + + Change width to match height on resize. + """ + + def __init__(self, *args, **kwargs): + super(SquareButton, self).__init__(*args, **kwargs) + + sp = self.sizePolicy() + sp.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum) + sp.setHorizontalPolicy(QtWidgets.QSizePolicy.Minimum) + self.setSizePolicy(sp) + self._ideal_width = None + + def showEvent(self, event): + super(SquareButton, self).showEvent(event) + self._ideal_width = self.height() + self.updateGeometry() + + def resizeEvent(self, event): + super(SquareButton, self).resizeEvent(event) + self._ideal_width = self.height() + self.updateGeometry() + + def sizeHint(self): + sh = super(SquareButton, self).sizeHint() + ideal_width = self._ideal_width + if ideal_width is None: + ideal_width = sh.height() + sh.setWidth(ideal_width) + return sh + + +class RefreshButton(VerticalExpandButton): + def __init__(self, parent=None): + super(RefreshButton, self).__init__(parent) + self.setIcon(get_refresh_icon()) + + +class GoToCurrentButton(VerticalExpandButton): + def __init__(self, parent=None): + super(GoToCurrentButton, self).__init__(parent) + self.setIcon(get_go_to_current_icon()) From bfb5868417f1fbf127b65bdf8cb214585a2312b7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Sep 2023 14:18:57 +0200 Subject: [PATCH 139/267] AYON: Fix task type short name conversion (#5641) * fix task type short name conversion * workfiles tool can query project entity * use project entity to fill task template data --- openpype/client/server/conversion_utils.py | 2 ++ openpype/tools/ayon_workfiles/abstract.py | 10 ++++++++++ openpype/tools/ayon_workfiles/control.py | 3 +++ .../tools/ayon_workfiles/models/hierarchy.py | 11 ++++++++++ .../tools/ayon_workfiles/models/workfiles.py | 20 ++++++++++++++----- 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/openpype/client/server/conversion_utils.py b/openpype/client/server/conversion_utils.py index f67a1ef9c4..8c18cb1c13 100644 --- a/openpype/client/server/conversion_utils.py +++ b/openpype/client/server/conversion_utils.py @@ -235,6 +235,8 @@ def convert_v4_project_to_v3(project): new_task_types = {} for task_type in task_types: name = task_type.pop("name") + # Change 'shortName' to 'short_name' + task_type["short_name"] = task_type.pop("shortName", None) new_task_types[name] = task_type config["tasks"] = new_task_types diff --git a/openpype/tools/ayon_workfiles/abstract.py b/openpype/tools/ayon_workfiles/abstract.py index e30a2c2499..f511181837 100644 --- a/openpype/tools/ayon_workfiles/abstract.py +++ b/openpype/tools/ayon_workfiles/abstract.py @@ -442,6 +442,16 @@ class AbstractWorkfilesBackend(AbstractWorkfilesCommon): pass + @abstractmethod + def get_project_entity(self): + """Get current project entity. + + Returns: + dict[str, Any]: Project entity data. + """ + + pass + @abstractmethod def get_folder_entity(self, folder_id): """Get folder entity by id. diff --git a/openpype/tools/ayon_workfiles/control.py b/openpype/tools/ayon_workfiles/control.py index fc8819bff3..1153a3c01f 100644 --- a/openpype/tools/ayon_workfiles/control.py +++ b/openpype/tools/ayon_workfiles/control.py @@ -193,6 +193,9 @@ class BaseWorkfileController( self._project_anatomy = Anatomy(self.get_current_project_name()) return self._project_anatomy + def get_project_entity(self): + return self._entities_model.get_project_entity() + def get_folder_entity(self, folder_id): return self._entities_model.get_folder_entity(folder_id) diff --git a/openpype/tools/ayon_workfiles/models/hierarchy.py b/openpype/tools/ayon_workfiles/models/hierarchy.py index 948c0b8a17..a1d51525da 100644 --- a/openpype/tools/ayon_workfiles/models/hierarchy.py +++ b/openpype/tools/ayon_workfiles/models/hierarchy.py @@ -77,8 +77,11 @@ class EntitiesModel(object): event_source = "entities.model" def __init__(self, controller): + project_cache = CacheItem() + project_cache.set_invalid({}) folders_cache = CacheItem() folders_cache.set_invalid({}) + self._project_cache = project_cache self._folders_cache = folders_cache self._tasks_cache = {} @@ -90,6 +93,7 @@ class EntitiesModel(object): self._controller = controller def reset(self): + self._project_cache.set_invalid({}) self._folders_cache.set_invalid({}) self._tasks_cache = {} @@ -99,6 +103,13 @@ class EntitiesModel(object): def refresh(self): self._refresh_folders_cache() + def get_project_entity(self): + if not self._project_cache.is_valid: + project_name = self._controller.get_current_project_name() + project_entity = ayon_api.get_project(project_name) + self._project_cache.update_data(project_entity) + return self._project_cache.get_data() + def get_folder_items(self, sender): if not self._folders_cache.is_valid: self._refresh_folders_cache(sender) diff --git a/openpype/tools/ayon_workfiles/models/workfiles.py b/openpype/tools/ayon_workfiles/models/workfiles.py index eb82f62de3..316d8b2a16 100644 --- a/openpype/tools/ayon_workfiles/models/workfiles.py +++ b/openpype/tools/ayon_workfiles/models/workfiles.py @@ -43,13 +43,21 @@ def get_folder_template_data(folder): } -def get_task_template_data(task): +def get_task_template_data(project_entity, task): if not task: return {} + short_name = None + task_type_name = task["taskType"] + for task_type_info in project_entity["config"]["taskTypes"]: + if task_type_info["name"] == task_type_name: + short_name = task_type_info["shortName"] + break + return { "task": { "name": task["name"], - "type": task["taskType"] + "type": task_type_name, + "short": short_name, } } @@ -145,12 +153,13 @@ class WorkareaModel: self._fill_data_by_folder_id[folder_id] = fill_data return copy.deepcopy(fill_data) - def _get_task_data(self, folder_id, task_id): + def _get_task_data(self, project_entity, folder_id, task_id): task_data = self._task_data_by_folder_id.setdefault(folder_id, {}) if task_id not in task_data: task = self._controller.get_task_entity(task_id) if task: - task_data[task_id] = get_task_template_data(task) + task_data[task_id] = get_task_template_data( + project_entity, task) return copy.deepcopy(task_data[task_id]) def _prepare_fill_data(self, folder_id, task_id): @@ -159,7 +168,8 @@ class WorkareaModel: base_data = self._get_base_data() folder_data = self._get_folder_data(folder_id) - task_data = self._get_task_data(folder_id, task_id) + project_entity = self._controller.get_project_entity() + task_data = self._get_task_data(project_entity, folder_id, task_id) base_data.update(folder_data) base_data.update(task_data) From dd2255f8fd13919d8e3d2d2df02652a515f00a57 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Sep 2023 20:39:33 +0800 Subject: [PATCH 140/267] jakub's comment --- openpype/hosts/nuke/api/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index a814615164..fe7b52cd8a 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -808,8 +808,8 @@ class ExporterReviewMov(ExporterReview): filename = os.path.basename(self.path_in) self.file = filename if ".{}".format(self.ext) not in self.file: - wrg_ext = filename.split(".")[-1] - self.file = filename.replace(wrg_ext, self.ext) + original_ext = filename.split(".")[-1] + self.file = filename.replace(original_ext, self.ext) self.path = os.path.join( self.staging_dir, self.file).replace("\\", "/") From d744a486d64134455ffc636fef92ce09c9742b5f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 22 Sep 2023 21:38:30 +0800 Subject: [PATCH 141/267] edit the settings where deprecated_setting used when it enabled; current_setting adopted when deprecated_setting diabled in extract_reiew_baking_streams --- .../publish/extract_review_baking_streams.py | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py b/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py index 59a3f659c9..d9ae673c2c 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py @@ -28,30 +28,18 @@ class ExtractReviewDataBakingStreams(publish.Extractor): @classmethod def apply_settings(cls, project_settings): """just in case there are some old presets - in deprecrated ExtractReviewDataMov Plugins + in deprecated ExtractReviewDataMov Plugins """ nuke_publish = project_settings["nuke"]["publish"] - deprecrated_review_settings = nuke_publish["ExtractReviewDataMov"] - current_review_settings = ( - nuke_publish["ExtractReviewDataBakingStreams"] - ) - if deprecrated_review_settings["viewer_lut_raw"] == ( - current_review_settings["viewer_lut_raw"] - ): - cls.viewer_lut_raw = ( - current_review_settings["viewer_lut_raw"] - ) + deprecated_setting = nuke_publish["ExtractReviewDataMov"] + current_setting = nuke_publish["ExtractReviewDataBakingStreams"] + if not deprecated_setting["enabled"]: + if current_setting["enabled"]: + cls.viewer_lut_raw = current_setting["viewer_lut_raw"] + cls.outputs = current_setting["outputs"] else: - cls.viewer_lut_raw = ( - deprecrated_review_settings["viewer_lut_raw"] - ) - - if deprecrated_review_settings["outputs"] == ( - current_review_settings["outputs"] - ): - cls.outputs = current_review_settings["outputs"] - else: - cls.outputs = deprecrated_review_settings["outputs"] + cls.viewer_lut_raw = deprecated_setting["viewer_lut_raw"] + cls.outputs = deprecated_setting["outputs"] def process(self, instance): families = set(instance.data["families"]) From d498afbf489eefbe3a2733a17d6f8ebe988abf79 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 22 Sep 2023 15:24:17 +0100 Subject: [PATCH 142/267] New family ue_yeticache, new creator and extractor --- .../plugins/create/create_unreal_yeticache.py | 39 ++++++++++++ .../plugins/publish/collect_yeti_cache.py | 2 +- .../publish/extract_unreal_yeticache.py | 62 +++++++++++++++++++ .../plugins/publish/collect_resources_path.py | 3 +- openpype/plugins/publish/integrate.py | 3 +- 5 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/maya/plugins/create/create_unreal_yeticache.py create mode 100644 openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py diff --git a/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py b/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py new file mode 100644 index 0000000000..8ff3dccea2 --- /dev/null +++ b/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py @@ -0,0 +1,39 @@ +from openpype.hosts.maya.api import ( + lib, + plugin +) +from openpype.lib import NumberDef + + +class CreateYetiCache(plugin.MayaCreator): + """Output for procedural plugin nodes of Yeti """ + + identifier = "io.openpype.creators.maya.unrealyeticache" + label = "Unreal Yeti Cache" + family = "ue_yeticache" + icon = "pagelines" + + def get_instance_attr_defs(self): + + defs = [ + NumberDef("preroll", + label="Preroll", + minimum=0, + default=0, + decimals=0) + ] + + # Add animation data without step and handles + defs.extend(lib.collect_animation_defs()) + remove = {"step", "handleStart", "handleEnd"} + defs = [attr_def for attr_def in defs if attr_def.key not in remove] + + # Add samples after frame range + defs.append( + NumberDef("samples", + label="Samples", + default=3, + decimals=0) + ) + + return defs diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py b/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py index 4dcda29050..426e330f89 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py @@ -39,7 +39,7 @@ class CollectYetiCache(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.45 label = "Collect Yeti Cache" - families = ["yetiRig", "yeticache"] + families = ["yetiRig", "yeticache", "ue_yeticache"] hosts = ["maya"] def process(self, instance): diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py b/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py new file mode 100644 index 0000000000..2279d52111 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py @@ -0,0 +1,62 @@ +import os +import json + +from maya import cmds + +from openpype.pipeline import publish + + +class ExtractYetiCache(publish.Extractor): + """Producing Yeti cache files using scene time range. + + This will extract Yeti cache file sequence and fur settings. + """ + + label = "Extract Yeti Cache" + hosts = ["maya"] + families = ["ue_yeticache"] + + def process(self, instance): + + yeti_nodes = cmds.ls(instance, type="pgYetiMaya") + if not yeti_nodes: + raise RuntimeError("No pgYetiMaya nodes found in the instance") + + # Define extract output file path + dirname = self.staging_dir(instance) + + # Collect information for writing cache + start_frame = instance.data["frameStartHandle"] + end_frame = instance.data["frameEndHandle"] + preroll = instance.data["preroll"] + if preroll > 0: + start_frame -= preroll + + kwargs = {} + samples = instance.data.get("samples", 0) + if samples == 0: + kwargs.update({"sampleTimes": "0.0 1.0"}) + else: + kwargs.update({"samples": samples}) + + self.log.debug(f"Writing out cache {start_frame} - {end_frame}") + filename = "{0}.abc".format(instance.name) + path = os.path.join(dirname, filename) + cmds.pgYetiCommand(yeti_nodes, + writeAlembic=path, + range=(start_frame, end_frame), + asUnrealAbc=True, + **kwargs) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'abc', + 'ext': 'abc', + 'files': filename, + 'stagingDir': dirname + } + instance.data["representations"].append(representation) + + self.log.debug(f"Extracted {instance} to {dirname}") diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index f96dd0ae18..6840509f79 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -62,7 +62,8 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "effect", "staticMesh", "skeletalMesh", - "xgen" + "xgen", + "ue_yeticache" ] def process(self, instance): diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 7e48155b9e..24fbc0d8e7 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -139,7 +139,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "simpleUnrealTexture", "online", "uasset", - "blendScene" + "blendScene", + "ue_yeticache" ] default_template_name = "publish" From 9781104f4e69f0d557fc4ec7e32afa8d26583e11 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 22 Sep 2023 15:49:38 +0100 Subject: [PATCH 143/267] Implemented Unreal loader --- .../unreal/plugins/load/load_yeticache.py | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 openpype/hosts/unreal/plugins/load/load_yeticache.py diff --git a/openpype/hosts/unreal/plugins/load/load_yeticache.py b/openpype/hosts/unreal/plugins/load/load_yeticache.py new file mode 100644 index 0000000000..59dbdbee31 --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_yeticache.py @@ -0,0 +1,151 @@ +# -*- coding: utf-8 -*- +"""Loader for Yeti Cache.""" +import os + +from openpype.pipeline import ( + get_representation_path, + AYON_CONTAINER_ID +) +from openpype.hosts.unreal.api import plugin +from openpype.hosts.unreal.api import pipeline as unreal_pipeline +import unreal # noqa + + +class YetiLoader(plugin.Loader): + """Load Yeti Cache""" + + families = ["ue_yeticache"] + label = "Import Yeti" + representations = ["abc"] + icon = "pagelines" + color = "orange" + + @staticmethod + def get_task(filename, asset_dir, asset_name, replace): + task = unreal.AssetImportTask() + options = unreal.AbcImportSettings() + + task.set_editor_property('filename', filename) + task.set_editor_property('destination_path', asset_dir) + task.set_editor_property('destination_name', asset_name) + task.set_editor_property('replace_existing', replace) + task.set_editor_property('automated', True) + task.set_editor_property('save', True) + + task.options = options + + return task + + def load(self, context, name, namespace, options): + """Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + + """ + # Create directory for asset and Ayon container + root = "/Game/Ayon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + asset_name = f"{asset}_{name}" if asset else f"{name}" + version = context.get('version') + # Check if version is hero version and use different name + if not version.get("name") and version.get('type') == "hero_version": + name_version = f"{name}_hero" + else: + name_version = f"{name}_v{version.get('name'):03d}" + + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + f"{root}/{asset}/{name_version}", suffix="") + + container_name += suffix + + if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): + unreal.EditorAssetLibrary.make_directory(asset_dir) + + path = self.filepath_from_context(context) + task = self.get_task(path, asset_dir, asset_name, False) + + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501 + + # Create Asset Container + unreal_pipeline.create_container( + container=container_name, path=asset_dir) + + data = { + "schema": "ayon:container-2.0", + "id": AYON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + name = container["asset_name"] + source_path = get_representation_path(representation) + destination_path = container["namespace"] + + task = self.get_task(source_path, destination_path, name, True) + + # do import fbx and replace existing data + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + container_path = f'{container["namespace"]}/{container["objectName"]}' + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = unreal.EditorAssetLibrary.list_assets( + destination_path, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + def remove(self, container): + path = container["namespace"] + parent_path = os.path.dirname(path) + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) From 41b207d657f8c4da054248791a946f0c86657000 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 22 Sep 2023 15:58:27 +0100 Subject: [PATCH 144/267] Hound fixes --- openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py b/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py index 2279d52111..816e125c33 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py @@ -1,5 +1,4 @@ import os -import json from maya import cmds From e27930c42064f9a6903bcedc0998c8e8b6e2eacb Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 23 Sep 2023 03:24:16 +0000 Subject: [PATCH 145/267] [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 483b70436a..d1ebde3d04 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.1-nightly.1" +__version__ = "3.17.1-nightly.2" From 60d75300114f9ebba17f3882a81627080ef92ac8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 23 Sep 2023 03:24:52 +0000 Subject: [PATCH 146/267] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0762eb2f20..87d904fc84 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,8 @@ body: label: Version description: What version are you running? Look to OpenPype Tray options: + - 3.17.1-nightly.2 + - 3.17.1-nightly.1 - 3.17.0 - 3.16.7 - 3.16.7-nightly.2 @@ -133,8 +135,6 @@ body: - 3.14.10-nightly.5 - 3.14.10-nightly.4 - 3.14.10-nightly.3 - - 3.14.10-nightly.2 - - 3.14.10-nightly.1 validations: required: true - type: dropdown From 21d547a085c1b06fc9f26829920b24dd0068d01b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sun, 24 Sep 2023 12:49:30 +0800 Subject: [PATCH 147/267] introduce the function for checking the filename to see if it consists of the frame hashes element --- openpype/hosts/nuke/api/lib.py | 18 ++++++++++++++++++ openpype/hosts/nuke/api/plugin.py | 9 +++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 41e6a27cef..ed517b472c 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3397,3 +3397,21 @@ def create_viewer_profile_string(viewer, display=None, path_like=False): if path_like: return "{}/{}".format(display, viewer) return "{} ({})".format(viewer, display) + +def get_file_with_name_and_hashes(original_path, name): + """Function to get the ranmed filename with frame hashes + + Args: + original_path (str): the filename with frame hashes + name (str): the name of the tags + + Returns: + filename: the renamed filename with the tag + """ + filename = os.path.basename(original_path) + fhead = filename.split(".")[0] + if "#" in fhead: + fhead = fhead.replace("#", "")[:-1] + new_fhead = "{}.{}".format(fhead, name) + filename = filename.replace(fhead, new_fhead) + return filename diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index fe7b52cd8a..b7927738d6 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -38,7 +38,8 @@ from .lib import ( get_node_data, get_view_process_node, get_viewer_config_from_string, - deprecated + deprecated, + get_file_with_name_and_hashes ) from .pipeline import ( list_instances, @@ -805,11 +806,12 @@ 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) + filename = get_file_with_name_and_hashes( + self.path_in, self.name) self.file = filename if ".{}".format(self.ext) not in self.file: original_ext = filename.split(".")[-1] - self.file = filename.replace(original_ext, self.ext) + self.file = filename.replace(original_ext, ext) self.path = os.path.join( self.staging_dir, self.file).replace("\\", "/") @@ -931,7 +933,6 @@ class ExporterReviewMov(ExporterReview): self.log.debug("Path: {}".format(self.path)) write_node["file"].setValue(str(self.path)) write_node["file_type"].setValue(str(self.ext)) - self.log.debug("{0}".format(self.ext)) # Knobs `meta_codec` and `mov64_codec` are not available on centos. # TODO shouldn't this come from settings on outputs? try: From 6f858a80ca4839bb581685d48d4d5d6304ac5403 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sun, 24 Sep 2023 12:50:42 +0800 Subject: [PATCH 148/267] hound --- openpype/hosts/nuke/api/lib.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ed517b472c..2c5838ffd3 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3398,6 +3398,7 @@ def create_viewer_profile_string(viewer, display=None, path_like=False): return "{}/{}".format(display, viewer) return "{} ({})".format(viewer, display) + def get_file_with_name_and_hashes(original_path, name): """Function to get the ranmed filename with frame hashes From 7dd64ff210078716b80271ab33e81a4ba7266993 Mon Sep 17 00:00:00 2001 From: Kayla Date: Sun, 24 Sep 2023 13:07:03 +0800 Subject: [PATCH 149/267] 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 150/267] 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 151/267] 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 5f7f4f08d1cf1ac38c4016e3c4d4bb2211b73767 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 25 Sep 2023 14:23:17 +0800 Subject: [PATCH 152/267] fix the slate in --- 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 b7927738d6..f587d109c1 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -591,6 +591,11 @@ class ExporterReview(object): # get first and last frame self.first_frame = min(self.collection.indexes) self.last_frame = max(self.collection.indexes) + + first = self.instance.data.get("frameStartHandle", None) + if first: + if first > self.first_frame: + self.first_frame = first else: self.fname = os.path.basename(self.path_in) self.fhead = os.path.splitext(self.fname)[0] + "." From 4c854600cbbc3a7d4d1475893ab3f900d2c92605 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 25 Sep 2023 10:07:37 +0100 Subject: [PATCH 153/267] Renamed family to yeticacheUE --- openpype/hosts/maya/plugins/create/create_unreal_yeticache.py | 2 +- openpype/hosts/maya/plugins/publish/collect_yeti_cache.py | 2 +- openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py | 2 +- openpype/hosts/unreal/plugins/load/load_yeticache.py | 2 +- openpype/plugins/publish/collect_resources_path.py | 2 +- openpype/plugins/publish/integrate.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py b/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py index 8ff3dccea2..defa6ed6d9 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py @@ -10,7 +10,7 @@ class CreateYetiCache(plugin.MayaCreator): identifier = "io.openpype.creators.maya.unrealyeticache" label = "Unreal Yeti Cache" - family = "ue_yeticache" + family = "yeticacheUE" icon = "pagelines" def get_instance_attr_defs(self): diff --git a/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py b/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py index 426e330f89..7a1516997a 100644 --- a/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py +++ b/openpype/hosts/maya/plugins/publish/collect_yeti_cache.py @@ -39,7 +39,7 @@ class CollectYetiCache(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.45 label = "Collect Yeti Cache" - families = ["yetiRig", "yeticache", "ue_yeticache"] + families = ["yetiRig", "yeticache", "yeticacheUE"] hosts = ["maya"] def process(self, instance): diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py b/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py index 816e125c33..e72146a871 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py @@ -13,7 +13,7 @@ class ExtractYetiCache(publish.Extractor): label = "Extract Yeti Cache" hosts = ["maya"] - families = ["ue_yeticache"] + families = ["yeticacheUE"] def process(self, instance): diff --git a/openpype/hosts/unreal/plugins/load/load_yeticache.py b/openpype/hosts/unreal/plugins/load/load_yeticache.py index 59dbdbee31..328f92d020 100644 --- a/openpype/hosts/unreal/plugins/load/load_yeticache.py +++ b/openpype/hosts/unreal/plugins/load/load_yeticache.py @@ -14,7 +14,7 @@ import unreal # noqa class YetiLoader(plugin.Loader): """Load Yeti Cache""" - families = ["ue_yeticache"] + families = ["yeticacheUE"] label = "Import Yeti" representations = ["abc"] icon = "pagelines" diff --git a/openpype/plugins/publish/collect_resources_path.py b/openpype/plugins/publish/collect_resources_path.py index 6840509f79..57a612c5ae 100644 --- a/openpype/plugins/publish/collect_resources_path.py +++ b/openpype/plugins/publish/collect_resources_path.py @@ -63,7 +63,7 @@ class CollectResourcesPath(pyblish.api.InstancePlugin): "staticMesh", "skeletalMesh", "xgen", - "ue_yeticache" + "yeticacheUE" ] def process(self, instance): diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 24fbc0d8e7..ce24dad1b5 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -140,7 +140,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "online", "uasset", "blendScene", - "ue_yeticache" + "yeticacheUE" ] default_template_name = "publish" From 51875f592e95a30f84563835fa63d7b4c31e3dc9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Sep 2023 11:21:02 +0200 Subject: [PATCH 154/267] instance data keys should not be optional --- openpype/hosts/nuke/api/plugin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 1e318e17cf..4ce314f6fb 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -580,8 +580,9 @@ class ExporterReview(object): def get_file_info(self): if self.collection: # get path - self.fname = os.path.basename(self.collection.format( - "{head}{padding}{tail}")) + self.fname = os.path.basename( + self.collection.format("{head}{padding}{tail}") + ) self.fhead = self.collection.format("{head}") # get first and last frame @@ -590,8 +591,8 @@ class ExporterReview(object): else: self.fname = os.path.basename(self.path_in) self.fhead = os.path.splitext(self.fname)[0] + "." - self.first_frame = self.instance.data.get("frameStartHandle", None) - self.last_frame = self.instance.data.get("frameEndHandle", None) + self.first_frame = self.instance.data["frameStartHandle"] + self.last_frame = self.instance.data["frameEndHandle"] if "#" in self.fhead: self.fhead = self.fhead.replace("#", "")[:-1] From 4289997553340bccb7bb682d6c14ac4b51ca6939 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Sep 2023 11:21:28 +0200 Subject: [PATCH 155/267] slate frame exception - in case it already exists --- openpype/hosts/nuke/api/plugin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 4ce314f6fb..6d5d7eddf1 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -588,6 +588,12 @@ class ExporterReview(object): # get first and last frame self.first_frame = min(self.collection.indexes) self.last_frame = max(self.collection.indexes) + + # make sure slate frame is not included + frame_start_handle = self.instance.data["frameStartHandle"] + if frame_start_handle > self.first_frame: + self.first_frame = frame_start_handle + else: self.fname = os.path.basename(self.path_in) self.fhead = os.path.splitext(self.fname)[0] + "." From 50bae2d049092f7f43e9ef619f1b65ee0c14db25 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 25 Sep 2023 10:34:53 +0100 Subject: [PATCH 156/267] Check if Groom plugin is active when loading in Unreal --- .../unreal/plugins/load/load_yeticache.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/openpype/hosts/unreal/plugins/load/load_yeticache.py b/openpype/hosts/unreal/plugins/load/load_yeticache.py index 328f92d020..92f080d7c5 100644 --- a/openpype/hosts/unreal/plugins/load/load_yeticache.py +++ b/openpype/hosts/unreal/plugins/load/load_yeticache.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Loader for Yeti Cache.""" import os +import json from openpype.pipeline import ( get_representation_path, @@ -36,6 +37,28 @@ class YetiLoader(plugin.Loader): return task + @staticmethod + def is_groom_module_active(): + """ + Check if Groom plugin is active. + + This is a workaround, because the Unreal python API don't have + any method to check if plugin is active. + """ + prj_file = unreal.Paths.get_project_file_path() + + with open(prj_file, "r") as fp: + data = json.load(fp) + + plugins = data.get("Plugins") + + if not plugins: + return False + + plugin_names = [p.get("Name") for p in plugins] + + return "HairStrands" in plugin_names + def load(self, context, name, namespace, options): """Load and containerise representation into Content Browser. @@ -58,6 +81,10 @@ class YetiLoader(plugin.Loader): list(str): list of container content """ + # Check if Groom plugin is active + if not self.is_groom_module_active(): + raise RuntimeError("Groom plugin is not activated.") + # Create directory for asset and Ayon container root = "/Game/Ayon/Assets" asset = context.get('asset').get('name') From 5cf8fdbb6ca60327634ed5b8110a395410d5be51 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 25 Sep 2023 10:45:28 +0100 Subject: [PATCH 157/267] Do not use version in the import folder --- .../plugins/create/create_unreal_yeticache.py | 2 +- .../unreal/plugins/load/load_yeticache.py | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py b/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py index defa6ed6d9..c9f9cd9ba8 100644 --- a/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py +++ b/openpype/hosts/maya/plugins/create/create_unreal_yeticache.py @@ -9,7 +9,7 @@ class CreateYetiCache(plugin.MayaCreator): """Output for procedural plugin nodes of Yeti """ identifier = "io.openpype.creators.maya.unrealyeticache" - label = "Unreal Yeti Cache" + label = "Unreal - Yeti Cache" family = "yeticacheUE" icon = "pagelines" diff --git a/openpype/hosts/unreal/plugins/load/load_yeticache.py b/openpype/hosts/unreal/plugins/load/load_yeticache.py index 92f080d7c5..bb6c2d78cc 100644 --- a/openpype/hosts/unreal/plugins/load/load_yeticache.py +++ b/openpype/hosts/unreal/plugins/load/load_yeticache.py @@ -90,18 +90,20 @@ class YetiLoader(plugin.Loader): asset = context.get('asset').get('name') suffix = "_CON" asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version') - # Check if version is hero version and use different name - if not version.get("name") and version.get('type') == "hero_version": - name_version = f"{name}_hero" - else: - name_version = f"{name}_v{version.get('name'):03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name_version}", suffix="") + f"{root}/{asset}/{name}", suffix="") + + unique_number = 1 + while unreal.EditorAssetLibrary.does_directory_exist( + f"{asset_dir}_{unique_number:02}" + ): + unique_number += 1 + + asset_dir = f"{asset_dir}_{unique_number:02}" + container_name = f"{container_name}_{unique_number:02}{suffix}" - container_name += suffix if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): unreal.EditorAssetLibrary.make_directory(asset_dir) From 24e6b756ec44976667500129df08cb3b7fae495b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 25 Sep 2023 10:46:33 +0100 Subject: [PATCH 158/267] Hound fix --- openpype/hosts/unreal/plugins/load/load_yeticache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_yeticache.py b/openpype/hosts/unreal/plugins/load/load_yeticache.py index bb6c2d78cc..22f5029bac 100644 --- a/openpype/hosts/unreal/plugins/load/load_yeticache.py +++ b/openpype/hosts/unreal/plugins/load/load_yeticache.py @@ -104,7 +104,6 @@ class YetiLoader(plugin.Loader): asset_dir = f"{asset_dir}_{unique_number:02}" container_name = f"{container_name}_{unique_number:02}{suffix}" - if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): unreal.EditorAssetLibrary.make_directory(asset_dir) From 41e81ef7a87faced226f2bcc6b904e58de5f78e0 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 25 Sep 2023 18:03:22 +0800 Subject: [PATCH 159/267] rename the function and the elaborate the docstring --- openpype/hosts/nuke/api/lib.py | 6 ++++-- openpype/hosts/nuke/api/plugin.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 2c5838ffd3..d95839bb8d 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3399,8 +3399,10 @@ def create_viewer_profile_string(viewer, display=None, path_like=False): return "{} ({})".format(viewer, display) -def get_file_with_name_and_hashes(original_path, name): - """Function to get the ranmed filename with frame hashes +def get_head_filename_without_hashes(original_path, name): + """Function to get the ranmed 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 Args: original_path (str): the filename with frame hashes diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 97e7c3ad8c..1550a60f32 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_file_with_name_and_hashes + get_head_filename_without_hashes ) from .pipeline import ( list_instances, @@ -813,7 +813,7 @@ class ExporterReviewMov(ExporterReview): self.file = self.fhead + self.name + ".{}".format(self.ext) if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: - filename = get_file_with_name_and_hashes( + filename = get_head_filename_without_hashes( self.path_in, self.name) self.file = filename if ".{}".format(self.ext) not in self.file: From 22ce181f2de6b1ed001a135442c1884a71882599 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 25 Sep 2023 18:24:48 +0800 Subject: [PATCH 160/267] make sure the deprecated setting used when it enabled while the current setting is used when the deprecrated setting diabled --- openpype/settings/ayon_settings.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index a66e1b6ec6..b43e0b7c5f 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -754,10 +754,9 @@ def _convert_nuke_project_settings(ayon_settings, output): current_review_settings = ( ayon_publish["ExtractReviewDataBakingStreams"] ) - if deprecrated_review_settings["outputs"] == ( - current_review_settings["outputs"] - ): - outputs_settings = current_review_settings["outputs"] + if not deprecrated_review_settings["enabled"]: + if current_review_settings["enabled"]: + outputs_settings = current_review_settings["outputs"] else: outputs_settings = deprecrated_review_settings["outputs"] From 3da4bac77db45edff25df7e4958a301ff5775cbc Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 25 Sep 2023 18:26:16 +0800 Subject: [PATCH 161/267] typo in lib --- openpype/hosts/nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index d95839bb8d..3617133d2b 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3400,7 +3400,7 @@ def create_viewer_profile_string(viewer, display=None, path_like=False): def get_head_filename_without_hashes(original_path, name): - """Function to get the ranmed head filename without frame hashes + """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 From e0ce8013f46a1cf1c46774f43697930c09a7fd6a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 25 Sep 2023 11:32:44 +0100 Subject: [PATCH 162/267] Use f-string --- openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py b/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py index e72146a871..963285093e 100644 --- a/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py +++ b/openpype/hosts/maya/plugins/publish/extract_unreal_yeticache.py @@ -39,7 +39,7 @@ class ExtractYetiCache(publish.Extractor): kwargs.update({"samples": samples}) self.log.debug(f"Writing out cache {start_frame} - {end_frame}") - filename = "{0}.abc".format(instance.name) + filename = f"{instance.name}.abc" path = os.path.join(dirname, filename) cmds.pgYetiCommand(yeti_nodes, writeAlembic=path, From 0e594b39cdf836b8ce41637efba825a3863deae4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Sep 2023 12:52:51 +0200 Subject: [PATCH 163/267] no need to check `config_data` exits in this section of code. --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index 44cff34c67..a77dc5763a 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -177,7 +177,7 @@ def get_colorspace_name_from_filepath( return None # validate matching colorspace with config - if validate and config_data: + if validate: validate_imageio_colorspace_in_config( config_data["path"], colorspace_name) From 5d1f2b0d9ed3ff44f1f924870e9a829c9ee4ee12 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Sep 2023 13:01:43 +0200 Subject: [PATCH 164/267] typo --- openpype/pipeline/colorspace.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index a77dc5763a..a67457b1bf 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -328,7 +328,7 @@ def parse_colorspace_from_filepath( str: name of colorspace """ def _get_colorspace_match_regex(colorspaces): - """Return a regex patter + """Return a regex pattern Allows to search a colorspace match in a filename From 8b76238b2d8199e2af541365e6ef074ef1c95825 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 25 Sep 2023 19:25:44 +0800 Subject: [PATCH 165/267] fixing get_head_filename_without_hashes not being able to get multiple hashes & some strip fix --- openpype/hosts/nuke/api/lib.py | 8 ++++++-- openpype/hosts/nuke/api/plugin.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 3617133d2b..29e7c88c71 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3406,15 +3406,19 @@ def get_head_filename_without_hashes(original_path, name): Args: original_path (str): the filename with frame hashes + e.g. "renderAssetMain.####.exr" name (str): the name of the tags + e.g. "baking" Returns: filename: the renamed filename with the tag + e.g. "renderAssetMain.baking.####.exr" """ filename = os.path.basename(original_path) fhead = filename.split(".")[0] - if "#" in fhead: - fhead = fhead.replace("#", "")[:-1] + tmp_fhead = re.sub("\d", "#", fhead) + if "#" in tmp_fhead: + fhead = tmp_fhead.replace("#", "").rstrip(".") new_fhead = "{}.{}".format(fhead, name) filename = filename.replace(fhead, new_fhead) return filename diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 1550a60f32..ca31068943 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -817,7 +817,7 @@ class ExporterReviewMov(ExporterReview): self.path_in, self.name) self.file = filename if ".{}".format(self.ext) not in self.file: - original_ext = filename.split(".")[-1] + original_ext = os.path.splitext(filename)[-1].strip(".") # noqa self.file = filename.replace(original_ext, ext) self.path = os.path.join( From e2509a9447c5900f4c469a2c81e89c9a79a2524e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 25 Sep 2023 19:29:35 +0800 Subject: [PATCH 166/267] hound --- openpype/hosts/nuke/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 29e7c88c71..7b1aaa8fe0 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3415,8 +3415,8 @@ def get_head_filename_without_hashes(original_path, name): e.g. "renderAssetMain.baking.####.exr" """ filename = os.path.basename(original_path) - fhead = filename.split(".")[0] - tmp_fhead = re.sub("\d", "#", fhead) + fhead = os.path.splitext(filename)[0].strip(".") + tmp_fhead = re.sub(r"\d", "#", fhead) if "#" in tmp_fhead: fhead = tmp_fhead.replace("#", "").rstrip(".") new_fhead = "{}.{}".format(fhead, name) From c77faa4be4f0db9c2d6f8d083bae70c41f0fc776 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Sep 2023 13:42:22 +0200 Subject: [PATCH 167/267] Fix audio node source in - source out on updating audio version --- .../hosts/maya/plugins/load/load_audio.py | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index ecf98303d2..a8a878d49d 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -1,12 +1,6 @@ from maya import cmds, mel -from openpype.client import ( - get_asset_by_id, - get_subset_by_id, - get_version_by_id, -) from openpype.pipeline import ( - get_current_project_name, load, get_representation_path, ) @@ -67,7 +61,26 @@ class AudioLoader(load.LoaderPlugin): activate_sound = current_sound == audio_node path = get_representation_path(representation) - cmds.setAttr("{}.filename".format(audio_node), path, type="string") + + cmds.sound( + audio_node, + edit=True, + file=path + ) + + # The source start + end does not automatically update itself to the + # length of thew new audio file, even though maya does do that when + # when creating a new audio node. So to update we compute it manually. + # This would however override any source start and source end a user + # might have done on the original audio node after load. + audio_frame_count = cmds.getAttr("{}.frameCount".format(audio_node)) + audio_sample_rate = cmds.getAttr("{}.sampleRate".format(audio_node)) + duration_in_seconds = audio_frame_count / audio_sample_rate + fps = mel.eval('currentTimeUnitToFPS()') # workfile FPS + source_start = 0 + source_end = (duration_in_seconds * fps) + cmds.setAttr("{}.sourceStart".format(audio_node), source_start) + cmds.setAttr("{}.sourceEnd".format(audio_node), source_end) if activate_sound: # maya by default deactivates it from timeline on file change @@ -84,26 +97,6 @@ class AudioLoader(load.LoaderPlugin): type="string" ) - # Set frame range. - project_name = get_current_project_name() - version = get_version_by_id( - project_name, representation["parent"], fields=["parent"] - ) - subset = get_subset_by_id( - project_name, version["parent"], fields=["parent"] - ) - asset = get_asset_by_id( - project_name, - subset["parent"], - fields=["parent", "data.frameStart", "data.frameEnd"] - ) - - source_start = 1 - asset["data"]["frameStart"] - source_end = asset["data"]["frameEnd"] - - cmds.setAttr("{}.sourceStart".format(audio_node), source_start) - cmds.setAttr("{}.sourceEnd".format(audio_node), source_end) - def switch(self, container, representation): self.update(container, representation) From 905038f6e86ef5c8116f852cab7547d7ea8d6ac8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 25 Sep 2023 13:42:39 +0200 Subject: [PATCH 168/267] Fix typo --- openpype/hosts/maya/plugins/load/load_audio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/load/load_audio.py b/openpype/hosts/maya/plugins/load/load_audio.py index a8a878d49d..90cadb31b1 100644 --- a/openpype/hosts/maya/plugins/load/load_audio.py +++ b/openpype/hosts/maya/plugins/load/load_audio.py @@ -70,7 +70,7 @@ class AudioLoader(load.LoaderPlugin): # The source start + end does not automatically update itself to the # length of thew new audio file, even though maya does do that when - # when creating a new audio node. So to update we compute it manually. + # creating a new audio node. So to update we compute it manually. # This would however override any source start and source end a user # might have done on the original audio node after load. audio_frame_count = cmds.getAttr("{}.frameCount".format(audio_node)) From b0ae4257f95f56a4d5510a48516945e5bfb4edbb Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Sep 2023 15:23:02 +0200 Subject: [PATCH 169/267] missing `allowed_exts` issue and unit tests fix --- openpype/pipeline/colorspace.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index a67457b1bf..2800050496 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -27,6 +27,9 @@ class CachedData: has_compatible_ocio_package = None config_version_data = {} ocio_config_colorspaces = {} + allowed_exts = { + ext.lstrip(".") for ext in IMAGE_EXTENSIONS.union(VIDEO_EXTENSIONS) + } class DeprecatedWarning(DeprecationWarning): @@ -361,7 +364,7 @@ def parse_colorspace_from_filepath( # match colorspace from filepath regex_pattern = _get_colorspace_match_regex( - colorspaces + underscored_colorspaces.keys()) + list(colorspaces) + list(underscored_colorspaces)) match = regex_pattern.search(filepath) colorspace = match.group(0) if match else None From 6a4ab981ad2a9f5b6f9d3225a260bb43e7d4ac9b Mon Sep 17 00:00:00 2001 From: Kayla Date: Mon, 25 Sep 2023 23:00:25 +0800 Subject: [PATCH 170/267] 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 92bc7c12f9241eb4a3c40c7c543ac84c30e78a92 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Sep 2023 17:23:34 +0200 Subject: [PATCH 171/267] fixing missing assetEntity --- openpype/plugins/publish/collect_sequence_frame_data.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index 6c2bfbf358..5fecc65446 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -28,6 +28,10 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): def get_frame_data_from_repre_sequence(self, instance): repres = instance.data.get("representations") + parent_entity = ( + instance.context.data.get("assetEntity") + or instance.context.data["projectEntity"] + ) if repres: first_repre = repres[0] if "ext" not in first_repre: @@ -52,5 +56,5 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): "frameEnd": repres_frames[-1], "handleStart": 0, "handleEnd": 0, - "fps": instance.context.data["assetEntity"]["data"]["fps"] + "fps": parent_entity["data"]["fps"] } From a73ba98209aa18fe6174a135e986962aac3d2ab0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Mon, 25 Sep 2023 17:29:14 +0200 Subject: [PATCH 172/267] assetEntity is not on context data --- openpype/plugins/publish/collect_sequence_frame_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index 5fecc65446..1c456563e6 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -29,7 +29,7 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): def get_frame_data_from_repre_sequence(self, instance): repres = instance.data.get("representations") parent_entity = ( - instance.context.data.get("assetEntity") + instance.data.get("assetEntity") or instance.context.data["projectEntity"] ) if repres: From 1bd07bd15b1bf238e186b23b7d112e9fe4637737 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 25 Sep 2023 18:01:24 +0200 Subject: [PATCH 173/267] OP-6874 - remove trailing underscore in subset name (#5647) If {layer} placeholder is at the end of subset name template and not used (for example in auto_image where separating it by layer doesn't make any sense) trailing '_' was kept. This updates cleaning logic and extracts it as it might be similar in regular `image` instance. --- openpype/hosts/photoshop/lib.py | 17 +++++++++++++ .../plugins/create/create_flatten_image.py | 14 ++--------- .../photoshop/plugins/create/create_image.py | 3 ++- .../unit/openpype/hosts/photoshop/test_lib.py | 25 +++++++++++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 tests/unit/openpype/hosts/photoshop/test_lib.py diff --git a/openpype/hosts/photoshop/lib.py b/openpype/hosts/photoshop/lib.py index ae7a33b7b6..9f603a70d2 100644 --- a/openpype/hosts/photoshop/lib.py +++ b/openpype/hosts/photoshop/lib.py @@ -1,5 +1,8 @@ +import re + import openpype.hosts.photoshop.api as api from openpype.client import get_asset_by_name +from openpype.lib import prepare_template_data from openpype.pipeline import ( AutoCreator, CreatedInstance @@ -78,3 +81,17 @@ class PSAutoCreator(AutoCreator): existing_instance["asset"] = asset_name existing_instance["task"] = task_name existing_instance["subset"] = subset_name + + +def clean_subset_name(subset_name): + """Clean all variants leftover {layer} from subset name.""" + dynamic_data = prepare_template_data({"layer": "{layer}"}) + for value in dynamic_data.values(): + if value in subset_name: + subset_name = (subset_name.replace(value, "") + .replace("__", "_") + .replace("..", ".")) + # clean trailing separator as Main_ + pattern = r'[\W_]+$' + replacement = '' + return re.sub(pattern, replacement, subset_name) diff --git a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py index e4229788bd..afde77fdb4 100644 --- a/openpype/hosts/photoshop/plugins/create/create_flatten_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_flatten_image.py @@ -2,7 +2,7 @@ from openpype.pipeline import CreatedInstance from openpype.lib import BoolDef import openpype.hosts.photoshop.api as api -from openpype.hosts.photoshop.lib import PSAutoCreator +from openpype.hosts.photoshop.lib import PSAutoCreator, clean_subset_name from openpype.pipeline.create import get_subset_name from openpype.lib import prepare_template_data from openpype.client import get_asset_by_name @@ -129,14 +129,4 @@ class AutoImageCreator(PSAutoCreator): self.family, variant, task_name, asset_doc, project_name, host_name, dynamic_data=dynamic_data ) - return self._clean_subset_name(subset_name) - - def _clean_subset_name(self, subset_name): - """Clean all variants leftover {layer} from subset name.""" - dynamic_data = prepare_template_data({"layer": "{layer}"}) - for value in dynamic_data.values(): - if value in subset_name: - return (subset_name.replace(value, "") - .replace("__", "_") - .replace("..", ".")) - return subset_name + return clean_subset_name(subset_name) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index af20d456e0..4f2e90886a 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -10,6 +10,7 @@ from openpype.pipeline import ( from openpype.lib import prepare_template_data from openpype.pipeline.create import SUBSET_NAME_ALLOWED_SYMBOLS from openpype.hosts.photoshop.api.pipeline import cache_and_get_instances +from openpype.hosts.photoshop.lib import clean_subset_name class ImageCreator(Creator): @@ -88,6 +89,7 @@ class ImageCreator(Creator): layer_fill = prepare_template_data({"layer": layer_name}) subset_name = subset_name.format(**layer_fill) + subset_name = clean_subset_name(subset_name) if group.long_name: for directory in group.long_name[::-1]: @@ -184,7 +186,6 @@ class ImageCreator(Creator): self.mark_for_review = plugin_settings["mark_for_review"] self.enabled = plugin_settings["enabled"] - def get_detail_description(self): return """Creator for Image instances diff --git a/tests/unit/openpype/hosts/photoshop/test_lib.py b/tests/unit/openpype/hosts/photoshop/test_lib.py new file mode 100644 index 0000000000..ad4feb42ae --- /dev/null +++ b/tests/unit/openpype/hosts/photoshop/test_lib.py @@ -0,0 +1,25 @@ +import pytest + +from openpype.hosts.photoshop.lib import clean_subset_name + +""" +Tests cleanup of unused layer placeholder ({layer}) from subset name. +Layer differentiation might be desired in subset name, but in some cases it +might be used (in `auto_image` - only single image without layer diff., +single image instance created without toggled use of subset name etc.) +""" + + +def test_no_layer_placeholder(): + clean_subset = clean_subset_name("imageMain") + assert "imageMain" == clean_subset + + +@pytest.mark.parametrize("subset_name", + ["imageMain{Layer}", + "imageMain_{layer}", # trailing _ + "image{Layer}Main", + "image{LAYER}Main"]) +def test_not_used_layer_placeholder(subset_name): + clean_subset = clean_subset_name(subset_name) + assert "imageMain" == clean_subset From 0595afe8a3240747cd852e3fc4073f58382ccd86 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Sep 2023 00:34:44 +0800 Subject: [PATCH 174/267] add % check on the fhead in the lib.py --- openpype/hosts/nuke/api/lib.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index af07092daf..8790794fcd 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3432,19 +3432,27 @@ def get_head_filename_without_hashes(original_path, name): Args: original_path (str): the filename with frame hashes - e.g. "renderAssetMain.####.exr" + e.g. "renderCompositingMain.####.exr" name (str): the name of the tags e.g. "baking" Returns: filename: the renamed filename with the tag - e.g. "renderAssetMain.baking.####.exr" + e.g. "renderCompositingMain.baking.####.exr" """ filename = os.path.basename(original_path) fhead = os.path.splitext(filename)[0].strip(".") - tmp_fhead = re.sub(r"\d", "#", fhead) - if "#" in tmp_fhead: - fhead = tmp_fhead.replace("#", "").rstrip(".") + if "#" in fhead: + fhead = re.sub("#+", "", fhead).rstrip(".") + elif "%" in fhead: + # use regex to convert %04d to {:0>4} + padding = re.search("%(\\d)+d", fhead) + padding = padding.group(1) if padding else 1 + fhead = re.sub( + "%.*d", + "{{:0>{}}}".format(padding), + fhead + ).rstip(".") new_fhead = "{}.{}".format(fhead, name) filename = filename.replace(fhead, new_fhead) return filename From 65bfe023c7d2b8a571185a49e78ef7b775d30fc9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 26 Sep 2023 00:41:04 +0800 Subject: [PATCH 175/267] transform the files with frame hashes to the list of filenames when publishing --- openpype/hosts/nuke/api/lib.py | 24 ++++++++++++++++++++++++ openpype/hosts/nuke/api/plugin.py | 7 ++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 8790794fcd..9e41dbe8bc 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3456,3 +3456,27 @@ def get_head_filename_without_hashes(original_path, name): new_fhead = "{}.{}".format(fhead, name) filename = filename.replace(fhead, new_fhead) return filename + + +def get_filenames_without_hash(filename, frame_start, frame_end): + """Get filenames without frame hash + i.e. "renderCompositingMain.baking.0001.exr" + + Args: + filename (str): filename with frame hash + frame_start (str): start of the frame + frame_end (str): end of the frame + + Returns: + filenames(list): list of filename + """ + filenames = [] + for frame in range(int(frame_start), (int(frame_end) + 1)): + if "#" in filename: + # use regex to convert #### to {:0>4} + def replace(match): + return "{{:0>{}}}".format(len(match.group())) + filename_without_hashes = re.sub("#+", replace, filename) + new_filename = filename_without_hashes.format(frame) + filenames.append(new_filename) + return filenames diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index ca31068943..348a0b5d76 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -39,7 +39,8 @@ from .lib import ( get_view_process_node, get_viewer_config_from_string, deprecated, - get_head_filename_without_hashes + get_head_filename_without_hashes, + get_filenames_without_hash ) from .pipeline import ( list_instances, @@ -638,6 +639,10 @@ class ExporterReview(object): "frameStart": self.first_frame, "frameEnd": self.last_frame, }) + if ".{}".format(self.ext) not in VIDEO_EXTENSIONS: + filenames = get_filenames_without_hash( + self.file, self.first_frame, self.last_frame) + repre["files"] = filenames if self.multiple_presets: repre["outputName"] = self.name From 8fd323fb16a52a2a0dbd3979cc4eaf7d37dca40d Mon Sep 17 00:00:00 2001 From: Kayla Date: Tue, 26 Sep 2023 12:49:24 +0800 Subject: [PATCH 176/267] 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 177/267] 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 4b1c9077e6c0f109c3315ddc88dd74f09a301bfe Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 26 Sep 2023 13:42:01 +0200 Subject: [PATCH 178/267] fix workfiles tool save button (#5653) --- openpype/tools/ayon_workfiles/models/workfiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/ayon_workfiles/models/workfiles.py b/openpype/tools/ayon_workfiles/models/workfiles.py index 316d8b2a16..4d989ed22c 100644 --- a/openpype/tools/ayon_workfiles/models/workfiles.py +++ b/openpype/tools/ayon_workfiles/models/workfiles.py @@ -48,7 +48,7 @@ def get_task_template_data(project_entity, task): return {} short_name = None task_type_name = task["taskType"] - for task_type_info in project_entity["config"]["taskTypes"]: + for task_type_info in project_entity["taskTypes"]: if task_type_info["name"] == task_type_name: short_name = task_type_info["shortName"] break From 40755fce119f388efa85e4cab5c94da749b54dbf Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 26 Sep 2023 18:56:01 +0200 Subject: [PATCH 179/267] Increase timout for deadline test (#5654) DL picks up jobs quite slow, so bump up delay. --- tests/integration/hosts/maya/test_deadline_publish_in_maya.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/hosts/maya/test_deadline_publish_in_maya.py b/tests/integration/hosts/maya/test_deadline_publish_in_maya.py index c5bf526f52..9332177944 100644 --- a/tests/integration/hosts/maya/test_deadline_publish_in_maya.py +++ b/tests/integration/hosts/maya/test_deadline_publish_in_maya.py @@ -32,7 +32,7 @@ class TestDeadlinePublishInMaya(MayaDeadlinePublishTestClass): # keep empty to locate latest installed variant or explicit APP_VARIANT = "" - TIMEOUT = 120 # publish timeout + TIMEOUT = 180 # publish timeout def test_db_asserts(self, dbcon, publish_finished): """Host and input data dependent expected results in DB.""" From 16bcbc155867c0daef9c990484035c6ba0f16ec2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 27 Sep 2023 03:24:58 +0000 Subject: [PATCH 180/267] [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 d1ebde3d04..c8ae6dffd8 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.1-nightly.2" +__version__ = "3.17.1-nightly.3" From 3ddfb13e2aae05e4df2b3a6da7900600d8649d1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 27 Sep 2023 03:25:45 +0000 Subject: [PATCH 181/267] 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 87d904fc84..a2edd28f5b 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.1-nightly.3 - 3.17.1-nightly.2 - 3.17.1-nightly.1 - 3.17.0 @@ -134,7 +135,6 @@ body: - 3.14.10-nightly.6 - 3.14.10-nightly.5 - 3.14.10-nightly.4 - - 3.14.10-nightly.3 validations: required: true - type: dropdown From 4ae8e7fa774e06ebc690998f2dd7d623a9e8c044 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Sep 2023 11:39:15 +0200 Subject: [PATCH 182/267] removing project entity redundancy --- .../plugins/publish/collect_sequence_frame_data.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index 1c456563e6..33ff3281a2 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -28,10 +28,12 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): def get_frame_data_from_repre_sequence(self, instance): repres = instance.data.get("representations") - parent_entity = ( - instance.data.get("assetEntity") - or instance.context.data["projectEntity"] - ) + parent_entity = instance.data.get("assetEntity") + + if not parent_entity: + self.log.warning("Cannot find parent entity data") + return + if repres: first_repre = repres[0] if "ext" not in first_repre: @@ -40,7 +42,7 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): return files = first_repre["files"] - collections, remainder = clique.assemble(files) + collections, _ = clique.assemble(files) if not collections: # No sequences detected and we can't retrieve # frame range From fbafc420aaaced3501be9e2b24f8025c85809c8c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Sep 2023 12:28:42 +0200 Subject: [PATCH 183/267] reverting enhancing UX of sequence or asset frame data collection --- ...> collect_frame_data_from_asset_entity.py} | 23 +++++++------------ .../publish/collect_sequence_frame_data.py | 21 +++++++++-------- 2 files changed, 19 insertions(+), 25 deletions(-) rename openpype/hosts/traypublisher/plugins/publish/{collect_missing_frame_range_asset_entity.py => collect_frame_data_from_asset_entity.py} (51%) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_missing_frame_range_asset_entity.py b/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py similarity index 51% rename from openpype/hosts/traypublisher/plugins/publish/collect_missing_frame_range_asset_entity.py rename to openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py index 72379ea4e1..f2e24d88eb 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_missing_frame_range_asset_entity.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py @@ -2,18 +2,18 @@ import pyblish.api from openpype.pipeline import OptionalPyblishPluginMixin -class CollectMissingFrameDataFromAssetEntity( +class CollectFrameDataFromAssetEntity( pyblish.api.InstancePlugin, OptionalPyblishPluginMixin ): - """Collect Missing Frame Range data From Asset Entity + """Collect Frame Data From AssetEntity found in context Frame range data will only be collected if the keys are not yet collected for the instance. """ order = pyblish.api.CollectorOrder + 0.491 - label = "Collect Missing Frame Data From Asset Entity" + label = "Collect Frame Data From Asset" families = ["plate", "pointcache", "vdbcache", "online", "render"] @@ -23,7 +23,9 @@ class CollectMissingFrameDataFromAssetEntity( def process(self, instance): if not self.is_active(instance.data): return - missing_keys = [] + + asset_data = instance.data["assetEntity"]["data"] + for key in ( "fps", "frameStart", @@ -31,14 +33,5 @@ class CollectMissingFrameDataFromAssetEntity( "handleStart", "handleEnd" ): - if key not in instance.data: - missing_keys.append(key) - keys_set = [] - for key in missing_keys: - asset_data = instance.data["assetEntity"]["data"] - if key in asset_data: - instance.data[key] = asset_data[key] - keys_set.append(key) - if keys_set: - self.log.debug(f"Frame range data {keys_set} " - "has been collected from asset entity.") + instance.data[key] = asset_data[key] + self.log.debug(f"Collected Frame range data '{key}':{asset_data[key]} ") diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index 33ff3281a2..d8ad5d0a21 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -9,7 +9,7 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): start and end frame respectively """ - order = pyblish.api.CollectorOrder + 0.2 + order = pyblish.api.CollectorOrder + 0.490 label = "Collect Sequence Frame Data" families = ["plate", "pointcache", "vdbcache", "online", @@ -18,21 +18,22 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): def process(self, instance): frame_data = self.get_frame_data_from_repre_sequence(instance) + if not frame_data: # if no dict data skip collecting the frame range data return + for key, value in frame_data.items(): - if key not in instance.data: - instance.data[key] = value - self.log.debug(f"Collected Frame range data '{key}':{value} ") + instance.data[key] = value + self.log.debug(f"Collected Frame range data '{key}':{value} ") + + test_keys = {key: value for key, value in instance.data.items() if key in frame_data} + self.log.debug(f"Final Instance frame data: {test_keys}") + def get_frame_data_from_repre_sequence(self, instance): repres = instance.data.get("representations") - parent_entity = instance.data.get("assetEntity") - - if not parent_entity: - self.log.warning("Cannot find parent entity data") - return + asset_data = instance.data["assetEntity"]["data"] if repres: first_repre = repres[0] @@ -58,5 +59,5 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): "frameEnd": repres_frames[-1], "handleStart": 0, "handleEnd": 0, - "fps": parent_entity["data"]["fps"] + "fps": asset_data["fps"] } From 5b1cbfaa6743c6bd4f9b6be4e86b3a0854dbb3c9 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Sep 2023 12:35:36 +0200 Subject: [PATCH 184/267] removing debug prints --- .../plugins/publish/collect_frame_data_from_asset_entity.py | 3 ++- openpype/plugins/publish/collect_sequence_frame_data.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py b/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py index f2e24d88eb..5ba84bc05b 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py @@ -34,4 +34,5 @@ class CollectFrameDataFromAssetEntity( "handleEnd" ): instance.data[key] = asset_data[key] - self.log.debug(f"Collected Frame range data '{key}':{asset_data[key]} ") + self.log.debug( + f"Collected Frame range data '{key}':{asset_data[key]} ") diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/plugins/publish/collect_sequence_frame_data.py index d8ad5d0a21..f9ac869ec3 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/plugins/publish/collect_sequence_frame_data.py @@ -27,9 +27,6 @@ class CollectSequenceFrameData(pyblish.api.InstancePlugin): instance.data[key] = value self.log.debug(f"Collected Frame range data '{key}':{value} ") - test_keys = {key: value for key, value in instance.data.items() if key in frame_data} - self.log.debug(f"Final Instance frame data: {test_keys}") - def get_frame_data_from_repre_sequence(self, instance): repres = instance.data.get("representations") From e90d227a234fec278b214c3ed4086350403eda80 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Sep 2023 13:37:01 +0200 Subject: [PATCH 185/267] reverting the functionality - sequencial original frame data should be optional plugin - sequential data are added if activated - asset data frame data are not optional anymore and are added only if missing --- .../collect_frame_data_from_asset_entity.py | 27 +++++++++---------- .../publish/collect_sequence_frame_data.py | 18 ++++++++++--- .../project_settings/traypublisher.json | 4 +-- .../schema_project_traypublisher.json | 4 +-- 4 files changed, 31 insertions(+), 22 deletions(-) rename openpype/{ => hosts/traypublisher}/plugins/publish/collect_sequence_frame_data.py (82%) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py b/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py index 5ba84bc05b..2950076cd0 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py @@ -1,11 +1,7 @@ import pyblish.api -from openpype.pipeline import OptionalPyblishPluginMixin -class CollectFrameDataFromAssetEntity( - pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin -): +class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin): """Collect Frame Data From AssetEntity found in context Frame range data will only be collected if the keys @@ -18,14 +14,9 @@ class CollectFrameDataFromAssetEntity( "vdbcache", "online", "render"] hosts = ["traypublisher"] - optional = True def process(self, instance): - if not self.is_active(instance.data): - return - - asset_data = instance.data["assetEntity"]["data"] - + missing_keys = [] for key in ( "fps", "frameStart", @@ -33,6 +24,14 @@ class CollectFrameDataFromAssetEntity( "handleStart", "handleEnd" ): - instance.data[key] = asset_data[key] - self.log.debug( - f"Collected Frame range data '{key}':{asset_data[key]} ") + if key not in instance.data: + missing_keys.append(key) + keys_set = [] + for key in missing_keys: + asset_data = instance.data["assetEntity"]["data"] + if key in asset_data: + instance.data[key] = asset_data[key] + keys_set.append(key) + if keys_set: + self.log.debug(f"Frame range data {keys_set} " + "has been collected from asset entity.") diff --git a/openpype/plugins/publish/collect_sequence_frame_data.py b/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py similarity index 82% rename from openpype/plugins/publish/collect_sequence_frame_data.py rename to openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py index f9ac869ec3..db70d4fe0a 100644 --- a/openpype/plugins/publish/collect_sequence_frame_data.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py @@ -1,22 +1,32 @@ import pyblish.api import clique +from openpype.pipeline import OptionalPyblishPluginMixin + + +class CollectSequenceFrameData( + pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin +): + """Collect Original Sequence Frame Data -class CollectSequenceFrameData(pyblish.api.InstancePlugin): - """Collect Sequence Frame Data If the representation includes files with frame numbers, then set `frameStart` and `frameEnd` for the instance to the start and end frame respectively """ - order = pyblish.api.CollectorOrder + 0.490 - label = "Collect Sequence Frame Data" + order = pyblish.api.CollectorOrder + 0.4905 + label = "Collect Original Sequence Frame Data" families = ["plate", "pointcache", "vdbcache", "online", "render"] hosts = ["traypublisher"] + optional = True def process(self, instance): + if not self.is_active(instance.data): + return + frame_data = self.get_frame_data_from_repre_sequence(instance) if not frame_data: diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 7f7b7d1452..e13de11414 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -346,10 +346,10 @@ } }, "publish": { - "CollectFrameDataFromAssetEntity": { + "CollectSequenceFrameData": { "enabled": true, "optional": true, - "active": true + "active": false }, "ValidateFrameRange": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json index 184fc657be..93e6325b5a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_traypublisher.json @@ -350,8 +350,8 @@ "name": "template_validate_plugin", "template_data": [ { - "key": "CollectFrameDataFromAssetEntity", - "label": "Collect frame range from asset entity" + "key": "CollectSequenceFrameData", + "label": "Collect Original Sequence Frame Data" }, { "key": "ValidateFrameRange", From 8897cdaa92f21e5c9de22cccd9e73ecc65c0c845 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 19:43:21 +0800 Subject: [PATCH 186/267] bug fix delete items from container to remove item --- openpype/hosts/max/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 3389447cb0..b23d156d0d 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -91,7 +91,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( current_selection = selectByName title:"Select Objects to remove from the Container" buttontext:"Remove" filter: nodes_to_rmv - if current_selection == undefined then return False + if current_selection == undefined or current_selection.count == 0 then return False temp_arr = #() i_node_arr = #() new_i_node_arr = #() From 1c8ab8ecfacc95543fa348a08c4e1b23495bc52c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 27 Sep 2023 13:47:16 +0200 Subject: [PATCH 187/267] better label --- .../plugins/publish/collect_frame_data_from_asset_entity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py b/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py index 2950076cd0..e8a2cae16c 100644 --- a/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py +++ b/openpype/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py @@ -9,7 +9,7 @@ class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin): """ order = pyblish.api.CollectorOrder + 0.491 - label = "Collect Frame Data From Asset" + label = "Collect Missing Frame Data From Asset" families = ["plate", "pointcache", "vdbcache", "online", "render"] From 723d187835a83d7f76c65017411b0a7263a8e18a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 19:54:01 +0800 Subject: [PATCH 188/267] hound --- openpype/hosts/max/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index b23d156d0d..31f01b6bbf 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -91,7 +91,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( current_selection = selectByName title:"Select Objects to remove from the Container" buttontext:"Remove" filter: nodes_to_rmv - if current_selection == undefined or current_selection.count == 0 then return False + if current_selection == undefined or current_selection.count == 0 then return False # noqa temp_arr = #() i_node_arr = #() new_i_node_arr = #() From 42132e50e98208f581f97005dba839ba414265ee Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 21:34:57 +0800 Subject: [PATCH 189/267] # noqa makes the maxscript not working --- openpype/hosts/max/api/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 31f01b6bbf..b23d156d0d 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -91,7 +91,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( current_selection = selectByName title:"Select Objects to remove from the Container" buttontext:"Remove" filter: nodes_to_rmv - if current_selection == undefined or current_selection.count == 0 then return False # noqa + if current_selection == undefined or current_selection.count == 0 then return False temp_arr = #() i_node_arr = #() new_i_node_arr = #() From 49e8a4b008a17ce97fb3ed4a77c53534f6e2509c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 21:42:08 +0800 Subject: [PATCH 190/267] hound --- openpype/hosts/max/api/plugin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index b23d156d0d..881295b317 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -91,7 +91,10 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" ( current_selection = selectByName title:"Select Objects to remove from the Container" buttontext:"Remove" filter: nodes_to_rmv - if current_selection == undefined or current_selection.count == 0 then return False + if current_selection == undefined or current_selection.count == 0 then + ( + return False + ) temp_arr = #() i_node_arr = #() new_i_node_arr = #() From 9fdd895bb6e4acb16313225f6c85604002471547 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 21:44:19 +0800 Subject: [PATCH 191/267] rename current_selection to current_sel --- openpype/hosts/max/api/plugin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/api/plugin.py b/openpype/hosts/max/api/plugin.py index 881295b317..fa6db073db 100644 --- a/openpype/hosts/max/api/plugin.py +++ b/openpype/hosts/max/api/plugin.py @@ -65,12 +65,12 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" on button_add pressed do ( - current_selection = selectByName title:"Select Objects to add to + current_sel = selectByName title:"Select Objects to add to the Container" buttontext:"Add" filter:nodes_to_add - if current_selection == undefined then return False + if current_sel == undefined then return False temp_arr = #() i_node_arr = #() - for c in current_selection do + for c in current_sel do ( handle_name = node_to_name c node_ref = NodeTransformMonitor node:c @@ -89,9 +89,9 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" on button_del pressed do ( - current_selection = selectByName title:"Select Objects to remove + current_sel = selectByName title:"Select Objects to remove from the Container" buttontext:"Remove" filter: nodes_to_rmv - if current_selection == undefined or current_selection.count == 0 then + if current_sel == undefined or current_sel.count == 0 then ( return False ) @@ -100,7 +100,7 @@ MS_CUSTOM_ATTRIB = """attributes "openPypeData" new_i_node_arr = #() new_temp_arr = #() - for c in current_selection do + for c in current_sel do ( node_ref = NodeTransformMonitor node:c as string handle_name = node_to_name c From 6deb9338cbd56447ea548f20eb0a51158b124b9e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 21:47:02 +0800 Subject: [PATCH 192/267] fix the typo of rstrip --- openpype/hosts/nuke/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 9e41dbe8bc..351778d997 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3452,7 +3452,7 @@ def get_head_filename_without_hashes(original_path, name): "%.*d", "{{:0>{}}}".format(padding), fhead - ).rstip(".") + ).rstrip(".") new_fhead = "{}.{}".format(fhead, name) filename = filename.replace(fhead, new_fhead) return filename From ebdcc49cd7895aa3e4aceadb0a0af116a0e41843 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 22:52:53 +0800 Subject: [PATCH 193/267] implement more concise function for getting filenames with hashes --- openpype/hosts/nuke/api/lib.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 351778d997..d34e7a1e0a 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3432,30 +3432,20 @@ def get_head_filename_without_hashes(original_path, name): Args: original_path (str): the filename with frame hashes - e.g. "renderCompositingMain.####.exr" + e.g. "renderCompositingMain.####.exr" name (str): the name of the tags - e.g. "baking" + e.g. "baking" Returns: - filename: the renamed filename with the tag - e.g. "renderCompositingMain.baking.####.exr" + str: the renamed filename with the tag + e.g. "renderCompositingMain.baking.####.exr" """ filename = os.path.basename(original_path) - fhead = os.path.splitext(filename)[0].strip(".") - if "#" in fhead: - fhead = re.sub("#+", "", fhead).rstrip(".") - elif "%" in fhead: - # use regex to convert %04d to {:0>4} - padding = re.search("%(\\d)+d", fhead) - padding = padding.group(1) if padding else 1 - fhead = re.sub( - "%.*d", - "{{:0>{}}}".format(padding), - fhead - ).rstrip(".") - new_fhead = "{}.{}".format(fhead, name) - filename = filename.replace(fhead, new_fhead) - return filename + + 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): From abef01cd0560ce60e108ed71c1de1d159b7efcb1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 22:54:18 +0800 Subject: [PATCH 194/267] edit docstring --- openpype/hosts/nuke/api/lib.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index d34e7a1e0a..cc2c5a6ec7 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3430,15 +3430,20 @@ def get_head_filename_without_hashes(original_path, name): To avoid the system being confused on finding the filename with frame hashes if the head of the filename has the hashed symbol + Examples: + >>> get_head_filename_without_hashes("render.####.exr", "baking") + render.baking.####.exr + >>> get_head_filename_without_hashes("render.%d.exr", "tag") + render.tag.%d.exr + >>> get_head_filename_without_hashes("exr.####.exr", "foo") + exr.foo.%04d.exr + Args: original_path (str): the filename with frame hashes - e.g. "renderCompositingMain.####.exr" name (str): the name of the tags - e.g. "baking" Returns: str: the renamed filename with the tag - e.g. "renderCompositingMain.baking.####.exr" """ filename = os.path.basename(original_path) From e493886f4de4316778d3129d40f441f8ded24b71 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 23:17:12 +0800 Subject: [PATCH 195/267] improve docstring on lib.py and add comment on the condition of setting filename with extension and improved the deprecrated settings --- openpype/hosts/nuke/api/lib.py | 2 +- openpype/hosts/nuke/api/plugin.py | 4 ++++ .../publish/extract_review_baking_streams.py | 10 +++++----- openpype/settings/ayon_settings.py | 15 ++++++--------- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index cc2c5a6ec7..dafc4bf838 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3463,7 +3463,7 @@ def get_filenames_without_hash(filename, frame_start, frame_end): frame_end (str): end of the frame Returns: - filenames(list): list of filename + list: filename per frame of the sequence """ filenames = [] for frame in range(int(frame_start), (int(frame_end) + 1)): diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 348a0b5d76..e16aef6740 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -818,9 +818,13 @@ 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 = get_head_filename_without_hashes( 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: original_ext = os.path.splitext(filename)[-1].strip(".") # noqa self.file = filename.replace(original_ext, ext) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py b/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py index d9ae673c2c..fe468bd263 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py @@ -33,13 +33,13 @@ class ExtractReviewDataBakingStreams(publish.Extractor): nuke_publish = project_settings["nuke"]["publish"] deprecated_setting = nuke_publish["ExtractReviewDataMov"] current_setting = nuke_publish["ExtractReviewDataBakingStreams"] - if not deprecated_setting["enabled"]: - if current_setting["enabled"]: - cls.viewer_lut_raw = current_setting["viewer_lut_raw"] - cls.outputs = current_setting["outputs"] - else: + 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["enabled"]: + cls.viewer_lut_raw = current_setting["viewer_lut_raw"] + cls.outputs = current_setting["outputs"] def process(self, instance): families = set(instance.data["families"]) diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index b43e0b7c5f..dc6e9fab12 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -754,11 +754,10 @@ def _convert_nuke_project_settings(ayon_settings, output): current_review_settings = ( ayon_publish["ExtractReviewDataBakingStreams"] ) - if not deprecrated_review_settings["enabled"]: - if current_review_settings["enabled"]: - outputs_settings = current_review_settings["outputs"] - else: + if deprecrated_review_settings["enabled"]: outputs_settings = deprecrated_review_settings["outputs"] + elif current_review_settings["enabled"]: + outputs_settings = current_review_settings["outputs"] for item in outputs_settings: item_filter = item["filter"] @@ -780,12 +779,10 @@ def _convert_nuke_project_settings(ayon_settings, output): name = item.pop("name") new_review_data_outputs[name] = item - if deprecrated_review_settings["outputs"] == ( - current_review_settings["outputs"] - ): - current_review_settings["outputs"] = new_review_data_outputs - else: + if deprecrated_review_settings["enabled"]: deprecrated_review_settings["outputs"] = new_review_data_outputs + elif current_review_settings["enabled"]: + current_review_settings["outputs"] = new_review_data_outputs collect_instance_data = ayon_publish["CollectInstanceData"] if "sync_workfile_version_on_product_types" in collect_instance_data: From 973e4804d5731dbb85e916177ecef7854d5d30d1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 27 Sep 2023 23:30:57 +0800 Subject: [PATCH 196/267] make sure not using .replace --- openpype/hosts/nuke/api/plugin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index e16aef6740..81841d17be 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -826,8 +826,8 @@ class ExporterReviewMov(ExporterReview): # make sure the filename are in # correct image output format if ".{}".format(self.ext) not in self.file: - original_ext = os.path.splitext(filename)[-1].strip(".") # noqa - self.file = filename.replace(original_ext, ext) + filename_no_ext, _ = os.path.splitext(filename) + self.file = "{}.{}".format(filename_no_ext, self.ext) self.path = os.path.join( self.staging_dir, self.file).replace("\\", "/") From 70d3f20de4c1963c67bd40be2613fc67ee34e017 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 28 Sep 2023 09:30:43 +0000 Subject: [PATCH 197/267] [Automated] Release --- CHANGELOG.md | 264 ++++++++++++++++++++++++++++++++++++++++++++ openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 266 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4bcf66a210..8f14340348 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,270 @@ # Changelog +## [3.17.1](https://github.com/ynput/OpenPype/tree/3.17.1) + + +[Full Changelog](https://github.com/ynput/OpenPype/compare/3.17.0...3.17.1) + +### **🆕 New features** + + +
+Unreal: Yeti support #5643 + +Implemented Yeti support for Unreal. + + +___ + +
+ + +
+Houdini: Add Static Mesh product-type (family) #5481 + +This PR adds support to publish Unreal Static Mesh in Houdini as FBXQuick recap +- [x] Add UE Static Mesh Creator +- [x] Dynamic subset name like in Maya +- [x] Collect Static Mesh Type +- [x] Update collect output node +- [x] Validate FBX output node +- [x] Validate mesh is static +- [x] Validate Unreal Static Mesh Name +- [x] Validate Subset Name +- [x] FBX Extractor +- [x] FBX Loader +- [x] Update OP Settings +- [x] Update AYON Settings + + +___ + +
+ + +
+Launcher tool: Refactor launcher tool (for AYON) #5612 + +Refactored launcher tool to new tool. Separated backend and frontend logic. Refactored logic is AYON-centric and is used only in AYON mode, so it does not affect OpenPype. + + +___ + +
+ +### **🚀 Enhancements** + + +
+Maya: Use custom staging dir function for Maya renders - OP-5265 #5186 + +Check for custom staging dir when setting the renders output folder in Maya. + + +___ + +
+ + +
+Colorspace: updating file path detection methods #5273 + +Support for OCIO v2 file rules integrated into the available color management API + + +___ + +
+ + +
+Chore: add default isort config #5572 + +Add default configuration for isort tool + + +___ + +
+ + +
+Deadline: set PATH environment in deadline jobs by GlobalJobPreLoad #5622 + +This PR makes `GlobalJobPreLoad` to set `PATH` environment in deadline jobs so that we don't have to use the full executable path for deadline to launch the dcc app. This trick should save us adding logic to pass houdini patch version and modifying Houdini deadline plugin. This trick should work with other DCCs + + +___ + +
+ + +
+nuke: extract review data mov read node with expression #5635 + +Some productions might have set default values for read nodes, those settings are not colliding anymore now. + + +___ + +
+ +### **🐛 Bug fixes** + + +
+Maya: Support new publisher for colorsets validation. #5630 + +Fix `validate_color_sets` for the new publisher.In current `develop` the repair option does not appear due to wrong error raising. + + +___ + +
+ + +
+Houdini: Camera Loader fix mismatch for Maya cameras #5584 + +This PR adds +- A workaround to match Maya render mask in Houdini +- `SetCameraResolution` inventory action +- set camera resolution when loading or updating camera + + +___ + +
+ + +
+Nuke: fix set colorspace on writes #5634 + +Colorspace is set correctly to any write node created from publisher. + + +___ + +
+ + +
+TVPaint: Fix review family extraction #5637 + +Extractor marks representation of review instance with review tag. + + +___ + +
+ + +
+AYON settings: Extract OIIO transcode settings #5639 + +Output definitions of Extract OIIO transcode have name to match OpenPype settings, and the settings are converted to dictionary in settings conversion. + + +___ + +
+ + +
+AYON: Fix task type short name conversion #5641 + +Convert AYON task type short name for OpenPype correctly. + + +___ + +
+ + +
+colorspace: missing `allowed_exts` fix #5646 + +Colorspace module is not failing due to missing `allowed_exts` attribute. + + +___ + +
+ + +
+Photoshop: remove trailing underscore in subset name #5647 + +If {layer} placeholder is at the end of subset name template and not used (for example in `auto_image` where separating it by layer doesn't make any sense) trailing '_' was kept. This updates cleaning logic and extracts it as it might be similar in regular `image` instance. + + +___ + +
+ + +
+traypublisher: missing `assetEntity` in context data #5648 + +Issue with missing `assetEnity` key in context data is not problem anymore. + + +___ + +
+ + +
+AYON: Workfiles tool save button works #5653 + +Fix save as button in workfiles tool.(It is mystery why this stopped to work??) + + +___ + +
+ + +
+Max: bug fix delete items from container #5658 + +Fix the bug shown when clicking "Delete Items from Container" and selecting nothing and press ok. + + +___ + +
+ +### **🔀 Refactored code** + + +
+Chore: Remove unused functions from Fusion integration #5617 + +Cleanup unused code from Fusion integration + + +___ + +
+ +### **Merged pull requests** + + +
+Increase timout for deadline test #5654 + +DL picks up jobs quite slow, so bump up delay. + + +___ + +
+ + + + ## [3.17.0](https://github.com/ynput/OpenPype/tree/3.17.0) diff --git a/openpype/version.py b/openpype/version.py index c8ae6dffd8..f1e0cd0b80 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.1-nightly.3" +__version__ = "3.17.1" diff --git a/pyproject.toml b/pyproject.toml index d0b1ecf589..2460185bdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.17.0" # OpenPype +version = "3.17.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 030d5843fa2baba9e723067f34db0e1dbc46f297 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 28 Sep 2023 09:31:44 +0000 Subject: [PATCH 198/267] 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 a2edd28f5b..591d865ca5 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.1 - 3.17.1-nightly.3 - 3.17.1-nightly.2 - 3.17.1-nightly.1 @@ -134,7 +135,6 @@ body: - 3.14.10-nightly.7 - 3.14.10-nightly.6 - 3.14.10-nightly.5 - - 3.14.10-nightly.4 validations: required: true - type: dropdown From ed02bf31114e885a157c8ef13f8474cc86d093a1 Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 17:51:08 +0800 Subject: [PATCH 199/267] 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 356f05ff91efd32dcecbb72081f94740ec8df908 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 28 Sep 2023 19:20:37 +0800 Subject: [PATCH 200/267] docstring tweaks --- openpype/hosts/nuke/api/lib.py | 2 +- .../nuke/plugins/publish/extract_review_baking_streams.py | 6 +++--- openpype/settings/ayon_settings.py | 3 ++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index dafc4bf838..07f394ec00 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -3433,7 +3433,7 @@ def get_head_filename_without_hashes(original_path, name): Examples: >>> get_head_filename_without_hashes("render.####.exr", "baking") render.baking.####.exr - >>> get_head_filename_without_hashes("render.%d.exr", "tag") + >>> get_head_filename_without_hashes("render.%04d.exr", "tag") render.tag.%d.exr >>> get_head_filename_without_hashes("exr.####.exr", "foo") exr.foo.%04d.exr diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py b/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py index fe468bd263..1ba107a3e7 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py @@ -9,7 +9,7 @@ from openpype.hosts.nuke.api.lib import maintained_selection class ExtractReviewDataBakingStreams(publish.Extractor): - """Extracts movie and thumbnail with baked in luts + """Extracts Sequences and thumbnail with baked in luts must be run after extract_render_local.py @@ -27,8 +27,8 @@ class ExtractReviewDataBakingStreams(publish.Extractor): @classmethod def apply_settings(cls, project_settings): - """just in case there are some old presets - in deprecated ExtractReviewDataMov Plugins + """Apply the settings from the deprecated + ExtractReviewDataMov plugin for backwards compatibility """ nuke_publish = project_settings["nuke"]["publish"] deprecated_setting = nuke_publish["ExtractReviewDataMov"] diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index dc6e9fab12..f23046e6c4 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -749,7 +749,8 @@ def _convert_nuke_project_settings(ayon_settings, output): new_review_data_outputs = {} outputs_settings = None - # just in case that the users having old presets in outputs setting + # Check deprecated ExtractReviewDataMov + # settings for backwards compatibility deprecrated_review_settings = ayon_publish["ExtractReviewDataMov"] current_review_settings = ( ayon_publish["ExtractReviewDataBakingStreams"] From a433c46e727862a28a0a4835f582135b588e67e6 Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 19:50:44 +0800 Subject: [PATCH 201/267] 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 6c1e066b3b7d58eba28d38900a886a0c89c556b5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 28 Sep 2023 20:07:28 +0800 Subject: [PATCH 202/267] Rename ExtractReviewDataBakingStreams to ExtractReviewIntermediate --- .../nuke/plugins/publish/extract_review_baking_streams.py | 6 +++--- openpype/settings/ayon_settings.py | 2 +- openpype/settings/defaults/project_settings/nuke.json | 2 +- .../projects_schema/schemas/schema_nuke_publish.json | 6 +++--- server_addon/nuke/server/settings/publish_plugins.py | 4 ++-- website/docs/project_settings/settings_project_global.md | 2 +- website/docs/pype2/admin_presets_plugins.md | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py b/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py index 1ba107a3e7..4407c039b4 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py @@ -8,7 +8,7 @@ from openpype.hosts.nuke.api import plugin from openpype.hosts.nuke.api.lib import maintained_selection -class ExtractReviewDataBakingStreams(publish.Extractor): +class ExtractReviewIntermediate(publish.Extractor): """Extracts Sequences and thumbnail with baked in luts must be run after extract_render_local.py @@ -16,7 +16,7 @@ class ExtractReviewDataBakingStreams(publish.Extractor): """ order = pyblish.api.ExtractorOrder + 0.01 - label = "Extract Review Data Baking Streams" + label = "Extract Review Intermediate" families = ["review"] hosts = ["nuke"] @@ -32,7 +32,7 @@ class ExtractReviewDataBakingStreams(publish.Extractor): """ nuke_publish = project_settings["nuke"]["publish"] deprecated_setting = nuke_publish["ExtractReviewDataMov"] - current_setting = nuke_publish["ExtractReviewDataBakingStreams"] + current_setting = nuke_publish["ExtractReviewIntermediate"] if deprecated_setting["enabled"]: # Use deprecated settings if they are still enabled cls.viewer_lut_raw = deprecated_setting["viewer_lut_raw"] diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index f23046e6c4..fbf35aec0a 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -753,7 +753,7 @@ def _convert_nuke_project_settings(ayon_settings, output): # settings for backwards compatibility deprecrated_review_settings = ayon_publish["ExtractReviewDataMov"] current_review_settings = ( - ayon_publish["ExtractReviewDataBakingStreams"] + ayon_publish["ExtractReviewIntermediate"] ) if deprecrated_review_settings["enabled"]: outputs_settings = deprecrated_review_settings["outputs"] diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index fac78dbcd5..7346c9d7b8 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -501,7 +501,7 @@ } } }, - "ExtractReviewDataBakingStreams": { + "ExtractReviewIntermediate": { "enabled": true, "viewer_lut_raw": false, "outputs": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index 0f366d55ba..c14f47a3a7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -373,14 +373,14 @@ }, { "type": "label", - "label": "^ Settings and for ExtractReviewDataMov is deprecated and will be soon removed.
Please use ExtractReviewDataBakingStreams instead." + "label": "^ Settings and for ExtractReviewDataMov is deprecated and will be soon removed.
Please use ExtractReviewIntermediate instead." }, { "type": "dict", "collapsible": true, "checkbox_key": "enabled", - "key": "ExtractReviewDataBakingStreams", - "label": "ExtractReviewDataBakingStreams", + "key": "ExtractReviewIntermediate", + "label": "ExtractReviewIntermediate", "is_group": true, "children": [ { diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 6459dd7225..399aa7e38e 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -282,7 +282,7 @@ class PublishPuginsModel(BaseSettingsModel): title="Extract Review Data Mov", default_factory=ExtractReviewDataMovModel ) - ExtractReviewDataBakingStreams: ExtractReviewBakingStreamsModel = Field( + ExtractReviewIntermediate: ExtractReviewBakingStreamsModel = Field( title="Extract Review Data Baking Streams", default_factory=ExtractReviewBakingStreamsModel ) @@ -481,7 +481,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { } ] }, - "ExtractReviewDataBakingStreams": { + "ExtractReviewIntermediate": { "enabled": True, "viewer_lut_raw": False, "outputs": [ diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 8ecfe0c5da..3aa9772118 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -189,7 +189,7 @@ A profile may generate multiple outputs from a single input. Each output must de - Profile filtering defines which group of output definitions is used but output definitions may require more specific filters on their own. - They may filter by subset name (regex can be used) or publish families. Publish families are more complex as are based on knowing code base. - Filtering by custom tags -> this is used for targeting to output definitions from other extractors using settings (at this moment only Nuke bake extractor can target using custom tags). - - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewDataBakingStreams/outputs/baking/add_custom_tags` + - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewIntermediate/outputs/baking/add_custom_tags` - Filtering by input length. Input may be video, sequence or single image. It is possible that `.mp4` should be created only when input is video or sequence and to create review `.png` when input is single frame. In some cases the output should be created even if it's single frame or multi frame input. diff --git a/website/docs/pype2/admin_presets_plugins.md b/website/docs/pype2/admin_presets_plugins.md index a869ead819..a039c5fbd8 100644 --- a/website/docs/pype2/admin_presets_plugins.md +++ b/website/docs/pype2/admin_presets_plugins.md @@ -534,7 +534,7 @@ Plugin responsible for generating thumbnails with colorspace controlled by Nuke. } ``` -### `ExtractReviewDataBakingStreams` +### `ExtractReviewIntermediate` `viewer_lut_raw` **true** will publish the baked mov file without any colorspace conversion. It will be baked with the workfile workspace. This can happen in case the Viewer input process uses baked screen space luts. #### baking with controlled colorspace From ef12a5229dec982aa525f9fee2c29493263e0faf Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 28 Sep 2023 20:27:55 +0800 Subject: [PATCH 203/267] plural form for extract_review_intermediate --- ...king_streams.py => extract_review_intermediates.py} | 4 ++-- openpype/settings/ayon_settings.py | 2 +- openpype/settings/defaults/project_settings/nuke.json | 2 +- .../projects_schema/schemas/schema_nuke_publish.json | 6 +++--- server_addon/nuke/server/settings/publish_plugins.py | 10 +++++----- .../docs/project_settings/settings_project_global.md | 2 +- website/docs/pype2/admin_presets_plugins.md | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) rename openpype/hosts/nuke/plugins/publish/{extract_review_baking_streams.py => extract_review_intermediates.py} (99%) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py similarity index 99% rename from openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py rename to openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py index 4407c039b4..2d996b1381 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_baking_streams.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py @@ -8,7 +8,7 @@ from openpype.hosts.nuke.api import plugin from openpype.hosts.nuke.api.lib import maintained_selection -class ExtractReviewIntermediate(publish.Extractor): +class ExtractReviewIntermediates(publish.Extractor): """Extracts Sequences and thumbnail with baked in luts must be run after extract_render_local.py @@ -32,7 +32,7 @@ class ExtractReviewIntermediate(publish.Extractor): """ nuke_publish = project_settings["nuke"]["publish"] deprecated_setting = nuke_publish["ExtractReviewDataMov"] - current_setting = nuke_publish["ExtractReviewIntermediate"] + current_setting = nuke_publish["ExtractReviewIntermediates"] if deprecated_setting["enabled"]: # Use deprecated settings if they are still enabled cls.viewer_lut_raw = deprecated_setting["viewer_lut_raw"] diff --git a/openpype/settings/ayon_settings.py b/openpype/settings/ayon_settings.py index fbf35aec0a..68693bb953 100644 --- a/openpype/settings/ayon_settings.py +++ b/openpype/settings/ayon_settings.py @@ -753,7 +753,7 @@ def _convert_nuke_project_settings(ayon_settings, output): # settings for backwards compatibility deprecrated_review_settings = ayon_publish["ExtractReviewDataMov"] current_review_settings = ( - ayon_publish["ExtractReviewIntermediate"] + ayon_publish["ExtractReviewIntermediates"] ) if deprecrated_review_settings["enabled"]: outputs_settings = deprecrated_review_settings["outputs"] diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 7346c9d7b8..ad9f46c8ab 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -501,7 +501,7 @@ } } }, - "ExtractReviewIntermediate": { + "ExtractReviewIntermediates": { "enabled": true, "viewer_lut_raw": false, "outputs": { diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json index c14f47a3a7..fa08e19c63 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_nuke_publish.json @@ -373,14 +373,14 @@ }, { "type": "label", - "label": "^ Settings and for ExtractReviewDataMov is deprecated and will be soon removed.
Please use ExtractReviewIntermediate instead." + "label": "^ Settings and for ExtractReviewDataMov is deprecated and will be soon removed.
Please use ExtractReviewIntermediates instead." }, { "type": "dict", "collapsible": true, "checkbox_key": "enabled", - "key": "ExtractReviewIntermediate", - "label": "ExtractReviewIntermediate", + "key": "ExtractReviewIntermediates", + "label": "ExtractReviewIntermediates", "is_group": true, "children": [ { diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index 399aa7e38e..efb814eff0 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -177,7 +177,7 @@ class ExtractReviewDataMovModel(BaseSettingsModel): ) -class ExtractReviewBakingStreamsModel(BaseSettingsModel): +class ExtractReviewIntermediatesModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") viewer_lut_raw: bool = Field(title="Viewer lut raw") outputs: list[BakingStreamModel] = Field( @@ -282,9 +282,9 @@ class PublishPuginsModel(BaseSettingsModel): title="Extract Review Data Mov", default_factory=ExtractReviewDataMovModel ) - ExtractReviewIntermediate: ExtractReviewBakingStreamsModel = Field( - title="Extract Review Data Baking Streams", - default_factory=ExtractReviewBakingStreamsModel + ExtractReviewIntermediates: ExtractReviewIntermediatesModel = Field( + title="Extract Review Intermediates", + default_factory=ExtractReviewIntermediatesModel ) ExtractSlateFrame: ExtractSlateFrameModel = Field( title="Extract Slate Frame", @@ -481,7 +481,7 @@ DEFAULT_PUBLISH_PLUGIN_SETTINGS = { } ] }, - "ExtractReviewIntermediate": { + "ExtractReviewIntermediates": { "enabled": True, "viewer_lut_raw": False, "outputs": [ diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 3aa9772118..27aa60a464 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -189,7 +189,7 @@ A profile may generate multiple outputs from a single input. Each output must de - Profile filtering defines which group of output definitions is used but output definitions may require more specific filters on their own. - They may filter by subset name (regex can be used) or publish families. Publish families are more complex as are based on knowing code base. - Filtering by custom tags -> this is used for targeting to output definitions from other extractors using settings (at this moment only Nuke bake extractor can target using custom tags). - - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewIntermediate/outputs/baking/add_custom_tags` + - Nuke extractor settings path: `project_settings/nuke/publish/ExtractReviewIntermediates/outputs/baking/add_custom_tags` - Filtering by input length. Input may be video, sequence or single image. It is possible that `.mp4` should be created only when input is video or sequence and to create review `.png` when input is single frame. In some cases the output should be created even if it's single frame or multi frame input. diff --git a/website/docs/pype2/admin_presets_plugins.md b/website/docs/pype2/admin_presets_plugins.md index a039c5fbd8..b5e8a3b8a8 100644 --- a/website/docs/pype2/admin_presets_plugins.md +++ b/website/docs/pype2/admin_presets_plugins.md @@ -534,7 +534,7 @@ Plugin responsible for generating thumbnails with colorspace controlled by Nuke. } ``` -### `ExtractReviewIntermediate` +### `ExtractReviewIntermediates` `viewer_lut_raw` **true** will publish the baked mov file without any colorspace conversion. It will be baked with the workfile workspace. This can happen in case the Viewer input process uses baked screen space luts. #### baking with controlled colorspace From f45552ff79fcc72b38825886584b4cecf9160a43 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 28 Sep 2023 20:29:09 +0800 Subject: [PATCH 204/267] label tweak --- .../hosts/nuke/plugins/publish/extract_review_intermediates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py index 2d996b1381..78fb37e8d7 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py @@ -16,7 +16,7 @@ class ExtractReviewIntermediates(publish.Extractor): """ order = pyblish.api.ExtractorOrder + 0.01 - label = "Extract Review Intermediate" + label = "Extract Review Intermediates" families = ["review"] hosts = ["nuke"] From dda932e83ef448c74533ea39f687a6d919a2551c Mon Sep 17 00:00:00 2001 From: Kayla Date: Thu, 28 Sep 2023 22:37:30 +0800 Subject: [PATCH 205/267] 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 206/267] 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 207/267] 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 208/267] 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 209/267] 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 210/267] 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 211/267] 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 212/267] 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 f57c1eb8889fea3b29a85ef0e425c909236dcc7a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 28 Sep 2023 23:32:39 +0800 Subject: [PATCH 213/267] edit docsting and rename BakingStreamModel as IntermediateOutputModel --- .../nuke/plugins/publish/extract_review_intermediates.py | 3 ++- server_addon/nuke/server/settings/publish_plugins.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py index 78fb37e8d7..da060e3157 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_intermediates.py @@ -9,7 +9,8 @@ from openpype.hosts.nuke.api.lib import maintained_selection class ExtractReviewIntermediates(publish.Extractor): - """Extracts Sequences and thumbnail with baked in luts + """Extracting intermediate videos or sequences with + thumbnail for transcoding. must be run after extract_render_local.py diff --git a/server_addon/nuke/server/settings/publish_plugins.py b/server_addon/nuke/server/settings/publish_plugins.py index efb814eff0..19206149b6 100644 --- a/server_addon/nuke/server/settings/publish_plugins.py +++ b/server_addon/nuke/server/settings/publish_plugins.py @@ -149,7 +149,7 @@ class ReformatNodesConfigModel(BaseSettingsModel): ) -class BakingStreamModel(BaseSettingsModel): +class IntermediateOutputModel(BaseSettingsModel): name: str = Field(title="Output name") filter: BakingStreamFilterModel = Field( title="Filter", default_factory=BakingStreamFilterModel) @@ -171,7 +171,7 @@ class ExtractReviewDataMovModel(BaseSettingsModel): """ enabled: bool = Field(title="Enabled") viewer_lut_raw: bool = Field(title="Viewer lut raw") - outputs: list[BakingStreamModel] = Field( + outputs: list[IntermediateOutputModel] = Field( default_factory=list, title="Baking streams" ) @@ -180,7 +180,7 @@ class ExtractReviewDataMovModel(BaseSettingsModel): class ExtractReviewIntermediatesModel(BaseSettingsModel): enabled: bool = Field(title="Enabled") viewer_lut_raw: bool = Field(title="Viewer lut raw") - outputs: list[BakingStreamModel] = Field( + outputs: list[IntermediateOutputModel] = Field( default_factory=list, title="Baking streams" ) From ea4ce1b8be7a721124445770a6169ab960145b3a Mon Sep 17 00:00:00 2001 From: Kayla Date: Fri, 29 Sep 2023 15:17:41 +0800 Subject: [PATCH 214/267] 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 215/267] 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 216/267] 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 217/267] 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 846bd0fd590688ceb4640b00f8f0f3af2ce6fa65 Mon Sep 17 00:00:00 2001 From: Kayla Date: Fri, 29 Sep 2023 22:12:02 +0800 Subject: [PATCH 218/267] 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 f287144616a5341c6651cb5af5b49416af3b4e30 Mon Sep 17 00:00:00 2001 From: Kayla Date: Fri, 29 Sep 2023 23:28:39 +0800 Subject: [PATCH 219/267] 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 a66edaf1d051be4c6be2cfe189fe7a5912296968 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 29 Sep 2023 17:33:28 +0200 Subject: [PATCH 220/267] Maya: implement matchmove publishing (#5445) * OP-6360 - allow export of multiple cameras as alembic * OP-6360 - make validation of camera count optional * OP-6360 - make ValidatorCameraContents optional This validator checks number of cameras, without optionality publish wouldn't be possible. * OP-6360 - allow extraction of multiple cameras to .ma * OP-6360 - update defaults for Ayon Changes to Ayon settings should also bump up version of addon. * OP-6360 - new matchmove creator This family should be for more complex sets (eg. multiple cameras, with geometry, planes etc. * OP-6360 - updated camera extractors Added matchmove family to extract multiple cameras. Single camera is protected by required validator. * OP-6360 - added matchmove to reference loader * Revert "OP-6360 - make ValidatorCameraContents optional" This reverts commit 4096e81f785b1299b54b1e485eb672403fb89a66. * Revert "OP-6360 - update defaults for Ayon" This reverts commit 4391b25cfc93fbb783146a726c6097477146c467. * OP-6360 - performance update Number of cameras might be quite large, set operations will be faster than loop. * Revert "OP-6360 - make validation of camera count optional" This reverts commit ee3d91a4cbec607b0f8cc9d47382684eba88d6d0. * OP-6360 - explicitly cast to list for Maya functions cmds.ls doesn't like sets in some older versions of Maya apparently. Sets are used here for performance reason, so explicitly cast them to list to make Maya happy. * OP-6360 - added documentation about matchmove family * OP-6360 - copy input planes * OP-6360 - expose Settings to keep Image planes Previous implementation didn't export Image planes in Maya file, to keep behavior backward compatible new Setting was added and set to False. * OP-6360 - make both camera extractors optional In Settings Alembic extractor was visible as optional even if code didn't follow that. * OP-6360 - used long name * OP-6360 - fix wrong variable * Update openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py Co-authored-by: Roy Nieterau * OP-6360 - removed shortening of varible * OP-6360 - Hound * OP-6360 - fix wrong key * Update openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py Co-authored-by: Toke Jepsen * Update openpype/hosts/maya/api/lib.py Co-authored-by: Toke Jepsen * Update openpype/hosts/maya/plugins/publish/extract_camera_alembic.py Co-authored-by: Toke Jepsen * OP-6360 - fix wrong variable * OP-6360 - added reattaching method Image planes were attached wrong, added method to reattach them properly. * Revert "Update openpype/hosts/maya/api/lib.py" This reverts commit 4f40ad613946903e8c51b2720ac52756e701f8b8. * OP-6360 - exported baked camera should be deleted Forgotten commenting just for development. * OP-6360 - updated docstring * OP-6360 - remove scale keys Currently parentConstraint from old camera to new one doesn't work for keyed scale attributes. To key scale attributes doesn't make much sense so as a workaround, keys for scale attributes are checked AND if they are diferent from defaults (1.0) publish fails (as artist might want to actually key scale). If all scale keys are defaults, they are temporarily removed, cameras are parent constrained, exported and old camera returned to original state. * OP-6360 - cleaned up resetting of scale keys Batch calls used instead of one by one. Cleaned up a return type as key value is no necessary as we are not setting it, just key. * OP-6360 - removed unnecessary logging * OP-6360 - reattach image plane to original camera Image plane must be reattached before baked camera(s) are deleted. * OP-6360 - added context manager to keep image planes attached to original camera Without this image planes would disappear after removal of baked cameras. * OP-6360 - refactored contextmanager * OP-6360 - renamed flag Input connections are not copied anymore as they might be dangerous. It is possible to epxlicitly attach only image planes instead. * OP-6360 - removed copyInputConnections Copying input connections might be dangerous (rig etc.), it is possible to explicitly attach only image planes. * OP-6360 - updated plugin labels * Update openpype/hosts/maya/plugins/create/create_matchmove.py Co-authored-by: Roy Nieterau * OP-6360 - fixed formatting --------- Co-authored-by: Roy Nieterau Co-authored-by: Toke Jepsen --- openpype/hosts/maya/api/lib.py | 2 +- .../maya/plugins/create/create_matchmove.py | 32 ++++ .../hosts/maya/plugins/load/load_reference.py | 3 +- .../plugins/publish/extract_camera_alembic.py | 22 ++- .../publish/extract_camera_mayaScene.py | 142 ++++++++++++++---- .../defaults/project_settings/maya.json | 6 + .../schemas/schema_maya_publish.json | 29 ++++ website/docs/artist_publish.md | 70 ++++----- 8 files changed, 229 insertions(+), 77 deletions(-) create mode 100644 openpype/hosts/maya/plugins/create/create_matchmove.py diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 40b3419e73..a197e5b592 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2571,7 +2571,7 @@ def bake_to_world_space(nodes, new_name = "{0}_baked".format(short_name) new_node = cmds.duplicate(node, name=new_name, - renameChildren=True)[0] + renameChildren=True)[0] # noqa # Connect all attributes on the node except for transform # attributes diff --git a/openpype/hosts/maya/plugins/create/create_matchmove.py b/openpype/hosts/maya/plugins/create/create_matchmove.py new file mode 100644 index 0000000000..e64eb6a471 --- /dev/null +++ b/openpype/hosts/maya/plugins/create/create_matchmove.py @@ -0,0 +1,32 @@ +from openpype.hosts.maya.api import ( + lib, + plugin +) +from openpype.lib import BoolDef + + +class CreateMatchmove(plugin.MayaCreator): + """Instance for more complex setup of cameras. + + Might contain multiple cameras, geometries etc. + + It is expected to be extracted into .abc or .ma + """ + + identifier = "io.openpype.creators.maya.matchmove" + label = "Matchmove" + family = "matchmove" + icon = "video-camera" + + def get_instance_attr_defs(self): + + defs = lib.collect_animation_defs() + + defs.extend([ + BoolDef("bakeToWorldSpace", + label="Bake Cameras to World-Space", + tooltip="Bake Cameras to World-Space", + default=True), + ]) + + return defs diff --git a/openpype/hosts/maya/plugins/load/load_reference.py b/openpype/hosts/maya/plugins/load/load_reference.py index 61f337f501..4b704fa706 100644 --- a/openpype/hosts/maya/plugins/load/load_reference.py +++ b/openpype/hosts/maya/plugins/load/load_reference.py @@ -101,7 +101,8 @@ class ReferenceLoader(openpype.hosts.maya.api.plugin.ReferenceLoader): "camerarig", "staticMesh", "skeletalMesh", - "mvLook"] + "mvLook", + "matchmove"] representations = ["ma", "abc", "fbx", "mb"] diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py index 4ec1399df4..43803743bc 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_alembic.py @@ -6,17 +6,21 @@ from openpype.pipeline import publish from openpype.hosts.maya.api import lib -class ExtractCameraAlembic(publish.Extractor): +class ExtractCameraAlembic(publish.Extractor, + publish.OptionalPyblishPluginMixin): """Extract a Camera as Alembic. - The cameras gets baked to world space by default. Only when the instance's + The camera gets baked to world space by default. Only when the instance's `bakeToWorldSpace` is set to False it will include its full hierarchy. + 'camera' family expects only single camera, if multiple cameras are needed, + 'matchmove' is better choice. + """ - label = "Camera (Alembic)" + label = "Extract Camera (Alembic)" hosts = ["maya"] - families = ["camera"] + families = ["camera", "matchmove"] bake_attributes = [] def process(self, instance): @@ -35,10 +39,11 @@ class ExtractCameraAlembic(publish.Extractor): # validate required settings assert isinstance(step, float), "Step must be a float value" - camera = cameras[0] # Define extract output file path dir_path = self.staging_dir(instance) + if not os.path.exists(dir_path): + os.makedirs(dir_path) filename = "{0}.abc".format(instance.name) path = os.path.join(dir_path, filename) @@ -64,9 +69,10 @@ class ExtractCameraAlembic(publish.Extractor): # if baked, drop the camera hierarchy to maintain # clean output and backwards compatibility - camera_root = cmds.listRelatives( - camera, parent=True, fullPath=True)[0] - job_str += ' -root {0}'.format(camera_root) + camera_roots = cmds.listRelatives( + cameras, parent=True, fullPath=True) + for camera_root in camera_roots: + job_str += ' -root {0}'.format(camera_root) for member in members: descendants = cmds.listRelatives(member, diff --git a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py index a50a8f0dfa..38cf00bbdd 100644 --- a/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py +++ b/openpype/hosts/maya/plugins/publish/extract_camera_mayaScene.py @@ -2,11 +2,15 @@ """Extract camera as Maya Scene.""" import os import itertools +import contextlib from maya import cmds from openpype.pipeline import publish from openpype.hosts.maya.api import lib +from openpype.lib import ( + BoolDef +) def massage_ma_file(path): @@ -78,7 +82,8 @@ def unlock(plug): cmds.disconnectAttr(source, destination) -class ExtractCameraMayaScene(publish.Extractor): +class ExtractCameraMayaScene(publish.Extractor, + publish.OptionalPyblishPluginMixin): """Extract a Camera as Maya Scene. This will create a duplicate of the camera that will be baked *with* @@ -88,17 +93,22 @@ class ExtractCameraMayaScene(publish.Extractor): The cameras gets baked to world space by default. Only when the instance's `bakeToWorldSpace` is set to False it will include its full hierarchy. + 'camera' family expects only single camera, if multiple cameras are needed, + 'matchmove' is better choice. + Note: The extracted Maya ascii file gets "massaged" removing the uuid values so they are valid for older versions of Fusion (e.g. 6.4) """ - label = "Camera (Maya Scene)" + label = "Extract Camera (Maya Scene)" hosts = ["maya"] - families = ["camera"] + families = ["camera", "matchmove"] scene_type = "ma" + keep_image_planes = True + def process(self, instance): """Plugin entry point.""" # get settings @@ -131,15 +141,15 @@ class ExtractCameraMayaScene(publish.Extractor): "bake to world space is ignored...") # get cameras - members = cmds.ls(instance.data['setMembers'], leaf=True, shapes=True, - long=True, dag=True) - cameras = cmds.ls(members, leaf=True, shapes=True, long=True, - dag=True, type="camera") + members = set(cmds.ls(instance.data['setMembers'], leaf=True, + shapes=True, long=True, dag=True)) + cameras = set(cmds.ls(members, leaf=True, shapes=True, long=True, + dag=True, type="camera")) # validate required settings assert isinstance(step, float), "Step must be a float value" - camera = cameras[0] - transform = cmds.listRelatives(camera, parent=True, fullPath=True) + transforms = cmds.listRelatives(list(cameras), + parent=True, fullPath=True) # Define extract output file path dir_path = self.staging_dir(instance) @@ -151,23 +161,21 @@ class ExtractCameraMayaScene(publish.Extractor): with lib.evaluation("off"): with lib.suspended_refresh(): if bake_to_worldspace: - self.log.debug( - "Performing camera bakes: {}".format(transform)) baked = lib.bake_to_world_space( - transform, + transforms, frame_range=[start, end], step=step ) - baked_camera_shapes = cmds.ls(baked, - type="camera", - dag=True, - shapes=True, - long=True) + baked_camera_shapes = set(cmds.ls(baked, + type="camera", + dag=True, + shapes=True, + long=True)) - members = members + baked_camera_shapes - members.remove(camera) + members.update(baked_camera_shapes) + members.difference_update(cameras) else: - baked_camera_shapes = cmds.ls(cameras, + baked_camera_shapes = cmds.ls(list(cameras), type="camera", dag=True, shapes=True, @@ -186,19 +194,28 @@ class ExtractCameraMayaScene(publish.Extractor): unlock(plug) cmds.setAttr(plug, value) - self.log.debug("Performing extraction..") - cmds.select(cmds.ls(members, dag=True, - shapes=True, long=True), noExpand=True) - cmds.file(path, - force=True, - typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 - exportSelected=True, - preserveReferences=False, - constructionHistory=False, - channels=True, # allow animation - constraints=False, - shader=False, - expressions=False) + attr_values = self.get_attr_values_from_data( + instance.data) + keep_image_planes = attr_values.get("keep_image_planes") + + with transfer_image_planes(sorted(cameras), + sorted(baked_camera_shapes), + keep_image_planes): + + self.log.info("Performing extraction..") + cmds.select(cmds.ls(list(members), dag=True, + shapes=True, long=True), + noExpand=True) + cmds.file(path, + force=True, + typ="mayaAscii" if self.scene_type == "ma" else "mayaBinary", # noqa: E501 + exportSelected=True, + preserveReferences=False, + constructionHistory=False, + channels=True, # allow animation + constraints=False, + shader=False, + expressions=False) # Delete the baked hierarchy if bake_to_worldspace: @@ -219,3 +236,62 @@ class ExtractCameraMayaScene(publish.Extractor): self.log.debug("Extracted instance '{0}' to: {1}".format( instance.name, path)) + + @classmethod + def get_attribute_defs(cls): + defs = super(ExtractCameraMayaScene, cls).get_attribute_defs() + + defs.extend([ + BoolDef("keep_image_planes", + label="Keep Image Planes", + tooltip="Preserving connected image planes on camera", + default=cls.keep_image_planes), + + ]) + + return defs + + +@contextlib.contextmanager +def transfer_image_planes(source_cameras, target_cameras, + keep_input_connections): + """Reattaches image planes to baked or original cameras. + + Baked cameras are duplicates of original ones. + This attaches it to duplicated camera properly and after + export it reattaches it back to original to keep image plane in workfile. + """ + originals = {} + try: + for source_camera, target_camera in zip(source_cameras, + target_cameras): + image_planes = cmds.listConnections(source_camera, + type="imagePlane") or [] + + # Split of the parent path they are attached - we want + # the image plane node name. + # TODO: Does this still mean the image plane name is unique? + image_planes = [x.split("->", 1)[1] for x in image_planes] + + if not image_planes: + continue + + originals[source_camera] = [] + for image_plane in image_planes: + if keep_input_connections: + if source_camera == target_camera: + continue + _attach_image_plane(target_camera, image_plane) + else: # explicitly dettaching image planes + cmds.imagePlane(image_plane, edit=True, detach=True) + originals[source_camera].append(image_plane) + yield + finally: + for camera, image_planes in originals.items(): + for image_plane in image_planes: + _attach_image_plane(camera, image_plane) + + +def _attach_image_plane(camera, image_plane): + cmds.imagePlane(image_plane, edit=True, detach=True) + cmds.imagePlane(image_plane, edit=True, camera=camera) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 38f14ec022..83ca6fecef 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1338,6 +1338,12 @@ "active": true, "bake_attributes": [] }, + "ExtractCameraMayaScene": { + "enabled": true, + "optional": true, + "active": true, + "keep_image_planes": false + }, "ExtractGLB": { "enabled": 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 b115ee3faa..13c00ff183 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 @@ -978,6 +978,35 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractCameraMayaScene", + "label": "Extract camera to Maya scene", + "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": "keep_image_planes", + "label": "Export Image planes" + } + ] + }, { "type": "dict", "collapsible": true, diff --git a/website/docs/artist_publish.md b/website/docs/artist_publish.md index 321eb5c56a..b1be2e629e 100644 --- a/website/docs/artist_publish.md +++ b/website/docs/artist_publish.md @@ -33,39 +33,41 @@ The Instances are categorized into ‘families’ based on what type of data the Following family definitions and requirements are OpenPype defaults and what we consider good industry practice, but most of the requirements can be easily altered to suit the studio or project needs. Here's a list of supported families -| Family | Comment | Example Subsets | -| ----------------------- | ------------------------------------------------ | ------------------------- | -| [Model](#model) | Cleaned geo without materials | main, proxy, broken | -| [Look](#look) | Package of shaders, assignments and textures | main, wet, dirty | -| [Rig](#rig) | Characters or props with animation controls | main, deform, sim | -| [Assembly](#assembly) | A complex model made from multiple other models. | main, deform, sim | -| [Layout](#layout) | Simple representation of the environment | main, | -| [Setdress](#setdress) | Environment containing only referenced assets | main, | -| [Camera](#camera) | May contain trackers or proxy geo | main, tracked, anim | -| [Animation](#animation) | Animation exported from a rig. | characterA, vehicleB | -| [Cache](#cache) | Arbitrary animated geometry or fx cache | rest, ROM , pose01 | -| MayaAscii | Maya publishes that don't fit other categories | | -| [Render](#render) | Rendered frames from CG or Comp | | -| RenderSetup | Scene render settings, AOVs and layers | | -| Plate | Ingested, transcode, conformed footage | raw, graded, imageplane | -| Write | Nuke write nodes for rendering | | -| Image | Any non-plate image to be used by artists | Reference, ConceptArt | -| LayeredImage | Software agnostic layered image with metadata | Reference, ConceptArt | -| Review | Reviewable video or image. | | -| Matchmove | Matchmoved camera, potentially with geometry | main | -| Workfile | Backup of the workfile with all its content | uses the task name | -| Nukenodes | Any collection of nuke nodes | maskSetup, usefulBackdrop | -| Yeticache | Cached out yeti fur setup | | -| YetiRig | Yeti groom ready to be applied to geometry cache | main, destroyed | -| VrayProxy | Vray proxy geometry for rendering | | -| VrayScene | Vray full scene export | | -| ArnodldStandin | All arnold .ass archives for rendering | main, wet, dirty | -| LUT | | | -| Nukenodes | | | -| Gizmo | | | -| Nukenodes | | | -| Harmony.template | | | -| Harmony.palette | | | +| Family | Comment | Example Subsets | +|-------------------------|-------------------------------------------------------| ------------------------- | +| [Model](#model) | Cleaned geo without materials | main, proxy, broken | +| [Look](#look) | Package of shaders, assignments and textures | main, wet, dirty | +| [Rig](#rig) | Characters or props with animation controls | main, deform, sim | +| [Assembly](#assembly) | A complex model made from multiple other models. | main, deform, sim | +| [Layout](#layout) | Simple representation of the environment | main, | +| [Setdress](#setdress) | Environment containing only referenced assets | main, | +| [Camera](#camera) | May contain trackers or proxy geo, only single camera | main, tracked, anim | +| | expected. | | +| [Animation](#animation) | Animation exported from a rig. | characterA, vehicleB | +| [Cache](#cache) | Arbitrary animated geometry or fx cache | rest, ROM , pose01 | +| MayaAscii | Maya publishes that don't fit other categories | | +| [Render](#render) | Rendered frames from CG or Comp | | +| RenderSetup | Scene render settings, AOVs and layers | | +| Plate | Ingested, transcode, conformed footage | raw, graded, imageplane | +| Write | Nuke write nodes for rendering | | +| Image | Any non-plate image to be used by artists | Reference, ConceptArt | +| LayeredImage | Software agnostic layered image with metadata | Reference, ConceptArt | +| Review | Reviewable video or image. | | +| Matchmove | Matchmoved camera, potentially with geometry, allows | main | +| | multiple cameras even with planes. | | +| Workfile | Backup of the workfile with all its content | uses the task name | +| Nukenodes | Any collection of nuke nodes | maskSetup, usefulBackdrop | +| Yeticache | Cached out yeti fur setup | | +| YetiRig | Yeti groom ready to be applied to geometry cache | main, destroyed | +| VrayProxy | Vray proxy geometry for rendering | | +| VrayScene | Vray full scene export | | +| ArnodldStandin | All arnold .ass archives for rendering | main, wet, dirty | +| LUT | | | +| Nukenodes | | | +| Gizmo | | | +| Nukenodes | | | +| Harmony.template | | | +| Harmony.palette | | | @@ -161,7 +163,7 @@ Example Representations: ### Animation Published result of an animation created with a rig. Animation can be extracted -as animation curves, cached out geometry or even fully animated rig with all the controllers. +as animation curves, cached out geometry or even fully animated rig with all the controllers. Animation cache is usually defined by a rigger in the rig file of a character or by FX TD in the effects rig, to ensure consistency of outputs. From 8520a91cc8a3b7b18a2c63f09fc2b21cdd5599e9 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Sat, 30 Sep 2023 03:24:20 +0000 Subject: [PATCH 221/267] [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 f1e0cd0b80..8234258f19 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.17.1" +__version__ = "3.17.2-nightly.1" From f113ddb4eda84a8e112059edfce596327c6cf826 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 30 Sep 2023 03:24:56 +0000 Subject: [PATCH 222/267] 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 591d865ca5..9fb7bbc66c 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.1 - 3.17.1 - 3.17.1-nightly.3 - 3.17.1-nightly.2 @@ -134,7 +135,6 @@ body: - 3.14.10-nightly.8 - 3.14.10-nightly.7 - 3.14.10-nightly.6 - - 3.14.10-nightly.5 validations: required: true - type: dropdown From 6d451ccd09fab8bad13a42c21850605133660d03 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 30 Sep 2023 13:15:12 +0200 Subject: [PATCH 223/267] 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 224/267] 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 225/267] 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 226/267] 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 227/267] 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 228/267] 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 229/267] 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 7f5879b0e8d12761add2ea5ec527259c462b567e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 1 Oct 2023 22:46:16 +0200 Subject: [PATCH 230/267] Avoid memory leak - actually clear stored plugins on reset --- openpype/tools/publisher/control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index d4e0ae0453..677c1da51a 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -194,6 +194,7 @@ class PublishReportMaker: self._publish_discover_result = create_context.publish_discover_result self._plugin_data = [] self._plugin_data_with_plugin = [] + self._stored_plugins = [] self._current_plugin_data = {} self._all_instances_by_id = {} self._current_context = context From 52965a827597532c7f11eee3592e51fd8473f650 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 1 Oct 2023 22:46:51 +0200 Subject: [PATCH 231/267] Use `set` since it's supposed to be unique entries and is used in many lookups --- openpype/tools/publisher/control.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index 677c1da51a..e6b68906fd 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -179,7 +179,7 @@ class PublishReportMaker: self._plugin_data = [] self._plugin_data_with_plugin = [] - self._stored_plugins = [] + self._stored_plugins = set() self._current_plugin_data = [] self._all_instances_by_id = {} self._current_context = None @@ -194,7 +194,7 @@ class PublishReportMaker: self._publish_discover_result = create_context.publish_discover_result self._plugin_data = [] self._plugin_data_with_plugin = [] - self._stored_plugins = [] + self._stored_plugins = set() self._current_plugin_data = {} self._all_instances_by_id = {} self._current_context = context @@ -230,7 +230,7 @@ class PublishReportMaker: raise ValueError( "Plugin '{}' is already stored".format(str(plugin))) - self._stored_plugins.append(plugin) + self._stored_plugins.add(plugin) plugin_data_item = self._create_plugin_data_item(plugin) From 61f7a2039b60567416d80005c046aab7a5e28de2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 00:57:22 +0200 Subject: [PATCH 232/267] 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 233/267] 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 234/267] 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 235/267] 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 236/267] 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 237/267] 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 238/267] 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 239/267] 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 240/267] 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 241/267] 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 242/267] 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 243/267] 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 244/267] 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 245/267] 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 246/267] 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 247/267] 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 248/267] 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 249/267] :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 250/267] :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 251/267] 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 252/267] [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 253/267] 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 254/267] 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 255/267] 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 256/267] 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 257/267] 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 258/267] 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 259/267] 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 260/267] 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 d26df62e1502beed52522efe3a4b5a6bb9679ee8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 5 Oct 2023 13:00:52 +0200 Subject: [PATCH 261/267] 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 262/267] 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 263/267] 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 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 264/267] 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 e255c20c440211d3578fc7bcc7b350b6756dd859 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Oct 2023 15:37:45 +0200 Subject: [PATCH 265/267] 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 266/267] 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 12f41289018c46ab09eb5336a3dcdea93057183d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 5 Oct 2023 16:46:20 +0200 Subject: [PATCH 267/267] 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,