From edc10cd85f232d132266b00dae6392ab33945b3f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 11:57:51 +0100 Subject: [PATCH 001/483] OP-4643 - added Settings for ExtractColorTranscode --- .../defaults/project_settings/global.json | 8 +- .../schemas/schema_global_publish.json | 73 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 0b4e4c74e6..167f7611ce 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -70,6 +70,10 @@ "output": [] } }, + "ExtractColorTranscode": { + "enabled": true, + "profiles": [] + }, "ExtractReview": { "enabled": true, "profiles": [ @@ -442,7 +446,9 @@ "template": "{family}{Task}" }, { - "families": ["render"], + "families": [ + "render" + ], "hosts": [ "aftereffects" ], diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5388d04bc9..46ae6ba554 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -197,6 +197,79 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractColorTranscode", + "label": "ExtractColorTranscode", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "hosts", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "key": "ext", + "label": "Output extension", + "type": "text" + }, + { + "key": "output_colorspace", + "label": "Output colorspace", + "type": "text" + }, + { + "key": "custom_tags", + "label": "Custom Tags", + "type": "list", + "object_type": "text" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From c9996bb0c00b2fdb3d4db964efe1933bc99438e1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 11:58:51 +0100 Subject: [PATCH 002/483] OP-4643 - added ExtractColorTranscode Added method to convert from one colorspace to another to transcoding lib --- openpype/lib/transcoding.py | 53 ++++++++ .../publish/extract_color_transcode.py | 124 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 openpype/plugins/publish/extract_color_transcode.py diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 57279d0380..6899811ed5 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1037,3 +1037,56 @@ def convert_ffprobe_fps_to_float(value): if divisor == 0.0: return 0.0 return dividend / divisor + + +def convert_colorspace_for_input_paths( + input_paths, + output_dir, + source_color_space, + target_color_space, + logger=None +): + """Convert source files from one color space to another. + + Filenames of input files are kept so make sure that output directory + is not the same directory as input files have. + - This way it can handle gaps and can keep input filenames without handling + frame template + + Args: + input_paths (str): Paths that should be converted. It is expected that + contains single file or image sequence of samy type. + output_dir (str): Path to directory where output will be rendered. + Must not be same as input's directory. + source_color_space (str): ocio valid color space of source files + target_color_space (str): ocio valid target color space + logger (logging.Logger): Logger used for logging. + + """ + if logger is None: + logger = logging.getLogger(__name__) + + input_arg = "-i" + oiio_cmd = [ + get_oiio_tools_path(), + + # Don't add any additional attributes + "--nosoftwareattrib", + "--colorconvert", source_color_space, target_color_space + ] + for input_path in input_paths: + # Prepare subprocess arguments + + oiio_cmd.extend([ + input_arg, input_path, + ]) + + # Add last argument - path to output + base_filename = os.path.basename(input_path) + output_path = os.path.join(output_dir, base_filename) + oiio_cmd.extend([ + "-o", output_path + ]) + + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=logger) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py new file mode 100644 index 0000000000..58508ab18f --- /dev/null +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -0,0 +1,124 @@ +import pyblish.api + +from openpype.pipeline import publish +from openpype.lib import ( + + is_oiio_supported, +) + +from openpype.lib.transcoding import ( + convert_colorspace_for_input_paths, + get_transcode_temp_directory, +) + +from openpype.lib.profiles_filtering import filter_profiles + + +class ExtractColorTranscode(publish.Extractor): + """ + Extractor to convert colors from one colorspace to different. + """ + + label = "Transcode color spaces" + order = pyblish.api.ExtractorOrder + 0.01 + + optional = True + + # Configurable by Settings + profiles = None + options = None + + def process(self, instance): + if not self.profiles: + self.log.warning("No profiles present for create burnin") + return + + if "representations" not in instance.data: + self.log.warning("No representations, skipping.") + return + + if not is_oiio_supported(): + self.log.warning("OIIO not supported, no transcoding possible.") + return + + colorspace_data = instance.data.get("colorspaceData") + if not colorspace_data: + # TODO get_colorspace ?? + self.log.warning("Instance has not colorspace data, skipping") + return + source_color_space = colorspace_data["colorspace"] + + host_name = instance.context.data["hostName"] + family = instance.data["family"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + subset = instance.data["subset"] + + filtering_criteria = { + "hosts": host_name, + "families": family, + "task_names": task_name, + "task_types": task_type, + "subset": subset + } + profile = filter_profiles(self.profiles, filtering_criteria, + logger=self.log) + + if not profile: + self.log.info(( + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) + return + + self.log.debug("profile: {}".format(profile)) + + target_colorspace = profile["output_colorspace"] + if not target_colorspace: + raise RuntimeError("Target colorspace must be set") + + repres = instance.data.get("representations") or [] + for idx, repre in enumerate(repres): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self.repre_is_valid(repre): + continue + + new_staging_dir = get_transcode_temp_directory() + repre["stagingDir"] = new_staging_dir + files_to_remove = repre["files"] + if not isinstance(files_to_remove, list): + files_to_remove = [files_to_remove] + instance.context.data["cleanupFullPaths"].extend(files_to_remove) + + convert_colorspace_for_input_paths( + repre["files"], + new_staging_dir, + source_color_space, + target_colorspace, + self.log + ) + + def repre_is_valid(self, repre): + """Validation if representation should be processed. + + Args: + repre (dict): Representation which should be checked. + + Returns: + bool: False if can't be processed else True. + """ + + if "review" not in (repre.get("tags") or []): + self.log.info(( + "Representation \"{}\" don't have \"review\" tag. Skipped." + ).format(repre["name"])) + return False + + if not repre.get("files"): + self.log.warning(( + "Representation \"{}\" have empty files. Skipped." + ).format(repre["name"])) + return False + return True From 23a5f21f6b54f4f01d40c73ef5bdb032a2c7440f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 12:05:57 +0100 Subject: [PATCH 003/483] OP-4643 - extractor must run just before ExtractReview Nuke render local is set to 0.01 --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 58508ab18f..5163cd4045 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -20,7 +20,7 @@ class ExtractColorTranscode(publish.Extractor): """ label = "Transcode color spaces" - order = pyblish.api.ExtractorOrder + 0.01 + order = pyblish.api.ExtractorOrder + 0.019 optional = True From 9f8107df36dd8d281482d93fb6a0e3530d91fe91 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:03:22 +0100 Subject: [PATCH 004/483] OP-4643 - fix for full file paths --- .../publish/extract_color_transcode.py | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 5163cd4045..6ad7599f2c 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,3 +1,4 @@ +import os import pyblish.api from openpype.pipeline import publish @@ -41,13 +42,6 @@ class ExtractColorTranscode(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return - colorspace_data = instance.data.get("colorspaceData") - if not colorspace_data: - # TODO get_colorspace ?? - self.log.warning("Instance has not colorspace data, skipping") - return - source_color_space = colorspace_data["colorspace"] - host_name = instance.context.data["hostName"] family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) @@ -82,18 +76,32 @@ class ExtractColorTranscode(publish.Extractor): repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - if not self.repre_is_valid(repre): + # if not self.repre_is_valid(repre): + # continue + + colorspace_data = repre.get("colorspaceData") + if not colorspace_data: + # TODO get_colorspace ?? + self.log.warning("Repre has not colorspace data, skipping") + continue + source_color_space = colorspace_data["colorspace"] + config_path = colorspace_data.get("configData", {}).get("path") + if not os.path.exists(config_path): + self.log.warning("Config file doesn't exist, skipping") continue new_staging_dir = get_transcode_temp_directory() + original_staging_dir = repre["stagingDir"] repre["stagingDir"] = new_staging_dir - files_to_remove = repre["files"] - if not isinstance(files_to_remove, list): - files_to_remove = [files_to_remove] - instance.context.data["cleanupFullPaths"].extend(files_to_remove) + files_to_convert = repre["files"] + if not isinstance(files_to_convert, list): + files_to_convert = [files_to_convert] + files_to_convert = [os.path.join(original_staging_dir, path) + for path in files_to_convert] + instance.context.data["cleanupFullPaths"].extend(files_to_convert) convert_colorspace_for_input_paths( - repre["files"], + files_to_convert, new_staging_dir, source_color_space, target_colorspace, From d588ee7ed967e34adfd4017824d9f323e2ab7ad4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:04:06 +0100 Subject: [PATCH 005/483] OP-4643 - pass path for ocio config --- openpype/lib/transcoding.py | 3 +++ openpype/plugins/publish/extract_color_transcode.py | 1 + 2 files changed, 4 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 6899811ed5..792e8ddd1e 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1042,6 +1042,7 @@ def convert_ffprobe_fps_to_float(value): def convert_colorspace_for_input_paths( input_paths, output_dir, + config_path, source_color_space, target_color_space, logger=None @@ -1058,6 +1059,7 @@ def convert_colorspace_for_input_paths( contains single file or image sequence of samy type. output_dir (str): Path to directory where output will be rendered. Must not be same as input's directory. + config_path (str): path to OCIO config file source_color_space (str): ocio valid color space of source files target_color_space (str): ocio valid target color space logger (logging.Logger): Logger used for logging. @@ -1072,6 +1074,7 @@ def convert_colorspace_for_input_paths( # Don't add any additional attributes "--nosoftwareattrib", + "--colorconfig", config_path, "--colorconvert", source_color_space, target_color_space ] for input_path in input_paths: diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 6ad7599f2c..fdb13a47e8 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -103,6 +103,7 @@ class ExtractColorTranscode(publish.Extractor): convert_colorspace_for_input_paths( files_to_convert, new_staging_dir, + config_path, source_color_space, target_colorspace, self.log From 5ca4f825006d28bed4d0c01df71af3b3ffe21deb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:15:33 +0100 Subject: [PATCH 006/483] OP-4643 - add custom_tags --- openpype/plugins/publish/extract_color_transcode.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index fdb13a47e8..ab932b2476 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -72,6 +72,7 @@ class ExtractColorTranscode(publish.Extractor): target_colorspace = profile["output_colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") + custom_tags = profile["custom_tags"] repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): @@ -109,6 +110,11 @@ class ExtractColorTranscode(publish.Extractor): self.log ) + if custom_tags: + if not repre.get("custom_tags"): + repre["custom_tags"] = [] + repre["custom_tags"].extend(custom_tags) + def repre_is_valid(self, repre): """Validation if representation should be processed. From 4854b521d408d326f741cc73cda0ddc3e67795ce Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:18:38 +0100 Subject: [PATCH 007/483] OP-4643 - added docstring --- openpype/plugins/publish/extract_color_transcode.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index ab932b2476..88e2eed90f 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -18,6 +18,17 @@ from openpype.lib.profiles_filtering import filter_profiles class ExtractColorTranscode(publish.Extractor): """ Extractor to convert colors from one colorspace to different. + + Expects "colorspaceData" on representation. This dictionary is collected + previously and denotes that representation files should be converted. + This dict contains source colorspace information, collected by hosts. + + Target colorspace is selected by profiles in the Settings, based on: + - families + - host + - task types + - task names + - subset names """ label = "Transcode color spaces" From 39b8c111cfd418b74e0cf915590bcae2547c30ae Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:15:44 +0100 Subject: [PATCH 008/483] OP-4643 - updated Settings schema --- .../schemas/schema_global_publish.json | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 46ae6ba554..c2c911d7d6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -246,24 +246,44 @@ "type": "list", "object_type": "text" }, + { + "type": "boolean", + "key": "delete_original", + "label": "Delete Original Representation" + }, { "type": "splitter" }, { - "key": "ext", - "label": "Output extension", - "type": "text" - }, - { - "key": "output_colorspace", - "label": "Output colorspace", - "type": "text" - }, - { - "key": "custom_tags", - "label": "Custom Tags", - "type": "list", - "object_type": "text" + "key": "outputs", + "label": "Output Definitions", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "output_extension", + "label": "Output extension", + "type": "text" + }, + { + "key": "output_colorspace", + "label": "Output colorspace", + "type": "text" + }, + { + "type": "schema", + "name": "schema_representation_tags" + }, + { + "key": "custom_tags", + "label": "Custom Tags", + "type": "list", + "object_type": "text" + } + ] + } } ] } From a63ddebc19c7078c33b49029e5de3059b0fcdd9d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:17:25 +0100 Subject: [PATCH 009/483] OP-4643 - skip video files Only frames currently supported. --- .../plugins/publish/extract_color_transcode.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 88e2eed90f..a0714c9a33 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -36,6 +36,9 @@ class ExtractColorTranscode(publish.Extractor): optional = True + # Supported extensions + supported_exts = ["exr", "jpg", "jpeg", "png", "dpx"] + # Configurable by Settings profiles = None options = None @@ -88,13 +91,7 @@ class ExtractColorTranscode(publish.Extractor): repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - # if not self.repre_is_valid(repre): - # continue - - colorspace_data = repre.get("colorspaceData") - if not colorspace_data: - # TODO get_colorspace ?? - self.log.warning("Repre has not colorspace data, skipping") + if not self._repre_is_valid(repre): continue source_color_space = colorspace_data["colorspace"] config_path = colorspace_data.get("configData", {}).get("path") @@ -136,9 +133,9 @@ class ExtractColorTranscode(publish.Extractor): bool: False if can't be processed else True. """ - if "review" not in (repre.get("tags") or []): - self.log.info(( - "Representation \"{}\" don't have \"review\" tag. Skipped." + if repre.get("ext") not in self.supported_exts: + self.log.warning(( + "Representation \"{}\" of unsupported extension. Skipped." ).format(repre["name"])) return False From eced917d1a06fb6c973396c22c37db70bf71e4d4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:19:08 +0100 Subject: [PATCH 010/483] OP-4643 - refactored profile, delete of original Implemented multiple outputs from single input representation --- .../publish/extract_color_transcode.py | 156 ++++++++++++------ 1 file changed, 109 insertions(+), 47 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index a0714c9a33..b0c851d5f4 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,4 +1,6 @@ import os +import copy + import pyblish.api from openpype.pipeline import publish @@ -56,13 +58,94 @@ class ExtractColorTranscode(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return + profile = self._get_profile(instance) + if not profile: + return + + repres = instance.data.get("representations") or [] + for idx, repre in enumerate(list(repres)): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self._repre_is_valid(repre): + continue + + colorspace_data = repre["colorspaceData"] + source_color_space = colorspace_data["colorspace"] + config_path = colorspace_data.get("configData", {}).get("path") + if not os.path.exists(config_path): + self.log.warning("Config file doesn't exist, skipping") + continue + + repre = self._handle_original_repre(repre, profile) + + for _, output_def in profile.get("outputs", {}).items(): + new_repre = copy.deepcopy(repre) + + new_staging_dir = get_transcode_temp_directory() + original_staging_dir = new_repre["stagingDir"] + new_repre["stagingDir"] = new_staging_dir + files_to_convert = new_repre["files"] + if not isinstance(files_to_convert, list): + files_to_convert = [files_to_convert] + + files_to_delete = copy.deepcopy(files_to_convert) + + output_extension = output_def["output_extension"] + files_to_convert = self._rename_output_files(files_to_convert, + output_extension) + + files_to_convert = [os.path.join(original_staging_dir, path) + for path in files_to_convert] + + target_colorspace = output_def["output_colorspace"] + if not target_colorspace: + raise RuntimeError("Target colorspace must be set") + + convert_colorspace_for_input_paths( + files_to_convert, + new_staging_dir, + config_path, + source_color_space, + target_colorspace, + self.log + ) + + instance.context.data["cleanupFullPaths"].extend( + files_to_delete) + + custom_tags = output_def.get("custom_tags") + if custom_tags: + if not new_repre.get("custom_tags"): + new_repre["custom_tags"] = [] + new_repre["custom_tags"].extend(custom_tags) + + # Add additional tags from output definition to representation + for tag in output_def["tags"]: + if tag not in new_repre["tags"]: + new_repre["tags"].append(tag) + + instance.data["representations"].append(new_repre) + + def _rename_output_files(self, files_to_convert, output_extension): + """Change extension of converted files.""" + if output_extension: + output_extension = output_extension.replace('.', '') + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + new_file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(new_file_name) + files_to_convert = renamed_files + return files_to_convert + + def _get_profile(self, instance): + """Returns profile if and how repre should be color transcoded.""" host_name = instance.context.data["hostName"] family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) task_name = task_data.get("name") task_type = task_data.get("type") subset = instance.data["subset"] - filtering_criteria = { "hosts": host_name, "families": family, @@ -75,55 +158,15 @@ class ExtractColorTranscode(publish.Extractor): if not profile: self.log.info(( - "Skipped instance. None of profiles in presets are for" - " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" - " | Task type \"{}\" | Subset \"{}\" " - ).format(host_name, family, task_name, task_type, subset)) - return + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) self.log.debug("profile: {}".format(profile)) + return profile - target_colorspace = profile["output_colorspace"] - if not target_colorspace: - raise RuntimeError("Target colorspace must be set") - custom_tags = profile["custom_tags"] - - repres = instance.data.get("representations") or [] - for idx, repre in enumerate(repres): - self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - if not self._repre_is_valid(repre): - continue - source_color_space = colorspace_data["colorspace"] - config_path = colorspace_data.get("configData", {}).get("path") - if not os.path.exists(config_path): - self.log.warning("Config file doesn't exist, skipping") - continue - - new_staging_dir = get_transcode_temp_directory() - original_staging_dir = repre["stagingDir"] - repre["stagingDir"] = new_staging_dir - files_to_convert = repre["files"] - if not isinstance(files_to_convert, list): - files_to_convert = [files_to_convert] - files_to_convert = [os.path.join(original_staging_dir, path) - for path in files_to_convert] - instance.context.data["cleanupFullPaths"].extend(files_to_convert) - - convert_colorspace_for_input_paths( - files_to_convert, - new_staging_dir, - config_path, - source_color_space, - target_colorspace, - self.log - ) - - if custom_tags: - if not repre.get("custom_tags"): - repre["custom_tags"] = [] - repre["custom_tags"].extend(custom_tags) - - def repre_is_valid(self, repre): + def _repre_is_valid(self, repre): """Validation if representation should be processed. Args: @@ -144,4 +187,23 @@ class ExtractColorTranscode(publish.Extractor): "Representation \"{}\" have empty files. Skipped." ).format(repre["name"])) return False + + if not repre.get("colorspaceData"): + self.log.warning("Repre has not colorspace data, skipping") + return False + return True + + def _handle_original_repre(self, repre, profile): + delete_original = profile["delete_original"] + + if delete_original: + if not repre.get("tags"): + repre["tags"] = [] + + if "review" in repre["tags"]: + repre["tags"].remove("review") + if "delete" not in repre["tags"]: + repre["tags"].append("delete") + + return repre From 82a8fda4d6fd319be4f570d0731881d0effb3dac Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:23:01 +0100 Subject: [PATCH 011/483] OP-4643 - switched logging levels Do not use warning unnecessary. --- openpype/plugins/publish/extract_color_transcode.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index b0c851d5f4..4d38514b8b 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -47,11 +47,11 @@ class ExtractColorTranscode(publish.Extractor): def process(self, instance): if not self.profiles: - self.log.warning("No profiles present for create burnin") + self.log.debug("No profiles present for color transcode") return if "representations" not in instance.data: - self.log.warning("No representations, skipping.") + self.log.debug("No representations, skipping.") return if not is_oiio_supported(): @@ -177,19 +177,19 @@ class ExtractColorTranscode(publish.Extractor): """ if repre.get("ext") not in self.supported_exts: - self.log.warning(( + self.log.debug(( "Representation \"{}\" of unsupported extension. Skipped." ).format(repre["name"])) return False if not repre.get("files"): - self.log.warning(( + self.log.debug(( "Representation \"{}\" have empty files. Skipped." ).format(repre["name"])) return False if not repre.get("colorspaceData"): - self.log.warning("Repre has not colorspace data, skipping") + self.log.debug("Repre has no colorspace data. Skipped.") return False return True From 6f7e1c3cb49fe0162220919d398e86cddf68d0fa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:46:14 +0100 Subject: [PATCH 012/483] OP-4643 - propagate new extension to representation --- .../publish/extract_color_transcode.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4d38514b8b..62cf8f0dee 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -90,8 +90,13 @@ class ExtractColorTranscode(publish.Extractor): files_to_delete = copy.deepcopy(files_to_convert) output_extension = output_def["output_extension"] - files_to_convert = self._rename_output_files(files_to_convert, - output_extension) + output_extension = output_extension.replace('.', '') + if output_extension: + new_repre["name"] = output_extension + new_repre["ext"] = output_extension + + files_to_convert = self._rename_output_files( + files_to_convert, output_extension) files_to_convert = [os.path.join(original_staging_dir, path) for path in files_to_convert] @@ -127,15 +132,13 @@ class ExtractColorTranscode(publish.Extractor): def _rename_output_files(self, files_to_convert, output_extension): """Change extension of converted files.""" - if output_extension: - output_extension = output_extension.replace('.', '') - renamed_files = [] - for file_name in files_to_convert: - file_name, _ = os.path.splitext(file_name) - new_file_name = '{}.{}'.format(file_name, - output_extension) - renamed_files.append(new_file_name) - files_to_convert = renamed_files + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + new_file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(new_file_name) + files_to_convert = renamed_files return files_to_convert def _get_profile(self, instance): From d27dab7c970903e483aa6335ebee62ffffbab191 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:46:35 +0100 Subject: [PATCH 013/483] OP-4643 - added label to Settings --- .../projects_schema/schemas/schema_global_publish.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index c2c911d7d6..7155510fef 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -201,10 +201,14 @@ "type": "dict", "collapsible": true, "key": "ExtractColorTranscode", - "label": "ExtractColorTranscode", + "label": "ExtractColorTranscode (ImageIO)", "checkbox_key": "enabled", "is_group": true, "children": [ + { + "type": "label", + "label": "Configure output format(s) and color spaces for matching representations. Empty 'Output extension' denotes keeping source extension." + }, { "type": "boolean", "key": "enabled", From 58808104280eba59fed182af0849a66e366450f3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jan 2023 13:44:57 +0100 Subject: [PATCH 014/483] OP-4642 - refactor weird assignment Co-authored-by: Roy Nieterau --- openpype/plugins/publish/extract_color_transcode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 62cf8f0dee..835b64b685 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -138,8 +138,7 @@ class ExtractColorTranscode(publish.Extractor): new_file_name = '{}.{}'.format(file_name, output_extension) renamed_files.append(new_file_name) - files_to_convert = renamed_files - return files_to_convert + return renamed_files def _get_profile(self, instance): """Returns profile if and how repre should be color transcoded.""" From b35ee739bcfcc158daa9ef2d9261f3bda328d4cc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 11:57:51 +0100 Subject: [PATCH 015/483] OP-4643 - added Settings for ExtractColorTranscode --- .../defaults/project_settings/global.json | 4 + .../schemas/schema_global_publish.json | 73 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 0e078dc157..474e878cb6 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -68,6 +68,10 @@ "output": [] } }, + "ExtractColorTranscode": { + "enabled": true, + "profiles": [] + }, "ExtractReview": { "enabled": true, "profiles": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5388d04bc9..46ae6ba554 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -197,6 +197,79 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractColorTranscode", + "label": "ExtractColorTranscode", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "hosts", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "key": "ext", + "label": "Output extension", + "type": "text" + }, + { + "key": "output_colorspace", + "label": "Output colorspace", + "type": "text" + }, + { + "key": "custom_tags", + "label": "Custom Tags", + "type": "list", + "object_type": "text" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From 687d6dbf2811db3b8c6f46ee10726b26c1120772 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 11:58:51 +0100 Subject: [PATCH 016/483] OP-4643 - added ExtractColorTranscode Added method to convert from one colorspace to another to transcoding lib --- openpype/lib/transcoding.py | 53 ++++++++ .../publish/extract_color_transcode.py | 124 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 openpype/plugins/publish/extract_color_transcode.py diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 57279d0380..6899811ed5 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1037,3 +1037,56 @@ def convert_ffprobe_fps_to_float(value): if divisor == 0.0: return 0.0 return dividend / divisor + + +def convert_colorspace_for_input_paths( + input_paths, + output_dir, + source_color_space, + target_color_space, + logger=None +): + """Convert source files from one color space to another. + + Filenames of input files are kept so make sure that output directory + is not the same directory as input files have. + - This way it can handle gaps and can keep input filenames without handling + frame template + + Args: + input_paths (str): Paths that should be converted. It is expected that + contains single file or image sequence of samy type. + output_dir (str): Path to directory where output will be rendered. + Must not be same as input's directory. + source_color_space (str): ocio valid color space of source files + target_color_space (str): ocio valid target color space + logger (logging.Logger): Logger used for logging. + + """ + if logger is None: + logger = logging.getLogger(__name__) + + input_arg = "-i" + oiio_cmd = [ + get_oiio_tools_path(), + + # Don't add any additional attributes + "--nosoftwareattrib", + "--colorconvert", source_color_space, target_color_space + ] + for input_path in input_paths: + # Prepare subprocess arguments + + oiio_cmd.extend([ + input_arg, input_path, + ]) + + # Add last argument - path to output + base_filename = os.path.basename(input_path) + output_path = os.path.join(output_dir, base_filename) + oiio_cmd.extend([ + "-o", output_path + ]) + + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=logger) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py new file mode 100644 index 0000000000..58508ab18f --- /dev/null +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -0,0 +1,124 @@ +import pyblish.api + +from openpype.pipeline import publish +from openpype.lib import ( + + is_oiio_supported, +) + +from openpype.lib.transcoding import ( + convert_colorspace_for_input_paths, + get_transcode_temp_directory, +) + +from openpype.lib.profiles_filtering import filter_profiles + + +class ExtractColorTranscode(publish.Extractor): + """ + Extractor to convert colors from one colorspace to different. + """ + + label = "Transcode color spaces" + order = pyblish.api.ExtractorOrder + 0.01 + + optional = True + + # Configurable by Settings + profiles = None + options = None + + def process(self, instance): + if not self.profiles: + self.log.warning("No profiles present for create burnin") + return + + if "representations" not in instance.data: + self.log.warning("No representations, skipping.") + return + + if not is_oiio_supported(): + self.log.warning("OIIO not supported, no transcoding possible.") + return + + colorspace_data = instance.data.get("colorspaceData") + if not colorspace_data: + # TODO get_colorspace ?? + self.log.warning("Instance has not colorspace data, skipping") + return + source_color_space = colorspace_data["colorspace"] + + host_name = instance.context.data["hostName"] + family = instance.data["family"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + subset = instance.data["subset"] + + filtering_criteria = { + "hosts": host_name, + "families": family, + "task_names": task_name, + "task_types": task_type, + "subset": subset + } + profile = filter_profiles(self.profiles, filtering_criteria, + logger=self.log) + + if not profile: + self.log.info(( + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) + return + + self.log.debug("profile: {}".format(profile)) + + target_colorspace = profile["output_colorspace"] + if not target_colorspace: + raise RuntimeError("Target colorspace must be set") + + repres = instance.data.get("representations") or [] + for idx, repre in enumerate(repres): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self.repre_is_valid(repre): + continue + + new_staging_dir = get_transcode_temp_directory() + repre["stagingDir"] = new_staging_dir + files_to_remove = repre["files"] + if not isinstance(files_to_remove, list): + files_to_remove = [files_to_remove] + instance.context.data["cleanupFullPaths"].extend(files_to_remove) + + convert_colorspace_for_input_paths( + repre["files"], + new_staging_dir, + source_color_space, + target_colorspace, + self.log + ) + + def repre_is_valid(self, repre): + """Validation if representation should be processed. + + Args: + repre (dict): Representation which should be checked. + + Returns: + bool: False if can't be processed else True. + """ + + if "review" not in (repre.get("tags") or []): + self.log.info(( + "Representation \"{}\" don't have \"review\" tag. Skipped." + ).format(repre["name"])) + return False + + if not repre.get("files"): + self.log.warning(( + "Representation \"{}\" have empty files. Skipped." + ).format(repre["name"])) + return False + return True From e36cf8004706a7d70133d79ea5e0ddd1f208f4c3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 12:05:57 +0100 Subject: [PATCH 017/483] OP-4643 - extractor must run just before ExtractReview Nuke render local is set to 0.01 --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 58508ab18f..5163cd4045 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -20,7 +20,7 @@ class ExtractColorTranscode(publish.Extractor): """ label = "Transcode color spaces" - order = pyblish.api.ExtractorOrder + 0.01 + order = pyblish.api.ExtractorOrder + 0.019 optional = True From cb27e5a4d6b512dfa193b031ab266340975245f0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:03:22 +0100 Subject: [PATCH 018/483] OP-4643 - fix for full file paths --- .../publish/extract_color_transcode.py | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 5163cd4045..6ad7599f2c 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,3 +1,4 @@ +import os import pyblish.api from openpype.pipeline import publish @@ -41,13 +42,6 @@ class ExtractColorTranscode(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return - colorspace_data = instance.data.get("colorspaceData") - if not colorspace_data: - # TODO get_colorspace ?? - self.log.warning("Instance has not colorspace data, skipping") - return - source_color_space = colorspace_data["colorspace"] - host_name = instance.context.data["hostName"] family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) @@ -82,18 +76,32 @@ class ExtractColorTranscode(publish.Extractor): repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - if not self.repre_is_valid(repre): + # if not self.repre_is_valid(repre): + # continue + + colorspace_data = repre.get("colorspaceData") + if not colorspace_data: + # TODO get_colorspace ?? + self.log.warning("Repre has not colorspace data, skipping") + continue + source_color_space = colorspace_data["colorspace"] + config_path = colorspace_data.get("configData", {}).get("path") + if not os.path.exists(config_path): + self.log.warning("Config file doesn't exist, skipping") continue new_staging_dir = get_transcode_temp_directory() + original_staging_dir = repre["stagingDir"] repre["stagingDir"] = new_staging_dir - files_to_remove = repre["files"] - if not isinstance(files_to_remove, list): - files_to_remove = [files_to_remove] - instance.context.data["cleanupFullPaths"].extend(files_to_remove) + files_to_convert = repre["files"] + if not isinstance(files_to_convert, list): + files_to_convert = [files_to_convert] + files_to_convert = [os.path.join(original_staging_dir, path) + for path in files_to_convert] + instance.context.data["cleanupFullPaths"].extend(files_to_convert) convert_colorspace_for_input_paths( - repre["files"], + files_to_convert, new_staging_dir, source_color_space, target_colorspace, From 7201c57ddc71cac47f6ea38fdbf7d6c1d2d03577 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:04:06 +0100 Subject: [PATCH 019/483] OP-4643 - pass path for ocio config --- openpype/lib/transcoding.py | 3 +++ openpype/plugins/publish/extract_color_transcode.py | 1 + 2 files changed, 4 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 6899811ed5..792e8ddd1e 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1042,6 +1042,7 @@ def convert_ffprobe_fps_to_float(value): def convert_colorspace_for_input_paths( input_paths, output_dir, + config_path, source_color_space, target_color_space, logger=None @@ -1058,6 +1059,7 @@ def convert_colorspace_for_input_paths( contains single file or image sequence of samy type. output_dir (str): Path to directory where output will be rendered. Must not be same as input's directory. + config_path (str): path to OCIO config file source_color_space (str): ocio valid color space of source files target_color_space (str): ocio valid target color space logger (logging.Logger): Logger used for logging. @@ -1072,6 +1074,7 @@ def convert_colorspace_for_input_paths( # Don't add any additional attributes "--nosoftwareattrib", + "--colorconfig", config_path, "--colorconvert", source_color_space, target_color_space ] for input_path in input_paths: diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 6ad7599f2c..fdb13a47e8 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -103,6 +103,7 @@ class ExtractColorTranscode(publish.Extractor): convert_colorspace_for_input_paths( files_to_convert, new_staging_dir, + config_path, source_color_space, target_colorspace, self.log From dc796b71b4c50d2bb0240667cf9d0f19d85ad5dc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:15:33 +0100 Subject: [PATCH 020/483] OP-4643 - add custom_tags --- openpype/plugins/publish/extract_color_transcode.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index fdb13a47e8..ab932b2476 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -72,6 +72,7 @@ class ExtractColorTranscode(publish.Extractor): target_colorspace = profile["output_colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") + custom_tags = profile["custom_tags"] repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): @@ -109,6 +110,11 @@ class ExtractColorTranscode(publish.Extractor): self.log ) + if custom_tags: + if not repre.get("custom_tags"): + repre["custom_tags"] = [] + repre["custom_tags"].extend(custom_tags) + def repre_is_valid(self, repre): """Validation if representation should be processed. From 50ff228070729e87dc0a846aa82461fdcc8b08b7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:18:38 +0100 Subject: [PATCH 021/483] OP-4643 - added docstring --- openpype/plugins/publish/extract_color_transcode.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index ab932b2476..88e2eed90f 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -18,6 +18,17 @@ from openpype.lib.profiles_filtering import filter_profiles class ExtractColorTranscode(publish.Extractor): """ Extractor to convert colors from one colorspace to different. + + Expects "colorspaceData" on representation. This dictionary is collected + previously and denotes that representation files should be converted. + This dict contains source colorspace information, collected by hosts. + + Target colorspace is selected by profiles in the Settings, based on: + - families + - host + - task types + - task names + - subset names """ label = "Transcode color spaces" From 7a162f9dba79e1c829b9a01338917033607ec074 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:15:44 +0100 Subject: [PATCH 022/483] OP-4643 - updated Settings schema --- .../schemas/schema_global_publish.json | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 46ae6ba554..c2c911d7d6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -246,24 +246,44 @@ "type": "list", "object_type": "text" }, + { + "type": "boolean", + "key": "delete_original", + "label": "Delete Original Representation" + }, { "type": "splitter" }, { - "key": "ext", - "label": "Output extension", - "type": "text" - }, - { - "key": "output_colorspace", - "label": "Output colorspace", - "type": "text" - }, - { - "key": "custom_tags", - "label": "Custom Tags", - "type": "list", - "object_type": "text" + "key": "outputs", + "label": "Output Definitions", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "output_extension", + "label": "Output extension", + "type": "text" + }, + { + "key": "output_colorspace", + "label": "Output colorspace", + "type": "text" + }, + { + "type": "schema", + "name": "schema_representation_tags" + }, + { + "key": "custom_tags", + "label": "Custom Tags", + "type": "list", + "object_type": "text" + } + ] + } } ] } From 171af695c3ebfdb5a946af8897002e853f34af81 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:17:25 +0100 Subject: [PATCH 023/483] OP-4643 - skip video files Only frames currently supported. --- .../plugins/publish/extract_color_transcode.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 88e2eed90f..a0714c9a33 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -36,6 +36,9 @@ class ExtractColorTranscode(publish.Extractor): optional = True + # Supported extensions + supported_exts = ["exr", "jpg", "jpeg", "png", "dpx"] + # Configurable by Settings profiles = None options = None @@ -88,13 +91,7 @@ class ExtractColorTranscode(publish.Extractor): repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - # if not self.repre_is_valid(repre): - # continue - - colorspace_data = repre.get("colorspaceData") - if not colorspace_data: - # TODO get_colorspace ?? - self.log.warning("Repre has not colorspace data, skipping") + if not self._repre_is_valid(repre): continue source_color_space = colorspace_data["colorspace"] config_path = colorspace_data.get("configData", {}).get("path") @@ -136,9 +133,9 @@ class ExtractColorTranscode(publish.Extractor): bool: False if can't be processed else True. """ - if "review" not in (repre.get("tags") or []): - self.log.info(( - "Representation \"{}\" don't have \"review\" tag. Skipped." + if repre.get("ext") not in self.supported_exts: + self.log.warning(( + "Representation \"{}\" of unsupported extension. Skipped." ).format(repre["name"])) return False From c7b443519e220ca14dc32ee135c6dcc8085c7d88 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:19:08 +0100 Subject: [PATCH 024/483] OP-4643 - refactored profile, delete of original Implemented multiple outputs from single input representation --- .../publish/extract_color_transcode.py | 156 ++++++++++++------ 1 file changed, 109 insertions(+), 47 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index a0714c9a33..b0c851d5f4 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,4 +1,6 @@ import os +import copy + import pyblish.api from openpype.pipeline import publish @@ -56,13 +58,94 @@ class ExtractColorTranscode(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return + profile = self._get_profile(instance) + if not profile: + return + + repres = instance.data.get("representations") or [] + for idx, repre in enumerate(list(repres)): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self._repre_is_valid(repre): + continue + + colorspace_data = repre["colorspaceData"] + source_color_space = colorspace_data["colorspace"] + config_path = colorspace_data.get("configData", {}).get("path") + if not os.path.exists(config_path): + self.log.warning("Config file doesn't exist, skipping") + continue + + repre = self._handle_original_repre(repre, profile) + + for _, output_def in profile.get("outputs", {}).items(): + new_repre = copy.deepcopy(repre) + + new_staging_dir = get_transcode_temp_directory() + original_staging_dir = new_repre["stagingDir"] + new_repre["stagingDir"] = new_staging_dir + files_to_convert = new_repre["files"] + if not isinstance(files_to_convert, list): + files_to_convert = [files_to_convert] + + files_to_delete = copy.deepcopy(files_to_convert) + + output_extension = output_def["output_extension"] + files_to_convert = self._rename_output_files(files_to_convert, + output_extension) + + files_to_convert = [os.path.join(original_staging_dir, path) + for path in files_to_convert] + + target_colorspace = output_def["output_colorspace"] + if not target_colorspace: + raise RuntimeError("Target colorspace must be set") + + convert_colorspace_for_input_paths( + files_to_convert, + new_staging_dir, + config_path, + source_color_space, + target_colorspace, + self.log + ) + + instance.context.data["cleanupFullPaths"].extend( + files_to_delete) + + custom_tags = output_def.get("custom_tags") + if custom_tags: + if not new_repre.get("custom_tags"): + new_repre["custom_tags"] = [] + new_repre["custom_tags"].extend(custom_tags) + + # Add additional tags from output definition to representation + for tag in output_def["tags"]: + if tag not in new_repre["tags"]: + new_repre["tags"].append(tag) + + instance.data["representations"].append(new_repre) + + def _rename_output_files(self, files_to_convert, output_extension): + """Change extension of converted files.""" + if output_extension: + output_extension = output_extension.replace('.', '') + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + new_file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(new_file_name) + files_to_convert = renamed_files + return files_to_convert + + def _get_profile(self, instance): + """Returns profile if and how repre should be color transcoded.""" host_name = instance.context.data["hostName"] family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) task_name = task_data.get("name") task_type = task_data.get("type") subset = instance.data["subset"] - filtering_criteria = { "hosts": host_name, "families": family, @@ -75,55 +158,15 @@ class ExtractColorTranscode(publish.Extractor): if not profile: self.log.info(( - "Skipped instance. None of profiles in presets are for" - " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" - " | Task type \"{}\" | Subset \"{}\" " - ).format(host_name, family, task_name, task_type, subset)) - return + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) self.log.debug("profile: {}".format(profile)) + return profile - target_colorspace = profile["output_colorspace"] - if not target_colorspace: - raise RuntimeError("Target colorspace must be set") - custom_tags = profile["custom_tags"] - - repres = instance.data.get("representations") or [] - for idx, repre in enumerate(repres): - self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - if not self._repre_is_valid(repre): - continue - source_color_space = colorspace_data["colorspace"] - config_path = colorspace_data.get("configData", {}).get("path") - if not os.path.exists(config_path): - self.log.warning("Config file doesn't exist, skipping") - continue - - new_staging_dir = get_transcode_temp_directory() - original_staging_dir = repre["stagingDir"] - repre["stagingDir"] = new_staging_dir - files_to_convert = repre["files"] - if not isinstance(files_to_convert, list): - files_to_convert = [files_to_convert] - files_to_convert = [os.path.join(original_staging_dir, path) - for path in files_to_convert] - instance.context.data["cleanupFullPaths"].extend(files_to_convert) - - convert_colorspace_for_input_paths( - files_to_convert, - new_staging_dir, - config_path, - source_color_space, - target_colorspace, - self.log - ) - - if custom_tags: - if not repre.get("custom_tags"): - repre["custom_tags"] = [] - repre["custom_tags"].extend(custom_tags) - - def repre_is_valid(self, repre): + def _repre_is_valid(self, repre): """Validation if representation should be processed. Args: @@ -144,4 +187,23 @@ class ExtractColorTranscode(publish.Extractor): "Representation \"{}\" have empty files. Skipped." ).format(repre["name"])) return False + + if not repre.get("colorspaceData"): + self.log.warning("Repre has not colorspace data, skipping") + return False + return True + + def _handle_original_repre(self, repre, profile): + delete_original = profile["delete_original"] + + if delete_original: + if not repre.get("tags"): + repre["tags"] = [] + + if "review" in repre["tags"]: + repre["tags"].remove("review") + if "delete" not in repre["tags"]: + repre["tags"].append("delete") + + return repre From 69c04bb01dc7bc220318a3d6f63e4ed568bddff2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:23:01 +0100 Subject: [PATCH 025/483] OP-4643 - switched logging levels Do not use warning unnecessary. --- openpype/plugins/publish/extract_color_transcode.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index b0c851d5f4..4d38514b8b 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -47,11 +47,11 @@ class ExtractColorTranscode(publish.Extractor): def process(self, instance): if not self.profiles: - self.log.warning("No profiles present for create burnin") + self.log.debug("No profiles present for color transcode") return if "representations" not in instance.data: - self.log.warning("No representations, skipping.") + self.log.debug("No representations, skipping.") return if not is_oiio_supported(): @@ -177,19 +177,19 @@ class ExtractColorTranscode(publish.Extractor): """ if repre.get("ext") not in self.supported_exts: - self.log.warning(( + self.log.debug(( "Representation \"{}\" of unsupported extension. Skipped." ).format(repre["name"])) return False if not repre.get("files"): - self.log.warning(( + self.log.debug(( "Representation \"{}\" have empty files. Skipped." ).format(repre["name"])) return False if not repre.get("colorspaceData"): - self.log.warning("Repre has not colorspace data, skipping") + self.log.debug("Repre has no colorspace data. Skipped.") return False return True From 44e12b05b95db1f0b1a43ad506850b5578705942 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:46:14 +0100 Subject: [PATCH 026/483] OP-4643 - propagate new extension to representation --- .../publish/extract_color_transcode.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4d38514b8b..62cf8f0dee 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -90,8 +90,13 @@ class ExtractColorTranscode(publish.Extractor): files_to_delete = copy.deepcopy(files_to_convert) output_extension = output_def["output_extension"] - files_to_convert = self._rename_output_files(files_to_convert, - output_extension) + output_extension = output_extension.replace('.', '') + if output_extension: + new_repre["name"] = output_extension + new_repre["ext"] = output_extension + + files_to_convert = self._rename_output_files( + files_to_convert, output_extension) files_to_convert = [os.path.join(original_staging_dir, path) for path in files_to_convert] @@ -127,15 +132,13 @@ class ExtractColorTranscode(publish.Extractor): def _rename_output_files(self, files_to_convert, output_extension): """Change extension of converted files.""" - if output_extension: - output_extension = output_extension.replace('.', '') - renamed_files = [] - for file_name in files_to_convert: - file_name, _ = os.path.splitext(file_name) - new_file_name = '{}.{}'.format(file_name, - output_extension) - renamed_files.append(new_file_name) - files_to_convert = renamed_files + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + new_file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(new_file_name) + files_to_convert = renamed_files return files_to_convert def _get_profile(self, instance): From 85fc41bd7427a395fbf1454b12a6ee6fea34e594 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:46:35 +0100 Subject: [PATCH 027/483] OP-4643 - added label to Settings --- .../projects_schema/schemas/schema_global_publish.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index c2c911d7d6..7155510fef 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -201,10 +201,14 @@ "type": "dict", "collapsible": true, "key": "ExtractColorTranscode", - "label": "ExtractColorTranscode", + "label": "ExtractColorTranscode (ImageIO)", "checkbox_key": "enabled", "is_group": true, "children": [ + { + "type": "label", + "label": "Configure output format(s) and color spaces for matching representations. Empty 'Output extension' denotes keeping source extension." + }, { "type": "boolean", "key": "enabled", From 24abe69437801ea4f022d3a0816664e93b2072f4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jan 2023 18:22:08 +0100 Subject: [PATCH 028/483] OP-4643 - refactored according to review Function turned into single filepath input. --- openpype/lib/transcoding.py | 43 ++++++----- .../publish/extract_color_transcode.py | 72 ++++++++++--------- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 792e8ddd1e..8e3432e0e9 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1039,12 +1039,12 @@ def convert_ffprobe_fps_to_float(value): return dividend / divisor -def convert_colorspace_for_input_paths( - input_paths, - output_dir, +def convert_colorspace( + input_path, + out_filepath, config_path, - source_color_space, - target_color_space, + source_colorspace, + target_colorspace, logger=None ): """Convert source files from one color space to another. @@ -1055,13 +1055,13 @@ def convert_colorspace_for_input_paths( frame template Args: - input_paths (str): Paths that should be converted. It is expected that + input_path (str): Paths that should be converted. It is expected that contains single file or image sequence of samy type. - output_dir (str): Path to directory where output will be rendered. + out_filepath (str): Path to directory where output will be rendered. Must not be same as input's directory. config_path (str): path to OCIO config file - source_color_space (str): ocio valid color space of source files - target_color_space (str): ocio valid target color space + source_colorspace (str): ocio valid color space of source files + target_colorspace (str): ocio valid target color space logger (logging.Logger): Logger used for logging. """ @@ -1075,21 +1075,18 @@ def convert_colorspace_for_input_paths( # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "--colorconvert", source_color_space, target_color_space + "--colorconvert", source_colorspace, target_colorspace ] - for input_path in input_paths: - # Prepare subprocess arguments + # Prepare subprocess arguments - oiio_cmd.extend([ - input_arg, input_path, - ]) + oiio_cmd.extend([ + input_arg, input_path, + ]) - # Add last argument - path to output - base_filename = os.path.basename(input_path) - output_path = os.path.join(output_dir, base_filename) - oiio_cmd.extend([ - "-o", output_path - ]) + # Add last argument - path to output + oiio_cmd.extend([ + "-o", out_filepath + ]) - logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) - run_subprocess(oiio_cmd, logger=logger) + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=logger) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 62cf8f0dee..3a05426432 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -10,7 +10,7 @@ from openpype.lib import ( ) from openpype.lib.transcoding import ( - convert_colorspace_for_input_paths, + convert_colorspace, get_transcode_temp_directory, ) @@ -69,7 +69,7 @@ class ExtractColorTranscode(publish.Extractor): continue colorspace_data = repre["colorspaceData"] - source_color_space = colorspace_data["colorspace"] + source_colorspace = colorspace_data["colorspace"] config_path = colorspace_data.get("configData", {}).get("path") if not os.path.exists(config_path): self.log.warning("Config file doesn't exist, skipping") @@ -80,8 +80,8 @@ class ExtractColorTranscode(publish.Extractor): for _, output_def in profile.get("outputs", {}).items(): new_repre = copy.deepcopy(repre) - new_staging_dir = get_transcode_temp_directory() original_staging_dir = new_repre["stagingDir"] + new_staging_dir = get_transcode_temp_directory() new_repre["stagingDir"] = new_staging_dir files_to_convert = new_repre["files"] if not isinstance(files_to_convert, list): @@ -92,27 +92,28 @@ class ExtractColorTranscode(publish.Extractor): output_extension = output_def["output_extension"] output_extension = output_extension.replace('.', '') if output_extension: - new_repre["name"] = output_extension + if new_repre["name"] == new_repre["ext"]: + new_repre["name"] = output_extension new_repre["ext"] = output_extension - files_to_convert = self._rename_output_files( - files_to_convert, output_extension) - - files_to_convert = [os.path.join(original_staging_dir, path) - for path in files_to_convert] - target_colorspace = output_def["output_colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") - convert_colorspace_for_input_paths( - files_to_convert, - new_staging_dir, - config_path, - source_color_space, - target_colorspace, - self.log - ) + for file_name in files_to_convert: + input_filepath = os.path.join(original_staging_dir, + file_name) + output_path = self._get_output_file_path(input_filepath, + new_staging_dir, + output_extension) + convert_colorspace( + input_filepath, + output_path, + config_path, + source_colorspace, + target_colorspace, + self.log + ) instance.context.data["cleanupFullPaths"].extend( files_to_delete) @@ -130,16 +131,16 @@ class ExtractColorTranscode(publish.Extractor): instance.data["representations"].append(new_repre) - def _rename_output_files(self, files_to_convert, output_extension): - """Change extension of converted files.""" - renamed_files = [] - for file_name in files_to_convert: - file_name, _ = os.path.splitext(file_name) - new_file_name = '{}.{}'.format(file_name, - output_extension) - renamed_files.append(new_file_name) - files_to_convert = renamed_files - return files_to_convert + def _get_output_file_path(self, input_filepath, output_dir, + output_extension): + """Create output file name path.""" + file_name = os.path.basename(input_filepath) + file_name, input_extension = os.path.splitext(file_name) + if not output_extension: + output_extension = input_extension + new_file_name = '{}.{}'.format(file_name, + output_extension) + return os.path.join(output_dir, new_file_name) def _get_profile(self, instance): """Returns profile if and how repre should be color transcoded.""" @@ -161,10 +162,10 @@ class ExtractColorTranscode(publish.Extractor): if not profile: self.log.info(( - "Skipped instance. None of profiles in presets are for" - " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" - " | Task type \"{}\" | Subset \"{}\" " - ).format(host_name, family, task_name, task_type, subset)) + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) self.log.debug("profile: {}".format(profile)) return profile @@ -181,18 +182,19 @@ class ExtractColorTranscode(publish.Extractor): if repre.get("ext") not in self.supported_exts: self.log.debug(( - "Representation \"{}\" of unsupported extension. Skipped." + "Representation '{}' of unsupported extension. Skipped." ).format(repre["name"])) return False if not repre.get("files"): self.log.debug(( - "Representation \"{}\" have empty files. Skipped." + "Representation '{}' have empty files. Skipped." ).format(repre["name"])) return False if not repre.get("colorspaceData"): - self.log.debug("Repre has no colorspace data. Skipped.") + self.log.debug("Representation '{}' has no colorspace data. " + "Skipped.") return False return True From 53470bb0333cd8662e3bf4433fa78898bb29eb19 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 18 Jan 2023 09:56:29 +0000 Subject: [PATCH 029/483] Initial draft --- openpype/hosts/maya/api/lib_renderproducts.py | 73 ++++++++++++++----- .../maya/plugins/publish/collect_render.py | 4 +- openpype/hosts/maya/startup/userSetup.py | 8 +- .../plugins/publish/submit_publish_job.py | 33 ++++++++- 4 files changed, 92 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index c54e3ab3e0..b76b441588 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -127,6 +127,7 @@ class RenderProduct(object): """ productName = attr.ib() ext = attr.ib() # extension + colorspace = attr.ib() # colorspace aov = attr.ib(default=None) # source aov driver = attr.ib(default=None) # source driver multipart = attr.ib(default=False) # multichannel file @@ -344,7 +345,6 @@ class ARenderProducts: separator = file_prefix[matches[0].end(1):matches[1].start(1)] return separator - def _get_layer_data(self): # type: () -> LayerMetadata # ______________________________________________ @@ -553,6 +553,9 @@ class RenderProductsArnold(ARenderProducts): ] for ai_driver in ai_drivers: + colorspace = self._get_colorspace( + ai_driver + ".colorManagement" + ) # todo: check aiAOVDriver.prefix as it could have # a custom path prefix set for this driver @@ -590,12 +593,15 @@ class RenderProductsArnold(ARenderProducts): global_aov = self._get_attr(aov, "globalAov") if global_aov: for camera in cameras: - product = RenderProduct(productName=name, - ext=ext, - aov=aov_name, - driver=ai_driver, - multipart=multipart, - camera=camera) + product = RenderProduct( + productName=name, + ext=ext, + aov=aov_name, + driver=ai_driver, + multipart=multipart, + camera=camera, + colorspace=colorspace + ) products.append(product) all_light_groups = self._get_attr(aov, "lightGroups") @@ -603,13 +609,16 @@ class RenderProductsArnold(ARenderProducts): # All light groups is enabled. A single multipart # Render Product for camera in cameras: - product = RenderProduct(productName=name + "_lgroups", - ext=ext, - aov=aov_name, - driver=ai_driver, - # Always multichannel output - multipart=True, - camera=camera) + product = RenderProduct( + productName=name + "_lgroups", + ext=ext, + aov=aov_name, + driver=ai_driver, + # Always multichannel output + multipart=True, + camera=camera, + colorspace=colorspace + ) products.append(product) else: value = self._get_attr(aov, "lightGroupsList") @@ -625,12 +634,28 @@ class RenderProductsArnold(ARenderProducts): aov=aov_name, driver=ai_driver, ext=ext, - camera=camera + camera=camera, + colorspace=colorspace ) products.append(product) return products + def _get_colorspace(self, attribute): + """Resolve colorspace from Arnold settings.""" + + def _view_transform(): + preferences = lib.get_color_management_preferences() + return preferences["view_transform"] + + resolved_values = { + "Raw": lambda: "Raw", + "Use View Transform": _view_transform, + # Default. Same as Maya Preferences. + "Use Output Transform": lib.get_color_management_output_transform + } + return resolved_values[self._get_attr(attribute)]() + def get_render_products(self): """Get all AOVs. @@ -659,11 +684,19 @@ class RenderProductsArnold(ARenderProducts): ] default_ext = self._get_attr("defaultRenderGlobals.imfPluginKey") - beauty_products = [RenderProduct( - productName="beauty", - ext=default_ext, - driver="defaultArnoldDriver", - camera=camera) for camera in cameras] + colorspace = self._get_colorspace( + "defaultArnoldDriver.colorManagement" + ) + beauty_products = [ + RenderProduct( + productName="beauty", + ext=default_ext, + driver="defaultArnoldDriver", + camera=camera, + colorspace=colorspace + ) for camera in cameras + ] + # AOVs > Legacy > Maya Render View > Mode aovs_enabled = bool( self._get_attr("defaultArnoldRenderOptions.aovMode") diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index b1ad3ca58e..2c89424381 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -42,7 +42,6 @@ Provides: import re import os import platform -import json from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup @@ -318,6 +317,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "aovSeparator": layer_render_products.layer_data.aov_separator, # noqa: E501 "renderSetupIncludeLights": render_instance.data.get( "renderSetupIncludeLights" + ), + "colorspaceConfig": ( + lib.get_color_management_preferences()["config"] ) } diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index 40cd51f2d8..cb5aa4a898 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -4,10 +4,16 @@ from openpype.pipeline import install_host from openpype.hosts.maya.api import MayaHost from maya import cmds +# MAYA_RESOURCES enviornment variable is referenced in default OCIO path but +# it's not part of the environment. Patching this so it works as expected. +if "MAYA_RESOURCES" not in os.environ: + os.environ["MAYA_RESOURCES"] = os.path.join( + os.environ["MAYA_LOCATION"], "resources" + ).replace("\\", "/") + host = MayaHost() install_host(host) - print("starting OpenPype usersetup") # build a shelf diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7e39a644a2..8811fa5d34 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -427,7 +427,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info( "Finished copying %i files" % len(resource_files)) - def _create_instances_for_aov(self, instance_data, exp_files): + def _create_instances_for_aov( + self, instance_data, exp_files, additional_data + ): """Create instance for each AOV found. This will create new instance for every aov it can detect in expected @@ -528,6 +530,14 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): else: files = os.path.basename(col) + # Copy render product "colorspace" data to representation. + colorspace = "" + products = additional_data["renderProducts"].layer_data.products + for product in products: + if product.productName == aov: + colorspace = product.colorspace + break + rep = { "name": ext, "ext": ext, @@ -537,7 +547,14 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # If expectedFile are absolute, we need only filenames "stagingDir": staging, "fps": new_instance.get("fps"), - "tags": ["review"] if preview else [] + "tags": ["review"] if preview else [], + "colorspaceData": { + "colorspace": colorspace, + "configData": { + "path": additional_data["colorspaceConfig"], + "template": "" + } + } } # support conversion from tiled to scanline @@ -561,7 +578,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.debug("instances:{}".format(instances)) return instances - def _get_representations(self, instance, exp_files): + def _get_representations(self, instance, exp_files, additional_data): """Create representations for file sequences. This will return representations of expected files if they are not @@ -897,6 +914,11 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info(data.get("expectedFiles")) + additional_data = { + "renderProducts": instance.data["renderProducts"], + "colorspaceConfig": instance.data["colorspaceConfig"] + } + if isinstance(data.get("expectedFiles")[0], dict): # we cannot attach AOVs to other subsets as we consider every # AOV subset of its own. @@ -911,12 +933,15 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # there are multiple renderable cameras in scene) instances = self._create_instances_for_aov( instance_skeleton_data, - data.get("expectedFiles")) + data.get("expectedFiles"), + additional_data + ) self.log.info("got {} instance{}".format( len(instances), "s" if len(instances) > 1 else "")) else: + #Need to inject colorspace here. representations = self._get_representations( instance_skeleton_data, data.get("expectedFiles") From ab9b2d5970b458447e442d9d031bc3d8e76ddc7e Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 18 Jan 2023 15:13:12 +0000 Subject: [PATCH 030/483] Missing initial commit --- openpype/hosts/maya/api/lib.py | 45 ++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index dd5da275e8..95b4db1b42 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -5,6 +5,7 @@ import sys import platform import uuid import math +import re import json import logging @@ -3446,3 +3447,47 @@ def iter_visible_nodes_in_range(nodes, start, end): def get_attribute_input(attr): connections = cmds.listConnections(attr, plugs=True, destination=False) return connections[0] if connections else None + + +def get_color_management_preferences(): + """Get and resolve OCIO preferences.""" + data = { + # Is color management enabled. + "enabled": cmds.colorManagementPrefs( + query=True, cmEnabled=True + ), + "rendering_space": cmds.colorManagementPrefs( + query=True, renderingSpaceName=True + ), + "output_transform": cmds.colorManagementPrefs( + query=True, outputTransformName=True + ), + "output_transform_enabled": cmds.colorManagementPrefs( + query=True, outputTransformEnabled=True + ), + "view_transform": cmds.colorManagementPrefs( + query=True, viewTransformName=True + ) + } + + path = cmds.colorManagementPrefs( + query=True, configFilePath=True + ) + # Resolve environment variables in config path. "MAYA_RESOURCES" are in the + # path by default. + for group in re.search(r'(<.*>)', path).groups(): + path = path.replace( + group, os.environ[group[1:-1]] + ) + + data["config"] = path + + return data + + +def get_color_management_output_transform(): + preferences = get_color_management_preferences() + colorspace = preferences["rendering_space"] + if preferences["output_transform_enabled"]: + colorspace = preferences["output_transform"] + return colorspace From 3279634dacd18e6d38a29708596c822d276781c6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 18 Jan 2023 17:57:28 +0000 Subject: [PATCH 031/483] Fix Environment variable substitution in path --- openpype/hosts/maya/api/lib.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 95b4db1b42..b4aa18af65 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3473,12 +3473,14 @@ def get_color_management_preferences(): path = cmds.colorManagementPrefs( query=True, configFilePath=True ) + # Resolve environment variables in config path. "MAYA_RESOURCES" are in the # path by default. - for group in re.search(r'(<.*>)', path).groups(): - path = path.replace( - group, os.environ[group[1:-1]] - ) + def _subst_with_env_value(match): + key = match.group(1) + return os.environ.get(key, "") + + path = re.sub(r'<([^>]+)>', _subst_with_env_value, path) data["config"] = path From 7a7f87e861abf4aae7fb3a96f660d74fe4866329 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 19 Jan 2023 09:30:11 +0000 Subject: [PATCH 032/483] Improvement Cleaner way to resolve in path. --- openpype/hosts/maya/api/lib.py | 11 ++++------- openpype/hosts/maya/startup/userSetup.py | 6 ------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index b4aa18af65..0807db88dc 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -3474,13 +3474,10 @@ def get_color_management_preferences(): query=True, configFilePath=True ) - # Resolve environment variables in config path. "MAYA_RESOURCES" are in the - # path by default. - def _subst_with_env_value(match): - key = match.group(1) - return os.environ.get(key, "") - - path = re.sub(r'<([^>]+)>', _subst_with_env_value, path) + # The OCIO config supports a custom token. + maya_resources_token = "" + maya_resources_path = om.MGlobal.getAbsolutePathToResources() + path = path.replace(maya_resources_token, maya_resources_path) data["config"] = path diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index cb5aa4a898..1104421cd9 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -4,12 +4,6 @@ from openpype.pipeline import install_host from openpype.hosts.maya.api import MayaHost from maya import cmds -# MAYA_RESOURCES enviornment variable is referenced in default OCIO path but -# it's not part of the environment. Patching this so it works as expected. -if "MAYA_RESOURCES" not in os.environ: - os.environ["MAYA_RESOURCES"] = os.path.join( - os.environ["MAYA_LOCATION"], "resources" - ).replace("\\", "/") host = MayaHost() install_host(host) From c2d0bc8f39f69fc8f2cea36972242e946a6c9deb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 19 Jan 2023 09:34:55 +0000 Subject: [PATCH 033/483] HOund --- openpype/hosts/maya/api/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index 0807db88dc..23f7319d4a 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -5,7 +5,6 @@ import sys import platform import uuid import math -import re import json import logging From a9a86d112093d6b4dd47f6edc5f9ac75ace3b13d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:53:02 +0100 Subject: [PATCH 034/483] OP-4643 - updated schema Co-authored-by: Toke Jepsen --- .../projects_schema/schemas/schema_global_publish.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 7155510fef..80c18ce118 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -267,8 +267,8 @@ "type": "dict", "children": [ { - "key": "output_extension", - "label": "Output extension", + "key": "extension", + "label": "Extension", "type": "text" }, { From 0858c16ce0882949c570beeefe050f71219d28dc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:54:46 +0100 Subject: [PATCH 035/483] OP-4643 - updated plugin name in schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- .../projects_schema/schemas/schema_global_publish.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 80c18ce118..357cbfb287 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -200,8 +200,8 @@ { "type": "dict", "collapsible": true, - "key": "ExtractColorTranscode", - "label": "ExtractColorTranscode (ImageIO)", + "key": "ExtractOIIOTranscode", + "label": "Extract OIIO Transcode", "checkbox_key": "enabled", "is_group": true, "children": [ From 875cac007dd0a3630b87cf545f73d038c4de79c0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:55:57 +0100 Subject: [PATCH 036/483] OP-4643 - updated key in schema Co-authored-by: Toke Jepsen --- .../projects_schema/schemas/schema_global_publish.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 357cbfb287..0281b0ded6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -272,8 +272,8 @@ "type": "text" }, { - "key": "output_colorspace", - "label": "Output colorspace", + "key": "colorspace", + "label": "Colorspace", "type": "text" }, { From 18b728aaf53a90422614a97ddf7e528ff9a50955 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:57:03 +0100 Subject: [PATCH 037/483] OP-4643 - changed oiio_cmd creation Co-authored-by: Toke Jepsen --- openpype/lib/transcoding.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 8e3432e0e9..828861e21e 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1068,25 +1068,15 @@ def convert_colorspace( if logger is None: logger = logging.getLogger(__name__) - input_arg = "-i" oiio_cmd = [ get_oiio_tools_path(), - + input_path, # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "--colorconvert", source_colorspace, target_colorspace - ] - # Prepare subprocess arguments - - oiio_cmd.extend([ - input_arg, input_path, - ]) - - # Add last argument - path to output - oiio_cmd.extend([ + "--colorconvert", source_colorspace, target_colorspace, "-o", out_filepath - ]) + ] logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) From 669e38d2c37481d089816d9dc663573f4a6895c3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 13:44:45 +0100 Subject: [PATCH 038/483] OP-4643 - updated new keys into settings --- .../settings/defaults/project_settings/global.json | 2 +- .../projects_schema/schemas/schema_global_publish.json | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 167f7611ce..f448f1a79a 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -70,7 +70,7 @@ "output": [] } }, - "ExtractColorTranscode": { + "ExtractOIIOTranscode": { "enabled": true, "profiles": [] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 0281b0ded6..74b81b13af 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -276,6 +276,16 @@ "label": "Colorspace", "type": "text" }, + { + "key": "display", + "label": "Display", + "type": "text" + }, + { + "key": "view", + "label": "View", + "type": "text" + }, { "type": "schema", "name": "schema_representation_tags" From 104dd91bba17cb59f5254426cfc98b7b48dbe91f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 13:45:42 +0100 Subject: [PATCH 039/483] OP-4643 - renanmed plugin, added new keys into outputs --- openpype/plugins/publish/extract_color_transcode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 3a05426432..cc63b35988 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -17,7 +17,7 @@ from openpype.lib.transcoding import ( from openpype.lib.profiles_filtering import filter_profiles -class ExtractColorTranscode(publish.Extractor): +class ExtractOIIOTranscode(publish.Extractor): """ Extractor to convert colors from one colorspace to different. @@ -89,14 +89,14 @@ class ExtractColorTranscode(publish.Extractor): files_to_delete = copy.deepcopy(files_to_convert) - output_extension = output_def["output_extension"] + output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') if output_extension: if new_repre["name"] == new_repre["ext"]: new_repre["name"] = output_extension new_repre["ext"] = output_extension - target_colorspace = output_def["output_colorspace"] + target_colorspace = output_def["colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") From 4179a8e48c5603ac8b17fbd48783e372135c3c33 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:03:13 +0100 Subject: [PATCH 040/483] OP-4643 - fixed config path key --- openpype/plugins/publish/extract_color_transcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index cc63b35988..245faeb306 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -70,8 +70,8 @@ class ExtractOIIOTranscode(publish.Extractor): colorspace_data = repre["colorspaceData"] source_colorspace = colorspace_data["colorspace"] - config_path = colorspace_data.get("configData", {}).get("path") - if not os.path.exists(config_path): + config_path = colorspace_data.get("config", {}).get("path") + if not config_path or not os.path.exists(config_path): self.log.warning("Config file doesn't exist, skipping") continue From 381f65bc8c973725f8d57a345d70a12ebcf95786 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:03:42 +0100 Subject: [PATCH 041/483] OP-4643 - fixed renaming files --- openpype/plugins/publish/extract_color_transcode.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 245faeb306..c079dcf70e 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -96,6 +96,14 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["name"] = output_extension new_repre["ext"] = output_extension + renamed_files = [] + _, orig_ext = os.path.splitext(files_to_convert[0]) + for file_name in files_to_convert: + file_name = file_name.replace(orig_ext, + "."+output_extension) + renamed_files.append(file_name) + new_repre["files"] = renamed_files + target_colorspace = output_def["colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") From c1bb93d0fbf75f978b1a04195edc102e3b113f70 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:04:44 +0100 Subject: [PATCH 042/483] OP-4643 - updated to calculate sequence format --- .../publish/extract_color_transcode.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index c079dcf70e..09c86909cb 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,5 +1,6 @@ import os import copy +import clique import pyblish.api @@ -108,6 +109,8 @@ class ExtractOIIOTranscode(publish.Extractor): if not target_colorspace: raise RuntimeError("Target colorspace must be set") + files_to_convert = self._translate_to_sequence( + files_to_convert) for file_name in files_to_convert: input_filepath = os.path.join(original_staging_dir, file_name) @@ -139,6 +142,40 @@ class ExtractOIIOTranscode(publish.Extractor): instance.data["representations"].append(new_repre) + def _translate_to_sequence(self, files_to_convert): + """Returns original list of files or single sequence format filename. + + Uses clique to find frame sequence, in this case it merges all frames + into sequence format (%0X) and returns it. + If sequence not found, it returns original list + + Args: + files_to_convert (list): list of file names + Returns: + (list) of [file.%04.exr] or [fileA.exr, fileB.exr] + """ + pattern = [clique.PATTERNS["frames"]] + collections, remainder = clique.assemble( + files_to_convert, patterns=pattern, + assume_padded_when_ambiguous=True) + + if collections: + if len(collections) > 1: + raise ValueError( + "Too many collections {}".format(collections)) + + collection = collections[0] + padding = collection.padding + padding_str = "%0{}".format(padding) + frames = list(collection.indexes) + frame_str = "{}-{}#".format(frames[0], frames[-1]) + file_name = "{}{}{}".format(collection.head, frame_str, + collection.tail) + + files_to_convert = [file_name] + + return files_to_convert + def _get_output_file_path(self, input_filepath, output_dir, output_extension): """Create output file name path.""" From 5a386b58d6bc9125adf6148bbb854e0688a0adf0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:54:02 +0100 Subject: [PATCH 043/483] OP-4643 - implemented display and viewer color space --- openpype/lib/transcoding.py | 23 +++++++++++++++++-- .../publish/extract_color_transcode.py | 13 +++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 828861e21e..fab9eeaaad 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1045,6 +1045,8 @@ def convert_colorspace( config_path, source_colorspace, target_colorspace, + view, + display, logger=None ): """Convert source files from one color space to another. @@ -1062,8 +1064,11 @@ def convert_colorspace( config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space + view (str): name for viewer space (ocio valid) + display (str): name for display-referred reference space (ocio valid) logger (logging.Logger): Logger used for logging. - + Raises: + ValueError: if misconfigured """ if logger is None: logger = logging.getLogger(__name__) @@ -1074,9 +1079,23 @@ def convert_colorspace( # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "--colorconvert", source_colorspace, target_colorspace, "-o", out_filepath ] + if all([target_colorspace, view, display]): + raise ValueError("Colorspace and both screen and display" + " cannot be set together." + "Choose colorspace or screen and display") + if not target_colorspace and not all([view, display]): + raise ValueError("Both screen and display must be set.") + + if target_colorspace: + oiio_cmd.extend(["--colorconvert", + source_colorspace, + target_colorspace]) + if view and display: + oiio_cmd.extend(["--iscolorspace", source_colorspace]) + oiio_cmd.extend(["--ociodisplay", display, view]) + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 09c86909cb..cd8421c0cd 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -106,8 +106,15 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["files"] = renamed_files target_colorspace = output_def["colorspace"] - if not target_colorspace: - raise RuntimeError("Target colorspace must be set") + view = output_def["view"] or colorspace_data.get("view") + display = (output_def["display"] or + colorspace_data.get("display")) + # both could be already collected by DCC, + # but could be overwritten + if view: + new_repre["colorspaceData"]["view"] = view + if display: + new_repre["colorspaceData"]["view"] = display files_to_convert = self._translate_to_sequence( files_to_convert) @@ -123,6 +130,8 @@ class ExtractOIIOTranscode(publish.Extractor): config_path, source_colorspace, target_colorspace, + view, + display, self.log ) From 2245869ffd0723177c2e76111723d9aeb43446c4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 19:03:10 +0100 Subject: [PATCH 044/483] OP-4643 - fix wrong order of deletion of representation --- openpype/plugins/publish/extract_color_transcode.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index cd8421c0cd..9cca5cc969 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -69,6 +69,8 @@ class ExtractOIIOTranscode(publish.Extractor): if not self._repre_is_valid(repre): continue + added_representations = False + colorspace_data = repre["colorspaceData"] source_colorspace = colorspace_data["colorspace"] config_path = colorspace_data.get("config", {}).get("path") @@ -76,8 +78,6 @@ class ExtractOIIOTranscode(publish.Extractor): self.log.warning("Config file doesn't exist, skipping") continue - repre = self._handle_original_repre(repre, profile) - for _, output_def in profile.get("outputs", {}).items(): new_repre = copy.deepcopy(repre) @@ -150,6 +150,10 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["tags"].append(tag) instance.data["representations"].append(new_repre) + added_representations = True + + if added_representations: + self._mark_original_repre_for_deletion(repre, profile) def _translate_to_sequence(self, files_to_convert): """Returns original list of files or single sequence format filename. @@ -253,7 +257,8 @@ class ExtractOIIOTranscode(publish.Extractor): return True - def _handle_original_repre(self, repre, profile): + def _mark_original_repre_for_deletion(self, repre, profile): + """If new transcoded representation created, delete old.""" delete_original = profile["delete_original"] if delete_original: @@ -264,5 +269,3 @@ class ExtractOIIOTranscode(publish.Extractor): repre["tags"].remove("review") if "delete" not in repre["tags"]: repre["tags"].append("delete") - - return repre From ce784ac78207e1a16f2a14f7cdaaad5268fd6c26 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 11:26:27 +0100 Subject: [PATCH 045/483] OP-4643 - updated docstring, standardized arguments --- openpype/lib/transcoding.py | 19 +++++++---------- .../publish/extract_color_transcode.py | 21 +++++++++---------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index fab9eeaaad..752712166f 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1041,7 +1041,7 @@ def convert_ffprobe_fps_to_float(value): def convert_colorspace( input_path, - out_filepath, + output_path, config_path, source_colorspace, target_colorspace, @@ -1049,18 +1049,13 @@ def convert_colorspace( display, logger=None ): - """Convert source files from one color space to another. - - Filenames of input files are kept so make sure that output directory - is not the same directory as input files have. - - This way it can handle gaps and can keep input filenames without handling - frame template + """Convert source file from one color space to another. Args: - input_path (str): Paths that should be converted. It is expected that - contains single file or image sequence of samy type. - out_filepath (str): Path to directory where output will be rendered. - Must not be same as input's directory. + input_path (str): Path that should be converted. It is expected that + contains single file or image sequence of same type + (sequence in format 'file.FRAMESTART-FRAMEEND#.exr', see oiio docs) + output_path (str): Path to output filename. config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space @@ -1079,7 +1074,7 @@ def convert_colorspace( # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "-o", out_filepath + "-o", output_path ] if all([target_colorspace, view, display]): diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 9cca5cc969..c4cef15ea6 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -119,13 +119,13 @@ class ExtractOIIOTranscode(publish.Extractor): files_to_convert = self._translate_to_sequence( files_to_convert) for file_name in files_to_convert: - input_filepath = os.path.join(original_staging_dir, - file_name) - output_path = self._get_output_file_path(input_filepath, + input_path = os.path.join(original_staging_dir, + file_name) + output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) convert_colorspace( - input_filepath, + input_path, output_path, config_path, source_colorspace, @@ -156,16 +156,17 @@ class ExtractOIIOTranscode(publish.Extractor): self._mark_original_repre_for_deletion(repre, profile) def _translate_to_sequence(self, files_to_convert): - """Returns original list of files or single sequence format filename. + """Returns original list or list with filename formatted in single + sequence format. Uses clique to find frame sequence, in this case it merges all frames - into sequence format (%0X) and returns it. + into sequence format (FRAMESTART-FRAMEEND#) and returns it. If sequence not found, it returns original list Args: files_to_convert (list): list of file names Returns: - (list) of [file.%04.exr] or [fileA.exr, fileB.exr] + (list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr] """ pattern = [clique.PATTERNS["frames"]] collections, remainder = clique.assemble( @@ -178,8 +179,6 @@ class ExtractOIIOTranscode(publish.Extractor): "Too many collections {}".format(collections)) collection = collections[0] - padding = collection.padding - padding_str = "%0{}".format(padding) frames = list(collection.indexes) frame_str = "{}-{}#".format(frames[0], frames[-1]) file_name = "{}{}{}".format(collection.head, frame_str, @@ -189,10 +188,10 @@ class ExtractOIIOTranscode(publish.Extractor): return files_to_convert - def _get_output_file_path(self, input_filepath, output_dir, + def _get_output_file_path(self, input_path, output_dir, output_extension): """Create output file name path.""" - file_name = os.path.basename(input_filepath) + file_name = os.path.basename(input_path) file_name, input_extension = os.path.splitext(file_name) if not output_extension: output_extension = input_extension From 02ad1d1998235e5416f41b3a973577659cb0d971 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 11:27:06 +0100 Subject: [PATCH 046/483] OP-4643 - fix wrong assignment --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index c4cef15ea6..4e899a519c 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -114,7 +114,7 @@ class ExtractOIIOTranscode(publish.Extractor): if view: new_repre["colorspaceData"]["view"] = view if display: - new_repre["colorspaceData"]["view"] = display + new_repre["colorspaceData"]["display"] = display files_to_convert = self._translate_to_sequence( files_to_convert) From 7d4a17169377dc8f8f68b54ae5338c296fd362a7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 11:59:13 +0100 Subject: [PATCH 047/483] OP-4643 - fix files to delete --- .../publish/extract_color_transcode.py | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4e899a519c..99e684ba21 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -84,26 +84,18 @@ class ExtractOIIOTranscode(publish.Extractor): original_staging_dir = new_repre["stagingDir"] new_staging_dir = get_transcode_temp_directory() new_repre["stagingDir"] = new_staging_dir - files_to_convert = new_repre["files"] - if not isinstance(files_to_convert, list): - files_to_convert = [files_to_convert] - files_to_delete = copy.deepcopy(files_to_convert) + if isinstance(new_repre["files"], list): + files_to_convert = copy.deepcopy(new_repre["files"]) + else: + files_to_convert = [new_repre["files"]] output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') if output_extension: - if new_repre["name"] == new_repre["ext"]: - new_repre["name"] = output_extension - new_repre["ext"] = output_extension - - renamed_files = [] - _, orig_ext = os.path.splitext(files_to_convert[0]) - for file_name in files_to_convert: - file_name = file_name.replace(orig_ext, - "."+output_extension) - renamed_files.append(file_name) - new_repre["files"] = renamed_files + self._rename_in_representation(new_repre, + files_to_convert, + output_extension) target_colorspace = output_def["colorspace"] view = output_def["view"] or colorspace_data.get("view") @@ -135,8 +127,12 @@ class ExtractOIIOTranscode(publish.Extractor): self.log ) - instance.context.data["cleanupFullPaths"].extend( - files_to_delete) + # cleanup temporary transcoded files + for file_name in new_repre["files"]: + transcoded_file_path = os.path.join(new_staging_dir, + file_name) + instance.context.data["cleanupFullPaths"].append( + transcoded_file_path) custom_tags = output_def.get("custom_tags") if custom_tags: @@ -155,6 +151,21 @@ class ExtractOIIOTranscode(publish.Extractor): if added_representations: self._mark_original_repre_for_deletion(repre, profile) + def _rename_in_representation(self, new_repre, files_to_convert, + output_extension): + """Replace old extension with new one everywhere in representation.""" + if new_repre["name"] == new_repre["ext"]: + new_repre["name"] = output_extension + new_repre["ext"] = output_extension + + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(file_name) + new_repre["files"] = renamed_files + def _translate_to_sequence(self, files_to_convert): """Returns original list or list with filename formatted in single sequence format. From 302a79095ec50031a2c411dd2704e75e6e51d6cf Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 13:17:59 +0100 Subject: [PATCH 048/483] OP-4643 - moved output argument to the end --- openpype/lib/transcoding.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 752712166f..1629058beb 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1073,8 +1073,7 @@ def convert_colorspace( input_path, # Don't add any additional attributes "--nosoftwareattrib", - "--colorconfig", config_path, - "-o", output_path + "--colorconfig", config_path ] if all([target_colorspace, view, display]): @@ -1092,5 +1091,7 @@ def convert_colorspace( oiio_cmd.extend(["--iscolorspace", source_colorspace]) oiio_cmd.extend(["--ociodisplay", display, view]) + oiio_cmd.extend(["-o", output_path]) + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) From 1de178e98fd0a59533826ae132b86ec85b742ce8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 13:18:33 +0100 Subject: [PATCH 049/483] OP-4643 - fix no tags in repre --- openpype/plugins/publish/extract_color_transcode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 99e684ba21..3d897c6d9f 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -142,6 +142,8 @@ class ExtractOIIOTranscode(publish.Extractor): # Add additional tags from output definition to representation for tag in output_def["tags"]: + if not new_repre.get("tags"): + new_repre["tags"] = [] if tag not in new_repre["tags"]: new_repre["tags"].append(tag) From 8ce2c151ce920e22089712e81b0d782a66e8fa38 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 13:26:07 +0100 Subject: [PATCH 050/483] OP-4643 - changed docstring Elaborated more that 'target_colorspace' and ('view', 'display') are disjunctive. --- openpype/lib/transcoding.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 1629058beb..6d91f514ec 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1045,8 +1045,8 @@ def convert_colorspace( config_path, source_colorspace, target_colorspace, - view, - display, + view=None, + display=None, logger=None ): """Convert source file from one color space to another. @@ -1059,7 +1059,9 @@ def convert_colorspace( config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space + if filled, 'view' and 'display' must be empty view (str): name for viewer space (ocio valid) + both 'view' and 'display' must be filled (if 'target_colorspace') display (str): name for display-referred reference space (ocio valid) logger (logging.Logger): Logger used for logging. Raises: From 3fa7610061298e4a3de96d176b74f5bd3ecca652 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 31 Jan 2023 18:35:50 +0000 Subject: [PATCH 051/483] Working version for Arnold. --- openpype/hosts/maya/api/lib.py | 13 +++++++++++-- openpype/hosts/maya/api/lib_renderproducts.py | 6 +++++- .../hosts/maya/plugins/publish/collect_render.py | 8 ++++---- .../deadline/plugins/publish/submit_publish_job.py | 10 +++++++--- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index f869dadaad..b31ab2408b 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -14,7 +14,7 @@ from math import ceil from six import string_types from maya import cmds, mel -import maya.api.OpenMaya as om +from maya.api import OpenMaya from openpype.client import ( get_project, @@ -3402,13 +3402,22 @@ def get_color_management_preferences(): ) } + # Split view and display from view_transform. view_transform comes in + # format of "{view} ({display})". + display = data["view_transform"].split("(")[-1].replace(")", "") + data.update({ + "display": display, + "view": data["view_transform"].replace("({})".format(display), "")[:-1] + }) + + # Get config absolute path. path = cmds.colorManagementPrefs( query=True, configFilePath=True ) # The OCIO config supports a custom token. maya_resources_token = "" - maya_resources_path = om.MGlobal.getAbsolutePathToResources() + maya_resources_path = OpenMaya.MGlobal.getAbsolutePathToResources() path = path.replace(maya_resources_token, maya_resources_path) data["config"] = path diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 0b585dc8cb..58ccbfd5a2 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -648,8 +648,12 @@ class RenderProductsArnold(ARenderProducts): preferences = lib.get_color_management_preferences() return preferences["view_transform"] + def _raw(): + preferences = lib.get_color_management_preferences() + return preferences["rendering_space"] + resolved_values = { - "Raw": lambda: "Raw", + "Raw": _raw, "Use View Transform": _view_transform, # Default. Same as Maya Preferences. "Use Output Transform": lib.get_color_management_output_transform diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 2c89424381..d0164f30a8 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -264,7 +264,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): self.log.info(full_exp_files) self.log.info("collecting layer: {}".format(layer_name)) # Get layer specific settings, might be overrides - + colorspace_data = lib.get_color_management_preferences() data = { "subset": expected_layer_name, "attachTo": attach_to, @@ -318,9 +318,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "renderSetupIncludeLights": render_instance.data.get( "renderSetupIncludeLights" ), - "colorspaceConfig": ( - lib.get_color_management_preferences()["config"] - ) + "colorspaceConfig": colorspace_data["config"], + "colorspaceDisplay": colorspace_data["display"], + "colorspaceView": colorspace_data["view"] } # Collect Deadline url if Deadline module is enabled diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 8811fa5d34..02aa1043d1 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -550,10 +550,12 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "tags": ["review"] if preview else [], "colorspaceData": { "colorspace": colorspace, - "configData": { + "config": { "path": additional_data["colorspaceConfig"], "template": "" - } + }, + "display": additional_data["display"], + "view": additional_data["view"] } } @@ -916,7 +918,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): additional_data = { "renderProducts": instance.data["renderProducts"], - "colorspaceConfig": instance.data["colorspaceConfig"] + "colorspaceConfig": instance.data["colorspaceConfig"], + "display": instance.data["colorspaceDisplay"], + "view": instance.data["colorspaceView"] } if isinstance(data.get("expectedFiles")[0], dict): From 9f17a4803f4f6fdfb8858a841e20dbe283b119f2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 11:14:07 +0100 Subject: [PATCH 052/483] OP-4663 - fix double dots in extension Co-authored-by: Toke Jepsen --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 3d897c6d9f..bfed69c300 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -207,7 +207,7 @@ class ExtractOIIOTranscode(publish.Extractor): file_name = os.path.basename(input_path) file_name, input_extension = os.path.splitext(file_name) if not output_extension: - output_extension = input_extension + output_extension = input_extension.replace(".", "") new_file_name = '{}.{}'.format(file_name, output_extension) return os.path.join(output_dir, new_file_name) From e8d4a752a94e0f760bc2430536fdd8d87eda7636 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 11:22:20 +0100 Subject: [PATCH 053/483] Fix pyproject.toml version because of Poetry Automatization injects wrong format --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 634aeda5ac..2fc4f6fe39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.15.1-nightly.2" # OpenPype +version = "3.15.1" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From 5c4856d72a302db1132b21f8fac87e751e045899 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Feb 2023 12:05:38 +0000 Subject: [PATCH 054/483] BigRoy feedback --- openpype/hosts/maya/api/lib.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index b31ab2408b..d83f18bf30 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -5,6 +5,7 @@ import sys import platform import uuid import math +import re import json import logging @@ -3404,10 +3405,11 @@ def get_color_management_preferences(): # Split view and display from view_transform. view_transform comes in # format of "{view} ({display})". - display = data["view_transform"].split("(")[-1].replace(")", "") + regex = re.compile(r"^(?P.+) \((?P.+)\)$") + match = regex.match(data["view_transform"]) data.update({ - "display": display, - "view": data["view_transform"].replace("({})".format(display), "")[:-1] + "display": match.group("display"), + "view": match.group("view") }) # Get config absolute path. From 89d7edd8dfe0e6917c18870c92628659a8635541 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Feb 2023 12:06:26 +0000 Subject: [PATCH 055/483] Support for Maya Hardware --- openpype/hosts/maya/api/lib_renderproducts.py | 7 ++++++- openpype/hosts/maya/api/lib_rendersettings.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 58ccbfd5a2..6a607e7ff3 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1354,7 +1354,12 @@ class RenderProductsMayaHardware(ARenderProducts): products = [] for cam in self.get_renderable_cameras(): - product = RenderProduct(productName="beauty", ext=ext, camera=cam) + product = RenderProduct( + productName="beauty", + ext=ext, + camera=cam, + colorspace=lib.get_color_management_output_transform() + ) products.append(product) return products diff --git a/openpype/hosts/maya/api/lib_rendersettings.py b/openpype/hosts/maya/api/lib_rendersettings.py index 6190a49401..4710b10128 100644 --- a/openpype/hosts/maya/api/lib_rendersettings.py +++ b/openpype/hosts/maya/api/lib_rendersettings.py @@ -23,7 +23,8 @@ class RenderSettings(object): 'vray': 'vraySettings.fileNamePrefix', 'arnold': 'defaultRenderGlobals.imageFilePrefix', 'renderman': 'rmanGlobals.imageFileFormat', - 'redshift': 'defaultRenderGlobals.imageFilePrefix' + 'redshift': 'defaultRenderGlobals.imageFilePrefix', + 'mayahardware2': 'defaultRenderGlobals.imageFilePrefix' } _image_prefixes = { From 21d275d46ae2e7d8c1cc9fae67cd8512da46dec1 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 1 Feb 2023 12:27:48 +0000 Subject: [PATCH 056/483] Support for Vray --- openpype/hosts/maya/api/lib_renderproducts.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 6a607e7ff3..38415c2ae2 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -841,9 +841,13 @@ class RenderProductsVray(ARenderProducts): if not dont_save_rgb: for camera in cameras: products.append( - RenderProduct(productName="", - ext=default_ext, - camera=camera)) + RenderProduct( + productName="", + ext=default_ext, + camera=camera, + colorspace=lib.get_color_management_output_transform() + ) + ) # separate alpha file separate_alpha = self._get_attr("vraySettings.separateAlpha") @@ -895,10 +899,13 @@ class RenderProductsVray(ARenderProducts): aov_name = self._get_vray_aov_name(aov) for camera in cameras: - product = RenderProduct(productName=aov_name, - ext=default_ext, - aov=aov, - camera=camera) + product = RenderProduct( + productName=aov_name, + ext=default_ext, + aov=aov, + camera=camera, + colorspace=lib.get_color_management_output_transform() + ) products.append(product) return products From e7fbe105fdd312ecdf3560e1db5859dbbda39076 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 16:11:45 +0100 Subject: [PATCH 057/483] OP-4643 - update documentation in Settings schema --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 74b81b13af..3956f403f4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -207,7 +207,7 @@ "children": [ { "type": "label", - "label": "Configure output format(s) and color spaces for matching representations. Empty 'Output extension' denotes keeping source extension." + "label": "Configure Output Definition(s) for new representation(s). \nEmpty 'Extension' denotes keeping source extension. \nName(key) of output definition will be used as new representation name \nunless 'passthrough' value is used to keep existing name. \nFill either 'Colorspace' (for target colorspace) or \nboth 'Display' and 'View' (for display and viewer colorspaces)." }, { "type": "boolean", From ed95995fc7b688d7ea5a66e6a818364a1aadc551 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 16:13:59 +0100 Subject: [PATCH 058/483] OP-4643 - name of new representation from output definition key --- .../publish/extract_color_transcode.py | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index bfed69c300..e39ea3add9 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -32,6 +32,25 @@ class ExtractOIIOTranscode(publish.Extractor): - task types - task names - subset names + + Can produce one or more representations (with different extensions) based + on output definition in format: + "output_name: { + "extension": "png", + "colorspace": "ACES - ACEScg", + "display": "", + "view": "", + "tags": [], + "custom_tags": [] + } + + If 'extension' is empty original representation extension is used. + 'output_name' will be used as name of new representation. In case of value + 'passthrough' name of original representation will be used. + + 'colorspace' denotes target colorspace to be transcoded into. Could be + empty if transcoding should be only into display and viewer colorspace. + (In that case both 'display' and 'view' must be filled.) """ label = "Transcode color spaces" @@ -78,7 +97,7 @@ class ExtractOIIOTranscode(publish.Extractor): self.log.warning("Config file doesn't exist, skipping") continue - for _, output_def in profile.get("outputs", {}).items(): + for output_name, output_def in profile.get("outputs", {}).items(): new_repre = copy.deepcopy(repre) original_staging_dir = new_repre["stagingDir"] @@ -92,10 +111,10 @@ class ExtractOIIOTranscode(publish.Extractor): output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') - if output_extension: - self._rename_in_representation(new_repre, - files_to_convert, - output_extension) + self._rename_in_representation(new_repre, + files_to_convert, + output_name, + output_extension) target_colorspace = output_def["colorspace"] view = output_def["view"] or colorspace_data.get("view") @@ -154,10 +173,22 @@ class ExtractOIIOTranscode(publish.Extractor): self._mark_original_repre_for_deletion(repre, profile) def _rename_in_representation(self, new_repre, files_to_convert, - output_extension): - """Replace old extension with new one everywhere in representation.""" - if new_repre["name"] == new_repre["ext"]: - new_repre["name"] = output_extension + output_name, output_extension): + """Replace old extension with new one everywhere in representation. + + Args: + new_repre (dict) + files_to_convert (list): of filenames from repre["files"], + standardized to always list + output_name (str): key of output definition from Settings, + if "" token used, keep original repre name + output_extension (str): extension from output definition + """ + if output_name != "passthrough": + new_repre["name"] = output_name + if not output_extension: + return + new_repre["ext"] = output_extension renamed_files = [] From a20646e82f5c39108c1ac5b0b9988226c49c1a56 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 16:42:48 +0100 Subject: [PATCH 059/483] OP-4643 - updated docstring for convert_colorspace --- openpype/lib/transcoding.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 6d91f514ec..18273dd432 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1054,8 +1054,11 @@ def convert_colorspace( Args: input_path (str): Path that should be converted. It is expected that contains single file or image sequence of same type - (sequence in format 'file.FRAMESTART-FRAMEEND#.exr', see oiio docs) + (sequence in format 'file.FRAMESTART-FRAMEEND#.ext', see oiio docs, + eg `big.1-3#.tif`) output_path (str): Path to output filename. + (must follow format of 'input_path', eg. single file or + sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`) config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space From e24665cf536aa4d48538f229718de57a731e75f1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Feb 2023 18:22:10 +0100 Subject: [PATCH 060/483] OP-4643 - remove review from old representation If new representation gets created and adds 'review' tag it becomes new reviewable representation. --- .../publish/extract_color_transcode.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index e39ea3add9..d10b887a0b 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -89,6 +89,7 @@ class ExtractOIIOTranscode(publish.Extractor): continue added_representations = False + added_review = False colorspace_data = repre["colorspaceData"] source_colorspace = colorspace_data["colorspace"] @@ -166,11 +167,15 @@ class ExtractOIIOTranscode(publish.Extractor): if tag not in new_repre["tags"]: new_repre["tags"].append(tag) + if tag == "review": + added_review = True + instance.data["representations"].append(new_repre) added_representations = True if added_representations: - self._mark_original_repre_for_deletion(repre, profile) + self._mark_original_repre_for_deletion(repre, profile, + added_review) def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): @@ -300,15 +305,16 @@ class ExtractOIIOTranscode(publish.Extractor): return True - def _mark_original_repre_for_deletion(self, repre, profile): + def _mark_original_repre_for_deletion(self, repre, profile, added_review): """If new transcoded representation created, delete old.""" + if not repre.get("tags"): + repre["tags"] = [] + delete_original = profile["delete_original"] if delete_original: - if not repre.get("tags"): - repre["tags"] = [] - - if "review" in repre["tags"]: - repre["tags"].remove("review") if "delete" not in repre["tags"]: repre["tags"].append("delete") + + if added_review and "review" in repre["tags"]: + repre["tags"].remove("review") From 3aa74b231c2e7116ea792901ac53cfcd848513fc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Feb 2023 18:23:42 +0100 Subject: [PATCH 061/483] OP-4643 - remove representation that should be deleted Or old revieable representation would be reviewed too. --- openpype/plugins/publish/extract_color_transcode.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index d10b887a0b..93ee1ec44d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -177,6 +177,11 @@ class ExtractOIIOTranscode(publish.Extractor): self._mark_original_repre_for_deletion(repre, profile, added_review) + for repre in tuple(instance.data["representations"]): + tags = repre.get("tags") or [] + if "delete" in tags and "thumbnail" not in tags: + instance.data["representations"].remove(repre) + def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): """Replace old extension with new one everywhere in representation. From 5e0c4a3ab1432e120b8f0c324f899070f1a5f831 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Feb 2023 12:07:00 +0100 Subject: [PATCH 062/483] Fix - added missed scopes for Slack bot --- openpype/modules/slack/manifest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml index 7a65cc5915..233c39fbaf 100644 --- a/openpype/modules/slack/manifest.yml +++ b/openpype/modules/slack/manifest.yml @@ -19,6 +19,8 @@ oauth_config: - chat:write.public - files:write - channels:read + - users:read + - usergroups:read settings: org_deploy_enabled: false socket_mode_enabled: false From 95aff1808fdb27d77b647f5b373c80e27eee56a1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Feb 2023 22:03:05 +0800 Subject: [PATCH 063/483] setting up deadline for 3dsmax --- openpype/hosts/max/api/lib.py | 33 +++++ openpype/hosts/max/api/lib_renderproducts.py | 102 +++++++++++++ openpype/hosts/max/api/lib_rendersettings.py | 125 ++++++++++++++++ .../hosts/max/plugins/create/create_render.py | 33 +++++ .../max/plugins/publish/collect_render.py | 72 +++++++++ .../maya/plugins/publish/collect_render.py | 2 - .../plugins/publish/submit_3dmax_deadline.py | 137 ++++++++++++++++++ .../defaults/project_settings/max.json | 7 + .../schemas/projects_schema/schema_main.json | 4 + .../projects_schema/schema_project_max.json | 52 +++++++ 10 files changed, 565 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/max/api/lib_renderproducts.py create mode 100644 openpype/hosts/max/api/lib_rendersettings.py create mode 100644 openpype/hosts/max/plugins/create/create_render.py create mode 100644 openpype/hosts/max/plugins/publish/collect_render.py create mode 100644 openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py create mode 100644 openpype/settings/defaults/project_settings/max.json create mode 100644 openpype/settings/entities/schemas/projects_schema/schema_project_max.json diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 9256ca9ac1..8c421b2f9b 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -120,3 +120,36 @@ def get_all_children(parent, node_type=None): return ([x for x in child_list if rt.superClassOf(x) == node_type] if node_type else child_list) + + +def get_current_renderer(): + """get current renderer""" + return rt.renderers.production + + +def get_default_render_folder(project_setting=None): + return (project_setting["max"] + ["RenderSettings"] + ["default_render_image_folder"] + ) + + +def set_framerange(startFrame, endFrame): + """Get/set the type of time range to be rendered. + + Possible values are: + + 1 -Single frame. + + 2 -Active time segment ( animationRange ). + + 3 -User specified Range. + + 4 -User specified Frame pickup string (for example "1,3,5-12"). + """ + # hard-code, there should be a custom setting for this + rt.rendTimeType = 4 + if startFrame is not None and endFrame is not None: + frameRange = "{0}-{1}".format(startFrame, endFrame) + rt.rendPickupFrames = frameRange + diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py new file mode 100644 index 0000000000..f3bb8bdad1 --- /dev/null +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -0,0 +1,102 @@ +# Render Element Example : For scanline render, VRay +# https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-E8F75D47-B998-4800-A3A5-610E22913CFC +# arnold +# https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html +import os +from pymxs import runtime as rt +from openpype.hosts.max.api.lib import ( + get_current_renderer, + get_default_render_folder +) +from openpype.pipeline.context_tools import get_current_project_asset +from openpype.settings import get_project_settings +from openpype.pipeline import legacy_io + + +class RenderProducts(object): + + @classmethod + def __init__(self, project_settings=None): + self._project_settings = project_settings + if not self._project_settings: + self._project_settings = get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) + + def render_product(self, container): + folder = rt.maxFilePath + folder = folder.replace("\\", "/") + setting = self._project_settings + render_folder = get_default_render_folder(setting) + + output_file = os.path.join(folder, render_folder, container) + context = get_current_project_asset() + startFrame = context["data"].get("frameStart") + endFrame = context["data"].get("frameEnd") + 1 + + img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] + full_render_list = self.beauty_render_product(output_file, + startFrame, + endFrame, + img_fmt) + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] + + if renderer == "VUE_File_Renderer": + return full_render_list + + if ( + renderer == "ART_Renderer" or + renderer == "Redshift Renderer" or + renderer == "V_Ray_6_Hotfix_3" or + renderer == "V_Ray_GPU_6_Hotfix_3" or + renderer == "Default_Scanline_Renderer" or + renderer == "Quicksilver_Hardware_Renderer" + ): + render_elem_list = self.render_elements_product(output_file, + startFrame, + endFrame, + img_fmt) + for render_elem in render_elem_list: + full_render_list.append(render_elem) + return full_render_list + + if renderer == "Arnold": + return full_render_list + + + def beauty_render_product(self, folder, startFrame, endFrame, fmt): + # get the beauty + beauty_frame_range = list() + + for f in range(startFrame, endFrame): + beauty = "{0}.{1}.{2}".format(folder, str(f), fmt) + beauty = beauty.replace("\\", "/") + beauty_frame_range.append(beauty) + + return beauty_frame_range + + # TODO: Get the arnold render product + def render_elements_product(self, folder, startFrame, endFrame, fmt): + """Get all the render element output files. """ + render_dirname = list() + + render_elem = rt.maxOps.GetCurRenderElementMgr() + render_elem_num = render_elem.NumRenderElements() + # get render elements from the renders + for i in range(render_elem_num): + renderlayer_name = render_elem.GetRenderElement(i) + target, renderpass = str(renderlayer_name).split(":") + + render_dir = os.path.join(folder, renderpass) + if renderlayer_name.enabled: + for f in range(startFrame, endFrame): + render_element = "{0}.{1}.{2}".format(render_dir, str(f), fmt) + render_element = render_element.replace("\\", "/") + render_dirname.append(render_element) + + return render_dirname + + def image_format(self): + img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] + return img_fmt diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py new file mode 100644 index 0000000000..8c8a82ae66 --- /dev/null +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -0,0 +1,125 @@ +import os +from pymxs import runtime as rt +from openpype.lib import Logger +from openpype.settings import get_project_settings +from openpype.pipeline import legacy_io +from openpype.pipeline.context_tools import get_current_project_asset + +from openpype.hosts.max.api.lib import ( + set_framerange, + get_current_renderer, + get_default_render_folder +) + + +class RenderSettings(object): + + log = Logger.get_logger("RenderSettings") + + _aov_chars = { + "dot": ".", + "dash": "-", + "underscore": "_" + } + + @classmethod + def __init__(self, project_settings=None): + self._project_settings = project_settings + if not self._project_settings: + self._project_settings = get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) + + def set_render_camera(self, selection): + for sel in selection: + # to avoid Attribute Error from pymxs wrapper + found = False + if rt.classOf(sel) in rt.Camera.classes: + found = True + rt.viewport.setCamera(sel) + break + if not found: + raise RuntimeError("Camera not found") + + + def set_renderoutput(self, container): + folder = rt.maxFilePath + # hard-coded, should be customized in the setting + folder = folder.replace("\\", "/") + # hard-coded, set the renderoutput path + setting = self._project_settings + render_folder = get_default_render_folder(setting) + output_dir = os.path.join(folder, render_folder) + if not os.path.exists(output_dir): + os.makedirs(output_dir) + # hard-coded, should be customized in the setting + context = get_current_project_asset() + + # get project reoslution + width = context["data"].get("resolutionWidth") + height = context["data"].get("resolutionHeight") + # Set Frame Range + startFrame = context["data"].get("frameStart") + endFrame = context["data"].get("frameEnd") + set_framerange(startFrame, endFrame) + # get the production render + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] + + img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] + output = os.path.join(output_dir, container) + try: + aov_separator = self._aov_chars[( + self._project_settings["maya"] + ["RenderSettings"] + ["aov_separator"] + )] + except KeyError: + aov_separator = "." + outputFilename = "{0}.{1}".format(output, img_fmt) + outputFilename = outputFilename.replace("{aov_separator}", aov_separator) + rt.rendOutputFilename = outputFilename + if renderer == "VUE_File_Renderer": + return + # TODO: Finish the arnold render setup + if renderer == "Arnold": + return + + if ( + renderer == "ART_Renderer" or + renderer == "Redshift Renderer" or + renderer == "V_Ray_6_Hotfix_3" or + renderer == "V_Ray_GPU_6_Hotfix_3" or + renderer == "Default_Scanline_Renderer" or + renderer == "Quicksilver_Hardware_Renderer" + ): + self.render_element_layer(output, width, height, img_fmt) + + rt.rendSaveFile= True + + + def render_element_layer(self, dir, width, height, ext): + """For Renderers with render elements""" + rt.renderWidth = width + rt.renderHeight = height + render_elem = rt.maxOps.GetCurRenderElementMgr() + render_elem_num = render_elem.NumRenderElements() + if render_elem_num < 0: + return + + for i in range(render_elem_num): + renderlayer_name = render_elem.GetRenderElement(i) + target, renderpass = str(renderlayer_name).split(":") + render_element = os.path.join(dir, renderpass) + aov_name = "{0}.{1}".format(render_element, ext) + try: + aov_separator = self._aov_chars[( + self._project_settings["maya"] + ["RenderSettings"] + ["aov_separator"] + )] + except KeyError: + aov_separator = "." + + aov_name = aov_name.replace("{aov_separator}", aov_separator) + render_elem.SetRenderElementFileName(i, aov_name) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py new file mode 100644 index 0000000000..76c10ca4a9 --- /dev/null +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +"""Creator plugin for creating camera.""" +from openpype.hosts.max.api import plugin +from openpype.pipeline import CreatedInstance +from openpype.hosts.max.api.lib_rendersettings import RenderSettings + + +class CreateRender(plugin.MaxCreator): + identifier = "io.openpype.creators.max.render" + label = "Render" + family = "maxrender" + icon = "gear" + + def create(self, subset_name, instance_data, pre_create_data): + from pymxs import runtime as rt + sel_obj = list(rt.selection) + instance = super(CreateRender, self).create( + subset_name, + instance_data, + pre_create_data) # type: CreatedInstance + container_name = instance.data.get("instance_node") + container = rt.getNodeByName(container_name) + # TODO: Disable "Add to Containers?" Panel + # parent the selected cameras into the container + for obj in sel_obj: + obj.parent = container + # for additional work on the node: + # instance_node = rt.getNodeByName(instance.get("instance_node")) + + # set viewport camera for rendering(mandatory for deadline) + RenderSettings().set_render_camera(sel_obj) + # set output paths for rendering(mandatory for deadline) + RenderSettings().set_renderoutput(container_name) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py new file mode 100644 index 0000000000..fc44c01206 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +"""Collect Render""" +import os +import pyblish.api + +from pymxs import runtime as rt +from openpype.pipeline import legacy_io +from openpype.hosts.max.api.lib import get_current_renderer +from openpype.hosts.max.api.lib_renderproducts import RenderProducts + + +class CollectRender(pyblish.api.InstancePlugin): + """Collect Render for Deadline""" + + order = pyblish.api.CollectorOrder + 0.01 + label = "Collect 3dmax Render Layers" + hosts = ['max'] + families = ["maxrender"] + + def process(self, instance): + context = instance.context + folder = rt.maxFilePath + file = rt.maxFileName + current_file = os.path.join(folder, file) + filepath = current_file.replace("\\", "/") + + context.data['currentFile'] = current_file + asset = legacy_io.Session["AVALON_ASSET"] + + render_layer_files = RenderProducts().render_product(instance.name) + folder = folder.replace("\\", "/") + + imgFormat = RenderProducts().image_format() + renderer_class = get_current_renderer() + renderer_name = str(renderer_class).split(":")[0] + # setup the plugin as 3dsmax for the internal renderer + if ( + renderer_name == "ART_Renderer" or + renderer_name == "Default_Scanline_Renderer" or + renderer_name == "Quicksilver_Hardware_Renderer" + ): + plugin = "3dsmax" + + if ( + renderer_name == "V_Ray_6_Hotfix_3" or + renderer_name == "V_Ray_GPU_6_Hotfix_3" + ): + plugin = "Vray" + + if renderer_name == "Redshift Renderer": + plugin = "redshift" + + if renderer_name == "Arnold": + plugin = "arnold" + + # https://forums.autodesk.com/t5/3ds-max-programming/pymxs-quickrender-animation-range/td-p/11216183 + + data = { + "subset": instance.name, + "asset": asset, + "publish": True, + "imageFormat": imgFormat, + "family": 'maxrender', + "families": ['maxrender'], + "source": filepath, + "files": render_layer_files, + "plugin": plugin, + "frameStart": context.data['frameStart'], + "frameEnd": context.data['frameEnd'] + } + self.log.info("data: {0}".format(data)) + instance.data.update(data) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index b1ad3ca58e..c5fce219fa 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -184,7 +184,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): self.log.info("multipart: {}".format( multipart)) assert exp_files, "no file names were generated, this is bug" - self.log.info(exp_files) # if we want to attach render to subset, check if we have AOV's # in expectedFiles. If so, raise error as we cannot attach AOV @@ -320,7 +319,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "renderSetupIncludeLights" ) } - # Collect Deadline url if Deadline module is enabled deadline_settings = ( context.data["system_settings"]["modules"]["deadline"] diff --git a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py new file mode 100644 index 0000000000..7e7173e4ce --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py @@ -0,0 +1,137 @@ +import os +import json +import getpass + +import requests +import pyblish.api + + +from openpype.pipeline import legacy_io + + +class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): + """ + 3DMax File Submit Render Deadline + + """ + + label = "Submit 3DsMax Render to Deadline" + order = pyblish.api.IntegratorOrder + hosts = ["max"] + families = ["maxrender"] + targets = ["local"] + + def process(self, instance): + context = instance.context + filepath = context.data["currentFile"] + filename = os.path.basename(filepath) + comment = context.data.get("comment", "") + deadline_user = context.data.get("deadlineUser", getpass.getuser()) + jobname ="{0} - {1}".format(filename, instance.name) + + # StartFrame to EndFrame + frames = "{start}-{end}".format( + start=int(instance.data["frameStart"]), + end=int(instance.data["frameEnd"]) + ) + + payload = { + "JobInfo": { + # Top-level group name + "BatchName": filename, + + # Job name, as seen in Monitor + "Name": jobname, + + # Arbitrary username, for visualisation in Monitor + "UserName": deadline_user, + + "Plugin": instance.data["plugin"], + "Pool": instance.data.get("primaryPool"), + "secondaryPool": instance.data.get("secondaryPool"), + "Frames": frames, + "ChunkSize" : instance.data.get("chunkSize", 10), + "Comment": comment + }, + "PluginInfo": { + # Input + "SceneFile": instance.data["source"], + "Version": "2023", + "SaveFile" : True, + # Mandatory for Deadline + # Houdini version without patch number + + "IgnoreInputs": True + }, + + # Mandatory for Deadline, may be empty + "AuxFiles": [] + } + # Include critical environment variables with submission + api.Session + keys = [ + # Submit along the current Avalon tool setup that we launched + # this application with so the Render Slave can build its own + # similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9" + "AVALON_TOOLS", + "OPENPYPE_VERSION" + ] + # Add mongo url if it's enabled + if 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) + + payload["JobInfo"].update({ + "EnvironmentKeyValue%d" % index: "{key}={value}".format( + key=key, + value=environment[key] + ) for index, key in enumerate(environment) + }) + + # Include OutputFilename entries + # The first entry also enables double-click to preview rendered + # frames from Deadline Monitor + output_data = {} + # need to be fixed + for i, filepath in enumerate(instance.data["files"]): + dirname = os.path.dirname(filepath) + fname = os.path.basename(filepath) + output_data["OutputDirectory%d" % i] = dirname.replace("\\", "/") + output_data["OutputFilename%d" % i] = fname + + if not os.path.exists(dirname): + self.log.info("Ensuring output directory exists: %s" % + dirname) + os.makedirs(dirname) + + payload["JobInfo"].update(output_data) + + self.submit(instance, payload) + + def submit(self, instance, payload): + + context = instance.context + deadline_url = context.data.get("defaultDeadline") + deadline_url = instance.data.get( + "deadlineUrl", deadline_url) + + assert deadline_url, "Requires Deadline Webservice URL" + + plugin = payload["JobInfo"]["Plugin"] + self.log.info("Using Render Plugin : {}".format(plugin)) + + self.log.info("Submitting..") + self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) + + # E.g. http://192.168.0.1:8082/api/jobs + url = "{}/api/jobs".format(deadline_url) + response = requests.post(url, json=payload, verify=False) + if not response.ok: + raise Exception(response.text) + # Store output dir for unified publisher (filesequence) + expected_files = instance.data["files"] + self.log.info("exp:{}".format(expected_files)) + output_dir = os.path.dirname(expected_files[0]) + instance.data["outputDir"] = output_dir + instance.data["deadlineSubmissionJob"] = response.json() diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json new file mode 100644 index 0000000000..651a074a08 --- /dev/null +++ b/openpype/settings/defaults/project_settings/max.json @@ -0,0 +1,7 @@ +{ + "RenderSettings": { + "default_render_image_folder": "renders/max", + "aov_separator": "underscore", + "image_format": "exr" + } +} \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_main.json b/openpype/settings/entities/schemas/projects_schema/schema_main.json index 0b9fbf7470..ebe59c7942 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_main.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_main.json @@ -82,6 +82,10 @@ "type": "schema", "name": "schema_project_slack" }, + { + "type": "schema", + "name": "schema_project_max" + }, { "type": "schema", "name": "schema_project_maya" diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json new file mode 100644 index 0000000000..3d4cd5c54a --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -0,0 +1,52 @@ +{ + "type": "dict", + "collapsible": true, + "key": "max", + "label": "Max", + "is_file": true, + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "RenderSettings", + "label": "Render Settings", + "children": [ + { + "type": "text", + "key": "default_render_image_folder", + "label": "Default render image folder" + }, + { + "key": "aov_separator", + "label": "AOV Separator character", + "type": "enum", + "multiselection": false, + "default": "underscore", + "enum_items": [ + {"dash": "- (dash)"}, + {"underscore": "_ (underscore)"}, + {"dot": ". (dot)"} + ] + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"avi": "avi"}, + {"bmp": "bmp"}, + {"exr": "exr"}, + {"tif": "tif"}, + {"tiff": "tiff"}, + {"jpg": "jpg"}, + {"png": "png"}, + {"tga": "tga"}, + {"dds": "dds"} + ] + } + ] + } + ] +} \ No newline at end of file From 1e5ec12070c36da7dc16c0eb2fbf0646424eb4ed Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Feb 2023 22:23:47 +0800 Subject: [PATCH 064/483] hound fix --- openpype/hosts/max/api/lib.py | 6 ++---- openpype/hosts/max/api/lib_renderproducts.py | 15 +++++++++------ openpype/hosts/max/api/lib_rendersettings.py | 18 +++++++++--------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 8c421b2f9b..0477b43182 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -130,8 +130,7 @@ def get_current_renderer(): def get_default_render_folder(project_setting=None): return (project_setting["max"] ["RenderSettings"] - ["default_render_image_folder"] - ) + ["default_render_image_folder"]) def set_framerange(startFrame, endFrame): @@ -147,9 +146,8 @@ def set_framerange(startFrame, endFrame): 4 -User specified Frame pickup string (for example "1,3,5-12"). """ - # hard-code, there should be a custom setting for this + # hard-code, there should be a custom setting for this rt.rendTimeType = 4 if startFrame is not None and endFrame is not None: frameRange = "{0}-{1}".format(startFrame, endFrame) rt.rendPickupFrames = frameRange - diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index f3bb8bdad1..3b7767478d 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -34,7 +34,7 @@ class RenderProducts(object): startFrame = context["data"].get("frameStart") endFrame = context["data"].get("frameEnd") + 1 - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] + img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa full_render_list = self.beauty_render_product(output_file, startFrame, endFrame, @@ -52,7 +52,7 @@ class RenderProducts(object): renderer == "V_Ray_GPU_6_Hotfix_3" or renderer == "Default_Scanline_Renderer" or renderer == "Quicksilver_Hardware_Renderer" - ): + ): render_elem_list = self.render_elements_product(output_file, startFrame, endFrame, @@ -64,13 +64,14 @@ class RenderProducts(object): if renderer == "Arnold": return full_render_list - def beauty_render_product(self, folder, startFrame, endFrame, fmt): # get the beauty beauty_frame_range = list() for f in range(startFrame, endFrame): - beauty = "{0}.{1}.{2}".format(folder, str(f), fmt) + beauty = "{0}.{1}.{2}".format(folder, + str(f), + fmt) beauty = beauty.replace("\\", "/") beauty_frame_range.append(beauty) @@ -83,7 +84,7 @@ class RenderProducts(object): render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() - # get render elements from the renders + # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") @@ -91,7 +92,9 @@ class RenderProducts(object): render_dir = os.path.join(folder, renderpass) if renderlayer_name.enabled: for f in range(startFrame, endFrame): - render_element = "{0}.{1}.{2}".format(render_dir, str(f), fmt) + render_element = "{0}.{1}.{2}".format(render_dir, + str(f), + fmt) render_element = render_element.replace("\\", "/") render_dirname.append(render_element) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 8c8a82ae66..11dd005ad7 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -41,7 +41,6 @@ class RenderSettings(object): if not found: raise RuntimeError("Camera not found") - def set_renderoutput(self, container): folder = rt.maxFilePath # hard-coded, should be customized in the setting @@ -66,7 +65,7 @@ class RenderSettings(object): renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] + img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa output = os.path.join(output_dir, container) try: aov_separator = self._aov_chars[( @@ -77,7 +76,8 @@ class RenderSettings(object): except KeyError: aov_separator = "." outputFilename = "{0}.{1}".format(output, img_fmt) - outputFilename = outputFilename.replace("{aov_separator}", aov_separator) + outputFilename = outputFilename.replace("{aov_separator}", + aov_separator) rt.rendOutputFilename = outputFilename if renderer == "VUE_File_Renderer": return @@ -92,11 +92,10 @@ class RenderSettings(object): renderer == "V_Ray_GPU_6_Hotfix_3" or renderer == "Default_Scanline_Renderer" or renderer == "Quicksilver_Hardware_Renderer" - ): + ): self.render_element_layer(output, width, height, img_fmt) - rt.rendSaveFile= True - + rt.rendSaveFile = True def render_element_layer(self, dir, width, height, ext): """For Renderers with render elements""" @@ -115,11 +114,12 @@ class RenderSettings(object): try: aov_separator = self._aov_chars[( self._project_settings["maya"] - ["RenderSettings"] - ["aov_separator"] + ["RenderSettings"] + ["aov_separator"] )] except KeyError: aov_separator = "." - aov_name = aov_name.replace("{aov_separator}", aov_separator) + aov_name = aov_name.replace("{aov_separator}", + aov_separator) render_elem.SetRenderElementFileName(i, aov_name) From 0ea62e664f5f0c54c5593147f69ae6740e07380b Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Feb 2023 22:29:28 +0800 Subject: [PATCH 065/483] hound fix --- openpype/hosts/max/api/lib_rendersettings.py | 6 +++--- openpype/hosts/max/plugins/publish/collect_render.py | 3 ++- .../deadline/plugins/publish/submit_3dmax_deadline.py | 8 ++++---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 11dd005ad7..90398e841c 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -35,9 +35,9 @@ class RenderSettings(object): # to avoid Attribute Error from pymxs wrapper found = False if rt.classOf(sel) in rt.Camera.classes: - found = True - rt.viewport.setCamera(sel) - break + found = True + rt.viewport.setCamera(sel) + break if not found: raise RuntimeError("Camera not found") diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index fc44c01206..cda774bf11 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -53,7 +53,8 @@ class CollectRender(pyblish.api.InstancePlugin): if renderer_name == "Arnold": plugin = "arnold" - # https://forums.autodesk.com/t5/3ds-max-programming/pymxs-quickrender-animation-range/td-p/11216183 + # https://forums.autodesk.com/t5/3ds-max-programming/ + # pymxs-quickrender-animation-range/td-p/11216183 data = { "subset": instance.name, diff --git a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py index 7e7173e4ce..faeb071524 100644 --- a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py @@ -27,7 +27,7 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): filename = os.path.basename(filepath) comment = context.data.get("comment", "") deadline_user = context.data.get("deadlineUser", getpass.getuser()) - jobname ="{0} - {1}".format(filename, instance.name) + jobname = "{0} - {1}".format(filename, instance.name) # StartFrame to EndFrame frames = "{start}-{end}".format( @@ -50,14 +50,14 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): "Pool": instance.data.get("primaryPool"), "secondaryPool": instance.data.get("secondaryPool"), "Frames": frames, - "ChunkSize" : instance.data.get("chunkSize", 10), + "ChunkSize": instance.data.get("chunkSize", 10), "Comment": comment }, "PluginInfo": { # Input "SceneFile": instance.data["source"], "Version": "2023", - "SaveFile" : True, + "SaveFile": True, # Mandatory for Deadline # Houdini version without patch number @@ -67,7 +67,7 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): # Mandatory for Deadline, may be empty "AuxFiles": [] } - # Include critical environment variables with submission + api.Session + # Include critical environment variables with submission + api.Session keys = [ # Submit along the current Avalon tool setup that we launched # this application with so the Render Slave can build its own From 6a55e2a9a3472214c077a66954ce50d1665b0bfa Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 8 Feb 2023 22:33:10 +0800 Subject: [PATCH 066/483] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 2 +- openpype/hosts/max/plugins/publish/collect_render.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 3b7767478d..a7361a5a25 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -101,5 +101,5 @@ class RenderProducts(object): return render_dirname def image_format(self): - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] + img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa return img_fmt diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index cda774bf11..dd85afd586 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -53,9 +53,6 @@ class CollectRender(pyblish.api.InstancePlugin): if renderer_name == "Arnold": plugin = "arnold" - # https://forums.autodesk.com/t5/3ds-max-programming/ - # pymxs-quickrender-animation-range/td-p/11216183 - data = { "subset": instance.name, "asset": asset, From abc4ecf59d938201478019fe1e9619a5600c9f70 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 16:22:22 +0800 Subject: [PATCH 067/483] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 3 +-- openpype/hosts/max/api/lib_rendersettings.py | 3 +-- openpype/hosts/max/plugins/publish/collect_render.py | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a7361a5a25..ddc5d8111f 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -51,8 +51,7 @@ class RenderProducts(object): renderer == "V_Ray_6_Hotfix_3" or renderer == "V_Ray_GPU_6_Hotfix_3" or renderer == "Default_Scanline_Renderer" or - renderer == "Quicksilver_Hardware_Renderer" - ): + renderer == "Quicksilver_Hardware_Renderer"): render_elem_list = self.render_elements_product(output_file, startFrame, endFrame, diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 90398e841c..aa523348dc 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -91,8 +91,7 @@ class RenderSettings(object): renderer == "V_Ray_6_Hotfix_3" or renderer == "V_Ray_GPU_6_Hotfix_3" or renderer == "Default_Scanline_Renderer" or - renderer == "Quicksilver_Hardware_Renderer" - ): + renderer == "Quicksilver_Hardware_Renderer"): self.render_element_layer(output, width, height, img_fmt) rt.rendSaveFile = True diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index dd85afd586..549c784e56 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -37,14 +37,12 @@ class CollectRender(pyblish.api.InstancePlugin): if ( renderer_name == "ART_Renderer" or renderer_name == "Default_Scanline_Renderer" or - renderer_name == "Quicksilver_Hardware_Renderer" - ): + renderer_name == "Quicksilver_Hardware_Renderer"): plugin = "3dsmax" if ( renderer_name == "V_Ray_6_Hotfix_3" or - renderer_name == "V_Ray_GPU_6_Hotfix_3" - ): + renderer_name == "V_Ray_GPU_6_Hotfix_3"): plugin = "Vray" if renderer_name == "Redshift Renderer": From 81b894ed13e5dc5d0743313f814ec9639f4d516a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 16:24:20 +0800 Subject: [PATCH 068/483] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 3 +-- openpype/hosts/max/api/lib_rendersettings.py | 3 +-- openpype/hosts/max/plugins/publish/collect_render.py | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index ddc5d8111f..9becd2b5e5 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -45,8 +45,7 @@ class RenderProducts(object): if renderer == "VUE_File_Renderer": return full_render_list - if ( - renderer == "ART_Renderer" or + if (renderer == "ART_Renderer" or renderer == "Redshift Renderer" or renderer == "V_Ray_6_Hotfix_3" or renderer == "V_Ray_GPU_6_Hotfix_3" or diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index aa523348dc..176c797405 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -85,8 +85,7 @@ class RenderSettings(object): if renderer == "Arnold": return - if ( - renderer == "ART_Renderer" or + if (renderer == "ART_Renderer" or renderer == "Redshift Renderer" or renderer == "V_Ray_6_Hotfix_3" or renderer == "V_Ray_GPU_6_Hotfix_3" or diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 549c784e56..cd991b36eb 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -34,14 +34,12 @@ class CollectRender(pyblish.api.InstancePlugin): renderer_class = get_current_renderer() renderer_name = str(renderer_class).split(":")[0] # setup the plugin as 3dsmax for the internal renderer - if ( - renderer_name == "ART_Renderer" or + if (renderer_name == "ART_Renderer" or renderer_name == "Default_Scanline_Renderer" or renderer_name == "Quicksilver_Hardware_Renderer"): plugin = "3dsmax" - if ( - renderer_name == "V_Ray_6_Hotfix_3" or + if (renderer_name == "V_Ray_6_Hotfix_3" or renderer_name == "V_Ray_GPU_6_Hotfix_3"): plugin = "Vray" From 8f016413d1075d027b863765e0a4595393b48ae8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 16:26:30 +0800 Subject: [PATCH 069/483] hound fix --- openpype/hosts/max/plugins/publish/collect_render.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index cd991b36eb..f5d99af63e 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -34,9 +34,9 @@ class CollectRender(pyblish.api.InstancePlugin): renderer_class = get_current_renderer() renderer_name = str(renderer_class).split(":")[0] # setup the plugin as 3dsmax for the internal renderer - if (renderer_name == "ART_Renderer" or - renderer_name == "Default_Scanline_Renderer" or - renderer_name == "Quicksilver_Hardware_Renderer"): + if (renderer_name == "ART_Renderer" + or renderer_name == "Default_Scanline_Renderer" + or renderer_name == "Quicksilver_Hardware_Renderer"): plugin = "3dsmax" if (renderer_name == "V_Ray_6_Hotfix_3" or From 98d05db60b1ec4fbfa4e870bf471e5f3b4844063 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 16:29:26 +0800 Subject: [PATCH 070/483] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 12 ++++++------ openpype/hosts/max/api/lib_rendersettings.py | 12 ++++++------ openpype/hosts/max/plugins/publish/collect_render.py | 4 ++-- .../plugins/publish/submit_3dmax_deadline.py | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 9becd2b5e5..fbd1f3d50e 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -45,12 +45,12 @@ class RenderProducts(object): if renderer == "VUE_File_Renderer": return full_render_list - if (renderer == "ART_Renderer" or - renderer == "Redshift Renderer" or - renderer == "V_Ray_6_Hotfix_3" or - renderer == "V_Ray_GPU_6_Hotfix_3" or - renderer == "Default_Scanline_Renderer" or - renderer == "Quicksilver_Hardware_Renderer"): + if (renderer == "ART_Renderer" + or renderer == "Redshift Renderer" + or renderer == "V_Ray_6_Hotfix_3" + or renderer == "V_Ray_GPU_6_Hotfix_3" + or renderer == "Default_Scanline_Renderer" + or renderer == "Quicksilver_Hardware_Renderer"): render_elem_list = self.render_elements_product(output_file, startFrame, endFrame, diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 176c797405..c1e376746a 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -85,12 +85,12 @@ class RenderSettings(object): if renderer == "Arnold": return - if (renderer == "ART_Renderer" or - renderer == "Redshift Renderer" or - renderer == "V_Ray_6_Hotfix_3" or - renderer == "V_Ray_GPU_6_Hotfix_3" or - renderer == "Default_Scanline_Renderer" or - renderer == "Quicksilver_Hardware_Renderer"): + if (renderer == "ART_Renderer" + or renderer == "Redshift Renderer" + or renderer == "V_Ray_6_Hotfix_3" + or renderer == "V_Ray_GPU_6_Hotfix_3" + or renderer == "Default_Scanline_Renderer" + or renderer == "Quicksilver_Hardware_Renderer"): self.render_element_layer(output, width, height, img_fmt) rt.rendSaveFile = True diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index f5d99af63e..c4b8ac4985 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -39,8 +39,8 @@ class CollectRender(pyblish.api.InstancePlugin): or renderer_name == "Quicksilver_Hardware_Renderer"): plugin = "3dsmax" - if (renderer_name == "V_Ray_6_Hotfix_3" or - renderer_name == "V_Ray_GPU_6_Hotfix_3"): + if (renderer_name == "V_Ray_6_Hotfix_3" + or renderer_name == "V_Ray_GPU_6_Hotfix_3"): plugin = "Vray" if renderer_name == "Redshift Renderer": diff --git a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py index faeb071524..88834e4a91 100644 --- a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py @@ -126,7 +126,7 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): # E.g. http://192.168.0.1:8082/api/jobs url = "{}/api/jobs".format(deadline_url) - response = requests.post(url, json=payload, verify=False) + response = requests.post(url, json=payload) if not response.ok: raise Exception(response.text) # Store output dir for unified publisher (filesequence) From fe7b6fbd315fcc7b13803ee7944ce37f46c9051c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 16:43:53 +0800 Subject: [PATCH 071/483] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 14 ++++++++------ openpype/hosts/max/api/lib_rendersettings.py | 14 ++++++++------ .../hosts/max/plugins/publish/collect_render.py | 14 +++++++++----- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index fbd1f3d50e..4ba92f06bd 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -45,12 +45,14 @@ class RenderProducts(object): if renderer == "VUE_File_Renderer": return full_render_list - if (renderer == "ART_Renderer" - or renderer == "Redshift Renderer" - or renderer == "V_Ray_6_Hotfix_3" - or renderer == "V_Ray_GPU_6_Hotfix_3" - or renderer == "Default_Scanline_Renderer" - or renderer == "Quicksilver_Hardware_Renderer"): + if ( + renderer == "ART_Renderer" or + renderer == "Redshift Renderer" or + renderer == "V_Ray_6_Hotfix_3" or + renderer == "V_Ray_GPU_6_Hotfix_3" or + renderer == "Default_Scanline_Renderer" or + renderer == "Quicksilver_Hardware_Renderer"\ + ): render_elem_list = self.render_elements_product(output_file, startFrame, endFrame, diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index c1e376746a..ef8ad6bdc5 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -85,12 +85,14 @@ class RenderSettings(object): if renderer == "Arnold": return - if (renderer == "ART_Renderer" - or renderer == "Redshift Renderer" - or renderer == "V_Ray_6_Hotfix_3" - or renderer == "V_Ray_GPU_6_Hotfix_3" - or renderer == "Default_Scanline_Renderer" - or renderer == "Quicksilver_Hardware_Renderer"): + if ( + renderer == "ART_Renderer" or + renderer == "Redshift Renderer" or + renderer == "V_Ray_6_Hotfix_3" or + renderer == "V_Ray_GPU_6_Hotfix_3" or + renderer == "Default_Scanline_Renderer" or + renderer == "Quicksilver_Hardware_Renderer" + ): self.render_element_layer(output, width, height, img_fmt) rt.rendSaveFile = True diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index c4b8ac4985..59a1450691 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -34,13 +34,17 @@ class CollectRender(pyblish.api.InstancePlugin): renderer_class = get_current_renderer() renderer_name = str(renderer_class).split(":")[0] # setup the plugin as 3dsmax for the internal renderer - if (renderer_name == "ART_Renderer" - or renderer_name == "Default_Scanline_Renderer" - or renderer_name == "Quicksilver_Hardware_Renderer"): + if ( + renderer_name == "ART_Renderer" or + renderer_name == "Default_Scanline_Renderer" or + renderer_name == "Quicksilver_Hardware_Renderer" + ): plugin = "3dsmax" - if (renderer_name == "V_Ray_6_Hotfix_3" - or renderer_name == "V_Ray_GPU_6_Hotfix_3"): + if ( + renderer_name == "V_Ray_6_Hotfix_3" or + renderer_name == "V_Ray_GPU_6_Hotfix_3" + ): plugin = "Vray" if renderer_name == "Redshift Renderer": From 07e14442b172a23a7c019be10a135333526d9ee2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 16:44:41 +0800 Subject: [PATCH 072/483] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 4ba92f06bd..44efed2c36 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -51,7 +51,7 @@ class RenderProducts(object): renderer == "V_Ray_6_Hotfix_3" or renderer == "V_Ray_GPU_6_Hotfix_3" or renderer == "Default_Scanline_Renderer" or - renderer == "Quicksilver_Hardware_Renderer"\ + renderer == "Quicksilver_Hardware_Renderer" ): render_elem_list = self.render_elements_product(output_file, startFrame, From c54ead7ad2b2110d7a4786e986002dd8de270cf5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 18:37:37 +0800 Subject: [PATCH 073/483] add arnold camera and add 3dmax as renderer --- openpype/hosts/max/api/lib_rendersettings.py | 14 ++++++++++++- .../max/plugins/publish/collect_render.py | 21 +------------------ .../projects_schema/schema_project_max.json | 1 - 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index ef8ad6bdc5..db4e53720e 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -83,7 +83,7 @@ class RenderSettings(object): return # TODO: Finish the arnold render setup if renderer == "Arnold": - return + self.arnold_setup() if ( renderer == "ART_Renderer" or @@ -97,6 +97,18 @@ class RenderSettings(object): rt.rendSaveFile = True + def arnold_setup(self): + # get Arnold RenderView run in the background + # for setting up renderable camera + arv = rt.MAXToAOps.ArnoldRenderView() + render_camera = rt.viewport.GetCamera() + arv.setOption("Camera", str(render_camera)) + + aovmgr = rt.renderers.current.AOVManager + aovmgr.drivers = "#()" + + arv.close() + def render_element_layer(self, dir, width, height, ext): """For Renderers with render elements""" rt.renderWidth = width diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 59a1450691..ce8d62e089 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -34,25 +34,6 @@ class CollectRender(pyblish.api.InstancePlugin): renderer_class = get_current_renderer() renderer_name = str(renderer_class).split(":")[0] # setup the plugin as 3dsmax for the internal renderer - if ( - renderer_name == "ART_Renderer" or - renderer_name == "Default_Scanline_Renderer" or - renderer_name == "Quicksilver_Hardware_Renderer" - ): - plugin = "3dsmax" - - if ( - renderer_name == "V_Ray_6_Hotfix_3" or - renderer_name == "V_Ray_GPU_6_Hotfix_3" - ): - plugin = "Vray" - - if renderer_name == "Redshift Renderer": - plugin = "redshift" - - if renderer_name == "Arnold": - plugin = "arnold" - data = { "subset": instance.name, "asset": asset, @@ -62,7 +43,7 @@ class CollectRender(pyblish.api.InstancePlugin): "families": ['maxrender'], "source": filepath, "files": render_layer_files, - "plugin": plugin, + "plugin": "3dsmax", "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'] } diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index 3d4cd5c54a..fbd9358c74 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -35,7 +35,6 @@ "multiselection": false, "defaults": "exr", "enum_items": [ - {"avi": "avi"}, {"bmp": "bmp"}, {"exr": "exr"}, {"tif": "tif"}, From 819148cfc059fb852b28c96d721dffbbf25dc995 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 18:38:42 +0800 Subject: [PATCH 074/483] remove unused variable --- openpype/hosts/max/plugins/publish/collect_render.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index ce8d62e089..d8b0312d43 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -31,8 +31,6 @@ class CollectRender(pyblish.api.InstancePlugin): folder = folder.replace("\\", "/") imgFormat = RenderProducts().image_format() - renderer_class = get_current_renderer() - renderer_name = str(renderer_class).split(":")[0] # setup the plugin as 3dsmax for the internal renderer data = { "subset": instance.name, From 1b1f92ba5a74c5c2dac1c941df33ae61903ec3f5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 18:39:42 +0800 Subject: [PATCH 075/483] remove unused variable --- openpype/hosts/max/plugins/publish/collect_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index d8b0312d43..857ac88ea6 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -5,7 +5,6 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import legacy_io -from openpype.hosts.max.api.lib import get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts From e1aff812525d8d2bd4aa0308e68db68994ade388 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 18:56:37 +0800 Subject: [PATCH 076/483] correct separator issue in naming convention --- openpype/hosts/max/api/lib_rendersettings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index db4e53720e..8f7e91822c 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -75,7 +75,7 @@ class RenderSettings(object): )] except KeyError: aov_separator = "." - outputFilename = "{0}.{1}".format(output, img_fmt) + outputFilename = "{0}..{1}".format(output, img_fmt) outputFilename = outputFilename.replace("{aov_separator}", aov_separator) rt.rendOutputFilename = outputFilename @@ -122,7 +122,7 @@ class RenderSettings(object): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") render_element = os.path.join(dir, renderpass) - aov_name = "{0}.{1}".format(render_element, ext) + aov_name = "{0}..{1}".format(render_element, ext) try: aov_separator = self._aov_chars[( self._project_settings["maya"] From 31424672aa22d08629e87f49dfadcdd2a8ba3e19 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 18:59:35 +0800 Subject: [PATCH 077/483] correct separator issue in naming convention --- openpype/hosts/max/api/lib_renderproducts.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 44efed2c36..fd0eb947af 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -92,9 +92,10 @@ class RenderProducts(object): render_dir = os.path.join(folder, renderpass) if renderlayer_name.enabled: for f in range(startFrame, endFrame): - render_element = "{0}.{1}.{2}".format(render_dir, - str(f), - fmt) + render_element = "{0}_{1}..{2}.{2}".format(folder, + renderpass, + str(f), + fmt) render_element = render_element.replace("\\", "/") render_dirname.append(render_element) From 536cbd2913d89b64acc8b41b58bd77efeefb51c4 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 18:59:58 +0800 Subject: [PATCH 078/483] correct separator issue in naming convention --- openpype/hosts/max/api/lib_renderproducts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index fd0eb947af..83b5a0bc35 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -89,7 +89,6 @@ class RenderProducts(object): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - render_dir = os.path.join(folder, renderpass) if renderlayer_name.enabled: for f in range(startFrame, endFrame): render_element = "{0}_{1}..{2}.{2}".format(folder, From 4bd05046236759e6209a256af639ee943e091406 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 19:00:23 +0800 Subject: [PATCH 079/483] correct separator issue in naming convention --- openpype/hosts/max/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 83b5a0bc35..a924505d7e 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -91,7 +91,7 @@ class RenderProducts(object): if renderlayer_name.enabled: for f in range(startFrame, endFrame): - render_element = "{0}_{1}..{2}.{2}".format(folder, + render_element = "{0}_{1}..{2}.{3}".format(folder, renderpass, str(f), fmt) From 0a7d7e45638987a53ac35c2dd9c69d38cfe9855e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 19:04:28 +0800 Subject: [PATCH 080/483] correct separator issue in naming convention --- openpype/hosts/max/api/lib_renderproducts.py | 2 +- openpype/hosts/max/api/lib_rendersettings.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index a924505d7e..4c9c9b8088 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -91,7 +91,7 @@ class RenderProducts(object): if renderlayer_name.enabled: for f in range(startFrame, endFrame): - render_element = "{0}_{1}..{2}.{3}".format(folder, + render_element = "{0}_{1}.{2}.{3}".format(folder, renderpass, str(f), fmt) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 8f7e91822c..30a252e07a 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -121,8 +121,7 @@ class RenderSettings(object): for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - render_element = os.path.join(dir, renderpass) - aov_name = "{0}..{1}".format(render_element, ext) + aov_name = "{0}_{1}..{2}".format(dir, renderpass, ext) try: aov_separator = self._aov_chars[( self._project_settings["maya"] From a3bc0e6debfe0e0e1dabff1bba543b30af738e65 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 19:54:14 +0800 Subject: [PATCH 081/483] update aov naming convention --- openpype/hosts/max/api/lib_renderproducts.py | 9 ++++----- openpype/hosts/max/api/lib_rendersettings.py | 15 +++------------ 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 4c9c9b8088..84cb0c1744 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -88,13 +88,12 @@ class RenderProducts(object): for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - + render_element = os.path.join(dir, renderpass) if renderlayer_name.enabled: for f in range(startFrame, endFrame): - render_element = "{0}_{1}.{2}.{3}".format(folder, - renderpass, - str(f), - fmt) + render_element = "{0}.{1}.{2}".format(render_element, + str(f), + fmt) render_element = render_element.replace("\\", "/") render_dirname.append(render_element) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 30a252e07a..1188d77e29 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -121,16 +121,7 @@ class RenderSettings(object): for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - aov_name = "{0}_{1}..{2}".format(dir, renderpass, ext) - try: - aov_separator = self._aov_chars[( - self._project_settings["maya"] - ["RenderSettings"] - ["aov_separator"] - )] - except KeyError: - aov_separator = "." - - aov_name = aov_name.replace("{aov_separator}", - aov_separator) + render_element = os.path.join(dir, renderpass) + dir = dir.replace(".", " ") + aov_name = "{0}..{1}".format(render_element, ext) render_elem.SetRenderElementFileName(i, aov_name) From 189a842660436b23f67de81a1548683ca3b066f5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 19:55:22 +0800 Subject: [PATCH 082/483] update aov naming convention --- openpype/hosts/max/api/lib_renderproducts.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 84cb0c1744..912c0c89d7 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -88,12 +88,12 @@ class RenderProducts(object): for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - render_element = os.path.join(dir, renderpass) + render_name = os.path.join(dir, renderpass) if renderlayer_name.enabled: for f in range(startFrame, endFrame): - render_element = "{0}.{1}.{2}".format(render_element, - str(f), - fmt) + render_element = "{0}.{1}.{2}".format(render_name, + str(f), + fmt) render_element = render_element.replace("\\", "/") render_dirname.append(render_element) From 0d2b6da3ef7e012d00b938a9b47609e66bb928e5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 20:02:28 +0800 Subject: [PATCH 083/483] update aov naming convention --- openpype/hosts/max/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 912c0c89d7..5f96b13273 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -88,7 +88,7 @@ class RenderProducts(object): for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - render_name = os.path.join(dir, renderpass) + render_name = os.path.join(folder, renderpass) if renderlayer_name.enabled: for f in range(startFrame, endFrame): render_element = "{0}.{1}.{2}".format(render_name, From be573252486287a04dc37a5d0daa6edb13fb8ce5 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 20:22:19 +0800 Subject: [PATCH 084/483] update aov naming convention --- openpype/hosts/max/api/lib_renderproducts.py | 8 ++++---- openpype/hosts/max/api/lib_rendersettings.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 5f96b13273..e3cccff982 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -88,12 +88,12 @@ class RenderProducts(object): for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - render_name = os.path.join(folder, renderpass) if renderlayer_name.enabled: for f in range(startFrame, endFrame): - render_element = "{0}.{1}.{2}".format(render_name, - str(f), - fmt) + render_element = "{0}_{1}.{2}.{3}".format(folder, + renderpass, + str(f), + fmt) render_element = render_element.replace("\\", "/") render_dirname.append(render_element) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 1188d77e29..6d2aa678b7 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -122,6 +122,5 @@ class RenderSettings(object): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") render_element = os.path.join(dir, renderpass) - dir = dir.replace(".", " ") aov_name = "{0}..{1}".format(render_element, ext) render_elem.SetRenderElementFileName(i, aov_name) From 9ba3d144f381d62127c90542d280ad80c0306b18 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 9 Feb 2023 20:24:03 +0800 Subject: [PATCH 085/483] update aov naming convention --- openpype/hosts/max/api/lib_rendersettings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 6d2aa678b7..2324d743eb 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -121,6 +121,5 @@ class RenderSettings(object): for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - render_element = os.path.join(dir, renderpass) - aov_name = "{0}..{1}".format(render_element, ext) + aov_name = "{0}_{1}..{2}".format(dir, renderpass, ext) render_elem.SetRenderElementFileName(i, aov_name) From 88bda4e1f6bfbe628d80edc76789393dcc9a68a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 14:24:28 +0100 Subject: [PATCH 086/483] TVPaint host inherit from IPublishHost --- openpype/hosts/tvpaint/api/pipeline.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 249326791b..bd6b929f6b 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -8,7 +8,7 @@ import requests import pyblish.api from openpype.client import get_project, get_asset_by_name -from openpype.host import HostBase, IWorkfileHost, ILoadHost +from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost from openpype.hosts.tvpaint import TVPAINT_ROOT_DIR from openpype.settings import get_current_project_settings from openpype.lib import register_event_callback @@ -58,7 +58,7 @@ instances=2 """ -class TVPaintHost(HostBase, IWorkfileHost, ILoadHost): +class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): name = "tvpaint" def install(self): From 2c0f057a913b9a891f40ffed06959749b90eaae5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 14:25:59 +0100 Subject: [PATCH 087/483] implemented methods for context data --- openpype/hosts/tvpaint/api/pipeline.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index bd6b929f6b..7ab660137a 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -29,6 +29,7 @@ log = logging.getLogger(__name__) METADATA_SECTION = "avalon" SECTION_NAME_CONTEXT = "context" +SECTION_NAME_CREATE_CONTEXT = "create_context" SECTION_NAME_INSTANCES = "instances" SECTION_NAME_CONTAINERS = "containers" # Maximum length of metadata chunk string @@ -93,6 +94,14 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_event_callback("application.launched", self.initial_launch) register_event_callback("application.exit", self.application_exit) + + # --- Create --- + def get_context_data(self): + return get_workfile_metadata(SECTION_NAME_CREATE_CONTEXT, {}) + + def update_context_data(self, data, changes): + return write_workfile_metadata(SECTION_NAME_CREATE_CONTEXT, data) + def open_workfile(self, filepath): george_script = "tv_LoadProject '\"'\"{}\"'\"'".format( filepath.replace("\\", "/") From 85afe4f53ba31678e0d35bbd675c114d7e133b96 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 14:27:22 +0100 Subject: [PATCH 088/483] cleanup of methods --- openpype/hosts/tvpaint/api/pipeline.py | 60 ++++++++++++++------------ 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 7ab660137a..38d3922f3b 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -102,6 +102,35 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def update_context_data(self, data, changes): return write_workfile_metadata(SECTION_NAME_CREATE_CONTEXT, data) + def list_instances(self): + """List all created instances from current workfile.""" + return list_instances() + + def write_instances(self, data): + return write_instances(data) + + # --- Legacy Create --- + def remove_instance(self, instance): + """Remove instance from current workfile metadata. + + Implementation for Subset manager tool. + """ + + current_instances = get_workfile_metadata(SECTION_NAME_INSTANCES) + instance_id = instance.get("uuid") + found_idx = None + if instance_id: + for idx, _inst in enumerate(current_instances): + if _inst["uuid"] == instance_id: + found_idx = idx + break + + if found_idx is None: + return + current_instances.pop(found_idx) + write_instances(current_instances) + + # --- Workfile --- def open_workfile(self, filepath): george_script = "tv_LoadProject '\"'\"{}\"'\"'".format( filepath.replace("\\", "/") @@ -134,6 +163,7 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def get_workfile_extensions(self): return [".tvpp"] + # --- Load --- def get_containers(self): return get_containers() @@ -148,26 +178,6 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): log.info("Setting up project...") set_context_settings() - def remove_instance(self, instance): - """Remove instance from current workfile metadata. - - Implementation for Subset manager tool. - """ - - current_instances = get_workfile_metadata(SECTION_NAME_INSTANCES) - instance_id = instance.get("uuid") - found_idx = None - if instance_id: - for idx, _inst in enumerate(current_instances): - if _inst["uuid"] == instance_id: - found_idx = idx - break - - if found_idx is None: - return - current_instances.pop(found_idx) - write_instances(current_instances) - def application_exit(self): """Logic related to TimerManager. @@ -186,6 +196,7 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url) requests.post(rest_api_url) + # --- Legacy Publish --- def on_instance_toggle(self, instance, old_value, new_value): """Update instance data in workfile on publish toggle.""" # Review may not have real instance in wokrfile metadata @@ -196,7 +207,7 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): found_idx = None current_instances = list_instances() for idx, workfile_instance in enumerate(current_instances): - if workfile_instance["uuid"] == instance_id: + if workfile_instance.get("uuid") == instance_id: found_idx = idx break @@ -207,13 +218,6 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): current_instances[found_idx]["active"] = new_value self.write_instances(current_instances) - def list_instances(self): - """List all created instances from current workfile.""" - return list_instances() - - def write_instances(self, data): - return write_instances(data) - def containerise( name, namespace, members, context, loader, current_containers=None From a17f46486405efe859b626e1633bc4666ac2b709 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 14:28:13 +0100 Subject: [PATCH 089/483] implement custom context methods --- openpype/hosts/tvpaint/api/pipeline.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 38d3922f3b..85ade41b9b 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -18,6 +18,7 @@ from openpype.pipeline import ( register_creator_plugin_path, AVALON_CONTAINER_ID, ) +from openpype.pipeline.context_tools import get_global_context from .lib import ( execute_george, @@ -94,6 +95,40 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): register_event_callback("application.launched", self.initial_launch) register_event_callback("application.exit", self.application_exit) + def get_current_project_name(self): + """ + Returns: + Union[str, None]: Current project name. + """ + + return self.get_current_context().get("project_name") + + def get_current_asset_name(self): + """ + Returns: + Union[str, None]: Current asset name. + """ + + return self.get_current_context().get("asset_name") + + def get_current_task_name(self): + """ + Returns: + Union[str, None]: Current task name. + """ + + return self.get_current_context().get("task_name") + + def get_current_context(self): + context = get_current_workfile_context() + if not context: + return get_global_context() + + return { + "project_name": context["project"], + "asset_name": context.get("asset"), + "task_name": context.get("task") + } # --- Create --- def get_context_data(self): From 65e7717b6c9bc64f5a832125340bcba0aed50981 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 14:28:25 +0100 Subject: [PATCH 090/483] use 'get_global_context' on workfile save --- openpype/hosts/tvpaint/api/pipeline.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 85ade41b9b..6a729e39c3 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -175,11 +175,7 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def save_workfile(self, filepath=None): if not filepath: filepath = self.get_current_workfile() - context = { - "project": legacy_io.Session["AVALON_PROJECT"], - "asset": legacy_io.Session["AVALON_ASSET"], - "task": legacy_io.Session["AVALON_TASK"] - } + context = get_global_context() save_current_workfile_context(context) # Execute george script to save workfile. From b9f22cf2bec8f93d673a3aba9a1cbfd264da69f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 14:28:45 +0100 Subject: [PATCH 091/483] removed unused method --- openpype/hosts/tvpaint/api/plugin.py | 31 ---------------------------- 1 file changed, 31 deletions(-) diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index da456e7067..c7feccd125 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -32,37 +32,6 @@ class Creator(LegacyCreator): dynamic_data["task"] = task_name return dynamic_data - @staticmethod - def are_instances_same(instance_1, instance_2): - """Compare instances but skip keys with unique values. - - During compare are skipped keys that will be 100% sure - different on new instance, like "id". - - Returns: - bool: True if instances are same. - """ - if ( - not isinstance(instance_1, dict) - or not isinstance(instance_2, dict) - ): - return instance_1 == instance_2 - - checked_keys = set() - checked_keys.add("id") - for key, value in instance_1.items(): - if key not in checked_keys: - if key not in instance_2: - return False - if value != instance_2[key]: - return False - checked_keys.add(key) - - for key in instance_2.keys(): - if key not in checked_keys: - return False - return True - def write_instances(self, data): self.log.debug( "Storing instance data to workfile. {}".format(str(data)) From 1cb7c37b9ef51066d2a2c3cc3b243d058303397f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 14:29:02 +0100 Subject: [PATCH 092/483] implemented base creators for tvpaint --- openpype/hosts/tvpaint/api/plugin.py | 125 +++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index c7feccd125..e6fc087665 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -6,11 +6,136 @@ from openpype.pipeline import ( LoaderPlugin, registered_host, ) +from openpype.pipeline.create import ( + CreatedInstance, + get_subset_name, + AutoCreator, + Creator as NewCreator, +) +from openpype.pipeline.create.creator_plugins import cache_and_get_instances from .lib import get_layers_data from .pipeline import get_current_workfile_context +SHARED_DATA_KEY = "openpype.tvpaint.instances" + + +def _collect_instances(creator): + instances_by_identifier = cache_and_get_instances( + creator, SHARED_DATA_KEY, creator.host.list_instances + ) + for instance_data in instances_by_identifier[creator.identifier]: + instance = CreatedInstance.from_existing(instance_data, creator) + creator._add_instance_to_context(instance) + + +def _update_instances(creator, update_list): + if not update_list: + return + + cur_instances = creator.host.list_instances() + cur_instances_by_id = {} + for instance_data in cur_instances: + instance_id = instance_data.get("instance_id") + if instance_id: + cur_instances_by_id[instance_id] = instance_data + + for instance, changes in update_list: + instance_data = instance.data_to_store() + cur_instance_data = cur_instances_by_id.get(instance.id) + if cur_instance_data is None: + cur_instances.append(instance_data) + continue + for key in set(cur_instance_data) - set(instance_data): + instance_data.pop(key) + instance_data.update(cur_instance_data) + creator.host.write_instances(cur_instances) + + +class TVPaintCreator(NewCreator): + @property + def subset_template_family(self): + return self.family + + def collect_instances(self): + _collect_instances(self) + + def update_instances(self, update_list): + _update_instances(self, update_list) + + def remove_instances(self, instances): + ids_to_remove = { + instance.id + for instance in instances + } + cur_instances = self.host.list_instances() + changed = False + new_instances = [] + for instance_data in cur_instances: + if instance_data.get("instance_id") in ids_to_remove: + changed = True + else: + new_instances.append(instance_data) + + if changed: + self.host.write_instances(new_instances) + + for instance in instances: + self._remove_instance_from_context(instance) + + def get_dynamic_data(self, *args, **kwargs): + # Change asset and name by current workfile context + # TODO use context from 'create_context' + workfile_context = self.host.get_current_context() + asset_name = workfile_context.get("asset") + task_name = workfile_context.get("task") + output = {} + if asset_name: + output["asset"] = asset_name + if task_name: + output["task"] = task_name + return output + + def get_subset_name( + self, + variant, + task_name, + asset_doc, + project_name, + host_name=None, + instance=None + ): + dynamic_data = self.get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name, instance + ) + + return get_subset_name( + self.subset_template_family, + variant, + task_name, + asset_doc, + project_name, + host_name, + dynamic_data=dynamic_data, + project_settings=self.project_settings + ) + + def _store_new_instance(self, new_instance): + instances_data = self.host.list_instances() + instances_data.append(new_instance.data_to_store()) + self.host.write_instances(instances_data) + self._add_instance_to_context(new_instance) + + +class TVPaintAutoCreator(AutoCreator): + def collect_instances(self): + _collect_instances(self) + + def update_instances(self, update_list): + _update_instances(self, update_list) + + class Creator(LegacyCreator): def __init__(self, *args, **kwargs): super(Creator, self).__init__(*args, **kwargs) From f984dd8fc5381dd901d69bca6fef5c234ffae1c5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 14:29:15 +0100 Subject: [PATCH 093/483] implemented workfile autocreator --- .../tvpaint/plugins/create/create_workfile.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 openpype/hosts/tvpaint/plugins/create/create_workfile.py diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py new file mode 100644 index 0000000000..3e5cd86852 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -0,0 +1,63 @@ +from openpype.client import get_asset_by_name +from openpype.pipeline import CreatedInstance +from openpype.hosts.tvpaint.api.plugin import TVPaintAutoCreator + + +class TVPaintWorkfileCreator(TVPaintAutoCreator): + family = "workfile" + identifier = "workfile" + + default_variant = "Main" + + def create(self): + existing_instance = None + for instance in self.create_context.instances: + if instance.creator_identifier == self.identifier: + existing_instance = instance + break + + context = self.host.get_current_context() + host_name = self.host.name + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] + + if existing_instance is None: + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + self.default_variant, + task_name, + asset_doc, + project_name, + host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": self.default_variant + } + + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + instances_data = self.host.list_instances() + instances_data.append(new_instance.data_to_store()) + self.host.write_instances(instances_data) + self._add_instance_to_context(new_instance) + + elif ( + existing_instance["asset"] != asset_name + or existing_instance["task"] != task_name + ): + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + existing_instance["variant"], + task_name, + asset_doc, + project_name, + host_name, + existing_instance + ) + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name + existing_instance["subset"] = subset_name From 2a137622fbe17d588e770e796ad3fdc48401918e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 14:29:37 +0100 Subject: [PATCH 094/483] base of render layer/pass creators --- .../tvpaint/plugins/create/create_render.py | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 openpype/hosts/tvpaint/plugins/create/create_render.py diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py new file mode 100644 index 0000000000..67337b77a3 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -0,0 +1,309 @@ +from openpype.lib import ( + prepare_template_data, + EnumDef, + TextDef, +) +from openpype.pipeline.create import ( + CreatedInstance, + CreatorError, +) +from openpype.hosts.tvpaint.api.plugin import TVPaintCreator +from openpype.hosts.tvpaint.api.lib import ( + get_layers_data, + get_groups_data, + execute_george_through_file, +) + + +class CreateRenderlayer(TVPaintCreator): + """Mark layer group as one instance.""" + label = "Render Layer" + family = "render" + subset_template_family = "renderLayer" + identifier = "render.layer" + icon = "fa.cube" + + # George script to change color group + rename_script_template = ( + "tv_layercolor \"setcolor\"" + " {clip_id} {group_id} {r} {g} {b} \"{name}\"" + ) + order = 90 + + # Settings + render_pass = "beauty" + + def get_dynamic_data( + self, variant, task_name, asset_doc, project_name, host_name, instance + ): + dynamic_data = super().get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name, instance + ) + dynamic_data["renderpass"] = self.render_pass + dynamic_data["renderlayer"] = variant + return dynamic_data + + def _get_selected_group_ids(self): + return { + layer["group_id"] + for layer in get_layers_data() + if layer["selected"] + } + + def create(self, subset_name, instance_data, pre_create_data): + self.log.debug("Query data from workfile.") + + group_id = pre_create_data.get("group_id") + # This creator should run only on one group + if group_id is None or group_id == -1: + selected_groups = self._get_selected_group_ids() + selected_groups.discard(0) + if len(selected_groups) > 1: + raise CreatorError("You have selected more than one group") + + if len(selected_groups) == 0: + raise CreatorError("You don't have selected any group") + group_id = tuple(selected_groups)[0] + + self.log.debug("Querying groups data from workfile.") + groups_data = get_groups_data() + group_item = None + for group_data in groups_data: + if group_data["group_id"] == group_id: + group_item = group_data + + for instance in self.create_context.instances: + if ( + instance.creator_identifier == self.identifier + and instance["creator_attributes"]["group_id"] == group_id + ): + raise CreatorError(( + f"Group \"{group_item.get('name')}\" is already used" + f" by another render layer \"{instance['subset']}\"" + )) + + self.log.debug(f"Selected group id is \"{group_id}\".") + if "creator_attributes" not in instance_data: + instance_data["creator_attributes"] = {} + instance_data["creator_attributes"]["group_id"] = group_id + + self.log.info(f"Subset name is {subset_name}") + new_instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self + ) + self._store_new_instance(new_instance) + + new_group_name = pre_create_data.get("group_name") + if not new_group_name or not group_id: + return + + self.log.debug("Changing name of the group.") + + new_group_name = pre_create_data.get("group_name") + if not new_group_name or group_item["name"] == new_group_name: + return + # Rename TVPaint group (keep color same) + # - groups can't contain spaces + rename_script = self.rename_script_template.format( + clip_id=group_item["clip_id"], + group_id=group_item["group_id"], + r=group_item["red"], + g=group_item["green"], + b=group_item["blue"], + name=new_group_name + ) + execute_george_through_file(rename_script) + + self.log.info(( + f"Name of group with index {group_id}" + f" was changed to \"{new_group_name}\"." + )) + + def get_pre_create_attr_defs(self): + groups_enum = [ + { + "label": group["name"], + "value": group["group_id"] + } + for group in get_groups_data() + if group["name"] + ] + groups_enum.insert(0, {"label": "", "value": -1}) + + return [ + EnumDef( + "group_id", + label="Group", + items=groups_enum + ), + TextDef( + "group_name", + label="New group name", + placeholder="< Keep unchanged >" + ) + ] + + def get_instance_attr_defs(self): + groups_enum = [ + { + "label": group["name"], + "value": group["group_id"] + } + for group in get_groups_data() + if group["name"] + ] + return [ + EnumDef( + "group_id", + label="Group", + items=groups_enum + ) + ] + + +class CreateRenderPass(TVPaintCreator): + icon = "fa.cube" + family = "render" + subset_template_family = "renderPass" + identifier = "render.pass" + label = "Render Pass" + + order = CreateRenderlayer.order + 10 + + def get_dynamic_data( + self, variant, task_name, asset_doc, project_name, host_name, instance + ): + dynamic_data = super().get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name, instance + ) + dynamic_data["renderpass"] = variant + dynamic_data["renderlayer"] = "{renderlayer}" + return dynamic_data + + def create(self, subset_name, instance_data, pre_create_data): + render_layer_instance_id = pre_create_data.get( + "render_layer_instance_id" + ) + if not render_layer_instance_id: + raise CreatorError("Missing RenderLayer instance") + + render_layer_instance = self.create_context.instances_by_id.get( + render_layer_instance_id + ) + if render_layer_instance is None: + raise CreatorError(( + "RenderLayer instance was not found" + f" by id \"{render_layer_instance_id}\"" + )) + + group_id = render_layer_instance["creator_attributes"]["group_id"] + self.log.debug("Query data from workfile.") + layers_data = get_layers_data() + + self.log.debug("Checking selection.") + # Get all selected layers and their group ids + selected_layers = [ + layer + for layer in layers_data + if layer["selected"] + ] + + # Raise if nothing is selected + if not selected_layers: + raise CreatorError("Nothing is selected. Please select layers.") + + selected_layer_names = {layer["name"] for layer in selected_layers} + instances_to_remove = [] + for instance in self.create_context.instances: + if instance.creator_identifier != self.identifier: + continue + layer_names = set(instance["layer_names"]) + if not layer_names.intersection(selected_layer_names): + continue + new_layer_names = layer_names - selected_layer_names + if new_layer_names: + instance["layer_names"] = list(new_layer_names) + else: + instances_to_remove.append(instance) + + render_layer = render_layer_instance["variant"] + subset_name_fill_data = {"renderlayer": render_layer} + + # Format dynamic keys in subset name + new_subset_name = subset_name.format( + **prepare_template_data(subset_name_fill_data) + ) + self.log.info(f"New subset name is \"{new_subset_name}\".") + instance_data["layer_names"] = list(selected_layer_names) + new_instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self + ) + instances_data = self._remove_and_filter_instances( + instances_to_remove + ) + instances_data.append(new_instance.data_to_store()) + + self.host.write_instances(instances_data) + self._add_instance_to_context(new_instance) + self._change_layers_group(selected_layers, group_id) + + def _change_layers_group(self, layers, group_id): + filtered_layers = [ + layer + for layer in layers + if layer["group_id"] != group_id + ] + if filtered_layers: + self.log.info(( + "Changing group of " + f"{','.join([l['name'] for l in filtered_layers])}" + f" to {group_id}" + )) + george_lines = [ + f"tv_layercolor \"set\" {layer['layer_id']} {group_id}" + for layer in filtered_layers + ] + execute_george_through_file("\n".join(george_lines)) + + def _remove_and_filter_instances(self, instances_to_remove): + instances_data = self.host.list_instances() + if not instances_to_remove: + return instances_data + + removed_ids = set() + for instance in instances_to_remove: + removed_ids.add(instance.id) + self._remove_instance_from_context(instance) + + return [ + instance_data + for instance_data in instances_data + if instance_data.get("instance_id") not in removed_ids + ] + + def get_pre_create_attr_defs(self): + render_layers = [] + for instance in self.create_context.instances: + if instance.creator_identifier == "render.layer": + render_layers.append({ + "value": instance.id, + "label": instance.label + }) + + if not render_layers: + render_layers.append({"value": None, "label": "N/A"}) + + return [ + EnumDef( + "render_layer_instance_id", + label="Render Layer", + items=render_layers + ) + ] + From bd538e7e70373caafd3067e0acd26acdf8297d28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 14:30:08 +0100 Subject: [PATCH 095/483] use publisher instead of pyblish pype --- openpype/hosts/tvpaint/api/communication_server.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py index 6ac3e6324c..6fd2d69373 100644 --- a/openpype/hosts/tvpaint/api/communication_server.py +++ b/openpype/hosts/tvpaint/api/communication_server.py @@ -344,7 +344,7 @@ class QtTVPaintRpc(BaseTVPaintRpc): async def publish_tool(self): log.info("Triggering Publish tool") - item = MainThreadItem(self.tools_helper.show_publish) + item = MainThreadItem(self.tools_helper.show_publisher_tool) self._execute_in_main_thread(item) return @@ -875,10 +875,6 @@ class QtCommunicator(BaseCommunicator): "callback": "library_loader_tool", "label": "Library", "help": "Open library loader tool" - }, { - "callback": "subset_manager_tool", - "label": "Subset Manager", - "help": "Open subset manager tool" }, { "callback": "experimental_tools", "label": "Experimental tools", From e7072008af1a316f47ed52ec34823d5a046710e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 17:33:35 +0100 Subject: [PATCH 096/483] added 'family_filter' argument to 'get_subset_name' --- openpype/pipeline/create/subset_name.py | 39 ++++++++++++++++--------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/create/subset_name.py b/openpype/pipeline/create/subset_name.py index ed05dd6083..3f0692b46a 100644 --- a/openpype/pipeline/create/subset_name.py +++ b/openpype/pipeline/create/subset_name.py @@ -70,7 +70,8 @@ def get_subset_name( host_name=None, default_template=None, dynamic_data=None, - project_settings=None + project_settings=None, + family_filter=None, ): """Calculate subset name based on passed context and OpenPype settings. @@ -82,23 +83,35 @@ def get_subset_name( That's main reason why so many arguments are required to calculate subset name. + Option to pass family filter was added for special cases when creator or + automated publishing require special subset name template which would be + hard to maintain using its family value. + Why not just pass the right family? -> Family is also used as fill + value and for filtering of publish plugins. + + Todos: + Find better filtering options to avoid requirement of + argument 'family_filter'. + Args: family (str): Instance family. variant (str): In most of the cases it is user input during creation. task_name (str): Task name on which context is instance created. asset_doc (dict): Queried asset document with its tasks in data. Used to get task type. - project_name (str): Name of project on which is instance created. - Important for project settings that are loaded. - host_name (str): One of filtering criteria for template profile - filters. - default_template (str): Default template if any profile does not match - passed context. Constant 'DEFAULT_SUBSET_TEMPLATE' is used if - is not passed. - dynamic_data (dict): Dynamic data specific for a creator which creates - instance. - project_settings (Union[Dict[str, Any], None]): Prepared settings for - project. Settings are queried if not passed. + project_name (Optional[str]): Name of project on which is instance + created. Important for project settings that are loaded. + host_name (Optional[str]): One of filtering criteria for template + profile filters. + default_template (Optional[str]): Default template if any profile does + not match passed context. Constant 'DEFAULT_SUBSET_TEMPLATE' + is used if is not passed. + dynamic_data (Optional[Dict[str, Any]]): Dynamic data specific for + a creator which creates instance. + project_settings (Optional[Union[Dict[str, Any]]]): Prepared settings + for project. Settings are queried if not passed. + family_filter (Optional[str]): Use different family for subset template + filtering. Value of 'family' is used when not passed. """ if not family: @@ -119,7 +132,7 @@ def get_subset_name( template = get_subset_name_template( project_name, - family, + family_filter or family, task_name, task_type, host_name, From 0ab0f323f81aa403789fb4e3b0114c8791d3b8db Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 17:34:03 +0100 Subject: [PATCH 097/483] use new option of family filter in TVPaint creators --- openpype/hosts/tvpaint/api/plugin.py | 7 ++++--- openpype/hosts/tvpaint/plugins/create/create_render.py | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index e6fc087665..c57baf7212 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -55,7 +55,7 @@ def _update_instances(creator, update_list): class TVPaintCreator(NewCreator): @property - def subset_template_family(self): + def subset_template_family_filter(self): return self.family def collect_instances(self): @@ -111,14 +111,15 @@ class TVPaintCreator(NewCreator): ) return get_subset_name( - self.subset_template_family, + self.family, variant, task_name, asset_doc, project_name, host_name, dynamic_data=dynamic_data, - project_settings=self.project_settings + project_settings=self.project_settings, + family_filter=self.subset_template_family_filter ) def _store_new_instance(self, new_instance): diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 67337b77a3..4050bddd52 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -19,7 +19,7 @@ class CreateRenderlayer(TVPaintCreator): """Mark layer group as one instance.""" label = "Render Layer" family = "render" - subset_template_family = "renderLayer" + subset_template_family_filter = "renderLayer" identifier = "render.layer" icon = "fa.cube" @@ -167,7 +167,7 @@ class CreateRenderlayer(TVPaintCreator): class CreateRenderPass(TVPaintCreator): icon = "fa.cube" family = "render" - subset_template_family = "renderPass" + subset_template_family_filter = "renderPass" identifier = "render.pass" label = "Render Pass" From 3fb87723a0194ad2644b8233dbc8daa7d81c8f01 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 18:29:54 +0100 Subject: [PATCH 098/483] added basic docstring for render creators --- .../tvpaint/plugins/create/create_render.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 4050bddd52..92439f329f 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -1,3 +1,34 @@ +"""Render Layer and Passes creators. + +Render layer is main part which is represented by group in TVPaint. All TVPaint +layers marked with that group color are part of the render layer. To be more +specific about some parts of layer it is possible to create sub-sets of layer +which are named passes. Render pass consist of layers in same color group as +render layer but define more specific part. + +For example render layer could be 'Bob' which consist of 5 TVPaint layers. +- Bob has 'head' which consist of 2 TVPaint layers -> Render pass 'head' +- Bob has 'body' which consist of 1 TVPaint layer -> Render pass 'body' +- Bob has 'arm' which consist of 1 TVPaint layer -> Render pass 'arm' +- Last layer does not belong to render pass at all + +Bob will be rendered as 'beauty' of bob (all visible layers in group). +His head will be rendered too but without any other parts. The same for body +and arm. + +What is this good for? Compositing has more power how the renders are used. +Can do transforms on each render pass without need to modify a re-render them +using TVPaint. + +The workflow may hit issues when there are used other blending modes than +default 'color' blend more. In that case it is not recommended to use this +workflow at all as other blend modes may affect all layers in clip which can't +be done. + +Todos: + Add option to extract marked layers and passes as json output format for + AfterEffects. +""" from openpype.lib import ( prepare_template_data, EnumDef, From e426c2c4f0216a3d280f75f9f865de3d3d5ab8d8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 18:32:34 +0100 Subject: [PATCH 099/483] added option to mark render instance for review --- .../tvpaint/plugins/create/create_render.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 92439f329f..3a89608c7c 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -33,6 +33,7 @@ from openpype.lib import ( prepare_template_data, EnumDef, TextDef, + BoolDef, ) from openpype.pipeline.create import ( CreatedInstance, @@ -48,6 +49,7 @@ from openpype.hosts.tvpaint.api.lib import ( class CreateRenderlayer(TVPaintCreator): """Mark layer group as one instance.""" + label = "Render Layer" family = "render" subset_template_family_filter = "renderLayer" @@ -63,6 +65,7 @@ class CreateRenderlayer(TVPaintCreator): # Settings render_pass = "beauty" + mark_for_review = True def get_dynamic_data( self, variant, task_name, asset_doc, project_name, host_name, instance @@ -116,7 +119,12 @@ class CreateRenderlayer(TVPaintCreator): self.log.debug(f"Selected group id is \"{group_id}\".") if "creator_attributes" not in instance_data: instance_data["creator_attributes"] = {} - instance_data["creator_attributes"]["group_id"] = group_id + creator_attributes = instance_data["creator_attributes"] + mark_for_review = pre_create_data.get("mark_for_review") + if mark_for_review is None: + mark_for_review = self.mark_for_review + creator_attributes["group_id"] = group_id + creator_attributes["mark_for_review"] = mark_for_review self.log.info(f"Subset name is {subset_name}") new_instance = CreatedInstance( @@ -174,6 +182,11 @@ class CreateRenderlayer(TVPaintCreator): "group_name", label="New group name", placeholder="< Keep unchanged >" + ), + BoolDef( + "mark_for_review", + label="Review", + default=self.mark_for_review ) ] @@ -191,6 +204,11 @@ class CreateRenderlayer(TVPaintCreator): "group_id", label="Group", items=groups_enum + ), + BoolDef( + "mark_for_review", + label="Review", + default=self.mark_for_review ) ] @@ -204,6 +222,9 @@ class CreateRenderPass(TVPaintCreator): order = CreateRenderlayer.order + 10 + # Settings + mark_for_review = True + def get_dynamic_data( self, variant, task_name, asset_doc, project_name, host_name, instance ): @@ -269,6 +290,18 @@ class CreateRenderPass(TVPaintCreator): ) self.log.info(f"New subset name is \"{new_subset_name}\".") instance_data["layer_names"] = list(selected_layer_names) + if "creator_attributes" not in instance_data: + instance_data["creator_attribtues"] = {} + + creator_attributes = instance_data["creator_attribtues"] + mark_for_review = pre_create_data.get("mark_for_review") + if mark_for_review is None: + mark_for_review = self.mark_for_review + creator_attributes["mark_for_review"] = mark_for_review + creator_attributes["render_layer_instance_id"] = ( + render_layer_instance_id + ) + new_instance = CreatedInstance( self.family, subset_name, @@ -321,7 +354,7 @@ class CreateRenderPass(TVPaintCreator): def get_pre_create_attr_defs(self): render_layers = [] for instance in self.create_context.instances: - if instance.creator_identifier == "render.layer": + if instance.creator_identifier == CreateRenderlayer.identifier: render_layers.append({ "value": instance.id, "label": instance.label @@ -335,6 +368,13 @@ class CreateRenderPass(TVPaintCreator): "render_layer_instance_id", label="Render Layer", items=render_layers + ), + BoolDef( + "mark_for_review", + label="Review", + default=self.mark_for_review ) ] + def get_instance_attr_defs(self): + return self.get_pre_create_attr_defs() From 4a53dce92ffe996900d2914c58ceaae0caedcb30 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 10 Feb 2023 18:32:59 +0100 Subject: [PATCH 100/483] render layer creator changes group ids of render pass layers on save --- .../tvpaint/plugins/create/create_render.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 3a89608c7c..e5d3fa1a59 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -29,6 +29,9 @@ Todos: Add option to extract marked layers and passes as json output format for AfterEffects. """ + +import collections + from openpype.lib import ( prepare_template_data, EnumDef, @@ -212,6 +215,51 @@ class CreateRenderlayer(TVPaintCreator): ) ] + def update_instances(self, update_list): + self._update_renderpass_groups() + + super().update_instances(update_list) + + def _update_renderpass_groups(self): + render_layer_instances = {} + render_pass_instances = collections.defaultdict(list) + + for instance in self.create_context.instances: + if instance.creator_identifier == CreateRenderPass.identifier: + render_layer_id = ( + instance["creator_attributes"]["render_layer_instance_id"] + ) + render_pass_instances[render_layer_id].append(instance) + elif instance.creator_identifier == self.identifier: + render_layer_instances[instance.id] = instance + + if not render_pass_instances or not render_layer_instances: + return + + layers_data = get_layers_data() + layers_by_name = collections.defaultdict(list) + for layer in layers_data: + layers_by_name[layer["name"]].append(layer) + + george_lines = [] + for render_layer_id, instances in render_pass_instances.items(): + render_layer_inst = render_layer_instances.get(render_layer_id) + if render_layer_inst is None: + continue + group_id = render_layer_inst["creator_attributes"]["group_id"] + layer_names = set() + for instance in instances: + layer_names |= set(instance["layer_names"]) + + for layer_name in layer_names: + george_lines.extend( + f"tv_layercolor \"set\" {layer['layer_id']} {group_id}" + for layer in layers_by_name[layer_name] + if layer["group_id"] != group_id + ) + if george_lines: + execute_george_through_file("\n".join(george_lines)) + class CreateRenderPass(TVPaintCreator): icon = "fa.cube" From c537999ffb6de7e9edfd14f226b44c521c809512 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Feb 2023 14:43:08 +0800 Subject: [PATCH 101/483] correct the renderer name for redshift and update arnold render product --- openpype/hosts/max/api/lib_renderproducts.py | 36 +++++++++++++++++++- openpype/hosts/max/api/lib_rendersettings.py | 22 ++++++++++-- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index e3cccff982..c6432412bf 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -47,7 +47,7 @@ class RenderProducts(object): if ( renderer == "ART_Renderer" or - renderer == "Redshift Renderer" or + renderer == "Redshift_Renderer" or renderer == "V_Ray_6_Hotfix_3" or renderer == "V_Ray_GPU_6_Hotfix_3" or renderer == "Default_Scanline_Renderer" or @@ -59,11 +59,20 @@ class RenderProducts(object): img_fmt) for render_elem in render_elem_list: full_render_list.append(render_elem) + return full_render_list if renderer == "Arnold": + aov_list = self.arnold_render_product(output_file, + startFrame, + endFrame, + img_fmt) + if aov_list: + for aov in aov_list: + full_render_list.append(aov) return full_render_list + def beauty_render_product(self, folder, startFrame, endFrame, fmt): # get the beauty beauty_frame_range = list() @@ -78,6 +87,31 @@ class RenderProducts(object): return beauty_frame_range # TODO: Get the arnold render product + def arnold_render_product(self, folder, startFrame, endFrame, fmt): + """Get all the Arnold AOVs""" + aovs = list() + + amw = rt.MaxtoAOps.AOVsManagerWindow() + aov_mgr = rt.renderers.current.AOVManager + # Check if there is any aov group set in AOV manager + aov_group_num = len(aov_mgr.drivers) + if aov_group_num < 1: + return + for i in range(aov_group_num): + # get the specific AOV group + for aov in aov_mgr.drivers[i].aov_list: + for f in range(startFrame, endFrame): + render_element = "{0}_{1}.{2}.{3}".format(folder, + str(aov.name), + str(f), + fmt) + render_element = render_element.replace("\\", "/") + aovs.append(render_element) + # close the AOVs manager window + amw.close() + + return aovs + def render_elements_product(self, folder, startFrame, endFrame, fmt): """Get all the render element output files. """ render_dirname = list() diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 2324d743eb..bc9b02bc77 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -87,7 +87,7 @@ class RenderSettings(object): if ( renderer == "ART_Renderer" or - renderer == "Redshift Renderer" or + renderer == "Redshift_Renderer" or renderer == "V_Ray_6_Hotfix_3" or renderer == "V_Ray_GPU_6_Hotfix_3" or renderer == "Default_Scanline_Renderer" or @@ -104,9 +104,25 @@ class RenderSettings(object): render_camera = rt.viewport.GetCamera() arv.setOption("Camera", str(render_camera)) - aovmgr = rt.renderers.current.AOVManager - aovmgr.drivers = "#()" + # TODO: add AOVs and extension + img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa + setup_cmd = ( + f""" + amw = MaxtoAOps.AOVsManagerWindow() + amw.close() + aovmgr = renderers.current.AOVManager + aovmgr.drivers = #() + img_fmt = "{img_fmt}" + if img_fmt == "png" then driver = ArnoldPNGDriver() + if img_fmt == "jpg" then driver = ArnoldJPEGDriver() + if img_fmt == "exr" then driver = ArnoldEXRDriver() + if img_fmt == "tif" then driver = ArnoldTIFFDriver() + if img_fmt == "tiff" then driver = ArnoldTIFFDriver() + append aovmgr.drivers driver + aovmgr.drivers[1].aov_list = #() + """) + rt.execute(setup_cmd) arv.close() def render_element_layer(self, dir, width, height, ext): From af8278f67ca073386fbcc8dd8f03efd0226366db Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Feb 2023 14:44:24 +0800 Subject: [PATCH 102/483] hound fix --- openpype/hosts/max/api/lib_renderproducts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index c6432412bf..b54e2513e1 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -72,7 +72,6 @@ class RenderProducts(object): full_render_list.append(aov) return full_render_list - def beauty_render_product(self, folder, startFrame, endFrame, fmt): # get the beauty beauty_frame_range = list() From 61024a476a36ec0dac03dccfa0bbf3a76edabf8d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Feb 2023 15:02:51 +0800 Subject: [PATCH 103/483] chucksize in create_render --- openpype/hosts/max/plugins/create/create_render.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 76c10ca4a9..699fc200d3 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -20,6 +20,8 @@ class CreateRender(plugin.MaxCreator): pre_create_data) # type: CreatedInstance container_name = instance.data.get("instance_node") container = rt.getNodeByName(container_name) + # chuckSize for submitting render + instance_data["chunkSize"] = 10 # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container for obj in sel_obj: From b284ad43c04cf05bb604603d211194526f80971c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Feb 2023 17:44:17 +0800 Subject: [PATCH 104/483] not include py script from the unrelated host --- openpype/hosts/maya/plugins/publish/collect_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index c5fce219fa..0683848c49 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -184,7 +184,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): self.log.info("multipart: {}".format( multipart)) assert exp_files, "no file names were generated, this is bug" - + self.log.info(exp_files) # if we want to attach render to subset, check if we have AOV's # in expectedFiles. If so, raise error as we cannot attach AOV # (considered to be subset on its own) to another subset @@ -319,6 +319,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "renderSetupIncludeLights" ) } + # Collect Deadline url if Deadline module is enabled deadline_settings = ( context.data["system_settings"]["modules"]["deadline"] From 6236922e81b40a7ca0a7ed4d325aa97a0de9361e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 13 Feb 2023 17:45:10 +0800 Subject: [PATCH 105/483] not include py script from the unrelated host --- openpype/hosts/maya/plugins/publish/collect_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index 0683848c49..b1ad3ca58e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -185,6 +185,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): multipart)) assert exp_files, "no file names were generated, this is bug" self.log.info(exp_files) + # if we want to attach render to subset, check if we have AOV's # in expectedFiles. If so, raise error as we cannot attach AOV # (considered to be subset on its own) to another subset From 21b2cbe34830e27fb02d797443e1e5025fffea8c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Feb 2023 15:59:22 +0100 Subject: [PATCH 106/483] better methods propagation --- openpype/hosts/tvpaint/api/plugin.py | 68 +++++++++++++++------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index c57baf7212..d267d87acd 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -21,48 +21,52 @@ from .pipeline import get_current_workfile_context SHARED_DATA_KEY = "openpype.tvpaint.instances" -def _collect_instances(creator): - instances_by_identifier = cache_and_get_instances( - creator, SHARED_DATA_KEY, creator.host.list_instances - ) - for instance_data in instances_by_identifier[creator.identifier]: - instance = CreatedInstance.from_existing(instance_data, creator) - creator._add_instance_to_context(instance) +class TVPaintCreatorCommon: + def _cache_and_get_instances(self): + return cache_and_get_instances( + self, SHARED_DATA_KEY, self.host.list_instances + ) + + def _collect_create_instances(self): + instances_by_identifier = self._cache_and_get_instances() + for instance_data in instances_by_identifier[self.identifier]: + instance = CreatedInstance.from_existing(instance_data, self) + self._add_instance_to_context(instance) -def _update_instances(creator, update_list): - if not update_list: - return + def _update_create_instances(self, update_list): + if not update_list: + return - cur_instances = creator.host.list_instances() - cur_instances_by_id = {} - for instance_data in cur_instances: - instance_id = instance_data.get("instance_id") - if instance_id: - cur_instances_by_id[instance_id] = instance_data + cur_instances = self.host.list_instances() + cur_instances_by_id = {} + for instance_data in cur_instances: + instance_id = instance_data.get("instance_id") + if instance_id: + cur_instances_by_id[instance_id] = instance_data - for instance, changes in update_list: - instance_data = instance.data_to_store() - cur_instance_data = cur_instances_by_id.get(instance.id) - if cur_instance_data is None: - cur_instances.append(instance_data) - continue - for key in set(cur_instance_data) - set(instance_data): - instance_data.pop(key) - instance_data.update(cur_instance_data) - creator.host.write_instances(cur_instances) + for instance, changes in update_list: + instance_data = changes.new_value + cur_instance_data = cur_instances_by_id.get(instance.id) + if cur_instance_data is None: + cur_instances.append(instance_data) + continue + for key in set(cur_instance_data) - set(instance_data): + cur_instance_data.pop(key) + cur_instance_data.update(instance_data) + self.host.write_instances(cur_instances) -class TVPaintCreator(NewCreator): +class TVPaintCreator(NewCreator, TVPaintCreatorCommon): @property def subset_template_family_filter(self): return self.family def collect_instances(self): - _collect_instances(self) + self._collect_create_instances() def update_instances(self, update_list): - _update_instances(self, update_list) + self._update_create_instances(update_list) def remove_instances(self, instances): ids_to_remove = { @@ -129,12 +133,12 @@ class TVPaintCreator(NewCreator): self._add_instance_to_context(new_instance) -class TVPaintAutoCreator(AutoCreator): +class TVPaintAutoCreator(AutoCreator, TVPaintCreatorCommon): def collect_instances(self): - _collect_instances(self) + self._collect_create_instances() def update_instances(self, update_list): - _update_instances(self, update_list) + self._update_create_instances(update_list) class Creator(LegacyCreator): From db7748995617721d7d564d0097f08ab3fecc141f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 13 Feb 2023 17:24:56 +0100 Subject: [PATCH 107/483] added labels to render pass instances --- .../tvpaint/plugins/create/create_render.py | 70 ++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index e5d3fa1a59..8f7ba121c1 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -273,6 +273,35 @@ class CreateRenderPass(TVPaintCreator): # Settings mark_for_review = True + def collect_instances(self): + instances_by_identifier = self._cache_and_get_instances() + render_layers = { + instance_data["instance_id"]: { + "variant": instance_data["variant"], + "template_data": prepare_template_data({ + "renderlayer": instance_data["variant"] + }) + } + for instance_data in ( + instances_by_identifier[CreateRenderlayer.identifier] + ) + } + + for instance_data in instances_by_identifier[self.identifier]: + render_layer_instance_id = ( + instance_data + .get("creator_attributes", {}) + .get("render_layer_instance_id") + ) + render_layer_info = render_layers.get(render_layer_instance_id) + self.update_instance_labels( + instance_data, + render_layer_info["variant"], + render_layer_info["template_data"] + ) + instance = CreatedInstance.from_existing(instance_data, self) + self._add_instance_to_context(instance) + def get_dynamic_data( self, variant, task_name, asset_doc, project_name, host_name, instance ): @@ -283,6 +312,29 @@ class CreateRenderPass(TVPaintCreator): dynamic_data["renderlayer"] = "{renderlayer}" return dynamic_data + def update_instance_labels( + self, instance, render_layer_variant, render_layer_data=None + ): + old_label = instance.get("label") + old_group = instance.get("group") + new_label = None + new_group = None + if render_layer_variant is not None: + if render_layer_data is None: + render_layer_data = prepare_template_data({ + "renderlayer": render_layer_variant + }) + try: + new_label = instance["subset"].format(**render_layer_data) + except (KeyError, ValueError): + pass + + new_group = f"{self.get_group_label()} ({render_layer_variant})" + + instance["label"] = new_label + instance["group"] = new_group + return old_group != new_group or old_label != new_label + def create(self, subset_name, instance_data, pre_create_data): render_layer_instance_id = pre_create_data.get( "render_layer_instance_id" @@ -337,6 +389,8 @@ class CreateRenderPass(TVPaintCreator): **prepare_template_data(subset_name_fill_data) ) self.log.info(f"New subset name is \"{new_subset_name}\".") + instance_data["label"] = new_subset_name + instance_data["group"] = f"{self.get_group_label()} ({render_layer})" instance_data["layer_names"] = list(selected_layer_names) if "creator_attributes" not in instance_data: instance_data["creator_attribtues"] = {} @@ -400,14 +454,14 @@ class CreateRenderPass(TVPaintCreator): ] def get_pre_create_attr_defs(self): - render_layers = [] - for instance in self.create_context.instances: - if instance.creator_identifier == CreateRenderlayer.identifier: - render_layers.append({ - "value": instance.id, - "label": instance.label - }) - + render_layers = [ + { + "value": instance.id, + "label": instance.label + } + for instance in self.create_context.instances + if instance.creator_identifier == CreateRenderlayer.identifier + ] if not render_layers: render_layers.append({"value": None, "label": "N/A"}) From d39dd9ce1de40544cbdf612c6f9e5ff00b126129 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 13 Feb 2023 17:59:40 +0000 Subject: [PATCH 108/483] Correct colorspace when using "View Transform" --- openpype/hosts/maya/api/lib_renderproducts.py | 7 +- openpype/pipeline/colorspace.py | 99 +++++++++---------- openpype/scripts/ocio_wrapper.py | 20 +++- 3 files changed, 70 insertions(+), 56 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 38415c2ae2..4930143fb1 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -46,6 +46,7 @@ import attr from . import lib from . import lib_rendersetup +from openpype.pipeline.colorspace import get_ocio_config_views from maya import cmds, mel @@ -646,7 +647,11 @@ class RenderProductsArnold(ARenderProducts): def _view_transform(): preferences = lib.get_color_management_preferences() - return preferences["view_transform"] + views_data = get_ocio_config_views(preferences["config"]) + view_data = views_data[ + "{}/{}".format(preferences["display"], preferences["view"]) + ] + return view_data["colorspace"] def _raw(): preferences = lib.get_color_management_preferences() diff --git a/openpype/pipeline/colorspace.py b/openpype/pipeline/colorspace.py index cb37b2c4ae..6f68bdc5bf 100644 --- a/openpype/pipeline/colorspace.py +++ b/openpype/pipeline/colorspace.py @@ -198,41 +198,19 @@ def validate_imageio_colorspace_in_config(config_path, colorspace_name): return True -def get_ocio_config_colorspaces(config_path): - """Get all colorspace data - - Wrapper function for aggregating all names and its families. - Families can be used for building menu and submenus in gui. - - Args: - config_path (str): path leading to config.ocio file - - Returns: - dict: colorspace and family in couple - """ - if sys.version_info[0] == 2: - return get_colorspace_data_subprocess(config_path) - - from ..scripts.ocio_wrapper import _get_colorspace_data - return _get_colorspace_data(config_path) - - -def get_colorspace_data_subprocess(config_path): - """Get colorspace data via subprocess +def get_data_subprocess(config_path, data_type): + """Get data via subprocess Wrapper for Python 2 hosts. Args: config_path (str): path leading to config.ocio file - - Returns: - dict: colorspace and family in couple """ with _make_temp_json_file() as tmp_json_path: # Prepare subprocess arguments args = [ "run", get_ocio_config_script_path(), - "config", "get_colorspace", + "config", data_type, "--in_path", config_path, "--out_path", tmp_json_path @@ -251,6 +229,47 @@ def get_colorspace_data_subprocess(config_path): return json.loads(return_json_data) +def compatible_python(): + """Only 3.9 or higher can directly use PyOpenColorIO in ocio_wrapper""" + compatible = False + if sys.version[0] == 3 and sys.version[1] >= 9: + compatible = True + return compatible + + +def get_ocio_config_colorspaces(config_path): + """Get all colorspace data + + Wrapper function for aggregating all names and its families. + Families can be used for building menu and submenus in gui. + + Args: + config_path (str): path leading to config.ocio file + + Returns: + dict: colorspace and family in couple + """ + if compatible_python(): + from ..scripts.ocio_wrapper import _get_colorspace_data + return _get_colorspace_data(config_path) + else: + return get_colorspace_data_subprocess(config_path) + + +def get_colorspace_data_subprocess(config_path): + """Get colorspace data via subprocess + + Wrapper for Python 2 hosts. + + Args: + config_path (str): path leading to config.ocio file + + Returns: + dict: colorspace and family in couple + """ + return get_data_subprocess(config_path, "get_colorspace") + + def get_ocio_config_views(config_path): """Get all viewer data @@ -263,12 +282,12 @@ def get_ocio_config_views(config_path): Returns: dict: `display/viewer` and viewer data """ - if sys.version_info[0] == 2: + if compatible_python(): + from ..scripts.ocio_wrapper import _get_views_data + return _get_views_data(config_path) + else: return get_views_data_subprocess(config_path) - from ..scripts.ocio_wrapper import _get_views_data - return _get_views_data(config_path) - def get_views_data_subprocess(config_path): """Get viewers data via subprocess @@ -281,27 +300,7 @@ def get_views_data_subprocess(config_path): Returns: dict: `display/viewer` and viewer data """ - with _make_temp_json_file() as tmp_json_path: - # Prepare subprocess arguments - args = [ - "run", get_ocio_config_script_path(), - "config", "get_views", - "--in_path", config_path, - "--out_path", tmp_json_path - - ] - log.info("Executing: {}".format(" ".join(args))) - - process_kwargs = { - "logger": log, - "env": {} - } - - run_openpype_process(*args, **process_kwargs) - - # return all colorspaces - return_json_data = open(tmp_json_path).read() - return json.loads(return_json_data) + return get_data_subprocess(config_path, "get_views") def get_imageio_config( diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index 0685b2e52a..aa9e11c841 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -157,11 +157,21 @@ def _get_views_data(config_path): config = ocio.Config().CreateFromFile(str(config_path)) - return { - f"{d}/{v}": {"display": d, "view": v} - for d in config.getDisplays() - for v in config.getViews(d) - } + data = {} + for display in config.getDisplays(): + for view in config.getViews(display): + colorspace = config.getDisplayViewColorSpaceName(display, view) + # Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views + if colorspace == "": + colorspace = display + + data[f"{display}/{view}"] = { + "display": display, + "view": view, + "colorspace": colorspace + } + + return data if __name__ == '__main__': From e21323c78841902bf66f8670b1890a7cde11773c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 13 Feb 2023 18:01:38 +0000 Subject: [PATCH 109/483] Hound --- openpype/scripts/ocio_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/scripts/ocio_wrapper.py b/openpype/scripts/ocio_wrapper.py index aa9e11c841..16558642c6 100644 --- a/openpype/scripts/ocio_wrapper.py +++ b/openpype/scripts/ocio_wrapper.py @@ -161,7 +161,7 @@ def _get_views_data(config_path): for display in config.getDisplays(): for view in config.getViews(display): colorspace = config.getDisplayViewColorSpaceName(display, view) - # Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views + # Special token. See https://opencolorio.readthedocs.io/en/latest/guides/authoring/authoring.html#shared-views # noqa if colorspace == "": colorspace = display From 1505478cbdd7def0ab2040d65d09be823b39d958 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 14 Feb 2023 21:05:54 +0800 Subject: [PATCH 110/483] add general 3dsmax settings and add max families into submit publish job --- .../hosts/max/plugins/create/create_render.py | 2 - .../max/plugins/publish/collect_render.py | 22 ++++++-- .../plugins/publish/submit_3dmax_deadline.py | 41 +++++++++++--- .../plugins/publish/submit_publish_job.py | 4 +- .../defaults/project_settings/deadline.json | 11 ++++ .../schema_project_deadline.json | 54 +++++++++++++++++++ 6 files changed, 120 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 699fc200d3..76c10ca4a9 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -20,8 +20,6 @@ class CreateRender(plugin.MaxCreator): pre_create_data) # type: CreatedInstance container_name = instance.data.get("instance_node") container = rt.getNodeByName(container_name) - # chuckSize for submitting render - instance_data["chunkSize"] = 10 # TODO: Disable "Add to Containers?" Panel # parent the selected cameras into the container for obj in sel_obj: diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 857ac88ea6..5089f107d3 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -6,7 +6,7 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import legacy_io from openpype.hosts.max.api.lib_renderproducts import RenderProducts - +from openpype.client import get_last_version_by_subset_name class CollectRender(pyblish.api.InstancePlugin): """Collect Render for Deadline""" @@ -30,6 +30,21 @@ class CollectRender(pyblish.api.InstancePlugin): folder = folder.replace("\\", "/") imgFormat = RenderProducts().image_format() + project_name = context.data["projectName"] + asset_doc = context.data["assetEntity"] + asset_id = asset_doc["_id"] + version_doc = get_last_version_by_subset_name(project_name, + instance.name, + asset_id) + + self.log.debug("version_doc: {0}".format(version_doc)) + version_int = 1 + if version_doc: + version_int += int(version_doc["name"]) + + self.log.debug(f"Setting {version_int} to context.") + context.data["version"] = version_int + # setup the plugin as 3dsmax for the internal renderer data = { "subset": instance.name, @@ -39,10 +54,11 @@ class CollectRender(pyblish.api.InstancePlugin): "family": 'maxrender', "families": ['maxrender'], "source": filepath, - "files": render_layer_files, + "expectedFiles": render_layer_files, "plugin": "3dsmax", "frameStart": context.data['frameStart'], - "frameEnd": context.data['frameEnd'] + "frameEnd": context.data['frameEnd'], + "version": version_int } self.log.info("data: {0}".format(data)) instance.data.update(data) diff --git a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py index 88834e4a91..056e74cf3f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py @@ -20,6 +20,12 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): hosts = ["max"] families = ["maxrender"] targets = ["local"] + use_published = True + priority = 50 + chunk_size = 1 + group = None + deadline_pool = None + deadline_pool_secondary = None def process(self, instance): context = instance.context @@ -34,6 +40,24 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): start=int(instance.data["frameStart"]), end=int(instance.data["frameEnd"]) ) + if self.use_published: + for item in context: + if "workfile" in item.data["families"]: + msg = "Workfile (scene) must be published along" + assert item.data["publish"] is True, msg + + template_data = item.data.get("anatomyData") + rep = item.data.get("representations")[0].get("name") + template_data["representation"] = rep + template_data["ext"] = rep + template_data["comment"] = None + anatomy_filled = context.data["anatomy"].format(template_data) + template_filled = anatomy_filled["publish"]["path"] + filepath = os.path.normpath(template_filled) + + self.log.info( + "Using published scene for render {}".format(filepath) + ) payload = { "JobInfo": { @@ -47,15 +71,17 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): "UserName": deadline_user, "Plugin": instance.data["plugin"], - "Pool": instance.data.get("primaryPool"), - "secondaryPool": instance.data.get("secondaryPool"), + "Group": self.group, + "Pool": self.deadline_pool, + "secondaryPool": self.deadline_pool_secondary, "Frames": frames, - "ChunkSize": instance.data.get("chunkSize", 10), + "ChunkSize": self.chunk_size, + "Priority": instance.data.get("priority", self.priority), "Comment": comment }, "PluginInfo": { # Input - "SceneFile": instance.data["source"], + "SceneFile": filepath, "Version": "2023", "SaveFile": True, # Mandatory for Deadline @@ -94,7 +120,7 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): # frames from Deadline Monitor output_data = {} # need to be fixed - for i, filepath in enumerate(instance.data["files"]): + for i, filepath in enumerate(instance.data["expectedFiles"]): dirname = os.path.dirname(filepath) fname = os.path.basename(filepath) output_data["OutputDirectory%d" % i] = dirname.replace("\\", "/") @@ -129,9 +155,10 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): response = requests.post(url, json=payload) if not response.ok: raise Exception(response.text) - # Store output dir for unified publisher (filesequence) - expected_files = instance.data["files"] + # Store output dir for unified publisher (expectedFilesequence) + expected_files = instance.data["expectedFiles"] self.log.info("exp:{}".format(expected_files)) output_dir = os.path.dirname(expected_files[0]) + instance.data["toBeRenderedOn"] = "deadline" instance.data["outputDir"] = output_dir instance.data["deadlineSubmissionJob"] = response.json() diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 7e39a644a2..71934aef93 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -117,10 +117,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): deadline_plugin = "OpenPype" targets = ["local"] - hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"] + hosts = ["fusion", "max", "maya", "nuke", "celaction", "aftereffects", "harmony"] families = ["render.farm", "prerender.farm", - "renderlayer", "imagesequence", "vrayscene"] + "renderlayer", "imagesequence", "maxrender", "vrayscene"] aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index ceb0b2e39a..0fab284c66 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -35,6 +35,17 @@ "pluginInfo": {}, "scene_patches": [] }, + "MaxSubmitRenderDeadline": { + "enabled": true, + "optional": false, + "active": true, + "use_published": true, + "priority": 50, + "chunk_size": 10, + "group": "none", + "deadline_pool": "", + "deadline_pool_secondary": "" + }, "NukeSubmitDeadline": { "enabled": true, "optional": false, 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 08a505bd47..afefd3266a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -198,6 +198,60 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "MaxSubmitRenderDeadline", + "label": "3dsMax 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": "Chunk Size" + }, + { + "type": "text", + "key": "group", + "label": "Group Name" + }, + { + "type": "text", + "key": "deadline_pool", + "label": "Deadline pool" + }, + { + "type": "text", + "key": "deadline_pool_secondary", + "label": "Deadline pool (secondary)" + } + ] + }, { "type": "dict", "collapsible": true, From 0dbf111d05843996f64a789e1a938a84ebf585bd Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 14 Feb 2023 21:15:46 +0800 Subject: [PATCH 111/483] hound fix --- openpype/hosts/max/plugins/publish/collect_render.py | 1 + .../modules/deadline/plugins/publish/submit_3dmax_deadline.py | 3 ++- .../modules/deadline/plugins/publish/submit_publish_job.py | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 5089f107d3..55391d40e8 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -8,6 +8,7 @@ from openpype.pipeline import legacy_io from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name + class CollectRender(pyblish.api.InstancePlugin): """Collect Render for Deadline""" diff --git a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py index 056e74cf3f..dec951da7a 100644 --- a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py @@ -51,7 +51,8 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): template_data["representation"] = rep template_data["ext"] = rep template_data["comment"] = None - anatomy_filled = context.data["anatomy"].format(template_data) + anatomy_data = context.data["anatomy"] + anatomy_filled = anatomy_data.format(template_data) template_filled = anatomy_filled["publish"]["path"] filepath = os.path.normpath(template_filled) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 71934aef93..b70301ab7e 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -117,7 +117,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): deadline_plugin = "OpenPype" targets = ["local"] - hosts = ["fusion", "max", "maya", "nuke", "celaction", "aftereffects", "harmony"] + hosts = ["fusion", "max", "maya", "nuke", + "celaction", "aftereffects", "harmony"] families = ["render.farm", "prerender.farm", "renderlayer", "imagesequence", "maxrender", "vrayscene"] From 714454d7300cf2e067785c888dadddb415f88baf Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Feb 2023 14:54:25 +0100 Subject: [PATCH 112/483] OP-4643 - fix logging Wrong variable used --- openpype/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index dcb43d7fa2..0f6dacba18 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -169,7 +169,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "Skipped representation. All output definitions from" " selected profile does not match to representation's" " custom tags. \"{}\"" - ).format(str(tags))) + ).format(str(custom_tags))) continue outputs_per_representations.append((repre, outputs)) From 2b5e4bc14e7a4241ee97a3bbb0716b148ec13513 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Feb 2023 15:14:14 +0100 Subject: [PATCH 113/483] OP-4643 - allow new repre to stay One might want to delete outputs with 'delete' tag, but repre must stay there at least until extract_review. More universal new tag might be created for this. --- openpype/plugins/publish/extract_color_transcode.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 93ee1ec44d..4a03e623fd 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -161,15 +161,17 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["custom_tags"].extend(custom_tags) # Add additional tags from output definition to representation + if not new_repre.get("tags"): + new_repre["tags"] = [] for tag in output_def["tags"]: - if not new_repre.get("tags"): - new_repre["tags"] = [] if tag not in new_repre["tags"]: new_repre["tags"].append(tag) if tag == "review": added_review = True + new_repre["tags"].append("newly_added") + instance.data["representations"].append(new_repre) added_representations = True @@ -179,6 +181,12 @@ class ExtractOIIOTranscode(publish.Extractor): for repre in tuple(instance.data["representations"]): tags = repre.get("tags") or [] + # TODO implement better way, for now do not delete new repre + # new repre might have 'delete' tag to removed, but it first must + # be there for review to be created + if "newly_added" in tags: + tags.remove("newly_added") + continue if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) From 2621016015b0b2c3e36f36d4c3853851d5154256 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 14 Feb 2023 16:27:43 +0000 Subject: [PATCH 114/483] Template for config. --- .../modules/deadline/plugins/publish/submit_publish_job.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 02aa1043d1..89a4e5d377 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -552,7 +552,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "colorspace": colorspace, "config": { "path": additional_data["colorspaceConfig"], - "template": "" + "template": additional_data["colorspaceTemplate"] }, "display": additional_data["display"], "view": additional_data["view"] @@ -920,7 +920,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "renderProducts": instance.data["renderProducts"], "colorspaceConfig": instance.data["colorspaceConfig"], "display": instance.data["colorspaceDisplay"], - "view": instance.data["colorspaceView"] + "view": instance.data["colorspaceView"], + "colorspaceTemplate": instance.data["colorspaceConfig"].replace( + context.data["anatomy"].roots["work"], "{root[work]}" + ) } if isinstance(data.get("expectedFiles")[0], dict): From bd8f68c6e9574431886a2dd31b8c1620b38a941f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 10:37:10 +0100 Subject: [PATCH 115/483] changed collect workfile to instance plugin --- .../plugins/publish/collect_workfile.py | 57 ++++--------------- 1 file changed, 10 insertions(+), 47 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py index 8c7c8c3899..a3449663f8 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile.py @@ -2,17 +2,15 @@ import os import json import pyblish.api -from openpype.client import get_asset_by_name -from openpype.pipeline import legacy_io -from openpype.pipeline.create import get_subset_name - -class CollectWorkfile(pyblish.api.ContextPlugin): +class CollectWorkfile(pyblish.api.InstancePlugin): label = "Collect Workfile" order = pyblish.api.CollectorOrder - 0.4 hosts = ["tvpaint"] + families = ["workfile"] - def process(self, context): + def process(self, instance): + context = instance.context current_file = context.data["currentFile"] self.log.info( @@ -21,49 +19,14 @@ class CollectWorkfile(pyblish.api.ContextPlugin): dirpath, filename = os.path.split(current_file) basename, ext = os.path.splitext(filename) - instance = context.create_instance(name=basename) - # Project name from workfile context - project_name = context.data["workfile_context"]["project"] - - # Get subset name of workfile instance - # Collect asset doc to get asset id - # - not sure if it's good idea to require asset id in - # get_subset_name? - family = "workfile" - asset_name = context.data["workfile_context"]["asset"] - asset_doc = get_asset_by_name(project_name, asset_name) - - # Host name from environment variable - host_name = os.environ["AVALON_APP"] - # Use empty variant value - variant = "" - task_name = legacy_io.Session["AVALON_TASK"] - subset_name = get_subset_name( - family, - variant, - task_name, - asset_doc, - project_name, - host_name, - project_settings=context.data["project_settings"] - ) - - # Create Workfile instance - instance.data.update({ - "subset": subset_name, - "asset": context.data["asset"], - "label": subset_name, - "publish": True, - "family": "workfile", - "families": ["workfile"], - "representations": [{ - "name": ext.lstrip("."), - "ext": ext.lstrip("."), - "files": filename, - "stagingDir": dirpath - }] + instance.data["representations"].append({ + "name": ext.lstrip("."), + "ext": ext.lstrip("."), + "files": filename, + "stagingDir": dirpath }) + self.log.info("Collected workfile instance: {}".format( json.dumps(instance.data, indent=4) )) From d26d9083ce3931ac521ee66a9dd526cf693f5adb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 10:43:20 +0100 Subject: [PATCH 116/483] fix how context information is returned --- openpype/hosts/tvpaint/api/pipeline.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 6a729e39c3..3794bf2e24 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -124,8 +124,11 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): if not context: return get_global_context() + if "project_name" in context: + return context + # This is legacy way how context was stored return { - "project_name": context["project"], + "project_name": context.get("project"), "asset_name": context.get("asset"), "task_name": context.get("task") } From 319c9c60eaae1748cb6662dd9e1c9922d01f17bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 10:43:39 +0100 Subject: [PATCH 117/483] added autocreator for scene review --- .../tvpaint/plugins/create/create_review.py | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 openpype/hosts/tvpaint/plugins/create/create_review.py diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py new file mode 100644 index 0000000000..84127eb9b7 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -0,0 +1,64 @@ +from openpype.client import get_asset_by_name +from openpype.pipeline import CreatedInstance +from openpype.hosts.tvpaint.api.plugin import TVPaintAutoCreator + + +class TVPaintReviewCreator(TVPaintAutoCreator): + family = "review" + identifier = "scene.review" + label = "Review" + + default_variant = "Main" + + def create(self): + existing_instance = None + for instance in self.create_context.instances: + if instance.creator_identifier == self.identifier: + existing_instance = instance + break + + context = self.host.get_current_context() + host_name = self.host.name + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] + + if existing_instance is None: + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + self.default_variant, + task_name, + asset_doc, + project_name, + host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": self.default_variant + } + + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + instances_data = self.host.list_instances() + instances_data.append(new_instance.data_to_store()) + self.host.write_instances(instances_data) + self._add_instance_to_context(new_instance) + + elif ( + existing_instance["asset"] != asset_name + or existing_instance["task"] != task_name + ): + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + existing_instance["variant"], + task_name, + asset_doc, + project_name, + host_name, + existing_instance + ) + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name + existing_instance["subset"] = subset_name From 8bad64e59f2fb2b10a912d57e451531da8099f7b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 10:43:48 +0100 Subject: [PATCH 118/483] change lael of workfile creator --- openpype/hosts/tvpaint/plugins/create/create_workfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index 3e5cd86852..e421fbc3f8 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -6,6 +6,7 @@ from openpype.hosts.tvpaint.api.plugin import TVPaintAutoCreator class TVPaintWorkfileCreator(TVPaintAutoCreator): family = "workfile" identifier = "workfile" + label = "Workfile" default_variant = "Main" From dae4094d8a3c3ec00f60c13e344a4e211b105ce1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 10:44:50 +0100 Subject: [PATCH 119/483] change how instance frames are calculated --- .../publish/collect_instance_frames.py | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py b/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py index d5b79758ad..5eb702a1da 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py @@ -1,37 +1,34 @@ import pyblish.api -class CollectOutputFrameRange(pyblish.api.ContextPlugin): +class CollectOutputFrameRange(pyblish.api.InstancePlugin): """Collect frame start/end from context. When instances are collected context does not contain `frameStart` and `frameEnd` keys yet. They are collected in global plugin `CollectContextEntities`. """ + label = "Collect output frame range" - order = pyblish.api.CollectorOrder + order = pyblish.api.CollectorOrder + 0.4999 hosts = ["tvpaint"] + families = ["review", "render"] - def process(self, context): - for instance in context: - frame_start = instance.data.get("frameStart") - frame_end = instance.data.get("frameEnd") - if frame_start is not None and frame_end is not None: - self.log.debug( - "Instance {} already has set frames {}-{}".format( - str(instance), frame_start, frame_end - ) - ) - return + def process(self, instance): + asset_doc = instance.data.get("assetEntity") + if not asset_doc: + return - frame_start = context.data.get("frameStart") - frame_end = context.data.get("frameEnd") + context = instance.context - instance.data["frameStart"] = frame_start - instance.data["frameEnd"] = frame_end - - self.log.info( - "Set frames {}-{} on instance {} ".format( - frame_start, frame_end, str(instance) - ) + frame_start = asset_doc["data"]["frameStart"] + frame_end = frame_start + ( + context.data["sceneMarkOut"] - context.data["sceneMarkIn"] + ) + instance.data["frameStart"] = frame_start + instance.data["frameEnd"] = frame_end + self.log.info( + "Set frames {}-{} on instance {} ".format( + frame_start, frame_end, instance.data["subset"] ) + ) From 71d9ceb91e5eddc9daa82da27cb21d55d04e3ffb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 10:45:57 +0100 Subject: [PATCH 120/483] fix collect workfile data --- .../plugins/publish/collect_workfile_data.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py index 8fe71a4a46..95a5cd77bd 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -65,9 +65,9 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): # Collect and store current context to have reference current_context = { - "project": legacy_io.Session["AVALON_PROJECT"], - "asset": legacy_io.Session["AVALON_ASSET"], - "task": legacy_io.Session["AVALON_TASK"] + "project_name": context.data["projectName"], + "asset_name": context.data["asset"], + "task_name": context.data["task"] } context.data["previous_context"] = current_context self.log.debug("Current context is: {}".format(current_context)) @@ -76,25 +76,31 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): self.log.info("Collecting workfile context") workfile_context = get_current_workfile_context() + if "project" in workfile_context: + workfile_context = { + "project_name": workfile_context.get("project"), + "asset_name": workfile_context.get("asset"), + "task_name": workfile_context.get("task"), + } # Store workfile context to pyblish context context.data["workfile_context"] = workfile_context if workfile_context: # Change current context with context from workfile key_map = ( - ("AVALON_ASSET", "asset"), - ("AVALON_TASK", "task") + ("AVALON_ASSET", "asset_name"), + ("AVALON_TASK", "task_name") ) for env_key, key in key_map: legacy_io.Session[env_key] = workfile_context[key] os.environ[env_key] = workfile_context[key] self.log.info("Context changed to: {}".format(workfile_context)) - asset_name = workfile_context["asset"] - task_name = workfile_context["task"] + asset_name = workfile_context["asset_name"] + task_name = workfile_context["task_name"] else: - asset_name = current_context["asset"] - task_name = current_context["task"] + asset_name = current_context["asset_name"] + task_name = current_context["task_name"] # Handle older workfiles or workfiles without metadata self.log.warning(( "Workfile does not contain information about context." @@ -103,6 +109,7 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): # Store context asset name context.data["asset"] = asset_name + context.data["task"] = task_name self.log.info( "Context is set to Asset: \"{}\" and Task: \"{}\"".format( asset_name, task_name From b3e34fce324603cf955f3fcf8dca795995796d98 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 10:46:17 +0100 Subject: [PATCH 121/483] fix few validators --- .../publish/validate_render_pass_group.py | 1 - .../publish/validate_workfile_metadata.py | 17 +++++++++++++---- .../publish/validate_workfile_project_name.py | 7 +++---- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_render_pass_group.py b/openpype/hosts/tvpaint/plugins/publish/validate_render_pass_group.py index 0fbfca6c56..2a3173c698 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_render_pass_group.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_render_pass_group.py @@ -85,6 +85,5 @@ class ValidateLayersGroup(pyblish.api.InstancePlugin): ), "expected_group": correct_group["name"], "layer_names": ", ".join(invalid_layer_names) - } ) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py index d66ae50c60..b38231e208 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_metadata.py @@ -1,5 +1,9 @@ import pyblish.api -from openpype.pipeline import PublishXmlValidationError, registered_host +from openpype.pipeline import ( + PublishXmlValidationError, + PublishValidationError, + registered_host, +) class ValidateWorkfileMetadataRepair(pyblish.api.Action): @@ -27,13 +31,18 @@ class ValidateWorkfileMetadata(pyblish.api.ContextPlugin): actions = [ValidateWorkfileMetadataRepair] - required_keys = {"project", "asset", "task"} + required_keys = {"project_name", "asset_name", "task_name"} def process(self, context): workfile_context = context.data["workfile_context"] if not workfile_context: - raise AssertionError( - "Current workfile is missing whole metadata about context." + raise PublishValidationError( + "Current workfile is missing whole metadata about context.", + "Missing context", + ( + "Current workfile is missing metadata about task." + " To fix this issue save the file using Workfiles tool." + ) ) missing_keys = [] diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py index 0f25f2f7be..2ed5afa11c 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_workfile_project_name.py @@ -1,4 +1,3 @@ -import os import pyblish.api from openpype.pipeline import PublishXmlValidationError @@ -16,15 +15,15 @@ class ValidateWorkfileProjectName(pyblish.api.ContextPlugin): def process(self, context): workfile_context = context.data.get("workfile_context") # If workfile context is missing than project is matching to - # `AVALON_PROJECT` value for 100% + # global project if not workfile_context: self.log.info( "Workfile context (\"workfile_context\") is not filled." ) return - workfile_project_name = workfile_context["project"] - env_project_name = os.environ["AVALON_PROJECT"] + workfile_project_name = workfile_context["project_name"] + env_project_name = context.data["projectName"] if workfile_project_name == env_project_name: self.log.info(( "Both workfile project and environment project are same. {}" From 8551b1f1b3969ef1fb2db815883169175dd1b0ce Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 10:59:06 +0100 Subject: [PATCH 122/483] change how extract sequence works --- .../tvpaint/plugins/publish/extract_sequence.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index 78074f720c..f2856c72a9 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -6,6 +6,7 @@ from PIL import Image import pyblish.api +from openpype.pipeline.publish import KnownPublishError from openpype.hosts.tvpaint.api.lib import ( execute_george, execute_george_through_file, @@ -24,8 +25,7 @@ from openpype.hosts.tvpaint.lib import ( class ExtractSequence(pyblish.api.Extractor): label = "Extract Sequence" hosts = ["tvpaint"] - families = ["review", "renderPass", "renderLayer", "renderScene"] - families_to_review = ["review"] + families = ["review", "render"] # Modifiable with settings review_bg = [255, 255, 255, 255] @@ -136,7 +136,7 @@ class ExtractSequence(pyblish.api.Extractor): # Fill tags and new families from project settings tags = [] - if family_lowered in self.families_to_review: + if family_lowered == "review": tags.append("review") # Sequence of one frame @@ -162,10 +162,6 @@ class ExtractSequence(pyblish.api.Extractor): instance.data["representations"].append(new_repre) - if family_lowered in ("renderpass", "renderlayer", "renderscene"): - # Change family to render - instance.data["family"] = "render" - if not thumbnail_fullpath: return @@ -259,7 +255,7 @@ class ExtractSequence(pyblish.api.Extractor): output_filepaths_by_frame_idx[frame_idx] = filepath if not os.path.exists(filepath): - raise AssertionError( + raise KnownPublishError( "Output was not rendered. File was not found {}".format( filepath ) From ff7b3004ad953253afbc8e3a9640aa99a24f1c47 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 11:00:10 +0100 Subject: [PATCH 123/483] added validator for duplicated usage of render layer groups --- .../help/validate_render_layer_group.xml | 18 +++++ .../publish/validate_render_layer_group.py | 74 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 openpype/hosts/tvpaint/plugins/publish/help/validate_render_layer_group.xml create mode 100644 openpype/hosts/tvpaint/plugins/publish/validate_render_layer_group.py diff --git a/openpype/hosts/tvpaint/plugins/publish/help/validate_render_layer_group.xml b/openpype/hosts/tvpaint/plugins/publish/help/validate_render_layer_group.xml new file mode 100644 index 0000000000..a95387356f --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/help/validate_render_layer_group.xml @@ -0,0 +1,18 @@ + + + +Overused Color group +## One Color group is used by multiple Render Layers + +Single color group used by multiple Render Layers would cause clashes of rendered TVPaint layers. The same layers would be used for output files of both groups. + +### Missing layer names + +{groups_information} + +### How to repair? + +Refresh, go to 'Publish' tab and go through Render Layers and change their groups to not clash each other. If you reach limit of TVPaint color groups there is nothing you can do about it to fix the issue. + + + diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_render_layer_group.py b/openpype/hosts/tvpaint/plugins/publish/validate_render_layer_group.py new file mode 100644 index 0000000000..bb0a9a4ffe --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/validate_render_layer_group.py @@ -0,0 +1,74 @@ +import collections +import pyblish.api +from openpype.pipeline import PublishXmlValidationError + + +class ValidateRenderLayerGroups(pyblish.api.ContextPlugin): + """Validate group ids of renderLayer subsets. + + Validate that there are not 2 render layers using the same group. + """ + + label = "Validate Render Layers Group" + order = pyblish.api.ValidatorOrder + 0.1 + + def process(self, context): + # Prepare layers + render_layers_by_group_id = collections.defaultdict(list) + for instance in context: + families = instance.data.get("families") + if not families or "renderLayer" not in families: + continue + + group_id = instance.data["creator_attributes"]["group_id"] + render_layers_by_group_id[group_id].append(instance) + + duplicated_instances = [] + for group_id, instances in render_layers_by_group_id.items(): + if len(instances) > 1: + duplicated_instances.append((group_id, instances)) + + if not duplicated_instances: + return + + # Exception message preparations + groups_data = context.data["groupsData"] + groups_by_id = { + group["group_id"]: group + for group in groups_data + } + + per_group_msgs = [] + groups_information_lines = [] + for group_id, instances in duplicated_instances: + group = groups_by_id[group_id] + group_label = "Group \"{}\" ({})".format( + group["name"], + group["group_id"], + ) + line_join_subset_names = "\n".join([ + f" - {instance['subset']}" + for instance in instances + ]) + joined_subset_names = ", ".join([ + f"\"{instance['subset']}\"" + for instance in instances + ]) + per_group_msgs.append( + "{} < {} >".format(group_label, joined_subset_names) + ) + groups_information_lines.append( + "{}\n{}".format(group_label, line_join_subset_names) + ) + + # Raise an error + raise PublishXmlValidationError( + self, + ( + "More than one Render Layer is using the same TVPaint" + " group color. {}" + ).format(" | ".join(per_group_msgs)), + formatting_data={ + "groups_information": "\n".join(groups_information_lines) + } + ) From b405cac45cc082cfd30e735a0b8e8ee0859aae74 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 11:07:59 +0100 Subject: [PATCH 124/483] implemented new collection of render instances --- .../plugins/publish/collect_instances.py | 280 ------------------ .../publish/collect_render_instances.py | 87 ++++++ 2 files changed, 87 insertions(+), 280 deletions(-) delete mode 100644 openpype/hosts/tvpaint/plugins/publish/collect_instances.py create mode 100644 openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_instances.py deleted file mode 100644 index ae1326a5bd..0000000000 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instances.py +++ /dev/null @@ -1,280 +0,0 @@ -import json -import copy -import pyblish.api - -from openpype.client import get_asset_by_name -from openpype.pipeline import legacy_io -from openpype.pipeline.create import get_subset_name - - -class CollectInstances(pyblish.api.ContextPlugin): - label = "Collect Instances" - order = pyblish.api.CollectorOrder - 0.4 - hosts = ["tvpaint"] - - def process(self, context): - workfile_instances = context.data["workfileInstances"] - - self.log.debug("Collected ({}) instances:\n{}".format( - len(workfile_instances), - json.dumps(workfile_instances, indent=4) - )) - - filtered_instance_data = [] - # Backwards compatibility for workfiles that already have review - # instance in metadata. - review_instance_exist = False - for instance_data in workfile_instances: - family = instance_data["family"] - if family == "review": - review_instance_exist = True - - elif family not in ("renderPass", "renderLayer"): - self.log.info("Unknown family \"{}\". Skipping {}".format( - family, json.dumps(instance_data, indent=4) - )) - continue - - filtered_instance_data.append(instance_data) - - # Fake review instance if review was not found in metadata families - if not review_instance_exist: - filtered_instance_data.append( - self._create_review_instance_data(context) - ) - - for instance_data in filtered_instance_data: - instance_data["fps"] = context.data["sceneFps"] - - # Conversion from older instances - # - change 'render_layer' to 'renderlayer' - render_layer = instance_data.get("instance_data") - if not render_layer: - # Render Layer has only variant - if instance_data["family"] == "renderLayer": - render_layer = instance_data.get("variant") - - # Backwards compatibility for renderPasses - elif "render_layer" in instance_data: - render_layer = instance_data["render_layer"] - - if render_layer: - instance_data["renderlayer"] = render_layer - - # Store workfile instance data to instance data - instance_data["originData"] = copy.deepcopy(instance_data) - # Global instance data modifications - # Fill families - family = instance_data["family"] - families = [family] - if family != "review": - families.append("review") - # Add `review` family for thumbnail integration - instance_data["families"] = families - - # Instance name - subset_name = instance_data["subset"] - name = instance_data.get("name", subset_name) - instance_data["name"] = name - instance_data["label"] = "{} [{}-{}]".format( - name, - context.data["sceneMarkIn"] + 1, - context.data["sceneMarkOut"] + 1 - ) - - active = instance_data.get("active", True) - instance_data["active"] = active - instance_data["publish"] = active - # Add representations key - instance_data["representations"] = [] - - # Different instance creation based on family - instance = None - if family == "review": - # Change subset name of review instance - - # Project name from workfile context - project_name = context.data["workfile_context"]["project"] - - # Collect asset doc to get asset id - # - not sure if it's good idea to require asset id in - # get_subset_name? - asset_name = context.data["workfile_context"]["asset"] - asset_doc = get_asset_by_name(project_name, asset_name) - - # Host name from environment variable - host_name = context.data["hostName"] - # Use empty variant value - variant = "" - task_name = legacy_io.Session["AVALON_TASK"] - new_subset_name = get_subset_name( - family, - variant, - task_name, - asset_doc, - project_name, - host_name, - project_settings=context.data["project_settings"] - ) - instance_data["subset"] = new_subset_name - - instance = context.create_instance(**instance_data) - - instance.data["layers"] = copy.deepcopy( - context.data["layersData"] - ) - - elif family == "renderLayer": - instance = self.create_render_layer_instance( - context, instance_data - ) - elif family == "renderPass": - instance = self.create_render_pass_instance( - context, instance_data - ) - - if instance is None: - continue - - any_visible = False - for layer in instance.data["layers"]: - if layer["visible"]: - any_visible = True - break - - instance.data["publish"] = any_visible - - self.log.debug("Created instance: {}\n{}".format( - instance, json.dumps(instance.data, indent=4) - )) - - def _create_review_instance_data(self, context): - """Fake review instance data.""" - - return { - "family": "review", - "asset": context.data["asset"], - # Dummy subset name - "subset": "reviewMain" - } - - def create_render_layer_instance(self, context, instance_data): - name = instance_data["name"] - # Change label - subset_name = instance_data["subset"] - - # Backwards compatibility - # - subset names were not stored as final subset names during creation - if "variant" not in instance_data: - instance_data["label"] = "{}_Beauty".format(name) - - # Change subset name - # Final family of an instance will be `render` - new_family = "render" - task_name = legacy_io.Session["AVALON_TASK"] - new_subset_name = "{}{}_{}_Beauty".format( - new_family, task_name.capitalize(), name - ) - instance_data["subset"] = new_subset_name - self.log.debug("Changed subset name \"{}\"->\"{}\"".format( - subset_name, new_subset_name - )) - - # Get all layers for the layer - layers_data = context.data["layersData"] - group_id = instance_data["group_id"] - group_layers = [] - for layer in layers_data: - if layer["group_id"] == group_id: - group_layers.append(layer) - - if not group_layers: - # Should be handled here? - self.log.warning(( - f"Group with id {group_id} does not contain any layers." - f" Instance \"{name}\" not created." - )) - return None - - instance_data["layers"] = group_layers - - return context.create_instance(**instance_data) - - def create_render_pass_instance(self, context, instance_data): - pass_name = instance_data["pass"] - self.log.info( - "Creating render pass instance. \"{}\"".format(pass_name) - ) - # Change label - render_layer = instance_data["renderlayer"] - - # Backwards compatibility - # - subset names were not stored as final subset names during creation - if "variant" not in instance_data: - instance_data["label"] = "{}_{}".format(render_layer, pass_name) - # Change subset name - # Final family of an instance will be `render` - new_family = "render" - old_subset_name = instance_data["subset"] - task_name = legacy_io.Session["AVALON_TASK"] - new_subset_name = "{}{}_{}_{}".format( - new_family, task_name.capitalize(), render_layer, pass_name - ) - instance_data["subset"] = new_subset_name - self.log.debug("Changed subset name \"{}\"->\"{}\"".format( - old_subset_name, new_subset_name - )) - - layers_data = context.data["layersData"] - layers_by_name = { - layer["name"]: layer - for layer in layers_data - } - - if "layer_names" in instance_data: - layer_names = instance_data["layer_names"] - else: - # Backwards compatibility - # - not 100% working as it was found out that layer ids can't be - # used as unified identifier across multiple workstations - layers_by_id = { - layer["layer_id"]: layer - for layer in layers_data - } - layer_ids = instance_data["layer_ids"] - layer_names = [] - for layer_id in layer_ids: - layer = layers_by_id.get(layer_id) - if layer: - layer_names.append(layer["name"]) - - if not layer_names: - raise ValueError(( - "Metadata contain old way of storing layers information." - " It is not possible to identify layers to publish with" - " these data. Please remove Render Pass instances with" - " Subset manager and use Creator tool to recreate them." - )) - - render_pass_layers = [] - for layer_name in layer_names: - layer = layers_by_name.get(layer_name) - # NOTE This is kind of validation before validators? - if not layer: - self.log.warning( - f"Layer with name {layer_name} was not found." - ) - continue - - render_pass_layers.append(layer) - - if not render_pass_layers: - name = instance_data["name"] - self.log.warning( - f"None of the layers from the RenderPass \"{name}\"" - " exist anymore. Instance not created." - ) - return None - - instance_data["layers"] = render_pass_layers - return context.create_instance(**instance_data) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py new file mode 100644 index 0000000000..34bb5aba24 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py @@ -0,0 +1,87 @@ +import copy +import pyblish.api +from openpype.lib import prepare_template_data + + +class CollectRenderInstances(pyblish.api.InstancePlugin): + label = "Collect Render Instances" + order = pyblish.api.CollectorOrder - 0.4 + hosts = ["tvpaint"] + families = ["render", "review"] + + def process(self, instance): + context = instance.context + creator_identifier = instance.data["creator_identifier"] + if creator_identifier == "render.layer": + self._collect_data_for_render_layer(instance) + + elif creator_identifier == "render.pass": + self._collect_data_for_render_pass(instance) + + else: + if creator_identifier == "scene.review": + self._collect_data_for_review(instance) + return + + subset_name = instance.data["subset"] + instance.data["name"] = subset_name + instance.data["label"] = "{} [{}-{}]".format( + subset_name, + context.data["sceneMarkIn"] + 1, + context.data["sceneMarkOut"] + 1 + ) + + def _collect_data_for_render_layer(self, instance): + instance.data["families"].append("renderLayer") + creator_attributes = instance.data["creator_attributes"] + group_id = creator_attributes["group_id"] + if creator_attributes["mark_for_review"]: + instance.data["families"].append("review") + + layers_data = instance.context.data["layersData"] + instance.data["layers"] = [ + copy.deepcopy(layer) + for layer in layers_data + if layer["group_id"] == group_id + ] + + def _collect_data_for_render_pass(self, instance): + instance.data["families"].append("renderPass") + + layer_names = set(instance.data["layer_names"]) + layers_data = instance.context.data["layersData"] + + creator_attributes = instance.data["creator_attributes"] + if creator_attributes["mark_for_review"]: + instance.data["families"].append("review") + + instance.data["layers"] = [ + copy.deepcopy(layer) + for layer in layers_data + if layer["name"] in layer_names + ] + + render_layer_data = None + render_layer_id = creator_attributes["render_layer_instance_id"] + for in_data in instance.context.data["workfileInstances"]: + if ( + in_data["creator_identifier"] == "render.layer" + and in_data["instance_id"] == render_layer_id + ): + render_layer_data = in_data + break + + instance.data["renderLayerData"] = copy.deepcopy(render_layer_data) + # Invalid state + if render_layer_data is None: + return + render_layer_name = render_layer_data["variant"] + subset_name = instance.data["subset"] + instance.data["subset"] = subset_name.format( + **prepare_template_data({"renderlayer": render_layer_name}) + ) + + def _collect_data_for_review(self, instance): + instance.data["layers"] = copy.deepcopy( + instance.context.data["layersData"] + ) From db75420899f3419e937dbf7b88d6f89e8dc42ae1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 11:09:49 +0100 Subject: [PATCH 125/483] removed legacy creators --- .../plugins/create/create_render_layer.py | 231 ------------------ .../plugins/create/create_render_pass.py | 167 ------------- 2 files changed, 398 deletions(-) delete mode 100644 openpype/hosts/tvpaint/plugins/create/create_render_layer.py delete mode 100644 openpype/hosts/tvpaint/plugins/create/create_render_pass.py diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py b/openpype/hosts/tvpaint/plugins/create/create_render_layer.py deleted file mode 100644 index 009b69c4f1..0000000000 --- a/openpype/hosts/tvpaint/plugins/create/create_render_layer.py +++ /dev/null @@ -1,231 +0,0 @@ -from openpype.lib import prepare_template_data -from openpype.pipeline import CreatorError -from openpype.hosts.tvpaint.api import ( - plugin, - CommunicationWrapper -) -from openpype.hosts.tvpaint.api.lib import ( - get_layers_data, - get_groups_data, - execute_george_through_file, -) -from openpype.hosts.tvpaint.api.pipeline import list_instances - - -class CreateRenderlayer(plugin.Creator): - """Mark layer group as one instance.""" - name = "render_layer" - label = "RenderLayer" - family = "renderLayer" - icon = "cube" - defaults = ["Main"] - - rename_group = True - render_pass = "beauty" - - rename_script_template = ( - "tv_layercolor \"setcolor\"" - " {clip_id} {group_id} {r} {g} {b} \"{name}\"" - ) - - dynamic_subset_keys = [ - "renderpass", "renderlayer", "render_pass", "render_layer", "group" - ] - - @classmethod - def get_dynamic_data( - cls, variant, task_name, asset_id, project_name, host_name - ): - dynamic_data = super(CreateRenderlayer, cls).get_dynamic_data( - variant, task_name, asset_id, project_name, host_name - ) - # Use render pass name from creator's plugin - dynamic_data["renderpass"] = cls.render_pass - # Add variant to render layer - dynamic_data["renderlayer"] = variant - # Change family for subset name fill - dynamic_data["family"] = "render" - - # TODO remove - Backwards compatibility for old subset name templates - # - added 2022/04/28 - dynamic_data["render_pass"] = dynamic_data["renderpass"] - dynamic_data["render_layer"] = dynamic_data["renderlayer"] - - return dynamic_data - - @classmethod - def get_default_variant(cls): - """Default value for variant in Creator tool. - - Method checks if TVPaint implementation is running and tries to find - selected layers from TVPaint. If only one is selected it's name is - returned. - - Returns: - str: Default variant name for Creator tool. - """ - # Validate that communication is initialized - if CommunicationWrapper.communicator: - # Get currently selected layers - layers_data = get_layers_data() - - selected_layers = [ - layer - for layer in layers_data - if layer["selected"] - ] - # Return layer name if only one is selected - if len(selected_layers) == 1: - return selected_layers[0]["name"] - - # Use defaults - if cls.defaults: - return cls.defaults[0] - return None - - def process(self): - self.log.debug("Query data from workfile.") - instances = list_instances() - layers_data = get_layers_data() - - self.log.debug("Checking for selection groups.") - # Collect group ids from selection - group_ids = set() - for layer in layers_data: - if layer["selected"]: - group_ids.add(layer["group_id"]) - - # Raise if there is no selection - if not group_ids: - raise CreatorError("Nothing is selected.") - - # This creator should run only on one group - if len(group_ids) > 1: - raise CreatorError("More than one group is in selection.") - - group_id = tuple(group_ids)[0] - # If group id is `0` it is `default` group which is invalid - if group_id == 0: - raise CreatorError( - "Selection is not in group. Can't mark selection as Beauty." - ) - - self.log.debug(f"Selected group id is \"{group_id}\".") - self.data["group_id"] = group_id - - group_data = get_groups_data() - group_name = None - for group in group_data: - if group["group_id"] == group_id: - group_name = group["name"] - break - - if group_name is None: - raise AssertionError( - "Couldn't find group by id \"{}\"".format(group_id) - ) - - subset_name_fill_data = { - "group": group_name - } - - family = self.family = self.data["family"] - - # Fill dynamic key 'group' - subset_name = self.data["subset"].format( - **prepare_template_data(subset_name_fill_data) - ) - self.data["subset"] = subset_name - - # Check for instances of same group - existing_instance = None - existing_instance_idx = None - # Check if subset name is not already taken - same_subset_instance = None - same_subset_instance_idx = None - for idx, instance in enumerate(instances): - if instance["family"] == family: - if instance["group_id"] == group_id: - existing_instance = instance - existing_instance_idx = idx - elif instance["subset"] == subset_name: - same_subset_instance = instance - same_subset_instance_idx = idx - - if ( - same_subset_instance_idx is not None - and existing_instance_idx is not None - ): - break - - if same_subset_instance_idx is not None: - if self._ask_user_subset_override(same_subset_instance): - instances.pop(same_subset_instance_idx) - else: - return - - if existing_instance is not None: - self.log.info( - f"Beauty instance for group id {group_id} already exists" - ", overriding" - ) - instances[existing_instance_idx] = self.data - else: - instances.append(self.data) - - self.write_instances(instances) - - if not self.rename_group: - self.log.info("Group rename function is turned off. Skipping") - return - - self.log.debug("Querying groups data from workfile.") - groups_data = get_groups_data() - - self.log.debug("Changing name of the group.") - selected_group = None - for group_data in groups_data: - if group_data["group_id"] == group_id: - selected_group = group_data - - # Rename TVPaint group (keep color same) - # - groups can't contain spaces - new_group_name = self.data["variant"].replace(" ", "_") - rename_script = self.rename_script_template.format( - clip_id=selected_group["clip_id"], - group_id=selected_group["group_id"], - r=selected_group["red"], - g=selected_group["green"], - b=selected_group["blue"], - name=new_group_name - ) - execute_george_through_file(rename_script) - - self.log.info( - f"Name of group with index {group_id}" - f" was changed to \"{new_group_name}\"." - ) - - def _ask_user_subset_override(self, instance): - from qtpy import QtCore - from qtpy.QtWidgets import QMessageBox - - title = "Subset \"{}\" already exist".format(instance["subset"]) - text = ( - "Instance with subset name \"{}\" already exists." - "\n\nDo you want to override existing?" - ).format(instance["subset"]) - - dialog = QMessageBox() - dialog.setWindowFlags( - dialog.windowFlags() - | QtCore.Qt.WindowStaysOnTopHint - ) - dialog.setWindowTitle(title) - dialog.setText(text) - dialog.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - dialog.setDefaultButton(QMessageBox.Yes) - dialog.exec_() - if dialog.result() == QMessageBox.Yes: - return True - return False diff --git a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py b/openpype/hosts/tvpaint/plugins/create/create_render_pass.py deleted file mode 100644 index a44cb29f20..0000000000 --- a/openpype/hosts/tvpaint/plugins/create/create_render_pass.py +++ /dev/null @@ -1,167 +0,0 @@ -from openpype.pipeline import CreatorError -from openpype.lib import prepare_template_data -from openpype.hosts.tvpaint.api import ( - plugin, - CommunicationWrapper -) -from openpype.hosts.tvpaint.api.lib import get_layers_data -from openpype.hosts.tvpaint.api.pipeline import list_instances - - -class CreateRenderPass(plugin.Creator): - """Render pass is combination of one or more layers from same group. - - Requirement to create Render Pass is to have already created beauty - instance. Beauty instance is used as base for subset name. - """ - name = "render_pass" - label = "RenderPass" - family = "renderPass" - icon = "cube" - defaults = ["Main"] - - dynamic_subset_keys = [ - "renderpass", "renderlayer", "render_pass", "render_layer" - ] - - @classmethod - def get_dynamic_data( - cls, variant, task_name, asset_id, project_name, host_name - ): - dynamic_data = super(CreateRenderPass, cls).get_dynamic_data( - variant, task_name, asset_id, project_name, host_name - ) - dynamic_data["renderpass"] = variant - dynamic_data["family"] = "render" - - # TODO remove - Backwards compatibility for old subset name templates - # - added 2022/04/28 - dynamic_data["render_pass"] = dynamic_data["renderpass"] - - return dynamic_data - - @classmethod - def get_default_variant(cls): - """Default value for variant in Creator tool. - - Method checks if TVPaint implementation is running and tries to find - selected layers from TVPaint. If only one is selected it's name is - returned. - - Returns: - str: Default variant name for Creator tool. - """ - # Validate that communication is initialized - if CommunicationWrapper.communicator: - # Get currently selected layers - layers_data = get_layers_data() - - selected_layers = [ - layer - for layer in layers_data - if layer["selected"] - ] - # Return layer name if only one is selected - if len(selected_layers) == 1: - return selected_layers[0]["name"] - - # Use defaults - if cls.defaults: - return cls.defaults[0] - return None - - def process(self): - self.log.debug("Query data from workfile.") - instances = list_instances() - layers_data = get_layers_data() - - self.log.debug("Checking selection.") - # Get all selected layers and their group ids - group_ids = set() - selected_layers = [] - for layer in layers_data: - if layer["selected"]: - selected_layers.append(layer) - group_ids.add(layer["group_id"]) - - # Raise if nothing is selected - if not selected_layers: - raise CreatorError("Nothing is selected.") - - # Raise if layers from multiple groups are selected - if len(group_ids) != 1: - raise CreatorError("More than one group is in selection.") - - group_id = tuple(group_ids)[0] - self.log.debug(f"Selected group id is \"{group_id}\".") - - # Find beauty instance for selected layers - beauty_instance = None - for instance in instances: - if ( - instance["family"] == "renderLayer" - and instance["group_id"] == group_id - ): - beauty_instance = instance - break - - # Beauty is required for this creator so raise if was not found - if beauty_instance is None: - raise CreatorError("Beauty pass does not exist yet.") - - subset_name = self.data["subset"] - - subset_name_fill_data = {} - - # Backwards compatibility - # - beauty may be created with older creator where variant was not - # stored - if "variant" not in beauty_instance: - render_layer = beauty_instance["name"] - else: - render_layer = beauty_instance["variant"] - - subset_name_fill_data["renderlayer"] = render_layer - subset_name_fill_data["render_layer"] = render_layer - - # Format dynamic keys in subset name - new_subset_name = subset_name.format( - **prepare_template_data(subset_name_fill_data) - ) - self.data["subset"] = new_subset_name - self.log.info(f"New subset name is \"{new_subset_name}\".") - - family = self.data["family"] - variant = self.data["variant"] - - self.data["group_id"] = group_id - self.data["pass"] = variant - self.data["renderlayer"] = render_layer - - # Collect selected layer ids to be stored into instance - layer_names = [layer["name"] for layer in selected_layers] - self.data["layer_names"] = layer_names - - # Check if same instance already exists - existing_instance = None - existing_instance_idx = None - for idx, instance in enumerate(instances): - if ( - instance["family"] == family - and instance["group_id"] == group_id - and instance["pass"] == variant - ): - existing_instance = instance - existing_instance_idx = idx - break - - if existing_instance is not None: - self.log.info( - f"Render pass instance for group id {group_id}" - f" and name \"{variant}\" already exists, overriding." - ) - instances[existing_instance_idx] = self.data - else: - instances.append(self.data) - - self.write_instances(instances) From 6097a9627607a70bb91d977274da8bd9f39742b6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 12:14:53 +0100 Subject: [PATCH 126/483] added option to use 'subset_template_family_filter' in all tvpaint creators --- openpype/hosts/tvpaint/api/plugin.py | 62 +++++++++++++++------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index d267d87acd..397e4295f5 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -22,6 +22,10 @@ SHARED_DATA_KEY = "openpype.tvpaint.instances" class TVPaintCreatorCommon: + @property + def subset_template_family_filter(self): + return self.family + def _cache_and_get_instances(self): return cache_and_get_instances( self, SHARED_DATA_KEY, self.host.list_instances @@ -56,12 +60,33 @@ class TVPaintCreatorCommon: cur_instance_data.update(instance_data) self.host.write_instances(cur_instances) + def _custom_get_subset_name( + self, + variant, + task_name, + asset_doc, + project_name, + host_name=None, + instance=None + ): + dynamic_data = self.get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name, instance + ) + + return get_subset_name( + self.family, + variant, + task_name, + asset_doc, + project_name, + host_name, + dynamic_data=dynamic_data, + project_settings=self.project_settings, + family_filter=self.subset_template_family_filter + ) + class TVPaintCreator(NewCreator, TVPaintCreatorCommon): - @property - def subset_template_family_filter(self): - return self.family - def collect_instances(self): self._collect_create_instances() @@ -101,30 +126,8 @@ class TVPaintCreator(NewCreator, TVPaintCreatorCommon): output["task"] = task_name return output - def get_subset_name( - self, - variant, - task_name, - asset_doc, - project_name, - host_name=None, - instance=None - ): - dynamic_data = self.get_dynamic_data( - variant, task_name, asset_doc, project_name, host_name, instance - ) - - return get_subset_name( - self.family, - variant, - task_name, - asset_doc, - project_name, - host_name, - dynamic_data=dynamic_data, - project_settings=self.project_settings, - family_filter=self.subset_template_family_filter - ) + def get_subset_name(self, *args, **kwargs): + return self._custom_get_subset_name(*args, **kwargs) def _store_new_instance(self, new_instance): instances_data = self.host.list_instances() @@ -140,6 +143,9 @@ class TVPaintAutoCreator(AutoCreator, TVPaintCreatorCommon): def update_instances(self, update_list): self._update_create_instances(update_list) + def get_subset_name(self, *args, **kwargs): + return self._custom_get_subset_name(*args, **kwargs) + class Creator(LegacyCreator): def __init__(self, *args, **kwargs): From d727ec1f5f58b1fcd18401cd9ad03b9a690a2cff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 12:23:24 +0100 Subject: [PATCH 127/483] added screne render auto creator --- .../tvpaint/plugins/create/create_render.py | 125 +++++++++++++++++- .../publish/collect_render_instances.py | 22 +++ .../plugins/publish/collect_scene_render.py | 114 ---------------- 3 files changed, 146 insertions(+), 115 deletions(-) delete mode 100644 openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 8f7ba121c1..2b693d4bc0 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -32,6 +32,7 @@ Todos: import collections +from openpype.client import get_asset_by_name from openpype.lib import ( prepare_template_data, EnumDef, @@ -42,7 +43,10 @@ from openpype.pipeline.create import ( CreatedInstance, CreatorError, ) -from openpype.hosts.tvpaint.api.plugin import TVPaintCreator +from openpype.hosts.tvpaint.api.plugin import ( + TVPaintCreator, + TVPaintAutoCreator, +) from openpype.hosts.tvpaint.api.lib import ( get_layers_data, get_groups_data, @@ -480,3 +484,122 @@ class CreateRenderPass(TVPaintCreator): def get_instance_attr_defs(self): return self.get_pre_create_attr_defs() + + +class TVPaintSceneRenderCreator(TVPaintAutoCreator): + family = "render" + subset_template_family_filter = "renderScene" + identifier = "render.scene" + label = "Scene Render" + + # Settings + default_variant = "Main" + default_pass_name = "beauty" + mark_for_review = True + + def get_dynamic_data(self, variant, *args, **kwargs): + dynamic_data = super().get_dynamic_data(variant, *args, **kwargs) + dynamic_data["renderpass"] = "{renderpass}" + dynamic_data["renderlayer"] = variant + return dynamic_data + + def _create_new_instance(self): + context = self.host.get_current_context() + host_name = self.host.name + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] + + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + self.default_variant, + task_name, + asset_doc, + project_name, + host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": self.default_variant, + "creator_attributes": { + "render_pass_name": self.default_pass_name, + "mark_for_review": True + }, + "label": self._get_label( + subset_name, + self.default_pass_name + ) + } + + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + instances_data = self.host.list_instances() + instances_data.append(new_instance.data_to_store()) + self.host.write_instances(instances_data) + self._add_instance_to_context(new_instance) + return new_instance + + def create(self): + existing_instance = None + for instance in self.create_context.instances: + if instance.creator_identifier == self.identifier: + existing_instance = instance + break + + if existing_instance is None: + return self._create_new_instance() + + context = self.host.get_current_context() + host_name = self.host.name + project_name = context["project_name"] + asset_name = context["asset_name"] + task_name = context["task_name"] + + if ( + existing_instance["asset"] != asset_name + or existing_instance["task"] != task_name + ): + asset_doc = get_asset_by_name(project_name, asset_name) + subset_name = self.get_subset_name( + existing_instance["variant"], + task_name, + asset_doc, + project_name, + host_name, + existing_instance + ) + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name + existing_instance["subset"] = subset_name + + existing_instance["label"] = self._get_label( + existing_instance["subset"], + existing_instance["creator_attributes"]["render_pass_name"] + ) + + + + def _get_label(self, subset_name, render_layer_name): + return subset_name.format(**prepare_template_data({ + "renderlayer": render_layer_name + })) + + def get_instance_attr_defs(self): + return [ + TextDef( + "render_pass_name", + label="Pass Name", + default=self.default_pass_name, + tooltip=( + "Value is calculated during publishing and UI will update" + " label after refresh." + ) + ), + BoolDef( + "mark_for_review", + label="Review", + default=self.mark_for_review + ) + ] \ No newline at end of file diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py index 34bb5aba24..ba89deac5d 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py @@ -18,6 +18,9 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): elif creator_identifier == "render.pass": self._collect_data_for_render_pass(instance) + elif creator_identifier == "render.scene": + self._collect_data_for_render_scene(instance) + else: if creator_identifier == "scene.review": self._collect_data_for_review(instance) @@ -81,6 +84,25 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): **prepare_template_data({"renderlayer": render_layer_name}) ) + def _collect_data_for_render_scene(self, instance): + instance.data["families"].append("renderScene") + + creator_attributes = instance.data["creator_attributes"] + if creator_attributes["mark_for_review"]: + instance.data["families"].append("review") + + instance.data["layers"] = copy.deepcopy( + instance.context.data["layersData"] + ) + + render_pass_name = ( + instance.data["creator_attributes"]["render_pass_name"] + ) + subset_name = instance.data["subset"] + instance.data["subset"] = subset_name.format( + **prepare_template_data({"renderpass": render_pass_name}) + ) + def _collect_data_for_review(self, instance): instance.data["layers"] = copy.deepcopy( instance.context.data["layersData"] diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py b/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py deleted file mode 100644 index 92a2815ba0..0000000000 --- a/openpype/hosts/tvpaint/plugins/publish/collect_scene_render.py +++ /dev/null @@ -1,114 +0,0 @@ -import json -import copy -import pyblish.api - -from openpype.client import get_asset_by_name -from openpype.pipeline.create import get_subset_name - - -class CollectRenderScene(pyblish.api.ContextPlugin): - """Collect instance which renders whole scene in PNG. - - Creates instance with family 'renderScene' which will have all layers - to render which will be composite into one result. The instance is not - collected from scene. - - Scene will be rendered with all visible layers similar way like review is. - - Instance is disabled if there are any created instances of 'renderLayer' - or 'renderPass'. That is because it is expected that this instance is - used as lazy publish of TVPaint file. - - Subset name is created similar way like 'renderLayer' family. It can use - `renderPass` and `renderLayer` keys which can be set using settings and - `variant` is filled using `renderPass` value. - """ - label = "Collect Render Scene" - order = pyblish.api.CollectorOrder - 0.39 - hosts = ["tvpaint"] - - # Value of 'render_pass' in subset name template - render_pass = "beauty" - - # Settings attributes - enabled = False - # Value of 'render_layer' and 'variant' in subset name template - render_layer = "Main" - - def process(self, context): - # Check if there are created instances of renderPass and renderLayer - # - that will define if renderScene instance is enabled after - # collection - any_created_instance = False - for instance in context: - family = instance.data["family"] - if family in ("renderPass", "renderLayer"): - any_created_instance = True - break - - # Global instance data modifications - # Fill families - family = "renderScene" - # Add `review` family for thumbnail integration - families = [family, "review"] - - # Collect asset doc to get asset id - # - not sure if it's good idea to require asset id in - # get_subset_name? - workfile_context = context.data["workfile_context"] - # Project name from workfile context - project_name = context.data["workfile_context"]["project"] - asset_name = workfile_context["asset"] - asset_doc = get_asset_by_name(project_name, asset_name) - - # Host name from environment variable - host_name = context.data["hostName"] - # Variant is using render pass name - variant = self.render_layer - dynamic_data = { - "renderlayer": self.render_layer, - "renderpass": self.render_pass, - } - # TODO remove - Backwards compatibility for old subset name templates - # - added 2022/04/28 - dynamic_data["render_layer"] = dynamic_data["renderlayer"] - dynamic_data["render_pass"] = dynamic_data["renderpass"] - - task_name = workfile_context["task"] - subset_name = get_subset_name( - "render", - variant, - task_name, - asset_doc, - project_name, - host_name, - dynamic_data=dynamic_data, - project_settings=context.data["project_settings"] - ) - - instance_data = { - "family": family, - "families": families, - "fps": context.data["sceneFps"], - "subset": subset_name, - "name": subset_name, - "label": "{} [{}-{}]".format( - subset_name, - context.data["sceneMarkIn"] + 1, - context.data["sceneMarkOut"] + 1 - ), - "active": not any_created_instance, - "publish": not any_created_instance, - "representations": [], - "layers": copy.deepcopy(context.data["layersData"]), - "asset": asset_name, - "task": task_name, - # Add render layer to instance data - "renderlayer": self.render_layer - } - - instance = context.create_instance(**instance_data) - - self.log.debug("Created instance: {}\n{}".format( - instance, json.dumps(instance.data, indent=4) - )) From b21d55c10d9885c1d184e609253592bf4f44c6e7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 12:24:09 +0100 Subject: [PATCH 128/483] change families filter for validate layers visibility --- openpype/hosts/tvpaint/plugins/create/create_render.py | 2 +- .../hosts/tvpaint/plugins/publish/validate_layers_visibility.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 2b693d4bc0..2d44282879 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -602,4 +602,4 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): label="Review", default=self.mark_for_review ) - ] \ No newline at end of file + ] diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py index d3a04cc69f..47632453fc 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py @@ -8,7 +8,7 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin): label = "Validate Layers Visibility" order = pyblish.api.ValidatorOrder - families = ["review", "renderPass", "renderLayer", "renderScene"] + families = ["review", "render"] def process(self, instance): layer_names = set() From 8598d5c1f3f5d10c9c72ad4c0f413cb5119d70fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 12:25:18 +0100 Subject: [PATCH 129/483] remove empty lines --- openpype/hosts/tvpaint/plugins/create/create_render.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 2d44282879..7acd9b2260 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -579,8 +579,6 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): existing_instance["creator_attributes"]["render_pass_name"] ) - - def _get_label(self, subset_name, render_layer_name): return subset_name.format(**prepare_template_data({ "renderlayer": render_layer_name From 122a72506257bf15354b0dedf20fdd73495c8c47 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 15:17:25 +0100 Subject: [PATCH 130/483] implemented basic of conver plugin --- .../tvpaint/plugins/create/convert_legacy.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 openpype/hosts/tvpaint/plugins/create/convert_legacy.py diff --git a/openpype/hosts/tvpaint/plugins/create/convert_legacy.py b/openpype/hosts/tvpaint/plugins/create/convert_legacy.py new file mode 100644 index 0000000000..79244b4fc4 --- /dev/null +++ b/openpype/hosts/tvpaint/plugins/create/convert_legacy.py @@ -0,0 +1,135 @@ +import collections + +from openpype.pipeline.create.creator_plugins import ( + SubsetConvertorPlugin, + cache_and_get_instances, +) +from openpype.hosts.tvpaint.api.plugin import SHARED_DATA_KEY +from openpype.hosts.tvpaint.api.lib import get_groups_data + + +class TVPaintLegacyConverted(SubsetConvertorPlugin): + identifier = "tvpaint.legacy.converter" + + def find_instances(self): + instances = cache_and_get_instances( + self, SHARED_DATA_KEY, self.host.list_instances + ) + + for instance in instances: + if instance.get("creator_identifier") is None: + self.add_convertor_item("Convert legacy instances") + return + + def convert(self): + current_instances = self.host.list_instances() + to_convert = collections.defaultdict(list) + converted = False + for instance in current_instances: + if instance.get("creator_identifier") is not None: + continue + converted = True + + family = instance.get("family") + if family in ( + "renderLayer", + "renderPass", + "renderScene", + "review", + "workfile", + ): + to_convert[family].append(instance) + else: + instance["keep"] = False + + # Skip if nothing was changed + if not converted: + self.remove_convertor_item() + return + + self._convert_render_layers( + to_convert["renderLayer"], current_instances) + self._convert_render_passes( + to_convert["renderpass"], current_instances) + self._convert_render_scenes( + to_convert["renderScene"], current_instances) + self._convert_workfiles( + to_convert["workfile"], current_instances) + self._convert_reviews( + to_convert["review"], current_instances) + + new_instances = [ + instance + for instance in current_instances + if instance.get("keep") is not False + ] + self.host.write_instances(new_instances) + # remove legacy item if all is fine + self.remove_convertor_item() + + def _convert_render_layers(self, render_layers, current_instances): + if not render_layers: + return + + render_layers_by_group_id = {} + for instance in current_instances: + if instance.get("creator_identifier") == "render.layer": + group_id = instance["creator_identifier"]["group_id"] + render_layers_by_group_id[group_id] = instance + + groups_by_id = { + group["group_id"]: group + for group in get_groups_data() + } + for render_layer in render_layers: + group_id = render_layer.pop("group_id") + if group_id in render_layers_by_group_id: + render_layer["keep"] = False + continue + render_layer["creator_identifier"] = "render.layer" + render_layer["instance_id"] = render_layer.pop("uuid") + render_layer["creator_attributes"] = { + "group_id": group_id + } + render_layer["family"] = "render" + group = groups_by_id[group_id] + group["variant"] = group["name"] + + def _convert_render_passes(self, render_passes, current_instances): + if not render_passes: + return + + render_layers_by_group_id = {} + for instance in current_instances: + if instance.get("creator_identifier") == "render.layer": + group_id = instance["creator_identifier"]["group_id"] + render_layers_by_group_id[group_id] = instance + + for render_pass in render_passes: + group_id = render_pass.pop("group_id") + render_layer = render_layers_by_group_id.get(group_id) + if not render_layer: + render_pass["keep"] = False + continue + + render_pass["creator_identifier"] = "render.pass" + render_pass["instance_id"] = render_pass.pop("uuid") + render_pass["family"] = "render" + + render_pass["creator_attributes"] = { + "render_layer_instance_id": render_layer["instance_id"] + } + render_pass["variant"] = render_pass.pop("pass") + render_pass.pop("renderlayer") + + def _convert_render_scenes(self, render_scenes, current_instances): + for render_scene in render_scenes: + render_scene["keep"] = False + + def _convert_workfiles(self, workfiles, current_instances): + for render_scene in workfiles: + render_scene["keep"] = False + + def _convert_reviews(self, reviews, current_instances): + for render_scene in reviews: + render_scene["keep"] = False From 06e11a45c20740307a45273ee236d44f3272d55c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 15:17:39 +0100 Subject: [PATCH 131/483] removed legacy creator --- openpype/hosts/tvpaint/api/plugin.py | 47 ++-------------------------- 1 file changed, 3 insertions(+), 44 deletions(-) diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index 397e4295f5..64784bfb83 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -1,21 +1,15 @@ import re -import uuid -from openpype.pipeline import ( - LegacyCreator, - LoaderPlugin, - registered_host, -) +from openpype.pipeline import LoaderPlugin from openpype.pipeline.create import ( CreatedInstance, get_subset_name, AutoCreator, - Creator as NewCreator, + Creator, ) from openpype.pipeline.create.creator_plugins import cache_and_get_instances from .lib import get_layers_data -from .pipeline import get_current_workfile_context SHARED_DATA_KEY = "openpype.tvpaint.instances" @@ -86,7 +80,7 @@ class TVPaintCreatorCommon: ) -class TVPaintCreator(NewCreator, TVPaintCreatorCommon): +class TVPaintCreator(Creator, TVPaintCreatorCommon): def collect_instances(self): self._collect_create_instances() @@ -147,41 +141,6 @@ class TVPaintAutoCreator(AutoCreator, TVPaintCreatorCommon): return self._custom_get_subset_name(*args, **kwargs) -class Creator(LegacyCreator): - def __init__(self, *args, **kwargs): - super(Creator, self).__init__(*args, **kwargs) - # Add unified identifier created with `uuid` module - self.data["uuid"] = str(uuid.uuid4()) - - @classmethod - def get_dynamic_data(cls, *args, **kwargs): - dynamic_data = super(Creator, cls).get_dynamic_data(*args, **kwargs) - - # Change asset and name by current workfile context - workfile_context = get_current_workfile_context() - asset_name = workfile_context.get("asset") - task_name = workfile_context.get("task") - if "asset" not in dynamic_data and asset_name: - dynamic_data["asset"] = asset_name - - if "task" not in dynamic_data and task_name: - dynamic_data["task"] = task_name - return dynamic_data - - def write_instances(self, data): - self.log.debug( - "Storing instance data to workfile. {}".format(str(data)) - ) - host = registered_host() - return host.write_instances(data) - - def process(self): - host = registered_host() - data = host.list_instances() - data.append(self.data) - self.write_instances(data) - - class Loader(LoaderPlugin): hosts = ["tvpaint"] From 3b87087a574fc35aa8cb7c7d190f1a5f8291f5c5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 15:39:51 +0100 Subject: [PATCH 132/483] don't use project document during context settings --- openpype/hosts/tvpaint/api/pipeline.py | 55 +++++--------------------- 1 file changed, 10 insertions(+), 45 deletions(-) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 3794bf2e24..88ad3b4b4d 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -147,27 +147,6 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): def write_instances(self, data): return write_instances(data) - # --- Legacy Create --- - def remove_instance(self, instance): - """Remove instance from current workfile metadata. - - Implementation for Subset manager tool. - """ - - current_instances = get_workfile_metadata(SECTION_NAME_INSTANCES) - instance_id = instance.get("uuid") - found_idx = None - if instance_id: - for idx, _inst in enumerate(current_instances): - if _inst["uuid"] == instance_id: - found_idx = idx - break - - if found_idx is None: - return - current_instances.pop(found_idx) - write_instances(current_instances) - # --- Workfile --- def open_workfile(self, filepath): george_script = "tv_LoadProject '\"'\"{}\"'\"'".format( @@ -515,17 +494,19 @@ def set_context_settings(asset_doc=None): Change fps, resolution and frame start/end. """ - project_name = legacy_io.active_project() - if asset_doc is None: - asset_name = legacy_io.Session["AVALON_ASSET"] - # Use current session asset if not passed - asset_doc = get_asset_by_name(project_name, asset_name) + width_key = "resolutionWidth" + height_key = "resolutionHeight" - project_doc = get_project(project_name) + width = asset_doc["data"].get(width_key) + height = asset_doc["data"].get(height_key) + if width is None or height is None: + print("Resolution was not found!") + else: + execute_george( + "tv_resizepage {} {} 0".format(width, height) + ) framerate = asset_doc["data"].get("fps") - if framerate is None: - framerate = project_doc["data"].get("fps") if framerate is not None: execute_george( @@ -534,22 +515,6 @@ def set_context_settings(asset_doc=None): else: print("Framerate was not found!") - width_key = "resolutionWidth" - height_key = "resolutionHeight" - - width = asset_doc["data"].get(width_key) - height = asset_doc["data"].get(height_key) - if width is None or height is None: - width = project_doc["data"].get(width_key) - height = project_doc["data"].get(height_key) - - if width is None or height is None: - print("Resolution was not found!") - else: - execute_george( - "tv_resizepage {} {} 0".format(width, height) - ) - frame_start = asset_doc["data"].get("frameStart") frame_end = asset_doc["data"].get("frameEnd") From 7438aebc58217d85327360ee228e469dd138b168 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 15:40:16 +0100 Subject: [PATCH 133/483] remove logic related to pyblish instance toggle --- openpype/hosts/tvpaint/api/pipeline.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 88ad3b4b4d..4a737f8f72 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -87,10 +87,6 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): registered_callbacks = ( pyblish.api.registered_callbacks().get("instanceToggled") or [] ) - if self.on_instance_toggle not in registered_callbacks: - pyblish.api.register_callback( - "instanceToggled", self.on_instance_toggle - ) register_event_callback("application.launched", self.initial_launch) register_event_callback("application.exit", self.application_exit) @@ -209,28 +205,6 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): rest_api_url = "{}/timers_manager/stop_timer".format(webserver_url) requests.post(rest_api_url) - # --- Legacy Publish --- - def on_instance_toggle(self, instance, old_value, new_value): - """Update instance data in workfile on publish toggle.""" - # Review may not have real instance in wokrfile metadata - if not instance.data.get("uuid"): - return - - instance_id = instance.data["uuid"] - found_idx = None - current_instances = list_instances() - for idx, workfile_instance in enumerate(current_instances): - if workfile_instance.get("uuid") == instance_id: - found_idx = idx - break - - if found_idx is None: - return - - if "active" in current_instances[found_idx]: - current_instances[found_idx]["active"] = new_value - self.write_instances(current_instances) - def containerise( name, namespace, members, context, loader, current_containers=None From 7721520dd8cbb58b96a39cc2bb46ce0034767db2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 15:40:36 +0100 Subject: [PATCH 134/483] set context settings is called with explicit arguments --- openpype/hosts/tvpaint/api/pipeline.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index 4a737f8f72..575e6aa755 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -185,7 +185,15 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return log.info("Setting up project...") - set_context_settings() + global_context = get_global_context() + project_name = global_context.get("project_name") + asset_name = global_context.get("aset_name") + if not project_name or not asset_name: + return + + asset_doc = get_asset_by_name(project_name, asset_name) + + set_context_settings(project_name, asset_doc) def application_exit(self): """Logic related to TimerManager. @@ -462,7 +470,7 @@ def get_containers(): return output -def set_context_settings(asset_doc=None): +def set_context_settings(project_name, asset_doc): """Set workfile settings by asset document data. Change fps, resolution and frame start/end. From 118cf08d91e9fd3a4a540a5bf5d118198965fad6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 15:40:50 +0100 Subject: [PATCH 135/483] removed not needed tools --- .../hosts/tvpaint/api/communication_server.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/openpype/hosts/tvpaint/api/communication_server.py b/openpype/hosts/tvpaint/api/communication_server.py index 6fd2d69373..e94e64e04a 100644 --- a/openpype/hosts/tvpaint/api/communication_server.py +++ b/openpype/hosts/tvpaint/api/communication_server.py @@ -309,8 +309,6 @@ class QtTVPaintRpc(BaseTVPaintRpc): self.add_methods( (route_name, self.workfiles_tool), (route_name, self.loader_tool), - (route_name, self.creator_tool), - (route_name, self.subset_manager_tool), (route_name, self.publish_tool), (route_name, self.scene_inventory_tool), (route_name, self.library_loader_tool), @@ -330,18 +328,6 @@ class QtTVPaintRpc(BaseTVPaintRpc): self._execute_in_main_thread(item) return - async def creator_tool(self): - log.info("Triggering Creator tool") - item = MainThreadItem(self.tools_helper.show_creator) - await self._async_execute_in_main_thread(item, wait=False) - - async def subset_manager_tool(self): - log.info("Triggering Subset Manager tool") - item = MainThreadItem(self.tools_helper.show_subset_manager) - # Do not wait for result of callback - self._execute_in_main_thread(item, wait=False) - return - async def publish_tool(self): log.info("Triggering Publish tool") item = MainThreadItem(self.tools_helper.show_publisher_tool) @@ -859,10 +845,6 @@ class QtCommunicator(BaseCommunicator): "callback": "loader_tool", "label": "Load", "help": "Open loader tool" - }, { - "callback": "creator_tool", - "label": "Create", - "help": "Open creator tool" }, { "callback": "scene_inventory_tool", "label": "Scene inventory", From 38ff3adc4ea9e614f44f7fbbb84655bb91b87250 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 15:42:17 +0100 Subject: [PATCH 136/483] use context from create context --- openpype/hosts/tvpaint/api/plugin.py | 8 +++----- .../tvpaint/plugins/create/create_render.py | 20 +++++++++---------- .../tvpaint/plugins/create/create_review.py | 10 +++++----- .../tvpaint/plugins/create/create_workfile.py | 10 +++++----- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/openpype/hosts/tvpaint/api/plugin.py b/openpype/hosts/tvpaint/api/plugin.py index 64784bfb83..96b99199f2 100644 --- a/openpype/hosts/tvpaint/api/plugin.py +++ b/openpype/hosts/tvpaint/api/plugin.py @@ -31,7 +31,6 @@ class TVPaintCreatorCommon: instance = CreatedInstance.from_existing(instance_data, self) self._add_instance_to_context(instance) - def _update_create_instances(self, update_list): if not update_list: return @@ -109,10 +108,9 @@ class TVPaintCreator(Creator, TVPaintCreatorCommon): def get_dynamic_data(self, *args, **kwargs): # Change asset and name by current workfile context - # TODO use context from 'create_context' - workfile_context = self.host.get_current_context() - asset_name = workfile_context.get("asset") - task_name = workfile_context.get("task") + create_context = self.create_context + asset_name = create_context.get_current_asset_name() + task_name = create_context.get_current_task_name() output = {} if asset_name: output["asset"] = asset_name diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 7acd9b2260..0df066edc4 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -504,11 +504,11 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): return dynamic_data def _create_new_instance(self): - context = self.host.get_current_context() - host_name = self.host.name - project_name = context["project_name"] - asset_name = context["asset_name"] - task_name = context["task_name"] + create_context = self.create_context + host_name = create_context.host_name + project_name = create_context.get_current_project_name() + asset_name = create_context.get_current_asset_name() + task_name = create_context.get_current_task_name() asset_doc = get_asset_by_name(project_name, asset_name) subset_name = self.get_subset_name( @@ -551,11 +551,11 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): if existing_instance is None: return self._create_new_instance() - context = self.host.get_current_context() - host_name = self.host.name - project_name = context["project_name"] - asset_name = context["asset_name"] - task_name = context["task_name"] + create_context = self.create_context + host_name = create_context.host_name + project_name = create_context.get_current_project_name() + asset_name = create_context.get_current_asset_name() + task_name = create_context.get_current_task_name() if ( existing_instance["asset"] != asset_name diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index 84127eb9b7..1c220831bf 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -17,11 +17,11 @@ class TVPaintReviewCreator(TVPaintAutoCreator): existing_instance = instance break - context = self.host.get_current_context() - host_name = self.host.name - project_name = context["project_name"] - asset_name = context["asset_name"] - task_name = context["task_name"] + create_context = self.create_context + host_name = create_context.host_name + project_name = create_context.get_current_project_name() + asset_name = create_context.get_current_asset_name() + task_name = create_context.get_current_task_name() if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index e421fbc3f8..d968e4f77d 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -17,11 +17,11 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): existing_instance = instance break - context = self.host.get_current_context() - host_name = self.host.name - project_name = context["project_name"] - asset_name = context["asset_name"] - task_name = context["task_name"] + create_context = self.create_context + host_name = create_context.host_name + project_name = create_context.get_current_project_name() + asset_name = create_context.get_current_asset_name() + task_name = create_context.get_current_task_name() if existing_instance is None: asset_doc = get_asset_by_name(project_name, asset_name) From 0abcbc152390c00aa596b80ccc40c6a64c4861c4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:04:59 +0100 Subject: [PATCH 137/483] OP-4642 - added additional command arguments to Settings --- .../schemas/schema_global_publish.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 3956f403f4..5333d514b5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -286,6 +286,20 @@ "label": "View", "type": "text" }, + { + "key": "oiiotool_args", + "label": "OIIOtool arguments", + "type": "dict", + "highlight_content": true, + "children": [ + { + "key": "additional_command_args", + "label": "Additional command line arguments", + "type": "list", + "object_type": "text" + } + ] + }, { "type": "schema", "name": "schema_representation_tags" From 5ec5cda2bcd1e68de459496c20a1cc9699ba7144 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:08:06 +0100 Subject: [PATCH 138/483] OP-4642 - added additional command arguments for oiiotool Some extension requires special command line arguments (.dpx and binary depth). --- openpype/lib/transcoding.py | 6 ++++++ openpype/plugins/publish/extract_color_transcode.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 18273dd432..95042fb74c 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1047,6 +1047,7 @@ def convert_colorspace( target_colorspace, view=None, display=None, + additional_command_args=None, logger=None ): """Convert source file from one color space to another. @@ -1066,6 +1067,8 @@ def convert_colorspace( view (str): name for viewer space (ocio valid) both 'view' and 'display' must be filled (if 'target_colorspace') display (str): name for display-referred reference space (ocio valid) + additional_command_args (list): arguments for oiiotool (like binary + depth for .dpx) logger (logging.Logger): Logger used for logging. Raises: ValueError: if misconfigured @@ -1088,6 +1091,9 @@ def convert_colorspace( if not target_colorspace and not all([view, display]): raise ValueError("Both screen and display must be set.") + if additional_command_args: + oiio_cmd.extend(additional_command_args) + if target_colorspace: oiio_cmd.extend(["--colorconvert", source_colorspace, diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4a03e623fd..3de404125d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -128,6 +128,9 @@ class ExtractOIIOTranscode(publish.Extractor): if display: new_repre["colorspaceData"]["display"] = display + additional_command_args = (output_def["oiiotool_args"] + ["additional_command_args"]) + files_to_convert = self._translate_to_sequence( files_to_convert) for file_name in files_to_convert: @@ -144,6 +147,7 @@ class ExtractOIIOTranscode(publish.Extractor): target_colorspace, view, display, + additional_command_args, self.log ) From b30979b8c1a4d4da0d998862516f95a89e522d9d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:21:25 +0100 Subject: [PATCH 139/483] OP-4642 - refactored newly added representations --- openpype/plugins/publish/extract_color_transcode.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 3de404125d..8c4ef59de9 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -82,6 +82,7 @@ class ExtractOIIOTranscode(publish.Extractor): if not profile: return + new_representations = [] repres = instance.data.get("representations") or [] for idx, repre in enumerate(list(repres)): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) @@ -174,9 +175,7 @@ class ExtractOIIOTranscode(publish.Extractor): if tag == "review": added_review = True - new_repre["tags"].append("newly_added") - - instance.data["representations"].append(new_repre) + new_representations.append(new_repre) added_representations = True if added_representations: @@ -185,15 +184,11 @@ class ExtractOIIOTranscode(publish.Extractor): for repre in tuple(instance.data["representations"]): tags = repre.get("tags") or [] - # TODO implement better way, for now do not delete new repre - # new repre might have 'delete' tag to removed, but it first must - # be there for review to be created - if "newly_added" in tags: - tags.remove("newly_added") - continue if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) + instance.data["representations"].extend(new_representations) + def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): """Replace old extension with new one everywhere in representation. From 73cca8299506e95b2ec515e6ac085b85a9b2049d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:24:53 +0100 Subject: [PATCH 140/483] OP-4642 - refactored query of representations line 73 returns if no representations. --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 8c4ef59de9..de36ea7d5f 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -83,7 +83,7 @@ class ExtractOIIOTranscode(publish.Extractor): return new_representations = [] - repres = instance.data.get("representations") or [] + repres = instance.data["representations"] for idx, repre in enumerate(list(repres)): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) if not self._repre_is_valid(repre): From 0182f73e32f42f130bf71249ce137d1b95788953 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 16:51:23 +0100 Subject: [PATCH 141/483] fix double spaces --- openpype/hosts/tvpaint/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 0df066edc4..2069a657b9 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -228,7 +228,7 @@ class CreateRenderlayer(TVPaintCreator): render_layer_instances = {} render_pass_instances = collections.defaultdict(list) - for instance in self.create_context.instances: + for instance in self.create_context.instances: if instance.creator_identifier == CreateRenderPass.identifier: render_layer_id = ( instance["creator_attributes"]["render_layer_instance_id"] From 7397bdcac23619177998c8460a9297af5d135319 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 17:52:42 +0100 Subject: [PATCH 142/483] added host property to legacy convertor --- openpype/pipeline/create/creator_plugins.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 53acb618ed..14c5d70462 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -79,6 +79,10 @@ class SubsetConvertorPlugin(object): self._log = Logger.get_logger(self.__class__.__name__) return self._log + @property + def host(self): + return self._create_context.host + @abstractproperty def identifier(self): """Converted identifier. From bb112ad1560294c92285b8794dedf28dad5c72cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 17:53:01 +0100 Subject: [PATCH 143/483] fix legacy convertor --- openpype/hosts/tvpaint/plugins/create/convert_legacy.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/convert_legacy.py b/openpype/hosts/tvpaint/plugins/create/convert_legacy.py index 79244b4fc4..215c87f3e5 100644 --- a/openpype/hosts/tvpaint/plugins/create/convert_legacy.py +++ b/openpype/hosts/tvpaint/plugins/create/convert_legacy.py @@ -12,14 +12,11 @@ class TVPaintLegacyConverted(SubsetConvertorPlugin): identifier = "tvpaint.legacy.converter" def find_instances(self): - instances = cache_and_get_instances( + instances_by_identifier = cache_and_get_instances( self, SHARED_DATA_KEY, self.host.list_instances ) - - for instance in instances: - if instance.get("creator_identifier") is None: - self.add_convertor_item("Convert legacy instances") - return + if instances_by_identifier[None]: + self.add_convertor_item("Convert legacy instances") def convert(self): current_instances = self.host.list_instances() From c0f9811808c6a60f221c1b80a3c3e6bd76d4a6ac Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 17:53:14 +0100 Subject: [PATCH 144/483] fix render scene subset name creation --- openpype/hosts/tvpaint/plugins/create/create_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 2069a657b9..d2eb693ab9 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -579,9 +579,9 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): existing_instance["creator_attributes"]["render_pass_name"] ) - def _get_label(self, subset_name, render_layer_name): + def _get_label(self, subset_name, render_pass_name): return subset_name.format(**prepare_template_data({ - "renderlayer": render_layer_name + "renderpass": render_pass_name })) def get_instance_attr_defs(self): From 54be749de62cbc5ee2847a71d6ddb4a02c7ff04c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 17:55:25 +0100 Subject: [PATCH 145/483] safe string formatting --- .../tvpaint/plugins/create/create_render.py | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index d2eb693ab9..ebce695801 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -389,11 +389,16 @@ class CreateRenderPass(TVPaintCreator): subset_name_fill_data = {"renderlayer": render_layer} # Format dynamic keys in subset name - new_subset_name = subset_name.format( - **prepare_template_data(subset_name_fill_data) - ) - self.log.info(f"New subset name is \"{new_subset_name}\".") - instance_data["label"] = new_subset_name + label = subset_name + try: + label = label.format( + **prepare_template_data(subset_name_fill_data) + ) + except (KeyError, ValueError): + pass + + self.log.info(f"New subset name is \"{label}\".") + instance_data["label"] = label instance_data["group"] = f"{self.get_group_label()} ({render_layer})" instance_data["layer_names"] = list(selected_layer_names) if "creator_attributes" not in instance_data: @@ -580,9 +585,14 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): ) def _get_label(self, subset_name, render_pass_name): - return subset_name.format(**prepare_template_data({ - "renderpass": render_pass_name - })) + try: + subset_name = subset_name.format(**prepare_template_data({ + "renderpass": render_pass_name + })) + except (KeyError, ValueError): + pass + + return subset_name def get_instance_attr_defs(self): return [ From 8441a5c54b64b14f1d3cd1911c89be36c7f46f3a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 18:14:47 +0100 Subject: [PATCH 146/483] added at least some icons to creators --- openpype/hosts/tvpaint/plugins/create/create_render.py | 5 +++-- openpype/hosts/tvpaint/plugins/create/create_review.py | 1 + openpype/hosts/tvpaint/plugins/create/create_workfile.py | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index ebce695801..e8d6d2bb88 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -61,7 +61,7 @@ class CreateRenderlayer(TVPaintCreator): family = "render" subset_template_family_filter = "renderLayer" identifier = "render.layer" - icon = "fa.cube" + icon = "fa5.images" # George script to change color group rename_script_template = ( @@ -266,11 +266,11 @@ class CreateRenderlayer(TVPaintCreator): class CreateRenderPass(TVPaintCreator): - icon = "fa.cube" family = "render" subset_template_family_filter = "renderPass" identifier = "render.pass" label = "Render Pass" + icon = "fa5.image" order = CreateRenderlayer.order + 10 @@ -496,6 +496,7 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): subset_template_family_filter = "renderScene" identifier = "render.scene" label = "Scene Render" + icon = "fa.file-image-o" # Settings default_variant = "Main" diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index 1c220831bf..1172b53032 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -7,6 +7,7 @@ class TVPaintReviewCreator(TVPaintAutoCreator): family = "review" identifier = "scene.review" label = "Review" + icon = "ei.video" default_variant = "Main" diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index d968e4f77d..7e8978e73a 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -7,6 +7,7 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): family = "workfile" identifier = "workfile" label = "Workfile" + icon = "fa.file-o" default_variant = "Main" From 7d7c8a8d74ef201e4e063a6454ca8b15ac6483dc Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 18:26:31 +0100 Subject: [PATCH 147/483] added some dosctrings, comments and descriptions --- .../tvpaint/plugins/create/convert_legacy.py | 18 ++++++++ .../tvpaint/plugins/create/create_render.py | 46 ++++++++++++++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/convert_legacy.py b/openpype/hosts/tvpaint/plugins/create/convert_legacy.py index 215c87f3e5..538c6e4c5e 100644 --- a/openpype/hosts/tvpaint/plugins/create/convert_legacy.py +++ b/openpype/hosts/tvpaint/plugins/create/convert_legacy.py @@ -9,6 +9,14 @@ from openpype.hosts.tvpaint.api.lib import get_groups_data class TVPaintLegacyConverted(SubsetConvertorPlugin): + """Conversion of legacy instances in scene to new creators. + + This convertor handles only instances created by core creators. + + All instances that would be created using auto-creators are removed as at + the moment of finding them would there already be existing instances. + """ + identifier = "tvpaint.legacy.converter" def find_instances(self): @@ -68,6 +76,7 @@ class TVPaintLegacyConverted(SubsetConvertorPlugin): if not render_layers: return + # Look for possible existing render layers in scene render_layers_by_group_id = {} for instance in current_instances: if instance.get("creator_identifier") == "render.layer": @@ -80,22 +89,30 @@ class TVPaintLegacyConverted(SubsetConvertorPlugin): } for render_layer in render_layers: group_id = render_layer.pop("group_id") + # Just remove legacy instance if group is already occupied if group_id in render_layers_by_group_id: render_layer["keep"] = False continue + # Add identifier render_layer["creator_identifier"] = "render.layer" + # Change 'uuid' to 'instance_id' render_layer["instance_id"] = render_layer.pop("uuid") + # Fill creator attributes render_layer["creator_attributes"] = { "group_id": group_id } render_layer["family"] = "render" group = groups_by_id[group_id] + # Use group name for variant group["variant"] = group["name"] def _convert_render_passes(self, render_passes, current_instances): if not render_passes: return + # Render passes must have available render layers so we look for render + # layers first + # - '_convert_render_layers' must be called before this method render_layers_by_group_id = {} for instance in current_instances: if instance.get("creator_identifier") == "render.layer": @@ -119,6 +136,7 @@ class TVPaintLegacyConverted(SubsetConvertorPlugin): render_pass["variant"] = render_pass.pop("pass") render_pass.pop("renderlayer") + # Rest of instances are just marked for deletion def _convert_render_scenes(self, render_scenes, current_instances): for render_scene in render_scenes: render_scene["keep"] = False diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index e8d6d2bb88..87d9014922 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -25,6 +25,10 @@ default 'color' blend more. In that case it is not recommended to use this workflow at all as other blend modes may affect all layers in clip which can't be done. +There is special case for simple publishing of scene which is called +'render.scene'. That will use all visible layers and render them as one big +sequence. + Todos: Add option to extract marked layers and passes as json output format for AfterEffects. @@ -53,9 +57,42 @@ from openpype.hosts.tvpaint.api.lib import ( execute_george_through_file, ) +RENDER_LAYER_DETAILED_DESCRIPTIONS = ( +"""Render Layer is "a group of TVPaint layers" + +Be aware Render Layer is not TVPaint layer. + +All TVPaint layers in the scene with the color group id are rendered in the +beauty pass. To create sub passes use Render Layer creator which is +dependent on existence of render layer instance. + +The group can represent an asset (tree) or different part of scene that consist +of one or more TVPaint layers that can be used as single item during +compositing (for example). + +In some cases may be needed to have sub parts of the layer. For example 'Bob' +could be Render Layer which has 'Arm', 'Head' and 'Body' as Render Passes. +""" +) + + +RENDER_PASS_DETAILED_DESCRIPTIONS = ( +"""Render Pass is sub part of Rende Layer. + +Render Pass can consist of one or more TVPaint layers. Render Layers must +belong to a Render Layer. Marker TVPaint layers will change it's group color +to match group color of Render Layer. +""" +) + class CreateRenderlayer(TVPaintCreator): - """Mark layer group as one instance.""" + """Mark layer group as Render layer instance. + + All TVPaint layers in the scene with the color group id are rendered in the + beauty pass. To create sub passes use Render Layer creator which is + dependent on existence of render layer instance. + """ label = "Render Layer" family = "render" @@ -68,10 +105,15 @@ class CreateRenderlayer(TVPaintCreator): "tv_layercolor \"setcolor\"" " {clip_id} {group_id} {r} {g} {b} \"{name}\"" ) + # Order to be executed before Render Pass creator order = 90 + description = "Mark TVPaint color group as one Render Layer." + detailed_description = RENDER_LAYER_DETAILED_DESCRIPTIONS # Settings + # - Default render pass name for beauty render_pass = "beauty" + # - Mark by default instance for review mark_for_review = True def get_dynamic_data( @@ -271,6 +313,8 @@ class CreateRenderPass(TVPaintCreator): identifier = "render.pass" label = "Render Pass" icon = "fa5.image" + description = "Mark selected TVPaint layers as pass of Render Layer." + detailed_description = RENDER_PASS_DETAILED_DESCRIPTIONS order = CreateRenderlayer.order + 10 From d440956b4ecee14b9d1ca8962f1c523112c6c9d7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 18:26:43 +0100 Subject: [PATCH 148/483] fix of empty chars --- openpype/hosts/tvpaint/plugins/create/create_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 87d9014922..21f6f86eb6 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -71,7 +71,7 @@ of one or more TVPaint layers that can be used as single item during compositing (for example). In some cases may be needed to have sub parts of the layer. For example 'Bob' -could be Render Layer which has 'Arm', 'Head' and 'Body' as Render Passes. +could be Render Layer which has 'Arm', 'Head' and 'Body' as Render Passes. """ ) @@ -81,7 +81,7 @@ RENDER_PASS_DETAILED_DESCRIPTIONS = ( Render Pass can consist of one or more TVPaint layers. Render Layers must belong to a Render Layer. Marker TVPaint layers will change it's group color -to match group color of Render Layer. +to match group color of Render Layer. """ ) From f73b84d6c36bdf31b48acd01292404400eee0196 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 18:40:33 +0100 Subject: [PATCH 149/483] added some basic options for settings --- openpype/hosts/tvpaint/plugins/create/create_render.py | 8 +++++--- openpype/hosts/tvpaint/plugins/create/create_workfile.py | 4 ++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 21f6f86eb6..255f2605aa 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -112,7 +112,7 @@ class CreateRenderlayer(TVPaintCreator): # Settings # - Default render pass name for beauty - render_pass = "beauty" + default_pass_name = "beauty" # - Mark by default instance for review mark_for_review = True @@ -122,7 +122,7 @@ class CreateRenderlayer(TVPaintCreator): dynamic_data = super().get_dynamic_data( variant, task_name, asset_doc, project_name, host_name, instance ) - dynamic_data["renderpass"] = self.render_pass + dynamic_data["renderpass"] = self.default_pass_name dynamic_data["renderlayer"] = variant return dynamic_data @@ -543,9 +543,9 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): icon = "fa.file-image-o" # Settings - default_variant = "Main" default_pass_name = "beauty" mark_for_review = True + active_on_create = False def get_dynamic_data(self, variant, *args, **kwargs): dynamic_data = super().get_dynamic_data(variant, *args, **kwargs) @@ -581,6 +581,8 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): self.default_pass_name ) } + if not self.active_on_create: + data["active"] = False new_instance = CreatedInstance( self.family, subset_name, data, self diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index 7e8978e73a..152d29cf6f 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -9,6 +9,8 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): label = "Workfile" icon = "fa.file-o" + # Settings + active_on_create = True default_variant = "Main" def create(self): @@ -38,6 +40,8 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): "task": task_name, "variant": self.default_variant } + if not self.active_on_create: + data["active"] = False new_instance = CreatedInstance( self.family, subset_name, data, self From 9bb6c606985aa936279633f4b5bef2f7af1cbb71 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 18:40:56 +0100 Subject: [PATCH 150/483] added settings for tvpaint creators --- .../defaults/project_settings/tvpaint.json | 32 ++++ .../schema_project_tvpaint.json | 171 ++++++++++++++++++ 2 files changed, 203 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 5a3e1dc2df..0441b2da00 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -10,6 +10,38 @@ } }, "stop_timer_on_application_exit": false, + "create": { + "create_workfile": { + "enabled": true, + "default_variant": "Main", + "default_variants": [] + }, + "create_review": { + "enabled": true, + "active_on_create": true, + "default_variant": "Main", + "default_variants": [] + }, + "create_render_scene": { + "enabled": true, + "active_on_create": false, + "mark_for_review": true, + "default_pass_name": "beauty", + "default_variant": "Main", + "default_variants": [] + }, + "create_render_layer": { + "mark_for_review": true, + "render_pass_name": "beauty", + "default_variant": "Main", + "default_variants": [] + }, + "create_render_pass": { + "mark_for_review": true, + "default_variant": "Main", + "default_variants": [] + } + }, "publish": { "CollectRenderScene": { "enabled": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index db38c938dc..10f4b538f7 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -27,6 +27,177 @@ "key": "stop_timer_on_application_exit", "label": "Stop timer on application exit" }, + { + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Create plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "create_workfile", + "label": "Create Workfile", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "create_review", + "label": "Create Review", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active_on_create", + "label": "Active by default" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "create_render_scene", + "label": "Create Render Scene", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active_on_create", + "label": "Active by default" + }, + { + "type": "boolean", + "key": "mark_for_review", + "label": "Review by default" + }, + { + "type": "text", + "key": "default_pass_name", + "label": "Default beauty pass" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "create_render_layer", + "label": "Create Render Layer", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "mark_for_review", + "label": "Review by default" + }, + { + "type": "text", + "key": "render_pass_name", + "label": "Default beauty pass" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "create_render_pass", + "label": "Create Render Pass", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "mark_for_review", + "label": "Review by default" + }, + { + "type": "text", + "key": "default_variant", + "label": "Default variant" + }, + { + "type": "list", + "key": "default_variants", + "label": "Default variants", + "object_type": { + "type": "text" + } + } + ] + } + ] + }, { "type": "dict", "collapsible": true, From 51196290b2419d37b3c71ead5501d918a47c9211 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 15 Feb 2023 18:46:48 +0100 Subject: [PATCH 151/483] apply settings in creators --- .../tvpaint/plugins/create/create_render.py | 27 +++++++++++++++++++ .../tvpaint/plugins/create/create_review.py | 5 +++- .../tvpaint/plugins/create/create_workfile.py | 7 ++++- .../defaults/project_settings/tvpaint.json | 2 +- .../schema_project_tvpaint.json | 2 +- 5 files changed, 39 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 255f2605aa..fa724fabe2 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -116,6 +116,15 @@ class CreateRenderlayer(TVPaintCreator): # - Mark by default instance for review mark_for_review = True + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["tvpain"]["create"]["create_render_layer"] + ) + self.default_variant = plugin_settings["default_variant"] + self.default_variants = plugin_settings["default_variants"] + self.default_pass_name = plugin_settings["default_pass_name"] + self.mark_for_review = plugin_settings["mark_for_review"] + def get_dynamic_data( self, variant, task_name, asset_doc, project_name, host_name, instance ): @@ -321,6 +330,14 @@ class CreateRenderPass(TVPaintCreator): # Settings mark_for_review = True + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["tvpain"]["create"]["create_render_pass"] + ) + self.default_variant = plugin_settings["default_variant"] + self.default_variants = plugin_settings["default_variants"] + self.mark_for_review = plugin_settings["mark_for_review"] + def collect_instances(self): instances_by_identifier = self._cache_and_get_instances() render_layers = { @@ -547,6 +564,16 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): mark_for_review = True active_on_create = False + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings["tvpain"]["create"]["create_render_scene"] + ) + self.default_variant = plugin_settings["default_variant"] + self.default_variants = plugin_settings["default_variants"] + self.mark_for_review = plugin_settings["mark_for_review"] + self.active_on_create = plugin_settings["active_on_create"] + self.default_pass_name = plugin_settings["default_pass_name"] + def get_dynamic_data(self, variant, *args, **kwargs): dynamic_data = super().get_dynamic_data(variant, *args, **kwargs) dynamic_data["renderpass"] = "{renderpass}" diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index 1172b53032..a0af10f3be 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -9,7 +9,10 @@ class TVPaintReviewCreator(TVPaintAutoCreator): label = "Review" icon = "ei.video" - default_variant = "Main" + def apply_settings(self, project_settings, system_settings): + plugin_settings = project_settings["tvpain"]["create"]["create_review"] + self.default_variant = plugin_settings["default_variant"] + self.default_variants = plugin_settings["default_variants"] def create(self): existing_instance = None diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index 152d29cf6f..e247072e3b 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -11,7 +11,12 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): # Settings active_on_create = True - default_variant = "Main" + + def apply_settings(self, project_settings, system_settings): + plugin_settings = project_settings["tvpain"]["create"]["create_workfile"] + self.default_variant = plugin_settings["default_variant"] + self.default_variants = plugin_settings["default_variants"] + self.active_on_create = plugin_settings["active_on_create"] def create(self): existing_instance = None diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 0441b2da00..74a5af403c 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -32,7 +32,7 @@ }, "create_render_layer": { "mark_for_review": true, - "render_pass_name": "beauty", + "default_pass_name": "beauty", "default_variant": "Main", "default_variants": [] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 10f4b538f7..d09c666d50 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -151,7 +151,7 @@ }, { "type": "text", - "key": "render_pass_name", + "key": "default_pass_name", "label": "Default beauty pass" }, { From a651553fe6847af29c5ee4becc02304cf2ee08e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 09:41:44 +0100 Subject: [PATCH 152/483] fix settings access --- openpype/hosts/tvpaint/plugins/create/create_render.py | 6 +++--- openpype/hosts/tvpaint/plugins/create/create_review.py | 4 +++- openpype/hosts/tvpaint/plugins/create/create_workfile.py | 4 +++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index fa724fabe2..cadd045fbf 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -118,7 +118,7 @@ class CreateRenderlayer(TVPaintCreator): def apply_settings(self, project_settings, system_settings): plugin_settings = ( - project_settings["tvpain"]["create"]["create_render_layer"] + project_settings["tvpaint"]["create"]["create_render_layer"] ) self.default_variant = plugin_settings["default_variant"] self.default_variants = plugin_settings["default_variants"] @@ -332,7 +332,7 @@ class CreateRenderPass(TVPaintCreator): def apply_settings(self, project_settings, system_settings): plugin_settings = ( - project_settings["tvpain"]["create"]["create_render_pass"] + project_settings["tvpaint"]["create"]["create_render_pass"] ) self.default_variant = plugin_settings["default_variant"] self.default_variants = plugin_settings["default_variants"] @@ -566,7 +566,7 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): def apply_settings(self, project_settings, system_settings): plugin_settings = ( - project_settings["tvpain"]["create"]["create_render_scene"] + project_settings["tvpaint"]["create"]["create_render_scene"] ) self.default_variant = plugin_settings["default_variant"] self.default_variants = plugin_settings["default_variants"] diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index a0af10f3be..423c3ab30f 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -10,7 +10,9 @@ class TVPaintReviewCreator(TVPaintAutoCreator): icon = "ei.video" def apply_settings(self, project_settings, system_settings): - plugin_settings = project_settings["tvpain"]["create"]["create_review"] + plugin_settings = ( + project_settings["tvpaint"]["create"]["create_review"] + ) self.default_variant = plugin_settings["default_variant"] self.default_variants = plugin_settings["default_variants"] diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index e247072e3b..cc64936bdd 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -13,7 +13,9 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): active_on_create = True def apply_settings(self, project_settings, system_settings): - plugin_settings = project_settings["tvpain"]["create"]["create_workfile"] + plugin_settings = ( + project_settings["tvpaint"]["create"]["create_workfile"] + ) self.default_variant = plugin_settings["default_variant"] self.default_variants = plugin_settings["default_variants"] self.active_on_create = plugin_settings["active_on_create"] From 69cc794ed1492e2c22c84ee8ad2499a23ecbf023 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 09:41:55 +0100 Subject: [PATCH 153/483] fix indentation --- openpype/hosts/tvpaint/plugins/create/create_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index cadd045fbf..41288e5968 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -58,7 +58,7 @@ from openpype.hosts.tvpaint.api.lib import ( ) RENDER_LAYER_DETAILED_DESCRIPTIONS = ( -"""Render Layer is "a group of TVPaint layers" + """Render Layer is "a group of TVPaint layers" Be aware Render Layer is not TVPaint layer. @@ -77,7 +77,7 @@ could be Render Layer which has 'Arm', 'Head' and 'Body' as Render Passes. RENDER_PASS_DETAILED_DESCRIPTIONS = ( -"""Render Pass is sub part of Rende Layer. + """Render Pass is sub part of Render Layer. Render Pass can consist of one or more TVPaint layers. Render Layers must belong to a Render Layer. Marker TVPaint layers will change it's group color From 71adfb8b5044e38ffc0c5edcb39f0eeb54e2ed18 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 09:56:07 +0100 Subject: [PATCH 154/483] update instance label on refresh --- openpype/tools/publisher/widgets/card_view_widgets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/tools/publisher/widgets/card_view_widgets.py b/openpype/tools/publisher/widgets/card_view_widgets.py index 47f8ebb914..3fd5243ce9 100644 --- a/openpype/tools/publisher/widgets/card_view_widgets.py +++ b/openpype/tools/publisher/widgets/card_view_widgets.py @@ -385,6 +385,7 @@ class InstanceCardWidget(CardWidget): self._last_subset_name = None self._last_variant = None + self._last_label = None icon_widget = IconValuePixmapLabel(group_icon, self) icon_widget.setObjectName("FamilyIconLabel") @@ -462,14 +463,17 @@ class InstanceCardWidget(CardWidget): def _update_subset_name(self): variant = self.instance["variant"] subset_name = self.instance["subset"] + label = self.instance.label if ( variant == self._last_variant and subset_name == self._last_subset_name + and label == self._last_label ): return self._last_variant = variant self._last_subset_name = subset_name + self._last_label = label # Make `variant` bold label = html_escape(self.instance.label) found_parts = set(re.findall(variant, label, re.IGNORECASE)) From 2fce95a9df1cf89fb801e1228698be03ad93aaa0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 16 Feb 2023 16:10:03 +0100 Subject: [PATCH 155/483] OP-3026 - first implementation of saving settings changes to DB This logs all changes in Settings to separate collection ('setting_log') to help debug/trace changes. This functionality is pretty basic as it will be replaced by Ayon implementation in the future. --- openpype/settings/handlers.py | 34 ++++++++++++++++++++++++++++++++++ openpype/settings/lib.py | 4 +++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 373029d9df..3706433692 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -235,6 +235,18 @@ class SettingsHandler(object): """ pass + @abstractmethod + def save_change_log(self, project_name, changes, settings_type): + """Stores changes to settings to separate logging collection. + + Args: + project_name(str, null): Project name for which overrides are + or None for global settings. + changes(dict): Data of project overrides with override metadata. + settings_type (str): system|project|anatomy + """ + pass + @abstractmethod def get_studio_system_settings_overrides(self, return_version): """Studio overrides of system settings.""" @@ -913,6 +925,28 @@ class MongoSettingsHandler(SettingsHandler): return data + def save_change_log(self, project_name, changes, settings_type): + """Log all settings changes to separate collection""" + if not changes: + return + + from openpype.lib import get_local_site_id + + if settings_type == "project" and not project_name: + project_name = "default" + + document = { + "user": get_local_site_id(), + "date_created": datetime.datetime.now(), + "project": project_name, + "settings_type": settings_type, + "changes": changes + } + collection_name = "settings_log" + collection = (self.settings_collection[self.database_name] + [collection_name]) + collection.insert_one(document) + def _save_project_anatomy_data(self, project_name, data_cache): # Create copy of data as they will be modified during save new_data = data_cache.data_copy() diff --git a/openpype/settings/lib.py b/openpype/settings/lib.py index 796eaeda01..73554df236 100644 --- a/openpype/settings/lib.py +++ b/openpype/settings/lib.py @@ -159,6 +159,7 @@ def save_studio_settings(data): except SaveWarningExc as exc: warnings.extend(exc.warnings) + _SETTINGS_HANDLER.save_change_log(None, changes, "system") _SETTINGS_HANDLER.save_studio_settings(data) if warnings: raise SaveWarningExc(warnings) @@ -218,7 +219,7 @@ def save_project_settings(project_name, overrides): ) except SaveWarningExc as exc: warnings.extend(exc.warnings) - + _SETTINGS_HANDLER.save_change_log(project_name, changes, "project") _SETTINGS_HANDLER.save_project_settings(project_name, overrides) if warnings: @@ -280,6 +281,7 @@ def save_project_anatomy(project_name, anatomy_data): except SaveWarningExc as exc: warnings.extend(exc.warnings) + _SETTINGS_HANDLER.save_change_log(project_name, changes, "anatomy") _SETTINGS_HANDLER.save_project_anatomy(project_name, anatomy_data) if warnings: From 43f88ca797b286f6dfaf0bcc8f1a45e099f4886c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 16 Feb 2023 16:31:27 +0100 Subject: [PATCH 156/483] OP-3026 - Hound --- openpype/settings/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 3706433692..3fa5737b6f 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -238,7 +238,7 @@ class SettingsHandler(object): @abstractmethod def save_change_log(self, project_name, changes, settings_type): """Stores changes to settings to separate logging collection. - + Args: project_name(str, null): Project name for which overrides are or None for global settings. From 7109c6ea1a483ed4b95a77e3dec2e379e569a2c2 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 17 Feb 2023 16:20:26 +0800 Subject: [PATCH 157/483] update the naming convention for the render outputs --- openpype/hosts/max/api/lib_renderproducts.py | 8 ++- openpype/hosts/max/api/lib_rendersettings.py | 25 ++++++- .../plugins/publish/submit_3dmax_deadline.py | 66 +++++++++++++++++-- .../plugins/publish/submit_publish_job.py | 5 +- .../defaults/project_settings/deadline.json | 3 +- .../defaults/project_settings/max.json | 2 +- .../schema_project_deadline.json | 5 ++ 7 files changed, 104 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index b54e2513e1..e09934e5de 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -25,11 +25,17 @@ class RenderProducts(object): def render_product(self, container): folder = rt.maxFilePath + file = rt.maxFileName folder = folder.replace("\\", "/") setting = self._project_settings render_folder = get_default_render_folder(setting) + filename, ext = os.path.splitext(file) + + output_file = os.path.join(folder, + render_folder, + filename, + container) - output_file = os.path.join(folder, render_folder, container) context = get_current_project_asset() startFrame = context["data"].get("frameStart") endFrame = context["data"].get("frameEnd") + 1 diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index bc9b02bc77..d4784d9dfb 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -44,11 +44,15 @@ class RenderSettings(object): def set_renderoutput(self, container): folder = rt.maxFilePath # hard-coded, should be customized in the setting + file = rt.maxFileName folder = folder.replace("\\", "/") # hard-coded, set the renderoutput path setting = self._project_settings render_folder = get_default_render_folder(setting) - output_dir = os.path.join(folder, render_folder) + filename, ext = os.path.splitext(file) + output_dir = os.path.join(folder, + render_folder, + filename) if not os.path.exists(output_dir): os.makedirs(output_dir) # hard-coded, should be customized in the setting @@ -139,3 +143,22 @@ class RenderSettings(object): target, renderpass = str(renderlayer_name).split(":") aov_name = "{0}_{1}..{2}".format(dir, renderpass, ext) render_elem.SetRenderElementFileName(i, aov_name) + + def get_renderoutput(self, container, output_dir): + output = os.path.join(output_dir, container) + img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa + outputFilename = "{0}..{1}".format(output, img_fmt) + return outputFilename + + def get_render_element(self): + orig_render_elem = list() + render_elem = rt.maxOps.GetCurRenderElementMgr() + render_elem_num = render_elem.NumRenderElements() + if render_elem_num < 0: + return + + for i in range(render_elem_num): + render_element = render_elem.GetRenderElementFilename(i) + orig_render_elem.append(render_element) + + return orig_render_elem diff --git a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py index dec951da7a..9316e34898 100644 --- a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py @@ -5,8 +5,10 @@ import getpass import requests import pyblish.api - +from pymxs import runtme as rt from openpype.pipeline import legacy_io +from openpype.hosts.max.api.lib import get_current_renderer +from openpype.hosts.max.api.lib_rendersettings import RenderSettings class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): @@ -26,6 +28,7 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): group = None deadline_pool = None deadline_pool_secondary = None + framePerTask = 1 def process(self, instance): context = instance.context @@ -55,10 +58,30 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): anatomy_filled = anatomy_data.format(template_data) template_filled = anatomy_filled["publish"]["path"] filepath = os.path.normpath(template_filled) - + filepath = filepath.replace("\\", "/") self.log.info( "Using published scene for render {}".format(filepath) ) + if not os.path.exists(filepath): + self.log.error("published scene does not exist!") + + new_scene = self._clean_name(filepath) + # use the anatomy data for setting up the path of the files + orig_scene = self._clean_name(instance.context.data["currentFile"]) + expected_files = instance.data.get("expectedFiles") + + new_exp = [] + for file in expected_files: + new_file = str(file).replace(orig_scene, new_scene) + new_exp.append(new_file) + + instance.data["expectedFiles"] = new_exp + + metadata_folder = instance.data.get("publishRenderMetadataFolder") + if metadata_folder: + metadata_folder = metadata_folder.replace(orig_scene, + new_scene) + instance.data["publishRenderMetadataFolder"] = metadata_folder payload = { "JobInfo": { @@ -78,7 +101,8 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): "Frames": frames, "ChunkSize": self.chunk_size, "Priority": instance.data.get("priority", self.priority), - "Comment": comment + "Comment": comment, + "FramesPerTask": self.framePerTask }, "PluginInfo": { # Input @@ -131,8 +155,37 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): self.log.info("Ensuring output directory exists: %s" % dirname) os.makedirs(dirname) + plugin_data = {} + if self.use_published: + old_output_dir = os.path.dirname(expected_files[0]) + output_beauty = RenderSettings().get_renderoutput(instance.name, + old_output_dir) + output_beauty = output_beauty.replace(orig_scene, new_scene) + output_beauty = output_beauty.replace("\\", "/") + plugin_data["RenderOutput"] = output_beauty + + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] + if ( + renderer == "ART_Renderer" or + renderer == "Redshift_Renderer" or + renderer == "V_Ray_6_Hotfix_3" or + renderer == "V_Ray_GPU_6_Hotfix_3" or + renderer == "Default_Scanline_Renderer" or + renderer == "Quicksilver_Hardware_Renderer" + ): + render_elem_list = RenderSettings().get_render_element() + for i, render_element in enumerate(render_elem_list): + render_element = render_element.replace(orig_scene, new_scene) + plugin_data["RenderElementOutputFilename%d" % i] = render_element + + self.log.debug("plugin data:{}".format(plugin_data)) + self.log.info("Scene name was switched {} -> {}".format( + orig_scene, new_scene + )) payload["JobInfo"].update(output_data) + payload["PluginInfo"].update(plugin_data) self.submit(instance, payload) @@ -158,8 +211,13 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): raise Exception(response.text) # Store output dir for unified publisher (expectedFilesequence) expected_files = instance.data["expectedFiles"] - self.log.info("exp:{}".format(expected_files)) output_dir = os.path.dirname(expected_files[0]) instance.data["toBeRenderedOn"] = "deadline" instance.data["outputDir"] = output_dir instance.data["deadlineSubmissionJob"] = response.json() + + def rename_render_element(self): + pass + + def _clean_name(self, path): + return os.path.splitext(os.path.basename(path))[0] \ No newline at end of file diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index b70301ab7e..34fa8a8c03 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -293,8 +293,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "Group": self.deadline_group, "Pool": instance.data.get("primaryPool"), "SecondaryPool": instance.data.get("secondaryPool"), - - "OutputDirectory0": output_dir + # ensure the outputdirectory with correct slashes + "OutputDirectory0": output_dir.replace("\\", "/") }, "PluginInfo": { "Version": self.plugin_pype_version, @@ -1000,6 +1000,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "FTRACK_SERVER": os.environ.get("FTRACK_SERVER"), } + submission_type = instance.data["toBeRenderedOn"] if submission_type == "deadline": # get default deadline webservice url from deadline module self.deadline_url = instance.context.data["defaultDeadline"] diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 0fab284c66..25d2988982 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -44,7 +44,8 @@ "chunk_size": 10, "group": "none", "deadline_pool": "", - "deadline_pool_secondary": "" + "deadline_pool_secondary": "", + "framePerTask": 1 }, "NukeSubmitDeadline": { "enabled": true, diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 651a074a08..617e298310 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -1,6 +1,6 @@ { "RenderSettings": { - "default_render_image_folder": "renders/max", + "default_render_image_folder": "renders/3dsmax", "aov_separator": "underscore", "image_format": "exr" } 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 afefd3266a..f71a253105 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -249,6 +249,11 @@ "type": "text", "key": "deadline_pool_secondary", "label": "Deadline pool (secondary)" + }, + { + "type": "number", + "key": "framePerTask", + "label": "Frame Per Task" } ] }, From f1843b1120575e5a40c87c09ccc222e785c23d91 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 17 Feb 2023 16:24:19 +0800 Subject: [PATCH 158/483] hound fix --- openpype/hosts/max/api/lib_rendersettings.py | 4 ++-- .../deadline/plugins/publish/submit_3dmax_deadline.py | 9 ++++----- .../deadline/plugins/publish/submit_publish_job.py | 3 +-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index d4784d9dfb..92f716ba54 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -158,7 +158,7 @@ class RenderSettings(object): return for i in range(render_elem_num): - render_element = render_elem.GetRenderElementFilename(i) - orig_render_elem.append(render_element) + render_element = render_elem.GetRenderElementFilename(i) + orig_render_elem.append(render_element) return orig_render_elem diff --git a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py index 9316e34898..656f550e9e 100644 --- a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py @@ -5,7 +5,6 @@ import getpass import requests import pyblish.api -from pymxs import runtme as rt from openpype.pipeline import legacy_io from openpype.hosts.max.api.lib import get_current_renderer from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -175,9 +174,9 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): renderer == "Quicksilver_Hardware_Renderer" ): render_elem_list = RenderSettings().get_render_element() - for i, render_element in enumerate(render_elem_list): - render_element = render_element.replace(orig_scene, new_scene) - plugin_data["RenderElementOutputFilename%d" % i] = render_element + for i, element in enumerate(render_elem_list): + element = element.replace(orig_scene, new_scene) + plugin_data["RenderElementOutputFilename%d" % i] = element # noqa self.log.debug("plugin data:{}".format(plugin_data)) self.log.info("Scene name was switched {} -> {}".format( @@ -220,4 +219,4 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): pass def _clean_name(self, path): - return os.path.splitext(os.path.basename(path))[0] \ No newline at end of file + return os.path.splitext(os.path.basename(path))[0] diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 34fa8a8c03..c347f50c59 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -339,7 +339,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10) + response = requests.post(url, json=payload, timeout=10, verify=False) if not response.ok: raise Exception(response.text) @@ -1000,7 +1000,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "FTRACK_SERVER": os.environ.get("FTRACK_SERVER"), } - submission_type = instance.data["toBeRenderedOn"] if submission_type == "deadline": # get default deadline webservice url from deadline module self.deadline_url = instance.context.data["defaultDeadline"] From bf3bf31d999c179e2a88e850e4f7db8e6c7082d3 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 17 Feb 2023 16:40:43 +0800 Subject: [PATCH 159/483] fix the bug of reference before assignment --- .../modules/deadline/plugins/publish/submit_publish_job.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index c347f50c59..22a5069ac2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -339,7 +339,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self.log.info("Submitting Deadline job ...") url = "{}/api/jobs".format(self.deadline_url) - response = requests.post(url, json=payload, timeout=10, verify=False) + response = requests.post(url, json=payload, timeout=10) if not response.ok: raise Exception(response.text) @@ -961,6 +961,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): ''' render_job = None + submission_type = "" if instance.data.get("toBeRenderedOn") == "deadline": render_job = data.pop("deadlineSubmissionJob", None) submission_type = "deadline" From 40ce393b808a60d37454712cbaa44609afe379d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 17 Feb 2023 12:28:32 +0100 Subject: [PATCH 160/483] swap workfile and review settings --- openpype/hosts/tvpaint/plugins/create/create_review.py | 4 ++++ openpype/hosts/tvpaint/plugins/create/create_workfile.py | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index 423c3ab30f..0164d58262 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -9,12 +9,16 @@ class TVPaintReviewCreator(TVPaintAutoCreator): label = "Review" icon = "ei.video" + # Settings + active_on_create = True + def apply_settings(self, project_settings, system_settings): plugin_settings = ( project_settings["tvpaint"]["create"]["create_review"] ) self.default_variant = plugin_settings["default_variant"] self.default_variants = plugin_settings["default_variants"] + self.active_on_create = plugin_settings["active_on_create"] def create(self): existing_instance = None diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index cc64936bdd..d56a6c59e7 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -9,16 +9,12 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): label = "Workfile" icon = "fa.file-o" - # Settings - active_on_create = True - def apply_settings(self, project_settings, system_settings): plugin_settings = ( project_settings["tvpaint"]["create"]["create_workfile"] ) self.default_variant = plugin_settings["default_variant"] self.default_variants = plugin_settings["default_variants"] - self.active_on_create = plugin_settings["active_on_create"] def create(self): existing_instance = None From 173b404d0e2af1712f9e7cf4d9f60ac5ea030570 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 17 Feb 2023 13:06:52 +0100 Subject: [PATCH 161/483] swap also usage of settings in review and workfile creators --- openpype/hosts/tvpaint/plugins/create/create_review.py | 2 ++ openpype/hosts/tvpaint/plugins/create/create_workfile.py | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_review.py b/openpype/hosts/tvpaint/plugins/create/create_review.py index 0164d58262..886dae7c39 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_review.py +++ b/openpype/hosts/tvpaint/plugins/create/create_review.py @@ -47,6 +47,8 @@ class TVPaintReviewCreator(TVPaintAutoCreator): "task": task_name, "variant": self.default_variant } + if not self.active_on_create: + data["active"] = False new_instance = CreatedInstance( self.family, subset_name, data, self diff --git a/openpype/hosts/tvpaint/plugins/create/create_workfile.py b/openpype/hosts/tvpaint/plugins/create/create_workfile.py index d56a6c59e7..41347576d5 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_workfile.py +++ b/openpype/hosts/tvpaint/plugins/create/create_workfile.py @@ -43,8 +43,6 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): "task": task_name, "variant": self.default_variant } - if not self.active_on_create: - data["active"] = False new_instance = CreatedInstance( self.family, subset_name, data, self From a28cc008302328fe7b615551a567cdd8496baa94 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 17 Feb 2023 21:13:14 +0800 Subject: [PATCH 162/483] add multipass as setting and regex filter in publish job setting --- openpype/hosts/max/api/lib.py | 5 +++++ .../plugins/publish/submit_3dmax_deadline.py | 17 ++++++++++++++++- .../plugins/publish/submit_publish_job.py | 3 ++- .../defaults/project_settings/deadline.json | 3 +++ .../settings/defaults/project_settings/max.json | 3 ++- .../projects_schema/schema_project_max.json | 5 +++++ 6 files changed, 33 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 0477b43182..a28ec4b6af 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -151,3 +151,8 @@ def set_framerange(startFrame, endFrame): if startFrame is not None and endFrame is not None: frameRange = "{0}-{1}".format(startFrame, endFrame) rt.rendPickupFrames = frameRange + +def get_multipass_setting(project_setting=None): + return (project_setting["max"] + ["RenderSettings"] + ["multipass"]) diff --git a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py index 656f550e9e..d7716527b1 100644 --- a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py @@ -6,7 +6,11 @@ import requests import pyblish.api from openpype.pipeline import legacy_io -from openpype.hosts.max.api.lib import get_current_renderer +from openpype.settings import get_project_settings +from openpype.hosts.max.api.lib import ( + get_current_renderer, + get_multipass_setting +) from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -154,7 +158,18 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): self.log.info("Ensuring output directory exists: %s" % dirname) os.makedirs(dirname) + plugin_data = {} + project_setting = get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) + + multipass = get_multipass_setting(project_setting) + if multipass: + plugin_data["DisableMultipass"] = 0 + else: + plugin_data["DisableMultipass"] = 1 + if self.use_published: old_output_dir = os.path.dirname(expected_files[0]) output_beauty = RenderSettings().get_renderoutput(instance.name, diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 22a5069ac2..505b940356 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -126,7 +126,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): aov_filter = {"maya": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE "harmony": [r".*"], # for everything from AE - "celaction": [r".*"]} + "celaction": [r".*"], + "max": [r".*"]} environ_job_filter = [ "OPENPYPE_METADATA_FILE" diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 24d1f7405b..7a5903d8e0 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -115,6 +115,9 @@ ], "harmony": [ ".*" + ], + "max": [ + ".*" ] } } diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 617e298310..84e0c7dba7 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -2,6 +2,7 @@ "RenderSettings": { "default_render_image_folder": "renders/3dsmax", "aov_separator": "underscore", - "image_format": "exr" + "image_format": "exr", + "multipass": true } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json index fbd9358c74..8a283c1acc 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_max.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_max.json @@ -44,6 +44,11 @@ {"tga": "tga"}, {"dds": "dds"} ] + }, + { + "type": "boolean", + "key": "multipass", + "label": "multipass" } ] } From 281d53bb01d1da9504be0a19ae0a004804ac8cc9 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 17 Feb 2023 21:14:43 +0800 Subject: [PATCH 163/483] hound fix --- openpype/hosts/max/api/lib.py | 1 + .../modules/deadline/plugins/publish/submit_3dmax_deadline.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index a28ec4b6af..ecea8b5541 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -152,6 +152,7 @@ def set_framerange(startFrame, endFrame): frameRange = "{0}-{1}".format(startFrame, endFrame) rt.rendPickupFrames = frameRange + def get_multipass_setting(project_setting=None): return (project_setting["max"] ["RenderSettings"] diff --git a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py index d7716527b1..ed448abe1f 100644 --- a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py @@ -161,8 +161,8 @@ class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): plugin_data = {} project_setting = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) + legacy_io.Session["AVALON_PROJECT"] + ) multipass = get_multipass_setting(project_setting) if multipass: From 7a2c94f9d511c2d15ca554709beded5fe3055515 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 11:06:37 +0100 Subject: [PATCH 164/483] limit groups query to 26 max --- openpype/hosts/tvpaint/api/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/api/lib.py b/openpype/hosts/tvpaint/api/lib.py index 5e64773b8e..312a211d49 100644 --- a/openpype/hosts/tvpaint/api/lib.py +++ b/openpype/hosts/tvpaint/api/lib.py @@ -202,8 +202,9 @@ def get_groups_data(communicator=None): # Variable containing full path to output file "output_path = \"{}\"".format(output_filepath), "empty = 0", - # Loop over 100 groups - "FOR idx = 1 TO 100", + # Loop over 26 groups which is ATM maximum possible (in 11.7) + # - ref: https://www.tvpaint.com/forum/viewtopic.php?t=13880 + "FOR idx = 1 TO 26", # Receive information about groups "tv_layercolor \"getcolor\" 0 idx", "PARSE result clip_id group_index c_red c_green c_blue group_name", From c3c850f0348de62126689e887cc99b20cdbd538c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 11:06:53 +0100 Subject: [PATCH 165/483] change how groups are filtered for render passes --- .../tvpaint/plugins/create/create_render.py | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 41288e5968..d9355c42fd 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -219,15 +219,30 @@ class CreateRenderlayer(TVPaintCreator): f" was changed to \"{new_group_name}\"." )) - def get_pre_create_attr_defs(self): - groups_enum = [ - { - "label": group["name"], + def _get_groups_enum(self): + groups_enum = [] + empty_groups = [] + for group in get_groups_data(): + group_name = group["name"] + item = { + "label": group_name, "value": group["group_id"] } - for group in get_groups_data() - if group["name"] - ] + # TVPaint have defined how many color groups is available, but + # the count is not consistent across versions. It is not possible + # to know how many groups there is. + # + if group_name and group_name != "0": + if empty_groups: + groups_enum.extend(empty_groups) + empty_groups = [] + groups_enum.append(item) + else: + empty_groups.append(item) + return groups_enum + + def get_pre_create_attr_defs(self): + groups_enum = self._get_groups_enum() groups_enum.insert(0, {"label": "", "value": -1}) return [ @@ -249,14 +264,7 @@ class CreateRenderlayer(TVPaintCreator): ] def get_instance_attr_defs(self): - groups_enum = [ - { - "label": group["name"], - "value": group["group_id"] - } - for group in get_groups_data() - if group["name"] - ] + groups_enum = self._get_groups_enum() return [ EnumDef( "group_id", From 1b69c5e3409d3c67e384eef8c4c1c12054bed2cd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 11:09:24 +0100 Subject: [PATCH 166/483] fix used key --- .../hosts/tvpaint/plugins/publish/validate_scene_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py b/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py index d235215ac9..4473e4b1b7 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py @@ -42,7 +42,7 @@ class ValidateProjectSettings(pyblish.api.ContextPlugin): "expected_width": expected_data["resolutionWidth"], "expected_height": expected_data["resolutionHeight"], "current_width": scene_data["resolutionWidth"], - "current_height": scene_data["resolutionWidth"], + "current_height": scene_data["resolutionHeight"], "expected_pixel_ratio": expected_data["pixelAspect"], "current_pixel_ratio": scene_data["pixelAspect"] } From a189a8df8a1aed35e7b393c4ec8027911d38d47f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 20 Feb 2023 13:23:25 +0100 Subject: [PATCH 167/483] OP-3026 - enhanced log with host info --- openpype/settings/handlers.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/settings/handlers.py b/openpype/settings/handlers.py index 3fa5737b6f..a1f3331ccc 100644 --- a/openpype/settings/handlers.py +++ b/openpype/settings/handlers.py @@ -9,6 +9,7 @@ import six import openpype.version from openpype.client.mongo import OpenPypeMongoConnection from openpype.client.entities import get_project_connection, get_project +from openpype.lib.pype_info import get_workstation_info from .constants import ( GLOBAL_SETTINGS_KEY, @@ -930,13 +931,17 @@ class MongoSettingsHandler(SettingsHandler): if not changes: return - from openpype.lib import get_local_site_id - if settings_type == "project" and not project_name: project_name = "default" + host_info = get_workstation_info() + document = { - "user": get_local_site_id(), + "local_id": host_info["local_id"], + "username": host_info["username"], + "hostname": host_info["hostname"], + "hostip": host_info["hostip"], + "system_name": host_info["system_name"], "date_created": datetime.datetime.now(), "project": project_name, "settings_type": settings_type, From 407068610afc447df075d555aa7b5e9068734f8a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 13:58:20 +0100 Subject: [PATCH 168/483] return created instance in creators --- openpype/hosts/tvpaint/plugins/create/create_render.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index d9355c42fd..71224927d1 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -195,13 +195,13 @@ class CreateRenderlayer(TVPaintCreator): new_group_name = pre_create_data.get("group_name") if not new_group_name or not group_id: - return + return new_instance self.log.debug("Changing name of the group.") new_group_name = pre_create_data.get("group_name") if not new_group_name or group_item["name"] == new_group_name: - return + return new_instance # Rename TVPaint group (keep color same) # - groups can't contain spaces rename_script = self.rename_script_template.format( @@ -218,6 +218,7 @@ class CreateRenderlayer(TVPaintCreator): f"Name of group with index {group_id}" f" was changed to \"{new_group_name}\"." )) + return new_instance def _get_groups_enum(self): groups_enum = [] @@ -497,6 +498,8 @@ class CreateRenderPass(TVPaintCreator): self._add_instance_to_context(new_instance) self._change_layers_group(selected_layers, group_id) + return new_instance + def _change_layers_group(self, layers, group_id): filtered_layers = [ layer From 7486591fab5e9ec0413262c5fdd80be774c6abab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 13:58:46 +0100 Subject: [PATCH 169/483] it is possible to pass layer names to create pass --- .../tvpaint/plugins/create/create_render.py | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 71224927d1..b324beb8f6 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -431,25 +431,40 @@ class CreateRenderPass(TVPaintCreator): self.log.debug("Checking selection.") # Get all selected layers and their group ids - selected_layers = [ - layer - for layer in layers_data - if layer["selected"] - ] + marked_layer_names = pre_create_data.get("layer_names") + if marked_layer_names is not None: + layers_by_name = {layer["name"]: layer for layer in layers_data} + marked_layers = [] + for layer_name in marked_layer_names: + layer = layers_by_name.get(layer_name) + if layer is None: + raise CreatorError( + f"Layer with name \"{layer_name}\" was not found") + marked_layers.append(layer) - # Raise if nothing is selected - if not selected_layers: - raise CreatorError("Nothing is selected. Please select layers.") + else: + marked_layers = [ + layer + for layer in layers_data + if layer["selected"] + ] + + # Raise if nothing is selected + if not marked_layers: + raise CreatorError("Nothing is selected. Please select layers.") + + marked_layer_names = {layer["name"] for layer in marked_layers} + + marked_layer_names = set(marked_layer_names) - selected_layer_names = {layer["name"] for layer in selected_layers} instances_to_remove = [] for instance in self.create_context.instances: if instance.creator_identifier != self.identifier: continue - layer_names = set(instance["layer_names"]) - if not layer_names.intersection(selected_layer_names): + cur_layer_names = set(instance["layer_names"]) + if not cur_layer_names.intersection(marked_layer_names): continue - new_layer_names = layer_names - selected_layer_names + new_layer_names = cur_layer_names - marked_layer_names if new_layer_names: instance["layer_names"] = list(new_layer_names) else: @@ -470,7 +485,7 @@ class CreateRenderPass(TVPaintCreator): self.log.info(f"New subset name is \"{label}\".") instance_data["label"] = label instance_data["group"] = f"{self.get_group_label()} ({render_layer})" - instance_data["layer_names"] = list(selected_layer_names) + instance_data["layer_names"] = list(marked_layer_names) if "creator_attributes" not in instance_data: instance_data["creator_attribtues"] = {} @@ -496,7 +511,7 @@ class CreateRenderPass(TVPaintCreator): self.host.write_instances(instances_data) self._add_instance_to_context(new_instance) - self._change_layers_group(selected_layers, group_id) + self._change_layers_group(marked_layers, group_id) return new_instance From 80a9c3e65ee6bd781f9906e115e58fa3fe46ff7f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 14:05:49 +0100 Subject: [PATCH 170/483] fix formatting --- openpype/hosts/tvpaint/plugins/create/create_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index b324beb8f6..cae894035a 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -451,7 +451,8 @@ class CreateRenderPass(TVPaintCreator): # Raise if nothing is selected if not marked_layers: - raise CreatorError("Nothing is selected. Please select layers.") + raise CreatorError( + "Nothing is selected. Please select layers.") marked_layer_names = {layer["name"] for layer in marked_layers} From 8002dc4f4fb937edb07336232285e48bfec59989 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Feb 2023 21:29:11 +0800 Subject: [PATCH 171/483] style fix --- openpype/hosts/max/api/lib.py | 38 ++- openpype/hosts/max/api/lib_renderproducts.py | 70 ++---- openpype/hosts/max/api/lib_rendersettings.py | 5 +- .../hosts/max/plugins/create/create_render.py | 2 +- .../max/plugins/publish/collect_render.py | 6 +- .../plugins/publish/submit_3dmax_deadline.py | 237 ------------------ .../plugins/publish/submit_max_deadline.py | 217 ++++++++++++++++ .../defaults/project_settings/deadline.json | 4 +- .../schema_project_deadline.json | 2 +- 9 files changed, 275 insertions(+), 306 deletions(-) delete mode 100644 openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py create mode 100644 openpype/modules/deadline/plugins/publish/submit_max_deadline.py diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index ecea8b5541..14aa4d750a 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -134,19 +134,21 @@ def get_default_render_folder(project_setting=None): def set_framerange(startFrame, endFrame): - """Get/set the type of time range to be rendered. - - Possible values are: - - 1 -Single frame. - - 2 -Active time segment ( animationRange ). - - 3 -User specified Range. - - 4 -User specified Frame pickup string (for example "1,3,5-12"). """ - # hard-code, there should be a custom setting for this + Args: + start_frame (int): Start frame number. + end_frame (int): End frame number. + Note: + Frame range can be specified in different types. Possible values are: + + * `1` - Single frame. + * `2` - Active time segment ( animationRange ). + * `3` - User specified Range. + * `4` - User specified Frame pickup string (for example `1,3,5-12`). + + Todo: + Current type is hard-coded, there should be a custom setting for this. + """ rt.rendTimeType = 4 if startFrame is not None and endFrame is not None: frameRange = "{0}-{1}".format(startFrame, endFrame) @@ -157,3 +159,15 @@ def get_multipass_setting(project_setting=None): return (project_setting["max"] ["RenderSettings"] ["multipass"]) + +def get_max_version(): + """ + Args: + get max version date for deadline + + Returns: + #(25000, 62, 0, 25, 0, 0, 997, 2023, "") + max_info[7] = max version date + """ + max_info = rt.maxversion() + return max_info[7] diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index e09934e5de..00e0978bc8 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -15,7 +15,6 @@ from openpype.pipeline import legacy_io class RenderProducts(object): - @classmethod def __init__(self, project_settings=None): self._project_settings = project_settings if not self._project_settings: @@ -36,15 +35,11 @@ class RenderProducts(object): filename, container) - context = get_current_project_asset() - startFrame = context["data"].get("frameStart") - endFrame = context["data"].get("frameEnd") + 1 - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - full_render_list = self.beauty_render_product(output_file, - startFrame, - endFrame, - img_fmt) + full_render_list = [] + beauty = self.beauty_render_product(output_file, img_fmt) + full_render_list.append(beauty) + renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] @@ -60,41 +55,29 @@ class RenderProducts(object): renderer == "Quicksilver_Hardware_Renderer" ): render_elem_list = self.render_elements_product(output_file, - startFrame, - endFrame, img_fmt) - for render_elem in render_elem_list: - full_render_list.append(render_elem) + if render_elem_list: + for render_elem in render_elem_list: + full_render_list.append(render_elem) return full_render_list if renderer == "Arnold": aov_list = self.arnold_render_product(output_file, - startFrame, - endFrame, img_fmt) if aov_list: for aov in aov_list: full_render_list.append(aov) return full_render_list - def beauty_render_product(self, folder, startFrame, endFrame, fmt): - # get the beauty - beauty_frame_range = list() - - for f in range(startFrame, endFrame): - beauty = "{0}.{1}.{2}".format(folder, - str(f), - fmt) - beauty = beauty.replace("\\", "/") - beauty_frame_range.append(beauty) - - return beauty_frame_range - + def beauty_render_product(self, folder, fmt): + beauty_output = f"{folder}.####.{fmt}" + beauty_output = beauty_output.replace("\\", "/") + return beauty_output # TODO: Get the arnold render product - def arnold_render_product(self, folder, startFrame, endFrame, fmt): + def arnold_render_product(self, folder, fmt): """Get all the Arnold AOVs""" - aovs = list() + aovs = [] amw = rt.MaxtoAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager @@ -105,21 +88,17 @@ class RenderProducts(object): for i in range(aov_group_num): # get the specific AOV group for aov in aov_mgr.drivers[i].aov_list: - for f in range(startFrame, endFrame): - render_element = "{0}_{1}.{2}.{3}".format(folder, - str(aov.name), - str(f), - fmt) - render_element = render_element.replace("\\", "/") - aovs.append(render_element) + render_element = f"{folder}_{aov.name}.####.{fmt}" + render_element = render_element.replace("\\", "/") + aovs.append(render_element) # close the AOVs manager window amw.close() return aovs - def render_elements_product(self, folder, startFrame, endFrame, fmt): + def render_elements_product(self, folder, fmt): """Get all the render element output files. """ - render_dirname = list() + render_dirname = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() @@ -128,16 +107,11 @@ class RenderProducts(object): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") if renderlayer_name.enabled: - for f in range(startFrame, endFrame): - render_element = "{0}_{1}.{2}.{3}".format(folder, - renderpass, - str(f), - fmt) - render_element = render_element.replace("\\", "/") - render_dirname.append(render_element) + render_element = f"{folder}_{renderpass}.####.{fmt}" + render_element = render_element.replace("\\", "/") + render_dirname.append(render_element) return render_dirname def image_format(self): - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - return img_fmt + return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 92f716ba54..212c08846b 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -22,7 +22,6 @@ class RenderSettings(object): "underscore": "_" } - @classmethod def __init__(self, project_settings=None): self._project_settings = project_settings if not self._project_settings: @@ -41,7 +40,7 @@ class RenderSettings(object): if not found: raise RuntimeError("Camera not found") - def set_renderoutput(self, container): + def render_output(self, container): folder = rt.maxFilePath # hard-coded, should be customized in the setting file = rt.maxFileName @@ -144,7 +143,7 @@ class RenderSettings(object): aov_name = "{0}_{1}..{2}".format(dir, renderpass, ext) render_elem.SetRenderElementFileName(i, aov_name) - def get_renderoutput(self, container, output_dir): + def get_render_output(self, container, output_dir): output = os.path.join(output_dir, container) img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa outputFilename = "{0}..{1}".format(output, img_fmt) diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 76c10ca4a9..269fff2e32 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -30,4 +30,4 @@ class CreateRender(plugin.MaxCreator): # set viewport camera for rendering(mandatory for deadline) RenderSettings().set_render_camera(sel_obj) # set output paths for rendering(mandatory for deadline) - RenderSettings().set_renderoutput(container_name) + RenderSettings().render_output(container_name) diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 55391d40e8..16f8821986 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -4,7 +4,8 @@ import os import pyblish.api from pymxs import runtime as rt -from openpype.pipeline import legacy_io +from openpype.pipeline import get_current_asset_name +from openpype.hosts.max.api.lib import get_max_version from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -25,7 +26,7 @@ class CollectRender(pyblish.api.InstancePlugin): filepath = current_file.replace("\\", "/") context.data['currentFile'] = current_file - asset = legacy_io.Session["AVALON_ASSET"] + asset = get_current_asset_name() render_layer_files = RenderProducts().render_product(instance.name) folder = folder.replace("\\", "/") @@ -51,6 +52,7 @@ class CollectRender(pyblish.api.InstancePlugin): "subset": instance.name, "asset": asset, "publish": True, + "maxversion": str(get_max_version()), "imageFormat": imgFormat, "family": 'maxrender', "families": ['maxrender'], diff --git a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py b/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py deleted file mode 100644 index ed448abe1f..0000000000 --- a/openpype/modules/deadline/plugins/publish/submit_3dmax_deadline.py +++ /dev/null @@ -1,237 +0,0 @@ -import os -import json -import getpass - -import requests -import pyblish.api - -from openpype.pipeline import legacy_io -from openpype.settings import get_project_settings -from openpype.hosts.max.api.lib import ( - get_current_renderer, - get_multipass_setting -) -from openpype.hosts.max.api.lib_rendersettings import RenderSettings - - -class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin): - """ - 3DMax File Submit Render Deadline - - """ - - label = "Submit 3DsMax Render to Deadline" - order = pyblish.api.IntegratorOrder - hosts = ["max"] - families = ["maxrender"] - targets = ["local"] - use_published = True - priority = 50 - chunk_size = 1 - group = None - deadline_pool = None - deadline_pool_secondary = None - framePerTask = 1 - - def process(self, instance): - context = instance.context - filepath = context.data["currentFile"] - filename = os.path.basename(filepath) - comment = context.data.get("comment", "") - deadline_user = context.data.get("deadlineUser", getpass.getuser()) - jobname = "{0} - {1}".format(filename, instance.name) - - # StartFrame to EndFrame - frames = "{start}-{end}".format( - start=int(instance.data["frameStart"]), - end=int(instance.data["frameEnd"]) - ) - if self.use_published: - for item in context: - if "workfile" in item.data["families"]: - msg = "Workfile (scene) must be published along" - assert item.data["publish"] is True, msg - - template_data = item.data.get("anatomyData") - rep = item.data.get("representations")[0].get("name") - template_data["representation"] = rep - template_data["ext"] = rep - template_data["comment"] = None - anatomy_data = context.data["anatomy"] - anatomy_filled = anatomy_data.format(template_data) - template_filled = anatomy_filled["publish"]["path"] - filepath = os.path.normpath(template_filled) - filepath = filepath.replace("\\", "/") - self.log.info( - "Using published scene for render {}".format(filepath) - ) - if not os.path.exists(filepath): - self.log.error("published scene does not exist!") - - new_scene = self._clean_name(filepath) - # use the anatomy data for setting up the path of the files - orig_scene = self._clean_name(instance.context.data["currentFile"]) - expected_files = instance.data.get("expectedFiles") - - new_exp = [] - for file in expected_files: - new_file = str(file).replace(orig_scene, new_scene) - new_exp.append(new_file) - - instance.data["expectedFiles"] = new_exp - - metadata_folder = instance.data.get("publishRenderMetadataFolder") - if metadata_folder: - metadata_folder = metadata_folder.replace(orig_scene, - new_scene) - instance.data["publishRenderMetadataFolder"] = metadata_folder - - payload = { - "JobInfo": { - # Top-level group name - "BatchName": filename, - - # Job name, as seen in Monitor - "Name": jobname, - - # Arbitrary username, for visualisation in Monitor - "UserName": deadline_user, - - "Plugin": instance.data["plugin"], - "Group": self.group, - "Pool": self.deadline_pool, - "secondaryPool": self.deadline_pool_secondary, - "Frames": frames, - "ChunkSize": self.chunk_size, - "Priority": instance.data.get("priority", self.priority), - "Comment": comment, - "FramesPerTask": self.framePerTask - }, - "PluginInfo": { - # Input - "SceneFile": filepath, - "Version": "2023", - "SaveFile": True, - # Mandatory for Deadline - # Houdini version without patch number - - "IgnoreInputs": True - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - # Include critical environment variables with submission + api.Session - keys = [ - # Submit along the current Avalon tool setup that we launched - # this application with so the Render Slave can build its own - # similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9" - "AVALON_TOOLS", - "OPENPYPE_VERSION" - ] - # Add mongo url if it's enabled - if 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) - - payload["JobInfo"].update({ - "EnvironmentKeyValue%d" % index: "{key}={value}".format( - key=key, - value=environment[key] - ) for index, key in enumerate(environment) - }) - - # Include OutputFilename entries - # The first entry also enables double-click to preview rendered - # frames from Deadline Monitor - output_data = {} - # need to be fixed - for i, filepath in enumerate(instance.data["expectedFiles"]): - dirname = os.path.dirname(filepath) - fname = os.path.basename(filepath) - output_data["OutputDirectory%d" % i] = dirname.replace("\\", "/") - output_data["OutputFilename%d" % i] = fname - - if not os.path.exists(dirname): - self.log.info("Ensuring output directory exists: %s" % - dirname) - os.makedirs(dirname) - - plugin_data = {} - project_setting = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) - - multipass = get_multipass_setting(project_setting) - if multipass: - plugin_data["DisableMultipass"] = 0 - else: - plugin_data["DisableMultipass"] = 1 - - if self.use_published: - old_output_dir = os.path.dirname(expected_files[0]) - output_beauty = RenderSettings().get_renderoutput(instance.name, - old_output_dir) - output_beauty = output_beauty.replace(orig_scene, new_scene) - output_beauty = output_beauty.replace("\\", "/") - plugin_data["RenderOutput"] = output_beauty - - renderer_class = get_current_renderer() - renderer = str(renderer_class).split(":")[0] - if ( - renderer == "ART_Renderer" or - renderer == "Redshift_Renderer" or - renderer == "V_Ray_6_Hotfix_3" or - renderer == "V_Ray_GPU_6_Hotfix_3" or - renderer == "Default_Scanline_Renderer" or - renderer == "Quicksilver_Hardware_Renderer" - ): - render_elem_list = RenderSettings().get_render_element() - for i, element in enumerate(render_elem_list): - element = element.replace(orig_scene, new_scene) - plugin_data["RenderElementOutputFilename%d" % i] = element # noqa - - self.log.debug("plugin data:{}".format(plugin_data)) - self.log.info("Scene name was switched {} -> {}".format( - orig_scene, new_scene - )) - - payload["JobInfo"].update(output_data) - payload["PluginInfo"].update(plugin_data) - - self.submit(instance, payload) - - def submit(self, instance, payload): - - context = instance.context - deadline_url = context.data.get("defaultDeadline") - deadline_url = instance.data.get( - "deadlineUrl", deadline_url) - - assert deadline_url, "Requires Deadline Webservice URL" - - plugin = payload["JobInfo"]["Plugin"] - self.log.info("Using Render Plugin : {}".format(plugin)) - - self.log.info("Submitting..") - self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) - - # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(deadline_url) - response = requests.post(url, json=payload) - if not response.ok: - raise Exception(response.text) - # Store output dir for unified publisher (expectedFilesequence) - expected_files = instance.data["expectedFiles"] - output_dir = os.path.dirname(expected_files[0]) - instance.data["toBeRenderedOn"] = "deadline" - instance.data["outputDir"] = output_dir - instance.data["deadlineSubmissionJob"] = response.json() - - def rename_render_element(self): - pass - - def _clean_name(self, path): - return os.path.splitext(os.path.basename(path))[0] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py new file mode 100644 index 0000000000..3e00f8fd15 --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -0,0 +1,217 @@ +import os +import getpass +import copy + +import attr +from openpype.pipeline import legacy_io +from openpype.settings import get_project_settings +from openpype.hosts.max.api.lib import ( + get_current_renderer, + get_multipass_setting +) +from openpype.hosts.max.api.lib_rendersettings import RenderSettings +from openpype_modules.deadline import abstract_submit_deadline +from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo + + +@attr.s +class MaxPluginInfo(object): + SceneFile = attr.ib(default=None) # Input + Version = attr.ib(default=None) # Mandatory for Deadline + SaveFile = attr.ib(default=True) + IgnoreInputs = attr.ib(default=True) + + +class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): + + label = "Submit Render to Deadline" + hosts = ["max"] + families = ["maxrender"] + targets = ["local"] + + use_published = True + priority = 50 + tile_priority = 50 + chunk_size = 1 + jobInfo = {} + pluginInfo = {} + group = None + deadline_pool = None + deadline_pool_secondary = None + framePerTask = 1 + + def get_job_info(self): + job_info = DeadlineJobInfo(Plugin="3dsmax") + + # todo: test whether this works for existing production cases + # where custom jobInfo was stored in the project settings + 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) + + job_info.Name = "%s - %s" % (src_filename, instance.name) + job_info.BatchName = src_filename + job_info.Plugin = instance.data["plugin"] + job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) + + # Deadline requires integers in frame range + frames = "{start}-{end}".format( + start=int(instance.data["frameStart"]), + end=int(instance.data["frameEnd"]) + ) + job_info.Frames = frames + + job_info.Pool = instance.data.get("primaryPool") + job_info.SecondaryPool = instance.data.get("secondaryPool") + job_info.ChunkSize = instance.data.get("chunkSize", 1) + job_info.Comment = context.data.get("comment") + job_info.Priority = instance.data.get("priority", self.priority) + job_info.FramesPerTask = instance.data.get("framesPerTask", 1) + + if self.group: + job_info.Group = self.group + + # 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", + "OPENPYPE_VERSION", + "IS_TEST" + ] + # 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" + + # Add list of expected files to job + # --------------------------------- + exp = instance.data.get("expectedFiles") + for filepath in 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 + + plugin_info = MaxPluginInfo( + SceneFile=self.scene_path, + Version=instance.data["maxversion"], + SaveFile = True, + IgnoreInputs = True + ) + + 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_submission(self): + + instance = self._instance + context = instance.context + filepath = self.scene_path + + 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" + + filename = os.path.basename(filepath) + + payload_data = { + "filename": filename, + "dirname": output_dir + } + + self.log.debug("Submitting 3dsMax render..") + payload = self._use_puhlished_name(payload_data) + job_info, plugin_info = payload + self.submit(self.assemble_payload(job_info, plugin_info)) + + def _use_puhlished_name(self, data): + instance = self._instance + job_info = copy.deepcopy(self.job_info) + plugin_info = copy.deepcopy(self.plugin_info) + plugin_data = {} + project_setting = get_project_settings( + legacy_io.Session["AVALON_PROJECT"] + ) + + multipass = get_multipass_setting(project_setting) + if multipass: + plugin_data["DisableMultipass"] = 0 + else: + plugin_data["DisableMultipass"] = 1 + + expected_files = instance.data.get("expectedFiles") + if not expected_files: + raise RuntimeError("No render elements found") + old_output_dir = os.path.dirname(expected_files[0]) + output_beauty = RenderSettings().get_render_output(instance.name, + old_output_dir) + filepath = self.from_published_scene() + def _clean_name(path): + return os.path.splitext(os.path.basename(path))[0] + new_scene = _clean_name(filepath) + orig_scene = _clean_name(instance.context.data["currentFile"]) + + output_beauty = output_beauty.replace(orig_scene, new_scene) + output_beauty = output_beauty.replace("\\", "/") + plugin_data["RenderOutput"] = output_beauty + + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] + if ( + renderer == "ART_Renderer" or + renderer == "Redshift_Renderer" or + renderer == "V_Ray_6_Hotfix_3" or + renderer == "V_Ray_GPU_6_Hotfix_3" or + renderer == "Default_Scanline_Renderer" or + renderer == "Quicksilver_Hardware_Renderer" + ): + render_elem_list = RenderSettings().get_render_element() + for i, element in enumerate(render_elem_list): + element = element.replace(orig_scene, new_scene) + plugin_data["RenderElementOutputFilename%d" % i] = element # noqa + + self.log.debug("plugin data:{}".format(plugin_data)) + plugin_info.update(plugin_data) + + return job_info, plugin_info diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 7a5903d8e0..7183603c4b 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -36,7 +36,7 @@ "scene_patches": [], "strict_error_checking": true }, - "MaxSubmitRenderDeadline": { + "MaxSubmitDeadline": { "enabled": true, "optional": false, "active": true, @@ -122,4 +122,4 @@ } } } -} \ No newline at end of file +} 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 3d1b413d6c..a320dfca4f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -207,7 +207,7 @@ { "type": "dict", "collapsible": true, - "key": "MaxSubmitRenderDeadline", + "key": "MaxSubmitDeadline", "label": "3dsMax Submit to Deadline", "checkbox_key": "enabled", "children": [ From 3aa6e9ac9d78ce3404c39bb91ca8e367014cb6c8 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Feb 2023 21:36:37 +0800 Subject: [PATCH 172/483] hound fix --- openpype/hosts/max/api/lib.py | 20 +++++++++---------- openpype/hosts/max/api/lib_renderproducts.py | 2 +- .../plugins/publish/submit_max_deadline.py | 7 ++++--- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 14aa4d750a..67e34c0a30 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -136,18 +136,17 @@ def get_default_render_folder(project_setting=None): def set_framerange(startFrame, endFrame): """ Args: - start_frame (int): Start frame number. - end_frame (int): End frame number. + start_frame (int): Start frame number. + end_frame (int): End frame number. Note: - Frame range can be specified in different types. Possible values are: + Frame range can be specified in different types. Possible values are: - * `1` - Single frame. - * `2` - Active time segment ( animationRange ). - * `3` - User specified Range. - * `4` - User specified Frame pickup string (for example `1,3,5-12`). - - Todo: - Current type is hard-coded, there should be a custom setting for this. + * `1` - Single frame. + * `2` - Active time segment ( animationRange ). + * `3` - User specified Range. + * `4`-User specified Frame pickup string (for example `1,3,5-12`). + TODO: + Current type is hard-coded, there should be a custom setting for this. """ rt.rendTimeType = 4 if startFrame is not None and endFrame is not None: @@ -160,6 +159,7 @@ def get_multipass_setting(project_setting=None): ["RenderSettings"] ["multipass"]) + def get_max_version(): """ Args: diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 00e0978bc8..6d476c9139 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -8,7 +8,6 @@ from openpype.hosts.max.api.lib import ( get_current_renderer, get_default_render_folder ) -from openpype.pipeline.context_tools import get_current_project_asset from openpype.settings import get_project_settings from openpype.pipeline import legacy_io @@ -74,6 +73,7 @@ class RenderProducts(object): beauty_output = f"{folder}.####.{fmt}" beauty_output = beauty_output.replace("\\", "/") return beauty_output + # TODO: Get the arnold render product def arnold_render_product(self, folder, fmt): """Get all the Arnold AOVs""" diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 3e00f8fd15..b53cc928d6 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -128,8 +128,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): plugin_info = MaxPluginInfo( SceneFile=self.scene_path, Version=instance.data["maxversion"], - SaveFile = True, - IgnoreInputs = True + SaveFile=True, + IgnoreInputs=True ) plugin_payload = attr.asdict(plugin_info) @@ -143,7 +143,6 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): def process_submission(self): instance = self._instance - context = instance.context filepath = self.scene_path expected_files = instance.data["expectedFiles"] @@ -187,8 +186,10 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) filepath = self.from_published_scene() + def _clean_name(path): return os.path.splitext(os.path.basename(path))[0] + new_scene = _clean_name(filepath) orig_scene = _clean_name(instance.context.data["currentFile"]) From db75941e00dfd9f503b0690cda7334ab71b0892d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 20 Feb 2023 21:39:31 +0800 Subject: [PATCH 173/483] hound fix --- 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 67e34c0a30..53e66c219e 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -144,7 +144,7 @@ def set_framerange(startFrame, endFrame): * `1` - Single frame. * `2` - Active time segment ( animationRange ). * `3` - User specified Range. - * `4`-User specified Frame pickup string (for example `1,3,5-12`). + * `4` - User specified Frame pickup string (for example `1,3,5-12`). TODO: Current type is hard-coded, there should be a custom setting for this. """ From 7b6fb46cd191ab39ae43b9664365a0422203ad58 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Mon, 20 Feb 2023 16:05:24 +0000 Subject: [PATCH 174/483] Fix colorspaceTemplate --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 89a4e5d377..bd5933926c 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -922,7 +922,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "display": instance.data["colorspaceDisplay"], "view": instance.data["colorspaceView"], "colorspaceTemplate": instance.data["colorspaceConfig"].replace( - context.data["anatomy"].roots["work"], "{root[work]}" + str(context.data["anatomy"].roots["work"]), "{root[work]}" ) } From ffcc1656d2a4641bf9ae36e4a28a0c831f8d8a4f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Feb 2023 11:08:07 +0800 Subject: [PATCH 175/483] cosmetic issue fix --- openpype/hosts/max/api/lib.py | 13 +++---- openpype/hosts/max/api/lib_renderproducts.py | 23 +++++------ openpype/hosts/max/api/lib_rendersettings.py | 39 +++++++++++-------- .../max/plugins/publish/collect_render.py | 2 +- .../plugins/publish/submit_max_deadline.py | 20 +++++----- 5 files changed, 48 insertions(+), 49 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 53e66c219e..6ee934d3da 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -135,17 +135,14 @@ def get_default_render_folder(project_setting=None): def set_framerange(startFrame, endFrame): """ - Args: - start_frame (int): Start frame number. - end_frame (int): End frame number. Note: Frame range can be specified in different types. Possible values are: + * `1` - Single frame. + * `2` - Active time segment ( animationRange ). + * `3` - User specified Range. + * `4` - User specified Frame pickup string (for example `1,3,5-12`). - * `1` - Single frame. - * `2` - Active time segment ( animationRange ). - * `3` - User specified Range. - * `4` - User specified Frame pickup string (for example `1,3,5-12`). - TODO: + Todo: Current type is hard-coded, there should be a custom setting for this. """ rt.rendTimeType = 4 diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 6d476c9139..a74a6a7426 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -45,28 +45,25 @@ class RenderProducts(object): if renderer == "VUE_File_Renderer": return full_render_list - if ( - renderer == "ART_Renderer" or - renderer == "Redshift_Renderer" or - renderer == "V_Ray_6_Hotfix_3" or - renderer == "V_Ray_GPU_6_Hotfix_3" or - renderer == "Default_Scanline_Renderer" or - renderer == "Quicksilver_Hardware_Renderer" - ): + if renderer in [ + "ART_Renderer", + "Redshift_Renderer", + "V_Ray_6_Hotfix_3", + "V_Ray_GPU_6_Hotfix_3", + "Default_Scanline_Renderer", + "Quicksilver_Hardware_Renderer", + ]: render_elem_list = self.render_elements_product(output_file, img_fmt) if render_elem_list: - for render_elem in render_elem_list: - full_render_list.append(render_elem) - + full_render_list.extend(iter(render_elem_list)) return full_render_list if renderer == "Arnold": aov_list = self.arnold_render_product(output_file, img_fmt) if aov_list: - for aov in aov_list: - full_render_list.append(aov) + full_render_list.extend(iter(aov_list)) return full_render_list def beauty_render_product(self, folder, fmt): diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 212c08846b..94c6ee775b 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -23,6 +23,11 @@ class RenderSettings(object): } def __init__(self, project_settings=None): + """ + Set up the naming convention for the render + elements for the deadline submission + """ + self._project_settings = project_settings if not self._project_settings: self._project_settings = get_project_settings( @@ -61,9 +66,9 @@ class RenderSettings(object): width = context["data"].get("resolutionWidth") height = context["data"].get("resolutionHeight") # Set Frame Range - startFrame = context["data"].get("frameStart") - endFrame = context["data"].get("frameEnd") - set_framerange(startFrame, endFrame) + frame_start = context["data"].get("frame_start") + frame_end = context["data"].get("frame_end") + set_framerange(frame_start, frame_end) # get the production render renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] @@ -78,24 +83,24 @@ class RenderSettings(object): )] except KeyError: aov_separator = "." - outputFilename = "{0}..{1}".format(output, img_fmt) - outputFilename = outputFilename.replace("{aov_separator}", + output_filename = "{0}..{1}".format(output, img_fmt) + output_filename = output_filename.replace("{aov_separator}", aov_separator) - rt.rendOutputFilename = outputFilename + rt.rendOutputFilename = output_filename if renderer == "VUE_File_Renderer": return # TODO: Finish the arnold render setup if renderer == "Arnold": self.arnold_setup() - if ( - renderer == "ART_Renderer" or - renderer == "Redshift_Renderer" or - renderer == "V_Ray_6_Hotfix_3" or - renderer == "V_Ray_GPU_6_Hotfix_3" or - renderer == "Default_Scanline_Renderer" or - renderer == "Quicksilver_Hardware_Renderer" - ): + if renderer in [ + "ART_Renderer", + "Redshift_Renderer", + "V_Ray_6_Hotfix_3", + "V_Ray_GPU_6_Hotfix_3", + "Default_Scanline_Renderer", + "Quicksilver_Hardware_Renderer", + ]: self.render_element_layer(output, width, height, img_fmt) rt.rendSaveFile = True @@ -146,11 +151,11 @@ class RenderSettings(object): def get_render_output(self, container, output_dir): output = os.path.join(output_dir, container) img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - outputFilename = "{0}..{1}".format(output, img_fmt) - return outputFilename + output_filename = "{0}..{1}".format(output, img_fmt) + return output_filename def get_render_element(self): - orig_render_elem = list() + orig_render_elem = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num < 0: diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 16f8821986..7656c641ed 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -14,7 +14,7 @@ class CollectRender(pyblish.api.InstancePlugin): """Collect Render for Deadline""" order = pyblish.api.CollectorOrder + 0.01 - label = "Collect 3dmax Render Layers" + label = "Collect 3dsmax Render Layers" hosts = ['max'] families = ["maxrender"] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index b53cc928d6..417a03de74 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -160,11 +160,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): } self.log.debug("Submitting 3dsMax render..") - payload = self._use_puhlished_name(payload_data) + payload = self._use_published_name(payload_data) job_info, plugin_info = payload self.submit(self.assemble_payload(job_info, plugin_info)) - def _use_puhlished_name(self, data): + def _use_published_name(self, data): instance = self._instance job_info = copy.deepcopy(self.job_info) plugin_info = copy.deepcopy(self.plugin_info) @@ -199,14 +199,14 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] - if ( - renderer == "ART_Renderer" or - renderer == "Redshift_Renderer" or - renderer == "V_Ray_6_Hotfix_3" or - renderer == "V_Ray_GPU_6_Hotfix_3" or - renderer == "Default_Scanline_Renderer" or - renderer == "Quicksilver_Hardware_Renderer" - ): + if renderer in [ + "ART_Renderer", + "Redshift_Renderer", + "V_Ray_6_Hotfix_3", + "V_Ray_GPU_6_Hotfix_3", + "Default_Scanline_Renderer", + "Quicksilver_Hardware_Renderer", + ]: render_elem_list = RenderSettings().get_render_element() for i, element in enumerate(render_elem_list): element = element.replace(orig_scene, new_scene) From 8ca7a719046bc2ceb2075db20a959ccc88797447 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 21 Feb 2023 11:10:02 +0800 Subject: [PATCH 176/483] hound fix --- openpype/hosts/max/api/lib.py | 6 +++--- openpype/hosts/max/api/lib_rendersettings.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 6ee934d3da..3383330792 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -137,9 +137,9 @@ def set_framerange(startFrame, endFrame): """ Note: Frame range can be specified in different types. Possible values are: - * `1` - Single frame. - * `2` - Active time segment ( animationRange ). - * `3` - User specified Range. + * `1` - Single frame. + * `2` - Active time segment ( animationRange ). + * `3` - User specified Range. * `4` - User specified Frame pickup string (for example `1,3,5-12`). Todo: diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index 94c6ee775b..b07d19f176 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -85,7 +85,7 @@ class RenderSettings(object): aov_separator = "." output_filename = "{0}..{1}".format(output, img_fmt) output_filename = output_filename.replace("{aov_separator}", - aov_separator) + aov_separator) rt.rendOutputFilename = output_filename if renderer == "VUE_File_Renderer": return From 78933eb56259045a27c1c56f06a31dfb39f38e37 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 10:44:10 +0100 Subject: [PATCH 177/483] OP-4643 - fixed subset filtering Co-authored-by: Toke Jepsen --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index de36ea7d5f..71124b527a 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -273,7 +273,7 @@ class ExtractOIIOTranscode(publish.Extractor): "families": family, "task_names": task_name, "task_types": task_type, - "subset": subset + "subsets": subset } profile = filter_profiles(self.profiles, filtering_criteria, logger=self.log) From deaad39437501f18fc3ba4be8b1fc5f0ee3be65d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 12:12:35 +0100 Subject: [PATCH 178/483] OP-4643 - split command line arguments to separate items Reuse existing method from ExtractReview, put it into transcoding.py --- openpype/lib/transcoding.py | 29 +++++++++++++++++++++- openpype/plugins/publish/extract_review.py | 27 +++----------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 95042fb74c..a87300c280 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1092,7 +1092,7 @@ def convert_colorspace( raise ValueError("Both screen and display must be set.") if additional_command_args: - oiio_cmd.extend(additional_command_args) + oiio_cmd.extend(split_cmd_args(additional_command_args)) if target_colorspace: oiio_cmd.extend(["--colorconvert", @@ -1106,3 +1106,30 @@ def convert_colorspace( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) + + +def split_cmd_args(in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + Args: + in_args (list): of arguments ['-n', '-d uint10'] + Returns + (list): ['-n', '-d', 'unint10'] + """ + splitted_args = [] + for arg in in_args: + sub_args = arg.split(" -") + if len(sub_args) == 1: + if arg and arg not in splitted_args: + splitted_args.append(arg) + continue + + for idx, arg in enumerate(sub_args): + if idx != 0: + arg = "-" + arg + + if arg and arg not in splitted_args: + splitted_args.append(arg) + return splitted_args diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 0f6dacba18..e80141fc4a 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -22,6 +22,7 @@ from openpype.lib.transcoding import ( should_convert_for_ffmpeg, convert_input_paths_for_ffmpeg, get_transcode_temp_directory, + split_cmd_args ) @@ -670,7 +671,7 @@ class ExtractReview(pyblish.api.InstancePlugin): res_filters = self.rescaling_filters(temp_data, output_def, new_repre) ffmpeg_video_filters.extend(res_filters) - ffmpeg_input_args = self.split_ffmpeg_args(ffmpeg_input_args) + ffmpeg_input_args = split_cmd_args(ffmpeg_input_args) lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args) ffmpeg_video_filters.extend(lut_filters) @@ -723,28 +724,6 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_output_args ) - def split_ffmpeg_args(self, in_args): - """Makes sure all entered arguments are separated in individual items. - - Split each argument string with " -" to identify if string contains - one or more arguments. - """ - splitted_args = [] - for arg in in_args: - sub_args = arg.split(" -") - if len(sub_args) == 1: - if arg and arg not in splitted_args: - splitted_args.append(arg) - continue - - for idx, arg in enumerate(sub_args): - if idx != 0: - arg = "-" + arg - - if arg and arg not in splitted_args: - splitted_args.append(arg) - return splitted_args - def ffmpeg_full_args( self, input_args, video_filters, audio_filters, output_args ): @@ -764,7 +743,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Returns: list: Containing all arguments ready to run in subprocess. """ - output_args = self.split_ffmpeg_args(output_args) + output_args = split_cmd_args(output_args) video_args_dentifiers = ["-vf", "-filter:v"] audio_args_dentifiers = ["-af", "-filter:a"] From 83cb0b0b04a59a5f4acffb05659a893f2318c91c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 13:02:41 +0100 Subject: [PATCH 179/483] OP-4643 - refactor - changed existence check --- openpype/plugins/publish/extract_color_transcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 71124b527a..456e40008d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -161,12 +161,12 @@ class ExtractOIIOTranscode(publish.Extractor): custom_tags = output_def.get("custom_tags") if custom_tags: - if not new_repre.get("custom_tags"): + if new_repre.get("custom_tags") is None: new_repre["custom_tags"] = [] new_repre["custom_tags"].extend(custom_tags) # Add additional tags from output definition to representation - if not new_repre.get("tags"): + if new_repre.get("tags") is None: new_repre["tags"] = [] for tag in output_def["tags"]: if tag not in new_repre["tags"]: From 909f51b702825be0fe23fd946023279787d28e1c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 13:10:11 +0100 Subject: [PATCH 180/483] OP-4643 - changed label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5333d514b5..3e9467af61 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -294,7 +294,7 @@ "children": [ { "key": "additional_command_args", - "label": "Additional command line arguments", + "label": "Arguments", "type": "list", "object_type": "text" } From 71013e45052bb0775cc80e799cf4556a935372ef Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 13:11:11 +0100 Subject: [PATCH 181/483] Revert "Fix - added missed scopes for Slack bot" This reverts commit 5e0c4a3ab1432e120b8f0c324f899070f1a5f831. --- openpype/modules/slack/manifest.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml index 233c39fbaf..7a65cc5915 100644 --- a/openpype/modules/slack/manifest.yml +++ b/openpype/modules/slack/manifest.yml @@ -19,8 +19,6 @@ oauth_config: - chat:write.public - files:write - channels:read - - users:read - - usergroups:read settings: org_deploy_enabled: false socket_mode_enabled: false From 12ba88e9573a3c33184264e2b6c86468a26b3a3b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 15:06:16 +0100 Subject: [PATCH 182/483] OP-4643 - added documentation --- .../assets/global_oiio_transcode.png | Bin 0 -> 29010 bytes .../project_settings/settings_project_global.md | 15 +++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 website/docs/project_settings/assets/global_oiio_transcode.png diff --git a/website/docs/project_settings/assets/global_oiio_transcode.png b/website/docs/project_settings/assets/global_oiio_transcode.png new file mode 100644 index 0000000000000000000000000000000000000000..99396d5bb3f16d434a92c6079515128488b82489 GIT binary patch literal 29010 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 literal 0 HcmV?d00001 diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 37fed93e69..52671d2db6 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -45,6 +45,21 @@ The _input pattern_ matching uses REGEX expression syntax (try [regexr.com](http The **colorspace name** value is a raw string input and no validation is run after saving project settings. We recommend to open the specified `config.ocio` file and copy pasting the exact colorspace names. ::: +### Extract OIIO Transcode +There is profile configurable (see lower) plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. +Plugin expects instances with filled dictionary `colorspaceData` on a representation. This data contains information about source colorspace and must be collected for transcoding. +`oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. + +Notable parameters: +- **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. +- **`Extension`** - target extension, could be empty - original extension is used +- **`Colorspace`** - target colorspace - must be available in used color config +- **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) +- **`Arguments`** - special additional command line arguments for `oiiotool` + + +Example here describes use case for creation of new color coded review of png image sequence. Original representation's files are kept intact, review is created from transcoded files, but these files are removed in cleanup process. +![global_oiio_transcode](assets/global_oiio_transcode.png) ## Profile filters From 2894cef94c15b35f0ffff4676278a422d80a0ff6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 15:11:48 +0100 Subject: [PATCH 183/483] rename color group by render layer variant --- .../tvpaint/plugins/create/create_render.py | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index cae894035a..841489e14b 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -145,6 +145,7 @@ class CreateRenderlayer(TVPaintCreator): def create(self, subset_name, instance_data, pre_create_data): self.log.debug("Query data from workfile.") + group_name = instance_data["variant"] group_id = pre_create_data.get("group_id") # This creator should run only on one group if group_id is None or group_id == -1: @@ -193,15 +194,10 @@ class CreateRenderlayer(TVPaintCreator): ) self._store_new_instance(new_instance) - new_group_name = pre_create_data.get("group_name") - if not new_group_name or not group_id: + if not group_id or group_item["name"] == group_name: return new_instance self.log.debug("Changing name of the group.") - - new_group_name = pre_create_data.get("group_name") - if not new_group_name or group_item["name"] == new_group_name: - return new_instance # Rename TVPaint group (keep color same) # - groups can't contain spaces rename_script = self.rename_script_template.format( @@ -210,13 +206,13 @@ class CreateRenderlayer(TVPaintCreator): r=group_item["red"], g=group_item["green"], b=group_item["blue"], - name=new_group_name + name=group_name ) execute_george_through_file(rename_script) self.log.info(( f"Name of group with index {group_id}" - f" was changed to \"{new_group_name}\"." + f" was changed to \"{group_name}\"." )) return new_instance @@ -252,11 +248,6 @@ class CreateRenderlayer(TVPaintCreator): label="Group", items=groups_enum ), - TextDef( - "group_name", - label="New group name", - placeholder="< Keep unchanged >" - ), BoolDef( "mark_for_review", label="Review", @@ -280,10 +271,44 @@ class CreateRenderlayer(TVPaintCreator): ] def update_instances(self, update_list): + self._update_color_groups() self._update_renderpass_groups() super().update_instances(update_list) + def _update_color_groups(self): + render_layer_instances = [] + for instance in self.create_context.instances: + if instance.creator_identifier == self.identifier: + render_layer_instances.append(instance) + + if not render_layer_instances: + return + + groups_by_id = { + group["group_id"]: group + for group in get_groups_data() + } + grg_script_lines = [] + for instance in render_layer_instances: + group_id = instance["creator_attributes"]["group_id"] + variant = instance["variant"] + group = groups_by_id[group_id] + if group["name"] == variant: + continue + + grg_script_lines.append(self.rename_script_template.format( + clip_id=group["clip_id"], + group_id=group["group_id"], + r=group["red"], + g=group["green"], + b=group["blue"], + name=variant + )) + + if grg_script_lines: + execute_george_through_file("\n".join(grg_script_lines)) + def _update_renderpass_groups(self): render_layer_instances = {} render_pass_instances = collections.defaultdict(list) From f26b44a2aadebc60ac92ae1a6de81e9442dab429 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 16:19:30 +0100 Subject: [PATCH 184/483] fix 'creator_attributes' key --- openpype/hosts/tvpaint/plugins/create/create_render.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 841489e14b..6a857676a5 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -513,9 +513,9 @@ class CreateRenderPass(TVPaintCreator): instance_data["group"] = f"{self.get_group_label()} ({render_layer})" instance_data["layer_names"] = list(marked_layer_names) if "creator_attributes" not in instance_data: - instance_data["creator_attribtues"] = {} + instance_data["creator_attributes"] = {} - creator_attributes = instance_data["creator_attribtues"] + creator_attributes = instance_data["creator_attributes"] mark_for_review = pre_create_data.get("mark_for_review") if mark_for_review is None: mark_for_review = self.mark_for_review From 08e2d36f199b617a35120b73fd9f0de2b429fa60 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 16:29:25 +0100 Subject: [PATCH 185/483] ignore missing layers in unrelated validators --- .../plugins/publish/validate_duplicated_layer_names.py | 3 +++ .../tvpaint/plugins/publish/validate_layers_visibility.py | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py b/openpype/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py index 9f61bdbcd0..722d76b4d2 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_duplicated_layer_names.py @@ -20,6 +20,9 @@ class ValidateLayersGroup(pyblish.api.InstancePlugin): duplicated_layer_names = [] for layer_name in layer_names: layers = layers_by_name.get(layer_name) + # It is not job of this validator to handle missing layers + if layers is None: + continue if len(layers) > 1: duplicated_layer_names.append(layer_name) diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py index 47632453fc..6a496a2e49 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py @@ -11,8 +11,13 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin): families = ["review", "render"] def process(self, instance): + layers = instance.data["layers"] + # Instance have empty layers + # - it is not job of this validator to check that + if not layers: + return layer_names = set() - for layer in instance.data["layers"]: + for layer in layers: layer_names.add(layer["name"]) if layer["visible"]: return From f1718284f9245dd24160d8975061d90cc6f8c11e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:41:42 +0100 Subject: [PATCH 186/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 52671d2db6..cc661a21fa 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -46,7 +46,7 @@ The **colorspace name** value is a raw string input and no validation is run aft ::: ### Extract OIIO Transcode -There is profile configurable (see lower) plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. +There is profile configurable plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. Plugin expects instances with filled dictionary `colorspaceData` on a representation. This data contains information about source colorspace and must be collected for transcoding. `oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. From 337e695c17d1d5314ffac99a83330f875053703a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:42:06 +0100 Subject: [PATCH 187/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index cc661a21fa..8e557a381c 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -52,7 +52,7 @@ Plugin expects instances with filled dictionary `colorspaceData` on a representa Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. -- **`Extension`** - target extension, could be empty - original extension is used +- **`Extension`** - target extension. If left empty, original extension is used. - **`Colorspace`** - target colorspace - must be available in used color config - **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) - **`Arguments`** - special additional command line arguments for `oiiotool` From 931d0002ec80b0cabd1bbc0b1d74cbdb06ab2d88 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:42:29 +0100 Subject: [PATCH 188/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 8e557a381c..166400cb7f 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -53,7 +53,7 @@ Plugin expects instances with filled dictionary `colorspaceData` on a representa Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. -- **`Colorspace`** - target colorspace - must be available in used color config +- **`Colorspace`** - target colorspace, which must be available in used color config. - **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) - **`Arguments`** - special additional command line arguments for `oiiotool` From 14a8a1449a6e46d82192dece2fb08c8aa807a032 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:42:48 +0100 Subject: [PATCH 189/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 166400cb7f..908191f122 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -54,7 +54,7 @@ Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. - **`Colorspace`** - target colorspace, which must be available in used color config. -- **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) +- **`Display & View`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both `Colorspace` and `Display & View` at the same time. - **`Arguments`** - special additional command line arguments for `oiiotool` From cb7b8d423e1591a9d0995dfba9d3cb697c551a57 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:43:06 +0100 Subject: [PATCH 190/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 908191f122..0a73868d2d 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -55,7 +55,7 @@ Notable parameters: - **`Extension`** - target extension. If left empty, original extension is used. - **`Colorspace`** - target colorspace, which must be available in used color config. - **`Display & View`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both `Colorspace` and `Display & View` at the same time. -- **`Arguments`** - special additional command line arguments for `oiiotool` +- **`Arguments`** - special additional command line arguments for `oiiotool`. Example here describes use case for creation of new color coded review of png image sequence. Original representation's files are kept intact, review is created from transcoded files, but these files are removed in cleanup process. From 748d8989496d6cbd4a5348fb9a78e75617a69cab Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 17:21:20 +0100 Subject: [PATCH 191/483] OP-4643 - updates to documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- website/docs/project_settings/settings_project_global.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 0a73868d2d..9e2ee187cc 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -46,8 +46,8 @@ The **colorspace name** value is a raw string input and no validation is run aft ::: ### Extract OIIO Transcode -There is profile configurable plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. -Plugin expects instances with filled dictionary `colorspaceData` on a representation. This data contains information about source colorspace and must be collected for transcoding. +OIIOTools transcoder plugin with configurable output presets. Any incoming representation with `colorspaceData` is convertable to single or multiple representations with different target colorspaces or display and viewer names found in linked **config.ocio** file. + `oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. Notable parameters: From bd3207f2b6a317e0d77e6eaaa19dc60390f76220 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 17:50:47 +0100 Subject: [PATCH 192/483] Modify artist docstrings to contain Publisher related information and images --- website/docs/artist_hosts_tvpaint.md | 154 ++++++++++------------- website/docs/assets/tvp_create_layer.png | Bin 29058 -> 174149 bytes website/docs/assets/tvp_create_pass.png | Bin 36545 -> 132716 bytes 3 files changed, 68 insertions(+), 86 deletions(-) diff --git a/website/docs/artist_hosts_tvpaint.md b/website/docs/artist_hosts_tvpaint.md index a0ce5d5ff8..baa8c0a09d 100644 --- a/website/docs/artist_hosts_tvpaint.md +++ b/website/docs/artist_hosts_tvpaint.md @@ -43,13 +43,55 @@ You can start your work. ## Usage In TVPaint you can find the Tools in OpenPype menu extension. The OpenPype Tools menu should be available in your work area. However, sometimes it happens that the Tools menu is hidden. You can display the extension panel by going to `Windows -> Plugins -> OpenPype`. +## Create & Publish +As you might already know, to be able to publish, you have to mark what should be published. The marking part is called **Create**. In TVPaint you can create and publish **[Reviews](#review)**, **[Workfile](#workfile)**, **[Render Layers](#render-layer)** and **[Render Passes](#render-pass)**. -## Create -In TVPaint you can create and publish **[Reviews](#review)**, **[Workfile](#workfile)**, **[Render Passes](#render-pass)** and **[Render Layers](#render-layer)**. +:::important +TVPaint integration tries to not guess what you want to publish from the scene. Therefore, you should tell what you want to publish. +::: -You have the possibility to organize your layers by using `Color group`. +![createlayer](assets/tvp_publisher.png) -On the bottom left corner of your timeline, you will note a `Color group` button. +### Review +`Review` will render all visible layers and create a reviewable output. +- Is automatically created without any manual work. +- You can disable the created instance if you want to skip review. + +### Workfile +`Workfile` integrate the source TVPaint file during publishing. Publishing of workfile is useful for backups. +- Is automatically created without any manual work. +- You can disable the created instance if you want to skip review. + +### Render Layer + +
+
+ +Render Layer bakes all the animation layers of one particular color group together. + +- In the **Create** tab, pick `Render Layer` +- Fill `variant`, type in the name that the final published RenderLayer should have according to the naming convention in your studio. *(L10, BG, Hero, etc.)* + - Color group will be renamed to the **variant** value +- Choose color group from combobox + - or select a layer of a particular color and set combobox to **<Use selection>** +- Hit `Create` button + +You have just created Render Layer. Now choose any amount of animation layers that need to be rendered together and assign them the color group. + +You can change `variant` later in **Publish** tab. + +
+
+ +![createlayer](assets/tvp_create_layer.png) + +
+
+
+ +**How to mark TVPaint layer to a group** + +In the bottom left corner of your timeline, you will note a **Color group** button. ![colorgroups](assets/tvp_color_groups.png) @@ -61,63 +103,29 @@ The timeline's animation layer can be marked by the color you pick from your Col ![timeline](assets/tvp_timeline_color.png) -:::important -OpenPype specifically never tries to guess what you want to publish from the scene. Therefore, you have to tell OpenPype what you want to publish. There are three ways how to publish render from the scene. -::: - -When you want to publish `review` or `render layer` or `render pass`, open the `Creator` through the Tools menu `Create` button. - -### Review -`Review` renders the whole file as is and sends the resulting QuickTime to Ftrack. -- Is automatically created during publishing. - -### Workfile -`Workfile` stores the source workfile as is during publishing (e.g. for backup). -- Is automatically created during publishing. - -### Render Layer - -
-
- - -Render Layer bakes all the animation layers of one particular color group together. - -- Choose any amount of animation layers that need to be rendered together and assign them a color group. -- Select any layer of a particular color -- Go to `Creator` and choose `RenderLayer`. -- In the `Subset`, type in the name that the final published RenderLayer should have according to the naming convention in your studio. *(L10, BG, Hero, etc.)* -- Press `Create` -- When you run [publish](#publish), the whole color group will be rendered together and published as a single `RenderLayer` - -
-
- -![createlayer](assets/tvp_create_layer.png) - -
-
- - - - ### Render Pass -Render Passes are smaller individual elements of a Render Layer. A `character` render layer might +Render Passes are smaller individual elements of a [Render Layer](artist_hosts_tvpaint.md#render-layer). A `character` render layer might consist of multiple render passes such as `Line`, `Color` and `Shadow`. +Render Passes are specific because they have to belong to a particular Render Layer. You have to select to which Render Layer the pass belongs. Try to refresh if you don't see demanded Render Layer in the options.
-Render Passes are specific because they have to belong to a particular layer. If you try to create a render pass and did not create any render layers before, an error message will pop up. -When you want to create `RenderPass` -- choose one or several animation layers within one color group that you want to publish -- In the Creator, pick `RenderPass` -- Fill the `Subset` with the name of your pass, e.g. `Color`. +When you want to create Render Pass +- choose one or several TVPaint layers +- In the **Create** tab, pick `Render Pass` +- Fill the `variant` with desired name of pass, e.g. `Color`. +- Select Render Layer to which belongs in Render Layer combobox + - If you don't see new Render Layer try refresh first - Press `Create` +You have just created Render Pass. Selected TVPaint layers should be marked with color group of Render Layer. + +You can change `variant` or Render Layer later in **Publish** tab. +
@@ -126,48 +134,22 @@ When you want to create `RenderPass`
+:::warning +You cannot change TVPaint layer name once you mark it as part of Render Pass. You would have to remove created Render Pass and create it again with new TVPaint layer name. +::: +

In this example, OpenPype will render selected animation layers within the given color group. E.i. the layers *L020_colour_fx*, *L020_colour_mouth*, and *L020_colour_eye* will be rendered as one pass belonging to the yellow RenderLayer. ![renderpass](assets/tvp_timeline_color2.png) - -:::note -You can check your RendrePasses and RenderLayers in [Subset Manager](#subset-manager) or you can start publishing. The publisher will show you a collection of all instances on the left side. -::: - - ---- - -## Publish - -
-
- -Now that you have created the required instances, you can publish them via `Publish` tool. -- Click on `Publish` in OpenPype Tools menu. -- wait until all instances are collected. -- You can check on the left side whether all your instances have been created and are ready for publishing. -- Fill the comment on the bottom of the window. -- Press the `Play` button to publish - -
-
- -![pyblish](assets/tvp_pyblish_render.png) - -
-
- -Once the `Publisher` turns gets green your renders have been published. - ---- - -## Subset Manager -All created instances (render layers, passes, and reviews) will be shown as a simple list. If you don't want to publish some, right click on the item in the list and select `Remove instance`. - -![subsetmanager](assets/tvp_subset_manager.png) +Now that you have created the required instances, you can publish them. +- Fill the comment on the bottom of the window +- Double check enabled instance and their context +- Press `Publish` +- Wait to finish +- Once the `Publisher` turns gets green your renders have been published. --- diff --git a/website/docs/assets/tvp_create_layer.png b/website/docs/assets/tvp_create_layer.png index 9d243da17a099cae576552193711c2898e76f8e9..25081bdf46ace8070b001c9b87dddd6314e26f41 100644 GIT binary patch literal 174149 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 LU9L#R)c1b?NI*?J literal 29058 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

gVQ+e}lCEN997 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 PpcSf0YWMQ*J`MaAgSm76 literal 36545 zcmb@tRahKN)HMnrxVt+60>NDew*(LF?ykWGg1ZF>?he5rcyNNd!{APE7+{$BdB1Zm z&Uf|SoQr;Dx~HqEr=HqtuT^_>w3>=MCK?$U92^{`qJoSD92|VX+wl(t>Fvs@<%;av z2fT-dycFE8DT&lif&UKp5tj-}IJg9DMH$Jj zzQ*U9$i5qgfq*N|5~nqc5$Ra}RG-Lo|J%{ME&t2z$1-MhymIE0tc5Lqy>y-3+?C7= zfZ}=$zv8^0at@`)7Ci`tuJ%Lr%u&6yq-h_tcXT^C*bQgA{qJfB`wx8nyDfOyFdX-4 zbvRrp6gPA<8YvW7XGwS&6xuh|Vx*Fa zBZZ=O)dPQbv<#Aa8p=rWx=~dSeH}|V$b%pV=YeC#?eN}uLx1ZA3SL`R5PBtM$PfCa z{)ZsnmnyyWRX1-XG%g;S{&wM#xO*y5)9>0UW1b-lM~W!G6wZkB6MFwLAmYG~1$vnO z0VTd#dg33fr%HKtNaufs9u)zfnSh2LfmhThJ}WfoQKsDF4S_G(T@TN!?CYJgSxT!BjtRSNA7G}Sgjd1FSJbZS8?PLCzazHo$>poHb(fuK$5Ha%T1Oei zHY*gM0NjzoR!O$K`Y{VaUM@ZGnkq274R&gu^TB+4U@zlHcG!wsYLxPfvejr$;NO`5 z#$Gn)Sy*2`TMs4k>fFP`ZFP&X=la4$vQT~RZ(RVQi%~$$P=P;1}66Y=1=R6!20^?0Qpu4g>x6 zt26x@1S22(DYq| zS6A14%e}8oW-Hk>y*3<%!TNP0kHEX)#8&#LPeQ?HDz9Y82YcWJP+dPD;K3QP*PRE> z2L49RfBsHU%9BWUe;4~1y8zG=(q(YIVXAFFu_Jy`AUyZkISzj1ixD2!U?zIqK z-BkAuxva`ThlpWuw|n%*7lw&UCccEgfTKi5{a(wx?xecIKPi;(Nx{Dhm8Y3JiMEf;`8! zU5%N*`dNB?j$X0_K!04I&pKTXxA}AfAmovrM;pj?t-$IXbP%l@Fq!x=#tH_segQtQ zR;TC&-Vvd93z!5Da=@OvmX>=P?>%IxQE1z`O(45R%&Z);?n-z;PrzQ+)!|&|9x~w1 z(Pa<0$;(ok>vgg`4omX(2;j*j!>Mlh9#L(TMEu@|^?KD`45zAL7yZ$1Pct94(DOO@ z486bviYpc_Wb0HV$IR3p`1roS?*7x()7iK$^og2gwnS>CN~?Zk*xa{(dgpNq^K$x| zRGwPw2<-k$u-!)N#~eG zDmAfK{>nkH3s+;`_=&pJ4p z-0%PavDjsg?`JN9Fl`jzxINBH8;5qn0T5y=s+BlXY*k;s$buppd}CAi`_fwxQ-QJY zo-EC|>xD#Mgn2DlN-s=gW6w-tYc1FJf@6AbB~SUv=Ji2;Cwf1QJ0SGp8|2`gcm}i* zOlN}^H>L}@dVxv9%ZyU)V*|mIYikDK11=`8VGs88-{igI(>hWR^|}}BUxUELu}I*M z*7}*(E8&dI_j)CB*?M+77$iImykOIQhFx%!Kcdz_={7{^dm0YdE>9HuD`hPdbbABb z-pIhFX>djZK;}zR-Rio+kjsbbTU{`FUPpBFaJmLrN{+7Q^MjCK53oF$0tRxEXO`-q zAgI%z?^bwYCH^M`XjIbx`HY@+TzX~%teW9OHo-%{wNrMSZTDk;+gKiNRYM(JW9sv>g-LH&6*J-!R zO;v6We{J0UowyX(oNcbT@a;LS9^jsePz6q* z=j%Jm_ML0KrHwhS_cI(^S=5n~<@tYnOn}HPucyljm?(q2&^vPQ8g^a?PdgHR=hTh} z=monA+zDbwD*$vqFo_1j%u32-2W%I&x2*U@9~OzP52t%WrDSQ?p?8E34g(>Oup6F{ zl`ME{x+gTf(Cy1~-VkdYd)O$t@6J)8^loK@B4M;{6Ye#xis!$cs3Q)FB;|`R%J1W2XNjS*c4?FI06QTX%JN?fDb7*JRGx)08>#7@x zbN#%9b=9J5f&tGXpFY*06uhCs^fZ-}h5M(TN3!g55sp0&)mG>vPbm0?0~j?njJYE4 z!eAVf4EX9Z1VMcRf_%tbZ@9Dz0J8IF)bmdTXl4BE-fHJN0yJkj5&F`5b?*X1PpDuv z^u3bodRX&syIpp9%XC4fvSBX1SjIaSS2G6|?cI3BSpmL>F7jhN`Sd4#>{4J5$Xr@Q z*gM~BapcTp=fcFOLDKrdBc&Om$V|-OjDWe!7yYGHh;rHV1wDPm)q*$+8DR(c2IZ!T6dUDqT|5f ziKOUbBz18|2;$6yjxJnx%WvR+D>9Y?ii$0M7wk$kx_I{QZf=}o@6XOaR7@;ByWnU>@oTGfl0FHlc2^@p4`_*cuQx& z*x%9RxPV(_lJcI|efr*@d^kTum+t%Cl}0yKZ&jl~@@W{A!%buT<-`COm0E%Wg7;A} z$oEtp77BdLhxHc@7;tra;jUEK*lF-^AGNclb-{4@)>pR5DoSF?_DiF3tGBDa0l z9Nu27gS~TsOatkq*@rX%dmP5Tlps;BFM{tMyFpMgZ1HQt=B0b{JuoBxMJRacu{XZA z>)C@>%d?obT9(B7Zbd!dLIgB&mcg2vaeaE(6Z<-k&-}}B&w2GjbGC@<`{{relk3A< z0P!=Vlp2QC>v!Y?IYb7@csR2ti6AxX&8X-2Z+`|0EG=i|vDc8E52lB%o4S`YX{5)$ zteX_{f0BP0e*FzI01*yfe43J5ALVVGo;1_6VVcSXM3{8Fk}vT6>g$w2G%@_bJ}5)csYK*C>$<2|~6kxEq7 z+(d(JDBmwr1$Q}P6`cy%3ad)#x1m=JZ=y_4hm<(dYwu6f1&;V^F$Hho{1Ws(yk@$- zXIklpTw95{x>D!8O!e-yxpc?-tQh}U+M$I!F9U{pq3cby_VzN;4kG;jL_$HmQ&oA? zfcWU>`{V4j@Jrafzb_=j==k`b)a5bb7w4^bm^II0*ycv%5g=`pmwk@L1i@)r9_(JamuIK82QOUpb&2$~)MZ5uEf}m+Xe=88k z_ZrdAl2Fm+bMDNqY%Mp?>qY+6L#a4y`z@DD*KDr4ykfzib*hDJ?TE_pPRAsxaYsEb zYyr0?_#`C9a?2g4ant|$eSlX0N29J_WYwvHA}ctNDX|XUjWR4pu>f3-u2B=MNBeAb6fF+N{Rn(t2g440?e={>VfqhEczEW)|9oK7>&8ai7vJ{cm#-#xR8d6^ zE(0(8-U#`1z*^Km!C}Wz02i-ZS)dk`WC)*T|2T=^)t4g&bq*);Y zY2!4n|9rm8qymDE`GQT71oXnMiC$hV1g?ZXYe6^0OrCbmT^@EnT%y3bZN0&7@xv8^~#6cK=F@3yfxhT{w@kfD#}3Z|EspHD?!i)#J4uIZENKWIVf-Ujn>NjKN5^N z7=3xZM)-iUl;?8pdNx{T&ybi??9{xHqrhQvM3b z$ja08(hyfiBDwy9kUDO*k`8?ckBX0tkgM)s8({vCDrM6^&qa%-UHqLb^vwHLl$Er~h(h|y1$Cc7;&srKuX0nkn9(kYVyN&bAYF_yDlbDP& zpZamWcsvtzAVy-2aEyly_2AQOqUnn%!tSCZ`v`4VlE|h!8Vz(tU&Wb7Sa{v__zJ%3 zD%%8g`UyHd&an%C-unN(`t$94y%(8Dm1;zw=b8`l;KQ+92MiDH7Qda}eVKgJV?o6)qu&(_;|c(V1vU2Eg3SV6o_eM^ zm;eFS&op64iFIdKG~pT%KNcA7?!3*8eo=xSdV}!mdZ8>^K`+XXI~y3=`|(3I9x1-T zHX{lhV#8*Wy@-FDm3W#ALS)s_dv)+uaC#U6n&zj@rC621ekMcZTsl)TAx(o^<8)~|(nKg4(O%D_`96|Q=fa#C@j0BxJJvTq< z2R=+&XImMgZ-~1&M7D&53X^OyUYe}>_GfD8`3U8DNFRug&2!&EX=L6DQBB5Da0v^0 zr?NaBr;4N}>!}y!PlXM*p=osjeLqZ>D`au!`}Z!_m_BSCJXoPd`+CU%l^6W_xU}xF zoehErw>sv^8V1CTyVWVOgo{?vBQ!LOs%x zYv2~Fs5&k!=SQoOClq4AlwgXT4V_rw#2Zjni#`d!quqaK*oWI?P5t!`OB4oy?Y>d6 z?{&aeA;--Q(%Ra|sOb3bU-{o~-A_iC($?Pj&u~2cZy40W_oNiC*RpKfm(cD+U4@6& z1jo&|xHBkA#L{V_ruQ{0ECsSllY1iX7eKJaPdN}mFVCfx9kE|yc;_e_Tqhdv6{X-ul`%!Y?u+mF zhT?fv-r!DZ;`T-Ayt4H1FTw ze1X#;{9IpFO;wtyj+4z31}EGd$mYoH@?LMg-tLi$Huy#Zzrk-A;Zs*DjYtaq6fL1w z-1c8&EHwD98+-}H(-0EmHpf4%I+XeyNNPIE37!UM`YaA^&k*B$N>?jX)gO#x#&F&?bumG-DXY%|seg7m>_>ND>ue*GGLa9YPLjJ8 zC(l9EpS6YcT22{9nbtSXDPFW>WQ~Dt%Y5mG>@12 zWl_+Ng7@cqSP8@B;v(l#f!+vc z$ktsfSNLNd1v2#DR zqWQ6Ur$Ew9Do6{m>Hf1!G{)zlql`AQ+KL&y(<0z?oOTC8#&F zZ}C+@`E$ggHBtW=y_Q7I`$Bh-i3dSJjFfSm@bEFuZH(x8*Y%_KsLz>@Ud*TTq>-N zL_BeF4{cjmp@zuSi?kB}{xPg_Bjie55_1P$oEHj>}0)o#p`16~LZwxPy zoJ>MzH*|j*2&E5t>;$2tVZkiE27z6IZXI@}P#{)w*0Jh>$jzLITRs00wc)uSNVzbh zseTvN!^mh3>?4@5uag;gw|7WN!Zl+gdjvKd1!J4?U+;N&hCrV9Z(%^I(JLqL(^chs2O&* zJ`%28J;!qI-w0>Y`hYSvP0x)G5T>^SNvu9Gj{c9miVOWOe))Uvox&h!L6aMp;RJmz z2ArRLN`3sb&XML?7|dw|Edgxa-TFlda`g+MT4p#upH@v?g!RIO!E&x)z+!O|A2P0i z-XloIF34wU^0L#l<)i;JI5rskIH5)EezvI&&k!*B18cCHh)#fmJp7y8s48ltEKJ?m zRhZ*T$#-2xDXA`exzPQNJmfn?;+nZXnRy>K*cHhAUR-KqH9j;iG3;Af(KF4Wg<6Zp zjdl)lV{yk~;;M6T#(Yrw{lu?sra^hmCB!_XDCrjm@4mW7ESYR@IfX`3`Tn`$%ekO0 z*-NXk#aWr##Dq#=M;D45VlUIf4r@j|C+3eaTgO$=kT_oClD0nNpPHk%`6)39k%TM) zNM#H5BO8@XQS+x}D0;rw;QR7mF;NOdMJ}~Uz+g8FJQmY^1hG1C=Ynw#8Z>qJ6_dta zAgnY5lKZXO<2dvDc6^K;7We=A;edo}D_Ehu|Kjmuv;5nJtRvC7HPfta+H}u&^Zb=t z{aco%uIo0l%)MHt(viFkO2?Cc#LGWX*BW>EN`jM5Ux}vzN3)aNlZGwLCd)3EJ&y}A zl#-3t6Z@cx3di;bNJ<(*Z4|G;>6WB%Zt_#GVcUx!ZnfuVrLBau)Td7xkCZHE zO+xhtci&1Xn(GlKsiE80Hd8u3D*M@DF_4C|_KeV}#qB$D5mmzKGuSk3UH;xMG7O^v zvJPDU2d7^BTPCFxZdO{lVxr##Ri;=>9;sRnFMCG;pHg8Lh1JW^{%?f=aHr92;%d+# zt19X=%#D!$SAZJaHSGX3OZKKH+eTJoZ|XZ3P3LggbM+c~IF7`ukggbCd=9e?WJyL#~S?krg-!Q0lt)_lC5L%vAqNCu$`Tq#P7}Fhe55V=mc3wMXE7YwAn3< zSdwxCcj%Sr&zLkQ(SF8ZVMzx{ZZ3kY!o^T#04n`Gm~W#ee ze2)f`k8X-Ia>J47tG-ZC3eRc+ia$_4YlXX|Xzo4burhx`XqY~e&821t%OeV9XG)>j+Xpq!!R}1PJS?IcB*`f=SWwZ0 zHhJH2+})g4E0@ceAMQa2%^@4Eur$j=?pU8$(?^dSc-J`@&F2u|w!JN0LwPQ%gyHYP zp=AJ{bBeG`2+PwESU`KNto$VVLyK1QtD19j!{aZsKqJb2e^T-@0_W;xSBDSKozOxu z^f?c-u#<GYGWlyjS@uBo{phshI9UdkFQk+ zm~9e_b99U>L9<7zJ8=}2;5oKh`T)+yYwHZZ%lSO|q zeR6UJa^+gs{=%0#&*s96z+N;o_pk%4&n?WK*mb+Q$ndL9v%;qNKpAEF4`TJFVRlL- z0a`iXnQR7OM;Tttp8aMq60*vqEK^OiCAk&~9@OJ#>nxA^LCsQNYeiRvHzD=m;yn5a#4& zVC+a5GJ5{su3hf0IzO7Gm5G&B`I$HX#B89iIXx1hlwZ)xFZDW-35I8s+#E!AZNk9ow@jG1bO zo3g#}n=xDn;UIDWUHG$U4l_=dN(vBIxNCmHuE-(3Udj+`T9l~jp10jezK&rFdhXsb3+v(wK^ zN=ZAF;Q&IfkHG=Fv$pvgiIse>#|~9}E4I`~&#N!wc@t6&nBJGG(Tkrdcyyzx*^Pee zmM8dvywWlaV=f}wirU*Wb#)B*6AFuYv6O0wp5kARPtGNoUMRfkyKi;?+p)|(-m7-rN zM?taj4IK9C;ye7ltwzlM87iu(v6?*+qGLfVy$?Np4HoYe2=2%9F3DIbt_lk35h+`u zXHD-Fu*C21GYE4dU=gZXnUcp}pN&%ZB&P~Pd7|sT|JE17(c|)@l*L3++Q)n!v zu^{2opP3c*xCbwDFB^JBCM0!rUDVW1c}~(x=(p7l7qXAr3|QycL;JdQSFab=Adr*A zWWC)H0&|!X$1X_eOAyFYP!n&b#`tklK%-J(plfp9n5wK*>jZ|LK|&Kwzajm zREE)1k^6^G7uT2l%op2M>wAkvaoo<2Pq7^DEAoJykO&)=>`534y5*Wjhe@A^kyCDTP zv~OyDq#oBeX6e02v-ImE_={8Ua9%Wp;N*CLl!h7|l;PSKgj)>Dd#mAs7AA2ER*nxmiFXhgelBB=whM9OS9MYuvP z)&=%vLPAFqSY?d#`1mI==!Dl8zS2YJo1UtBr9vmA;3&n!BY${iUxA&QR%3DvNDqR)Og%9Azi^3u z#V+o{Xx~Vvyr#7*)1_^dH5+M{PdbVUi`HU19VKvF`He7iKE7h8FGwxnjc~dgS6Ew! zsM6Y}mFc)rA1H8>s#%DQPuupJpqRXzOS}hPr0sGDI_a3{q_p2$j~1yVpq@j*l1nA# zNc!|gIrifp=*3=C!*!yF4f!7wChtjS340|BJSB%!l_6d{FC^PV;f4D;Ne3ssB0{Z-UnVwDHaC76U&GisPssN&#G&O%_W)03-MAnztkX)w|OgMpl+kUH^!f@OO zfC~+Lb**=b%DW}{cQN{C(L>vq0EQQfK;ew~Jf+8FClZ#}@U z9?U|k#hk3Q824{#QK4F#rTIrq2)TV1({0zN_2l0b6CWwSD3c)Xmn?~ubJ7$+5@yj2 z|0~(ogB{vkGgr;}Jv`6^#1TA(j~+hVv{{J2q*e5*j6%9QzJC2z;4hdGHA^tjRfxKg zX0&Fx>NW@P+cg3LGe0{SLTDg(OecTX1^Dv)>Pw+~?_GV;RRFWmbTP%&biYtRVWrT1 z_`3R!L4JZzw)){FZ2bTs{^>@s@+l;O0~_>6D9Z{l;}T1@Nke2Jy<%Ym?T@tIdW5zH z0-hZMIV>3zD1eO3Kf7;^v?xhB(v*}h#voo zWUOhrVIh-)YXOJN(W;iNU8$1g*5ZsnRIt1pK?COPak^r2y`5|%%XXZKoO_TEu@EbZHlh*G}cbk;xPM z&ugELhQ2rkdSTSrdNA;wWd5Rw#Wooj;9` ztTrd_()Dci=Kgk$Uipu9*#2dmrWS;TDxV`cK_m@7 zwD9H3zTwYTC1tKgA!v3-Y1HU8=QXXocpl`n$Ia7aIGwMNpC*-{S@m%NQ9icS!)Z1( z^}}u>|ANII6P)CnvJ%3hF{P{hSCNFId;|M5meSNY@TUS7mlg`En~?s>mXd6F#W_)k zsO6-_c6B{XYcywuCfwqd6$>2CtgMZ(Jj(63P%j+~o=SSQc%tXuZW=5Zdl{#|R)cXQ zK3aYDOT39itJ8lgoyeo-+xROj@x656NOTY2D>O0`w6z%c#sQt3Nzu^B-ONFZL=s5a zc55@dt(0CLl$pob2R1Cp=~j^1yMP4r2HxnP@b4Q>CnF+wVVBDrhnTVH0#Qvg{G}v| zR%t6;Zg}L+r?_@{0l1p@)p2XOaFtqII)TVfn($g)UNyU?#Lz=*`8D@!y@Lj#A$1i; z{pFCcM&DK_5fJzU$DL`;*=@)6;seb~zdhpD`t-giIZ5-oaVtXSw=5@~#+#(k9$-_p zjhepp1oW3V`2|+a)`&Nj<3`bRiCm@8oe_#wlTvcvuiBkeN29i#22JEywV$*#so%T4 z5R}ROsmjKd5Dx}tGh{6Cn%R%@JFYb#H+5p0R}_+{eDrtuwX$RvT6)!FS@bJZt8-B? z1JoJrWfdP22!m4^B^K9V+=D!`ef%haT|IT-9eeet5m*AZdwmqqPszVS_nj%<4^jo^ z_f{qj?=-zVN&wqOW9PX9zL57T8bTGTbhhH|iaT!T!rn!~d$$T`Zmfl7b?~>ECH&bJ zWaSZAD=;Ic?-4VBCmHHtN2u`Mlq0C`zVr%xt*EXI`%)(Pby$ z`ryOTf+3s%wxr{-Jxp=JduM zBrH-^^Ok?v@qS(E$E&iU4*EFA{gJMyxWU+2g7UNK8JkO}= zN0~-0EdR$#If`{uJ>6-hqb2&jN+S(8yME1viz{h`DAY(Ls)&iMPmWun;l8O6%77^= zxXRV7tCpoje}qx8jD)_-JzMizf$Eh<#W8XD^QIDXbn7$w9uAFCGSbou5<@_^Yb+6C z6Va;!@{C$vl`LB?a*uE5MCTUVoNS3vOBYX5_Ua=e>ivh?NJV1tX>QH)|A{KG0rF(B z%=p4QS^}bd3A*&A+)(ozkuJ;+l|U$}G+gY-zrQg;2(~dtd6@3@TPo@+#G@o z83-@%`V`hQASj-YDk!K({g8VXYkS=~MZ#cac3^6@7$eylr=J?`W^OMH5FSe{G^g!w z{>Oker?KKq_x_`!yd^Ai?-u^Y*EmZ5@|`%Fbyp+7r8^%p_kwh%wv?IQ-gt!Vq^?)_ zu?~hPcgYC0eNs3rt$cgyu;)ox+CdZj$$XAo>UmCat{8Bb?oL2T^+^DM4iXNKkOIa# zS5YMFn6vV|3QSPKP-?ojL7|c({_(4m&7%`BRrfYauwMjIvV9vZy-;S_qk-G*!s|*; zAT_H`x%yAb?{&O*sIp?d#}h$@qTPwgmvF-bM4NX!YYai!f@YKg2#A);bjN~G17dR2rVu47VSQOvq?WL{M8#v=k z@#;3h##KblAzGaFiTTCiN9(d!P0FqK6XJJH@tQWDCON~wg*i^J^qmj6;Arge)ML= zr^$$r@F4b=ly)>4lPSMRulp^ylBPhm_=jDmx9207b*o3=aHrPr zmO3K^wS~FLw{K-n(a^tt*#t!eJ_aN*(B;;P^Pg`-E5AI@g-lA1@O#sF5UnB0g`a?c zhj0m$pwPJudtHKP9X1y!Nz8|wAO)~vzOb7@h3c57QXzLcw!v4fV($lTD1`2aXQg@XHgahbPpCUks;pB+P^4)3i+6OIud_f z`O)FOrl^I5mOog$^oTk7-!Nm8Nf=E$wew6v{A{TkfqO-Z{gN?VTrvJI7h0s$ogW{8JCt|}PvOA* zp>Mv=(VAL@UkrM^uhF_(YqQ%!K(RH^pL7|DDwm2&*7D`IrS9-ywZN2W zY_G%bhT{g#*>hOEd1drb6mZ{}Y9 zHau$oy&CU_v4zN0hvn3=2EE$fK|1D9=C@z^bb2TpJiZw9o+hb<)=S@^7hpp`rfP)ATN+<7=bc6{zl!y)Gx8-#_sPuI(9z0YZH8O?n~18Z1wbS-_I9w7tdytNCmo z2l!qHz}8c=+a`n#;zM{*8DF)DfOXxjM}_Z@wK{vAckci-eGhC>fOWqh>Pv9 z@?gWJ%P{qy!=l}su%~Y?4U|V&Sl+h~2ul^q?AlhVtpU?UT9|U;Uay1W zmfR8(w6nhs(C!FL=5dGe_?++*^4YGmYN4a9H~cjf;Ww~G#vvSX$@tRmV?b*&ZwW$$ zc22ougCE_^a=Y@$sU*Pu0i#6P4J{duyLKio5+h!j@-Rx<}Cv~jvR#NdS`vO3SL3oJ2gV+NK>yM>iKd2T`jT9WL-EI~)X& z`PVud=Ko(NyxlB#URW3aVXoBuYpnOo`I&VGC1bb)8)=X!E zd~#=SG{GoJr@|C16UfW`pTTWDl71BJUt-1%)jJ11kv>xo_iq+r zQ*9)i*R055f96Kkd@~x3<~qK)z_ShFS{&BQ+Rli(shH>KB+T%rDj43UPU!r;wXgV! zm24P~GxfgR5V5J>#8(#=q+R=C0z_t5LO>-~su3YYYeR^GUSLNgq0)jXmrNk%Mngcy z^-k7Lp_JR!DkRD6R<-8s%`JET&R=kA^@YUI(dzy958!gVF-!CPAB*PGmB?<_g^^b? z1K4Jc1tD7UP*=t>Q|l3`FV1V>k<@+!OjynX$15n_)MBynR6W7%nO-PuGScPI{bt(m zvU0YEtOfK;N#v<9wPz#A_zpJk4Byu%BJ=(kzf~xtz71(&pwfnI^rTZrtUhT8`aXP8 z$pbKnS_HTUa_nB9je0WL(x<+?JR+;1flDRUP0GcO`=#<=<=ejKn;*$^(N3+wkG|D$ zH3qL7SLwlLWSEqI;Ro6@F~H5%8j4ydh3JW=V#Ja)uFtuqgVxUdzHGw%?(Pn4uWE?; z;tBuFttn3RHnLv3)v+-TO6n#HMuvOgjKhJgv%|<#H*$l+rO>&UE3T@^#)tPwT%{!R zO`XXl1kxNCv`y{U7fzH>X-J`(Eb2|xV%458hVN%kWtfYT$+M$Z8&n2|h16E&5)aF) zWfVdSe!MqXl>g5qnvWfbH5zJ>rm9c*Fd0`gLmO)HlQMlpDd6c`dvLAaRB3wnIQ>E) z>0_adyT0(u>{>KQ13`||Z4Z32E3QT%c~xa8)e{?LAavWos+K>UhLkAncYstW}1XbR1CRX3(Y~FXqewcx<(3-61@PD z?(Ny^MW;a*`UCOHK$hA5AR&!5RlEhV06mKayq+a5Kecd#ZyFL3Zsc*Clq0Bxcn{$s zs+qk99P(xsy12-KXGm6f6S+8)grP+c43aw*v){U(5Jv2h42%;K_$+=J@-jEaxWtHLO+Bne+M!By{W-OQ)cZ@Evym8Jf-iYhxN zT%81+MVq!3BpBP`^|yb~OXoiRZB(bTHTc13y%mLUn%(IaSHqv20X^I)bU#vQf2N}M zB5TA*v$vt9E8z~ism1iuq(z1IKG|IXV^?{lODIa{J`xmuvK8dddmldt6MKEL{JM)6NWpFDo2$GFrzM)qb>I|l#hPq6aIMgFjXv2$kV0us z4LE-nRrmXcKE3;GY*KaxVyYxUc$k(#%F|fv+cW0wSdjc@8Wuo5_=x5iPQiKo>8=?YY!9B+u(uyFA;fTmtJ*~L(VKPS$KSH)BMq%|oqlE`Q zUtd47CwEFZ`o?;I%#|i=S5<}R{FOYzukbqp>3-zh(yPn z^O6MUZHGm32411KJNhZ;Y{dI_y>rda$e&pnaS;06)x!+woEt}H6@9JWpoa@G9>+QM z66oqWl#ZZ2GM<(I*P=+9TfXB@XqF&2jklr-(VZIJ8M(LGyPChbyE03|%Z_Zr-kU7* zq7LZN`2)g<4Vlf9g>T*%m;cER5Qnw(E_e3_JnaNfBZ#Hb{;_E1%umbf=|2m|2fKm! z(f6)yjZQ9TCtMC#fM!rhDn~&r{kP{|WkaDsjd^BNOU=coo;E_^7vc1}d58Dgv_fYK z-N;jT>5J3A`JvsNkHA17s*GzeGTt1w+VWgvA*KPRfJnk&%$N5_6E6GlZhE?aPU3aB zGOi^+)P1NI3i@@ZPd(AMQZJJHyrGRlt_9)`jdsyHN97VmI@IQk?kml5Gn>YVoS*5m zo386H3jFm(M=wy1jl1QJr+-b-_Xh+@l~&lFg=(@&k5;|ZZ^%cu+Tc~8Y0?lyzKI(m z`uVrw*Xejup~d1)RRx?egZ1PrfY5fzk+BPhk=9c&h_)LXN~?9nBw>xRu5 z`4@iGHJcH3vxf1WDsh{-5f#>2kch|`(5AibPuB`REn{a8Dc+7tko`Oj=WecHhN-=M z7cJzNa~{q!hZL%g4bT*Q-qM7>hHvKnf{;5j^^BhY{z3Akk!D^gw}SnAcwgc}e-{k# zF-n=)nM_97jdBiOV`gbJ?0AJlF!!3|xI*q*I%Wu(*zm*KERJ%z_bb{Isv7^voR4(7JJu8Rh4NxKo`inr zPxzj|^_7s|&$)+yXg`B({#FRH4c1)hxLbe+#{#^`pChq&S4IN!k!LyGFG^qQ*1tCq zQ;!VM%e(D^g>nM+N$+&{=o!C6@4YbL9Q{BE^y~GI2!OjLTpgSsq5$-jxjm4mi25Q~ z>xp{O&i;mNqj;uRq11mU_V{rxPaV0KivXT0E_b`x=b{kt#!?jzz&q=V#i>Fc*=|i` zj^dT-ylIME`VU$Rys>QGY3WtggSSE>9<;x<5_AbT%y117C z0eCaK8Mi?xlD^v|kK^E;f-*{M8zx{dsiA-2#=LT>KEt(=! zgERBE6anOm!T>-0BRO-qMbjnjzhM>g?-ZJGd#{b9H?sa}|@;U%D8OZJPKJW_R_5!>Gny z`L1@nz`uO3&H{h!@Llogr_V**T^R$*M^x`WJxXo|La*+7bimp9iIEvhJ-?O0{KXCt zqI=K(4|{JJ7Du;ri{dWbxVtv)60~s%7F+_|NO1Sy?lc}GIKeeoNN~3x!6ktJ!9vgk z2_dI=_wzmX?ECERo@+nPZ>U~Xt7=UdbIdWH^c~ar2l!jxCg{{%yy{*1xIZr*&Mg;V z=)YBT4={9k9xf=xK0N5!56&c>&J0xS-d8N$3)Wc~koDr-cis!0H3DJ@0n1Mbi=Xsw z(!tTj2f*ai@fJ@+nbq0J2WAb-fPhLv0l5{8E131bv>W{1&rQX-RRN2>=fZO_8*uIO z|7rkA1QII8*JM5YB_Pf=EQ6!R!=|LDj{gBQ!b+Ih%^8W)8zaiz+1SEYB-wxS^EN6 zA3MOC_r4svsiMnqhl*ZqDTKMuFpeg+L2$mc8u8jNN) z(6w*CRR=QMi&VsMiz{Mdve0!{aEr4ZJXJG}tS0MXKH3a)Gn%4}>h~iSzk5mnFNA)x z9=G@k1ubz1VX^}%)FrUCL4J6!L@Ci|v}$~d-zz}y_bm7K8R6d`!XH1gp|ob z&1n@IPw)pQ9+nRabok+0WdEo}^jRjAxLz%o?zbyauHFQoK4A5349Ub%h$fdbPGd$% z(@r#B1@dtIoE7gp89{x5^^9^`lTVSQ_PcZSPyUu3^;SFkLF?%4?dq(i%JbM;^G1It z)@}Lwz1Xb?muf_%ep9jp=1X&Q_**aV8-Xpihi`Pi^9h9h4fu7s**T*ziNa1;5*~;~ znO;2Zs;KD*{t&FBqr%R-KocK|lN2sf<~7GCuAzwh;U48oEcAND+0<`ZJN5jNc(E3w zPc|exTKnogfXT^_2dLeg^A)`)x$z6)=N+1#ez& zY@ayII!{WuuLm^l=g~JC(jj$U)qw6TJhx`>wSOMC&>TancBBRgdtpiV6%+0&CSyVM z5{;Qr+0PY0Vqu@Xzykw|{=dFM?04o@gn#N#?pP&jDK*WHj_;yJ`OpL)ZT&{e;Lc9kop3<8fh4NN;k~BgWWudPo z3}{gf?tJU2)F!N@9m6}9*LPnpuWz?6C!yPwsx}5Hq~ZZAioZ1NO^_r$se@Sr48>ADXm&=|C z6oG|E(h?)6{e6MrF(TUl?oxF9pX|5SbvMovDXI;c4*9&^+(FnGnMB#hN zji)wzwtsaw=-;D02FKxTv^;%3WZKPU8G+0_uwzmmM+r??CiT7+pM5mj{n+ymHr z1lPF{0j8=5|Hc85-m&)?ewJKa8-u>nqbDWX4*hUC{ugh=xvLque9ociF{AnLp?PC6 z7RcaCqGqrP50z!p&1#es>c3i1H?EMn){7M6e{*~+l8k0Q zm5q+&EmY9(l7<>>CW2C5lDBndzwbuz$T2#LrOE-eesmJ>dwh43Ity>HS#NJ=V{V|V z;_^ND^!$!4ss)f}|>-ZiLSCF%G5 z@Y&_HJfaa(v91OC{5e5r5$V}4Q%+6miVi}<+dJ#%4nC=S6i^GCHNr5En{L^Sb?9@E z0%9ik?EL3|;0(jY4x`;C^Jtx-_a3J;pzTxsFE2s6KZliqA2F%p%?PBgQAm!Q;|dWA z^h=rE&Qd`r?Q*hJ-BJis^Tfe@`jKHw`o0Dip|PZv;ue0~WPAez@V+jt!WOst?#?|9 z>b6&cZzyWZhd=?C+;o1w*~%prImWaUHOz!qAVMS~;xsF5qz35Y#@mDaL|MtX_|jPC zT~506kDydw$1ccxwa>0VA_PoGCmq|k(Tk~7f%#xtK_KWw_Uq)eyiGZ_2Nza#KU|efJa9gU1ZvXC;=XI71?Q3E?2wUGl$KOtLR6&E)${m) z8glLrwmKZq(i$gXWk;dn=RZ>u?^JPCn)~ViIcPQd51jd zX2KWm+86ew8wb{R7H+cT6tSv2wudJSK8R!?0SUrquI%uAFLh+f2S`s6+>F>((%yMPu&ZUyS z8tE$uU#bZtTJI9&oSlX<{3ww!5=_4zeR4_!A0o2irW!^S*ne3$7K3}}xX_J$r2(IQ zz27I)I6|}s-6dTzX7SU#&PgsbY;=(-UR9gLF=@o=*ST4s(v8|p@!df|&65Obob`2& zG%9MBMSsyqZc>uspc#jbv4o?}Eox(io{{c3@959VpRq*)5f`Pg9Y)}jv72N)N`tu?dKo`y~yR>gYLnyv1je4Qu=KbCp zj7^RellmaFwcSnh+1b3u(d5TTwC(51*x4WxYNMUNz(AbAZfPUz(8i0ieosL0#{UbZ zrQ6*JU<`uH0W0umi_A;@|7tP+w+{FJyM5{Z`=bA(5AeTt@84zV{(Cg^|KaY5Nria_ z;s*tFoBsoNZl6ndD4@m_A^NK#p_~dj?nr>c{7PimbQZvvrDpzZJS&?-u0S8JLY<&Y zjj(oF^U)Ty>eO{kq+grrJt{gdic$T)DH09)VCukhWkjF4DXKfBPcA>EffdG{Na%?M zScu`Wx@zJlDb5Pk4j6FpP!1MG-t|xlWI;+ODb4|Ou$TbXYc&S2O|M}9VTDOo{+L5f z$&1|dE}wsXIqKe>XE&WcPC~2GtbM$W{71yZ7a^?%ggjeOZ#<`L(x~jbJX?kwWT)8n zB$-Wg!zN0dH8K-7dcnDWYejsXiKeM0b4$E=vgZ*F{=*{?CSHLfL5gqhaIB>6O{9$R zQYSk<3{;H`w~%UgS&tB^Ox<2MBo_C+VVIx4K;-2`Gsj+UUmflYoXv?Ss5Tj=Au^-p zc+A|dh9nB40+4H;sBg{Qmih!yf0X+3H0W@}Rr9J%o-0Fv?G1ud< zeJtYqr6d8vy;ffz238SbheHW*A1h+WWdTTq1^%hzS=_QZ3v{VO1lC6#3+Mx;xO*T8giG22ybSwhxqMm=njT};`3bRWskdIMA z^|CM4`QxsS3}ouGhk3{(18R(kS}I;I#300)v+;kTwq@YVG?Y(`JAAR;Im}f7jVIR6 zq#g6knnRpX&~r^x<5S6F7R5wL--d#^>uJYYRf>ChkRPf1C(gvrPwODia&1`RA0L-{eYi#nVh^7ui=Qf>5pVtem$iT3%qpHpP=N&p_YzS^l$erY z2*_5HyzRiW%_5zx$sAMF0LS=T`zgJoLPgWE-W9O;LTjhY~8x!F^*Ok=3 zVF-y(kESt=rlQKifwVm;xd2f#rVRUnAnWee+ut!E2%t`;`BA;9e6f)l%&-%x5ZRq@`o9sYG#45(j ze>1EK3I?@!4&S?LYy11(FzB$zYnh;9C@ar)+wm~rtE)<%j^Gf6a$jOa<)}t~`^?B< zG64Ga6CFnLcZ;88Z?`mb5Gyb?HtnEQ{+kb9kTl=Z$8>2l;Wvt^^}tQfVAJngg)ZmS zdwy*`GHAH!*qFj(-cMB?P8C=%^P4+U(C96EwFacx2I#DOPJPWzuNa(wf-@6JG}5fX zTI}MFW|Ac`R$bGt>PFp?OoQ^_@ribK4uD#a;0)5n4miOesI#dzvik=V%d)KriE7;s z%~Y14WKs&e`%{TAEIT%)tf2%wnN%!zCJ>_@28<>i)GgERFE9Jac0}qg!_W)SU0Ce{?+n+vFK!*x%ev( z8SAbx;O^e+g#kBco?nv_d7+^Cvj|~c!unwAu_i@WQ!^Fmt*-GNanj3sZwmS&Ii)Vr zhmgX7)&@L=-2L7@q1mML7D?}g7yYqMPZMRRyhWEwD#Qs&t%_>zfQ=ja;~I!`dvnX6 zPZA^7Ob`Mm##o04%<7|QCxeN(!<-+GvCIz9P0V?5i*?0qB^SQg!a92i*j|z_J%cm_ z@9@K~!q3j1b824F8*`k;ZXN0ABU+HUMio*zqr5foItD0-aW$M7*~IV|*-i07>AP@J zfXZnKux2%PnsK)Sc}js)JTZGJY-Rz&9z}YK&j{}Bj4}}>hvx4uPfVA%cE@I1&WLq} ztL)(-OF$!$0=TXc7G|*Em#njkJi_AlL9A7$CW%uNuicei^YisGuGBDu!*(J(IQ;3yA6|2v!=H2;sO zJ0}vd1b|IR%>jxsz-mC1_TM57G2t*0K}s9Fk9$dgQ9^K4li-Y1h;5~96e!}cvd8N2 z_5!tq$G4Hy#}H~y0^wj0%*pkvU_}SXGPXejZgprLL4xP^X@XhijQVnVKQL$_c2HH> zzB25WBgR*Ix&J{VuJtB%2f#wrS8LmYX7dsZGEwIEk8nhTw{$>r`9GP2tMI=`3IC^< zO@n|zfL)nlRNnh=R7sDQM35JahTPY{tk8Hso++(qS>a0uKJX0M4HZUm5PZDmT-9kV zsOxjl1g~@is38jaqm+l<5QbsjCdx8oR-zx(Wv>7jqm1tWpTLeZRrQSm+*CAXRGDAo zEjD!}8^4G^($@`^Dt%s8@lgo9?kmzm2c=hqRAF;{H6_>W@3ihy^A9ka0l;9t(+(+G zeHTlYwXEbFru83N{+dXO_V?+p&~=RChz~h!Uu`bnXUzG$KL)$LWt{(JJWp!~*!iG) z3&5EZaVmOZs&i2_!7S*Rm^8P_84(%-#h^q6qZ)*P&RB#RI_qyUIDc_d7lE>_uxEyP zlhdhkYx{D^TQ4M$tO{;LlC536@?Emh`nR;3(yu8F9OU)tXv6hLr_!1aT-}r`{V*M! zxL^WiRvp7||7l6*++0OJCu#V1wy|=f>#WBpW@l|b%vgk(tkw!URsN|N;ZjQ7=xm;r z4eLlhN48vjht$Mk2mr zt@}kbVX1*vSah+hvYLcbiwBA~XHQ?K?4}_kNp(E8{wd|I%Wi0t%-R!9-0nuLpyC21 znud%W4qyPEWf2_JDERMQY+7DA# z$C>@KiIE|V@ofxmcaH?-?T_j^U8kU22Uat1SqZ$5&m<=)%zx7WQH^Y+GCzCK-+Xk0zTM!M4mpOY7tobI+Vw7jLscXEIZZ@n zi}`n1ub>Bzxt>7#$rrR2ABgp;!LNCKuFP8Ezg|Ugi${i6(|jDm(yTwW`2>lS?85S5 zr&rNZC($=Y%GU<%!UZ(p#9eD$b8E}UiO-Q=iz5kQTdX|=h&pebFhGBIp2Z6gC2yjN z``n>v6^N(qjMDcn#Dv%=YHT<1m;M(EmYu-b*_B!JWQ`k52WBqQPPdU%|I!a<=lf$ScjjSF6XS zkr<}kRDxvYIPdTz0jZQK;*q&y5gm>p#CU~s=fiv}9zRuE9rSB04}oAs#M9U!vcFx0 z(<-b|YX%Kll@-=5J~|L__l7$);AFRZZ0il=w1LKHp-uIyhMqW>7GM0+1c@j74QD>e!|Nk;wb2|Cy4Rd|#>n}@^+7jUqL z?s+`*1y_g)#+lbBFzMxj zK<@%i)R*y!`E#+pwc2NPEcaWIM3KtWK7QSdQqW+qS$2Hg#r@>^pHw3x+o9s}3aY~A z|NK=x`*#D>-<8R;Hi6gen)7k9HoX5W+T4gi-VN{DLPA5k9b8{WeeS#h;nTiByHTJa zL!)oZGHUHcaXO}_!3qtPT^hkHDG5Kjy!77mjh8aE^zRAzob!81mdAjbAUrfI_1Hs7 z+N7u(%mxqs`R>l=v3%26k1{qmm>;dhKS6z@`(>S=K2u>VA?t?SjTJr-=pCSY9M&M3D{&$8Z>%=J9z8BV>!Zu!_YH5a8G`BH2pxD zot6Avk0P{!zo&!!_wwbD7yuT!s%YRdJp7Ux=Zxnyse7a3;&vxdJo8c&^!iKkQw4=M zqZpXK4>bm6-oEAauZYwsw3l$U^mJknEi6LPhB8Ru-W+3=JQ%vgBZA|Sk+Fjyt&N3# zXV({X9K7#DM(#o6l@%M+_66{%xVtm7Dl;2GPAHySFie!;>D1qRFO}iFzUU8@$FX+- z5465bX9f+U%sXW{>mmhzHj9$0P_oa+qlpx*q-p_$)|s+K7&}C4IK~Q1#OzYwmfx3* z8gNB3eDHk!c*0E`k;Zpv9{cedWqwaFU0aIB&bNF89tVhWb|}#-KPJRCt6ll|ZuPZ$ zh$qtkW#j=_{qTuzqIHz{ePag{qID+<4l%SPg3Ga6*_i4&n{8xII;O1vF{vrn&v6$M zI0tne2gipBb}XUKq@)9v+~l=iQco4of8Al46I;Pm&?9)0DvhLpV7KA=VY)DyB@@Cw zRBr9>jgk*YX$SZ+1G0Wlb}zZFF9x=|cc4f-xQ4|JR;a$tt5HvhsrlO+@NQS%fqsw8 z=`1J3uX^ASURyVHkYZ%fgghq2KS0Y~SXyNUu=QU&IfMK6e_rT5*9|mypAQh72i~eT z-A?7q=^`*TYi71B4Ueo6c!v+4y_{Py4!^hvB$JC=5uC@e?tE;Ca@Lyq{sj3wG)kwD z6p-5~@$+OSV%D2`wjP(gA--E~Q`+uB;}rNFLBy-{QUl%bI02TTJv7u1taEe6;5~d_ z(uE$*gpG3~sbgeDXi!Xk&!WE2kgw#S2c|)Gq}hDJhK68c0%xpW4>IA)Hx-F-uVx*5 zFOn-*Neb_RXll0(*duuJL>~=O!!->kbo6+2`V(|-2NLAp!5x;Zp?~nj7YQ3eDLmO5 zH`6fB++Br9u6=5ir7MzWwl=%87B_#d=q{NxiIgx2>6=|3h^p_xl&{T(MbD@F^7+IO zr>1D4%)6Fv5~sgZ%)w4hTVFpmIX%1^4|0qf`JNV=W|u8@)O_<5C~n9HILQhM zwM035IUx<3`X(<-F3Kdi;4TrCj#Fdo)23^`v)@d_xIJ!vN~zQ!K)0DyQPrrf=)U#a zd{qTF)6IllV3g3e^DP%dIgRH)ov$7-CeUfk_caSrb!x_@FZb;LUa;OvH}`Ez$>qx_ zq%#_4jE(TUIWoSMt5T_X(!v8dI>%_Sfz;A_#6v z#sX9vtY^jb?@@)!#H?++rA6@?mDLOdBa~nq$KT~c0x7RTOsBN!l)TGvYVRdT7QJD1 zfjB#5)ckJ)m}|H+K;L$d%lhNO9@$T^wyDC$IQQJxr$nk4k6*0) z-RyH*$KTsL2fOMpQfPdpKwTL1x!>ni0mb0dlyaSg7*^Q;9wlxu>Pu1iT7td_By>pr zoq%*3n0Tiw=OgkS_I_AiO|6OLJeWc?$B7vFL#o3EiRYRyYz>V4amMmS8o_@*Z{WTi zZ!(Z&>wrBMx8#XswHUo(6=7Z;Zg0y$Lqwx-{hJ1 zmoHpaz64Q+1OcY}H0oq5$cQ* z$8Mww&Y()|+UE6kp<)AmBG!I$Hg;h^bF3ARRSg2VYk&8g`}Oq*;x;HjvgH`l7ze;1 zJ1BAF6A(sA1B%DrR1FivW4z$swqG9jRoMH*taA|f9ZPJUEYs?!X`AqANAQorVB46^ zmfgo>>|&fDORZSU9`@EO%JCrZ6^}B9!KvXh`l$a!7`35OpvY~Wesa{t#lsnPn~4Q?Q&ylxq`drJAGAQqepHPqtRnM z+X}h-pL&$1%fDinucClQy`PVLGRk7Nb58c~t+Pp~F;v*-+5r|q^E>qOOUvHX2Dbp& z`#{zx#IcVV212#mhFXvGN|M!P{umblY6kRTEIJcw$IBy?_#)LqP@$?*@=WOpWqQ!E zZ27>$U%TN2L?!Cd&W`B9UuSDatrhhM^xDYObEziyC9bGQNiL{~(HL=PNtD<3_S&e7 z>9ZOtd59(%$u5r#&~HioJ^<_HZv%HJauxW*(P^UCP{X1Q!6@vimffEdOz2%b}C`%f&VV;3e1RtM=2M}SE^YW5*tWDTTYU-p@DGxJuRkT(R z!Mh{;i{Jzh%!GBpFsQ=M*+V8>OCzl05uf~P;!?qq@^Tm7$16n&4#gI6>a}JRICla%^u)s+(z)2is&xfGOD+VHOpz(n&+D!|EA zV_0JqG?`Q)@UL(%J$>qdJ$tKTuDeC2x|^JT6l(LFuBvKW^dKx6%7)4Q&G>~#+|xJ( zXHATZn69HB`vrL1%CWNOA2iW!MbQ-L4W=Y!Yf|jL8`v4cecy=DD?g?1w^3HK0kSLA zLnR0^YaJc^82$GI%1f9k`Xpb8h>U-uk)hae?7=`96# zN7A%Wi4X&)j3J2n*yh*>Jy#DkaTujO;b}&_SJ@_V;y6;9u;z_N&Z;9uC9aC?Bofhy z>A}!L;%8#;-$A~Lg!+7USl7MeTjOMBTBj0(DIcZ|2Ycj*Ri4|RqLM_h9%r=rjJ@!H zquAhhjFQTeVnfaqcgaviER!xET7jw43Qkbzn3~7Te+aUvEQdDgFo`@Nf2UNzF~z6k zArEdzXD(TUfYS<+FD4{QctSa902MmJNLtf(;8kbi0G~E~EnRu+yc*o%XI_YW=Cs0R zQ7!i(Lrm5^|ulbMf_uS96(EQrHp9J+FrUP@EW zrYKjfqtGB+*3?P%?l#N5np3e{4N}md$APChIj%rw_MSPVdkJ?Ne?6_%=G?3%!?Jd8GN?BJoEn79p z(d=+Ph%HNjIyb5`U44Os98h$ms9<%bc`9zkP9|)LQ!Lj`>~9gB!@QL?owlz^r!ojvwG07mQ&P@mUxK$v_K9id$1SGC0=<+5s0 zEN!gBL=OlU`#1rk?qw2QCL0_%)vW|{C_9ks$n+dVUF97pOAD~$nnp|C_27_|5y8_i zz}p`74jkdn8^eS|$lIRMa!h=b5Ah`f%tcyJE@tgo3;;yi*swMw%Z8?~>x-iqtuDlO zm70^hc)?I)08!%{pKXFDR(@iZ5x}R+MIZaft8C;Y0zZM+KP4c^6k`yQrj=|jG4Kr6 z8E3^xRZ|MlMXy~dEzxbNNx|O-fD@ugE3c8Vo^Eo=x{LYvND#-|08wsSLyGV2{<00Rhz+UZ{Xs zjsSO(r`J3puMkPWMp&JCwX<$*E{dn;M!vz2nO7Q<(}7ZFa({-@wpm9rN!{jnvk#?8 z6N^DQU)#Zj`O1;C*0iE&bcmU+7tJ?0dRmrtxMH=A2u{z?{=9++Y~y{63mx<6JVa*E zt2Rd052tu3eg$K?QTOH_vV;4)#J%{GzDgG?IR7afL3A4Su~=ErQF4BM9=3amfEX}V z6r;>}IK(BWL#+eIk_5j1&=4LQ=~okD{RL5G_N-_X#{~YTl{_fQ(R7QX=mR4`+d=Ax ziHMdru9%74Y;}QNK_x1(LLdA%tCBak_>mn8acAY`SE7r|rx%_x zGnVzsK#uxw_T0g?f?xVU@5rhiHFtPX)q4do@qBV-g7hwP3{!y(H}i%?zez4JX%_0* z@)oCx9?)QKEgjx(7Y^i^Z)pM1QZwJv_Em39aVsgqahMjUTFr5a4{rQPvNN$<2E+;U zoM;3uCyamE!zK}7tqZC^B=N)o2zlr_q{5&>kgDV~dpEb(MMu)LGozyF;eN!Vcn)$V zMgG)?Uul|w=>0pUW>vtBqqT};LmQvR?h^hqTgm=itqX*3WsrNc)WoJK0ZEwt|pW^*Cym$1y1fH z(md6m?&j=x4PL#wNrwm%ku51{Q!`|W@`s$REo$wX3$V*PGot|MJ6oTsTHc6F2IV;O z7G{7BP#4FmF3{++T)< zwk@_fUhX5T#bE|=hFPSm+y{?f3;i$;Fzrbd8GGHG-PWe$O*N%Rn?Y55gB)Wm)`Iik z0VGThP6RWkO3N!uIl2p6&=)DL>!iWY3YcxiuhJw>{(kQ%{I;*Ek%#;vwZw_Qz-`hs z#%yWr7O%xmGPX2EO*?r#Q_-U%C!J>&fEC7RUC3gdcoXxPF1|GH&27(;VQGyprf)4W zIR22M)&&%Ec{!~aACvb}#rVfH8Yq-Nm3J9;zsHe^?`drfjmfz{9m=zJc*Qy&!w!am zwp$MFy3`*f{#gaG(Q7F%B2C0QvNJMpfi5fzCo~+YKpj2AM-(QId;yAQ8ST4()dAL( z`}-r~FOxVz#;R1s0>ezbO`5Mc@8NJo$7P1DusAr*w_H@7g3#$lsVOfOQGGtE_&pE8R;|>h+47_7$%aavjB(A}X zzSu@~l<)PdLz+>&wTH;hY=TZ-= znz(<+?i^sj@YCq1dZE|yqcPLkW}`JUocUriQ#A^?=G^)*I7^kcKOLio;Upt_EPD1L zkY6&*dw;NU5{RKPtF9sckWG016J5&H8EKAK7r-XD`|*QW#6mHVq7^Zc)zQ&IE6DOW z9tkZtpWPVSEBbs=I9ritE;TifsMs1#8^M3( zJ6?>p?IN}0O;>R?LVavmTUvw=xmge4H=@Hz(e@2aib0xn|4fd&-8J<@VjUXAhJ9^X zkEW;OiS92}Fo9u1)F;g!hfj{NFQ=n&07ET){%o6dYHOm<9Gf0fHqnjO7`}~;Ykg9j z&V_iL5ucX_$@zr0n|Zdspp&RjOp0zvM>nCynBD{;j%OtPl6R>BXVYP`z5G^;j~l5c z{8#4*@FZfRz!1H`ASBjNRycf+;@9cg@qB}e7smvTzgU=~1*Xm`_0)$s2aqI z20)qeY9k1*filLBt=ATLXq+WZRcmByB-y=cLFX}p9DC}siCn%|ora)@9dGua*?9Y< zV)Kb_X)(WcXTTvQ5I~Paq$GP)X(cMzyz1cBoIv4t*F+I{tYVcm;c5XI6Q}sp(iCEc zOJt)?0fkz0c!f6>qSy$=?Rq|PfYPdU^p|5kf0hyd2^nM6C?W%@QF=knsPClO09e7FQoI}a7Q`EyDK2`h2a%Tx+C0_>#AXw7+*^FQO*@swXq6|@F6#eJ7@7J+3 zRPmr)hC4GeaK$HP0Up|CPyD|2^#dHD1Cqew7TB{poKIL8W5!BE;ZqSu6{)oZAQbz} zE72kKr*Wc^vy$R*-La`VTm2b8K^huZj>4$NTyWSX-swibp89>{P+@&`RgaTi1hDhltl{TvKi{+g>Z7S4>NOnSIq)VGD&(;})X=V%^x41oV9m zsOvh|i+Ki&7 zCx)oYfr?u&@Ao-QiuDj0lj?XKul^+2%oZyOz01{3?hgw@2c$F?)Mj(@3z3S!!b^np zN@_}A@d`S&mo!@63l+8M$n;d0zG;~&hUgLDc~Yv^s5giBgS4`UAYfXcHFjfFrYCl5 zDS2SZA&n$I<}?1YgClxHT^Ir0FMq`fgYARQ(&Ak%RBzt`nuD8B7N49D88kAoF7Z*i z%#w`OhdXbSv5;E>?brtfy#yA-B5-4UtzCV~X6;75W8qeC?`D#~3V1A&#+eFE!QZY6 zFI58B@(tujd1}G*mMQ1qS|p>K#aj_kznH0kSuv#)>0=a{C$dQ_(Nr-y1ld!1R}FN^ z*=hu@Ijlm~9^sI#AApX8u&QQUuN@L8*{(u0BGes;GuhtwJ-8d+plK4yknOJHBy8{K z2KBQfBq73AWMYNdKv-Vbz#Ts+xBB5RNOzO0+r3E3NkhqXerRsQh$p;Czg2L*zb&$A zrs^kJoFC2lNFPC+w~AaTf&Lm_+Iam7eug}AtF8u0lyBPkcpZvlSb3GIy-60+3jnDTFE?BQ*a<^!rPt4IH1UWnO5%J zc0~ZJd*E<7aC4`dvqXr`EX4XJF9&wni=3dz)psyNk!MDwYq6P>{Y}f6@=(#xlO&&eV)ez+x3(R;3P1eZ{1=RsmBbai{5f_oY6($TJbb8@yZK1;1 zA_UhRLWz^b94gYTsi{)>JqvI0BjUg$|D_?P6Tv!6P7(EtT)Af zp@{Ys4mVFoP9a+Zcm=zAyTxzTe2}8u1<^ckSSnab>bucTxED-KP16b~81=~tlvA$? z%)PzaOMqH-TQYu}hj=kKu|$YUM|Vc_JFUyW@$2M8{u*&u7n32f#dW76rM~YI&jfX= z-OSkulfBH`{sG6Hr$$bw=>?)>t`H71ni}LVx_`tPV(brJ0 zG(9~nJrkw-Rdl>z>_CE`aED)61(3~PZ_ldzVGy9uF}A=@Oqw?14ri0XIW#fBGV;<< zhc=j60v{Z^i6@ehio_~iDDnErn?9Rwe3pg1t+#*9TuMus=5#D#gUHYt-aM8dGn~c& zD_)Z}`0HanHTB%mM#R#RU2A(c;V)A&T&GV9NiFk4#@uZGhnoY&YysW)ijp*OsR>HX zn!<2+8L)xLWNV5l;xPUrhHmecqDPJypyl5+uSdr}+EIcz2RPyYSwOTBaKVVwg#QId z*EyUH@H2Lb^(RX7IE(d&AHlO{HM<%QC;Na<2;07u@B!Gts}I&ONaxD>$k8AIQ}*mq zgSY`iPan5qz1X1oaJ1qPuOkl~EpU&;_l#cHJ2nCct>?t8Yg4?IKw~|$VK=S`mypHh z=i2`jhwcB&__#$XDIn=7Ad{@6Jzi=Yj>i1B(;rLMw?p`P=)ekh!SUVXITqtGFzIXz z6KmUMR*?n%XT$**!b2kS<(_<`3NJ?O2WAScmjoT-?iv8g-y$*Mh@?BcZ+6`b+%+}O zNsJ`xrek=3V^=lE1l~w%=1oS@dMcsL-E?@b#0#<{kpY=-4tBdy-1}S4AVcF)}Ps zT#Pv}ym_*W9YVlH%7l#!4V1s7ZWHkVW>p{($$RM>6GngFS`KemQ9GA}oMjC$uy)Wr zX-{RA;3U)4RWsiFlz<<+H`{E5v$zIJj0+Qjpq%Fd8qz-~zE(7k30HjW`wV`i>1k*% zT-N%AIDVf?v{Bvx@fC!S?1#?_aDGx{YWNX@e#SW}UT#=-2PBw`TkQ ztOS4M*OIyGGeBmUdr|CmLP7_KF{IF}acSdY`p*;|(auhZ z&0?m21nrZLmLF*3Vt=LfyDl^6zmCZMg{)a$9WyQGm&NYK`?#ZWaf$?4(xlygB3Gnj z!oH0_GL14zYf7;^bVXH92%Keo2;!+|HTwZNuATUfU{gRK?OkAAbo*XKbez{3n2t2E z2cOv9!ByVCgwqt`x5w57vcY$5|GpxCAGUt0=IL-mwZC?tx{JHsd5@C zHxng(<8fk&2}+v;&F#-k3UjfDZ67E8cMa66xw;6-{Tc8osk*(wKHqpYBY}3 zx}LTu&7qMT4sB3SmPf^iS;89Rf_Tm@7&9d~ZP}=DRV&QQ1`3mv50)D+R~O@SJ1yh7 zs9?TFP+Ldp9d(69ZKV9OoK`%43h{%XyS$EPI98S?R{k z8rlAvtpVNV`$Fc01?FP3YrGt+n72ZffmDQr4j&|8IvAj`6UP0&{OX;^MG+Jszb+KwwIOZu#R2fGfZlMaYo&8BL{=7NEM$ zkjjjuKkAs2ELMXLy&2d<69Aj&5zlA1KvjXDZn)T02Z*`AMZ3yn{~!8OmI)Z*DVUlI z{!GeGL`S^8I@r>QN9l*-+obDkgGX=&-P+`;@xlcu7ho`*#DnG$dX~6u%Wyc4Z~^^7 zLGPp};3;ak8N*nTsB$S+SHvYEzJK?I`NtWfmq}wgVRV6yXTvkPD6Yv6c~cad?Wk!~ zJgp@pa`cLCl83&3%|t28TtcT(w#lE#rx^L7&)0b-5}rc8jaznD4ybdh8fqV?eW zEuEj01cw>}BT(1K3@bUtpoDkLGW9Mdu#V0YKk{>Y=Zg3D6`Al?6t$D>r?IEhp`&I- zjhlEt9&h%GR!J*}#rvyVjtg*Gl>oO{)ke`%i*>EC9hex0dN--J8l#6?mv}xyVjUSR>#8GyM1xC80>}1fG85zsVKH6K zq0-8uqQ#{xny(sE%Og!mr{2)-ww(Mnkrn3NaNS&V5>p7%PiBZlRwk1P1OsG7zjRhi z9amR@r3MF;XD^xWR8+upS@^ZmgzJ?{Sjf9vB7pQdiMEtKct-l1f_Ed7DtC3YY51G< z$*gH6JBL+=*{sYv`bY~G)g=r@MqV7JrABhPpMXAsnN9RscEqR3l&^=Cbb}e%(ka33 ze=%~g5xTT3R#j_8=$A%Av*TRyM-f@BF=Irjt2>qh@vq;XC6Ce#4iaw8Kw_wtKE@41 zcuKi%;}v!Z*Pn|xf$GH=AdJX>>f?}gYAC6;)HzKa7ZKPMZ=`E;^nBK+H@Z>QpQuc_ zepc1i*)8<^?hI^ZmB2Up=Y{sxY9-x>SfO}qPdkJnU8C7y#zB+Nt&jNHa!AgLnu+-} z#phP+RH|<^A-)@RY^Z3ecj$z&j7A`!ULemc2xHc!>KK2dV4t%>D-( zf9!cqhj#Cf^|OZ+V99b{Ml4p|TVoamf1h;z5IeiNie(Z8hcN2 zqx2t&9&cwViRw9MkrK|Mml_1r)#59Q0*KgsPZdg;Q9k7_4cH4Yys1JbHY2oJUcjT1 zdI?@!oTh+h&F9c{k1cKPKxJ#W)|))w1ePy<=t^DgqOSBhpmKXQyZei>i_u3qb;10m zWLszHPKIW0IB2lnX@YqR;FDM4F9{3%Nm!L`bz<|~>2NVY9V>FYa9I}2s1 z@hK65_Hu!NM55DE3_QKa-J9L^M=t#YSVTe)&P5jtEC+y?b@0=R7cr?^r zy=V{7wt9ILh*n$ApNrRIoHWLzI{0l`h`%OXS9da}Uv`sR`F5AND2q)#sWv z$Z-*CNHQctjq|GBxAkowFDD;7s|gLiKDo+vO4s4lT0jCijEV+By0mPd`NcBLeA#7M z&hw2R-x>^(iGJ$(QQZLc)Bn(d!lPP@?h%32| zxbmBWKT(wr*MKd}BdxVVaIcG`qA%;Z9*LIUj6pV)-^zMwC@pA%?W>)XHEd~@t_X_7 z8$r)903<3U(p(@^OP)nB+KTN2Nh&+Y1KjTH}*3=Jq5jOtO!WLEG%%$~bGwT;lbD*mFx%ImUv zNz9TrSPPA>NtPBR+8a_VrIJsc_%S`FO3WUB)R{fQo&M`~x=}d4Mj(Fg=PmikOOTQe zD=r}{3=4~iT(^&F>raS6a%tQ(E)}VoCVYzJsi)?AVQWTtk(UB2J-gp2ObmHdRcgipNQ`!5b zk#)$Qsej}-fi{1V>=N^xU+$B5#H*o3UNt)UeF|z?ot&-jzP%8J_n$P~{mrlD80CXW z-v~1PQ5Pb)xkT&vNB@CzBHIk!OKk2f>yZ%dYQh$ZQf zKRMf#u;bL+Dj@jk+xTY^f7&Joc^rU;={W)CgVRYZu;rAznv2DbFd$is6&!F^CHcqWOn)wrO(;chv&cJ zeG>Ah!Sh*&A3w+;5BPdQ;LS@&PXC~qGZ^2k&yqPp@&48;W`I|7jYz##lLZsw=G)%0 zd9L-Lu91L;n}R4%x_miPg+=WTF7B+3=3ye5*Ro~PENJDNO0~;1Mg0u3q2CGJ$|GOS zd;tN-5DEu*ItTG#&DVsmn#r0u@h2uFmn1u(h>y)uSrl_Rjw{Z)$*>#|~e4 zbMen>KCq5UZjU&IB=a7VE>B~5!0!_$wwIOXdBGn-|Kg>NR?+nu=^XKZuO-%FjVFG* zEii>!!;|zospWb{kd?D<`ODkq3^!F#ZVIyd=mWX+IffHq63GbBRv$L4j$kmt z_S3iQ?e5cQGZ5|=Was&xcy@n>_WE1u?~9Z=ztUSUy8Ofw-Si}S7@C-3Ls{+%wK6v~ z!{+fZ{lgPXPA>57@(}|O2bB&#Pv8BG)%iIFd%9VepC*5DgxejUsVSh=)s=`dmtG+& zCbkZd;FLs^+21GGpH#1p!~!^~DyR(iaI`E1zT9=t9$)(S0EANPXFj7{9x zeM%-h&tNjfXgo;BYrZ0&x)Qw*mP0)MJlpv^!xLj_J42med?p=UkqYlVd83vk`xC6^ zw>f)qMoV=Aquqn--@h-C;Up~$5sp7D!F^6ZIU%4CpPO!vo7LPYaWMfab@q2fXlpQG z{I`P(Um~EYas%aN7lF2@T32TxsV?s=kBs9NDf9gDlBvZzBmyq1`l8^RJlAa#HCsM7Zrit|7B?sFpj3!%2_C{%MZ^5K#BHlJ|ML;?sTt_%gd!kou_h9o5 zD>J=RH8p?Q18TE)$eo=roLV40kYay(o7{XCc1O);1=QCuKu@TPfBgA{NL7t9j!uu&b_xgEgd6LrsHtOabz3bv zIzD7@dPqoWmlg89gHky2{kq| z(34+AzurAmygrj z5HzxjFoxfc{@;^ft1PPSf7m@!9J)feO7&-SgVWU$E5c#;*UlS{{i-S Date: Tue, 21 Feb 2023 17:51:05 +0100 Subject: [PATCH 193/483] update plugin menu --- website/docs/artist_hosts_tvpaint.md | 34 ++++++++++------------ website/docs/assets/tvp_openpype_menu.png | Bin 3565 -> 6704 bytes 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/website/docs/artist_hosts_tvpaint.md b/website/docs/artist_hosts_tvpaint.md index baa8c0a09d..3e1fd6339b 100644 --- a/website/docs/artist_hosts_tvpaint.md +++ b/website/docs/artist_hosts_tvpaint.md @@ -6,37 +6,35 @@ sidebar_label: TVPaint - [Work Files](artist_tools_workfiles) - [Load](artist_tools_loader) -- [Create](artist_tools_creator) -- [Subset Manager](artist_tools_subset_manager) - [Scene Inventory](artist_tools_inventory) - [Publish](artist_tools_publisher) - [Library](artist_tools_library) ## Setup -When you launch TVPaint with OpenPype for the very first time it is necessary to do some additional steps. Right after the TVPaint launching a few system windows will pop up. +When you launch TVPaint with OpenPype for the very first time it is necessary to do some additional steps. Right after the TVPaint launching a few system windows will pop up. ![permission](assets/tvp_permission.png) -Choose `Replace the file in the destination`. Then another window shows up. +Choose `Replace the file in the destination`. Then another window shows up. ![permission2](assets/tvp_permission2.png) Click on `Continue`. -After opening TVPaint go to the menu bar: `Windows → Plugins → OpenPype`. +After opening TVPaint go to the menu bar: `Windows → Plugins → OpenPype`. ![pypewindow](assets/tvp_hidden_window.gif) -Another TVPaint window pop up. Please press `Yes`. This window will be presented in every single TVPaint launching. Unfortunately, there is no other way how to workaround it. +Another TVPaint window pop up. Please press `Yes`. This window will be presented in every single TVPaint launching. Unfortunately, there is no other way how to workaround it. ![writefile](assets/tvp_write_file.png) -Now OpenPype Tools menu is in your TVPaint work area. +Now OpenPype Tools menu is in your TVPaint work area. ![openpypetools](assets/tvp_openpype_menu.png) -You can start your work. +You can start your work. --- @@ -67,7 +65,7 @@ TVPaint integration tries to not guess what you want to publish from the scene.
-Render Layer bakes all the animation layers of one particular color group together. +Render Layer bakes all the animation layers of one particular color group together. - In the **Create** tab, pick `Render Layer` - Fill `variant`, type in the name that the final published RenderLayer should have according to the naming convention in your studio. *(L10, BG, Hero, etc.)* @@ -95,11 +93,11 @@ In the bottom left corner of your timeline, you will note a **Color group** butt ![colorgroups](assets/tvp_color_groups.png) -It allows you to choose a group by checking one of the colors of the color list. +It allows you to choose a group by checking one of the colors of the color list. ![colorgroups](assets/tvp_color_groups2.png) -The timeline's animation layer can be marked by the color you pick from your Color group. Layers in the timeline with the same color are gathered into a group represents one render layer. +The timeline's animation layer can be marked by the color you pick from your Color group. Layers in the timeline with the same color are gathered into a group represents one render layer. ![timeline](assets/tvp_timeline_color.png) @@ -135,25 +133,25 @@ You can change `variant` or Render Layer later in **Publish** tab.
:::warning -You cannot change TVPaint layer name once you mark it as part of Render Pass. You would have to remove created Render Pass and create it again with new TVPaint layer name. +You cannot change TVPaint layer name once you mark it as part of Render Pass. You would have to remove created Render Pass and create it again with new TVPaint layer name. :::

-In this example, OpenPype will render selected animation layers within the given color group. E.i. the layers *L020_colour_fx*, *L020_colour_mouth*, and *L020_colour_eye* will be rendered as one pass belonging to the yellow RenderLayer. +In this example, OpenPype will render selected animation layers within the given color group. E.i. the layers *L020_colour_fx*, *L020_colour_mouth*, and *L020_colour_eye* will be rendered as one pass belonging to the yellow RenderLayer. ![renderpass](assets/tvp_timeline_color2.png) -Now that you have created the required instances, you can publish them. +Now that you have created the required instances, you can publish them. - Fill the comment on the bottom of the window -- Double check enabled instance and their context +- Double check enabled instance and their context - Press `Publish` - Wait to finish -- Once the `Publisher` turns gets green your renders have been published. +- Once the `Publisher` turns gets green your renders have been published. --- -## Load +## Load When you want to load existing published work you can reach the `Loader` through the OpenPype Tools `Load` button. The supported families for TVPaint are: @@ -174,4 +172,4 @@ Scene Inventory shows you everything that you have loaded into your scene using ![sceneinventory](assets/tvp_scene_inventory.png) -You can switch to a previous version of the file or update it to the latest or delete items. +You can switch to a previous version of the file or update it to the latest or delete items. diff --git a/website/docs/assets/tvp_openpype_menu.png b/website/docs/assets/tvp_openpype_menu.png index cb5c2d4aac02c9187d38420947c3dad25cc1eaa6..23eaf33fc3d2ba30452454eb2ffb4abf1f5337c8 100644 GIT binary patch literal 6704 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 literal 3565 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ΜdRthcI%Jgf}%x5NHEtv;k^Aby4zi}pSE zX^T;r#Y2J95#iRF!a|{Gi<^5X`mWgR4~Xv9V(e<|#ci0WrFz5hL4W@qES1gxO8zF{ zR!Zdct|L(e|BwqC+{%UXbKUhOO{)_hHsY$PE3VnF+G#UIXUO;Spkz{rRLBEghCl$130pcakJZ>XE!$F)^q{0R9-;U2E#vhNCUdul1XsSl7G- zZ^`XZa{qeD!CRPiYn;Auh{bjs&S9Hd$y8akoNClwn^2ghvzl5vl#S-o ze2-v>;jA$x^wjdBfY5A4HT@J5dDKKd7o9=0#o>rD?jLg}t`#Q_uU@#Ppm!7h5n{9T zVw%OzZ;vRmw<~RQGFm%3XF8g=`&T+Fr(FCsapB@vhEG7*YqG6I#@5m8t#ifpmPtaBVF5&;DghigBLQ{#sk#MC+Vq84C6?)Ws>|KD5=cpqH z%x87A#w0NuoFhL+6hb~#89p6AyEGwok8)&A(Gn9RC6n@?fL^^GpcISOG3%%`fx5%B zdBJknoTdz+xN-+x6)+&|x#+jt1ObkEC#5BUA6o zm>xS9lu-#4(K3UL{gR5pT za^^bfj_7GcrQ)lf5va|t@~s*!^+a#nO=C=|-p5wURu0UhLxi(GCJ+Hj3(l@%HPsK~ y997S?#{lekeon!^F!dMW+Tem`8+CxYLZ^#+FOju}V_3gv08=9~!%~AQ(f Date: Tue, 21 Feb 2023 17:56:21 +0100 Subject: [PATCH 194/483] Revert "OP-4643 - split command line arguments to separate items" This reverts commit deaad39437501f18fc3ba4be8b1fc5f0ee3be65d. --- openpype/lib/transcoding.py | 29 +--------------------- openpype/plugins/publish/extract_review.py | 27 +++++++++++++++++--- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index a87300c280..95042fb74c 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1092,7 +1092,7 @@ def convert_colorspace( raise ValueError("Both screen and display must be set.") if additional_command_args: - oiio_cmd.extend(split_cmd_args(additional_command_args)) + oiio_cmd.extend(additional_command_args) if target_colorspace: oiio_cmd.extend(["--colorconvert", @@ -1106,30 +1106,3 @@ def convert_colorspace( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) - - -def split_cmd_args(in_args): - """Makes sure all entered arguments are separated in individual items. - - Split each argument string with " -" to identify if string contains - one or more arguments. - Args: - in_args (list): of arguments ['-n', '-d uint10'] - Returns - (list): ['-n', '-d', 'unint10'] - """ - splitted_args = [] - for arg in in_args: - sub_args = arg.split(" -") - if len(sub_args) == 1: - if arg and arg not in splitted_args: - splitted_args.append(arg) - continue - - for idx, arg in enumerate(sub_args): - if idx != 0: - arg = "-" + arg - - if arg and arg not in splitted_args: - splitted_args.append(arg) - return splitted_args diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index e80141fc4a..0f6dacba18 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -22,7 +22,6 @@ from openpype.lib.transcoding import ( should_convert_for_ffmpeg, convert_input_paths_for_ffmpeg, get_transcode_temp_directory, - split_cmd_args ) @@ -671,7 +670,7 @@ class ExtractReview(pyblish.api.InstancePlugin): res_filters = self.rescaling_filters(temp_data, output_def, new_repre) ffmpeg_video_filters.extend(res_filters) - ffmpeg_input_args = split_cmd_args(ffmpeg_input_args) + ffmpeg_input_args = self.split_ffmpeg_args(ffmpeg_input_args) lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args) ffmpeg_video_filters.extend(lut_filters) @@ -724,6 +723,28 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_output_args ) + def split_ffmpeg_args(self, in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + """ + splitted_args = [] + for arg in in_args: + sub_args = arg.split(" -") + if len(sub_args) == 1: + if arg and arg not in splitted_args: + splitted_args.append(arg) + continue + + for idx, arg in enumerate(sub_args): + if idx != 0: + arg = "-" + arg + + if arg and arg not in splitted_args: + splitted_args.append(arg) + return splitted_args + def ffmpeg_full_args( self, input_args, video_filters, audio_filters, output_args ): @@ -743,7 +764,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Returns: list: Containing all arguments ready to run in subprocess. """ - output_args = split_cmd_args(output_args) + output_args = self.split_ffmpeg_args(output_args) video_args_dentifiers = ["-vf", "-filter:v"] audio_args_dentifiers = ["-af", "-filter:a"] From 7e15a91217d4d74d2cddfe6d219742d5146ba52d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 18:02:17 +0100 Subject: [PATCH 195/483] OP-4643 - different splitting for oiio It seems that logic in ExtractReview does different thing. --- openpype/lib/transcoding.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 95042fb74c..8a80e88d3a 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1092,7 +1092,7 @@ def convert_colorspace( raise ValueError("Both screen and display must be set.") if additional_command_args: - oiio_cmd.extend(additional_command_args) + oiio_cmd.extend(split_cmd_args(additional_command_args)) if target_colorspace: oiio_cmd.extend(["--colorconvert", @@ -1106,3 +1106,21 @@ def convert_colorspace( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) + + +def split_cmd_args(in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + Args: + in_args (list): of arguments ['-n', '-d uint10'] + Returns + (list): ['-n', '-d', 'unint10'] + """ + splitted_args = [] + for arg in in_args: + if not arg.strip(): + continue + splitted_args.extend(arg.split(" ")) + return splitted_args From 0871625951f2054392ba74cbe6ba5ea47b36d44b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 18:14:57 +0100 Subject: [PATCH 196/483] OP-4643 - allow colorspace to be empty and collected from DCC --- openpype/plugins/publish/extract_color_transcode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 456e40008d..82b92ec93e 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -118,7 +118,8 @@ class ExtractOIIOTranscode(publish.Extractor): output_name, output_extension) - target_colorspace = output_def["colorspace"] + target_colorspace = (output_def["colorspace"] or + colorspace_data.get("colorspace")) view = output_def["view"] or colorspace_data.get("view") display = (output_def["display"] or colorspace_data.get("display")) From d9f9ec45a86dec05c2b5f602951c805ef2186c61 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 18:29:37 +0100 Subject: [PATCH 197/483] add missing image --- website/docs/assets/tvp_publisher.png | Bin 0 -> 200677 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/assets/tvp_publisher.png diff --git a/website/docs/assets/tvp_publisher.png b/website/docs/assets/tvp_publisher.png new file mode 100644 index 0000000000000000000000000000000000000000..e5b1f936dfec1ec4f35fce41a419e53a72c6468e GIT binary patch literal 200677 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

;r>#l_M$0;nH4viktX?A?B}GQ;WQaE zX|AXJ^Z)Z-s$^K(V9+6hR$2q@5m?zzCbF4Rhu2)U!X0*pE6F&At3+j+yg}WUyGZzK zP>cZ~UbhkdmbPxNU*GBbl4rVpzV4>jtmYfvT7lpiytHrxHo~>EQ0S`pa+GOyYFsRVHTc){aa&SA`L&lr;j zQ~8Foqvbv?7E-uOTJ>H}&Ncr^lu)AA*eCV&P@?_){dm0g+fb07N%@28Y$3R;mxEVR zRls=?zh@5)eyl_ztl!*yRs z6c$dx6M~Qq@Rk1^51Ja=9RLKBG-S6*OO*ZaKuCl9#Cw!c?gP+YfR@1ggRs=^M5r&6 zFo{y$Q5pk~7#K!qaeJb}On)I6E<1AFiTK;p5`3#@H#)gQVo~LF`P!MJAU}Uob2A9* ziK4Bjhz<)O2|ofcs4J^qik*G@+y}Q!Z#N0(1NK)4S9oH04-e4L z&{5lad#D3rGV<~+0|qR3SWeIy1C20d+$jHRJkbIX%8n*JkdeBHO7Ro}q2m*y{SSfE z69=QZ1?C}V{T<*_XW@k0MMdVj;R9|Ie7yjqESJr&ByqE^HX<VVxpK78UV z7C#bnbO@-tdOG3Q!J-l_XRg97JIA(0Q};^ylZ)A79lSCLyB}JRhKs7Is$9YI%1==9(teBTy=o~d|F&Lf zG=j+#iGSt^NbGRw(?1vS@)9&oS_(9Y0DU_7@qOyxd11^u@CFv`{PuK=u=nn2<8ME5 zEw2}EZhrC(2M1@fIf1lMiUH?nY>1e5jTs8J=ZxTpwvrbA5AZx4Evk)|rst)V_e zRXN%P&U3i~F&pn@zlcG?D>XeHi@tn*^>YfwQJ*yf4tke`fDEMHHm}2JQ8C?0Yw5SW z`N!R4D@>-XY|(glEm1{C{^zle>L^UiG&Ic{r`YOWQMH^^q^B0*xQnjH%HbHGf{bQR zG%DRe`L=8|KyRPB>@s&Uqa4T6Y`xMgBx#sG?it}~&PQ`B3>MZqW9S4$j)Tzi=}#!} zd^voI&ph-lH?E#+PiL8)nY}L%6)A(u6ux|!%pj>)+`U|LUb0HTTm{ksffd(*tAYl! zI#TT95LK01!^)OQG(7w+|9jEPKqqJ_7U@?*9=`aWnm78`rt}7hG zdI?ZBuX{h=V$Lj1Noawb}spz)9M-UNl-8#R$ z03ErCaw}xjyvh`%6m-9xmX4eTFvOWv< zEiJWD5J3xoyoS19oa+NGnN%)TNy>}AfAKDvdb8t8+vfo{}F+ z<-H0dwDzSxZ(4g9WnlMrw{Cm6&C8==U;DII>YGlWlYz-CB8UdJ?DO_=D~WjW>u(TS z{E8Y^PQy~pnqOjsUpB5CyziRmU(3anMy+i|Ws$6pML9%-BL^1_Z{K#9eHJo3YHij= z>E$|p3@VTz;xO0-s&5DLcut2h+}NylK_^u1G#C97hHTv2#yheSPAU6?5VUB?)h zuXTdR`OhZ^!*Zn0kFmxL7Qe3j1g>uh-+XW?4gRRu=e4WBVk}p0ARa+HtPqOdG-c(C?7UFSmrHZ%P@mIw(f0B}W8m>FiFBC)Vap^dZqcYs z9gkGhm>Pve{i(4Ke!TvIbdt&okeGcL_Q!`a#c>7pmjl$X^g;8SkfU)9Q?lGv<>=3);_xA5xnf@L@rO{0b}rHGs(vnZ_oBY`yJo(Hsd8gzH9I(OWlePdIS){fd_Qq z`uyETkXyeKDq3-GG_CjmBeip&jmL1Y#w^Kf;Im@5BsWI>&VsxAe>0Q;=5%T3EyI=yessuAo_(50Xa>0j?*d46=$ z+QPPJK6fd~3u6DE${ZWMp7uCx*h_U73$Qp{U!WX!LU(_?^PkJ);1|R%`@};KG!Y@J zCy+YV)0P4K0>`JSq47A~ELlpAwq;b0+a_p36($=Rw86<@*ZHdFqj$PSdNLdOhxG=F zexg^6NF#V>+X0Om3^-9(kVl#jo!+M=DCwLR6y-Y5om#SIjw>aU$;9BHyUL#JcGC_v z=n3uayvN8d>g~4l?YwFp+mqpv`uNelX+=+(f=_e5ZYEGM>ST;xZH6BdMC7gZixej3 zoi-Y8$efW*0*|a?E-nuCDL1Xxed0G}+IPI0D56mmBZY|m`&(EERNV>XtM^lfWoLSE z>t9`6{6Ks+74*yxCvUA!yBm@uZ-4~h-#GJuR|ix7C|wdyW1SPV_QZT;BY;Xrb_(|8Ur6MF!X=-OJP z7vY#gXhNwkdIUbV_wzk`5D|yjMkjN+_vyp;(AIR8hxRQw9=nmE40DSjM1;#o-p7J+ z;Ws#sz()mT9`i@mtmGqhR=AB zvY2eoV1`P^o`3rRAZl*jHLkiIwbWM^G0!zSril;?#oLzPkuWo1QJ)q{1jAYJj_D2 z8!ewsc zVG@b&@c`{74ic@bW)nCGKuFpie^%Y!HZ%HZ3-}&=; z_d9VBZUWYLW?gIZm5u|)bjL|)&00&h!@~Sppt3ukESjXd^|pTd$bi%Ia=WDKen3eB ztw)85^JD=}Ck(8muGbCrPbRery{p}k^VXKF2>x-1o0R0_KqP#FS!};SFvMj$hC^fxwY6Bs}3HMO2j6O#3BI~ z4d_OFn_xZytZVwf6ge}3iSycLQM&JJzUnnYUa-OHXqOqs`)*qJjYy{3v6dYNGl%m} z5BlLr$qql(B-Nt>N`6K%(s6e%#BNk9Lclw(qfnT?m;MM|2g=TzE?B*AE_i@eG4ib^ z*iknIr#BkpvYmxe)`cyRa@`&b8NqkZKOn?}ryT1lIa@vkSi)#~)Qa4`MDoAPK^G=5 z)C5KbWVRf&Q?cps4fYF@A2_~E zU5-%le{}wN1VYWe=!KDJhVgG448hZaB(tXr#z0k3weEY2R?9BRD=W*RLl4MekzpzN zsXDg>4y^-D%P>sHM&a>vdt%B!OS@2;bL>*h)Ormd4@+$cBsvc=6dx-0Ou0_w`)McH z1QBPYS!hFoM&Sw+Emp1V9*91wea2e-s`f`K%?-DXtSdx>&`UP@S{sR`t{rLFv1hXd z3`&CT^bKveO~;0*Np=-R%@T=}3T_tb6@}>76j;IaBzPUus2%_5-&c zk4LubzvkuF3eH9?A7}f&Y=(<2s);~P@dlRpc>Nac+p)WE^%@xZd&D8K^ z5qs#oQS|%e;Nj;_Bg9{zv;j32P+Estpxd>VDg&%G|4K0v*j3flVp-#z-x2v!8c6_o zK5CR(t;zYM-fnArmD*EZ&zKKvS6nV%F@6Cd$Gc4zt0y;?f?;rAAf(5@$?2Fi$P1@@ zZmEm}){m?}&lm>!y@LP>gu1e_Q=$v^nWfPzJFqWZuSU2>K5=&>q5wvs&+fMiCTjpw z920tCXifnpeypxu$wp1$5)v@Y>8fQ~A?o8cjR$#q89v_A1qu#(qu(L1ac~S2GRDna zR}6r9mRGAeJw2^=j5O(Hqhe;GE0VUw&Q{$lr?81NC2GtAK0(=?D#EB6{+ET8+FFD_ zzop*gYo4v3bKE+Z$>gLT590B%sV_aRJejIjn?1P*Kj)gEL@C5lXLz4BwDIn#c1do_ zzz3yV$qDQCM)wiBYz*rXv)Ne+X8h!P_C@r0p^r*x0F-rw08u_fwnfYk+n#z=06<2HM9HZM49nfG;4gaDA@G^SJg1qnq)(8;Ib*aaII!1 z^4ASVhKI|#yg{ZT!2#*7a@udcYfF64jFJ|cioNM2J)=$MIBBjn@lUng?y&t?iz6`T zYn)*5?xd>IR|JzIP8Bnzh_|6$MyB5BlXeW5>Bi3wV#$j?vjQjM^qd)xV%E@#UM`QA zNnT8hjdGZ?{1Sk<+6N8YaG_RHl?^(o z6U2#t^0+^*W@c*Gl%HF*mGO^#v){^4gz#qQNmsCao`aqhE)Uwyz3ZOg^N$lKv3MIPI{HPo*L^eel&JccEvS~{LoOVonwSuc+(e268ZABC;a9IAEVxGH!iQOt*t|xg5ch=8WV$orwsIW3`6+% z__NhWR`TEyaM(KD4@NBSzXNlw-@ox>VYya=WfTrD~|U(_MVwGsx? zDxRGDQm<&-n$kW|T{SPEUC?C$-Fx8pqPEiLtNmfC!Iq&hr_t>CTL6eDvKJmB|6c2h zvqn*_g>fp%UzF-Qe^16avWMMsyuCe2Twxwc+Ko+FznNmQT-4ZFQpk1M?&R>b)a)vm zvOf7!ZDgp5&4P+D?$!=6_JyqSV8!#-e6$#YaLQsIq@&hLn9?$LL$8rMD^7Q#@#s{B z5QFV@NUGGkqv|o&Su~Ag5q+sZBEv~29a^5V1=mP zluj|i+*1SCP+Gr~2jhwKnK(72#$;U(&2xD9zekrmpUTi~6wno#eVz%X!P(tyPd&V@ z_IBFkWp}s=q=1fD(x^BgZEuw*5pU`R|3SA;$}0KZu38U-)zkIRGas)UI)v+0b2C-P z?RDR~8@a0W)Pt)`ud?5fsJKBH4ta2zOJJz8S-J0V)*8$2*zH!$G{6kf5HKIM;$BNU zzSH{z8)%s7{Kza3L7=UqW_zzc#ng0vXDt~jo_BJ=j5jqg>wh#85~=F->O_}eY#CKsnKYY+iH03d|rOnfR`0;bMd&1jlE_&L==l6 z@U^?3axeX52;ijc-ji1dubxN83NSvcy9gCxJ8IKf$oVHu`~Ch3!ZBSpPoYe_o9AMI zI6K`D=VP7u+KO)drm$Kb9F5bl>t0zBeeX>?im;hLrhbbat{ljj`-8nD+AHsP{ zw0UvA_Ig`KXwz(WF!^gYtbh^lC?7c9N+n8&i3Jooi2&Sg*horDfO$GQgq<%tfT6r2)5ZK#6*xU?vra=mD5@FaAby{JD!)tP(*;Z=s9_ z(i4avs?Nku-=mgIb_Du!68HTqkNYogk4NPh?uUi6ih0XUbK2a?wt!*KyV32+n8APU z4JZ$&gMfaA@GC#fdGTsYxXty#fMErBvFwAQx_VN4JTMkb5Ph@7YFiUF8BNu0J&U?I zn(J}}`gc=PL_xp~y*VLC!a2m37pr0w1lxY&=f?|*C@yP6_z5^GI{2}U$59vGK?4$8 zW`=kuj(+iq-cUTF?FDdjVXdou-MRVsa!AD{4OAS8i#7_uv+)fxbCKESL1t>v2*>MX zq&Yu5ont4H|Ew{8CiL4MMIzgwSqm zjEuL$V1G2Z@T=VkP?4UdnrtMz570j4^J{Y46Vj z;nlz6t36rAhZ-zt7~d`+_w&Du1*-5qM*yGrE}j$p5y-a`yq-a*--=-g&QE~s z>-Cf$DzjpJ7o87+yd9*}wOXtxR2hL8Dx|J7RGy>Obe|Ts1av@|H84{@hLlVin^OWW&ahM39n2DCT- zW%gdD;x5vP_4i#QEmNi;$y-I|JIl5cnk!fH;r30hc@jtvszIG$vFSG}_@FF@IUNV+ zT+~scReEg`&WVT|jBvtmK4PNCnNDl9VY|(tu5tcGHrEliv>I@GWP+eWK5PQgl^;I8wS+g&Gcl`Ibu%3O1egpE=Z$&8$nPruU~_c|B$w49 z7D|xMO|h;hX5wo1mZb(9WI<7pKe6|1b;d@X65KV+P30T=m;fg3W~Hyn&d!c?9ieJ( z2ww4CaJHI?N+9+k^y6})(-AN|vc6fEHBcey|Mvb}b8SBwB)cJ$1z@9RKi_a8Cby%I z?tCc%s~!fcGVF^GhsG1~y!V}Nr$>VeBj6m=xG#kD!}V+mJ#&;RR%zjG?(n^+h%;uG zLE5x!Vw;@$=wi8-Lb6WwXD}ebVYOG@Z)GM-7lc<5oSg`CRHoIGZUV&Rg@p(E6SaQW zan+pC&9{pds`=GwDFwr$N^I$bgxaP5c&Z;}7>;QM*`LN^Q_nR)`*Mjo!xJMYNP)2>96zD^fPs#BgdZ^bTr(}?*gxW2 z=g9*%LLMn#zk!QLPfs6&JRo>dGiA>~u(kuO0Q&GFUgredBD+!3Rp_;S%1#PG9d0>xedPD z3IV3$HefLrh2?y5GEmEH{9M#EG!|!6b?xp}d|I9_L)~=J9QIa@X06*PS4TenZ6bOf zsnei#KP#bBTEM6%$$d7CNGS=p4Kj0hZ;WgdS6*Ju{IV;lto#y^X3-4=jhiZN-w~CZ zoP5b?nWR%bSWB@MTL5~R3ackZHYiK2qMkQ?x+Qm}^v|0+n5tYTZ}5`MXu-ss#ii^wpr2Qgciciol;D(mK}AK=R_!?!I1NfirNE`l)C z>53Uf;Ht%H>~};6;uB9*7dB_N2Nz`fgUseGqTQ}#h>V=i^5Ay!=@;gC*43cPDWU{}5pj}EukH1|ttrwf{P*{GrH0{t|Lcwb zJ8yvkZ_MZ>=j1e%_8j{(jMU4=Z`gPWTzKkJ)+NZfR)!9)znzGrI;EH|5_mclg^3=h}Ga*;jPEA`={M$&);<7h$yOq_S1_TjEN2s*P}gyi)U8-Jk{o5mv{rZdMuS9RJd3z{p4tfI-gbSaR@1r2e`h zieR$Z;`g$~NE{j)nSs(Sh%N)ql>Xpbm%e&Dq~9s@kRC-u-_CfEUqsC!DslhzSuvmx9TNFUuu1b+NAq5d_4n#KJf!4g@Ugo4 zb5`NL^TPSL*UK?8-Rnk9!OS?2|1lJ+g(_u9SpxLG8kTW6Z4&t8cC*G+c) zaL-TRdm?h;Ee7+r@QY(TTnZEv(4dfL(p}*gQ=6@wBhG*mAT5&vP7^IfK^z%CgtB+_ z00_d0ii%T{lceG?F)__L%s&l|k(2_dCQPwZ8#{!Sj1mkjip3Blv!ET}45B*fE;C(p zha=oE(^VN+Wk-&G>#M;o?rbcg#QZnGU`TFl4j6NF#dp5i#|ez>4RJ;xfI^ciC1-17 z`G2&A9AV`8eSl)X4_BTjVEeGOZ0#*9>cY2kU65_on0d|+PH2T0XkEQ;YnOO8wLv>NeM&w z+)DFqgaV=I?Bda|+>!wg)|n$8U@Ng_vj_CA4L zvXs-5iQNvPzxeJ+ku5p+OfS#?<78^OKN_;}dv#Ly}EmG9gEiW>+wm!|31`J3f zhj-Ph?bb6@hi8^1XIbPcfg6QpGMePoJVqwY&bJlI`n0sPAs?~}3kQVOBd(n%oQUMY zJ01RoGk>s0Hacq33Qju-*M&zO2$=bK11mmh_2$4zL}gJ^8H((Ts176JM#W8Z?Y+g2 zRuR6YQFO9Qn zM43kY{(i!=rl4RyGXOZHaIe8w-u0U3qdsU_%i?Sv7T+*>P(XJ*QB?39IwEaT4qwR9 z)!m&Bi66jEXQf8}gH=;!x4%pP4XP4#l3RgyVG<`{C~l_&vZcqIC$KB~gpRjfF|N0g zBVRwXtK-F-X3y~lQ>~Rt=sscCU}fK~W3JfXV_o0wJcCt^y=5hDCVP`t9LNJnPKLjl zBVUfEBaxIo`7*FVduAG0oWWai8cA3&McJNja!65KX>_&(ty-+ygXqWL0Xhv`(7rH0 z-Tn&!NTiQcbbUuRd?1wF6jMlor92S|3_#DuqzVI$%BJ=ias<6)p()el(fC zv4#*#(vU&&2+dZ(F@$So82tYIJM;tegeeEHiAT8RyyUz=4cB#iD=JW!(fmjMwz~#( z=#pW0`b8~a?Xw;eS7{bkF*&pm?sfhCA3re%#cd+>#nUzB+P#l&l(i^o@@!iV)4#}i zugs-NYhKh&SiD=I!mz`Mli8W=nwiw=E*7f2r>rwK!dLR!sB#IJ2*$!y{{wkkiPBmU zLbF+dC@dm>*oeWtULP^2mrHq=>@X%55&EVqE4-{LTMWfaN`38fT)M65)5cGp=o5SB z7copck*7xH-D`28*I@RR4(+{2M82m;?UEL89#FgyFWSCN1(L+3s{pZ77Hn57;<$EC zDAO=U|!f?IH(JrA_1@hNC;01bPk!1ClvLMCC_dVyc;nUaf z=j5mRdy*0CwpW9fEdRa6zUI-@gTQ;d7F329nAhrGC3LTjR=HL2!1-Clv>Qz7yzR@I zk~oM`zU7=Eq9!Y^)ZooY4y&9*@B`C(o^+O(f_cHAHWKJjl;VaM5ZsL_9;4rLH{erZp zG&T8+hZDt61_En8LozVZb&=eM(mpLsM&LEZsbWzQga^LQ%FD~sV+p1|^Hoq*b_D6k zN{Q;WxZeQBf7D@65QS0p>TH#?(QBWI3c)0(4fIXfZnkNHMz49RAYusE`+IvuGoa7)m!h&p2L3)FI-JgV_&O#(O*k!4%&YSI2c_k<(=aMOy~n8Sm*$AS)kzk^ z098Pk=&XLhiRw}-=WZz2^o*rseJ#)L`O)`$@w_{R)GVcpj=8!x9wSM<$SY*hYq=p?+*5HqWHie7)(8q+e2EOz8!f?79j#U)MP(ekClff{UV$pLkPeIi(+KqeU9Z4DybJ}PC#ZuZ&JpCwF zJ!_>Y#s0YE3*O&kX{Ld+&}Nr=ne}WC$gyKL;-c<$qj~@);O=x0*hee^oFET&8*efKuRb~~xyT(#7Qd$!+exXv-8pS6X` zrNPQwYmAuYGvZeM4*8*r%J=&cqbiQ*)Zlr__Y>>aKhgVsgQEvzoQMt#R5ZBLF1*q+ z-pBQ0`9d$}Ps$rUcQu4U@7;fhvb^4$XurMG!?Fl@eldZ6YDE~2wtRVwesUDP8`38) zS0+ijwCeiZ#w$)k{5WLxu;Foc$!`-bePVyw`N<#vK<^wRvUT3+XOt@lT+QI&f}OjY zOXg+PD)2emB#eTmI+ACksEE$QN{PA1vQI#)KW(3k;(Z`R#sB&2H&I{JQt8 zCv~6FsCn99e|ubAr3t@o_uLJs~Cz;a#!Smi%44>`(U*Q5p$cK$yI{EXDPh3YZtS(<- z$Qmu#0DCu3n*5v+Q;4WD5K(zuZa(qaMjQU7saaiDx7A=GuB)p{)%2WVgD);A3F}6S zpTHHQrlyuG(*RG_g`A54{@mEeMi_TGt&G+<_(3?ynGkL|9hYy(xzhlvi5e)m`hG&^ z-?cd7Ge*sU*ttVz&yTbT-@YUh^tlB>FMw4M@PIP+J?=5o_h2?<82Oy6S{4Yp{E(?= zHXej(fN0ZJVnbF9+=304{%_E)Z!U1{5%8e)pQ8gKvv(wmT!5k z%LGu04+N5T?cE<{2I0v7|2nKY`WY(WD`1{Y6a)k9v(HuIp^WJ?e#4g!GQ8PZC;Qm~2ON>N|7GiDOZI5*9|B*SN94RSGgrUwOYM!Ik$V$Bb8~MJ1_f&B@AwjSJ)4sEp*@Eem52aEf#(Hz4m2^O>@Bg%%8EN*@8ge zk#^_xjMK1AD5wjN+v}TKydR!i5fEY7kJsFAe;MiLd7(|z7T(fH83#xbD%~beYmTo! z_le}S0-sk;`IuaLbdAjp&?d*?2%k?GST4CeQ2Hz+=&<8c17A;VWf9HQ8NGE|kC(;C zEkcdpW-n5aSqwkoRc>f8(f@kIdYJt>3y?~VwgaBeEerhVI61lb`E}9v)Wog7qJ3PQ zy||L~Hn3VaMCIpMydIo5juak|I&}JKTFRpJ^i=(Z@{1HbUt9D_C_D0upIRX^F(V_g z9M0Ou$7daWqNXDE`Lb_)-zk$U9}YjQ#CZs9QVUGjyd7xdZNITH|0o7K$oGHmU(RPF zZTQAdPA-O)Fm`h#M0#x+P}qQg^)TA(Cye*TrbTr8>Mbkk>G+jZr@#yV9DnMXnrS%q zO*9%B8Ux10_z<_VqtJLV=sUE7{frRZ@=Q2IRybiqauK6 z0kQ_byrlc&*n79+fv0D)KEa4@HDLRTyZ>pr^C;gJFgy1Dnqcr{hbYG~*4PFX$b*cJ z@vDkCTzTGySr${W4<$-F@xbjOD@%w1kn2rD5I)%ZH7=?2`Ao*sUJj@~Kg|MYm4hGZ z!f$8tMp&X(jWs2}@&}lA`>%Sob3G=amkr;JtQh0b^nRUnsy&XI2^kn20U7Gqr|GGi z8Ns9CSQhW6H0?3)X2O%lzx7W1p4&gU!qdG0UP@`Ro5#tv`V!lP-GjI}lnB>O9rw%X zHi4dBmZ+r7lS||>I1IV=9r$7U$=v(XbCKD!$r1r{B55M%bJ+w2akz> zs>rAs!MT-!{cS1dZEx5@tfEi9N8?k8TX-p8FzDo-ZhFD zNm;6BF{vdLmJR8SesT-Z`rRL<vJ9|dCrD5Lf;JFC0}wT#(@hq+N|s7W zpwnvY`nu|B=mQ{L0K-qX4$IdAjI_V2JLyM;?0@QiQLj?7-7bFugq3(}RFbq}(zrM{ z6f#*&evCsL3m}J?jOJCEuzg_q1`w+Jo|b@NdA^0oMWU*wSM}3Q+!=*B1l!D&RsZ8h z$8BL>RyOs3hme>Ctry_d_u?-TEp;=wA0a5xxZe? zt?z&R{gtMm=e%wGkVxS9tl3_>C0Kh0umcF&E-}{Je8EzNfeHrjQJ+goi{8`GRaQm| zzC+*yk}@d0(^>2^xUsGF;UxEntc$>=GwU3kWfsp1w?$}GUscrw>_YFWRrS<`%>stPY@2efol&P=4<;=7D!7cW8-OY3L+waTvzL4O-$-`pT+1LB# z1Rl{!+lp-eqfNlXG10#5jrAi7&*;AFlcY_RN6kn6qb=W~*L^aEjJM3Rr2DOP$Dl`O zI#k7XZfnK1$=>*)i=8LZq~Y#cUeCSX!DsdT`WrSy=Kl&xa0LIc8n`E1^U)hHFGmTu zPSWJno*H}MUS(V!Zubu||FrSK?H>$%`|OD#jsbL@HqCOtHe`6u;`6fSSW~3Cf(O!9vzox=!V9T z;p+LN|92m{o38mQOI`}g3|8ptz-Liw6s9P(rh}LGqJ3^d^qtefNflljvlw8od5DzT0iK4 zyr_({kL4L5$w~>h_ogJ|`VXUWpH&!N#=a_~x{4C+tn@lyuRsaP+F*Z6yogN2(;bL6 z?z!LOTd02IkWp`yk}ELba^VKlL-KgDwm9{PG-pVzO zB1!sp20)4`!#V7vYfkkW&&f4f0@h~$wI$$IK}Pve2XXX(<7chfqcEmwIguA{N>CV2F#1aR+%G&LxvoJSod^i}xmmrX z?!&s1>1?Bmu4_^UJJ6wa@4S>o_y}o&GV}haM8UA(|Fr69C2b~BE+&XO&tGD=1+i7BQ5Jn)C1oV(icdj}(a`KXtJG*Z>MB2SDergv zNW5lIu}kgni;oAEVMV=dg(C_f!;DP!sI{FYX`43oEb=IlU{9I6og~q;rZyhkxbl&A z^wEP?G^$7TMml(W^!rY6OOxX$C%AqBsgu+3`*d5SDH{*#ND9iPiPwfPN=Qu?ag3RU zxTrI$Z)1-tE{Ri7A!P}nl}g+lG9`Dw*Q1n>zEl;p)_PI3)j+Qwf;7aS$)=6X@)%ak zv3(Or*jiGop9pU2*A%g~_6umiLGyM6bGk7|q`<{|uiF8x%pP0=6dNxA!2(rZJm#O9G^iU6CzW-@g$LYNQFajhgVc{)u5`8Ejdf~htvHJmKDVdnDL=MP0BHHQjspU z$ynBLvK^smOk(hA3lt1W<$H`uD2i1{OEhWh{TUdc(1wHKs@#Vzi5T97SJFMfJ|7k3 z1apX*^n8&d4{i~#)0%yw#E2ypNXW~gSImH7_rEd~x*&QQC4!iZ4ZSM`eqC4_QuQZV z)?ne+N4tuIaK^b2+yt|eh|zGdqHtvH#ib?Fn=NwOSlCce9xCu~%phdpcs?viA{C;V z;c~m(YRI%NF}||3Zl&pkU3dVVdZYeS>d_d?s04-GiMrOOAU@OCp&iAze?A*1MnnZq zAPpKktw4*%wDDhdv;V)jyU06BHPxeM#fjNw@MNXC|N;f4iT6Qys1hJ5aXgi zTwL^&LIIUGOwkA?zHbzTHTfHn-nI8dS+SLcyM;hESF5H#e9>@)&S|u_AlpO3=iS}& zyG84FigU1B7Ptw+H+HpT9_G5{MoQzLk)qgK{$nKXZfwv3FLv4jB_JHk2NqWRuLjzH zkSaI`*wy@m7$CwVBk;QngImhV4qFzQa+PR)=uV*`d$3gxyxEx(Q)0r)1JK<5KQ7{o zOWo?RPCPZVNMLBUMsH4HTOC}LFn3m4j)T8-COOB#s?+=P^%B*#de&}%Dav8&^h_jq zRVvx+RKCha=Od}1C32>@kn#xJe>XPZ5yR1TlciB2v8m2U#c%9-aPQP%k!m#E{3Fy@?jQ05@|1% zq>k-?Xh|Ae>=N|~EqU;dQuGA^iu$QLVf)gt+ZN~4LlV6MuTPz+M=X<|(kGA8K%#HNqZPYF#&f{RtpCMRGqh zwmFYD7Tt4Z!j8G>sj+5lHu2SHL?F*IyXIjO8^eE({1vkM3pH;xMs3NU$nI?+sJ_C5 zanxHdWIc^OO7P8=%R!*W)M~Z3rgcAr@>Jcd5|z%WnMcW@8^_h|{GHhdu|oJpOm6hX zBbsS}FNu)m_osB*gTkQjWmsKjA0ofIuf8p@?EhU9*m;b}FuE}x`?u#_buh2f*&?oX zmkUhZ0&Wixu0K{@$h~!1{f{DpOBlE<_Fu_;QiLq{n^{Dq2Z5|Zs=4=6$Mn>qNk^j&Pkiu9vFr+mh&pj1Xi`6L zl;Fweh>L_8o<6lr0gDke;zmQW_kv6sxV2Jow9hNn7a>PWP!Pur^Q7?*)mBA2xf$Yd zMnH5GBN;1Gtq@ewCwOS79~5b*zaJMTk>M50&&c5R0PhCuai-rx)nyLvUr!m{(&Qhp zpMFEh`O>`KsA2i@In&x)?_6nn;eaV{-UO5h#i>0fR! z>u116&%VtGV56XcYdaq?A*CDoe@<~{6vMJSU3jnTDhOQX1~u6$cmof{%Z)*$KI!b{^nB_T0VzQrr;? z(np#Wj!E=Tene;yk&Ff%=Z)gO9F~EP*7w%?d;bbXUayuA!8{Hc#i?`PF2`#4e{DlTE zqGXU_nVOW{RhGd-tsY-&DrD<&`^+7czDK``1bQ5x@a&j=yZgs?04GSINZ~P-mXh82 zfre)6qy~zlPk~7JvZ4MvenM!M{1%^&+iN8a!8K3U^|N(O1u@fM*vW5oN26PyM8_$4 zQ>5uwLTf;gq=G7q*=5F|sFH~sIkILP21+-LQ6lbkr73D~4Z}5_*F0SMN|ubz=T@Krm@91xy4Te=mz&@ynV&VyZ*_Abf|FA>r)&Ha|4Q5A4YT>BRYM&@@v0+qYrNy9eo61 zRMT9&C|z)1arTwDY`j5H*waFc25@FL#nNb-1BhM(D3nlPvc=DJ+imv=){U;d%i@oS zTT;>ce8nv++%IeY!7atZ5j`LKe;gI?h{K`ILnkZhlw8s>wO>gqqIt7XV`c(kao62W+G45a0K&VI6VZ#(-VcZ0h%+eQ(V%%ChYO5>7G z!Cb6%ir?L3*lfn5=Gcnu+;EGAs|l+sY^DByS`J{eQWi}bd9y+GSs=p`zErs)Cn{d^ zOmT++%6rUG-n5^|T#7Y0_(};h++{>bJWg)x^wQ?G=-0XQ*Hu4eZaE!5;mP7oF8I-A z9Zp_=gc$aio90W@Van6|fQ@`i_DzP$^y5VPL(rgI?WzrF z#*_KfcOAX#&PA0SC0<^dC3q2QSi(lYbM}Aw5*(7D#{mM8jEjYeTZT2?`hwoRXH26C zsJ_a@-Tmq|%Rp^O8!;X}`i59W%oyo2c?@ku^5cZQwEyd!Rr8UN)Xsa=L~mR?DuQ|L zWDH$Of<`V(mIh6i!c=KD{iG~yRdQa|UpzIqG`8Zc}7bWPHSd`Zj;V#8H` zSgX?vT0ghg!e7tkv4W>O)(PVSp}8Tmn$oT4P9ClhBbTIGt74Y|!SUnn<<&m_?y71p zQB?@}u^RnPG!7NiHGR(UxI8ZG|L4LLMVII=ueLoH&u{vnF^E%PsV_jlP}@M`J0$&_l@(kwx-_3un^y(iA#pV)Hni-*7^igUs;Qhy8Q(K- z3qJQ9Io-+E5>r>Xqlvrfu$_Xu1-)~u_`3PSUI8&a=5xk@l@xybgmX+%@xW8lprupN zbhHyW47V7&sq0l;W6p>J$@g7s-rcAUvwU=qEu8uRC3mrkCBdVxvwClZB{QrB7BXpfk zT$e3IZ+*c1I9#l%SD#j5qm)Rg^g|$Pn}$becUsfMQ~=HNv~$-5#d`-gfy(d^W615!|9!D>En>|Zn8%!xY{AKMaq#(HN#vRrpP~!3DMl0P)XcY z3qc{a2O^wS$o}-vA>T;qMyZ#LqImVZjxFoKUUc+U!jPGbf zruaur6x>Mb;IPbzLqS7faPoraJIJtLN+Om;Hg57EH}WI=F(w zN{k=-a&c~jl*+NRzlySYNF`IDR5yxQL$Hd)=uXBjqBlUS;4+ZwAdL+%ri-=J)J)-v z)&ETc&0(HR4Bfr#(A#OoU9)uhi3;u}M6LKy{0&5lU3vedIP_>meev^4h9w)%~&v`D?Jo zhNaxjds(+aC@7@aXuBJ_!NK%?wh&thBW&=UGb>6Y9TXNr06fe?-8XLx&ju*uCGLmz zJ@cg^jGX2Yj`|k6DBfhF5;m3XM2!dCwLtBQc49N9vumEw2(2Ds1s6%RUmd2(hF#wM zcAn@G2X4G)?h<}#Ly)E(ivOF@5`m@iyZFo*$oq<{P0%9Uk{pD29M+g?wMeyqf40%j zn|o<#6ALOhh)UK;1)r--YX>nl*Ltf4=^QsR96PoKN$VK(6BB=sWa8S-694+zOm#!` zT5G~BG#S)jSF>_U*?<5c1HY0O#BMS;i~RpBi&2n6b%wkl-D@eBU&uk=Gvh&$uFg}W zH6o5uCj-WJ4d%{gNjx2LAich)%Zl@#5!re7)X7xu-U4$Sr1mXmeZ2W|9bLERVjTvdF?5|M%YCSj7G2u!$W-So=Xv{ZTEkK5htD)(9DZnlkU20a)8##F( zuNYnu)iqVQtg_sRTi5F)~VTQ$E7&hO`>Fg#`1gf`I+2$ehh*!ay5gp*GAxr-i zf{q6>4b)^^ipwl?cybA&%SKJD8}UC^Hd@J1CFiE=CH>GQBq}QKqRl0UwX`(X5PM_1 zD)mHoCL}W8eAXr0)ue;y_%(p(n=0|k)jG_jQ{Q-6CS92!FPj zANl5UxINd!O&r;e(KY_vk)ufexgUwB?WO}ye_q|>FbBg)=yQk1*^2XK37}dO6T?8q znz2zUK%CmBzdmd``rZT^5?jsgdiPaWg)vSNx^7!lO*97)D1M5jKJEh1E#PQXRZN*K znlQ{*DJ;81>tddv4AjvgeO5_1GBZv(P!S+SfIAa?AAj@2#X}r_nmv`B zO?XVeC{3(2B62zUy<0{Q>H>k`5yAHt_v0M*w)XS_?Yc|))o%zxs>XC8@VQ~*@H zXiOY!G{f83dn&Fr*KkEscbe;@jhN1`sBs;wtmfQCIR5qair%7kdmMbe?2lpcW&9uw z#|J{s1zo!!SV$jjtwHcTGZC?#c?h=LF9SHu?N|THE|;C4tgNh)lT|y+G-C%s!~Lu} z!g!+aCxp3?l&EI&1oym7M~$>aHud2mvn37GND%V+{QRC%>JUtQRZ}KsE3fm}z|)Xy zd6_)`jjpJ{M$Uq~{tK81%d?j}0Gap(4|gA1{f%gs1lN5yyA1 zPcXL0Knmb#n&xC@#2X;_NMjksLheok--M9*87TvR%POiJqqAQ~^SPX!UbgBM6n<^EH6h~wfp_m# zLUYe`N*)(+oF0_oU)v`CTeW zJgknJB>!dlMIHEZ(P~n@t!Q73U69scohFz6ou_V6jH*RJuasy2hJy-_ZNg-U$|a9( zQpY5*yQH6w=2vU1t(1t?L^l_80jgxgmk^Eg#w4nyG4FFPWWSi>{+t(;0R@#-U8^L_ zG(&Q5ku*5?x0UgVOv&{Q-0xOvSq4~ck0E(BH2oBRZ1j_i^{>&$Th7ja-PI(Ji02ob zQiA7UE-xwSVeR z=4F5wodVf_gX=V4kM+&85URli)kxY;v3n<#8;^Yhh(aJIVQpA+aP zo2U14AisD#lGsy3>@5VdJuMdd*+I(1oL#EuXW`ErM$F}qV)z-Dphr!|9t-3&fP?`Jw^|19SL2$+PB0>y5JMRk@)OdZT8W*`a18mp1C!x~bKW zYn~-~-iU_hy^3kYBdM`R9WjL!?XN&EAc~D8YV}Ac2mmvLjDA7jFQFwZ7Wc|~=ny~? zW_{lG#1-&*+YIo0o}|J0&kQ>weLTqhlwoSb0u5%nrk>bKtDXy~-jz^~csWRVk_IVH zs@mFo&;4#9-SarEIIKKc%EMnir0TmV)}`B%zs&zzyA5hmDpD6grb-m zxU|^5X1nI=dr^w#*#BJT$D3Z19e^u&9c{Yi-fT(cKS4Ju_&gPR^YQVapNy2BOB<=I zJbJjm=NAjsJ$GO-8hHKfkme}Pt*fcH@mm;6=Oekm?~o(1Y*xNV!`MSwq<5^JX2fUX zi8-OD{gw^Kc)#{beP+YTE3Rv@&|TP4Am5TTYt|*{bhpYeDn!rb1j{ zJ`X2TvqMCgS@#vSwbSFq&-%JS^WU?@MXRcOxT(rJI`GBqY6b{^mQ8wmWw>)l>cq6i z_ed2){yE0D3E!$Jm{cIEM)sXO&JG5y(7^*6ovwcof&er-1mm`?cFN4jpgQ}0D+mzyH(e%~9zR9Hq5xD2Q1_2y z`9dg^vuxWsYtK@mSP$hdjLq{GhE!U-^Il}irV`B6caa_PC!I3nsp8AfiGRObb9f)nt?cLbR z$gb+?Ykyzi2szS`0kKRlVn>B?N7%lM?rL(TOvCkxSR0Eg9&&-dL^mF|OyRhaPqv9C z{01zoRim0_z2w-o+zi<7cA*Bh;K;niBNGITzda(t2PtH;I(VrHtvNGMfhVF}m_6l` zOgv&z3_JTizk$P8*-W<;S{IH5{&VoWhh0HC>Dh7X>=o)?IRC_sX62SGr8*696f-nmJ-W=Z5%c{&}`@Z=FPSw%pN=Kw0??7p;B^V@94O-|HBE8e^0; zdMYU^USaIW(eEOrE8Q_BpxKum!xRO9j#+4-F%4Q}s!mhjp4 zp{&c^w$#~Vb!kgef2x6H|GTYzO4WOvsUFW|NAQn?DI6%%G~JB|x8D11&(}pO7z}~m zuMzayUu47U-1@#TawT&1_7#6%cN`C+gWu~j#Bcp1y!4ZyZLthAc^IC`lf#MWi<-B* z>pFbk^5iE}K0KXId#V(sWLdxiXr;>dHSPeCLUv&SgY1RBH= z3mzpgSTma`TB41ys@0H2UPgu}@;T@-{C=}q60`P!V3=U0Jm_LtA2{_y8&`czZ%U>KAiGS3ZFA0#0$pOHrbxQB0E+jvfp5tSCP#;^y^=cY9Ba z7}Il{ns=|QMB{ckvAU?P4?uSCSA*Y?P8*9LG(T*evKrNUK3?tiU^qkXr-Jj^SYf2> zj5x;VTg4n9s<9c2>B{C1%PMq?>F8B{xO8qfv@d)mwG77&R@2nnbt=?((btiZ0T?1z z*gnwu8lVy&OxiuG48`h#ywIZDWIl=OHE((aI(C|j0FfZIqcj0niSe&rgYh6VLOIyk zZvZBbUVlKZe`p&};-K8K)CIntkrAneysT`Jxi`SsvI5``j<(hhw6#*-+S0sbiZgft zT8+U)A>g#K4uPPfklSpLPZWtZxeo zfsecJV6`j&Glp-{$yLhZ1WDqU zb;QI>KoMk~f%l!b8!Cv`fAat$UGWO zc03=LiFqMDS6Yji?LS78MUzKMh^Qv-igYNT>C=v_Xe0AM4qm6GDy;SS$9NM8%28F8 z%^Nz5?BIXn;~J|qzupaM$(1@0H3{Uu3l1P%JlE*jE~*GGSvgY?#H!XTxl&#Ui{=5NqY78 zH*NJz-tqOQ==ri|Z~M*9dbo<|!fBI8S(xuI75V7T(E>|;pP}xJY}Q^K1{9rXIPvP^ zB@4Y{*25xv{l-g<`h%~_7EzpH9e5Xu?9UMcOE1i(06z!s)k$@IM~;~CtNYA2)=bC4 z0h*ED3E-hF;P#df@@x0rtqqraCJTO6Yqd8AO%52bqt|%QC`sN^8DKA z{^ogurK!l2u5K0eRk&_7{>bI)HN4;bt)Zrvc@i?!U~YkQ2{c=dFi{wU<~WBgBwcBZ$7 znFj@a=i8IXxojdeCZm|NY8I>-t)^za zr1EjZ`$1wI9h)T%(t60)#toEi*A;y+6KBEfi4ZM~a1vO#thz>g;^UMPsTk?tfI@g$ zb8A<+?+cs+Oo?5@>xRtBsdU>-;`g*$RbmNau3v^aEdnfALHv~9c|HA>!n!(U+?7sD z{56cl!3%e%FA16FQ9$1_9+m%3e_9e|4E@*ebKzMP<|9k(|J`Js1kKsWBoA`u(J~dRv66`9Sw~iV69ZZq~Ib8q|GUj7mCWN z*k>!I#^~wk+q9$$HD8uAi&SW5&Tswba#Qp2@^VH_Z9~dvCk_ji#H)B$g>*@iYQL+i z86lS@GwP%(9>krKnw#m46Ut`ZwAi$k^bFt4Mkx(w`+6uju;NP~13R~0)yg)qhl^To z;Pd+Shj04*AkMt=y@yEQ7lLo`0=8EyqSed0#*zywzJML}FFKt~ya#}IulB|C9^}BR zoyPv10RjW=M*83TS3u?cQwxij$ zc}Jioo$K$IWWF?EsQ~MV!1xNW4tGl}WBZ+021|)c_}8)Bi?;8yfCK!(uH&Q@Y7dzj zj0IMr=`>s%tfw^9y*+!JSrJ?IP~YxY+>tT%$dMbLW2Wmgknd@~jjbOD@i#@wEQXyG zi*G2=;9u4Ca_16btkraR=gz-sP_JaxukYK!^BHPZ>H>3RQDyH8={O$!^YJudv>hm! z!0ZS=`FMG+FA`7UkR|?Nk9&Tm*u_nDQDOvX*!n%2&>>2c{l4>*4Eiu;ox79jz21o^ zj8QggKr!t&z7J(INQMm)UMC@s`qI6cy1>lEx;}aWjpnbRptD^2l{!{3A++ZBAY2 ztD8cLOb+Sp(4CddzUW5IbJJdpDmJ)T+Qm;jw{}2lkBT;Kt1G2sqNuUv!@krQM%xT# zW~($JWt=%h2&kDyQj4%hqHffytFI5f{GfiZ1GWIQPm>qI;#@a*5t03 z1`&X*sw)s*c9jF`s_gL#G1QzlM_~Gc!SJH7h0U#F6d@=fy+wvgZs?CSaM7{E2&5x}gAa7~g8yj_^;Y|LNDPc zjL(>E)$}s!*&QsJ>sl3*_t~T9dqzxfn2UboJoL>~!7m0^j4JJ9lB$B_7?DUJ&fHdG zzj+SBPGPT4OwbLO5Pt4B4PkvQk%|Mli&;cWyAU`r;xmEpnF@n2aAcfY2_e8WS&gD;Pd#b& z%k)!XNVqUysqQ=-MRHCbQ=EaQS{29!Y};a39EG2!r!`5p)3tAyN)4?TU`_4RW z(wVIf_E*+wY$?pY2-AjbnUoJ0w`>I-#)M5{d zjg7tIJE#_ARNU5fUsYXKRlE5lS+qJdR0GExEV2Wzt2+SPy)yrK3y@{P#byOst!(-H zti3_K=zj|sU$f2k-11ggFoIFYkg*X^oL8ys0~b|Eu#v1w#d_QvQV|eLsBP z#b4A6U$ddE)x1i%{aUVB+Hvu490SuoF-gdrjq*k;{(ApJO(8*R=sVLM?#Wu1`;0cD6*OiRxv@XQfFq9xDN>2_W4kl-i{c49qoR+g#KD(f4pPBkQX9B`@ zJ%2E$&dGBZr|tJIRWd?<4-xlk%b!d2`^X+b#^%y^D-6^EzkR1XzCid7R8>^a62_%x zWaM~@cwpn&|fd!-h>hL{3gkj(cM;|x!@kEl-53#Zno`8uBe zqN3*HGlpUi zbK`-#;pg>S3=V^fE4!9H8k2RwBR5vHZXF?qXJO<^qu(focC3+ zfxos2V++{#qA(~GT2bSB2oJMPjQLdSxz73nld?@}l2N5?L8>NM7F@kNDAf1@`Z67s zJV1>H>T4zIS4(w051B(gQIhy!Vr8jZ4CGZyR&J#9ri1L&@hP<>yfI=1i%OHhanejb z0uPif6Khxh{iq&{&0*5CJi3VZswF_)vX=ykj>Vn7}GFD=M zkIh_?K&OgfvWTPTMvCEnX-FHp_|(tdo@ogMjtE#9IMO1!4OCS@xDCxXnr@JcXRV6k>S&_EBblySC3fPR?Y~Re%@tB!a<~;#g@!mi8kS467UZyke-}&4iM|>_d z_xrtyXYsL{Vy3vyfIU73^rkTdAdUatfc#%yCcdW7kom8)0Qh6Tn_j}8UFX*M<9dJ0 zakHtmsmbN{HGAP<&ls6ugYjTGQx4Fqt&HwBP~RI>%ya{Yj&BV=)&Yd^EPdBq4?wl|;qw^k>mb^n5iz`e6d!u8!G!=aG|(aJL8$|z)>yurPf#s;AF63NT?>mDxqfujy2%- zHrm(1(B+TEk2K4*;Vg24xs@Fg=10P)8wlv5S`CyKRCzJvcKLMmlUGN22%k}s9qc}s z*I>cWfI=8%SQR6Yhn5zsyN5sk*ZORHzzFMO~B6(b_QPg}0N?Ltbb82VJNH}!q|m+g23p1beUbA5M9^Twa>jQ)J-Ioc^{ zsAz!rNVB4^?c_W!p}v@~%@UCyV1P<9BfpO(@MH05HYoS{CdJ$^&V?DhJb%Zm$VsSk#HgNe) zJkJF>Z)L~%h5*BOwx!W(o}#%>@NcVXYa`P2L7>S*S!p9GA!FAf@TLAM{23OpYlB9S z-KhQ_w%#c`uXy{yjcwzLZQE{>#1d8&vP!$RWJKx z;kV`-bIf<(tr#ctSTk722=xH`YyeRP)q~X0<5whU8bKG23#E)fg#(xG96VXiOHcwy zmmHnQwPQqbMW%%*cFj3vY&}h`A07bd-oWjz>~bO4w#7hVya=O*16G zx(M?XgcKVW_Jk*7I*xL(2qXkp8E6anZG?YLKj`-J^$Eor3NZpGTE^(KKiwHaAf*-|6xuP#6e#{7J*PlpTD3tGu*UPLvWk zTVABZA@&`=_;7hR)12VXr4^E}2YY%CVBoQ1B;qrFuCR5;@qbL##?AbCVR%^Z1ldmi zVAMeUzsqoT4!`VY>^D~1U^q1TZ9K)3znKzDx-SO}IYfsPg>sl&8ZOCuUFq0C1F=c7}6 zmxdy1)XMzT7?H=s5Yhnqtkoq}y~#Vf%I)7GvZ3D{iK3u0jDLYv^M-?3McjBIaCC_P zMGQa+U|?qEmKTx%QjUdu?!QsV0XuTO0G2t+sTbY|e0=8~7{hr2$v<1Jb^y=W(Z=R^ zD+G1AlTiQv+zK<;{A)e3{8q@W$d_j#&Z@s0`F3Obf_FRFZ6Zpsl znOo|v+m9}9%378uH!SziCk09_sYNw%`R6EzGSPBLhY4tj={_1RxSyLsQ-o?bFW_P8 z#3otM-*A=C+h(NR0P!Xk8V3Q|#Xexwyxaa~4_7A^om59%@NtiB8{uE$gJt_>4<@e( zZWHm|veM21!2ykkC6X1hs^F9!^?D_9Pdi)vJH1Gpzg>zYXxsNf! z_iyRx&(jj9Z;n?OCGgdIz!>`DDuU$pX<6yyFM);r^AI3&hxo5M4{f+EodU|8>*P~c z7-Kfee;y_w#8<=4g{76_C#{4S75jl=|g4;c2=V#Nlk4>>eofiY+yXw-`$62$2oD3J~DqBBZ$mz2?uGxc;HcGv1` zMp*P9IE{)I^AJ^!kb)U>)M_CW^%zvA0zA1#Mj(<2z-8DW*4-QI0d$)q{!_7BiKE4F zHzZ#;(Pw~0(M?0I^AAJ|kiBhyP^>&9I3^(C4bTxIIt&hmz`uQm1hUe#9QzOcb12PF zbT_>LQ6Z^g(9zc9a3+C}j+UOPmS$$RXD_YwB_QKje!NV2s&QYrvMk*M;pg0Cu@85vsVw3Iw^5loj~V-4?AwP ziMdiO^Y3oE3Yudwx}`E zL`I|P=E)VVKtmQrL1mEU^+A(Fu?zB5RPLJvJp-GpDkat)%|=s2eQ+dOHtrbOLc0=L zo_bOodg9?Xv<`ao`sNO#V=0hZk9pMdLBiU)`8;@#$#t1UM4V4_)k|#=RiI`==p13& zgEY*DJI5}jyRZv2b#?1ws(lBP`nxX(1#MNCw-xK1ZyVV_?G9N~4y4^! z6dfMz9DcFPVYC~-R}U@~ZzDfCHwwWyB(6otue6JYkC_BjYn47zZzuzzzfv1Sqe0IqzCqF z5S*82iBgxVdFX72>9vfNvX#`i6459`U&N!*V#RXcESxsvorC2mEZ2Z-(c z$dia+gNX?1`@|>>qhgT!H9KsZ6>$o}m~zBCyfso`TKhFd8I!_LREsiRIVfxv51b3H zAOxofkBv7sMAQ$b(zJhG6n(MZ2H5I#*U(O8Ga1HU6(^F%{Mn1hKh(!72IaPX3Bhsw zYcSi_B=}CuO~0Z#M@8eeAG zJr$;iwlgM}W-EYJQR^Ny(XrFj2FH7X2t;>dgobV%(8x97(rD8T$~VF}!IQC2rq_y= zo=^IX19fT@nqMkE|o?|MurDAYGqcUta-h=+A2j9F^%F2=|KVUANz-&ILy00Dyo>J?M`+9}L^J zot+)%D#2yi`GVeuXyT`m8I#}vK+&VqTc4mZa@z({O$8petaGU`TFc5n7lHUF%zPDP zzQ8_~n!A*fv8h25P=}sUhRLC1j1d#QsB7EjsV$6~f5Wy7U@&!LtRZATLo}*oQ9ydr zDm=Ba<1+wX-GLZireUtKW85^ow~IsT7IqG8&aG>tWiNL;SvmZJ1qU@4@0H1Q!dyNX z_(D}BOz05u=CExG8+TGy9m?On!)YgAtRUHZ*@>Y2<0-Sz>NFC(_LL2p4y{9Miup@O zwaQD~kPOk+d4Os~MF`9GCt1Ju5$r%M9;n<>FZ{N|`hMG*fL=gOb-6+uQiw$;ID?j} zyNM@1NiTpUx`2-NXdnr))^38mh7P@`sPoPDn;?h%D3?(BR84G3%iddg~>Ri~x|^R(I? zP&=}r-_4H~>xRe$0*g@Mgw7-rwa;&@ZgU>7&>n()WUo(uG#d1qmDtX<<*Qa{ zZAi*jtVJN*=16PP0D&LmVOi>o1%f2TwW)iL+x6Bbr25S7G}@)L;LuzBj_o>E!H?*W z^5)RPr}bmuI28z4zl#3u*g_q@KN^IKoI2sloW)A2$JbMo^<6ahi0<+`nlK()TSNnp zj(z{nadm#wx9tk1Ppl9Ar|Z)H$$H}1>*gTrm%g7gMsXXDp5N2s)h@|T*At*on}(qK zlI_@a`~gJ%y$`Rqb=T2Uf&T1w0I>$)$fhK?KNu{5QUQgz)9rZnqy3gdW7BCrGvMGD z6JRX?rMr2mKKHpOj#+3`rVO!{*~~8~cOLLrss99(-rU^aGHQudW&VM=m;rJGgv>{= z-1aYPr!)Cn7ppelp8t64ciZ{^$@ESqGoJv3fx^u>+FYy(IN;Z{sz5)9Zd6V#$}itk zm6M6qmOb*S{*{&r`h3;`TQM%I-;pO##_>t9v19~dJb zk6BIL@X+}yW!cON!%<{{o~H@5+)@^LewR(gvpc`WIH`Y!99tQ2!+&eb&|^+D;D&Yp zd$wkk(nLA53(jsTp0Kc0F1IsTbT`Q_whJeIOs=%(goFuYpqX!Y(Je-ol-WAWn92!V z5h|!;h+ryM;5xl91gU$7!H&i)8320(glnDNKNG z{2H8be%_M22Ng+__oB34GJ;kjK?}nTNm3!=Y5ijq(nKBohw@4sAdu`I|- zxchKU@hbc_qA9o`YY`Su*J$kw|08WZp@*va%2W8tsB@;SRj}IZO3Kjg&dJ_lx#ih% z?d}3@8sz?#_1q-D`^%_qs{_)HhXN}lA0I%^K^%cM*jlLfu52>4c3_7)egE;5-emw` zwZ&}XgHf8}Q}v3EP*c))3(Hn#OI^{H)n=d(AH-}3q9KbeVSgbgy1|&d47#%VtL6N@ z8vnzgUO4t((tDyUL(G4rpm9(YNIo?jnQ(E2=jq!-8(fN&6t0Nns>shUzCwp6?Lp_u zh4$av7Wibq7c;1P(osR*7)YKbz1FvS+_!Rk&5 z43+9mHMUlK|LZ^5ZASnKeruGF=4iCrEc3sYHW4rqaW2gJaJ-T5@e77E6 zm=aiNfeEfrf5iYG2^eu_CnbSfOR8QZ1?e!7AUge*OTa$&5VWus7~)!+Uy8#|@OnojCuh=cKy^9pN- z(A~!w)EVg39$;CZVS_Gp&B{l>5V>>3+x}b3ajopT#8x?6Ty8$P)f&7Of9w0%?BAVk zyz}-Og_kkx+usl75Apw9*q!kKVnNY&9hi5f`8rd!9Ax16^7t198#C*q?zaAIfC1H=%;)4x-qaPr2iFQs$ZYSRU@$X2gwy1$V;#bcG|}2DX?*ucJvGu;N_&nf3L~)Q07N(`4mx(h-4Tf+j*^Z7l;ER&Hyn4ux0$xj@ znY?LDoitn^z(e{JX!}#xt3R=!0GZ*3P3!HBTy?hJJv6B+iTB@ASC;ESN_^#Sr>?UD z5#KXMog?Hs|9-ns%z0{C`t$MHd&A-;u%Wo=ark_J)A@SvVmaQvb>4znPi9S=NJ-;y z^HtOT4oGGp19k_Uc*pVr#>{i5L?8^RJyd(eF|@ zodJLFyP%&o6Kp?xZ$J@i&^ejM{zEIpIiCCbT`g}1B8x)}v=?wO-~L{iyro&@cwAMC zp=P>sUfkZK!v){5bhs_b^XQ|}IBk4gT1j{b;MvR?)U5ug{yx>Bafh4ZI}J|pUFhRsP4$EKCe(1U{c=t!<_cl$ z-?>-}`@XH}r(plQtqKItd?H;%-cYbNVL6fUo8?}z~t$CseaoOk@I-x)wL`1 z2e=_duaBPAYiNf48~Jpk4%jKd%0^8R)}T8BAyQI` zO#0xUt*OzhIvC|>KtQ=UGBTn>LhuWE4MCa~*+O}S6B(-nh7RV`2(u(=0f`31HY`a% z3o}dL2ReTcRv>j?2Yi81kBVKS3^-A_acKuZAXhB`?KG6E@(@;Bi4hl+TX_IuoOJ%8 z)Di!5DD)PqF;9>-e;;Y^Ok+m%%aHgz z1CyC_qBHP++{;jSOrSEDI|czHRsbMpiqo(MnCEP4Oi+hOova=U*g2%fdmEPw1RVdg z10=|_B88w3g&bib9q`=$@aMpXj6ul%aI}UV8()!;nX*&uxR4NS1?5ti?_S=Vn5`V; z)<|X}=?gILP zEGOSqPg0YUIk~yx33(<<+AvSGe~e$%NhL}YAuM9*p!Kt~LuhzceF ztZPqPV9O)ATCmr?PdDl?7@~Lt=ZNi`?gHg_(w+L#3yQhR)^t6y?|Yi1p||ln2TS|= zqljVpQ+c1@^(5d~&8+zW73_>4HUqw+iiK9+JVuEn63G}Y`sF(^V@d%jvxE38+$C9H z4kT$hGya$(J_%j;K*c;Xcj_tc3(uiE{}Ehb6fDt-RM03avG?sA&mT~f$C1+o0${#? zsMr1}b%?tk)J0|L;e)0NJic9Q8+Q-t?T+(eVE2yJBfHR=U zhc4;zS<}F^aE?k-UDN0k{(ci(d7{Z|U@>=#o`aO)9tl9j@BnM}p`5Wq=*Qqd$ z0I&P%sf@2P`yW$(yuhTD-B5i!9II3JW--5k1u2PNsLQIA1qKIV!AC}hDGG!4h<{f^ z7pvLcl*Lv;JBIs3PEXr`=?#%rkb3gfH2?;LoHUCn9tM zxXV2#xxr#UJTc7B7xb%Zk}w$ClhYb;eagz<+8ABzaMwGdIm1FsyjOf&qkgM_4$9Fv zF4aWr@+n0b1Si)<(FgHGBc|`{7at#7Ror)HQowAD64pI-)h`qn>ns&Y=;U89H9jKk z+`PfA;!jfuLD#0Qmm%V`H`6b=lkE*1w$PI?aWFltlztV6C1SXMG-<^D(w`>B(!qZm zWjQoq*lEAqO|uQhK+6*uAoBKrNW4$#L>0JY8tDW5dHM%8<#j#_?rV+7$~P%Qn*k~H z1o*FP?1IQ>U<(e~AG=#UAxqVU7X{d))7zKRMjtATyX3nj;=IyeS0c8LdXk}`%M%u( z=HdS0E*kFfD`7>IGZE@RvYk)Y-_ZNxYZzFhd$_cyrld) zcbhide_LHeW^*H-Z)|_jk&SN*L`lFEw_VrM1*Sftq-by+L4>h9(}L|`pUO^VtZ2&@ zq6I|}gq%gAD1^nu#hni707R^1vTWfON#<0Juy-!BZ7CAa^ls*_La3HKQ4Cr#Y;s{u zf?5dphKAo-kdl-saAt#|^mgJfAaLZWIZZN2-$#`$P3&cFqDzu$<+4iU&*=58` z{E+WXAOn2krAy{bwBdwUHGc@K!I_aD)l#)}qd(+UI?+NEP%tpRZI>KQmup#3zzuxP z8i96_GS>+uNkCFuy!Ujae!-@3#tMkmWFbq&p#QWoAl3y860PJ`sn@BuG$EUsmzS5D zn|a~L{9U^!_AFJ!w$910JI%jP&rN0t-+0RYM6a zXvLFW^YZejelS@!DuI|-w2)F{%~|nsF|6BeA)zBYZjWXtq+_j?Yw|wkS$S#=ixvOH zXCF>8sT4F&{aUQlg-xZscc%-W%l(w<8&M^mlKjaU_7HeH8i9)dbkP6;zgBLwvPlZK zZ@}$ylA*I5?(xQojVTmz8DyCFNzronI8lr77lnbh2RDssh-R5*Xo3Dl7P;Z^6F#To zAAaVlAI+`b_da$mrpR*#+LbY6u@ebVC`^^Pb;7BO&^sKRhOs7VB*;Vv_@o#pFYr#e zB=Sn#t)#PHExYJym2g;s>F8t9pQ@SGSqA?kT!*i)_0EfLDiboY7pgt#pB%*$sGNzUwO8CCZU zAG%$8FDb%L-UAeR8!U%qpg)N$Nq0%X`skP(o zJ>~_Z^OUK#Dc5gAB=~r8MJAsncv5b=r=@;BywbuO-00BN3^CBmrik@pBQNjoUg>$8 zQQ=N>#jSVwa;~ufv#&ZxN{8Pidf!*jzz~xFjXl~PsTsvHd%hFVu-Nm7nbFooW3 z>dAy2*+@|fE9f+Z{!U|L4HM+hPPNRv9N0klOepjZyTG6EfxHDfnY{h@fjiEbupW3j zT2TlwkL*rXUU%32wvZbb&_s{D6Sh|vACbr}JCHoGmp~rv76AE8e5ckO&T?HCnVJd! z#@=M1o-g-jwRNwLxHRiT!NI{>)D-qu5Cqe5gb%0ygh;bWkI(Pf4gKOLBrHnzn=#P*G5r>-5yDx1_5LIuOu5t0Q2ag(mpoqz;-G-uERgGxF{p*rF-;Jfg+_(PN& zgTut{dJwzDw1P+qQ4CLlFbuZx+NQh{RHmE{vj{fb|9p4iY_Wy&fT+XL8TteD=!=5F zBoH~wpf(z?3BH+Eu0(~mSs?_b2WGA(<&@Rk`Et-hr>J|0Wi_2E_MKY3!2Z7(ZDa>r@CPqn?fT`|oV&lMA-bE!8N?|a}4l&E@F4`e_ zmWPrymUsVaFw(*l*)Cjxczv^l9^pVfjx+t_@mRSV_XpLZ=|OPc4y_X*TBy(A!1vJ8T`)_He{a9kZIJac}gX}MjLm8+S6@0)0YY5 z$F)OMi!ke&2 zy&e$rx;Ek^E~J+T0oeQ`xya#<;R!iwKm-h+N`!`nURa>0(5D>#-r65aMYVPWR?Kzk zTJHd&^x_m~t;umS)O|QPznIbI{uH0rBqQQZ)B{NiZ%;RX8~y1G4XE~S*XQ(eVWb_hfTcJ?TNY8|92R`=(ihbIplm1C zEyGL&$3Qgyu1f~d_^(TlT`bLVdY|R6c}8F2D7VE1zT}k`+OEMU3SRXOaZm#b?uf5- z3@Gl9GbN?K>kKIR4og+~k0=t&*fqL|GSm(BYxiNsAv6k^R$~$Y_m`|ksS3asZy<;V z+|%;DS*~U>^^=xa16G?~vnNxir?(`hI!KU+_V%}9p&Qx{82^tUNA}E_$5DprS9YKH z0h1BsAD7@PL87!Y+Q+G6q0VA@dYO;#3h)JmKWvlF@-rXOz7gI;U>1)hamzx4N3N`( zWXga7DPDI)Jv~c3eoRkK1J!~Aaim0~pNek=>(7Ull5XmLXZk=6u;GM(DO(tFUU-?B zLqxkBHJ}b!?spLARBJzW{n0~UrbHVn!F`Gkm4T;+_5%CFDV$3hs)f46i$W%%FtA|a zM1{5k2UeLn#0EXlU$6(w$Gm{MJL_tSF-T;c&^xS?RTYSwE^%# z5pdh#y&8z}dEXuZZ-4ML0Q)-w6xXCE`ai(j29Vz?3_1wdMc2D9wt&(BxRDT4d-ErE zz`+tKI~Y%N@)2x0h#%8k5hrAC{Dhs^*IgBw-=(w>4LTm1PuHPDGIOzD>o+^VtO{Si^U7 zlx?kG7Bus&L}z?OwCQ=}!FDSxBCIx2x2wuL-Llh-ozPy(#E$x+A?4|?cO#oZQ2lA3 zj@~K}U9+Z_V{%xVlL}cU9brN*U($6m(!0F~J5)5GA)0v@ zaa_#`nV9^6yPgn4Ga%-P4al$TSRp_9F%GZj>^{XWHvLj+Jl5_|VHQ4cCKM}>IEX0E z<&IpyLEH(R5U0zef(`pJtX%F}ELzHE5Y-y@3RP996E;O%IGN9ta@7ZNyS>yydLYAh z`^jIWE}!K(^$cy(lak~@KlZn3q1n8$wKZ4i(6RitO=fht!a(oWwL%DT@ML(&K-)N+ zieT^p9ib5B^|wP*U5HbcKg7S!BR1-s%(cX^45ZA5!dcYM5vl3HB+GgeHe{6Zy{lm* zRnQ&4ed)w$-~U4U`t4AXXwhl!J94HWZU^;C(TN%0KOFqR7O{VOpSz1WEU{kd{m8Jy zbt6?NLI}3J{&v2-6K$;mv0c=y=&Dng#eEZkjo9`3z^vlQI81MAiz~c&4-GC<7SaE~ zcC~JCtQzIz{X^L6u&cWLb@dii*kIo8vRkJzy<89p&Hxp?Yx{%_5iVP?^7F1Y(%Ark zN6=gT!-1KAyaJzNegmmzm}H0%bWbH#HS7cU)N1(62`vNMA|_mD+jv|AKBxRkySQWk z3VcP6nb{*v-WBq5F{?$ACJ*;G`^tYf7l8 zsR2%cLb+6j^=9XVGIeKXXP_B?E*vnqdh-)zU_hLLQYMhPe%WfsbX$ zdUs%nYO%tl#&O2w$<9Db&C}j@#a;%If_Qw6B8do;WlKu{j0*gQ5JABD?l_&>lb;}r zFp5qgedN{(xPx~oBd30{Nw}S-2>{I4Q+fj(AuzQt4#@p07;>QFizLi&nO_9+3c~Sm z+UkOp4Zy~-Z_qNy)KRP>@Y(USUWKfd`6Ut`As)PYtUr})&bnJ2rFn+yN#I1CnmPK1FS7U#s%WpGWZhLOLst$Se zroI`eRqey0?%utsK8(nUSVQrQ&!hOeBxi?V_x$%vjQ7J3b}Cs&3^ho!*>!z688Vq$ z6Vd$$3=3fZ2wbWqfwO!5_IfkTNrdmUK_mY5@_-5t-(!JziVZ=g2A%iCO+Sd2ww74D zqCbtyfN}@YGGtmLI4Dyof#ufPDIoVdS};&$6Eiq}IMer&h4L;+M1Jqb7!Dc4h3y|z zvS^1#C*j3Dapnf&3GLbF;|rPvx#q%g(jiFtUfHGt+`|&tQRGqa0(A=*_eUZHV?;k?Nt+XHa06_+uTp@ng(72cHAKtjz1zDHBe}!+Fky2#(=Ov zjAx{jnBikDMSv8W;frW>zFZ6G<9o5*GI!<<3!6W{BTLE7u2MYr26!GlF|Tbo36yr^ z4TS)ebKa(LTGGJ40C1ZEsf$i9K%U~%BDfIOiinVE#txz%bH6m zVaAH5m&#D{9~)KU&y2wo?@(^;2`J&%k_L0+3Ywr^o$@I`mPt}a@Ny!jIg4e=(~CLUm}xEOPSl3^|q|%o3yEnnu;02KX(f?FZLK8ISTD?jD9(| z92G=E^+}qs*y@7W<%pajxSz|zg02`Gt#Q!NSmY5dH0I8z{xRvt6IMV&^Kh9H5clET zQ_)EH!?&wWQk%m=d^JZT)0KbBpc7%bex-uuEUYPY;o|i=JT61jU%w+)UK~56#c1JG zk?@xAYK7$^TkpY3bTEj4_$vx#39cO}jt*j-=UsercvOI$jYqZ5 zMl7>teR+t*;DdFqs$t~{5|2A;9-ce`tmZ9Whg>Zm{?4R~r$~^lOE|RS@ljK}$I2GE zSWW-(cqes@vN}hH+J5bJT_`6U=jT`q3CG<~s*y|RChJN-sTP5!@Xg|Ly51X!Bj64a zWTtUJMenET79!re>~(HfJ1eq+p1NVim6>bk9#f`|5Y+Hw*$whd2B~ZtqC4j~vs@co zxzMJFlc?vZCFhuS7*r1-Dig4P$NR-nDpH7nm-3>NqV*w$%6at{jw;()k!d-z(V_57 zIuWZS%rhH+I-KBL)fOqj+nW@QNJa4^Pb#D~eA6E}b-bbh+tC)dab3=BLvIs%3jQc_aJ6BA6*D6sNHl8DSZ z43)3z>n3|RFieO8(Mya5>$SZ6_&Vcj`%K31+Zb%{!}t@TxT1KU@>sKE&bb+5vVC=` zt$t(eYD0L_Yi`$(eKDPOjwzZW<#f(NTh(@B3R9s?d9WcJPa*Rl%oIsQjZmtTm2qZC zPaPG_GbFFlc905)yZSqA{NNqMb*M4(cV;Cbrd|icUWwDiiyy7{`EwAOed66P{J*or zQzq(G|K&fJ$4XX|j-w~p--2s`r@ytiF29%Vf* zf4o0*MWlY|aM~76Ncxx<9bIkkB0Sm%r=9+&yCn47B!1A_86pYroeciNRO^4iL()h1 zUbB?*uz-nXXu4cuobk>xXHu)D_KRl?E$jwJ9Q-Wc_Xtp`s5iCHi7n{wO!n};Rw-+u z1tW2W)$cvF3tsYT5Vs7~rbP4W4sDXa`W$UFs}6?ffjs>^A_CSLt(V%ZU3J{PCov>j zwc=}llc)BH67UUta^SVvcI4_Og9J{e+2jTsY)LbuSD-=Fcb#_-PX`(lw=D>?d2*f;d;bCdbs$9Q62zda_a_kJjF-6Dpg7^pdmmNA z?g{^8>$T;$?N1gV?DP|<(?;Od*CNhXe94|0(#L8LiUDeHhuwHA3}qrcSPz5N2p%rn zdB*pppk{oSevz)s#z4;wezbNN9EYSE8?hQq6or&lx&NxNqRrJxV_(>rU)nZV8jl|= zHf*+SMz;%>>D6u9H<~7*7IF&tLqQsvqJ#t<=k|fd7*og&B!ng+Uj;X^?Q<0aRTR>vDy-l)YA|a;2WA_37F0gQ$6H=!%8M;-*q<7m0#=33;H>#} zm)lcmgp5)v{Y6F;Br67$dMD)sx=et4_cknu#1P+%<@+^+AlxwQ109G7Rf-oWK^{t< zf^f(JLC-*pUxLFQp27p}_BlEQ%3)`lOhhDLIFYR5b-C6wB3ar~Nfbr{j`p#5KCSFO zuO2DdIa&k?(eq9Wi*y2UTxyc{abapFuuB^Pi@KwR2bU=l;D0clmI$=J#ayqai(??LPSvsey9gYkHXUh=g#FH zcTWF!RK}3ROOvhCaLrtXHG4wyj0dC}&dTuSR7_-Y!RK}OrKuZFZgZyocH-nw)uS8Q zY8i0nVP>7r{faq)C4I!zAnX3$?FP5XDXDY0yb*sfc(3y56IA9&GqB#_#&*TH0*SBX zNOZ3rIoOCsd-Jj%uU~eEn}}rx)OEoRcih-rrb?CH2;mmOO%wmRLFJu7xL~Q2UmLT~ zD!gJ1>p_Zx#aD7dVJDCgM}{vgN7$)<8F8gg{gi}S0W4YpNEJkc>0${yD;iQ#Qg(Ki zk#AH@k3ra{DCLqUIghfmB0LWOfn16B5>`WW;cp`uqrdYv)QbOciV}J(KiNy0Whr?Ql|u)cVP}>(Zqt}vWMR!IYd0Ds z$V&Q<<^mQk6P_V_Aq>5nSyRE92`g@=*KLF>9i7kBUOg7I8~kBYVM1&-oOn66mHFOy zC8*=t;Q7W&qmbefT`V~X!;17wjBp<&K1b8XJdz?y_3RGMd2~V##y1Iv#oqj>G0Jsw znlpV4>YixF(;pQ7y`C{fUT|3}Bms$g->465^5X6n4jGDf^$?hG;lQ1**|htW5KMwrf&knC1j0OaF0Clm_gQel2r#vV%l| z1rR<%y4N;yE{hO{auWv<{;A0FjWHcAN})w5=?t=kvv|tegdJUULw4M{m4ym=)eAO| z5Cvi^Z2LeJ9Z47QZ%h~(@!bWb)^}ZAHNJZ+&+)?#Ti&Prq1*X0aO#~?KJ+vU;Z^fO zBF69baL2#&dC}pm?NS;Zg@|)JlJ}aHiPHWcRYT&9bvn9yu*&W&b(Mu=7oFURAjm>& z=ubKFbwliK`8Nr}z{J!wK(!KetjE^Bj{W!l{^zo+!>>C$0Pcvr3?AUe2zC$KEK0^4 z|G;d~Ep$IZ5O;+}_#h+jE2W6WLl^xFts87V;(ka08Ws>`dv6!a9J4Fg%06Jo>`^GBe3L}UzUbAFg z8)-~{DCidvpm^#;wabxM-BB?@e~l2eMVu2<91*3&1{$`$u48LxO8| z7Q>}$YLByI7upsdFY3Dlc6WD|%wEYSOZ?vz|99cHe=D)6#*db&@F$P@RxV<6&tYMJ zM+Ls0e=&{I9IV%YZVa_?YEJ5wcpFBBZP`515#^x*d( z`JcaFJA3yfM$S3O<$n(lmP##l1uIIUM4nP+b%=&=Z@EUEW}J9EivOWehaCVYe+iE^ zU%9;mnR$!4?(31LSfM0euIG-=&&OXh1~#vTSxS-V_4Qekt$S@t@P6aA<%Awe3xTBN zjLkfPI`u4zuY+UFOdZ?z;S1w7$grTvL*n z0xo-3R7_$_yCV#U5X^9J9tK`=mapG3!)*-*Ey6P z8Oc+}$`{IEg>c5uDYu5L8iyHR9dno3dzGla_iP96Qd=-$4=e3`>G_;wf{NVcapC`V z$78!#(PFbuhEUmQNG@Ag=ksvx$#mZ)3d|S_@&2QJ=vm3q$`^qC_bP?0x6{#F%bNA? z!=>jA=-}Z?)5F)g85C{oEN=Pomf_J; zQlwoNmszepWqhJ};t+T>@GTImO(y?reBNKJd+Tt#|S$5-zNWnw{vsH{~wEfwZ5 zgDSo9+XZT*1e1v5rLU~Y;EL28t;W05iSoJuN3WiU)ta#EUe|lT%)`WQXp#t8=W^-x z@3icc!17iJOPArzTeyfA4Ao+!8MA!atNV!t6G?#gUR_*32cJ1Yruc_ShX4=vG|y8C z=>K!boU1zgNrrR+(L`+T=~^3rafdI}OJ8#02!-WwY<{{>*H zM4VQMbH@Oa!x`9%q&y=s0;;AC$pc`hWHV2jKeG%JuYj@)4hduQ7EdG)ZM!81*!cW_ z1&bO!>)8TeK~}5>2#NKW$`%6Bc44UcV^Y7RQ~vK{1ODli=$puJ*(2sVv5=S*|Hc06 z6|nEnKJ#y85@;{92k=oO;Mo>y3<&csg$)bIT+i_-(d_Cm46QUB*>@;9#e$J@-A{KT zG7jf>TRzUwdlxZ^Ex${g&FYuiaR0dWr%u3il zc4SBF)md1!LL8yME^&N!C?Qb|2rFo%wJ+f52L&;I-ANTXDM$tIen)N?yORi|n16+B z+4wV-Dq(7p-e7H-izo`x>mG1R?K;5HAH!a1EShYwe1mA*8vP^&ZObLCDgzG8M*0Vi zSqcSS<`%7%cueLh=G`h=*!vx-5B)yc zD1z=PH~NXLOc3)#aJj)@<2i!Ff7*%@@a^#WKGqa;KmrjI_E_4jZlwkZ76l3l3dB?p zvY(igF>!EU4vC@zE&kupfYid1ay!=l9=5#M9rt`}Wq7Dw$dQh>pv^Km8b$$1S|KIV zr9iA423T7`cJPALm2hZ7GbsyLjS;qIpPk34pA^!|evNesTprn;nolQ*SP{XHNd!K* z&Nl>!_CG#|m0mBtP4h2ooboGzL;^q+OSbH8>27~IS%3fSVm3IzapP7jSRgWX9{Q<2 znbsiQUT)u=lG34^lHahRLz1Z(yGBXZYQ zVE2E|67t^aetxqoNk__f_8lv`{{Ehhnr)Byt@dUdOOpc=%J1)_ zc+2xvoX0~6yT~6(qwsk-(&Y<_4lC@mv~bqMC~EZTYHC<6GQoX;@q>*LYJw0gBZX;{&;@_-W~?)3p_qPJ|GT;yu;^V(16cL=L*1= zf>u^msBBbJRa0d51HmCl4MKnZ!~@Y8ME|e;RI=XoBL1_oHIe9RE%oo`PT~8{U(R(l zb=kJ+HaV!=;yBT7xU{_5ZOEY=;uC8lLzz*2E8@*l&csn#KmQsL9&Ui&BL-}F7=XbA z-1Vs_MW^OZD`F-Uj`8~Z-ab6C%`fpyDG8V!-X|wpmr-Peg3ajIcCuD72*-dLg^Z|W z-K!c<4^~GDrSHXM%}yQbr(iS?cM26lMZdL9@ocaLe?upw-b<7%G|^@lbrS$!n=@9N zwY9ag?oYg55txceFfbrCLI`RwF7H;Fjv+s|tp+9-U+n9D7W}jV$7eO_4u(c##EB`O zhepglN4z}`DoVnUeK#NNI6eVvi7n5l)-T@ceNeVNJ>B8@rbl5~bg$gPEd1-w-N0-#&qg!(zy7@#VuFW<=de^z zqfdDOyeBQLhyM>zZ^2as*R_q(T}laTTDrSi0ck0b6ai`J?naO<=`Jbh2I=nZ?(T-O zeC~IA=Lay@d+il-UNsLky7B3@y3MDsk0~^p?d8==bcw|GBzF)c&a8dO-VxdN;lKXh z3G#oyaNh0E%If*YmchY52X4R+;p6uttkcHChaj|XAaGuO}C=(K`H_kwq_s4>51Xij= ziAqq=J)czp%ikTJ9a?F&4>^d$9zthv^?JjsZJ#xna<^>LmhUHSCJy)dDka)0q;dph zg1|x&8?^AX4vT&|5j=#ura$Xz%tNu+F12>;}>qruvgHKggpL2uhc8(?fUuTG=f&nB9!$NFZ zp#3SPdv66DALG-TBKL=K)bJ0bEX*@XaP$xI*P|JqU0o8R)y^M;sw)i!&tlstod(E1 z20q$+feYIn@_9;l;KbvyRKMsO3K)o>W7>;Xxqmd9(<~o;AaZxda9O~kHBUuKwN;2R z!M{|{M3=EIc$Ap^#OW?&L$wjvw`g6iisw5pHVCfw!JMsN(OLve4NWi6b&L>852XAzkB~ETdJ?Qq(rhAxV4& zzb6b@-TBl~WQxbzI&hhLC+X*Y5}Y^-e$x|a)+K%m4{s)%a-n~oYnZT_B;;|uyyRx& znCy!$m~~cORq@PA7nktW2nP}Iv8ZDnf%HOi4B4O!nRp`a}N5fPyWGQvW{5ilUMRDzd4auW{;a~nQt7CzbKqryk~@6ZMLEbt9u4r3w> zjuCKN>AQ$IAJL0l(u+;g-8aOQ09C>tevJoUz}feP3gsUCEpd#o8~WS)K*L8eGPXE0 zH-)i!KWdON`=^v~&lCHi3)Ltr1(&;C)Q14S$E?SZmRr6aaoyQrk-cw*x&)!mvy9lK zsAs>`tf35jCG|h{$#ZRn`f8_}+M_YqwH0EoYP2IW{>-rYjY8>qg_|wRp_GuVmM4o2 zGz2n%yWgocOn*Bx){qYVx?HMF7ZfRAk#23Hj4#zT#ziAzmO*YG|Bgkq1!IFspI!i0 zt0*(ELHK8t(8LF!jjtR5^%(Q|&US(k>HDW)RI<9*pV!{bCIm<=MHfvlogWHCAI&=+ zde38-Ryd!p&fvFUlI1YgSFoMmdVdLf5nit#k--=*zM{km`3`|mP*eJjbr`puheHM_ zgKGl<$an+MQ{i=C1EFCFT9Jgp-Psyg^`SR^E*4%e@Lay0hOQJZel^A+Yx)HHnoL8K zQ5*`1*(hU>>{~z7dRe)WYh+D{7CTN^J|mfK8Ad00jL)hduSmCuT9<~{=v4QGx+W|I zzf!+xH*D4pF`fpl)5s@XF1wxF z322K#9I-WftVc`RI=B{S6-0f=Fvj#$YgbTV`bh=dHv4ApDhiu|u&LjrB-+ zRPp%aVEV~4sz0;%WWGpA2nwes5!&|bzZ$#1V$oAYVQ}hnG#0Alb4jh4KK2~`OEGP5J%*Ldx^9arK-({btmy86Z%D+0Ey(Tub#m%})} z1&=0=${Ag`eWBX`(%Q$Te)EagdwR`fc;6jQ@fQxXYY(X0C|ts+-_Pg4nY=HnYb>nv zewx-kUq%QSa$)!8^FCgE;X3{RPHN4`{7I%!mGV(*gkOxVVXTjdFg?c7)Xq?{ll_Z` z*7OVSY2qu6l^5ruyp;oPi(I9BXP>G5IQavf#mfuopn*(vWMYA;+Y!|4m^6Z_XAqai zaSVcT0>?R2sKW!6+_w|gRPve-8wt=4}(aOGH+3@vG442ys47Fvqdh^=+6^)kX&Sa~fXy~t2edmDg_4;Z+67!hL;W`Ay z3J#Fn{VIw(K32?&v5)<|!fl#TmDdrw9qlw2ClC7^LTPUv*W-11tiAqE1VNANWJZCT zKS77&JE91uvYscI-PhmWV0%iIwt@HA&3*2XVkQok=W{?7=rT)OuYRlKhs#mNMAK8D zb-a_3;xU`604M`{{QCzOH@}-+{2R>j7S=&U*~&Q!*86ru=%5zqGx>V$V-v|BwrQZ_qgm<|1o1A?*xX8QrR{~iC}iEbHN?#!d}9;mQwD0q3_%y{3dgP;Q5 zLg%1?%4Gj@rL=>KKq;c()GpunWd`v%_$L+U;9EVNtyYTCUd6S>%|&FuJw?T=dBYD%C(*3>@Pzy^;G<) z+WyBlnu{p@qGYOH4RIDRM|TQP0w{PXULTukL_{fft*~GFwq4BpX9sFCRYB5a>GJ*J zVOTJx-*+8LY!k`+sx?|rdRZ)Hug7_(tKp$idsbz=zp zO>wcZ5_4kKpPM3hMH)5?HayAAx?!SC%b%Jy$}}xW28=H~+#DKHT)U&v%T~{PrC|RM;swkt}rB+n~+lE#l7ubExqLaxT9`XlPzj$;sA1{}jK{K~8=D#17k z3EvLCpC_Kx>y@P3R(FoylV`oX&8C=8!lL*q_JfewJD5j)bk24TU$+>!Mk!B+b5%#LuuOa`}wE7DSI8X*H zppIN%1a0=~c7Eve!tMbNo^F{)vxl3Q7&QCC7jT@Wpln{`L%yp0mJB1T-kUjzk|L>J z(~CI4kj#G7Fj*xL1pG*eyVQOS+HWW%u$STW7WF6tS;^|%AL|tI4cSP_rn7aNoWDg?|as?bGIuSR7*P3io(BuBnZbT@k z)g2l*F6oSz0%da9-=HZ0Ytmaz&ZKcUX#?04~qcdi1^Q}VB0w;ijMw%V-MPvFal(oOhW4~6LOrz=xQZ%dHVNw`dg-B$eUp36a2-Hy zlteVlRR&>u;R+%S=_288{B83xoq^YibuqHoQS^6 zr6()tXT`-X_$hIW4K>ZjC!0r!pV8Gmod_u*(=;4a`0r!@SrGRI-T<-Bk*;;+(dld1 z8v25u&!^Al_;f|V3V-}kWCwcL1hgEzs3S1Ya!|S>4tq4m8BL|awb^y5MxXE9lc~0B zlZ3PA%P>?&71}IqxARQtwC)2!HPxSi!^IIrK3f+~$yacC!&K80EzdT{-h8cEZ@9(w z@VAOx+h8}3`(!^nJZ*rd5UJXjBNj0BoQ<5+Y#QdJR-^J`6)I@Lza}BUkodkBv$&`| ziht8`hDh=n6te`%R2U+ZZ_@a@?jN@%5Yvfn{RiR`ZSHV%`39&mFswD3<*84Wnqvl% zQr(VU2a9L!PUJ`YZ4mvl1MljNMGtepdNNy==d!X>kSX|FXaWGB%()tqcr+8S) z$H{XM`^AkC5Fcgr8!XkjWiva;u($3Xpk8>rdBP+O?sH`YJRibtoJ)O%HFGpy9lF7AD%P*_SB`+}aJt45 z#F#f?>v1N^B@~+ED)Tw*&w?3)ce~7-g-pW|&VTwocDJb|-CU^F$*_E6i;f3i78OeI9{yt25>I5t`psXm^y%B%TTp+EysTK{Pb4ysenGC?MpuS;qUB?s4yZ$s=yT#>qfSAT%@%`RDDxR)2&^GPeI=C ztkhT}+|E8>p`jbHHnAUM-@~q*s|-I3>YQ(!x;8sD5+m%4b5{&%6FtVh31WL$>j+|b zd|>0JR&&pmaQB( zc^m$)7C>r!=iilVFfFPTH|O;bemKFmg(SG_s{$SISTLS@*At6C=c^Stet#{*VNdW1 z0z*t_zJ>Eq^$dy2=BqlioXuqT;XM$S{rdImf6g`QO4>M?IxDM{mL9*zY)1S$}cNIY*gvX+*-5Ee5EWDiW&#Mm8#Q;47NCYHG z{9cfVBo~MB#MOWXaF^gvSS8rYwdR}U3#Vlt-qM{w%eBXU8ZK*|CRhjIF?A|_X;Fu}xp49t2 z*`r*|tanN9ohO~$6w@&=4k6}O)uoGs_aEWGW&YTa22*A)9`$^Dd~^lHpq?2#%ds9d zMB&7REPYuu9WcVjbO094;aHf%nS6r=g$K^2EQU*-3r4#qs*`QKa@yF zRLdYXGVB1Z&R0lnFdFFAX>`<=H~Jf}Y!!E+;F0Zmwcm4nlEh<`Ms&OraDS7haDTUx zeseM(&h-_!-S4L?0m`SnvGmr!qS#ygS3tk@f$v5081|w44YQSAy{!>5HmmjO-(zGo zAn(5Jt##|Fs?QRQ+-J zFeh6o!n5*F(Rp7!h40n*VCpHsuE_1`=TWbQ%P;QkkApLf@@ay1RkdT_J~!XA3{)TH zE4zzJ0OIhz3(0EWTx`DtxjayJf&$6NB$=P(E}eX)^zt5`j>IS@8)2L2{cr89ib|7VcO^d60#KkT5AezQ*B5 z(@gR!CW*f)`xi|Gqc{cB!5-1DyPygIq>>!$q;g1msdwvI)Y>!&% zvdd1YZ|XoG8bNe+n6K0WrWLw8sNqv;N`YbMssKCkB0(sB1$gyLty<9uOcN!nh776xdA#-9$+W|M;Fgg2NlZZ-c*RSQ|9qJl-w&|9cVAo*h8V zZ~XHBny|?*$A`U4ul2doa(C(E#N`FtPiSc$5LFf#dfsK;B#yy0UIS=tWT(J}IE~k4 zy|jrClpBYyjnY7VQ!kG$;!_{n^`G+{q>7$W@uaF9ISNYAqM00Tc71KGM$*~ovr)J|>oV(dKD+JzqJUPa{2+-r-n={xS{g7X=*)`HZ2 zKjT!>y9R|JiJ!KdM_vSllKf6R4OTkL*uBe?4>zO&&WE=y;h&C&3~a;Belljs*vBOC z@1$L!PWm!XZmGsk=@i+>>V6S(w76B?YFRyhQ z8xBRp)D$;!^dc~Cs4m5p#{JU#`PEeEh;%UvCZ0@0LNQF0FKiAOOUFUYIQyrYv{Z+A zvRw%{k15;Z5w)FP{C@f77cBKE;miNBP}aiRrC*j&fmE{V>zd2*N?VSg4Uo8smZ+!C zH#$9p;KEBqxHivhKQSGbDr@_R-780jsBmV#IO;>5LN~DRo>5=yAjZOO*DFj~!>PQP zVUr)B?FxM7i;8^7wGk zF)>m_1iI|1hh%#(&*C4UI9FKFXY6etkFxLQ0%5h+n#XhrlM1OMnu{W*hW| zj~)^M^li7_j-b24h!CHA+Pc~UQ1!u_N}W1uv3{wKr^|0%zg|y9+@YTT=Kzdt6#ArK z13^(JQP=w&sE$-oz-w58W!x>s%tzI2YV#hKE!^FaR%UoCF)>jUL~u)nEu#Mx8{QzN zM=D`V!|t&vO`%}W%~XQsgNHVMG#;$?EQR=F3GNcrWmT@-UV0vAm@bi|?U*l&uMHP8 zSU&=Nwzw<(B1$t3Dq)~8S*Q8O4-CrsD@n@Y)nEMnpg54&(wibGFPZmh^BHX-7p{1M z`IHPRp3`gAo2Kbg(PJpr7wyi1eK0Q^Lk!Fa7pBp(Ox`C?pi&@|T6f5g-kW%G@l$?TpxT1PVL6mN`@K9JOvur3kv% zUgoOF#xs7%ED}>_#xEgoQqw{_XTSsm;JQdB3K9=;;y=#rp8^)!dH0h^Y0AT^G^xsP zZdr?H5Q614_|AEQX(XuxuDxThgB`5u7Y*Rw*53_355uc{kToySBQ7Cv#vEHm`TT(@@DGa; zO($|(`sAc4Xd=G+g#u)JzXFqAGA$5w`cZ=6wr16Y6sHmc!^7P&|9~o0u*^=T6kGp3 zaU*<_BiJ%_Ko&+lG4yB?3%E!4<7QA{JS^d;>aTW3;rTCXI@>TiJ??xz^zgfg-L9lW z%Bp@NA1={%s15omEL_cIR|mBif1nVxD0zLZJ(|rIc(G|l`>~S;F)nVpsy#FPlfsW8 z6=cOh&cPK7~ePZ1Ac*hNdKCF>jerU{U6 zk;2O%B=qFRl0THlvGcd>k)p72`Bz#}62HfCqxnqflgC4=Qb%P+)TiOW7xJ={Ux8u& zcEuV4%g)Zuf`r*nx`+rr)RoMCf`s(K#p2JE*JTgRU~3R)TYyZV;tzg35u{$+Qs`%i z95n4^rZ2}!Ao9T7W6@bPq&x?Vxk_Q}uz$+g@DH(VY_iqnynicP;RuVg#L2J^794Rg zU>L}>b{GO$jRSxz!AX$@mUkyczmN$3R!f&nJSu`Q&Ig!9Tz8NTCH!pDQsY$J@YkzEU2#4XFCvqSg$wu zJ8xT>EEZ;VMP$e?Tl!x4WLq*ry35)B=^HYQD0P;j-L}rMenc+P3;46j`6_Lxk~bq< z^{oA5jQ(wt%`jak%493(8nN@f5{i-p6O{quDs?xcfkI-Y#E03NHBwxkFhD^J&Er)& z8b+6d;V1IJwJb8j&kX+TXMfk{C($(Hyl=#l*D-^vD}exFt!?$0YwOhYH-IgAMo77m zhnPoT*)x>uWr$hUd}skI#J7q+2?{zCU=?t7^2HxpJd{IP!_^%R0gJj23!CXHegmPh zXnQ~g;&!C1)lBZK+l_ciKkIK*KlOiE2sJp!P*RP_ZCJImosGLYK86WsuK+hzClCa|64Gz$Ez9seen*Dlg;isG<}>^&71{$XxI1J zN@kwlKG|dY|xFJW?^aeCK$z3_M4#$gpFE~ja6_f&1PW;iIh~BDY_)A4X zr~FhHMHvY7kT|2yl%d6wnh}V)6lxIHIGlGk$SlXOpC)NHIM#35*Ih#V1xFB_?fAk& zaYpB-W+`-$$@3uX5*Ys+0%*r7DhhIPgGMTZf>QR5n-|jH>M^oo$b6@cBaPG})i7yIR5+P#w2Dn89FeF+&w92otY1%|ZBg2Vz6&}{o zgl{n7$?EE+P5hw7qw`zO!_IhT^uADu>OK3$P=z_bLok9mDDNMn|M~grh&#KwhJYbI zyS^q+2>e6Jgn^~+=Q9}p5et}HaZ9n;PGe*arL5(~scI1#`)OP4L-zgwQSO%SPLctm z=cNeodc1(*G2W#c)7Xd@*+6*z_?OS9>aUXfFD0!0of*fJpB$uN4rzsLxn{HiNW=d$ z*W8d;Lc{SQmJ*%kv9bY?`6k2TZL1yoQf!j;M6nz>+<yZv4K zKtbYmd~a#+C8?l9wASV@#s57c5AcU{+S)mWKz3x%4BdJ-nRGQH}D! zJ%^5s=);0#GocBMEsEq5B-kXj$DLI@)Pzh1A{$fs_4V~DEIJrAsbV~!pJQpgZ3Xg~ z0CJPzl*B2tUE}jPd%{9iaWKEBaxNt`75yYwfksnLTZR_yv)M6oT^GfXc+V>0q* z0lK#fmJH1G;v_^jBw_p7N$T~;XTMu8mMv9lI)T6$NzncSo8}mp`ey$m z7JCGLn#gJTnlp%yIW^t$-n26~Av#+39_acrKRPok)!7*MA)udX3y!`6uz#(^T)WL8 zNYX2K+XNa#(ACCrzxx7qOQJP*?va2ffW1RGHMwJG6alX2ATI6;;s=sJe4Qx}tYRI* zpZ*Dth`>ZcGyGhkO67Cv4JBfH`?2Gnj5T^+*Sa;yR)ge2Y(NMnssjt}@AUpM7uM|K z^2Of?mS>;SSOqY%R%x+*T3x3^9|?Fe=*CH&`HGkG+2Hu%znwIPF5DXo=E|QM5`LZT znc=$JH$Q!`rY(#lyR^d02l|xZHWj%Wk|WQ%waSteulaH#zqym9sEyUIF)j+fSTBpG z9~+aVrY`Z1mwD*s1bf;*JzuK{2_LZN?KVizZXgSyuJCkLLicm+*+w^8wwub#NfhH5A_Vd;;jL>&WrtlPpgdOdT31a;-uK2by8s@qASO3%=C zlQ#92oxLK$xGaZ(ZMMnWX*FjDliuTv9hjm?WrQx!^ZNJyCGBB;&vUFAu;ts#oy;@K z5V}7}yFWg*!qTWVtAom_^bM=*Q;`2RQH@06cfG^uXZv;bQY|`dTC~k0T?jk1`O!2d*b>TScm}}&suj~_oJ-*xOmGc$Z5`ti=dSrgSJQ>Ky zfg`yj15KsBQ3>8a?vC!8gv5i2QBAD0Pu{C(Z=3~!C!xTTx%$`XP2hTHaICx5N2#el z{pb*Rzke6Db-JI|*y%(dX+p z-UE**FaX$7iioUyt8@`0N%?0%+JYX6<1iUj6nT1U8JbSDZ$CdGoZ9@gtKF*QF3W7n zL{x?W!B7$-9b+$sppKwD?A1Y-=kvkeYKx@9*WA5mZVwL!@Q zV}b_qJvpE!V9~X4Nj-1%0WJXmYC*8qPft(59Q9)L5>2Xa>hy5{6*?HpMl?jA^5@;D zv)KR;>~g*$DNtQ;Zmo@GN?7L-to392AZa@QHpUrST1IAGLmFVXqOdN2OOB0<%up{f z9!Yxw>FXsRfl4`4;1)a{h!OQv&Q|D&(6y$xoK-Eyw9;k8Ii7sqHgEd;NI8Sb)_&=Fagn^&0WyL5~PloY) zv*3Aaj9z-wan#m86@MGX`9;5#6z1`iUaN8!hzaH7*{3RR&MDrIt7;b@JkYvhA?1^dlw z1@sKLgLwD(#u;p~Y>|hS4TcG`J_+#>_JBc@Xco8y+>agD+4*m)P3JeJSZ0VxZ_d53 zQn3?&WCxQ3=6wW_2=v2tu$T7EA4!>(l2Kc}vmSCR66w|aA=*-TYUdjlYdOB$Hk*`* zy($ex@b2&J?SYWA09FnT4gmoH8X6jKDh=7ieh+=~v-n=SuyKYI;M}Y`3)j5#y;ig2 zTO|Se&eoQgni3v8&_&VH(t1}gCmb9*L%lkt1?&i5qEv96sX>SFnWb79{g>r=Da=VB&w}Z_4Tq7`pRLXf z!&`P-9L$r$Evl3v&ELc%)ucSRjKzX`FtTt2$z~f^xa8_E+w7?Rz+kqcG6sLpyE$Uv zX1-mMN@sj|_Jba&nsVjCD%42)QkEk68|s~M+&{+1b;o#xWSK8X615DA-|DbT6i!++V^w6Vyiw%%88fDo$FpTMZo-{Amvt*MaJmJq}z3DL7PQ+@lVWM^b@4MFh*mM_Zb)gEEcdd&F z)#PeU6wcmY1*+DuaGdp@iS}o@Tqfid>^_3W!$m@oMq_q}? zKgmVDKP;J6a=^qL#Ve*=cx{1u`0mH)FFVYaT|>|yH$P5Z#I`3Ch`eO1oC(z}k7%!=M0F)=bS5&<39=-!(E(XF#{UDZdi z@1aEsb5&L=x*@Mf*o8s(n9)G|THU}_61!197|H=<6WVn?F1ryOokqleS)Y@G50Fcn zeSn}_Z^y>QvKi?%dBh2pcM@jKA#GNaIgNYcU4OcqSyBu)#B*Qzi{ zAi?r}gk~MqmEC^VD-eU2ykB}3;k(W4X_Oyggm#_v8o+0)?Vuhy49|5hskqCyEcZm~HqIJUF1GeOtY^mMfitNpGkQ8q_B(du8G)b0CATt1t1 zf2(fV3_wQeG}sexTWEj`Rk9`tV(?VoPAeQ`%xqI2<7DS>dOPUVec z@qF7cewRKN^9ffI3hL$Z!#gq6j--(F|L0TejU|whPktDG#%# zVuW_hSs72h+?t6iXb}KPy!gik8%b4qd$CY(G*;;`!dZikYHaK2SXseEvDGnp9~t2FuRl*0m*WHb6mV0gfYHVOc)t3PxEPl$q!PR#m6 zl;zbSXQnE>*b9)1`+fte8NlUsSMtgIXQt%c0qj?%?EhvX+yL^{^~y|1yr~lf*}or3 zd-mmFpM&{(^>eSnA?N1e9wI%&eWAn#)tsM-rDNv;slvg%YrR9LS z=?g?BQa6ETkTWx*zr5J&i@CeIi^9BwsPJ(cyslJ+l9K zR7?!WtZ_G=h>rmqQVbyTFf5l!u<9^>_E2p)5z{^-sV@%Z%WC_ny(PAu+d@5I@) zV$LHwA%$%>n8F{kltBi65^w|x`^8J@HhlfZN0wLc)*i?CtvTmvmONV@YXSZ*<>D#x zb+avdb^~a2*Mis-H+ObOvkBjXbq}RuCV`c$*!Y*zD5a-a4OSMXfp9YK#+9t;cM0qj zEv5&;{eXBEhATO%=R_Co=_{-r8Jf$IA4+iVe-LP2a+t)}=>HX^nvC(w&2Ae*|1VsxyR&mu`i=pZj&JZ_t;#kj z94>`>zE$3_82=0A#Y8~sKqC=!2J^;9KShG!c)P7HPbb7W+Hs)#5I=6F>k%sielNtV z%tJC|-{7!u-_^P#TkqAn)N$`oPQ)xc?OAX1)yJ-bwpC@`V?ClgXRYVErvsq*F4=J4 zPvLxBOjm;YGJhH(jW*&<3q~ve3b2XO@12r&0WA&E@QYF#vMqswL2DqzXl$MH=#^aB z^Kq=xatybOPfg15#lh>^1#}zrRaubSt0sXJzpjxMUXg&ph+^@BszMx34+Gqo z<5kD4+}lOsQ)O?maqcy81h?Q7V%OS2k==4hHnEBVD0)tB=6rv|(N{i$8|c}(F84Nt z6fi!g*48H=s0ard1b~(h&}+&yO#@)$Om)7x%*rCRM$8WZ)-`J@XfoE|WVt!H_67B9 z`iEkb#oTMjE|Li2jsBB~@$o+f`r3ZeKxehs=yC?$UU&63q;)tonUIBto$I3oVkL9( zx?;R}M!BI}Um#W#s4m7Ntnb-8&@wFnMyNNDoSb0%3Kf~1=$ZN{3Hr&LEhvE9!c!5$ zVw8RIyns(*XyAiZjYVSDkETm(LK;yoT9SGh>XQfbeV-Jp)`!Io%wk?Kkiftvqp4Jr zG8+4(T;uzCC@!ksFpg|yqWtH~&7fL^c*M*Bt@Cu@bJHA5oKYGMXqikqzYl2=vsrVz@2gtz6;a64#RB=P}G^4K3i@LumSzKw=7@kS(*Kil8D zKKH=|yn*NA-~J9;`nccUvg^PKA}4mbOV&}|{^U(TTk42A?teadn`!ZV3zi{aA}T%I z%Srgpz2b_L+qL^;Z`1_1FfPZpJSO~F+IZLZmUTwb72RyM6aK087MYIM5Y=y%V+~aN zMJ@(83KtUcb93KwTBiQ`Rrl>#96Tth6e__PC4eowyg`U1)ZkhJiiG*QiT0+ZrW!?b zon}|A6I=1;Te7(bk>_8sL=QJ75kov_6GRcj0`ZjoKbXiMRj-wcOf~^XSm_$sWhDKQ zM#OE=>whK;aSa*UvoHI(3tKxpfq!mbV32+^;{uv}W7|1!f~_8GK!nxtvq>_L;ZQMn z`K0Ki@2liEaFqDD@|NKx+6G=Nmm5gQm|;v_D3?(K7L?EUeIX$s`moz2DMn^EcRFzG z{vE5tGIk&z?ca`LAb(ywT?Uc#vsFi|RqUI0Vx#F*sGl;^J_;>}3MCxgYzI8(0!KB* zQJLHi?Ob~PS+Wj;cH(xPd#0m!9Tz#i+xJ*RaOI}a*(dp80&5BzjlXh_9uVbmbyQU+ z=c*=vP6-1)aX8WX;bTK7-||<#@B}7vXaR8}gl-%VJnjf7{B@S_DpFSAyCbzfjK>iH z`pw{iNq?K7@gv5ma%&IxwJe6hDZzjvbfr-cuYx{l&TTnw&$%%VHRKh}s@vqk0A~dT zc;tm018nfR-K`TO-rxkNWZb|R{e)k3(O?>3{rSS8xhD1BIGDJSZNiB z@xZv`V}~_ay+$M?-w`~|$PCLUS0Nn5;o0(64+Fr>%zS{}=Mr^6s$bEPG;!0C;Sj#_ z+9Lt7WkK?P>45g9yQq(^U`a=YhIFnTY=Vj441EdsSCL08?kz<+b=d7sF0Gp1sVD$o zGtKN(y~SLG;CsfZiiZgFQHDqKnP6xgk37hsdG_{bsCX^&5c%6Y2pXVl=*e5gPCWGc zKS)czNsQ&Roac-ne&UJ-6x8!A^EcCn=eA677M?M#EErhW&+v)cG1d>{c`1m6>(|TwVG9$GeFiVW6VMyx*3HO~@{*eDWHTDT6G>3%sw+Gu1p0b@EiG`jupO z4q94^N2Bx}J3lQ%wk^S!dx3j7x;)cLfHpnWS?4YsZEqGFM7(uG$H367G{fyY+fk@4 z-~f5|iqeq93L{ycgme+F=bLolAXSEdpL8y&N&srUn>YYOrl!a;{QljOz~$+h!lv0Y~kPRG8CC9-^A#l6aULf5= z(J*5ih5p2_=7k*K4OeKE%zm-I0bCkPTaeYI8wkw#hLV_XESCtyV(9#pao#i$?7=&1 zDW!e+r|VQSRLp$*Ky;uoP5L51x*NAMA&4DttW!rLgR&HqDFX7`mo9o(T2$Npd``Z` z1Dm`!`K-I2zO(&K4*a<+IoWEO3z0mZLHhn}C$^3}%+}0e zSIOtz+8t4a!-xsyVP23O9g&a>sudrG-3U%b2P-)~HHx30f%@F~NRu#NWMCzT?W-l~ z)zBS-`9<&T<6P2@C~ErV0cRnEIGL;aUT%@cQ;7W9I(C2kWkQ|zsoxifg=BGJK6 z=&~C!^!+2>t2b3F(DIBx_|Wszc5_W4M!Y{$#&Q$i)ViPuS!r-EpKIa6d_YlW8~|3A z+c2JGld-I>3ZsG3oJylga4meM07wy-5)3)~9cxW#&%XrH2Q!(F$eB%L^E(6kzb_qJ zm1>m3D1%W8E28gY>Ry50u2zE)XaqWvLCT>2Z!mEKswW@lk2U`BY!>Qex=XTf-^qZM zvSuv!5&Z~{r<$_!}ZwEX)8K@ z^HhCTEMrCcXHMk)_CitQLD@R#lr#mp=@v;>3#IH0E^+Zk5KfTg;Z^!*yy96eNrZ$#AL=%Ez?vn+;OGdHdHR8xG+e=v_};HI_&$wTfK-O&1}@lt)GkW! zxK#ujW%`26qOF70mD`@0u%bpsGfGzNFMrlHSmL=YFJ`CJ2JqZ3#WIr4mT+wpV+9e< zZ;mMo6YSL1Y`$sou$o>*5P2=txiCrhD5S1oAlG)*E+>_Pup-6`|8>9`btl&`6-y2V|dqT|FX$PIq@6w%Xxfa)TNo;IK!Vt9Cx9gnNn=kjWXIM2q{Zfl46+^{b^r1 z?*Mays99<`ZFpTUwAX5??tP=xbQl*FQI6f(ov_@La1oSjdHoupz zfx5wp5VgUyfg}VtabqoJrP=v>5ki=ONaG&PH$h1IuH3@yA-_~5=uqtzo^FWKQMHMt zSF?j2%5p87t4Y0$ow3||%sq+8{=cXsE00vW;k9EgZmsGQ%XfYO!h%2`);InLbT15su;rTI9$#O&d4(5{uJE=rAwiAjxvw!zQcUukU>%hqUd=j+IH z%aTOx)hKu z>F#c6WCPM5-Q5i$CEeZ9-MP>5eP_P&&6zn4|A@2Q@$6@H`O{VsCDCGV~CgEW{U z7XfoPIL5!dp(4fIs}>0ts={Jkj)tEOFV(6V=o@xprB+%|Tw%1n&14s>w;OO0d(Y^S zyR}e|mpLI-KsPi#uUSq@W@lt{U3T15wTzT&&k%R(t2OC*Jts(&y(5u*ER+bJ@0@{U-N4`oZ~iB2nHl z!}r;prsw&3(U!Bs{btz}I(NmkJ4$kQlb58s@O-0So~N1V4~qYaj>jdSc>F?AoxOS2 znwvo)jDE&_L=Fp%2y0IKH{=S>&krZQj|MN6SfquZ8=znsFIHa!iuAVIjY5#+Adb8A z#P7O~kTRa5hcFzo>WfTVh_FV_ULb2C(flKi<)2bviI8~Z+TA{S=sfFNDQ&OZPsP4h z{{FCwH#B-}9(9u9QCf8y3cCG4Wl* zS^C9ttiV)jd=@wGUScFW5XlLgEF=zIC9&y^yv=b<7(4AJ(DSPD?3)34NB^5N(9JdK z8|@|xpV(b%+xYhWvRR)y707THqI`f`G};R-2;-$i2A?Bsm39MIr;ecnW$zE6?s4LI zR|#d>0%TI3G!&F`v8+?-z?_1Jf4|A$x+aLR)6Eg&Zr36zT?q~S%y2UC?z?kxsu_IOXX}5Yn}vyn^~X>^a6E% z^i7#o4+{TT)d+~ct9P7Dqh2%TK%dODmnVF9Z2Ed}yxek=F839LM?nVRcW%Np;dh*V zt-%6CwY=3Tl2k=e5{DIcs%CjG4}F=?OR0e6hwIQ+q?kOd_D#u_W@NKV&B}F=k&$T0 z8#@z97>Wj0lw1 z!d|z@FU^_(oJcU6Uq@LLx>*1;kc|!e%?l}J$23BhwzjydLsb6xvAP2&ZC-QuyA7^rX(n|;w zcT$Hn|785I0B39rfS=0IbH_n?959<8;eJ?aR|Kv1cKe=YU#ms0jgML`{&gl1`F48^ zCu8|qR=>X665xGGXVn@9Gr0D)mQch!%f4BQnf64t5W>@{$?I{d?^R~8oVmM_ z?cKu7lKQzc-#avVU~1Q4=ctKbgm!09sO@c~82r(MmG?%;ggtfXt z{1yWNw&$hCB=Jq^L$s1wY>yeSrk|S);(wQEH5`nUdzWYo$B-?cb?5|hBWRt?p{*fb z7(=@NRSOdnHL^mz#k%Abh%{414C=vp!~K4;$~#pf#>r}_)#SWtt0al-_iX8WPT$KT z@~^YS>NP!~H%~R5B6KPK$HM)avjX*^14p^+GWvgHtD0|v0h!~WX$sHyGGmT z7~(y5;7gd^iz;6>?XiVUm6HLJ=N?VWd0w#~CevL@|x|txo&=x%ncd@8fk#n&JKRaeAEl z^F;4OEk)pK)-sI!`2s!N`_E%*NyKvkhJ8hle*`Bb_KmhDet+l-gt!)i!LK?(UQ0s~ z=$v`+li!A4HrLyjkKv_nlCHcRNf&lqQs~KCc~KB;YA-gdU2jW1Q1N)Z zNseh;BUbg48cIHYwfH&Wk6EAnnU+?5^5w_Z^bcp%Q5`1>@|Mm=WCt$WWA=Hoi_Pww zm+LhZ8qlkoj9Y$XYyR5L9TrbBQK_R_e|cD{0mAWyt7PkC4x& zvVhwW@3jswPcbRBe&iI`+oMlr+CS%JEPL;sR&nf*?4RbYzac*O!dFB$x}{2~JncC% zqsLeOz=%Hun&&A~ssNX2(ENHSjpiWdUBA&d#uzU|- zz|XRE?-pi}>sTCE|Jd+9sW2Y0_cyhsT?=`BS$33K|GQ9M4EF$H&&!SF{TQ=#mzg|o zGKIXxumQC1s=IG!_K45EzVle#e2MB}&L+6;SnVrtxNL1?d`V8K_!Q?j=M7U)i-`D| zPkkVtBuaI@Q0z_FD)mjT0YXZii8hpv+B)f%5a+f|4e1IKpeKXrGwA=F{sk?jM(+og zlIi;ay3DoDMijqZPQrYGmwd&l6UG&b!5$&0{J9hXuqh8pCPRIZ3Ssw~zJPPuIM>9 zoYUhdiCcUl{;yDQTe?hxBUnN_N(zI@dI=Mh(xIyu0fB+*%9yFxpf?hH6AI=u5ueMq z`Fx*SYM@t%|0bWb{d`eG<$$idKWcqW~$>%HQ2JEh>igzD|?w>?9z340Ep?d+{+sPU;ML|skC#Orm*2{`FNkjVs4jbX< zUxtwWCCzw^*=-jU74h!*`0tEcP3HM4O3y>+M%y-4QXNhs(ZOgrpu^c|z#JR6XHE$` zZa+whr8Oaq<_2!Magzw-z;s=V=-aEJV(Xwb{G(d=zY+!ML>oUL6t_&$8Tq1wsNRg< z95?tpPSbcBk(Q{QH>%tYc(HmK#ZFyQRhR;Z5gGzIzWfsU1uD^#H_!KYzE5F8y0h9< zWIBL(2&%1z?D$!hr**SNF)o{FF~wApc{5c-GGgwmo2(U5F|w0sXbBC|usB7S+Q;|3 zo?_J;H5mNxWiu`2eDW{W4YH9CbMOT7OFuBsVS$vTf}8j+&6gg;2#ogOn7k-w2lfoC z0iHkC+_AkHoCfb^VqABp^EL|&;2$pCEN)|W9IUw;x3gcn`FM64v4uLE1ObE}@WBs7!6kpat6Jz4T3sarqNHF&o5x6V7)V7xnh zb?-)(c%;2g@590hEq<@b(Eqx)j`V6!9b21sr*9G`$76ODM@Z^*HjIkb@LRjPvLkP0 zCkxtUz%Ka+yJ13)E$AETD`HLNo{-5!e3poI3x$YI_vw5m?AJQ^N6?B^U^)B9 z$*VZ;_7&;1o=ITi(eR5$91RA!tnB+i91kE-5!g%U`1$WOUVUXe zQi4RuBJ`atDuXQ56?6>Ah^@5&dKkUnPredsq-;QH^!9D#FGn15@>Q@>fbnS0UvO|o z&}>xu6T|MZ{&;r*97!NLF`%IZ=zKmbJRrLj*k+o+g)%ClIiG;-Oen^Djv z;VIW$X|^dnIZL-Q&DdfBrpYTEJ9ZYNk6?LwfNMrF94=&U02{*!d>@`7joJZr0%prIw#Fl#Hrrf z)W{H|kxQ~;yZcG5@in&b9w{V<2cPgwNZN(2Bcv1*BB@7ulYPB54*gBhCcw!eaqnaf zcRuw-o`0qUHHq1GbN(*efopN^mD3SFch0ARU7POp8&rmQEH{JGEgeB4&G!w*A+zG$ z2P6jG<$pds#AWc(DZZoNjRr|3~~@Rm|S z3)I~{e0+TwOiZ%^fl(TCwTIX=7f7DgPsG8R_PO~c+h2w`2r3*EGh)ec_0<1{oxbAb z6P~r|^66_N1N3c#kP!E;w4#-M`rco4Mz(6s=eeydYaAS@lOD{ERAvc6r3j@iWF9o0 zGLJbtf4wB_+4WLL)>sox_n^6wb9p~}--ay0vspCrWNE?i<-CW0S~Xhx<&?Aj#FS52 zW^rDf8>DX}->6OG76aqN$oowu_8JR4eLrG&I@+APnHcQ#YYUL32v9>NE^=@6>KMf> zYsc)5MN>|un_a^EXx$Ok3X3I-x^D%s;Z0nOGnVZKz8sBrp|&i$Y@?1ic69PPM_YBp zml%UNQ?*f=oNKetg>550oE#ynyD?mUrt}E8Tv@AcLhgUW4nsmjEb9jFqw0t7AxT-^ zqM{P&hx!2SngadB3mk+lGiRhqhAA`b;4rLciBW}Q++il&@twlyfcC$BJMUInkKEdt zf$+WvltXEXA048R`8Nn)nQ>XPS|tNQ-w?RK8Uv>`2Y zH3p0leM{|!p^s*ekFA%3P))JNS8$3?dOq{d>&2_T%D$g@pfe`yAB`UY!$2#P%HvqT zozy%(<`}NW)6K=RZDo6i2Q)dHja6nr&?M9zFKSVu7(wXia#9;=o7`(Su7N?u?^LMZ zg(hUbHX0TdhRF2nxA|sv5;#UtU(sC}v|cALX_AdxL=(Q-xJ@wuN!PcYBxq$(F)^8s z#*SgBUrB>fd93+BpGvccP|(dTO2!zY%Jj=8ka2<^8fP+}6iRb{KbGU%17gD}G@u`(nPb+h0*VT1+maP(}v~0|Nsy zeOnEHV`Q2nDJiMJw1;?1Z~S*p+>{A<@c8>pB#iaQ$BTm*5M7i%Sn)Z+ZkSDY5z!*Q zHz9Yc7R{sO1UscyDnFipk3gE8q>>=2XZ-W+&0NP@pS!1mFYPr&_;hcD9WG}r1F3`-JCYx3$vYzal z4dQHIF0MU?ppJiu%i)!qpxqxDYuPQb`DvpFJKB zyMAajZujz&HE@%zzh!1b2x&ZZd$1gO1n7Zzq3>uW!To=WJBLkX(5?#PQxl7dk2tnD z_s;2;?vq%w7`Iz+Oi~?4Mr35ZRIkkg9oL};J6$WMZr;kZ4+3wC2U@Hai=1M#FZGz2x|7i~0iD|RVu`!&R1TBd zEYiC^XLQN2;_dkT?d|QmGQ%Vm?L@|MuF{RMOrq1wUk*QT)5I7NxT)wrUz%+$YCF1j z`chb_@!bBSbe#IGYBRqQ(5^1}d7gtQc_Qe*()Tt}l)&3JY$^!wOItid(I1;hbu~Ra z?lT{vftyL`F$(IX0EB4|FAN7^!&4nl>771IPt1C;yX(Tb|9!IM-I5)RYu7Epe$G!* zjea}!zA^S-$=CE_i_@M;@JBvu3kVVYNcQF)*1K&rlt#RNF+loj|Cxt_PeGxeC-XjD z4+9)*Ce58a8rPS&R58sfMXV&dGV0r}bjjQpV;K5qdBA+DF>!AS$k05Z!Rbv&IB*E4 zff~p=)NNhoRQ3OSfVc4+-WFTiwoA(-pE)|;9`dzXhEI8zg>0&4^B{{w3ob>ZI;gQ; zqD}BhvN<5-@tfO+z#0xx3hIMqg;Ic~!n?tdp0f>PuIw(*+`}6C$?(ME^|%D2o|qns zpjqj~;~1+_^sxbvO|RWc#&fgw>*V+8GJp|j@cBCj+J&^W5{~ox3`5`~A|?MYV9r^7 zf2|$}D_;uiJ?y^S0HybarS=LSHlQaBooYDX zJqW54p#IvfWnyAFh&bVH)?tmYg}r+q_X-NdIMN0QD5W)hpvG12%o^Q5>1|5NPy0JO zP8We79!!y+wrDu0Tr#aHlI9JhsCj6b)ZkO%DtZ8;JZKenI&hLbk8cB|g?+!JsGx$& zJ{V7)COZHDZJ&bg4p?|B6aR#IRDXg%9nP_0S4Un0w7LgRWbk8wbJX9_Oxsou^bJ!q zP(zA*d5e_lkcFp9#D8}77p%7)&i$d-boprxa7k4E@nw1u)YN*5z_ii)%UZFUZJf+c z!_p<2EB*ZDFjVzVokE zeVnn#Agmt@$l{4&v-ug)8O*;Rs>C;A5LMj+kg49-W;|xF->AXmJR)>JP>850HCV)4 z(xS1MShv3)AfDb)woq)(Sk8}+Hv6R^%Lv8<4#yn!7bBr)@ifP7_pH-N9?lRV?Ybb zZH{Pkk5YM^q0~Uj!~}H|LB;oZr%-KN0?CJrkN#@8VG{;K;usoCQMfx%goS34Qww1F z#k`ds5Je>y=jbA@QF-f93|siOR_Ir|)9vz*G)t9XU^yuWu^lNMU?^NHieK_j$UXFtDDl zc8h4}Cgo>G=$4O7PUiEzzl?j+r=h0nW^f3m4C`mHBE7&Y$QBtQT1YWbC(+1-o;I1B z;j;f{-9PYNaJ~So|BaB8Pqp1po>Qp!OZ>vKV#1Q7>;S56CsYkq$bEXWuaCfxpLwew zN`1c3DhWf0Obe?uzsUGmof#75$wHp%#E%a%$ol|{4sa>B@k&N(7o$FRGeP!>H)NL| zRVFHxJr%ZhF0vhxF%WjD2DT`)*pN3)dX2LF+-fUyitmZpksPpy!(7YUd@Zfb_ep(! zxTxZ@N`gg$yzWMWa=mzCQ$dxO$w zE2dm!$`jM(5PlP$-Ig|Mf4KXiB9zUVJP+wD9DMi0@suOoX_Zi_NkrkY+R&I>2GjcE zD-le?ib`ara@>ZY3smgAeiY7r=k^bIwn*+#1N)dqX1ev%W3J_a& zmK2IW7WirEq%x{HEp?KP+$erx&6$ zv1I@pu7r9CI(FmZ<5?BTIU$OA8MLA=h`^L=(=rc*HeR-1vJ0m(RS7%8#O?Dtbir(; z(Dz=_Cn2QtfdK(VGs9vB_#AYl4-CCPGBL;8hw^VVqFHpga^-EQK9G1B%nkG zjCnNp7%8}HahchXo+!D5b*GH`vy%_;!dfj`4+%7b`_AM(&(jA2&^gNymh}TVq10iI z30*hO&rc(4wsVaywq~c%7O2&flhHJWp|xTiIGIo9Cj-uz$i^g%8_GLx{XM#n472Mf zA~a9JEPvegdiA|krnB=x3JEU}3gpt?2#&3F2yS*X*VMj#FIRX;5em0O@8=OM1ec4@ zP5S2Hkw&pRuWzU#pQ&!nH3a0Co_;~xsj6bfPM<}?&AQ|)fBCd=y55uN{&&baR3OGn zl@<;75Is}JmTsi$Uo%(VkCwPs^OLttL$~zM?$K{YgBnCgd2=%#2gZS9hYrg1X<{BJxxe>G5#$Faa17{J?-2Rfz_{mRwTSHGA_e z_Vj>+@5z$mrAkPVMIE7(RFg^Zsl{17(~jcd-NmXzAG(3E1MwFv1r2=F27fLg8u!uY zHafP~Mh8Vi$!Vb}yZwyWeb?l?o{vG|1+hgYiC$9T2i&S$R- zP8cFS%$JHyN0?z9E_vRn4Q}`M%Y+y_UOd)3>?Sy=M)9B3ifd@4hm4`5)!kU zoeC95H~Q3IlMZR2KEJpKMJJm0@Q;`Zm}S5VirqH|U}6@40FN08?;vh1$=>>K+*0|X z2q+kO?A{kS0qAW%H;}U#dxi^%Q2B2uLxDxuocB;{+ z*y0V{aA;cbd9V&ErN4R4m&#-N+s7Bg=u9760lXZTg#x6_a0*us5sVTsI#NH(+5g3Z zJ2~bS6y&)3Fvyim4;r%z%KsQP-Q*qF@!TB(shSi?Lqs8Gqv*0kL_~(a9n|6c+HZAC zpG{y3RkM|D%RlLmyiNXSw;4Pr_99{J`F6)21%i(N%;NP*;c zeSHc%1SkG4U+)ZfK)!bEy7U>QzBdk7_^NU+4nb{|B|}tzq^cE>2YaW^h%X${=nxTH z8Uu_80G0srh}W*(8j1RTz>ea3F)t^Y_&b@(0MO(msi22@gB&->i87-EL47}hLh(t} zXHZ?^up=^Cj9Q*1&}6>9zaI&Mc?>;5q+;2t$TK9VS4t^7}hh?AH9q|D9SK z=F2|2&Q)m_aa(aP$AnxUw@kJ4rWAs?RmKo6Rtv#+FGr8muEp4ZE&M$3>=+zV5Ki@DVig4J$&^U%lArMBzW%{=3KNXK^c;6W4Se5){1(r;gn7E<1hS{*Q&ReE|rX+<*$<}`TmFA4HM66yORd>r^Wzp zMc}6SnArEe?Ne02-2|!^@{8P7^V!}tAz~I9+2eMckTZ{E1)apdCd_3@g`fSKY*;y< z{AbK>Z<~9Io-Zob9x8o(-|}Li%?j)|xFaTuf6+=$W4JlY_U^LB*sql=68)4o}oEPFCV3LNcxVh=jR9Tewo-<%lHFi&(7$NGNQi(od!@NK3eOD2;+-p45 z0+Wk+is4jFwQ8CrOgr-MlNJ$&1?Tl8`$J2|I=_m=(i(*Vm7=+god!Xcjfarhb>yAv zEmM=%0o|>5+ANG?Mnm=XwGr($(f>`bXQXTxW-5pdtF@2uqkDW4BM6sW`vu1)&&mWw zmw)~CUpd0$6NV`Y+;A^sRq`i_lkSZSlvgu<$YrV z@P)4pCNRmTaK_c6`46dQE6Kd}>3jq7?tl+OV(I@SjMFP-3O_A5j>?in1|nDJG~p&S zOX{#D&0DPi9m0xuxo#U{9)3=k0J`9bU8`WioE3M&;+Lg2`7-GZ0Uv3NKEKjp$We&t zb39!W+qFQ($9-fPM7-Ko^-+~6P{yLt;&OVoCea%h>R1Tcr~iZ&lwxw zfIHLWO2%SxFhxbl!Q%*Be{$A#s#oC`$WtaRVb*PI)Lr&qS3vr%)bQMm7?Be=cT*5) zy;LkwQ4m_XTDe`;P=JOGd06~^7FUaBI2{%S)v-ST`h z`UHfj2zWCz5fFklU-4KiaWa3sg$&sC$m1rZymS8<;NaQnf$glH5YzE#puwBxobilhv2BFzMy-|gd!jswhbAozO(wI zMhIA+oZmNuNtsXn49Re46qTSvUg-%87}%qbiIFS!cJ=cS5omKHIyCYz-5+0ttgRq* zzbm-133(A7{__j>iVbU-f>z$uAl|7F#GWiEB5<`?EiOz`sb3w|gd7%K*oIc2>_tS+ z11>QhjIEf8!@JW56%Vok)G&;|MGcK&=$ZfDC04p?ThtizqygJ={BGa!G<{NQB@g$8 zWfKm3k&a$%W5NasUMrhCIr3n}?CJ41v3B>a$C${JdiOG-q_R@seAol_$?Ko-pMWC| zRF`}PUVybs-T@@pk~m?Vreb3L5Dm<54Ncs+?9E@_UMfMsz7Ign>UnbbVA?PJJ~zs6 zt(XE*FDa*%7=I3FOSnIw2d&732?9Sq>V66nmKQpYo&NCHq1Z|1*00#2Il!j57_C)S zu351H?ieuLM~#!NUw5NaHrDE~xIQ7@3&2CO+j zQ)Zdo^}tFgXU|6n4FFUhVs5J+-@wZ@wE?;_}$-``)xWt8k<|;lhF+afU^}_@CCOkVf<9k2{2I2^MR*t%jbk`LS z{J59QVWz_h_{tF=j1_Yb0IB_dA0;9UXp}gE7F8pPv7ePf!DPpGAjJdZ9|?>avr)ju z`X#r@ENJ%=7fRi0G^3{%{)cwyw$bt31WOw5^@DG7-=Igy8_ zoi1&M#{&Qz;E)z;PyC83EP^T^m9)<}PIWPPPGK7p}&G+_f_?_yKSTnNkY?%PNC=$uuq*BYG<*h4Nk?MJ^&gE9=P4+g*4)X9AUxY&X9A^3K`fWOmWHI@Q#I3=fNi z*A-%qo1Z8Db)=6*)8B6WxLGd%AQwJolBY0>*$M%J?x3oQn9A`Kq__xhIR;ycZUKed>H?OZl#Y@<+9Ul{kG(wAc!jb|^j zBwOcVMbVT{n-TKXZvB35O5Rs0xjaAKMZEH^1=WI%aI1_p`LGAFg+R0KB53m_mMx;X zf2<`tk9;^$>!0NO2wT=FyhmeBllsmD6!2yzW~*+SQApF>V$i=qnxYkxl}gtw&K;dE zw+;YV2jHt*pj8+PQKhREz~~$ylKH}!%2grUL3x5&3<2i2#G|eKTySU*{_<7EGxtGT z=8sn`oe)P4U`8r{UmtY+B7~q>_i}i{6|3{X1VtiER=DS4b}0{*CTb}nHuL;QVr~3^ zy46fP%(uXS*nJegqh;mA>GOV1w~?WtO7!84N1M3{HC#4!nF5v(2J}X@5v?n%XOp!e z#!Ts{o_Xl|LAa>fsh?S5-{yeG8QU*+e-?WkKY-0YIsKPbNz@Ev@fzo%9J4!K)H%?DKpm9xIV*W47fnPpo!u-5rK(;6(B6r;#qs+Jf2AQ`4Z+HX* zT0Yl!RK~a?n(wN+>-uJoa>`3+<{cg2bf~@rJ$Ghn0(gQxd#GF%MKm5mMr!KA+f)Y& zZaP(e!Q4-T1Ns*H->lkQyCD}H0~HMx)2O}(xj)nFDpK!PN*z)h_+9oN0c&X3YttmO zNBJj10azOYJ#TzAqj*YRP3_!clwgnxdo2jqN<d|oEftHi`ePWfL!pu0RH$EUA4x?`?eP_N{CjT@DA%*bQYR&#u^rF z24VL(El_&CfGfT7%i&`D^+Gz*iEY$9#`*dA$0SuTiI61(UmJi9ES$mqjkncIg-s^7 zmaML%wAOBiw{i0e5-3c3j$Aj=S@MEPM!rd&h|jbn$zT$VH+F1HsZQ&N-6Q~|mU-29 zfG0JeN8H}WcWh8-7tOKopqVyH)=l31ay%Lo;{2Z6by=N^{XxWamQH&R6qOoX37+9@ zG&mUpSfa2s`!o@6acoFa6L-w4BnOJ%u>Mgza-zv8g!6WNmbHJWeq&~vs3#Y8j09(CUU(^$Z!(NHvNQk-tf4QcxV4VAhnkh^49SgJ z3<|H3LK?Ds9~w=}eOh#^=#Nf#EiU6E&rQD|Zh3~v<|bKFS@||k<(p=ee=1Jc_Z`vy zC1LyEE@n6Ub{B5l&BP<*1$$wFg}ptctJ|kSxo@WP{i7chh1|{O{8j=zToLvr4)!XN z9{n`?ySYRq+>?o(-94_nECzmV@31ciup%{tIVK`48GEa=*7tc{K`jR4YOqnL@UfN8 zbg`jN0ZCtX>&3e05n~t*R0aonKzQ=rlQF?A2a4Lp>!H2-Wh_g!FWLmUSj5B3><2j)(@*U7Lm0yzh?2xoJOJ zXN*Fhdo5)Q21Xi|uG_~W`-bLY$d|m`P85~~kJ|v_gH#&Zn3JVqI?KN2RT+_I#@?X2 z)2!RBo&$=o&8+utfczUV3ZW#gR|64m$S{u)A(`Bdoix*JS%x_{CDV+(kMzo)k?0gN z9wiWbr$|9u$D4Q{)%Qng>Rf86fC!N(g7{_dHG*cgUHHZPyd}Fm*0q&oZIjpy!WE~h z9mAoakVj+)z>^dUSIL!mFNyaa?|rYxtt$2&3LGPi(_R)QEPPO`sE0$_(evsGb6>I) zoPW@1duGz`pGD9tYFlngceP!SIT`r{L{?%csMv~m;os)}Kv^+3EvCY#QOZk8M6(-b z{$4EEn?$UVh{du~va+%QUypz`O7n{s{Q=+K=a_@25qf;oUI159M8)pz4m!;QJM1LT zcxD&_(;u3)|94}5FGL+xd4FBncnrd~e#Xo86|MToUii&+*6Mh@hgB3hUh?Y~H5V_^qbL029?ojNYy6YVizZTX& zgL?J;0EI{&anI3L55_z2{%S2X2EEc^B8xva-eC@y3f+|X01k`h_&~=WR)d6T9r+Vi zi6>~zL#8}8Eo;S27s#(i&vGzBa}?AhfR^OuTYPM}*1-x)39#U#343dqeQ%m*!juF} zarW|z_4E|_m2*S0?aF%fk8QwatJj{kWwA(_QwYR$uH|Wn-sWZepc7w!l?ei#uW~H9 zwAj_c9Xj(Te#^hf{yTPBtW|bBHNZg}mG9H0MW>Kf`RPtE%z((;H}JResdr{lJYtFi zRCX-5qod;iFZUP1rp*jE1IvE+@RA*oBDDpb$5`UVs(qM)ojv0(tOo3G3kt*5SDFi4 z5lrQ72NU*Axg;~cJUNDBz^(f`eDD$%xBp^Nfdh0E+dSId0!OX|j{4^2rns*_& z1mNKPR`4LW_nhA6f@_Oeasb7|{E6>RCHlXA9?-S6#kOf^X%$lWdyU!S%&Ma{lj;tp z1JB7u-buC8mjQgx{pDwJaiV0`u_Qg8qUrMyF5Da&P?i*o=*2`vrd>RfB!6_d?~em6 z139nrBtGwBBtaIDFXvInqC2G^av}9(_EfvK14}zys2QuCz3LH$iJW{|T)#_am;5`W zrUkjJrnO-pjzxqUjd=wxA@cE0O$@Atca4 z5!}Y6RgLFzENiCCoLE%zH$Ok0Ri|<{UQw9O)!%om_HYc}!mo@X{qjVeH=h5EpeEyB zPRo-2&VD>^Cu%u@uxQ5eGkwM>ia5QS-rCi5E$l3e$gU!FzzM;PBKhT*CH)-?6M|R< z(jc9N!n=GDp)z!I^vEJo)UU54{)Gvpne!fc z6(tt$@qk&u&RCz-#EJ({!EJQUL(XkZ zYYiqgw$ath2KHPUy^yv8dJK_CF;ZPeAr^Su`%zNpkVV~0At~9bS4ah~>z@uTX>!J8 zK7w72bis&SOMg*|t3;fLH$my|QnDks<8kVW1AiBe3iv#_byeJ*!znO34L}CFd(OJE z0qm&54n@2jrhIrbjc?NihH5?_HHSf3hEkEKH`M_q#1M?hzOl5pt>I4ZIn7Ck4kEgh z3xCTe2{8cN+5zK@w5mkxEoG(C_D5joBJ<;i`3u6}m3y7of|B*LN^rJ9VrVdtCPdOU_I-`tRiNRp$$bya6R^O2VL; z)=Iq{ja>C(YsZRA+AiL&4Nv@Bc*99_T=7QstBL4#brUz&2R?Y)m|k;0sLo~EU| z_~|YA-&n#`tfTWuK234rZd!r~hX>t_0s z0a+~8D~n;rrVn__u@JltfcnO~PD|4&?C|jL&!0cI(dR68e-joV;C`F{4$PDGhw?%S zC013;v_i@^dTK>#&)n^Rs&2`hO5ptPqLflC1`mFj9?8krx&G^_%AuPDH7np zC!U>8GGXebAKq(-{yT<3W*)Ah-KEna)Kz&xzz$i>d{7LVnp`oG0oi@9XemDpm^#bv z4es(jK+(zH(Xz0Vt@(Uo=rf(A#|wX#;|V}cqf{GT#yQD{*Qdq}7fH#8jJ;vePHeNdig1f~X z*m72V9@4>54I)EcB?erJ;Y9R;maK9Kxl1jcQv--j-4xL}-H6zfiuU5+IqAUI~yy1Ar9BZt3Tuxq!n~e{S-oK}_KNbplAlT^0mmyt*BC zrjrSSxL9Fp%z*EXzcBzk=J2=oBeT)x+F2lKv0R?N+Kdq)!{ZA5J`_5|P)n_%k)`?X z-P67H0Pi*c5{~<<<%1QS39q@m_d@#}`g2$`MFMQ13D9~8(}iM5Zjgk0)8#`^QRW5D z|ADQ|_@BE^2ZVpom=C1@ITJE6G8Xx}>)s@(0HCGuQ=y6j{*}k^hi4s7YAp+ta zm7>e5tAv3x4w+RNPDrL!IRy>dH}K@(M^J?@#NU;pNhjKc1fhtt^9Y@%E;Cc_Ze{lN zqgZVBItfXyO6^NA^?aF6tUN5sO#LFoCQSGyFaySdVpEXk&S>X`KR>OcYL2_h7KL%d z4C9i98BIQ8SF_052jAPz5fbN8)8PE)3dBlBEeu6?X3f9FMkreAEW_YwjAg(Yg9K6p zj{*Du7?;y03<9Ro_Mc>FE`El0@6LvK=vr?#A}oEhp3aBZ(6s)0A#+tI66AvM0w@xq z&)d4*X<({YFnX7!HD)XWB0*+GhLGW}up!NI)P)v^dMU~49`d+_kxghA*EV>4zb2k{8-%h{25E2qH8$AXup;Tf|j6M0V?{i!BA1E&Fo%OQec3OT8 zP_Bc{+cGN4HwZo<6LJxWU|f}hxfE4ge=aU%1OcbvqmdiVn{%2- zgY|9^l)k1nGA6VvTttbyM11GQN=@>7;Jo+n^(q_~5Y0y+AZs_TZ=L;+7S3dv&Z zglyO}@pRRB1nRhA*juvFj5s@gjXCfl0?uU+n~(3P^@92Aq2&m-4Y7X)ezeP<+=8>b&K+~k}`el%a3zNzp!-%!NIh4LMDsz_aM84JMa zGvHsa%&J;^;(zajp~c~gM}$km{4~$}3D-4-IVGmXBzLF`D&hw! z6GU(`f;E(1$X=JvqK==73*Y7)Wei13NWcxEa3K>itQcjUN60O5@cxwt*zU_Qz^lUk z_3u&rFF^zfVGwc)v=6?nC?a2xG1_Ev@VqPWjzx6S8B5T@0v-A@V5fQ`IhZk1u zR`cwiP~HD~=RPPe!7!oG1m>YXt3uLhx+gwftgE5nl9FN!O%~jVAmwrlg_07x6Fjg;ebAm6m;Iw{|8(V-*`sSD5V4E9mBB%5w z4Aa)P5T-7e5}j~gIL)&yAmpoYp=L|~ZI9%NN6(`O zw)Xx%BlzI-A;`*@#?Uu-Hu+lQ_0Vhc=Q&q|M4ahjPY;i0_j|b9X_D21ysfX2lFatPc~O27K?B6Kcs4-SMCT8Q(d2zW=;C$K1E?c%+1Bt?mh>H%5KK!$aVa zApZN`{v0O2h~X=rDC@=n{<4kM^do{&n}`!goyO1P0RqCY_;v+_B7zU5UCGp!9q)2d zI!aDf*8O5x9jvMcY|=fhQQxL@I()qE{(J+3ynNeBQ2t>tdtHF)Z>R7ctiU?#MMnT3 z7@caNn9k32GwKYFgx;@)^r>tH<18op4-2j6oU{QR=k}J;Bei!Kl?0F-xY{mM1J6`b zrXt%Egk$@OjiPOBnMb?UTnzows?WI(0Mrju+=D`7ujm|hm@OhCkTwA$(209EGwnM> zamodJW7dzGa@celV{tywtu8puBx;qp9G%;fk&(%TdV?H4cZ22Z5f41*OfB2`U(n)h zzqL7}%k(;5v=1)Hjk}Wh`0>%?|bteQ06*dKJ$VwPIUFkUc=8EPbPCoiUs4K zjdQy*85_Z zM<&JjxRUw|i_nZHI{*4u{?@!pQ-L&qA|}Rx^WQP4b9I$`g$^i=rK(Sb#nNoDzQLcs zeXX8J4?v{Gl?8>mfb%{A@kU1=1Wj(+dr9_K8%7Mr@0D%2oB+e=0GNr=F=WA}60at@e?yl^ zctM>C+K-|yN{rcwIFX`pF2RH*coR{m7P;CEQw-ixmIfTD(!kK^MNtdhlpwdbxVXOi z`Bo9I#eno%4C6t_!;rwq$1gReWtNVhS|dX}F(o78+k9Yqd268%O-Pa&zo=8*xq~*S+nkTR3K{+w2(uBO}bvH!;kkTU{IZ#FPY2 z?i0m4K6^I?(<60B;Trv?GdAhIKjcfhWoMgzXpP9W#Yp_oB>%2CsGi&g2TL|sAhIAg zy`QaCJJ67@7H-}aKs0MgcVm#ZEt@X93ry&ce_$%Eqi?LY5W;fyb6!m$&5-&QeStu^ zEmWj~eZ$?76y(_GKAiazCfV2cEuZF#4(5363T>-}z|ac0Itwk9Nrxti8Z+PVwuhl- zQeB?n$UZ~qg$*x*d>z56SZuAgjV7h|v!J%;o%+g3lmXI#womOyn| z#*3X7%{&B4cnC*zJ~|D|M?eF%CUxYSG5!x@Zyi+y)3=R+NT+nO3F(lK?rx9{0qO4U zl9Wzq5D@8=kLi;71xu_Vj=R0P#e@0?vV0;8? z?qSiovMF>!r+oI$AD4uyiwl_69+gQE_}e!bBc7g~$`Nra(7>cz#pf{sj6E1?*P8&i zqRe`(=iM96%RRfk4$FEx@qdCD0ou>!CIUbJtKiex-ipozVUD5@=PG}*kkw}+(g0OiW6r~$u4|zOl=G3_g=1~w3a0J>2qCf&!O4N)_{ib8q8KmfG%NI3%?jl5 zGj3(D-8o#C!uJAqt~=(ME9MmLLIL3Q=rHM4lp^_szH}mCAC~oA`Tr zyALL4h30WOW-VRK;VBrghwM}E-y=xx9S}oi_9?WT z|GU(#5DWG8gq7jjAjs>0<%rldvar*^pKX<~_|tj5QR07ciUQwLzmDOIgAh*USgAEE znqTY$Oh+J46V_avVJS0h%$#kuNNixXUF&G!Ajqjgx{+lR_C-F<#FCWG=+dL+up5g9 zG)O2Fi%5GfUy4q^2N}PHm`S2+bnNRrCcw$Ue#u3JV5Y#3|J@)j_j2!`ibE(Ko;#QR z%I9LwejCM+geI=_FwXzTkaDx>@%HL_h$D3f;zv_0j2aEI2YuE;sO{LM}(>CK+h>_?C>SIqaDmQSa{VK$i?q1l;}oE1GTrK_yfP za9DdG&11xYba4X3d=cHO-Pa1`WB@WcbsFph0A_@VZ`An_5oqmw1BB6!0fqqjR*A(( z!FVbXz!eBY5UxNmaN3BaCJ_fh&{I=))8!07M+I`Bu=$*S%m2-Cp)-Z8 z9>>HmgqQaG@CLtZ)?0E}nMSmCp9UTFBZ`wyxTYdhDjLN)F$7KygR=Z`;`}6~LRabD zMWbx7?(k_A<#0`s?sg3OgG5J-3WlT!+$%LjaGj8Df7La2IN#=|`l7${I@J45uQ#<wQsxvQM-6_=$sm2Nfrr3-K0!XL!LmmLR za!m={p7$95olLb;N7$y2=gDTNISUbwtHnsK5Ri#I&K>R6Gg%$yT>BsR%QQG_uIjDU zbvc|fuKe&*cQJ8QVegZ*n*FAkaNcHv7A|2meW_XhqZL}af!SKJzSwbT)e%m63>Lk?-9#yCUH)2+-D+b#ygBsQ*z9}H$kCx3 zS(v6S*gy|f0tinTOTU3P7mKeyTZBmg-X{_1p;s?GRWKqf)AO#5`93^UKI!Sy`_0~6 z)Z5+7eBgLx5Gv_fI+DSzR}Nb2UK`Ptr~ck-sHQ0cDd#2$~5wK zLVk-_XuRC;o8FR7xH>&KW|V8O>DNa%X8LpH!10Yzesm89vjPz%eu5gKraWFHD6Taz zA99&ECQXwN7*{K61=)(&f*kAO6uMVNAIeE;zZb2ih~xC0Tv*cZml4c=B54`-oSwFF z>K%h??Y_^cY3}tkQ0YYw{mto!D>3r-DJG8~rPUvl`iQ zNYEf^hy*6dgs^2i!*Iy_^h0j6DeVe44SZ~*|C^0R!Jm^i+<6@)k~ST6XK>5eESaaw_;?@fGqo3_ zUph0Y%X>|jdjf<3S0Zlv#4mh_$>j3qJ& zmRJJ#+|I*Gb;{u;xaXE(+9scK{|C(Xo?0@uVVK{MC`!g8kzHp+WeRVcC+t^P_QJ_E zr8P9oHhv9?iyO1 z-b{>z9BVZ3s`rzkXZEgU{T;e-7YYqG=f@{JTko4H-`2GBlJ6|#8s56o3-nn_FPWe{ zUYYm!EFS-|uIC9(E%~*5bxexN;jh>~m2D%pT4^)=(5{W~(y`B%?L6W18x7@?*O?SB z6K7nkn}?y3k-oM0kRv!|ztUt5nq(Mz{`{V?9u60g%s2ge*o4yM^JkT#Fri_yv5M!L z>j7uQ&3X~2#2Ed|b_^uGl}ew<%NS7hjJqgZ4sTw8B))^wj@GHEsdlZ?L~#mF=UI%Qt=o9_Fg5kss$TB{{fZ5 z`cukdPLC0*7i{~*C|@A`W0{@K9D0>-K z9;}?4oXY;a797lfZQf?KhOT%4@d>^BfzM)*-raf_8S`>omj=6eR3o^OFHq?L(I5NR zBTTv1QNCvG@NY|l&eHs1uOr}ol?=PUrKp(^qVpjW$03YYI?-RcbomO(RG?MFKsRm~ z01n94++R-a3}D>>vh1Y)j3M{zo4z?18{kui!}3SRHHNO~*79CF2htVE$<i4jD5Y zJ2RP|t80MoT4>Jf@3TYxhRbTwA6L1XL_o* zBK9DWp23O#58xICF4o&w^)^f1TU@9)q=tOHh4{+9vfn^U=gb(HDjEyo7}@q;r%~%3 z=!Z69p|WIWf(=Z?#NZpjSVH}kjXS}K;V~Z=$MK#RvX`vt2!f1He~pfexHjzyzo^M& z9E=H0!eF6?|LC!YvCfzcIld0(8()A*V>RvtT`dDA4XsA`>k1F`+6taCTJ3%Udkwbl z7aILM(2nvDDvliQu$^7o#mlA+5@PfwKKT&E2_*2L@ybEm#dWATYCgyqrUI}J{Udy` z)FC(2?aU~ZYrs4)Jpdo^dyz!_eBahuVF-@BL6U?QrFel69j_W3htI3Si}i83*ciKK z$x#*|Z7zbJNM^({3bR|!ezyR!HD`HVFZO4vHDp?B)l*hw^+BNMmkuU$^!;C14$Q?D z6hYU5F}3X2;m(Cg;@Am}oEX;v2`P>C*7;NxeIjwP^1+L*j6=~A!fBPFZS?P_V|V1q zAGwdiGeyil$|FKZu>^Hgmz|?`Tkg)x$^4V4v9A3tE|Yxug#3-WyI&t1^h8rV;?Wn9 ze^rl;H5<)-ob7%KKh}+cU$I_iHG6RBH&RLb*K%36_G{~%;|lJ`p={H@`aZ_(2=T-4 zrSU;u%iHVxF+YzAb4_$5Ptt=U<&UkCtyc|amGb&kvt_ng2G+x^r|=Q7VvCz?OlQ!y zo99`po94IdQAHz?i>K?&&gM$lX%@po=cC~rf|XT-gN7e=$9INIwuIA2nkd z2z&bOJ^On@#*&}gbyuftwzMyy{K_WcR4*|((Troa{K9zuNz0(Q=J)X2U#?H%#ESDdr+3Vs4CXKF+m0A6uHvnbf6LgA zsOYYJe)Etlusg-Q*uE;alJ2RhU7+aRZ~ihBSGsSpEwYRO_%h@zlY??d1@`c90>&cCwB+8IKZ{i10`_A;CfPT*e z%}H7qH>uNVixrcI=wNr3>%-50Y(-F5E=-p=AS~o@`eFR}`VHVFq>#?FHK8>RoogP| z03aa*QUx<}^R))5e_(P1mH2n`o@4?+vV}nO%75Bv={U-XkBwcRQ3@2$Q2Rje+p2F+ zYo=fx?`_Jg--3V|CtG+N0%{CBtzl(V5P@QtjomYqz#C-f*4+Sfuj5{_4g+8#n-i0^2j@x#BMrF*(2$HGusQNVE@^8qmRh zBFkbU`3@*0^{S14hrl{_VLbFp&?O2XM|k&OAe`9JuT%1O(?#-)Ac(D)!d2LlTjks_ zZcs!_Bblj(kKv~$hKhu00^2rFmS7b4#PKvPPxJX18cvk!c#iWxx@#rn%&Bek?by!H z9IeNwOg-jKu|9FQ5uN=ZUvjd$$X?#)fb93+#H8}H>kWJ*jh?%iK}b&xqBFs zQp&;Oq-D)q_%hUA$I21jK07owwOM|nWbSu$dETU&AIw2Yo+laq`Q}{$TaYqlv^!MM z5N^gLhcV1R|9Hj-F!l}j1oQLznvY~R+e}3DT&y9YRuJxy(Hk+MH8V4HS(tBk+s7x< z&$I=KQfA^46fbkdP0Mg_D#D2lCUVaAxM2_+KuxeaVQiGr?r33LyQRy2Y2nu(_ywy{ zHTr>fA;o2Hrpfw?mx4~+2fs1W6(q?VI;4#$ax*%Nno41Vrjev<^H&)>h2fh{?CdM8 zW=KxQId%MrEoLs_(UR)ha0f>hsRKO2n=QkbPjhgW_ii1B_JZ%ZcSjlu3ExgD#mLbQ zRBPMo@my9;Q-qNO#w~{De9HXJ65E4sOW;8GTN& zeooF4IXf*pz>kVcNQiiS2;+^}+SF7SCOH!uAyvhQ7c-gBHAJNNwp=B9IIuv+Sq&qu zj1y2;yfuvt-xY{Z68Z9Z(JX^i+bK;07V2xPN`5GyGCMmv1Drk6H1)SQDMSUwQ#*JT zOi@yixW4ciE2brbB0vU5zR2ceE=uSd1}X5fsR1CFACZV3Lt9%LEaVJ9-@gUTAmdC= zOEbXd_|59-DwSS>tct2?ZEdX{7V?jN&D}1jWMOq>Lsr+{mw!PT2GHOFw0;qtJz1qq zuTfFr-A_NjvJi1tVBp}u?#dQ5b77Z)o>~AJgpR#Z`~}c!x$XHEx`^h>17H@5oD+fq z#BrG1D>MwWre8(GBqStQSQh}57ODT%+f|P#v)&cRq7OR8U%hMhdl(-d2ljBJ#hTDz zL2c|*W<7d88>F(8cK`OcVvqa2q{Ki-2>E+5=`;#VB4L~T{d@zDJijLo&p&bQVXg>; z$Z6qob7m&?dJWbO3RdN#$L6UYHPJCl*Z8w6k&S0i{j;YGpSWQB7ccDTiFSa`k`OSyvPX<5fsWn)@6uu_9(9zI;Y3T{+!xVx+?x zBNFIZYM1`#I z*K~a>_$?)M-Vqk7zlk<&8@(Q5$Um)bHm{`;FXq1s<3^9r>g9#2N4$QkFRRc513pGw zmh##DVwh=*TKa%u3OV-BO*(H=b$&r0TuhZ$gW z-&V{uD5biulB()b3<`R-X$)r@1{{f~GW|aZ%D#!3RnbeInKDMva&ymTXPdF5J(`w)f&|K6e=EWE?rT2b1M~`u zduGp-%bYef3pUveHYj`ki=H%WY)&0f<}?cFE?w9U+o=Y}x|gzm_4pc1mX}+5M~5AG zf`>Yyd2aTKNt_hQ0~UCBt3J9UhcAk6zIR5yIqxDUi=zRE^apjvfF%XyRJraOd9gV7 zKp#VaCIjAMgGO{W?AJmtK-i!!E|ZITeXbn*XJYunOucC$2rA$$s=~^>Xg9#_V`;a! z+gKv|C~v4$zH0UTZ>zia{mo`9Ud8EbB!3u-#m*Rhu1c|%hE?rC3X6VPLc;mqf>b?c ze9zf3K`@_}I|~NR;W(N^gGtzfM?oMA{RjmaS-1Me{8Nw7#hK4;mb*{#JyJ1~`9h5; zO|6N2>*Z_&4w*ifkrlef&b-2|KE=djvwy=W$df}KiWVvZ9V1El>jI!PJDvQVQ8bP2 z-bS^F7d3Oefg07}`+A!BkC(8p4{J{k=#vEkx~q36S9i->nn2M*FvkY>hcN^GuGJ?b zr*`IR!geY9!Xd0T8P^*$4r*3kARi$Uyw#wHkIJ~E$czq7-BvGA^Ypjjnm<{;u_#)wN9?~?L(Q#bs|MZx$KHae9=RM_g!jL=+ zclmdTWs;-S;&BH!86vQ}e-b>J^>U5(5!{|J0CC+udqCXlUr>R9h!KnrxJls6>bRRFJe(5&0D9zf!&e z^~W$_8SLy#`qiYf^rf6|BilJ^!Ny_6FBKMy@XlD)C54nTfe|bqg%S-w_ z*6(Qca5+igt?onG1h_ii4ZCpnLU4Emha`ALKS;R$o|ciOkRIqC8S&-+N*os4o+Zxa z=Y@UEasnU$1)h&?FrnQ&J+~1^Pb436x<`y|eSz{NgG*P-FaZy5q0ZdK=I{ry~ zFLzhSbAw;nzs_L?z`kxLJTLaELf?T8*Q3kf0T51;qAvCH_1z8K6x*h$2|4dhb0W=7 zaNh#j3MA1VT2z=;GTBtGxweHzK%ix;<-&VL+|=I$uqVT*PpywrNg;=35d#sWig9eY;DB6TRCaPNDkwNy^8W=QrI;hw z6+P2|?`5Bo>Q#XW)$7-JbYXGiJO1jYE(p$7yJF%?ABuBtzK5hceUL$6MG7V3ux>bq z!&4j-k^}}$#A2+A*d_?OfmqEvUP`tabGwTd{c7$_aq+SOk9bbE`u3FF>xm@Q`dm8r zVh->NfP~ffRe-J`T+ZGy=N($)30&%x?d?F}#%Cv|f;<3-ppNiAlVPc5J_Rbp@6F-? zZhpk4o^8uO8YTw#JDO2^O=jC2EkvK7d}_3(-5mcIn|bJTDceETp&Rd6`t4L$1s*44 zvLk7;R=ioMMlyy*I(T?3^1|(E)T)x3uf$vXFBN#C3)ktM#ZW=~CB3Nn zW=o*SD5NAap2bwR8#wpE?AH^H(y!vij%Us)!$Lz>9}j*-;xP0FG}GXLZ>fX{B$(Mw zK=GzV_$+%vP@x~3O~Mi~HUeU6?p83^xZ+xRjApMkF<~fSs;5v8 zhN>Vvl`|105b-Ye-MyzIe-{c4lK<#8;e$5qn~6A?6vOR-B*~%WB5#JEQL^`J zia&$aPtXUkrziqBmgJGI)%$oqE52KvEeCYO83_E5b8@N?!I}jmY;dVR2R@3~erq{W z2$}pAv5d*~R#j95!E}?eIZ>Yyw(n7;m@sKqSQEXKE!+{%7>u-$=mx^YY*)5F^152! z6`-OR>`WNS%XUI*RCS9m^yfo?<(V8xV;6vP%iRA5Sl;96vvVZ#^GvVXNcxk$WP62Z zltv?3{XRLV#2#S69lGR`0e8Qfi+8F7S5p9T6sZ6oD~r9>BIi8oPiqA}pxHa9aG<+SkH+#@ z>{a}$fBXFN=g*SSgg_-$&Hco}%Aqg^Y+=AJ3H-Fm35NI~q9C0Z1haV<-`|{_7BT`M z^z&DGCcNsni;gRU^0tDI# z<;II%(nk#m6$cjT<^5ii`iDy;5*Oc6j%v=MwD$>4De!Q#efxhxE2KAAuTu1Wk(=5! zbw3gs=7LnYX8lT^p&Zp8DpN02{H$?y=qy0kT{kdGfYNXh&NB)c%$+3j-)#&$Sy<*y z=c!w(Y3_|?ac4@EUi=BIf+A}Yy|j0dWwn;5>sBQfa{P-gr^Q|?CNTE-{myj`vBKo;p0nfUH*HX_@CNzEA~y@SYdTYRt~?9VRr9* zlV2s$!6DzeM0WhHGfJ@|dPzs1Lg4@8gbgDw5cFt7m6JGH(MU$T;i>jl*CBI&pSOp-8 zn@1dtBzUmOdQiwh8@>UbEn-?HupeoIYR3?i|41C=df2w5dG1D;h}yNT^prg==T`Aa zFiA~{2wHo*Zo65NyWD8FE_8m6Y!xh7Hq^SnWu-QxE{bJ&(OkNnyRw?LdobJ$lIQ{M z;4NYF z-eK3w@>nOwRp4VA*U*{aB%uGFYa{H>EiNON%+jQG4$$W2@>WV4veR$4MI3mO$ zq-vznq}$M6@?0P$VhmjY4*Lsdo77*^Yob*&`aFZ;zMC+lZ@llIwn0Iq0QzcV$`|Bb z?k&G>cS~*^J3=tArl)C?C{2(cZf`I(rZ>}$@zar>H^VrZtVq*Aa-g1@Db=K*p?MBM zQBM9mJS+>(I&(F!J(N76B9x4#8ioP}h4Sj$3^0e{p??Gim0~$YyDcb_5K|k$8W{FFHDtK;gX!o33k`ZKP!fWYyZgZ}f?kM~ivF+*uQv;j ztQ>-Q2S0s|6!8(YG!zaF7A_#v#=5Wv!Zr%Gp)Q=sfjcu^vWs8f8=`a=H=WR9dQ${Cwp^g*@2; z+J2Tv==Z$gADsj68AYP%W~GMOb${3ioK0z0TTRtG>9SY(6BQPScTr{uNyrhz6NoGG zR4=@RJV>G#)EeLQDj-6T*NBdp6eE6*ANVg_J=M4p_1BYFo40Yv*rfzxGt#Mjgdm&? z@6n=$OQtUhF%QP=OSh%xU5umT@NE8#`Of~=&6mzkVpBx5%i`~RZ}bSE=C$$ysvB7| zZKIglc+QgM*G2IK3LIP2euB!euh;F3PtE!;tC{!58+dS+m!}%~KN5G|6_)gD(4@{G zR1(muOh$L|w=`wOFcInVc#F@s5=A8$rcwCnB~vubJLcFsH>oL7{{Sia$N(11H_NI!^ZC=Pp2Y*#AoUu}n zkqJBm0T+cTn34_CAbF3J-=oTWEW5JxN(VSf0D`LYO}khPjfk@h9AzODOlov$niY*G zTvXH544?SlfXfg#d?-kHnFqVo;m57M0|T4tSNT9|%lX<-mBEmvn-vUu{-ZqqU$)qm;$%wl`lbRC#}aF2TxK`f$H6rI9f*`ex34 z^&U*J7U`zi2e%h^P8TTH66QrkBI}fWFEYQXJdF_(?6Tv z@let1YZh*r%`}MU_(QWoTQkS@L+PVa#Gn;fvEC3dI-h)On^hrwGm6ip#6x zHKoDELr$2VJH(@_6*ZL#O zbI^H`Ig`g*SiLBkn&bReo~oEWLCJ87kH>Ykjj07Nv?@J$c6SL?sfKBc2`-fyJgq+5 zxuE`0D_Z?gyD4v$fi_mCzqnFv{psRP(TwV=c-?^qj~>^X`qg8DS+n$uvBbjSuj+E@ zZjS5Z13~K0vIgI$^_Np^j3%Q65WV#hb-~`2lrTB+!L(LO{WN_p zF;qev_a|Rl{-EX3KTKBg^K$ZM=6Wz}wkn_jF)*-wnyVggkDg(oLHHT6#GI@6lxZWi(nLU19F zZEb=6gOl;~VR&NGg#R5yrh?ptXEv;js`S`#(u%%@o%)354-}`mC}ai;B%g+%w?{5-T{?1k zR5BU?=)H2kB&uUH_B8!g?g*^yJFiF0Dm;-8aQBq`6%BmI3GZ-jR-z#YdEfk?$@dA@ zUNQ4*@fbUM9pk|OZ;5o1<9cUEyyEjxbJ@tp#Gz=_vwSc&ndP++dqiQOxE~ zi;rhD>PGm~!v>P6ffhXhpBMP(sGQXTAD^GWWC0@M$j+0v1B+E6P}4?BI)mjyi*5>m z)>cj^KkslPwhWGAf#TO3%i_}<*Mdn0SirOP9Z<;6H;V$y0DYco=vBNEz9w~8@!{&d zWSF7I!(?MakLX^zVAIHw=SYgrn+#yp-i>~_y=`$yLCqk>#^&&}YX@3%J!w=mTI@9z zLL}4rqaS!m9AfIIR*gT*-s$Kp>at7WsA770i~~~_M^ZOK(Th%24j*{f8H5Q7j;Oky z0{s&@<(%|i^o4~3I76RA zGql*o#>TyPh!h^)1m~xo^ShL{;Hp<&b^q>pEAH*>Z4+{&D!_?iW@U9e@Aiy)v{)Yo zQ~uGa^UFy?)O>1?P_REg2PhT+!un6KvDc|*Ft{oIAYbE#d&40A*wcx3++%V9d@gQG zPY9&prFyp*()v6_z=z_W*N#6Am=uE`+y7dU;+)O~MN~(9e?n`(im1kXLa8Q9$wtE( z*bx)I2rk0LZm(BXMPqE`Zp%UnW)L8UDNbUc5!btS!`p^(oLxR zH&~jE0bv5)k|!KUq)*#hrx>LsAdCuuKrI~}#TgiPz9o%Z!KC$!Y>Z=PzEYIe6_Kw) z5f@z$mbR5Z{O@<`PYhqFfQx4X_N)*{zyGV5Z|}sUQy3U~l!lkv6y(A5qg!VKJzb`M z1Uf$*xjn>2^)5z(F+lMKm^POj$A9#|QR7J|o!J!eun z%^H_J)+0$21_E3ODoL`SZ-C`Hkn_Cxc@3md8nU?9*weLghwV>~)^wZ|)l6J?^*C&% zsQ!vn4jQt%DN1na@fsqiiYvaqGRgQUQ+S;Vn-R(s!|K8s9WV-{3)pxV*DZmu3>oc# z@;393Q8T^a7#CH)RGLSW+3fkF-8~LGl0hoeP>L!AZlr$w-)WG<$ zWglqq-=>qTyD%fDP-D-qK(=<4KQXJY-Rz5wUNw98zwp^%f^^)@NuK4?S2jfo~ zn88_KW%3|7VyE9Iyg;JCIwWGyPhQ9!AH^DJ(Hx>$lN6qJ+hyzlkLSlU5e;+3JT<#qg%oV>=%}K2HJz#OQmRxR2X2g`s zIhftPCRviFP9%w`v5++y1egtIaZ^qfry#d9?0Eo2(DS5VU`(G3_(}}62Ne}w)tHz` zExNQQr`}mZ8;%hxVhsZGhJ+jsxrm6Fc^++(k#Mdx{Oh^umAWpIqJHo5hiYXu_}A25 zzC;-fioC25jevi>L`pUFv5l{0BvgGUVUjxri$KDNh$+SCUi&=v+$0J1N)c(d%T7~{ zD*g7Ncfr2k8|K_#V@mHQZ~xH0JyUD20i%)Gq3wzf84 zOD2EU9RPsvp=~GNdrg*o3pgOaQp7kIo4NYam?A+jWA%!8WkQ&xNH;Kp@1Y=sXdyJU#-JbB{3~ZZA!o zC24mHLa_)(VaS8Q#{i#)&xa6EMYq9?!D9+{YdbUsV&wOE_oA;;%1GJHqv^jB_Hz>< z6z5FK>*y=z=W{@)6c*(t%@7Y%-|kfg_hECU*U1hQR6hQ04OGNE`O9GUyK@ogU9NiN zCcj9(Ms_CJ#{y?MC5!~O%G(q1p%F%jB}qXYNBRi}!p6pcgBp9r2%up@KX!ZodBWos zH(}CXMhu5h1YCY*;WuEMp-oG18{?{^1v_W>5IWRCf{h9@esnJgpLs@5LBC;kc6OBy z_^pBW(f2UcFL?hzLI2(5(Yrv$S-FO_urO4?`pP3%!_*ZFTPq+MXCzyo8SG}Kk2zyG zg4 zFo^KQAuAZz1qAf^pHjgL+fnn2ytx5&R0aewhOa(+W5SgKf%Milz(jofe6$JnmBm?b zH+9p0$qY4)*RVQQ*P3#1!+PEAKwc}zij9m(jH{FHR-QbR%5r;uahbuXL(*BMWYR{X zvP#0nj-7@jE0U%q)>}Pfsjp7-wq(P$#g%SOGGnh3-eD)WE_%35!e-uNhmOY@{+)BL zvJzeOLIxcV!bqg>{dXriTQ>(x3>@=~R(qr}rUKHUF>umKCuM^x4bT*qkjxle=OyxS zRa9>5QgVFzR0bpCo))2FnU21Fiv+&SZP@K! z&T@;p?Ip_D+zyrfCrc@%&^!z;Vgy)FiI~*cF>wUU_I`pK9u&X*!a1pEtJxngOjGmt z>F@f@+t?q+vq@OmxNUZZ(Y??;)W0I06*!5#Jq!>OSoJwdE}k+YP;lFrP-?LW7fy>& zu=@Dd8kDf zEb=osv2HZ7<_=XeYc(2|;VR${Hf_(@e`7>AAvSkxHdvk;_ojYNkzpSsKl;8?9+c?E zp?x0{n;K~}2y%eH!Ui2c6aYP01bBT^6bhTPEORPo)E1)v(HL*uC>KqE3`Sb}+6Cc- zMn~i7&#`l`Uf*>Th;5ID0Q}LFB`m5efB`Oe0RouTj&WGy*d;EwyL2wLd9u=SR0#l% zA>nrTi9Ff7XE+m9X4relw7zZ@JC4*(=&C zSE$8$v*#0@Gu;?qi+-pF2`46`7&R>UubgMhSVddnO@Y@d)F>;R2|!v*OZCBQ@Mx!4 zn>}^5FxHp*bYP1f%Wb{8JkU&52>nCi9nZ`|KtM3q9E+K&+h9&fL198{FY3h22rdqd z4h%)$qwdRBzwwqMD=zNzx`zQAe6?OcCOuG1ywrfYXrl>-hX3Xl2wjP*P_LD2`>tK5 z+2$pTxZlgP6e0d)Gwe!qk>=ZA?U>*JxtZb7zW+@tb_gpS;%Gq}T~JvWqrJU-;y{5i zt+bAoqtK1>myfkhCv{050e{nhsng>~oHh~(r9GEG^`4u!B~X=0_3W<-A>SphUz}|o zvyN%dN=2w?YRA}5*S(`mzMq|@b_+GH4QJe>iMo46STwNFQrgi9joP-UqSfTp2>rOZ zmfOK?nBj)~RnzYZ8hzJ7&zz1gp3`&mLD(=&pTX%Ghxx+3VBDPiqAqEx`3{$a!*Qeg z-}|xiO6m^Ko;~nee+2ofc3h``Q$nb}h(|3OZAyEbud)PDTvI}sppf^f_3jZ)p`7qL zI0d!;H0Scu<9c(tl<{#K?2Q5g2Bg!@yq;qh|Q zr#2mr+jG;5VE<3l!q$1OL8fa^O5C*-drz$Wiw9(umhpCTC20nWV8tRVd45uLu=2=$ zZ3pnqJ=NjPE((+eQg;W>-T%FC&n3@uW)w7JHy>SQJ9;}$pN0s)m$amB)l$a*r>MN zg50j)j!mcbrc5F(Qs&(Q;7Q-MQNDNTckbQE+^)x`kE4T6VN1_Zh`{GA;CPaDN^=7F zk#UJbIfOVP8Idw3jIe!Vq(i4GREr%-`%;rAJuDK62LV|-4IA8T)+4;-&`vS_H)i9j zspId%86FepczZ*INqW+RF+QDW}$2Kv|t$Z{4kybGk;{o!cy6=qY%nM zCB^#`9GcpW-`0z(lm>5LjBYddjusnPD60)9hE}{iHYrtDML+_OC74@-AkxJi_^P8q zk$XVN_i#6)2~Gy5>7(a&luYom5obmOos^}yDVsJ3H}dy?;?mFIeePS6ZzG%ds+Yf; z?R^}4Z5|pDBJecX589hQvu>aAIiCu^8rVA)%kz{Q>!5-cx_4{f6pm=Gr+uUi6d=Zp zZr4?5lyZn=_lUI8ICu~^HlZuk|_lX;Bsu6(0AtE8?@iE03Pi4 zqo4!!)7EJ@{a}*upAy)=l%FI;ZJ#fi*x*{JdNWEYq_T+eA{x#SZFkGKJKzlkJIiSQ z5fvw21e@5G;>0}=WEBwC&vqL3g8XvcGIZV%N~I`#BV<@eooSeBUdfk-E}Fkb7NHoP zv=M@<^FmZd)GV9gZ6Z(YjT480j%h9qq~M`o1a=O$EDT{G-#b0&72(GuER)qhC}tAz zP99;D6o{nGLH_m{lU@TTdGv$(-#o-t_I?Sz^sv9v2r@klXER*ETKKxPo6TAy9kj{1 zcUNl#g6;1#5y94z;h>NWwYBzK-y+V(rHkhkBY0ZP}IK?B3WYrzA5`;1hu%?`Ff2jr>>P7=hLW{ z`>J7SEBbykIy1w4e_#v;8 zA>!TOVOIrDj55^rD32=iu%9UN)nyQ!*KVCWkdDiK>M;0THWHGY0S-<&xZsY{cAx`G z5p9c-R#bd!f4XaDtHkxR0((8dTdO4Ym?Z&f`Oo8k?^GHKdsxveodI}?Vj5c^pe@8g zQHlY~W@196U8qb8^iPtR*#Snf6#vdJ+x-KHG{AN)8HI0Htp##T)~b!AFqM0K7rzDx z8`7Q*vya7frlqCjlK)N6RKXsnirLK7efhS3+X+(wTOSeucY1lbQe!G7g1SH)V0I7A zP9~i?jB~~J)lNIZX)tVENF zgrKebFDO+dRX?X@`L+f!pjZwx*wCtiw)2VVj5y)FJUxBSWejdGWtCX&!-6)3Or~0V z%{!XyJGsbsewdPt2?4L`e5?>J2%y@-(~lc4sbg{mtZaLhD@b@?_V~UEB(;?Up5}Az z<_f_qEhNpOqFx)}qgB(Y_rmS5PxdTH_eLHoKCiEjc%Uo`$^a+?kQ<;)OO-O})-z0$l!oedgY9ye$6AKLb-n&sMAi z5uJ*URIsrr%gI>_fQIqnm64W)DwL;&c>u(M$TN94kuSx79T}twqgJc}n*xN%fWOgg zPKlL}qAmH44D1oETr z##N#{PD_I|n4UT+HvI`-0Pa?$ib@7hr_cD4^um`9E)X)Gzf9q|UKZU(tsM>r-!-X; z7rfKO>2Ne42u^q{4F6kU4K}iGxlF8ppW^j@LpA^*Rc6m=3=rPe7*I#mP zEGW?ph!W592R>+?3h z?IZwfif1NX0b+32*=DVu)~x_zFr{%MT?9WrcQB5MCD7%9XD$m%R)%3-C5o}to-~A_ zfisr`nskY@0BebnQV1}}SP(_i*pG36_1cT~hN1lVnk(l-f@?JxScuoxpxUwb!SWx` zoK-+!@V0s0WW+54Pyg^-9OI6dwzo>teZpvHVRK-pYc^@GXCn`GB})m_`RV`GRy`}{ z;)3{b65})Inxwbcy{7y19R+a8P(N;pL)emcPv2EuM|(?(p0yeTKvD&$$YCa!oxELoK-O&XVUeE2dOr(FzmxQn>0*M-Vg($wTgdUOSkHPdyNQH8cde zoIcO(+R-g$8dwDc49e*H`ho{K8-y_)rRS7h9rM;wPpLD9aZ1 zm=(VdL+D^vUzn@{%U=C@ThC_L>;ec}6QWe+sXoi{c%0_ytC!oawaw~k9z2`k*g2mEgK;;560-x*qCY%Hw6U>7^ZAm#eIA~1tp{M4)3R~wd z2@3#GeBEYQzfsc2%eA&h>H944mYyn!wraY$K_jScDDyu@Gl6s-&M>VL}1S15^qqszp#D+ zQfolKgC`5ZtH&92>kpex+JbGbPu5tlTgjApMEdyx52uX(C@U3E}kBWNkXS$W4QrN&AhepN#|FJBj zrNfBlybtZQb*l%vz?3Mv8(nx3XK>se4^?(dRjm;#Vf0jHV;RqXh@a;N3ITJaqS_aI zI3RGtb+GP2Wi}ZM?z56qDOLk}I+&|T!G$&a73H**E4YUst0NI%LpV6|AC9k{7|jYdC)p!AJBO5vkH{%Cz$4_ovFXdwi3j6DglA3Zfa5 zoj>llOu9V&XIyf4OsjWGWO?eHJ#2I%6F{-3sgpkeTDdYts5foOzYxv8mStxu%woUX z!Y^B^cyBtptA>nD-NtDWL!UtH$7|BdKqLmQUVc6oGK7rBqE~ZMO9_Ad%7C<{@@~U! zM1=3()Hm=DLxMI-08MsLhf?(d&;e;lN#YwHq2s`+2QLuwH>wF|B&V|t2@oa-Vw`|V zLxi<<0cDWWc2O7?N}w!+t-yhG&*cXgA`;32K>aHcgKX$fI?VsY+FJl+-TnKbC@J0D zA&nB!-Q6wSDH76xbi+eScS}i2Nl8gdcZ+m4+{GLJz0cWa?!9y8&Nw)u z{D*D&=LG2ev7Xj3AbHF#A4|^dD!ZpUz_S!;TI7+3*?VG+kKvHL+SnPA80Tr09yP+f zF7cL*fQhCm*q`v4LhI$s$l}M?I<_RYZ`H58Gh9R1K$FAgWOp#9Fwy|T|6PQQNc-JM zL^S;(vL-lKs=N{RxxpR`K+7nOn?)j6@~lH1_HDnW2aXwP7J81!#lTPrw@G8;?Xl5X|;PwCHiH* z_%{uBx(y@ULwe2+N$~J!llinhM*?{I`a>74G-d=7W4Grwp#wpyjRETS?_s2Spj(|F zvQQo^#cxQdLGGh}0?c1QfF$52gcc24fq;!ShD^vOJN^aM%Ub*Ssfp7ZNyh(GH}%g$ z)#vL$sK5ix3sJu9)?9A5?%`}XR8>`OMHoupCd09O9?MII&R&*O>@TQae=?uNS$<_r z(uXE=U+cX&(DpGCgB3TpzPWjRBOuQz`G}O*EeNP$T|+Sc=WNeaQkBErw$!i>4-xXY z-`Ra7s2t{vZ%aWFwD`|!0nd<L_tO>2jpn8&V5yz4|J0-O%bmL@c(|OJf7!(_RfDb^8!Qc+SOkY9(!3qGv47) z3O*TGR&rX`?xLPU_$#p_S6SjGWx_3c+ug}vmzUeIXE?YElMOu@fic2&j}I!f^g|j5 z;=A5=Jd~7>P&hOM31=nA3T@e0g0?pR%<|PfhR%Oc*>b4^tgY)(ZHP3q=Vfo@Wj0nj zfO7|=i2RMQh*~7k&l7wRz2gV<8`vk0ci38@@)m=uT_Fwy+fOl$q^~x)112Y)LP#O} zoAI&9IRIAhSt@1HgBu)L@wB_C+>NKK&N7Tc7* zpRqf*5b8-5A3zMQ?{cnR>5)me)Wosvr7&lh$BE{r!1jFhG)%{!1e@gL5xU!INCp^d zb!MTFS5erZ0*|0*0fK*{aqOJCb)SdJ$>hb0qkXbHcJe+Nk&zI;THm(w-_}%HAFq}= zm4xn$miDUGC8w-a2Vb_v&=f3q__sQFfAlDV-cVUT5D{~?GvGmmf4mKpE?hewoo7bD znPc3pc=3jtn_FPeh%JR%kptio=^M_#u61wrkPgUo`$acm#9v<~%s_reO|K$r+EA7+ z58;{6ehU@>L6gys8ZgO!OJSirhf2N4SOTY&p+(d;;L}(bDZ+cwB8! zQK<%KhfcZ4dwCv^nAAKW-2|0fA(=mKlI+_8vxL@N?oPj7TjeFj$Iz;rRYBwD4bbI;!r4) zzay7|@2ae0+XH!jzSq`7XPxP=5iqSz=o?{v#$U)$tC`iZ9_Cjk$e12num22@FF&5u z$obq|`;yyEu{_>IprMk?6b%j)atGtlzK;*krqfB(MdAGNw(jHn4}tav?Ki^TEcQJO zM(BmDqTymkanU7HEgct9n#|k|StBkEw5!cJ=7lP>GtzR9q9b+Me5PN$ZSb>8qH#AU z7Yzo08$ToQ1Bokx$Ndn>kiO3)n*6Z+v!V6&O&NW0c#m+2Fm~-T@lGe8$L>Z6>3f!I zRWLUF`mGr*di0i_D{-*7@VBe_gRNyv`y?zrRACEB%^3++l++9F$|UM9eUluOMB*iUU-79#p8wi;OZBDcf-zZkrT%ydsLNIi|eoY(<)_{Jd7)E#7)Y?CYu_L9}YIh)OwTk zGUn9Z9JS{a(x2F`&%qfs@J#R9Q-&EOTOH4ty%KEINrNCr`i^BtxW%9g8`0(g{w_fVA4ZhUcR00ECpNW z$<|EA`-^VkM;jNj%PElDu}lAk{^)bHZtJAYdGn!*4k!t0*AW~OMn~E2{~FwU=Ju78 z8vTu*Db4%*%4lB!4i5GH6Rn6}Ni}w}U)w7!3C2w1 zMOmPzbU(RP8%Rp(;{j3-biYcvhqnVeVe2c@uma-t4R@Q#wN*ErX_j_}PZbu`f$O7zRTmzHt0Au$^L zU%UB&_kx6Vf6G`pyuGL4S^96uOcr#dLES>3zZYYQ$tQ`#uYbo}Rj*M}@_Hg1G7u6QUECKXZH}W+a0vkkOG)@OFktBqD|LIIWUY4QZs~ zLnSv)Q7oTFN(m1s296Zbuie~qj(U0v&WpSaR%4J(bw}S=tdjo!-yQ{E|?P3C&e++!Z3ol&l=Nk?70S!&C z*CG_*MZ{Yl-sG|~o;FdKRFwg{%fWlqb~D%|2~skyn%BL9{pV)amaxP~c?3=Di zfFeZ8;Y!-$|v zSSmp)MD_jX3P#yotZN0C_|Yo-00B+DaRuH?MFqv+3y33JB3i^xLQ>LqsyGMplnt_} z9M)^r>%G3%ur89Tq_Z`)S&&!A0hwe-ohmIcN-CE|?-U=5SuO*7hpUFSgC_7fB)S36>&J$I+}d+kb(P;H3FDQY>_N=*<=yB8obDOcZK!l26^|5bW6B2(ZZ#~T_j8vLQy5mm=@}aDZ{U5u}>Px z!hQb^`zQE*EMbZcpI_T6R^kO$Ja~W;)56hSxoAKNxZx%dF?z}%GuPe-b-kx*IdP-~6#yp)=v_BW10 z9qg>o!M5Dxspp=phNp}nK7^N3U#t&(^cp1Cn^oUAx$}zquuBO!-_P8zIt<%?XJ4wHpDDqMFKk2(BECDsfHXyqdCg%p z$~i}Sx$9;~dDGhKu*G{9+B*=Qxcqp2*+kXcFr0oim%BNy=P9sljB$HrhkJIO9{Wm3 z`|324G2r&YiRR}h8}rBN%GguRyVindLzyV(K($8!Hev%#65CsbuNCRC1#ATxi1n|9 z_l}(KC)sSxhQ6n$Eq}D^+?a$PuGXm1yy z1My~LBqR!mAk&IkBs}(uIOPsVxdj`G@H&GY&wCzLoSu(J|9RS^cYp%C3`-p#S3J&J z#gmU7a+Uhcy58R2tnFjCN+6xPr}LH`*zrpy|3Jf|3WM~0jmqiiFZA6D9}=6~k12OL zn(*p%%aym4mujrXl8%QM0}19}a6WUwPYTU2`yUR1k7o^HwDffjMM-0PTwj7wfHcXIhG&Ad*22B& z@x6Dx*|CEhkJC!Yb9*bve43b?twnf+CN8k0}}Cudj>*E)G<~I{^G5%vQBmY zKUTbFtn171Apa{!q~;xwb(+7`*NJ!1*6ajDqWtZFrHmx1WxrzI=F|7mH5iPndv){} z;nYk|9H+O}&R*}^^;ADPVPn`+^ty1I*KRllWokJkXz|e5{;r>>P zRXTp<6>7-S(>Tn`K+%0j4=RuUrIbAEglWxx6>C|iJ||$?*hCY&5-!(7*d=}Tf1A>i zjRz-C+1Doxl6Sp72qG)LP>03ARVy;6%mTswpm4!vx?(X;3QgoKuv>P1Ue<}Q2LO_ z%L$zwoHDL_8rrE+LZ&b8srYtL#pZ_+hy!Qd$xsun^-$&5k!$KAtB&Hj1ELE^e0-~~ zoFM`5sPP1;he%<@isR3o32KxDzng8e9x8n@csX$s%MQyE)r{zm93w=3!CEs`RQfe* z%|0};Tz|t-q?8f#G4?Ybsw|8sST!^I{?2OufynfX$~a+D z9AF5MMb69cs1D?rUg9g7_(u~VfIN3V%eg!ABqc;_M`f<=8&V5&)T>~Ss%N4l+bc9> z$HR!b{dtAW{7E!{(J9}zfws;jWZINy`ct_y%BtR1>dh8F2wLzbCf8B%aUB1TO|G?sYwzXj#wM)Rg!ME)5aybm2N&}FCZ zsPX$5uF-DrCMdo6bCvnL8&WdJp(JO#C|tsxwAFbv6EI9cgW*3;%|*Ratge~&iu&aj z9SemwOFR&7j=>s;(gKT#h+@Xo?qLnibpN`DJdgccEydV-kpG}tKK%3NuN@aqnb%DR z1?+=x<4#Tdz^H^dHZ}$c78lIM|0QfPiw+tF4t96TJR8wll#6;yDC%EWmHs}EpMO&M z)IWoe>wqF9Wqwgxx1NrV<8;Z_KNi2Jb?OxSXPa}*w&ZX-=%v9PpZCQ7qPYt~S>^%d zj``CZz+gS@uEMJTLkHYS687-$@Jm*4S?&HOVC3#tN^1o_8-VFm`yiwPJlP5yV624D z#G1(Z4_VxTu8}9_>kNQTaZ};vaW!bqfYC5_q1CHFIU*iJNKFB@LbLT`fYHCy>V)L$ zt~z%3E~Gb#sKMjZ;`bk1=U;FEtAN_Njt>1L_GyW0Cm5tpq*;9emc{R1fqZUQhd%c1 zyM*e`AtxXo0zoS{XG711fZSk}3jG8J^Ba&XgWHm{hQWK4Qu7uq?LScw0AiP8*?gg; zku}Vk?Kpb&)XFsiU>p5bpG9!)jehwJg0C?|Qc;LF&1Mq7OKf{@WeLI${MPp0?9bH) z@}=ZxH!nU6CfR4lSkM0`xc)GRJBrPFH9DOv4=rDf$|FSJp$FgSZ5mhBd~+bPQU{-k^pp!7Zr|kBy3@geAy}tiQUp#PAi0;XlkFmL%!tg&shKFOTOB< zy5eht-hZ{5Un|7-`@>!B`$YF6TGi-cj!yZze?hdE%IqW<`1;r4Lx{B1CtK&Rve3Z^;|G^FqIc4vwxrX$65#TOoyeYoX~hzVIh*ZGmw| zuk)Yj%FE+UbHF&fj87IqsdA<9MvSMu0E5wT?79|=y$3|{8YnN@XIaep<7l*a+T2Rs z|CrTK@%}_aQm~370F&ST#V_?BH~-3TSs%-ff<_#t;`r~#7M~w`kC^$$#XztgXo;_< z^|G0UhQ|D{Ex`Bjo~vu=F;iAA>21sbt0>YRH6gx+z?@^vx{R5+&h?Tuwie>8PH9;%8(9e8m%S*g(ZPYj>m-oi3}1bu z5_To!oYNMh#QT2r=IRQ&cI$hCiiL%R%5f`bydl5-yQ4}D?{A-uzf1K1sK5bjJoo$6 zJUmV9hdfMHa$?ib06fnm^Z80mK>-{+F5 zu$#B~@SX3l1*`bmC0mpAWCIvGMb#9oSs8v-)yiJ;HDuFEy<6q$lVxvrITA6>M)&2? z0DlkLVLm|5llhE-m6cV>)s8Ji9ygUsTEKpk1MCkth_I}`zh9!KBeoihEY{^e#i(4b za4&JlAg4Osd-eA8yfB4V_yG>nyKlxw)cyAIk#RJBzhua_B%ZrgGrR;cFb4SdJ5OzY zfT#M-UW2T_1Jy$0+i=ESpn798Vq)btW<_#*6X@@-mxv#o_`T?8s?qrNVxQ0TBt841 z9zOkMm2sEun-#H*KC34W3_!FxO{!<`??4a@@KFUEK@}34v>h9HG}IPNB%DHJ)6}Amm0&fen zN3rPtm45@YHx^0d^zg;{Z;Hi2P}z0@&J>t5=RxSnV^b!>Miwe}l8_cAhR{~mFb=8U z+L&?vm7h8_FB?IQiQ5q&;07{4uc3%L6fmFs^?I0g?>blT0R?usB_A0RGcvGAwuv~B z4b<>n#n)0iJ6-Ixizd~3SDV{_*1$4VP=UVG^8|2B{~Zr_D*>B%$*}@$+<$riH7_&d zeLp?&-~hy%kGgr;cU)P~0q`M_U#UUmsJCrSzuBKt-G?}x3(>+9F;aZ+ZW5szU7nt| zzv0X{mZXTd&rU!+;6yK_rnt%{hMr%$v9S>eiUe1DQ(ha30yy9yYVlh^MZ|dcf%K{%-mO ztOsaP92o#UeUpbP+s0=*2M8&*Ou45N&>uYPpuCDa)*|46avn<7hqdKhTioph@h6Ud zy94F$02~jrDH1Pynce)J+5wwHDv4n{2*aYH&QDK?UK>F!05 zT`%GsX_@({Ul&TKVQfO>(4zutTfMz*8CMz!N9%Ss>;RQAy#^Z-6=)R5hl|b?dHxX^ z+WUDb7qU;aTbhNKppBr5ft`SZ?~Q9m95ry8{za+-@E&*<;Y9|bknmU{UA_X%R*qN* z;@mjo^XEpRj_1;5mX?p8c@%tNRG$BNY58AIuH<=$X`eePx1dJ>)D?2pWm?oteT6^{ z@;c*K828f8mh{{>ke*0XH{x`d!2GZ*SQ#PyjXjJ?6p?e`zZU%c{k>TFQzA3ACRGf) z)BWiGWT{}>0@K+7ApYI$KPKK)=P%U$kHE&%q>^9m9?}J>NkK@5p5yjHQY^`y1x7Ne zME|@$@HoCG);8d=Psf%|W*!4l7i)+#^$7nda!ShY4jVc{amv`NtgPf@yt8|2TF56R zV6j$4@Q4KVQATV;>Cf_oz%850lmIBD9hYy##dOkA;^I~uso+t?G}qH3rmNGn2hgCy zLZgb6OdH~VeMYKBR~A)H+}NrmF5e>rygz=lj&_5--u+b#F3cC9yU;t}asiuEK3^Za znFAeiQh?Nh^CO76^L1@2*x_?1Iv`W#<_Av=XWN&e#J+uTjr$xn3kXS;iW!d93xnEp zf@WX(cyi)G>pS8aF;cV@fUGAwX|cf}%kYySXi@E~*B^y$2t8=3o#`-moebcZC(c()U6Q�d92;(`4A*w~J*L5*xpBb*ZM1C~{kGgc3F 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 literal 0 HcmV?d00001 From c3d02ca051cbbec4bb9d81f5d68bacb66ee8fbf9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 21 Feb 2023 18:32:09 +0100 Subject: [PATCH 198/483] Apply suggestions from code review Co-authored-by: Toke Jepsen --- website/docs/artist_hosts_tvpaint.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/website/docs/artist_hosts_tvpaint.md b/website/docs/artist_hosts_tvpaint.md index 3e1fd6339b..a8a6cee5f8 100644 --- a/website/docs/artist_hosts_tvpaint.md +++ b/website/docs/artist_hosts_tvpaint.md @@ -42,7 +42,7 @@ You can start your work. In TVPaint you can find the Tools in OpenPype menu extension. The OpenPype Tools menu should be available in your work area. However, sometimes it happens that the Tools menu is hidden. You can display the extension panel by going to `Windows -> Plugins -> OpenPype`. ## Create & Publish -As you might already know, to be able to publish, you have to mark what should be published. The marking part is called **Create**. In TVPaint you can create and publish **[Reviews](#review)**, **[Workfile](#workfile)**, **[Render Layers](#render-layer)** and **[Render Passes](#render-pass)**. +To be able to publish, you have to mark what should be published. The marking part is called **Create**. In TVPaint you can create and publish **[Reviews](#review)**, **[Workfile](#workfile)**, **[Render Layers](#render-layer)** and **[Render Passes](#render-pass)**. :::important TVPaint integration tries to not guess what you want to publish from the scene. Therefore, you should tell what you want to publish. @@ -74,7 +74,7 @@ Render Layer bakes all the animation layers of one particular color group togeth - or select a layer of a particular color and set combobox to **<Use selection>** - Hit `Create` button -You have just created Render Layer. Now choose any amount of animation layers that need to be rendered together and assign them the color group. +After creating a RenderLayer, choose any amount of animation layers that need to be rendered together and assign them the color group. You can change `variant` later in **Publish** tab. @@ -107,20 +107,20 @@ The timeline's animation layer can be marked by the color you pick from your Col Render Passes are smaller individual elements of a [Render Layer](artist_hosts_tvpaint.md#render-layer). A `character` render layer might consist of multiple render passes such as `Line`, `Color` and `Shadow`. -Render Passes are specific because they have to belong to a particular Render Layer. You have to select to which Render Layer the pass belongs. Try to refresh if you don't see demanded Render Layer in the options. +Render Passes are specific because they have to belong to a particular Render Layer. You have to select to which Render Layer the pass belongs. Try to refresh if you don't see a specific Render Layer in the options.

When you want to create Render Pass -- choose one or several TVPaint layers -- In the **Create** tab, pick `Render Pass` -- Fill the `variant` with desired name of pass, e.g. `Color`. -- Select Render Layer to which belongs in Render Layer combobox - - If you don't see new Render Layer try refresh first +- choose one or several TVPaint layers. +- in the **Create** tab, pick `Render Pass`. +- fill the `variant` with desired name of pass, e.g. `Color`. +- select the Render Layer you want the Render Pass to belong to from the combobox. + - if you don't see new Render Layer try refresh first. - Press `Create` -You have just created Render Pass. Selected TVPaint layers should be marked with color group of Render Layer. +After creating a Render Pass, selected the TVPaint layers that should be marked with color group of Render Layer. You can change `variant` or Render Layer later in **Publish** tab. @@ -143,11 +143,11 @@ In this example, OpenPype will render selected animation layers within the given ![renderpass](assets/tvp_timeline_color2.png) Now that you have created the required instances, you can publish them. -- Fill the comment on the bottom of the window -- Double check enabled instance and their context -- Press `Publish` -- Wait to finish -- Once the `Publisher` turns gets green your renders have been published. +- Fill the comment on the bottom of the window. +- Double check enabled instance and their context. +- Press `Publish`. +- Wait to finish. +- Once the `Publisher` turns turns green your renders have been published. --- From 8f7b6c6a0e9f8b708e618fb5fec9277305786859 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Feb 2023 06:58:20 +0000 Subject: [PATCH 199/483] Hound. --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 425c236b7f..c651782392 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -962,7 +962,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "s" if len(instances) > 1 else "")) else: - #Need to inject colorspace here. representations = self._get_representations( instance_skeleton_data, data.get("expectedFiles") From 03d01430e60f5d4ec37c1d5e1e8df2868b7f6b1e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 22 Feb 2023 10:27:31 +0100 Subject: [PATCH 200/483] :rotating_light: few style fixes --- openpype/hosts/max/api/lib.py | 8 ++++---- openpype/hosts/max/api/lib_rendersettings.py | 2 +- openpype/hosts/max/plugins/publish/collect_render.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 3383330792..4fb750d91b 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -133,7 +133,7 @@ def get_default_render_folder(project_setting=None): ["default_render_image_folder"]) -def set_framerange(startFrame, endFrame): +def set_framerange(start_frame, end_frame): """ Note: Frame range can be specified in different types. Possible values are: @@ -146,9 +146,9 @@ def set_framerange(startFrame, endFrame): Current type is hard-coded, there should be a custom setting for this. """ rt.rendTimeType = 4 - if startFrame is not None and endFrame is not None: - frameRange = "{0}-{1}".format(startFrame, endFrame) - rt.rendPickupFrames = frameRange + if start_frame is not None and end_frame is not None: + frame_range = "{0}-{1}".format(start_frame, end_frame) + rt.rendPickupFrames = frame_range def get_multipass_setting(project_setting=None): diff --git a/openpype/hosts/max/api/lib_rendersettings.py b/openpype/hosts/max/api/lib_rendersettings.py index b07d19f176..4940265a23 100644 --- a/openpype/hosts/max/api/lib_rendersettings.py +++ b/openpype/hosts/max/api/lib_rendersettings.py @@ -62,7 +62,7 @@ class RenderSettings(object): # hard-coded, should be customized in the setting context = get_current_project_asset() - # get project reoslution + # get project resolution width = context["data"].get("resolutionWidth") height = context["data"].get("resolutionHeight") # Set Frame Range diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 7656c641ed..7c9e311c2f 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -31,7 +31,7 @@ class CollectRender(pyblish.api.InstancePlugin): render_layer_files = RenderProducts().render_product(instance.name) folder = folder.replace("\\", "/") - imgFormat = RenderProducts().image_format() + img_format = RenderProducts().image_format() project_name = context.data["projectName"] asset_doc = context.data["assetEntity"] asset_id = asset_doc["_id"] @@ -53,7 +53,7 @@ class CollectRender(pyblish.api.InstancePlugin): "asset": asset, "publish": True, "maxversion": str(get_max_version()), - "imageFormat": imgFormat, + "imageFormat": img_format, "family": 'maxrender', "families": ['maxrender'], "source": filepath, From 3b432690ace63aa1d69baff80de3ff3d86c9c8c8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 11:00:20 +0100 Subject: [PATCH 201/483] removed settings for plugin 'CollectRenderScene' The plugin is not available anymore --- .../defaults/project_settings/tvpaint.json | 4 ---- .../schema_project_tvpaint.json | 24 ------------------- 2 files changed, 28 deletions(-) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 74a5af403c..340181b3a4 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -43,10 +43,6 @@ } }, "publish": { - "CollectRenderScene": { - "enabled": false, - "render_layer": "Main" - }, "ExtractSequence": { "review_bg": [ 255, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index d09c666d50..55e60357e5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -204,30 +204,6 @@ "key": "publish", "label": "Publish plugins", "children": [ - { - "type": "dict", - "collapsible": true, - "key": "CollectRenderScene", - "label": "Collect Render Scene", - "is_group": true, - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "label", - "label": "It is possible to fill 'render_layer' or 'variant' in subset name template with custom value.
- value of 'render_pass' is always \"beauty\"." - }, - { - "type": "text", - "key": "render_layer", - "label": "Render Layer" - } - ] - }, { "type": "dict", "collapsible": true, From 7692ff252f21af427062e1edef1852589e904d6f Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Tue, 31 Jan 2023 16:56:01 +0100 Subject: [PATCH 202/483] use .get() to not throw error handle_start/end is optional and keys isn't created on Kitsu sync if data doesn't exists on Kitsus side --- openpype/hosts/fusion/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index a33e5cf289..088f597208 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -68,8 +68,8 @@ def set_asset_framerange(): asset_doc = get_current_project_asset() start = asset_doc["data"]["frameStart"] end = asset_doc["data"]["frameEnd"] - handle_start = asset_doc["data"]["handleStart"] - handle_end = asset_doc["data"]["handleEnd"] + handle_start = asset_doc["data"].get("handleStart") + handle_end = asset_doc["data"].get("handleEnd") update_frame_range(start, end, set_render_range=True, handle_start=handle_start, handle_end=handle_end) From 9eadff95ed3dcfcb214b22c1e77a3ec15d4fa82f Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Wed, 1 Feb 2023 19:41:00 +0100 Subject: [PATCH 203/483] Generate the file list using metadata instead of files in render folder This solves the problem with the preview file generated from a previous render is still in the render folder --- .../fusion/plugins/publish/render_local.py | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 79e458b40a..d1d32fbf91 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -1,5 +1,5 @@ import os -from pprint import pformat +import copy import pyblish.api from openpype.hosts.fusion.api import comp_lock_and_undo_chunk @@ -19,10 +19,43 @@ class Fusionlocal(pyblish.api.InstancePlugin): families = ["render.local"] def process(self, instance): - + # This plug-in runs only once and thus assumes all instances # currently will render the same frame range context = instance.context + self.render_once(context) + + frame_start = context.data["frameStartHandle"] + frame_end = context.data["frameEndHandle"] + path = instance.data["path"] + output_dir = instance.data["outputDir"] + + basename = os.path.basename(path) + head, ext = os.path.splitext(basename) + files = [ + f"{head}{frame}{ext}" for frame in range(frame_start, frame_end+1) + ] + repre = { + 'name': ext[1:], + 'ext': ext[1:], + 'frameStart': "%0{}d".format(len(str(frame_end))) % frame_start, + 'files': files, + "stagingDir": output_dir, + } + + if "representations" not in instance.data: + instance.data["representations"] = [] + instance.data["representations"].append(repre) + + # review representation + repre_preview = repre.copy() + repre_preview["name"] = repre_preview["ext"] = "mp4" + repre_preview["tags"] = ["review", "preview", "ftrackreview", "delete"] + instance.data["representations"].append(repre_preview) + + def render_once(self, context): + """Render context comp only once, even with more render instances""" + key = "__hasRun{}".format(self.__class__.__name__) if context.data.get(key, False): return @@ -32,10 +65,6 @@ class Fusionlocal(pyblish.api.InstancePlugin): current_comp = context.data["currentComp"] frame_start = context.data["frameStartHandle"] frame_end = context.data["frameEndHandle"] - path = instance.data["path"] - output_dir = instance.data["outputDir"] - - ext = os.path.splitext(os.path.basename(path))[-1] self.log.info("Starting render") self.log.info("Start frame: {}".format(frame_start)) @@ -48,26 +77,5 @@ class Fusionlocal(pyblish.api.InstancePlugin): "Wait": True }) - if "representations" not in instance.data: - instance.data["representations"] = [] - - collected_frames = os.listdir(output_dir) - repre = { - 'name': ext[1:], - 'ext': ext[1:], - 'frameStart': "%0{}d".format(len(str(frame_end))) % frame_start, - 'files': collected_frames, - "stagingDir": output_dir, - } - instance.data["representations"].append(repre) - - # review representation - repre_preview = repre.copy() - repre_preview["name"] = repre_preview["ext"] = "mp4" - repre_preview["tags"] = ["review", "preview", "ftrackreview", "delete"] - instance.data["representations"].append(repre_preview) - - self.log.debug(f"_ instance.data: {pformat(instance.data)}") - if not result: raise RuntimeError("Comp render failed") From 723778c3ad92b4a2609996b4c0d6d357335c3830 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 2 Feb 2023 11:46:03 +0100 Subject: [PATCH 204/483] zfill 4 for frame value Fusion defaults with 4 numbers for the frame number so if shot doesn't start at 1000 we should zfill to match. --- openpype/hosts/fusion/plugins/publish/render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index d1d32fbf91..5a0b068e8c 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -33,7 +33,7 @@ class Fusionlocal(pyblish.api.InstancePlugin): basename = os.path.basename(path) head, ext = os.path.splitext(basename) files = [ - f"{head}{frame}{ext}" for frame in range(frame_start, frame_end+1) + f"{head}{str(frame).zfill(4)}{ext}" for frame in range(frame_start, frame_end+1) ] repre = { 'name': ext[1:], From 4ad830ffdb9b48ff3455f8c11fd7a681d603c7cf Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Feb 2023 00:21:37 +0100 Subject: [PATCH 205/483] Fixed hound-bots comments --- .../fusion/plugins/publish/render_local.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 5a0b068e8c..b02fa55d20 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -1,6 +1,4 @@ import os -import copy - import pyblish.api from openpype.hosts.fusion.api import comp_lock_and_undo_chunk @@ -19,7 +17,7 @@ class Fusionlocal(pyblish.api.InstancePlugin): families = ["render.local"] def process(self, instance): - + # This plug-in runs only once and thus assumes all instances # currently will render the same frame range context = instance.context @@ -33,12 +31,12 @@ class Fusionlocal(pyblish.api.InstancePlugin): basename = os.path.basename(path) head, ext = os.path.splitext(basename) files = [ - f"{head}{str(frame).zfill(4)}{ext}" for frame in range(frame_start, frame_end+1) + f"{head}{str(frame).zfill(4)}{ext}" for frame in range(frame_start, frame_end + 1) ] repre = { 'name': ext[1:], 'ext': ext[1:], - 'frameStart': "%0{}d".format(len(str(frame_end))) % frame_start, + 'frameStart': f"%0{len(str(frame_end))}d" % frame_start, 'files': files, "stagingDir": output_dir, } @@ -56,19 +54,19 @@ class Fusionlocal(pyblish.api.InstancePlugin): def render_once(self, context): """Render context comp only once, even with more render instances""" - key = "__hasRun{}".format(self.__class__.__name__) + key = f'__hasRun{self.__class__.__name__}' if context.data.get(key, False): return - else: - context.data[key] = True + + context.data[key] = True current_comp = context.data["currentComp"] frame_start = context.data["frameStartHandle"] frame_end = context.data["frameEndHandle"] self.log.info("Starting render") - self.log.info("Start frame: {}".format(frame_start)) - self.log.info("End frame: {}".format(frame_end)) + self.log.info(f"Start frame: {frame_start}") + self.log.info(f"End frame: {frame_end}") with comp_lock_and_undo_chunk(current_comp): result = current_comp.Render({ From f7175511f044745f88acb9c927a6f7ea51da705f Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Feb 2023 12:11:04 +0100 Subject: [PATCH 206/483] Move hasRun check back to process function --- .../hosts/fusion/plugins/publish/render_local.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index b02fa55d20..b49c11e366 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -21,6 +21,12 @@ class Fusionlocal(pyblish.api.InstancePlugin): # This plug-in runs only once and thus assumes all instances # currently will render the same frame range context = instance.context + key = f"__hasRun{self.__class__.__name__}" + if context.data.get(key, False): + return + + context.data[key] = True + self.render_once(context) frame_start = context.data["frameStartHandle"] @@ -54,12 +60,6 @@ class Fusionlocal(pyblish.api.InstancePlugin): def render_once(self, context): """Render context comp only once, even with more render instances""" - key = f'__hasRun{self.__class__.__name__}' - if context.data.get(key, False): - return - - context.data[key] = True - current_comp = context.data["currentComp"] frame_start = context.data["frameStartHandle"] frame_end = context.data["frameEndHandle"] From fa526b3e4b2fe40a24dc7f34492c5864b818d9a3 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 3 Feb 2023 12:14:45 +0100 Subject: [PATCH 207/483] Made line 40 shorter --- openpype/hosts/fusion/plugins/publish/render_local.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index b49c11e366..6f8cd66bd6 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -37,7 +37,8 @@ class Fusionlocal(pyblish.api.InstancePlugin): basename = os.path.basename(path) head, ext = os.path.splitext(basename) files = [ - f"{head}{str(frame).zfill(4)}{ext}" for frame in range(frame_start, frame_end + 1) + f"{head}{str(frame).zfill(4)}{ext}" + for frame in range(frame_start, frame_end + 1) ] repre = { 'name': ext[1:], From eb3dd357e3a5406bae2f6ddb856412d44a78a815 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 10 Feb 2023 14:11:15 +0100 Subject: [PATCH 208/483] Changed LaunchFailed message from PYTHON36 to PYTHON PATH --- openpype/hosts/fusion/hooks/pre_fusion_setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/hooks/pre_fusion_setup.py b/openpype/hosts/fusion/hooks/pre_fusion_setup.py index d043d54322..323b8b0029 100644 --- a/openpype/hosts/fusion/hooks/pre_fusion_setup.py +++ b/openpype/hosts/fusion/hooks/pre_fusion_setup.py @@ -36,7 +36,7 @@ class FusionPrelaunch(PreLaunchHook): "Make sure the environment in fusion settings has " "'FUSION_PYTHON3_HOME' set correctly and make sure " "Python 3 is installed in the given path." - f"\n\nPYTHON36: {fusion_python3_home}" + f"\n\nPYTHON PATH: {fusion_python3_home}" ) self.log.info(f"Setting {py3_var}: '{py3_dir}'...") From 6482481cf55089420b479f16ab4fa579ec76676e Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 10 Feb 2023 14:13:59 +0100 Subject: [PATCH 209/483] handleStart/End is now required --- openpype/hosts/fusion/api/lib.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index 088f597208..a33e5cf289 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -68,8 +68,8 @@ def set_asset_framerange(): asset_doc = get_current_project_asset() start = asset_doc["data"]["frameStart"] end = asset_doc["data"]["frameEnd"] - handle_start = asset_doc["data"].get("handleStart") - handle_end = asset_doc["data"].get("handleEnd") + handle_start = asset_doc["data"]["handleStart"] + handle_end = asset_doc["data"]["handleEnd"] update_frame_range(start, end, set_render_range=True, handle_start=handle_start, handle_end=handle_end) From a9e1140ec9a5c4c0f347e4a8e5afa7da4a89f781 Mon Sep 17 00:00:00 2001 From: Ember Light <49758407+EmberLightVFX@users.noreply.github.com> Date: Fri, 10 Feb 2023 14:14:30 +0100 Subject: [PATCH 210/483] Update openpype/hosts/fusion/plugins/publish/render_local.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- openpype/hosts/fusion/plugins/publish/render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 6f8cd66bd6..9bd867a55a 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -55,7 +55,7 @@ class Fusionlocal(pyblish.api.InstancePlugin): # review representation repre_preview = repre.copy() repre_preview["name"] = repre_preview["ext"] = "mp4" - repre_preview["tags"] = ["review", "preview", "ftrackreview", "delete"] + repre_preview["tags"] = ["review", "ftrackreview", "delete"] instance.data["representations"].append(repre_preview) def render_once(self, context): From aa1dd161a5c9add531d53733b77b8cbc5f651e48 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Fri, 10 Feb 2023 14:17:59 +0100 Subject: [PATCH 211/483] Removed double space after "," in repre_preview["tags"] --- openpype/hosts/fusion/plugins/publish/render_local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/plugins/publish/render_local.py b/openpype/hosts/fusion/plugins/publish/render_local.py index 9bd867a55a..53d8eb64e1 100644 --- a/openpype/hosts/fusion/plugins/publish/render_local.py +++ b/openpype/hosts/fusion/plugins/publish/render_local.py @@ -55,7 +55,7 @@ class Fusionlocal(pyblish.api.InstancePlugin): # review representation repre_preview = repre.copy() repre_preview["name"] = repre_preview["ext"] = "mp4" - repre_preview["tags"] = ["review", "ftrackreview", "delete"] + repre_preview["tags"] = ["review", "ftrackreview", "delete"] instance.data["representations"].append(repre_preview) def render_once(self, context): From 0124bba40cd55c30517e9a89fc89405dc86c6081 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 13:24:50 +0100 Subject: [PATCH 212/483] get_representation_parents() missed project_name --- openpype/hosts/fusion/api/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index a33e5cf289..ac04efa6a0 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -210,7 +210,7 @@ def switch_item(container, if any(not x for x in [asset_name, subset_name, representation_name]): repre_id = container["representation"] representation = get_representation_by_id(project_name, repre_id) - repre_parent_docs = get_representation_parents(representation) + repre_parent_docs = get_representation_parents(project_name, representation) if repre_parent_docs: version, subset, asset, _ = repre_parent_docs else: From 7e4457b241347b39fb1606751ba723e1be774632 Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 13:25:57 +0100 Subject: [PATCH 213/483] Fixed hound's max-length note --- openpype/hosts/fusion/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/fusion/api/lib.py b/openpype/hosts/fusion/api/lib.py index ac04efa6a0..88a3f0b49b 100644 --- a/openpype/hosts/fusion/api/lib.py +++ b/openpype/hosts/fusion/api/lib.py @@ -210,7 +210,8 @@ def switch_item(container, if any(not x for x in [asset_name, subset_name, representation_name]): repre_id = container["representation"] representation = get_representation_by_id(project_name, repre_id) - repre_parent_docs = get_representation_parents(project_name, representation) + repre_parent_docs = get_representation_parents( + project_name, representation) if repre_parent_docs: version, subset, asset, _ = repre_parent_docs else: From 44672f3f827517575119d541a1d3c76a3c7fcc2b Mon Sep 17 00:00:00 2001 From: Jacob Danell Date: Thu, 16 Feb 2023 17:29:18 +0100 Subject: [PATCH 214/483] add task to instance --- openpype/hosts/fusion/plugins/publish/collect_instances.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/fusion/plugins/publish/collect_instances.py b/openpype/hosts/fusion/plugins/publish/collect_instances.py index fe60b83827..7b0a1b6369 100644 --- a/openpype/hosts/fusion/plugins/publish/collect_instances.py +++ b/openpype/hosts/fusion/plugins/publish/collect_instances.py @@ -80,6 +80,7 @@ class CollectInstances(pyblish.api.ContextPlugin): "outputDir": os.path.dirname(path), "ext": ext, # todo: should be redundant "label": label, + "task": context.data["task"], "frameStart": context.data["frameStart"], "frameEnd": context.data["frameEnd"], "frameStartHandle": context.data["frameStartHandle"], From c26f86f08c8517c081e35292973b0713f2346e15 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Feb 2023 17:12:19 +0100 Subject: [PATCH 215/483] Nuke: adding solution for originalBasename frame temlate formating https://github.com/ynput/OpenPype/pull/4452#issuecomment-1426020567 --- openpype/hosts/nuke/plugins/load/load_clip.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index 565d777811..f9364172ea 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -220,8 +220,21 @@ class LoadClip(plugin.NukeLoader): dict: altered representation data """ representation = deepcopy(representation) - frame = representation["context"]["frame"] - representation["context"]["frame"] = "#" * len(str(frame)) + context = representation["context"] + template = representation["data"]["template"] + frame = context["frame"] + hashed_frame = "#" * len(str(frame)) + + if ( + "{originalBasename}" in template + and "frame" in context + ): + origin_basename = context["originalBasename"] + context["originalBasename"] = origin_basename.replace( + frame, hashed_frame + ) + + representation["context"]["frame"] = hashed_frame return representation def update(self, container, representation): From ebb477e068b19289963aab53a99a2da503d5f52b Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Feb 2023 17:51:29 +0100 Subject: [PATCH 216/483] publishing files with fixed versionData to fit originalBasename tempate --- .../publish/collect_otio_subset_resources.py | 43 ++++++++++++++++--- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index e72c12d9a9..537aef683c 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -14,16 +14,41 @@ from openpype.pipeline.editorial import ( range_from_frames, make_sequence_collection ) - +from openpype.pipeline.publish import ( + get_publish_template_name +) class CollectOtioSubsetResources(pyblish.api.InstancePlugin): """Get Resources for a subset version""" label = "Collect OTIO Subset Resources" - order = pyblish.api.CollectorOrder - 0.077 + order = pyblish.api.CollectorOrder + 0.0021 families = ["clip"] hosts = ["resolve", "hiero", "flame"] + def get_template_name(self, instance): + """Return anatomy template name to use for integration""" + + # Anatomy data is pre-filled by Collectors + context = instance.context + project_name = context.data["projectName"] + + # Task can be optional in anatomy data + host_name = context.data["hostName"] + family = instance.data["family"] + anatomy_data = instance.context.data["anatomyData"] + task_info = anatomy_data.get("task") or {} + + return get_publish_template_name( + project_name, + host_name, + family, + task_name=task_info.get("name"), + task_type=task_info.get("type"), + project_settings=context.data["project_settings"], + logger=self.log + ) + def process(self, instance): if "audio" in instance.data["family"]: @@ -35,6 +60,13 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): if not instance.data.get("versionData"): instance.data["versionData"] = {} + template_name = self.get_template_name(instance) + anatomy = instance.context.data["anatomy"] + publish_template_category = anatomy.templates[template_name] + template = os.path.normpath(publish_template_category["path"]) + self.log.debug( + ">> template: {}".format(template)) + handle_start = instance.data["handleStart"] handle_end = instance.data["handleEnd"] @@ -84,6 +116,10 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): frame_start = instance.data["frameStart"] frame_end = frame_start + (media_out - media_in) + if "{originalDirname}" in template: + frame_start = media_in + frame_end = media_out + # add to version data start and end range data # for loader plugins to be correctly displayed and loaded instance.data["versionData"].update({ @@ -153,7 +189,6 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): repre = self._create_representation( frame_start, frame_end, collection=collection) - instance.data["originalBasename"] = collection.format("{head}") else: _trim = False dirname, filename = os.path.split(media_ref.target_url) @@ -168,8 +203,6 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): repre = self._create_representation( frame_start, frame_end, file=filename, trim=_trim) - instance.data["originalBasename"] = os.path.splitext(filename)[0] - instance.data["originalDirname"] = self.staging_dir if repre: From 8eae684f01da394e18407692d89062b3385a3bd0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Feb 2023 17:53:36 +0100 Subject: [PATCH 217/483] polishing fixes --- .../publish/collect_otio_subset_resources.py | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 537aef683c..5daa1b40fe 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -26,28 +26,6 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): families = ["clip"] hosts = ["resolve", "hiero", "flame"] - def get_template_name(self, instance): - """Return anatomy template name to use for integration""" - - # Anatomy data is pre-filled by Collectors - context = instance.context - project_name = context.data["projectName"] - - # Task can be optional in anatomy data - host_name = context.data["hostName"] - family = instance.data["family"] - anatomy_data = instance.context.data["anatomyData"] - task_info = anatomy_data.get("task") or {} - - return get_publish_template_name( - project_name, - host_name, - family, - task_name=task_info.get("name"), - task_type=task_info.get("type"), - project_settings=context.data["project_settings"], - logger=self.log - ) def process(self, instance): @@ -116,6 +94,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): frame_start = instance.data["frameStart"] frame_end = frame_start + (media_out - media_in) + # Fit start /end frame to media in /out if "{originalDirname}" in template: frame_start = media_in frame_end = media_out @@ -258,3 +237,26 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): if kwargs.get("trim") is True: representation_data["tags"] = ["trim"] return representation_data + + def get_template_name(self, instance): + """Return anatomy template name to use for integration""" + + # Anatomy data is pre-filled by Collectors + context = instance.context + project_name = context.data["projectName"] + + # Task can be optional in anatomy data + host_name = context.data["hostName"] + family = instance.data["family"] + anatomy_data = instance.context.data["anatomyData"] + task_info = anatomy_data.get("task") or {} + + return get_publish_template_name( + project_name, + host_name, + family, + task_name=task_info.get("name"), + task_type=task_info.get("type"), + project_settings=context.data["project_settings"], + logger=self.log + ) \ No newline at end of file From c103ac19076736ded0755c54737c2f9af2b408ab Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 10 Feb 2023 17:54:54 +0100 Subject: [PATCH 218/483] wrong template key name --- openpype/plugins/publish/collect_otio_subset_resources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index 5daa1b40fe..dab52986da 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -95,7 +95,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): frame_end = frame_start + (media_out - media_in) # Fit start /end frame to media in /out - if "{originalDirname}" in template: + if "{originalBasename}" in template: frame_start = media_in frame_end = media_out From 80ac19d4c9af3e24f72738f820b64b546fc9b764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Tue, 14 Feb 2023 12:18:00 +0100 Subject: [PATCH 219/483] Update openpype/hosts/nuke/plugins/load/load_clip.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/plugins/load/load_clip.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/nuke/plugins/load/load_clip.py b/openpype/hosts/nuke/plugins/load/load_clip.py index f9364172ea..8f9b463037 100644 --- a/openpype/hosts/nuke/plugins/load/load_clip.py +++ b/openpype/hosts/nuke/plugins/load/load_clip.py @@ -222,13 +222,12 @@ class LoadClip(plugin.NukeLoader): representation = deepcopy(representation) context = representation["context"] template = representation["data"]["template"] - frame = context["frame"] - hashed_frame = "#" * len(str(frame)) - if ( "{originalBasename}" in template and "frame" in context ): + frame = context["frame"] + hashed_frame = "#" * len(str(frame)) origin_basename = context["originalBasename"] context["originalBasename"] = origin_basename.replace( frame, hashed_frame From 6a8f40f5bb29e8b1bd04497cffe433fc1792af3c Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 14 Feb 2023 14:27:42 +0100 Subject: [PATCH 220/483] anatomy data from instance rather then context --- .../plugins/publish/collect_otio_subset_resources.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/collect_otio_subset_resources.py b/openpype/plugins/publish/collect_otio_subset_resources.py index dab52986da..f659791d95 100644 --- a/openpype/plugins/publish/collect_otio_subset_resources.py +++ b/openpype/plugins/publish/collect_otio_subset_resources.py @@ -22,7 +22,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): """Get Resources for a subset version""" label = "Collect OTIO Subset Resources" - order = pyblish.api.CollectorOrder + 0.0021 + order = pyblish.api.CollectorOrder + 0.491 families = ["clip"] hosts = ["resolve", "hiero", "flame"] @@ -50,9 +50,9 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): # get basic variables otio_clip = instance.data["otioClip"] - otio_avalable_range = otio_clip.available_range() - media_fps = otio_avalable_range.start_time.rate - available_duration = otio_avalable_range.duration.value + otio_available_range = otio_clip.available_range() + media_fps = otio_available_range.start_time.rate + available_duration = otio_available_range.duration.value # get available range trimmed with processed retimes retimed_attributes = get_media_range_with_retimes( @@ -248,7 +248,7 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): # Task can be optional in anatomy data host_name = context.data["hostName"] family = instance.data["family"] - anatomy_data = instance.context.data["anatomyData"] + anatomy_data = instance.data["anatomyData"] task_info = anatomy_data.get("task") or {} return get_publish_template_name( @@ -259,4 +259,4 @@ class CollectOtioSubsetResources(pyblish.api.InstancePlugin): task_type=task_info.get("type"), project_settings=context.data["project_settings"], logger=self.log - ) \ No newline at end of file + ) From 2563d302fa0f9b1140b226e7e992b70bae403430 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 12:22:53 +0100 Subject: [PATCH 221/483] OP-4643 - fix colorspace from DCC representation["colorspaceData"]["colorspace"] is only input colorspace --- openpype/plugins/publish/extract_color_transcode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 82b92ec93e..456e40008d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -118,8 +118,7 @@ class ExtractOIIOTranscode(publish.Extractor): output_name, output_extension) - target_colorspace = (output_def["colorspace"] or - colorspace_data.get("colorspace")) + target_colorspace = output_def["colorspace"] view = output_def["view"] or colorspace_data.get("view") display = (output_def["display"] or colorspace_data.get("display")) From 8f5e958e60bf258bb05d8ca72576c20d410f08a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 11:37:20 +0100 Subject: [PATCH 222/483] handle disabled creators in create context --- openpype/pipeline/create/context.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/pipeline/create/context.py b/openpype/pipeline/create/context.py index 7672c49eb3..acc2bb054f 100644 --- a/openpype/pipeline/create/context.py +++ b/openpype/pipeline/create/context.py @@ -1390,6 +1390,8 @@ class CreateContext: self.autocreators = {} # Manual creators self.manual_creators = {} + # Creators that are disabled + self.disabled_creators = {} self.convertors_plugins = {} self.convertor_items_by_id = {} @@ -1667,6 +1669,7 @@ class CreateContext: # Discover and prepare creators creators = {} + disabled_creators = {} autocreators = {} manual_creators = {} report = discover_creator_plugins(return_report=True) @@ -1703,6 +1706,9 @@ class CreateContext: self, self.headless ) + if not creator.enabled: + disabled_creators[creator_identifier] = creator + continue creators[creator_identifier] = creator if isinstance(creator, AutoCreator): autocreators[creator_identifier] = creator @@ -1713,6 +1719,7 @@ class CreateContext: self.manual_creators = manual_creators self.creators = creators + self.disabled_creators = disabled_creators def _reset_convertor_plugins(self): convertors_plugins = {} From 8fc144b419976675a8f3b7b43f19d5a52c84b616 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 13:59:20 +0100 Subject: [PATCH 223/483] base of auto detect creator --- .../tvpaint/plugins/create/create_render.py | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 6a857676a5..2cb83b7fb7 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -35,6 +35,7 @@ Todos: """ import collections +from typing import Any from openpype.client import get_asset_by_name from openpype.lib import ( @@ -604,6 +605,201 @@ class CreateRenderPass(TVPaintCreator): return self.get_pre_create_attr_defs() +class TVPaintAutoDetectRenderCreator(TVPaintCreator): + """Create Render Layer and Render Pass instances based on scene data. + + This is auto-detection creator which can be triggered by user to create + instances based on information in scene. Each used color group in scene + will be created as Render Layer where group name is used as variant and + each TVPaint layer as Render Pass where layer name is used as variant. + """ + + family = "render" + label = "Auto detect renders" + identifier = "render.auto.detect.creator" + + # Settings + enabled = False + + def apply_settings(self, project_settings, system_settings): + plugin_settings = ( + project_settings + ["tvpaint"] + ["create"] + ["auto_detect_render_create"] + ) + self.enabled = plugin_settings["enabled"] + + def create(self, subset_name, instance_data, pre_create_data): + project_name: str = self.create_context.get_current_project_name() + asset_name: str = instance_data["asset"] + task_name: str = instance_data["task"] + asset_doc: dict[str, Any] = get_asset_by_name(project_name, asset_name) + + render_layers_by_group_id: dict[int, CreatedInstance] = {} + render_passes_by_render_layer_id: dict[int, CreatedInstance] = ( + collections.defaultdict(list) + ) + for instance in self.create_context.instances: + if instance.creator_identifier == CreateRenderlayer.identifier: + group_id = instance["creator_attributes"]["group_id"] + render_layers_by_group_id[group_id] = instance + elif instance.creator_identifier == CreateRenderPass.identifier: + render_layer_id = ( + instance + ["creator_attributes"] + ["render_layer_instance_id"] + ) + render_passes_by_render_layer_id[render_layer_id].append( + instance + ) + + layers_by_group_id: dict[int, list[dict[str, Any]]] = ( + collections.defaultdict(list) + ) + scene_layers: list[dict[str, Any]] = get_layers_data() + scene_groups: list[dict[str, Any]] = get_groups_data() + for layer in scene_layers: + group_id: int = layer["group_id"] + layers_by_group_id[group_id].append(layer) + + # Remove '0' (default) group + layers_by_group_id.pop(0, None) + + # Make sure all render layers are created + for group_id in layers_by_group_id.keys(): + render_layer_instance: CreatedInstance | None = ( + render_layers_by_group_id.get(group_id) + ) + + instance: CreatedInstance | None = self._prepare_render_layer( + project_name, + asset_doc, + task_name, + group_id, + scene_groups, + render_layer_instance + ) + if instance is not None: + render_layers_by_group_id[group_id] = instance + + for group_id, layers in layers_by_group_id.items(): + render_layer_instance: CreatedInstance | None = ( + render_layers_by_group_id.get(group_id) + ) + if render_layer_instance is not None: + continue + + self._prepare_render_passes( + project_name, + asset_doc, + task_name, + render_layer_instance, + layers, + render_passes_by_render_layer_id[render_layer_instance.id] + ) + + def _prepare_render_layer( + self, + project_name: str, + asset_doc: dict[str, Any], + task_name: str, + group_id: int, + groups: list[dict[str, Any]], + existing_instance: CreatedInstance | None=None + ) -> CreatedInstance | None: + match_group: dict[str, Any] | None = next( + ( + for group in groups + if group["group_id"] == group_id + ), + None + ) + if not match_group: + return None + + variant: str = match_group["name"] + creator: CreateRenderlayer = ( + self.create_context.creators[CreateRenderlayer.identifier] + ) + + subset_name: str = creator.get_subset_name( + variant, + task_name, + asset_doc, + project_name, + host_name=self.create_context.host_name, + ) + if existing_instance is not None: + existing_instance["asset"] = asset_doc["name"] + existing_instance["task"] = task_name + existing_instance["subset"] = subset_name + return existing_instance + + instance_data: dict[str, str] = { + "asset": asset_doc["name"], + "task": task_name, + "family": creator.family, + "variant": variant + } + pre_create_data: dict[str, str] = { + "group_id": group_id + } + return creator.create(subset_name, instance_data, pre_create_data) + + def _prepare_render_passes( + self, + project_name: str, + asset_doc: dict[str, Any], + task_name: str, + render_layer_instance: CreatedInstance, + layers: list[dict[str, Any]], + existing_render_passes: list[CreatedInstance] + ): + creator: CreateRenderPass = ( + self.create_context.creators[CreateRenderPass.identifier] + ) + render_pass_by_layer_name = {} + for render_pass in existing_render_passes: + for layer_name in render_pass["layer_names"]: + render_pass_by_layer_name[layer_name] = render_pass + + for layer in layers: + layer_name = layer["name"] + variant = layer_name + render_pass = render_pass_by_layer_name.get(layer_name) + if render_pass is not None: + if (render_pass["layer_names"]) > 1: + variant = render_pass["variant"] + + subset_name = creator.get_subset_name( + variant, + task_name, + asset_doc, + project_name, + host_name=self.create_context.host_name, + instance=render_pass + ) + + if render_pass is not None: + render_pass["asset"] = asset_doc["name"] + render_pass["task"] = task_name + render_pass["subset"] = subset_name + continue + + instance_data: dict[str, str] = { + "asset": asset_doc["name"], + "task": task_name, + "family": creator.family, + "variant": variant + } + pre_create_data: dict[str, Any] = { + "render_layer_instance_id": render_layer_instance.id, + "layer_names": [layer_name] + } + creator.create(subset_name, instance_data, pre_create_data) + + class TVPaintSceneRenderCreator(TVPaintAutoCreator): family = "render" subset_template_family_filter = "renderScene" From 2b9e40aece80efa238adf9ee0af9f6d8ea0911de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 14:04:50 +0100 Subject: [PATCH 224/483] added settings for auto-detect creator --- .../hosts/tvpaint/plugins/create/create_render.py | 3 ++- .../defaults/project_settings/tvpaint.json | 3 +++ .../projects_schema/schema_project_tvpaint.json | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 2cb83b7fb7..5239c7aeb9 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -617,6 +617,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): family = "render" label = "Auto detect renders" identifier = "render.auto.detect.creator" + order = CreateRenderPass.order + 10 # Settings enabled = False @@ -626,7 +627,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): project_settings ["tvpaint"] ["create"] - ["auto_detect_render_create"] + ["auto_detect_render"] ) self.enabled = plugin_settings["enabled"] diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 340181b3a4..1a0d0e22ab 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -40,6 +40,9 @@ "mark_for_review": true, "default_variant": "Main", "default_variants": [] + }, + "auto_detect_render": { + "enabled": false } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 55e60357e5..5639dee0c2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -195,6 +195,20 @@ } } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "auto_detect_render", + "label": "Auto-Detect Create Render", + "is_group": true, + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled" + } + ] } ] }, From 4a81519968796677482ccf1c8984a114ed9e9e0a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 10:29:37 +0100 Subject: [PATCH 225/483] add more options to create new instances --- .../tvpaint/plugins/create/create_render.py | 276 +++++++++++++----- 1 file changed, 209 insertions(+), 67 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 5239c7aeb9..cee56ab0f4 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -40,6 +40,8 @@ from typing import Any from openpype.client import get_asset_by_name from openpype.lib import ( prepare_template_data, + AbstractAttrDef, + UISeparatorDef, EnumDef, TextDef, BoolDef, @@ -612,6 +614,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): instances based on information in scene. Each used color group in scene will be created as Render Layer where group name is used as variant and each TVPaint layer as Render Pass where layer name is used as variant. + + Never will have any instances, all instances belong to different creators. """ family = "render" @@ -621,6 +625,10 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): # Settings enabled = False + allow_group_rename = True + group_name_template = "L{group_index}" + group_idx_offset = 10 + group_idx_padding = 3 def apply_settings(self, project_settings, system_settings): plugin_settings = ( @@ -630,75 +638,73 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): ["auto_detect_render"] ) self.enabled = plugin_settings["enabled"] + self.allow_group_rename = plugin_settings["allow_group_rename"] - def create(self, subset_name, instance_data, pre_create_data): - project_name: str = self.create_context.get_current_project_name() - asset_name: str = instance_data["asset"] - task_name: str = instance_data["task"] - asset_doc: dict[str, Any] = get_asset_by_name(project_name, asset_name) + def _rename_groups( + self, + groups_order, + scene_groups, + layers_by_group_id, + rename_only_visible, - render_layers_by_group_id: dict[int, CreatedInstance] = {} - render_passes_by_render_layer_id: dict[int, CreatedInstance] = ( - collections.defaultdict(list) - ) - for instance in self.create_context.instances: - if instance.creator_identifier == CreateRenderlayer.identifier: - group_id = instance["creator_attributes"]["group_id"] - render_layers_by_group_id[group_id] = instance - elif instance.creator_identifier == CreateRenderPass.identifier: - render_layer_id = ( - instance - ["creator_attributes"] - ["render_layer_instance_id"] - ) - render_passes_by_render_layer_id[render_layer_id].append( - instance - ) - - layers_by_group_id: dict[int, list[dict[str, Any]]] = ( - collections.defaultdict(list) - ) - scene_layers: list[dict[str, Any]] = get_layers_data() - scene_groups: list[dict[str, Any]] = get_groups_data() - for layer in scene_layers: - group_id: int = layer["group_id"] - layers_by_group_id[group_id].append(layer) - - # Remove '0' (default) group - layers_by_group_id.pop(0, None) - - # Make sure all render layers are created - for group_id in layers_by_group_id.keys(): - render_layer_instance: CreatedInstance | None = ( - render_layers_by_group_id.get(group_id) - ) - - instance: CreatedInstance | None = self._prepare_render_layer( - project_name, - asset_doc, - task_name, - group_id, - scene_groups, - render_layer_instance - ) - if instance is not None: - render_layers_by_group_id[group_id] = instance - - for group_id, layers in layers_by_group_id.items(): - render_layer_instance: CreatedInstance | None = ( - render_layers_by_group_id.get(group_id) - ) - if render_layer_instance is not None: + ): + new_group_name_by_id = {} + groups_by_id = { + group["group_id"]: group + for group in scene_groups + } + # Count only renamed groups + group_idx = 1 + for group_id in groups_order: + layers = layers_by_group_id[group_id] + if not layers: continue - self._prepare_render_passes( - project_name, - asset_doc, - task_name, - render_layer_instance, - layers, - render_passes_by_render_layer_id[render_layer_instance.id] + if ( + rename_only_visible + and not any( + layer + for layer in layers + if layer["visible"] + ) + ): + continue + group_index_value = ( + "{{:0<{}}}" + .format(self.group_idx_padding) + .format(group_idx * self.group_idx_offset) ) + group_name_fill_values = { + "groupIdx": group_index_value, + "groupidx": group_index_value, + "group_idx": group_index_value, + "group_index": group_index_value, + } + + group_name = self.group_name_template.format( + **group_name_fill_values + ) + group = groups_by_id[group_id] + if group["name"] != group_name: + new_group_name_by_id[group_id] = group_name + group_idx += 1 + + grg_lines = [] + for group_id, group_name in new_group_name_by_id.items(): + group = groups_by_id[group_id] + grg_line = "tv_layercolor \"setcolor\" {} {} {} {} {}".format( + group["clip_id"], + group_id, + group["red"], + group["green"], + group["blue"], + group_name + ) + grg_lines.append(grg_line) + group["name"] = group_name + + if grg_lines: + execute_george_through_file(grg_lines) def _prepare_render_layer( self, @@ -707,7 +713,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): task_name: str, group_id: int, groups: list[dict[str, Any]], - existing_instance: CreatedInstance | None=None + mark_for_review: bool, + existing_instance: CreatedInstance | None=None, ) -> CreatedInstance | None: match_group: dict[str, Any] | None = next( ( @@ -744,7 +751,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): "variant": variant } pre_create_data: dict[str, str] = { - "group_id": group_id + "group_id": group_id, + "mark_for_review": mark_for_review } return creator.create(subset_name, instance_data, pre_create_data) @@ -755,6 +763,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): task_name: str, render_layer_instance: CreatedInstance, layers: list[dict[str, Any]], + mark_for_review: bool, existing_render_passes: list[CreatedInstance] ): creator: CreateRenderPass = ( @@ -796,10 +805,143 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): } pre_create_data: dict[str, Any] = { "render_layer_instance_id": render_layer_instance.id, - "layer_names": [layer_name] + "layer_names": [layer_name], + "mark_for_review": mark_for_review } creator.create(subset_name, instance_data, pre_create_data) + def create(self, subset_name, instance_data, pre_create_data): + project_name: str = self.create_context.get_current_project_name() + asset_name: str = instance_data["asset"] + task_name: str = instance_data["task"] + asset_doc: dict[str, Any] = get_asset_by_name(project_name, asset_name) + + render_layers_by_group_id: dict[int, CreatedInstance] = {} + render_passes_by_render_layer_id: dict[int, list[CreatedInstance]] = ( + collections.defaultdict(list) + ) + for instance in self.create_context.instances: + if instance.creator_identifier == CreateRenderlayer.identifier: + group_id = instance["creator_attributes"]["group_id"] + render_layers_by_group_id[group_id] = instance + elif instance.creator_identifier == CreateRenderPass.identifier: + render_layer_id = ( + instance + ["creator_attributes"] + ["render_layer_instance_id"] + ) + render_passes_by_render_layer_id[render_layer_id].append( + instance + ) + + layers_by_group_id: dict[int, list[dict[str, Any]]] = ( + collections.defaultdict(list) + ) + scene_layers: list[dict[str, Any]] = get_layers_data() + scene_groups: list[dict[str, Any]] = get_groups_data() + groups_order: list[int] = [] + for layer in scene_layers: + group_id: int = layer["group_id"] + # Skip 'default' group + if group_id == 0: + continue + + layers_by_group_id[group_id].append(layer) + if group_id not in groups_order: + groups_order.append(group_id) + + mark_layers_for_review = pre_create_data.get( + "mark_layers_for_review", False + ) + mark_passes_for_review = pre_create_data.get( + "mark_passes_for_review", False + ) + rename_groups = pre_create_data.get("rename_groups", False) + rename_only_visible = pre_create_data.get("rename_only_visible", False) + if rename_groups: + self._rename_groups( + groups_order, + scene_groups, + layers_by_group_id, + rename_only_visible + ) + + # Make sure all render layers are created + for group_id in layers_by_group_id.keys(): + render_layer_instance: CreatedInstance | None = ( + render_layers_by_group_id.get(group_id) + ) + + instance: CreatedInstance | None = self._prepare_render_layer( + project_name, + asset_doc, + task_name, + group_id, + scene_groups, + mark_layers_for_review, + render_layer_instance, + ) + if instance is not None: + render_layers_by_group_id[group_id] = instance + + for group_id, layers in layers_by_group_id.items(): + render_layer_instance: CreatedInstance | None = ( + render_layers_by_group_id.get(group_id) + ) + if render_layer_instance is not None: + continue + + self._prepare_render_passes( + project_name, + asset_doc, + task_name, + render_layer_instance, + layers, + mark_passes_for_review, + render_passes_by_render_layer_id[render_layer_instance.id] + ) + + def get_pre_create_attr_defs(self) -> list[AbstractAttrDef]: + render_layer_creator: CreateRenderlayer = ( + self.create_context.creators[CreateRenderlayer.identifier] + ) + render_pass_creator: CreateRenderPass = ( + self.create_context.creators[CreateRenderPass.identifier] + ) + output = [] + if self.allow_group_rename: + output.extend([ + BoolDef( + "rename_groups", + label="Rename color groups", + tooltip="Will rename color groups using studio template", + default=True + ), + BoolDef( + "rename_only_visible", + label="Only visible color groups", + tooltip=( + "Rename of groups will affect only groups with visible" + " layers." + ), + default=True + ), + UISeparatorDef() + ]) + output.extend([ + BoolDef( + "mark_layers_for_review", + label="Mark RenderLayers for review", + default=render_layer_creator.mark_for_review + ), + BoolDef( + "mark_passes_for_review", + label="Mark RenderPasses for review", + default=render_pass_creator.mark_for_review + ) + ]) + return output + class TVPaintSceneRenderCreator(TVPaintAutoCreator): family = "render" From 5fdadaf454c94cd8e575b5d71a0768cb10385978 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 10:30:55 +0100 Subject: [PATCH 226/483] added more settings --- .../tvpaint/plugins/create/create_render.py | 3 ++ .../defaults/project_settings/tvpaint.json | 6 +++- .../schema_project_tvpaint.json | 28 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index cee56ab0f4..129b8dc1c5 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -639,6 +639,9 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): ) self.enabled = plugin_settings["enabled"] self.allow_group_rename = plugin_settings["allow_group_rename"] + self.group_name_template = plugin_settings["group_name_template"] + self.group_idx_offset = plugin_settings["group_idx_offset"] + self.group_idx_padding = plugin_settings["group_idx_padding"] def _rename_groups( self, diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 1a0d0e22ab..1cae94f590 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -42,7 +42,11 @@ "default_variants": [] }, "auto_detect_render": { - "enabled": false + "enabled": false, + "allow_group_rename": true, + "group_name_template": "L{group_index}", + "group_idx_offset": 10, + "group_idx_padding": 3 } }, "publish": { diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 5639dee0c2..05cfd99047 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -207,6 +207,34 @@ { "type": "boolean", "key": "enabled" + }, + { + "type": "label", + "label": "The creator tries to auto-detect Render Layers and Render Passes in scene. For Render Layers is used group name as a variant and for Render Passes is used TVPaint layer name.

Group names can be renamed by their used order in scene. The renaming template where can be used {group_index} formatting key which is filled by \"used position index of group\".
- Template: L{group_index}
- Group offset: 10
- Group padding: 3
Would create group names \"L010\", \"L020\", ..." + }, + { + "type": "boolean", + "key": "allow_group_rename", + "label": "Allow group rename" + }, + { + "type": "text", + "key": "group_name_template", + "label": "Group name template" + }, + { + "key": "group_idx_offset", + "label": "Group index Offset", + "type": "number", + "decimal": 0, + "minimum": 1 + }, + { + "key": "group_idx_padding", + "type": "number", + "label": "Group index Padding", + "decimal": 0, + "minimum": 1 } ] } From 51f43076b560e858b78289ce9954d814243c6d3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 15:33:08 +0100 Subject: [PATCH 227/483] fix smaller bugs in logic --- openpype/hosts/tvpaint/plugins/create/create_render.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 129b8dc1c5..7d241f93f6 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -673,7 +673,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): ): continue group_index_value = ( - "{{:0<{}}}" + "{{:0>{}}}" .format(self.group_idx_padding) .format(group_idx * self.group_idx_offset) ) @@ -707,7 +707,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): group["name"] = group_name if grg_lines: - execute_george_through_file(grg_lines) + execute_george_through_file("\n".join(grg_lines)) def _prepare_render_layer( self, @@ -853,6 +853,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): if group_id not in groups_order: groups_order.append(group_id) + groups_order.reverse() + mark_layers_for_review = pre_create_data.get( "mark_layers_for_review", False ) @@ -891,7 +893,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): render_layer_instance: CreatedInstance | None = ( render_layers_by_group_id.get(group_id) ) - if render_layer_instance is not None: + if render_layer_instance is None: continue self._prepare_render_passes( From a2074308142dbc999ac4175b01cfb7affc6ebe0b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 15:40:20 +0100 Subject: [PATCH 228/483] fix groups iter --- openpype/hosts/tvpaint/plugins/create/create_render.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 7d241f93f6..2d4f9f1bc1 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -35,7 +35,7 @@ Todos: """ import collections -from typing import Any +from typing import Any, Optional, Union from openpype.client import get_asset_by_name from openpype.lib import ( @@ -719,8 +719,9 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): mark_for_review: bool, existing_instance: CreatedInstance | None=None, ) -> CreatedInstance | None: - match_group: dict[str, Any] | None = next( + match_group: Union[dict[str, Any], None] = next( ( + group for group in groups if group["group_id"] == group_id ), From a44f928804f21b893bdace9b74b186b079426b4d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 15:41:03 +0100 Subject: [PATCH 229/483] handle valid group ids --- .../hosts/tvpaint/plugins/create/create_render.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 2d4f9f1bc1..c7b80ae256 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -653,6 +653,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): ): new_group_name_by_id = {} groups_by_id = { + valid_group_ids: set[int] = set() group["group_id"]: group for group in scene_groups } @@ -672,7 +673,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): ) ): continue - group_index_value = ( + valid_group_ids.add(group_id) + group_index_value: str = ( "{{:0>{}}}" .format(self.group_idx_padding) .format(group_idx * self.group_idx_offset) @@ -708,6 +710,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): if grg_lines: execute_george_through_file("\n".join(grg_lines)) + return valid_group_ids def _prepare_render_layer( self, @@ -865,16 +868,20 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): rename_groups = pre_create_data.get("rename_groups", False) rename_only_visible = pre_create_data.get("rename_only_visible", False) if rename_groups: - self._rename_groups( + valid_group_ids: set[int] = self._rename_groups( groups_order, scene_groups, layers_by_group_id, rename_only_visible ) + else: + valid_group_ids: set[int] = set(groups_order) # Make sure all render layers are created for group_id in layers_by_group_id.keys(): - render_layer_instance: CreatedInstance | None = ( + if group_id not in valid_group_ids: + continue + render_layer_instance: Union[CreatedInstance, None] = ( render_layers_by_group_id.get(group_id) ) From faaade284ab14d8cfe4dade4011981c0f0d26623 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 15:41:44 +0100 Subject: [PATCH 230/483] added type hints --- .../tvpaint/plugins/create/create_render.py | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index c7b80ae256..883383ec76 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -645,22 +645,21 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): def _rename_groups( self, - groups_order, - scene_groups, - layers_by_group_id, - rename_only_visible, - - ): - new_group_name_by_id = {} - groups_by_id = { + groups_order: list[int], + scene_groups: list[dict[str, Any]], + layers_by_group_id: dict[int, dict[str, Any]], + rename_only_visible: bool, + ) -> set[int]: valid_group_ids: set[int] = set() + new_group_name_by_id: dict[int, str] = {} + groups_by_id: dict[int, dict[str, Any]] = { group["group_id"]: group for group in scene_groups } # Count only renamed groups - group_idx = 1 + group_idx: int = 1 for group_id in groups_order: - layers = layers_by_group_id[group_id] + layers: list[dict[str, Any]] = layers_by_group_id[group_id] if not layers: continue @@ -679,25 +678,25 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): .format(self.group_idx_padding) .format(group_idx * self.group_idx_offset) ) - group_name_fill_values = { + group_name_fill_values: dict[str, str] = { "groupIdx": group_index_value, "groupidx": group_index_value, "group_idx": group_index_value, "group_index": group_index_value, } - group_name = self.group_name_template.format( + group_name: str = self.group_name_template.format( **group_name_fill_values ) - group = groups_by_id[group_id] + group: dict[str, Any] = groups_by_id[group_id] if group["name"] != group_name: new_group_name_by_id[group_id] = group_name group_idx += 1 - grg_lines = [] + grg_lines: list[str] = [] for group_id, group_name in new_group_name_by_id.items(): - group = groups_by_id[group_id] - grg_line = "tv_layercolor \"setcolor\" {} {} {} {} {}".format( + group: dict[str, Any] = groups_by_id[group_id] + grg_line: str = "tv_layercolor \"setcolor\" {} {} {} {} {}".format( group["clip_id"], group_id, group["red"], @@ -720,8 +719,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): group_id: int, groups: list[dict[str, Any]], mark_for_review: bool, - existing_instance: CreatedInstance | None=None, - ) -> CreatedInstance | None: + existing_instance: Optional[CreatedInstance]=None, + ) -> Union[CreatedInstance, None]: match_group: Union[dict[str, Any], None] = next( ( group @@ -885,7 +884,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): render_layers_by_group_id.get(group_id) ) - instance: CreatedInstance | None = self._prepare_render_layer( + instance: Union[CreatedInstance, None] = self._prepare_render_layer( project_name, asset_doc, task_name, @@ -898,7 +897,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): render_layers_by_group_id[group_id] = instance for group_id, layers in layers_by_group_id.items(): - render_layer_instance: CreatedInstance | None = ( + render_layer_instance: Union[CreatedInstance, None] = ( render_layers_by_group_id.get(group_id) ) if render_layer_instance is None: From 07c87b2efd8c6eec6c1df2ec5d8110eda6d5af4b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 16:18:40 +0100 Subject: [PATCH 231/483] fix filtering of groups --- .../tvpaint/plugins/create/create_render.py | 108 +++++++++--------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 883383ec76..2e17995b7a 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -646,37 +646,19 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): def _rename_groups( self, groups_order: list[int], - scene_groups: list[dict[str, Any]], - layers_by_group_id: dict[int, dict[str, Any]], - rename_only_visible: bool, - ) -> set[int]: - valid_group_ids: set[int] = set() + scene_groups: list[dict[str, Any]] + ): new_group_name_by_id: dict[int, str] = {} groups_by_id: dict[int, dict[str, Any]] = { group["group_id"]: group for group in scene_groups } # Count only renamed groups - group_idx: int = 1 - for group_id in groups_order: - layers: list[dict[str, Any]] = layers_by_group_id[group_id] - if not layers: - continue - - if ( - rename_only_visible - and not any( - layer - for layer in layers - if layer["visible"] - ) - ): - continue - valid_group_ids.add(group_id) + for idx, group_id in enumerate(groups_order): group_index_value: str = ( "{{:0>{}}}" .format(self.group_idx_padding) - .format(group_idx * self.group_idx_offset) + .format((idx + 1) * self.group_idx_offset) ) group_name_fill_values: dict[str, str] = { "groupIdx": group_index_value, @@ -691,7 +673,6 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): group: dict[str, Any] = groups_by_id[group_id] if group["name"] != group_name: new_group_name_by_id[group_id] = group_name - group_idx += 1 grg_lines: list[str] = [] for group_id, group_name in new_group_name_by_id.items(): @@ -709,7 +690,6 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): if grg_lines: execute_george_through_file("\n".join(grg_lines)) - return valid_group_ids def _prepare_render_layer( self, @@ -816,6 +796,30 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): } creator.create(subset_name, instance_data, pre_create_data) + def _filter_groups( + self, + layers_by_group_id, + groups_order, + only_visible_groups + ): + new_groups_order = [] + for group_id in groups_order: + layers: list[dict[str, Any]] = layers_by_group_id[group_id] + if not layers: + continue + + if ( + only_visible_groups + and not any( + layer + for layer in layers + if layer["visible"] + ) + ): + continue + new_groups_order.append(group_id) + return new_groups_order + def create(self, subset_name, instance_data, pre_create_data): project_name: str = self.create_context.get_current_project_name() asset_name: str = instance_data["asset"] @@ -865,42 +869,40 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): "mark_passes_for_review", False ) rename_groups = pre_create_data.get("rename_groups", False) - rename_only_visible = pre_create_data.get("rename_only_visible", False) + only_visible_groups = pre_create_data.get("only_visible_groups", False) + groups_order = self._filter_groups( + layers_by_group_id, + groups_order, + only_visible_groups + ) + if not groups_order: + return + if rename_groups: - valid_group_ids: set[int] = self._rename_groups( - groups_order, - scene_groups, - layers_by_group_id, - rename_only_visible - ) - else: - valid_group_ids: set[int] = set(groups_order) + self._rename_groups(groups_order, scene_groups) # Make sure all render layers are created - for group_id in layers_by_group_id.keys(): - if group_id not in valid_group_ids: - continue - render_layer_instance: Union[CreatedInstance, None] = ( - render_layers_by_group_id.get(group_id) - ) - - instance: Union[CreatedInstance, None] = self._prepare_render_layer( - project_name, - asset_doc, - task_name, - group_id, - scene_groups, - mark_layers_for_review, - render_layer_instance, + for group_id in groups_order: + instance: Union[CreatedInstance, None] = ( + self._prepare_render_layer( + project_name, + asset_doc, + task_name, + group_id, + scene_groups, + mark_layers_for_review, + render_layers_by_group_id.get(group_id), + ) ) if instance is not None: render_layers_by_group_id[group_id] = instance - for group_id, layers in layers_by_group_id.items(): + for group_id in groups_order: + layers: list[dict[str, Any]] = layers_by_group_id[group_id] render_layer_instance: Union[CreatedInstance, None] = ( render_layers_by_group_id.get(group_id) ) - if render_layer_instance is None: + if not layers or render_layer_instance is None: continue self._prepare_render_passes( @@ -930,11 +932,11 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): default=True ), BoolDef( - "rename_only_visible", + "only_visible_groups", label="Only visible color groups", tooltip=( - "Rename of groups will affect only groups with visible" - " layers." + "Render Layers and rename will happen only on color" + " groups with visible layers." ), default=True ), From abb3134bb4c8bb7413d415e29b5a950715ab23ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 21 Feb 2023 18:16:33 +0100 Subject: [PATCH 232/483] fix whitespace --- openpype/hosts/tvpaint/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 2e17995b7a..f16c3a437b 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -699,7 +699,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): group_id: int, groups: list[dict[str, Any]], mark_for_review: bool, - existing_instance: Optional[CreatedInstance]=None, + existing_instance: Optional[CreatedInstance] = None, ) -> Union[CreatedInstance, None]: match_group: Union[dict[str, Any], None] = next( ( From c334a5fc221fa6650d67c38338f9c71f96f11824 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 11:43:33 +0100 Subject: [PATCH 233/483] removed 'enabled' from 'auto_detect_render' settings --- openpype/settings/defaults/project_settings/tvpaint.json | 1 - .../schemas/projects_schema/schema_project_tvpaint.json | 5 ----- 2 files changed, 6 deletions(-) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 1cae94f590..40603ed874 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -42,7 +42,6 @@ "default_variants": [] }, "auto_detect_render": { - "enabled": false, "allow_group_rename": true, "group_name_template": "L{group_index}", "group_idx_offset": 10, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 05cfd99047..57016a8311 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -202,12 +202,7 @@ "key": "auto_detect_render", "label": "Auto-Detect Create Render", "is_group": true, - "checkbox_key": "enabled", "children": [ - { - "type": "boolean", - "key": "enabled" - }, { "type": "label", "label": "The creator tries to auto-detect Render Layers and Render Passes in scene. For Render Layers is used group name as a variant and for Render Passes is used TVPaint layer name.

Group names can be renamed by their used order in scene. The renaming template where can be used {group_index} formatting key which is filled by \"used position index of group\".
- Template: L{group_index}
- Group offset: 10
- Group padding: 3
Would create group names \"L010\", \"L020\", ..." From 0f20ed34a2a811599dccd2ebb46a2464cd66ebad Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 12:32:16 +0100 Subject: [PATCH 234/483] change label and add description to creator --- .../tvpaint/plugins/create/create_render.py | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index f16c3a437b..40386efe91 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -89,6 +89,25 @@ to match group color of Render Layer. ) +AUTODETECT_RENDER_DETAILED_DESCRIPTION = ( + """Semi-automated Render Layer and Render Pass creation. + +Based on information in TVPaint scene will be created Render Layers and Render +Passes. All color groups used in scene will be used for Render Layer creation. +Name of the group is used as a variant. + +All TVPaint layers under the color group will be created as Render Pass where +layer name is used as variant. + +The plugin will use all used color groups and layers, or can skip those that +are not visible. + +There is option to auto-rename color groups before Render Layer creation. That +is based on settings template where is filled index of used group from bottom +to top. +""" +) + class CreateRenderlayer(TVPaintCreator): """Mark layer group as Render layer instance. @@ -619,9 +638,13 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): """ family = "render" - label = "Auto detect renders" + label = "Render Layer/Passes" identifier = "render.auto.detect.creator" order = CreateRenderPass.order + 10 + description = ( + "Create Render Layers and Render Passes based on scene setup" + ) + detailed_description = AUTODETECT_RENDER_DETAILED_DESCRIPTION # Settings enabled = False From e0879fd67d642f7e34e4e70ba43979fc1cdb6dd9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 16 Feb 2023 17:03:04 +0100 Subject: [PATCH 235/483] Pass creation flags on windows in UI executables --- openpype/lib/execute.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 39532b7aa5..e60bffbd7a 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -74,18 +74,23 @@ def execute(args, return popen.returncode -def run_subprocess(*args, **kwargs): +def run_subprocess(*args, creationflags=None, **kwargs): """Convenience method for getting output errors for subprocess. Output logged when process finish. Entered arguments and keyword arguments are passed to subprocess Popen. + Set 'creationflags' to '0' (int) if auto-fix for creation of new window + should be ignored. + Args: - *args: Variable length arument list passed to Popen. + *args: Variable length argument list passed to Popen. + creationflags (int): Creation flags for 'subprocess.Popen'. + Differentiate on OS. **kwargs : Arbitrary keyword arguments passed to Popen. Is possible to - pass `logging.Logger` object under "logger" if want to use - different than lib's logger. + pass `logging.Logger` object under "logger" to use custom logger + for output. Returns: str: Full output of subprocess concatenated stdout and stderr. @@ -95,6 +100,21 @@ def run_subprocess(*args, **kwargs): return code. """ + # Modify creation flags on windows to hide console window if in UI mode + if ( + sys.__stdout__ is None + and platform.system().lower() == "windows" + and creationflags is None + ): + creationflags = ( + subprocess.CREATE_NEW_PROCESS_GROUP + | subprocess.DETACHED_PROCESS + ) + + # Ignore if creation flags is set to '0' or 'None' + if creationflags: + kwargs["creationflags"] = creationflags + # Get environents from kwarg or use current process environments if were # not passed. env = kwargs.get("env") or os.environ From 567b6dfb140bafe31fa4c3daef052315e65efb67 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 17 Feb 2023 09:59:47 +0100 Subject: [PATCH 236/483] on windows always autofill creation flags --- openpype/lib/execute.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index e60bffbd7a..6a3d777427 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -102,8 +102,7 @@ def run_subprocess(*args, creationflags=None, **kwargs): # Modify creation flags on windows to hide console window if in UI mode if ( - sys.__stdout__ is None - and platform.system().lower() == "windows" + platform.system().lower() == "windows" and creationflags is None ): creationflags = ( From e69ba621d1063b9c2cd72448a3f49512e83b38b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 17 Feb 2023 10:01:05 +0100 Subject: [PATCH 237/483] unify quotes --- openpype/lib/execute.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 6a3d777427..47b4255e3b 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -126,10 +126,10 @@ def run_subprocess(*args, creationflags=None, **kwargs): logger = Logger.get_logger("run_subprocess") # set overrides - kwargs['stdout'] = kwargs.get('stdout', subprocess.PIPE) - kwargs['stderr'] = kwargs.get('stderr', subprocess.PIPE) - kwargs['stdin'] = kwargs.get('stdin', subprocess.PIPE) - kwargs['env'] = filtered_env + kwargs["stdout"] = kwargs.get("stdout", subprocess.PIPE) + kwargs["stderr"] = kwargs.get("stderr", subprocess.PIPE) + kwargs["stdin"] = kwargs.get("stdin", subprocess.PIPE) + kwargs["env"] = filtered_env proc = subprocess.Popen(*args, **kwargs) From 0f9e12955c1553e61eac5c9c43ddfa3b46e8cdeb Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 10:23:24 +0100 Subject: [PATCH 238/483] look for creationflags in kwargs instead of explicit argument --- openpype/lib/execute.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 47b4255e3b..6c1361d7c2 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -74,20 +74,18 @@ def execute(args, return popen.returncode -def run_subprocess(*args, creationflags=None, **kwargs): +def run_subprocess(*args, **kwargs): """Convenience method for getting output errors for subprocess. Output logged when process finish. Entered arguments and keyword arguments are passed to subprocess Popen. - Set 'creationflags' to '0' (int) if auto-fix for creation of new window - should be ignored. + On windows are 'creationflags' filled with flags that should cause ignore + creation of new window. Args: *args: Variable length argument list passed to Popen. - creationflags (int): Creation flags for 'subprocess.Popen'. - Differentiate on OS. **kwargs : Arbitrary keyword arguments passed to Popen. Is possible to pass `logging.Logger` object under "logger" to use custom logger for output. @@ -103,17 +101,13 @@ def run_subprocess(*args, creationflags=None, **kwargs): # Modify creation flags on windows to hide console window if in UI mode if ( platform.system().lower() == "windows" - and creationflags is None + and "creationflags" not in kwargs ): - creationflags = ( + kwargs["creationflags"] = ( subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS ) - # Ignore if creation flags is set to '0' or 'None' - if creationflags: - kwargs["creationflags"] = creationflags - # Get environents from kwarg or use current process environments if were # not passed. env = kwargs.get("env") or os.environ From ed909ca5278825b1d897aaa1d975090a13ecf5b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 10:23:59 +0100 Subject: [PATCH 239/483] add 'CREATE_NO_WINDOW' to flags (if is available) --- openpype/lib/execute.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index 6c1361d7c2..f5745f9fdc 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -103,9 +103,11 @@ def run_subprocess(*args, **kwargs): platform.system().lower() == "windows" and "creationflags" not in kwargs ): + no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) kwargs["creationflags"] = ( subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS + | no_window ) # Get environents from kwarg or use current process environments if were From c177c26c863dbfee91cb67e78d5e54f8c5f44f5d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 20 Feb 2023 19:02:59 +0100 Subject: [PATCH 240/483] safe access to constants --- openpype/lib/execute.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index f5745f9fdc..759a4db0cb 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -103,11 +103,10 @@ def run_subprocess(*args, **kwargs): platform.system().lower() == "windows" and "creationflags" not in kwargs ): - no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) kwargs["creationflags"] = ( subprocess.CREATE_NEW_PROCESS_GROUP - | subprocess.DETACHED_PROCESS - | no_window + | getattr(subprocess, "DETACHED_PROCESS", 0) + | getattr(subprocess, "CREATE_NO_WINDOW", 0) ) # Get environents from kwarg or use current process environments if were From cf17ff50b572b0f55b9c60b9981f872cd79ef34f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 10:41:01 +0100 Subject: [PATCH 241/483] use detached process in ffprobe --- openpype/lib/transcoding.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 57279d0380..039255d937 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -5,6 +5,7 @@ import json import collections import tempfile import subprocess +import platform import xml.etree.ElementTree @@ -745,11 +746,18 @@ def get_ffprobe_data(path_to_file, logger=None): logger.debug("FFprobe command: {}".format( subprocess.list2cmdline(args) )) - popen = subprocess.Popen( - args, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE - ) + kwargs = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + } + if platform.system().lower() == "windows": + kwargs["creationflags"] = ( + subprocess.CREATE_NEW_PROCESS_GROUP + | getattr(subprocess, "DETACHED_PROCESS", 0) + | getattr(subprocess, "CREATE_NO_WINDOW", 0) + ) + + popen = subprocess.Popen(args, **kwargs) popen_stdout, popen_stderr = popen.communicate() if popen_stdout: From 79eb375c06d1dfeb6cb65c87f69c5f0476a05eb2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 10:43:21 +0100 Subject: [PATCH 242/483] hide console in extract burnin --- openpype/scripts/otio_burnin.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/openpype/scripts/otio_burnin.py b/openpype/scripts/otio_burnin.py index 3e40bf0c8b..cb4646c099 100644 --- a/openpype/scripts/otio_burnin.py +++ b/openpype/scripts/otio_burnin.py @@ -52,7 +52,16 @@ def _get_ffprobe_data(source): "-show_streams", source ] - proc = subprocess.Popen(command, stdout=subprocess.PIPE) + kwargs = { + "stdout": subprocess.PIPE, + } + if platform.system().lower() == "windows": + kwargs["creationflags"] = ( + subprocess.CREATE_NEW_PROCESS_GROUP + | getattr(subprocess, "DETACHED_PROCESS", 0) + | getattr(subprocess, "CREATE_NO_WINDOW", 0) + ) + proc = subprocess.Popen(command, **kwargs) out = proc.communicate()[0] if proc.returncode != 0: raise RuntimeError("Failed to run: %s" % command) @@ -331,12 +340,18 @@ class ModifiedBurnins(ffmpeg_burnins.Burnins): ) print("Launching command: {}".format(command)) - proc = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True - ) + kwargs = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + "shell": True, + } + if platform.system().lower() == "windows": + kwargs["creationflags"] = ( + subprocess.CREATE_NEW_PROCESS_GROUP + | getattr(subprocess, "DETACHED_PROCESS", 0) + | getattr(subprocess, "CREATE_NO_WINDOW", 0) + ) + proc = subprocess.Popen(command, **kwargs) _stdout, _stderr = proc.communicate() if _stdout: From 3cb530ce6048bf2e5cb85798b66d11893ba7acfb Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Feb 2023 22:19:47 +0800 Subject: [PATCH 243/483] bug fix for not being able to remove item in scene inventory --- openpype/hosts/max/plugins/load/load_pointcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/max/plugins/load/load_pointcache.py b/openpype/hosts/max/plugins/load/load_pointcache.py index 65d0662faa..f7a72ece25 100644 --- a/openpype/hosts/max/plugins/load/load_pointcache.py +++ b/openpype/hosts/max/plugins/load/load_pointcache.py @@ -80,7 +80,7 @@ importFile @"{file_path}" #noPrompt def remove(self, container): from pymxs import runtime as rt - node = container["node"] + node = rt.getNodeByName(container["instance_node"]) rt.delete(node) @staticmethod From 6860d6dcfeac07138887ea34ffbe2e56d102b098 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Feb 2023 22:51:15 +0800 Subject: [PATCH 244/483] bug fix for not being able to remove item in scene inventory --- .../hosts/max/plugins/load/load_camera_fbx.py | 27 ++++++++++++++---- .../hosts/max/plugins/load/load_max_scene.py | 28 +++++++++++++------ 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 1b1df364c1..23933b29e6 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -1,7 +1,10 @@ import os from openpype.pipeline import ( - load + load, + get_representation_path ) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib class FbxLoader(load.LoaderPlugin): @@ -36,14 +39,26 @@ importFile @"{filepath}" #noPrompt using:FBXIMP container_name = f"{name}_CON" asset = rt.getNodeByName(f"{name}") - # rename the container with "_CON" - container = rt.container(name=container_name) - asset.Parent = container - return container + return containerise( + name, [asset], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + alembic_objects = self.get_container_children(node, "AlembicObject") + for alembic_object in alembic_objects: + alembic_object.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) def remove(self, container): from pymxs import runtime as rt - node = container["node"] + node = rt.getNodeByName(container["instance_node"]) rt.delete(node) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 57f172cf6a..57a74c7ad7 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -1,7 +1,9 @@ import os from openpype.pipeline import ( - load + load, get_representation_path ) +from openpype.hosts.max.api.pipeline import containerise +from openpype.hosts.max.api import lib class MaxSceneLoader(load.LoaderPlugin): @@ -35,16 +37,26 @@ class MaxSceneLoader(load.LoaderPlugin): self.log.error("Something failed when loading.") max_container = max_containers.pop() - container_name = f"{name}_CON" - # rename the container with "_CON" - # get the original container - container = rt.container(name=container_name) - max_container.Parent = container - return container + return containerise( + name, [max_container], context, loader=self.__class__.__name__) + + def update(self, container, representation): + from pymxs import runtime as rt + + path = get_representation_path(representation) + node = rt.getNodeByName(container["instance_node"]) + + alembic_objects = self.get_container_children(node, "AlembicObject") + for alembic_object in alembic_objects: + alembic_object.source = path + + lib.imprint(container["instance_node"], { + "representation": str(representation["_id"]) + }) def remove(self, container): from pymxs import runtime as rt - node = container["node"] + node = rt.getNodeByName(container["instance_node"]) rt.delete(node) From 0ef87d0949105a386ba4e3200e421ee1af130176 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Feb 2023 23:46:34 +0800 Subject: [PATCH 245/483] renaming some variables --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 6 +++--- openpype/hosts/max/plugins/load/load_max_scene.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index 23933b29e6..e6eac25cfc 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -49,9 +49,9 @@ importFile @"{filepath}" #noPrompt using:FBXIMP path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - alembic_objects = self.get_container_children(node, "AlembicObject") - for alembic_object in alembic_objects: - alembic_object.source = path + fbx_objects = self.get_container_children(node, "AlembicObject") + for fbx_object in fbx_objects: + fbx_object.source = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 57a74c7ad7..13cf1e6019 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -47,9 +47,9 @@ class MaxSceneLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - alembic_objects = self.get_container_children(node, "AlembicObject") - for alembic_object in alembic_objects: - alembic_object.source = path + max_objects = self.get_container_children(node, "AlembicObject") + for max_object in max_objects: + max_object.source = path lib.imprint(container["instance_node"], { "representation": str(representation["_id"]) From 810b57ffb190434aa433511c17aa38c792aac618 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 22 Feb 2023 23:58:02 +0800 Subject: [PATCH 246/483] renaming some variables --- openpype/hosts/max/plugins/load/load_camera_fbx.py | 2 +- openpype/hosts/max/plugins/load/load_max_scene.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/max/plugins/load/load_camera_fbx.py b/openpype/hosts/max/plugins/load/load_camera_fbx.py index e6eac25cfc..3a6947798e 100644 --- a/openpype/hosts/max/plugins/load/load_camera_fbx.py +++ b/openpype/hosts/max/plugins/load/load_camera_fbx.py @@ -49,7 +49,7 @@ importFile @"{filepath}" #noPrompt using:FBXIMP path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - fbx_objects = self.get_container_children(node, "AlembicObject") + fbx_objects = self.get_container_children(node) for fbx_object in fbx_objects: fbx_object.source = path diff --git a/openpype/hosts/max/plugins/load/load_max_scene.py b/openpype/hosts/max/plugins/load/load_max_scene.py index 13cf1e6019..b863b9363f 100644 --- a/openpype/hosts/max/plugins/load/load_max_scene.py +++ b/openpype/hosts/max/plugins/load/load_max_scene.py @@ -47,7 +47,7 @@ class MaxSceneLoader(load.LoaderPlugin): path = get_representation_path(representation) node = rt.getNodeByName(container["instance_node"]) - max_objects = self.get_container_children(node, "AlembicObject") + max_objects = self.get_container_children(node) for max_object in max_objects: max_object.source = path From 9bf7246f52e085c8bd61816b0516c72f275536c3 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Tue, 21 Feb 2023 18:43:28 +0100 Subject: [PATCH 247/483] Fix typos --- .../nuke/plugins/publish/extract_review_data.py | 2 +- .../plugins/publish/submit_publish_job.py | 16 ++++++++-------- .../publish/integrate_shotgrid_publish.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/plugins/publish/extract_review_data.py b/openpype/hosts/nuke/plugins/publish/extract_review_data.py index 3c85b21b08..dee8248295 100644 --- a/openpype/hosts/nuke/plugins/publish/extract_review_data.py +++ b/openpype/hosts/nuke/plugins/publish/extract_review_data.py @@ -23,7 +23,7 @@ class ExtractReviewData(publish.Extractor): representations = instance.data.get("representations", []) # review can be removed since `ProcessSubmittedJobOnFarm` will create - # reviable representation if needed + # reviewable representation if needed if ( "render.farm" in instance.data["families"] and "review" in instance.data["families"] diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index c7a559466c..b8edd0f161 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -194,7 +194,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): metadata_path = os.path.join(output_dir, metadata_filename) # Convert output dir to `{root}/rest/of/path/...` with Anatomy - success, roothless_mtdt_p = self.anatomy.find_root_template_from_path( + success, rootless_mtdt_p = self.anatomy.find_root_template_from_path( metadata_path) if not success: # `rootless_path` is not set to `output_dir` if none of roots match @@ -202,9 +202,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "Could not find root path for remapping \"{}\"." " This may cause issues on farm." ).format(output_dir)) - roothless_mtdt_p = metadata_path + rootless_mtdt_p = metadata_path - return metadata_path, roothless_mtdt_p + return metadata_path, rootless_mtdt_p def _submit_deadline_post_job(self, instance, job, instances): """Submit publish job to Deadline. @@ -237,7 +237,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # Transfer the environment from the original job to this dependent # job so they use the same environment - metadata_path, roothless_metadata_path = \ + metadata_path, rootless_metadata_path = \ self._create_metadata_path(instance) environment = { @@ -274,7 +274,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): args = [ "--headless", 'publish', - roothless_metadata_path, + rootless_metadata_path, "--targets", "deadline", "--targets", "farm" ] @@ -588,7 +588,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): host_name = os.environ.get("AVALON_APP", "") collections, remainders = clique.assemble(exp_files) - # create representation for every collected sequento ce + # create representation for every collected sequence for collection in collections: ext = collection.tail.lstrip(".") preview = False @@ -656,7 +656,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): self._solve_families(instance, preview) - # add reminders as representations + # add remainders as representations for remainder in remainders: ext = remainder.split(".")[-1] @@ -1060,7 +1060,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): } publish_job.update({"ftrack": ftrack}) - metadata_path, roothless_metadata_path = self._create_metadata_path( + metadata_path, rootless_metadata_path = self._create_metadata_path( instance) self.log.info("Writing json file: {}".format(metadata_path)) diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py index fc15d5515f..a1eb2e188c 100644 --- a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py @@ -7,7 +7,7 @@ from openpype.pipeline.publish import get_publish_repre_path class IntegrateShotgridPublish(pyblish.api.InstancePlugin): """ Create published Files from representations and add it to version. If - representation is tagged add shotgrid review, it will add it in + representation is tagged as shotgrid review, it will add it in path to movie for a movie file or path to frame for an image sequence. """ From e5ebc8566376bf3be30e3b2be27b72ad2c237538 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Tue, 21 Feb 2023 18:44:15 +0100 Subject: [PATCH 248/483] Add OPENPYPE_SG_USER environment variable to deadline submission env --- .../modules/deadline/plugins/publish/submit_nuke_deadline.py | 3 ++- .../modules/deadline/plugins/publish/submit_publish_job.py | 3 ++- .../shotgrid/plugins/publish/collect_shotgrid_session.py | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py index cca2a4d896..faa66effbd 100644 --- a/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_nuke_deadline.py @@ -266,7 +266,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin): "PYBLISHPLUGINPATH", "NUKE_PATH", "TOOL_ENV", - "FOUNDRY_LICENSE" + "FOUNDRY_LICENSE", + "OPENPYPE_SG_USER", ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index b8edd0f161..afab041b7d 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -139,7 +139,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "FTRACK_API_KEY", "FTRACK_SERVER", "AVALON_APP_NAME", - "OPENPYPE_USERNAME" + "OPENPYPE_USERNAME", + "OPENPYPE_SG_USER", ] # Add OpenPype version if we are running from build. diff --git a/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py index 9d5d2271bf..74f039ba22 100644 --- a/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py +++ b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py @@ -83,6 +83,10 @@ class CollectShotgridSession(pyblish.api.ContextPlugin): "login to shotgrid withing openpype Tray" ) + # Set OPENPYPE_SG_USER with login so other deadline tasks can make use of it + self.log.info("Setting OPENPYPE_SG_USER to '%s'.", login) + os.environ["OPENPYPE_SG_USER"] = login + session = shotgun_api3.Shotgun( base_url=shotgrid_url, script_name=shotgrid_script_name, From 35e4313ddbd21161e2f14073caea63a1608be499 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Tue, 21 Feb 2023 18:44:34 +0100 Subject: [PATCH 249/483] Fix typo on function as 'fill_roots' doesn't exist --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index afab041b7d..84092a3bb2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -412,7 +412,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): assert fn is not None, "padding string wasn't found" # list of tuples (source, destination) staging = representation.get("stagingDir") - staging = self.anatomy.fill_roots(staging) + staging = self.anatomy.fill_root(staging) resource_files.append( (frame, os.path.join(staging, From 03c64a7290a0159fcacea503ad85f5692041a922 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Tue, 21 Feb 2023 18:44:56 +0100 Subject: [PATCH 250/483] Set staging dir with rootless syntax like the other representations --- openpype/modules/deadline/plugins/publish/submit_publish_job.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 84092a3bb2..8748da1b35 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -677,7 +677,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "name": ext, "ext": ext, "files": os.path.basename(remainder), - "stagingDir": os.path.dirname(remainder), + "stagingDir": staging, } preview = match_aov_pattern( From 271b19a90d6a4599c08917f6be78eea182d324fa Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Tue, 21 Feb 2023 18:45:18 +0100 Subject: [PATCH 251/483] Move variable initialization closer to its use --- .../shotgrid/plugins/publish/integrate_shotgrid_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py index a1eb2e188c..ad400572c9 100644 --- a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_publish.py @@ -27,11 +27,11 @@ class IntegrateShotgridPublish(pyblish.api.InstancePlugin): local_path = get_publish_repre_path( instance, representation, False ) - code = os.path.basename(local_path) if representation.get("tags", []): continue + code = os.path.basename(local_path) published_file = self._find_existing_publish( code, context, shotgrid_version ) From ab5ef5469f355175094312c4815d018b1479a5a3 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Tue, 21 Feb 2023 18:46:13 +0100 Subject: [PATCH 252/483] Fix code so it doesn't error out when 'intent' attribute returns None or empty string --- .../shotgrid/plugins/publish/integrate_shotgrid_version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py index adfdca718c..e1fa0c5174 100644 --- a/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py +++ b/openpype/modules/shotgrid/plugins/publish/integrate_shotgrid_version.py @@ -37,9 +37,9 @@ class IntegrateShotgridVersion(pyblish.api.InstancePlugin): self.log.info("Use existing Shotgrid version: {}".format(version)) data_to_update = {} - status = context.data.get("intent", {}).get("value") - if status: - data_to_update["sg_status_list"] = status + intent = context.data.get("intent") + if intent: + data_to_update["sg_status_list"] = intent["value"] for representation in instance.data.get("representations", []): local_path = get_publish_repre_path( From 7dd5c42ec9f46ebc83245fec9eb867290c8babde Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Tue, 21 Feb 2023 18:46:36 +0100 Subject: [PATCH 253/483] Load plugin modules with importlib so we can debug symbols --- openpype/pipeline/publish/lib.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index bbc511fc5a..313e80a02b 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -5,6 +5,7 @@ import inspect import copy import tempfile import xml.etree.ElementTree +import importlib import six import pyblish.plugin @@ -305,8 +306,15 @@ def publish_plugins_discover(paths=None): module.__file__ = abspath try: - with open(abspath, "rb") as f: - six.exec_(f.read(), module.__dict__) + if six.PY3: + # Use loader so module has full specs + module_loader = importlib.machinery.SourceFileLoader( + mod_name, abspath + ) + module_loader.exec_module(module) + else: + with open(abspath, "rb") as f: + six.exec_(f.read(), module.__dict__) # Store reference to original module, to avoid # garbage collection from collecting it's global From 0b811854ba67c23e6c8746d7776e5662a437fb57 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Tue, 21 Feb 2023 18:47:03 +0100 Subject: [PATCH 254/483] Expand stating dir paths on integrate plugin so it doesn't error out with rootless paths --- openpype/pipeline/publish/lib.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 313e80a02b..c563bc8207 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -691,6 +691,12 @@ def get_publish_repre_path(instance, repre, only_published=False): staging_dir = repre.get("stagingDir") if not staging_dir: staging_dir = get_instance_staging_dir(instance) + + # Expand the staging dir path in case it's been stored with the root + # template syntax + anatomy = instance.context.data.get("anatomy") + staging_dir = anatomy.fill_root(staging_dir) + src_path = os.path.normpath(os.path.join(staging_dir, filename)) if os.path.exists(src_path): return src_path From f9dc9f892076cb1ee8570ad04d6cd2c203dbc309 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Tue, 21 Feb 2023 18:47:19 +0100 Subject: [PATCH 255/483] Protect code for cases where 'Frames' key doesn't exist --- .../plugins/publish/validate_expected_and_rendered_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py index f0a3ddd246..f34f71d213 100644 --- a/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py +++ b/openpype/modules/deadline/plugins/publish/validate_expected_and_rendered_files.py @@ -91,7 +91,7 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin): for job_id in render_job_ids: job_info = self._get_job_info(job_id) - frame_list = job_info["Props"]["Frames"] + frame_list = job_info["Props"].get("Frames") if frame_list: all_frame_lists.extend(frame_list.split(',')) From f62e6c9c50327cc3f6869c635693900427ec0206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Tue, 21 Feb 2023 22:09:47 +0100 Subject: [PATCH 256/483] Break line so it fits under 79 chars --- .../shotgrid/plugins/publish/collect_shotgrid_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py index 74f039ba22..acfd6d1820 100644 --- a/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py +++ b/openpype/modules/shotgrid/plugins/publish/collect_shotgrid_session.py @@ -83,7 +83,8 @@ class CollectShotgridSession(pyblish.api.ContextPlugin): "login to shotgrid withing openpype Tray" ) - # Set OPENPYPE_SG_USER with login so other deadline tasks can make use of it + # Set OPENPYPE_SG_USER with login so other deadline tasks can make + # use of it self.log.info("Setting OPENPYPE_SG_USER to '%s'.", login) os.environ["OPENPYPE_SG_USER"] = login From bfbfdf7067703226c921916d2e4bbd1e0bd14854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Tue, 21 Feb 2023 23:33:28 +0100 Subject: [PATCH 257/483] Use dict indexing instead of .get to assert existence Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/pipeline/publish/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index c563bc8207..5dce74156b 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -694,7 +694,7 @@ def get_publish_repre_path(instance, repre, only_published=False): # Expand the staging dir path in case it's been stored with the root # template syntax - anatomy = instance.context.data.get("anatomy") + anatomy = instance.context.data["anatomy"] staging_dir = anatomy.fill_root(staging_dir) src_path = os.path.normpath(os.path.join(staging_dir, filename)) From 574f8d2c6d5701b59b4d824310d88fb029e3f110 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Tue, 21 Feb 2023 23:43:21 +0100 Subject: [PATCH 258/483] Make use of openpype.lib.import_filepath function --- openpype/pipeline/publish/lib.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 5dce74156b..c51a96f2be 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -13,6 +13,7 @@ import pyblish.api from openpype.lib import ( Logger, + import_filepath, filter_profiles ) from openpype.settings import ( @@ -302,19 +303,8 @@ def publish_plugins_discover(paths=None): if not mod_ext == ".py": continue - module = types.ModuleType(mod_name) - module.__file__ = abspath - try: - if six.PY3: - # Use loader so module has full specs - module_loader = importlib.machinery.SourceFileLoader( - mod_name, abspath - ) - module_loader.exec_module(module) - else: - with open(abspath, "rb") as f: - six.exec_(f.read(), module.__dict__) + module = import_filepath(abspath) # Store reference to original module, to avoid # garbage collection from collecting it's global From c41a3b1cb851e3ce4e7c2954b8d20147a98d16e5 Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Wed, 22 Feb 2023 00:43:26 +0100 Subject: [PATCH 259/483] Pass module name and set __file__ on module to avoid AttributeErrors saying module has no __file__ attribute --- openpype/lib/python_module_tools.py | 2 +- openpype/pipeline/publish/lib.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/lib/python_module_tools.py b/openpype/lib/python_module_tools.py index 6fad3b547f..9e8e94842c 100644 --- a/openpype/lib/python_module_tools.py +++ b/openpype/lib/python_module_tools.py @@ -28,6 +28,7 @@ def import_filepath(filepath, module_name=None): # Prepare module object where content of file will be parsed module = types.ModuleType(module_name) + module.__file__ = filepath if six.PY3: # Use loader so module has full specs @@ -41,7 +42,6 @@ def import_filepath(filepath, module_name=None): # Execute content and store it to module object six.exec_(_stream.read(), module.__dict__) - module.__file__ = filepath return module diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index c51a96f2be..7dcfec4ebc 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -304,7 +304,7 @@ def publish_plugins_discover(paths=None): continue try: - module = import_filepath(abspath) + module = import_filepath(abspath, mod_name) # Store reference to original module, to avoid # garbage collection from collecting it's global From a946eae6c413e64f50e96594e292906cc769f1d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A0=20Serra=20Arrizabalaga?= Date: Wed, 22 Feb 2023 12:49:05 +0100 Subject: [PATCH 260/483] Remove unused module --- openpype/pipeline/publish/lib.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/pipeline/publish/lib.py b/openpype/pipeline/publish/lib.py index 7dcfec4ebc..1ec641bac4 100644 --- a/openpype/pipeline/publish/lib.py +++ b/openpype/pipeline/publish/lib.py @@ -5,7 +5,6 @@ import inspect import copy import tempfile import xml.etree.ElementTree -import importlib import six import pyblish.plugin From 36b7fa32df20c9640ccda6a81ed61766017d607c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 17:33:20 +0100 Subject: [PATCH 261/483] OP-4643 - added explicit enum for transcoding type As transcoding info (colorspace, display) might be collected from DCC, it must be explicit which should be used. --- openpype/lib/transcoding.py | 2 +- .../plugins/publish/extract_color_transcode.py | 15 +++++++++++---- .../schemas/schema_global_publish.json | 9 +++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 8a80e88d3a..42db374402 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1044,7 +1044,7 @@ def convert_colorspace( output_path, config_path, source_colorspace, - target_colorspace, + target_colorspace=None, view=None, display=None, additional_command_args=None, diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 456e40008d..b0921688e9 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -118,10 +118,17 @@ class ExtractOIIOTranscode(publish.Extractor): output_name, output_extension) - target_colorspace = output_def["colorspace"] - view = output_def["view"] or colorspace_data.get("view") - display = (output_def["display"] or - colorspace_data.get("display")) + transcoding_type = output_def["transcoding_type"] + + target_colorspace = view = display = None + if transcoding_type == "colorspace": + target_colorspace = (output_def["colorspace"] or + colorspace_data.get("colorspace")) + else: + view = output_def["view"] or colorspace_data.get("view") + display = (output_def["display"] or + colorspace_data.get("display")) + # both could be already collected by DCC, # but could be overwritten if view: diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 3e9467af61..76574e8b9b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -271,6 +271,15 @@ "label": "Extension", "type": "text" }, + { + "type": "enum", + "key": "transcoding_type", + "label": "Transcoding type", + "enum_items": [ + { "colorspace": "Use Colorspace" }, + { "display": "Use Display&View" } + ] + }, { "key": "colorspace", "label": "Colorspace", From 90afe4185110f2571242007f88566be7dce78dff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 17:34:03 +0100 Subject: [PATCH 262/483] OP-4643 - added explicit enum for transcoding type As transcoding info (colorspace, display) might be collected from DCC, it must be explicit which should be used. --- .../assets/global_oiio_transcode.png | Bin 29010 -> 17936 bytes .../settings_project_global.md | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/website/docs/project_settings/assets/global_oiio_transcode.png b/website/docs/project_settings/assets/global_oiio_transcode.png index 99396d5bb3f16d434a92c6079515128488b82489..d818ecfe19f93366e5f4664734d68c7b448d7b4f 100644 GIT binary patch literal 17936 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 DbV~re literal 29010 zcmd43cUTnLw>H>_$dRZbasUC5C`dTutR#^vSwO%*hHkK%oDG18lA7E!IX4Xwn8K$NPF zm2^QMVp$OAQr#byfHS1V^FM+Ah+w)Z3ZTNS+l#=D%Qo_w@*q%gIQj7l65#h$=f}n{ z5QysA`9Gp&r(8?mCIu^^^;?!$N6SYzs7#)xFy6$VS3|FOCp6Y#YEfsmQEDK5C2rU_H#OT1o!xk<^8Ww zASTs2_jJ^B9gPlVr$6Q z>DWnLOFk*|im|ueJsgx(+*d*EcN|X;s~d&zO^y}%y3UyZlNPy#r38UenOeZWkJX0| zVi3qSSOP-?oSXXhbEHs45a^+F1P-`t><#`32-HLM8gkT}QJ>?Wo`IKUS=4xoSb#~5@N|KBnN8~tj`wR*t&c7IU4AX4`0os-`1&oE3OMR+ zE%~3!#DqKTcmb~%pkoCwt*~E>J?`OTFY^rP-0MF(Y%ZuOA8Ilc-d=l_bT;HwbQ=WH z&u+u-?aJbS5v6xR<4SnJVxv;7Thz63Y*I9tkhKk-${~S+nlX^h$W$0pMharFv!fpS zN+FKrL6X+5uBq4ScCSsp*;%o7BHjZs`&xh!!ho?GWpRU!{)Z3b_THKdk@}xb*2u8| zvOL~c=zRQIVx-u{ube17^_#7p2!=l4>6VA~&H5qjnk>EHDGupO^nO_-c&vV6?jQw) z-kxFmIb?U`d~He#xiOw;YNfm$TXG*#PX^JZ=0?W4&C9|!S}wm{NH8v8L#HZ)n>2j9 z6hZs$hf~|kx0$Fn1KGC4YO-O8+`<{nn%&FIlZhd0(VskEDqp^3maL7MpXu?!bcMWR zyz;s15-YW(%Ul=RGKR9Zq-lp`>QsUsc=tDLvI{IB4FVI`QpugJP#Zy=m{06gvE!&ObG&} zU)6sgmv_~`+;ixD5r#fP^$)?2CjJ4>F7u>Nl+~G+hH()lET16NyrA)_)g}HhG(4tu zT4Yr6{COpui_y^|d1cuXmUGIoq@$w!svCb)1}?HAp(hN=Dy`_EeW@}%z=Wi;PU|#p z#2DQWY4@MJ^;k=a<`4L1B`pYJxk9+F|C7*)M{mBZskL{4cZ)Ei9Fl-oMYh9}IcKsp zt<&onD45>V)NZu(XJo<(-@p8W;hJ#!|$E~=cid^{- z1QLofNn=m$%VkFWmdlF50rDeDFwB(ws!z#G1KZDAk5t$aEA*19Cjbl1*S)X;a6{bM zNAvo6KJyRI2il2+T8lE4Xx6+f>0S$4Wp@zlZr3MA&C{D5ArhMw?&C=ziU!%=GE>+; ze%FkuZwiMS9|ym-f4NjNU6b=2?3-Y=;FWU;tI2sMSbr%8%ja`>WhP~Hlz%uQ{!l%v z-Ets@!{uBp;&@;P?oIEjEY?QjnMw+#r|TR|l3yQ}CYFx8x&%LU%(&O8bUTk_o}%?u z6tn={Mal&)m~po~)Un<@!rd4($QR9?=jvzhb2(y-(dFt7_j~W#!a6i zV?Yqwe;@b$!!Z8|d8;-SCe)phJlc3QWL$R+cQP$LWY36_1l-D##-OD=)()$kp84#x%00=Iw$(!%kv&pwq;@VCObCNvAl zPM&#J&2}9x>}doRtoHj_g*k6)RtseIa|^>ERB;9k^tJml_=6jTRF0`IhcWK74g`8_ z_$KH{s5uO`w^$OA6Y;F~s7+D!i7@8YpXn4voPxW;-AM!)k^xOTTiflzF$+D=Lq4x|@REKqY&7C*SNm23|7{-;{^-LvHz#q_Gbyn=t%Ah9 zo~C>KsslW0&4teeh?PPqbrw_~Y0O7pC*Jcb@U$irUQ%o+=S$9c5=zrBes5AHF9N9{ zMKiUkJTbK-n9#AX$KKB>Rr|E6#Is;T7Ev3au>W~V(^T#4JGHd^yY`;&>vj*kT)b^X z#rwl=8{~AaJoPgQBu(2!@VQvFQN6$nuHPzWkByF(73FZP$p!_C?L^}ST-znbxdxQS zlBNfDIqRUgP#A4-+p79o?E4Yq2<5ngM%2e?@)%f%T4Ijtjj04g@^;V$zgfb#s*P?o zHtp~)qvj7|ak=a@be_SyE{^@Jl^N2Mnw}wa>I^2N{nUzo8BDfje-wRFBR?1xEY*Sd zG^h>rDM%ZXONM9O81bzgWajNLc#f~EQNn$VyeF=-B{4rf-E{c3K)b+0ixV5|$aYnR zCk6IHb6j~Vu2JUd8cvaAiP6E*`^~!;hV0-P)|&RrVYHn=2{tkz`CjX2=+~(&DOJX7 z_*ILU6UE*<#iBZ|B~7`zMMmn3{EZnOJ`~b`O=xw8K3| zS{e%Vq?(_r8_&s+*crpTr=G6Mfcr18vV-J%5^$=K5TAHizmTzCmMi>&R$bU ze;mzGW42_DH@1vD64k-chW#T$S${ zpEitt;@bCZ`nGO0N`GRCj0v~%r7|No$$83VqK|q+XjC?ukGKj*21q&7|*bVyOT=W(ejO2Tp`0M4abyrOjYym*Y7d;#JTYSv% zy8+qpC@i0eWV^rQz{`#=w__!DZgeLAU=}1#14C3rTwWU%X~uVgVOIqH0;Ps9_@~?i z9K`kiBJ%x*v)Co%bx=zi+L&z?aZ!$BcdVBfy%eMV88OpC^c6p73elU4itsZ~;h5Yg z^Q}7G|6*D%J5G_hxNq2BW^}5ya`?ZRSXcYZ4Y^ z-Y<(>AQrQi$n~Qfw_v6_sAy|>g-&@dSa1)xT}ykUGo!6<$WimCn2>;r5%mncnV8te zvzimCdc{T;(p)NStMqM=HC>fcKr@VS+!k?OMECFx>cwR>wPSl373*ex4&vt&kTf+5FEvet4rR6$zr&7GLaIUCsAbv!DxHDSnWOt1lDbx#AMMi z)1-9_s=pS{GVD+j$XyH6G7RSt{IuSeokb4IgEmt0blCKNU~q-}~+pBFqf zK|jwa;g|@!>1Yscx_!QiYTuW^*`Ly9Y!K2}-kSP8R4|)&WE5jYUm} zQx}Q0*$mwN&~5;m!sS)v=7oH0by9{>C3IzWge@QL=_yju4VF<^e}M~?YNohJJ(*aG ze>LqqpX7bHPcbdCDA06GTtLDQS&^@4*sbZeP6RT^n!v}JOVr|TiAHL!#faXKph@F* zRncxC5vqMDsrdNp&F!Aew>dmYNOqd-H zJv2Cw)m1xs^G1^D7-AeP{oa1zN_3^5bAu_SwL`wMtbwiB;ehI>D#1B0DF?R`!z_L>NVgKzx0s zkmUq#<$JLc-3s2W+aKv7#NQzSf*>BwfWYZ%4qX{5io$10sT$@3=c*Y1`wiW3E$si6 zwvW$VlU$Mx=+b-$0uc&{=7H21wDaLSlI)@S2hyD9(TdVg#5UO3*K>(`k`JIN#`0*f zok!`tE5j*nY0H1a`@FL6ua8zD7$2v#yQu8Bk`;L_x>e{EEl$=<@2MBhy(T(xiHu_)>kY^kif^tkY{Qs!{& z%FJx7gx#!}X2QkF7*4USR2+hnWM3vo^f2rJ?fIi4Du2o{I1HrKa|1{NT0?ek&)goY$UDY`^LH%)gi3FRvF4NAER3-J9se z`2~6_5U=JaB4xTf8BJ9RmiA3U$o3Z8<`#-QyEBgT+B1_z<{( z1aVoeAUIs|L~_0%v(6tH4V6sDZ~wH&hZn56`Q5cft3+>CNMo(6KfY!EO5X z$29~g4>zH2Iy&c-F*5!QJ|UB`KcP~QYi^(|P|dyg^ql@1UVkX>u%mV`9#}7!`AI{-hI@Rv|+=23e{UOveqm z1S0P7hxE^vEUF$9$#pK~yw;Tb7Awcq_O4^Dr#?N`FG>$f&w+oke?O*{8Q&`^9Q<^w zsd|JP8#Df!lD1P$oMM%CovdbeQ(H zZqb>%YVH4ZDdWm#>8oP;16DuZo~iJymCM7tp2%`(R^>Uyp_&O}sY{tgqqcD04^UwlK7mPhW~iuD027A}yS%GWc6(oE%z)3v40C=Y1guZxg~F?c|@L_>Mg ziA+Rae||J?ERLHESs|GdI(x$^-)^eSOTx_0rKsE@`&p;hSl~~tyXvCaJh-_kKPmXv z-Y;%Vw~5N>-k=hFKP08CD$>aSYhjX3)yev~Ffm)Z-+%87tD+K0)63QdrLUDei)p*f zD_dN~;w_+X;?cQEXQmqd=rEbuJ$a~G@(DI${*!mB^e zct0?j*jz0NlSP&JiLYa7$QpW)ZDz;}(DUa2_!HAgFvVr_i$P59Z3?6l6U@~E8m4MV zL90VSc*3OYJ#Dir{Gib8S(~c`rqV5)zh)nlIzASOtI8?VUV8LMz|N`S^{SGY$2!r& zYD$N5=J-#se|J~^f%X-Q$6NdRTZdBo`sQ9L@+^haB60t4&a>8d_)Pn1Youe#5&F27 z@z?7spt2`NP{bkg_UO?xezkvSzgCn3vx{k0jKn1kXJy3BirM4<^hiLLN##U^>`~}g z%xnL_{Cik*y;RLC5ECcMtXHHp6Tr0Vtv4U79Bw^z z(iHx9D0vwpXc`qYHGK^(o^(p2Qt#QUvy!8u#WR9jR4pGSsuR zSrh8sJZ7b7FBRugdWUZ8weV-5?bh31l)-6e6b_cFYJ1;TkoG8`NvJ6FW;XJ@;oS_j zym^}4q}!;U5Nq;nix1J0JW^X-o1d=4kWOX9jo#BMeeNP``;(higrb(WrcDUuAAYw% z0rU{e(+ys99e96rKQa25k|t_}Iwa3UPSReu1Cstc6E>6T ztDsz+T`V8vH9Jq;Ny#Iw7=tuM=@O0?Yt9n|eT}wF&F~n=;&-fzhpe)pyw;gPNN3f10CUkWrJQWD@$w@4|y<%6qTkB25Y!u>6A6JX!bi z!6~a3n(8+AG2Cz6D&$z~xBcAo>1ClTQso{JS1`U`lxmGK?G)KxQujO-ajFzd$TuGU zi*CFpEG}MqOzCwvc3h3whIe7>N%tlvO4YdlPaFj`kHZr~a@pv2onq!Gk$fHg7PPK0 zkPn6NM`vw`_8BMI{(}?)ERgo^Y0&CL7|OViNc(C7td&ILngyxmI^x z=09dZch+**=FgZB;Z{beFY9aMk*g2zYL7eJ!;?461MwNdW{~YYDd%V?gOot{~Y zS<=MI%}_Elag=W0+OqCb3!Wx{h`}%0J#J|*cH$Eo)2vs4q7n^+mt=OtoUa&w z6-&uPDC&8W+Vb%o>^4fTwv>Dvi9qj$0XBro-E~>%_V{wrf;0(}=WX#%>)d&}0hvk` z&jA$^!e@2bdphG03H$o0WN<}UUszK|73Csc8mP1V z^{9=2fxe5TFWXpi zd0$~r7phFnPCbxIxz0Nw1`_0o#4XXH1LL!(L=?6sjcUN4=V5m-gAd2`gfS^kMeUM; z>WVyoTAB&+yr4$7_Fr)f6;o3_IU1HD@P6?f)qY2S7(mPV8 z>4+OpA$z6yZd;z}fNrKQQo#F1cQ5@o2q-|{6V-%U?kHQU5~s|wYDx|U>Lfu_GZW?^$P2VCK= zZX#KIwAR6RQ1$-I?1L`c1pxwG6a0y{=HPPuzO{z(8h>=ahRrkbUD$BsXeeY z4DNbp{gQ;XK%V#keZpvrjtV z$rI`Aj@YtkR~@s17iD;MA`o|4V!cx+zdFG_(Ko`GAG+jq3vG}&Sc2s<>ktIPq+c7# zUBat7`gvNPD$~}`d%>LDS;4mO#uY#1q#H%NM%iF5c6F_ZD1bQ8udSH4#>q9;@c{2{ zO-E)kF)*yWH{K5B;JONWc=er`lb*w$ByWnIL6!$p`3Dtq!&>Q!JC3Cdoy8LFwdx75 zWNK%KGMAFK`gx)Rrew=`GA*BRnP#e#gtj)<(1{Bg6rsi6-?ljsu{oW>-@>0vC%6|o z=tgNh-!qT<>Sam?zYI!!*FX@1DZ6J5s?AksA!qfNuH1OHC>fjJ-%Pg1*-0@w_b#)p zY6C%)B{(Wz6I~-qxn&c@+*-)BUPsoD=LN21)zEEC@Rz`lxYv6S%+|*W>iOT`hjsxu zwHQnSq)eZEAKG;kCQ!JzTcpUPJP*j7`0rfr`0IZNf8jCW=HbI$ZDpv?(!xN@R8e$_L# z7g*IxpeKz1Fa*Ddc)ZY{zC@^4-f~sd&p;>w5;dt>%w0vVde$5GjE>U?g6R&U#V5~5 z^)2n!zGsFpw&~EVbZkwmI77kc0s~ZbB62v3M4Ad@D-OjK zFg%C!@Bh5C&q8|N#5CWZ;k)&Ws}8R-u{W9s1f#)%4Jk!1gO8OT!6v_uIB3D=sY!Oj97X7}KAO}<>`@VA&$A`7>w!s8;NQR|tKdFIcv z%g(}+x%ey>Qrz~X7+)Rh8G{8qO_c)gW9>_z9j7N)@KM?R?#cJg{uJhqJUNqpP5CGf z1-yJCn!xk^u7c3}F=GKS4#c4vMT6Osz#-SNk|KXL45dCW0x2|t(R~M$YIP+VSMs2G zAnjmpkzKJeW-zZlg(NMRAG&Cnf9=yBt=AF)Xdgps|5$xn!@Y<4S@+178}a5XLo$vb zR~Cyt+6GGG{V)jmX-!51>T!u!?6W_R0#*i#toC)X2a#<*4O9v<8t|O#Z=24Blg*1) zSkv1)6*D#U6}Au(4IH1JEL!R&5bpZdXOil1)0SLkWooRYKn0z4jgcBtgH`xlxi!w9 zKF)KImUjybaC5rP9oOFMzy;-d7P+J$RQ)pU_4=MAM_R}`M^E##j%93i zv-yA6+ZK&UVkuOPo_ZuiCwh(jg({JK;jIFM0$ZNa@g1HU0#D8a)I9DB%c49-jaYj! z@vM~5l>%zkY9vJ_bVY&RWs|Wq!bh}!Di>2-2c^=*;JGvlZ!ZqE@KMiIlwl{^Wr%*pFblqKiX zJx|&KR((;YrkdeK&On`cCQfUXk3D|MDnA4+gQ5*pA?nG8boD!zp4dT31s)SFv4syZ zC0Ir$(VJ+dyOO|oxsn5nsUxn^Sv*(hgzdFU-`6!y!lnD_oIL)v)50!n6lnd7V{FdD zA&GWpu=v}X7|yPt9nm&lrcPp`HL9`Qx#9%03^Il;cZ}{k#f#4|8b!fIX~-Q?3U888%Q#6+XNBh z$3plum{F~d10A~yowt)tw%i20=h}PX#P?G<^r9vd0AigKJ)V>t{pMp~gtD$Y&!10DAD(5jUJ2JAM{H6!7EsN;+J-IA5t~KFg z`xn#K8wdwSR&Ewv6Q(tCY04+kgDa>anJ{m4cRq&3XE!s*(RnP{OHV^t_CyCKAs-K%f;m8qa$UwuvGpBvvwr%smTp->QCvY zgf0V`Wq@Gh&cfh_eAWi5c$cDHWxZ#$4vv?fA+ZmWGY>6nA*Z8g1_kuBfSCuK*X|SN zR@?0!*IpNR0;Ax>J@l8&SbYbK@Nu8fN<>9H#i){g8SlfkP&suz76bfYqCyaz3aOtCWrag z>~terzViw^lx=vZa*z-d99!fEU9Om;acGW}odyF`F$}NBYgpvrfm8}ClC3Fh#=Ff% zsG0rXg8K!&%s)sgUMOxV$0p;%z~dFuQ|0EYKoi5}AT3H<3$iMU8Bb;iPtoG9>9?AN zCN>jnA*S0SKjvuO11_ki;bZWyVI(d#%mOi_4(Xjvs3VoC^+1iWbF`K`dzQ{)%E4JU z3Y0;t47XNj>mYIaiH`lV;#gUr14GKod6dlnjY;rEa;99R?bRqp>#Otg)R;?>4wp`B*RuU^RggujdsT?=lvYf z`mMgYY?{^|>mM8PWAX;2rEiXjuI4DdJ*U&v5`%xz}~Lp))l`KpE{Zo<{@Q z0%m@5OWqIsR;waNxAe(s?Z3ONN_(9SE*ni8jG~4gT-$n>td?d(3F-+40)u=72*P{T zUwvnidgQGC<(YEBPno@@3UVXJZJE7{5K%@J58J!^{~dse52iUr*=cJyK>-sHIQvHs2NKt~$%0=We+|od^+)3? zyw4(E_0PxgTdc+~mZ8vh@QQ7Y&o@!ck*KC#@Wk|9#c_t18*}KdMQDNDi9Ps7ESvWtLYo#QgSKI z2mA9^`C_C4d};Yg##W$;zQp#@NW0?%@yQ5ky`9$Ft~5fPUA6;5c^q zYXbRjjgR3&9@w{=phKnKBT5xuvL4Xi@V&-$y71}riPnvlz9QlKE#RTxyw2f;Uz6{i zz5zsa6VKck)z;U<>vQr$g#@Egnpq;UDMbWw3B(%;Lrr--{3l~uop03i;jLqv2Hqwe zs8{YRQ*6T!+cCkaM)_Y7t;E3FdwK7g{bp(HCw&hiHpt4~JxD5k@S@6LvKKC3z87Vz z!qsg~pveaHP(w{STfpd^O}V_dx{1&io&O&UiD_uEt(KBCxlQwFg z!2-@HP`_8<={1K;$+9@mnaD;y@I4~#zY0nEF&#z&;^64lO)MI|}NWtem{6wI!i<=p*+&^TFpf7r| zyyg8+?L_zO=XD~Y0J+F>jr;aA~@2 zjDOzFdYi?p`y5fK|HY+G=u}@dXLr-#1!FXISp~}MqJDHet2A`}rZ1*SqQD5!|2-x9 zoe>%w|1<3CjbPy=nruS5Jab}1jxIN%1Xy*qHK~om&*56hwFPo7?CWk@_G@|a1eS#0 zGC)sgFIFvgoUp3LW$rOIH#{*kNqm=jHt%GySLob8_xcbIe5Fr+n0emOKG@*~>HmIJ zz~)0Pgz-OjaJau~(C#F6lqF3!PXr43p`tAyv5PLAYp=IX*#HcW4U^{Y@3%1Vz}x!t zIMCt>NCf{;Ma+Z4Q%jrwR}S6UX=fVA-qnG;!IU!s$%|_n_*F{0lSGnvjc6Vldb%-mTQ0#Rxhlcwcxu@KW=($tJ-I0;*ZJqUmDJJJOl+WN8rXj2W#F1;sPm3x=j4 zr^*~3TpxUwm$dgBKB2D!Z$9THTRj&)3`Gw;ZVY7zBFrpGV>)ZLM+No|0$=?5WV>_| z(qM?IkBW=6T_SYnPD>2sA?gl?O!j@?M4X0|&1koXi`V0&V8To!Qs}I)Hr{{=u#PM5 z@k5Z=3S)z^2}02;4lJKv!Hr})92laPk*Qkj0?`YFWyeHOEOj@tAv)@v1*~Hf0dWv* zLUH8|PL`vzvlLE$uWM(1I-YPJa#b2*$78oh7@w=ynaEv$)}j|;ZKYVWwIB%3`F?Tp zkHjP3j^uPbcsXDSSdg4-2VB*w?Ib%RxpKi-;9o3D5-MBdp9N8sd+rf1Z>;mtZq zylr5fGLGw=?2^@qFRuXk-yM$>5rML+P}9-~oVCFV2n`Qc>w~M`@4JkO^J=DjG}H-! zPe7rF!PV6~07vFvh!0L7kJ5MRfBHNtX+0(%UgT*t<@8qNT#mp^9Q2qNN$<6L#PFBS zT3-ylzK(mFrrGjuN(#WCMGZ_UKz7@z6^C2 zIuvv|2Q1)=^H~I(FQ(W|I1mef<$v3P%0IQ2@}v}o*oZO=T+sm%=?jIXJOLUxADaF> zj3-ue`>s(+O&1ZUmi1iUDNo1=KxjxDd9JEKrzeXOfjL}s9%lnYAj$y)2x6k@C*9Gl z5iXw#JX)CC#{D?$Cg1n5>ouzJ-v9+<-%G)|A9HIegbK#E#QTeW2c1uA|_Ce#<85;f5MFgmfz0g zKS|h>{Idmu8C&s#w}9rK7gSU|L+m|x?Q7$;Iiyh@p@juQr&jY`*E@@~&erWd?4)rgXk)rsGNY3Rin2z8Yn@giqug!Pr1Uh-vAsJ!P(} z(U|qyhg;=#HmD^F^pdiyMt+5krZh>L&otqHW63vy*3D5-8lz`wM{bbMPPyZkiiWKA9G0P zp^Qj&`|djpAQjG6mK%rNUu;GMXp4EC0ZP_6J6GW_Jke6Z`!BOWA!2YHwY`;inIZkE zk5O=$4~Y`HdcZ)pIx_2+uGQjW9itMJF-o16YoOsxZKNxPqYuHI7+`>EktF{AtZ{VsE2o9U)Ibawf5_x8^o@h!b*+i~=w5lXR?Fk_DY=Q~92?}Hn9 zIR6a?ubwg)#O!^1VBbS~A)pI$$K6iVP4xDM1C|K`LI}VQDe_f?di`}I?hIsK@fj^( zz~lT!#|E6YCI4UC*Z)Qh8G*3%A8zVDTJ`^=)h?dksQ@hoW(s<8<03HnpF0=;YCw7O z7FEY>gZqBRW3H?~ymQlQ@Z;*3`_3_HxTt2k)Tqe+&g>hR7=+9ZN69J8qT z)P~1O2$-%AFX6GGLgOGNx6z~Y?6l{-jI4*_BOol(lkDy?BQ>wz>}{nJ(Dw80uF{kq zmfZwf0dbet)^~Lv!|>1qodY8f^NN9nv**XL3yDA&pvPw!55ujUsDx>zSXB9x8$p1y z)AVD*U$gXDVY^5t)rsZ~;$BqI2*)ALQY=^^3iy1ViBeSLp~i-~thv z?$`SY(bF+YMi;g!|BVUf8S)R;&c|i0x-lx6AEozs1Z2_)GlSRPGNwRswAv)XS)qarjWP>QL6TZkgjl_1I3Hz^z?ti1U$jg!K>ZCs(b}#RKe;= zdO32!!%%HAwz-cw5C-)?)T-m`Euzk&PE)k8%g93echa(a83btkc}_tb?Vh6Fmht}P zA2w2$Jkc*b3UMEe?XapQVVN*Y37Kc`k7=pww0=e12ZE?N#|b&tjgjygh1F_pmK)}2 zZa_r5$ED#8gtA-T+s%cR7iLG>eHwvOO8z{RI+up^WSd58lM9nU6rS2(>R$de3Lh+% zZ@@DIpM7gygO?*ANE}u7-%Mr19vVW_oh+81+fViAdC63_?$}QcxaX5VO)Ix=U^SU< zj^7Y1-D<1e22`-G=-8QdR?Kc|%fShe1Ej;ohcPU3;IV^#%mYCru=_^&_e}BlSoTa? zutszCYj{G~Z72+J>KveIQhbb$U9Hi8kaL!PEhs>bxBoYD9vsLR%cu6ZwYCL_Ra7U0 z_PP71$tpFI76?jz3HTi#t78B}j!@-<^^{U}BmsLD*;^3N z4D9N^@}l1TfDVs9lekG@-KDr2jy}|XfOdYT3|GR}$4LTWf8W$F<6a0#&ARZ@X}7BP zS@=$>x4P%g3Xj##lcKxzywo5dv;dSmsOm9;w3u(*&!9ZVC^FA9O}K~~p|SieV>Vz= z*Tz7pH-2YK8O_NfZc}ZaflO5=MNrg&Y6~#U7=_X3TG_;M*zlWSCm)xs6|3E=dfRh% z5N>UH_o6lN%p?K%7u0~_s+d!X)Mg{^d8&EJd<{r@R<*-pi-ClZu8pH zH-ED*pa8#%_h$UC>Cg{kAUkBmOT}CjW|h#HpT#Vi1*5;dD$=|@P^8(~`B_X4uqYaH zGSco$9sr}jWl)t+i(xnR1}ZH^pukE?iuw92ZVj4a&+%;SMA^YQSUXu7HZ^wGIvGA=5(KPruy)<7 z=;iZh^qjB=AG%*R&Waj6>;$V&buyOVIn!22RkXYV<#qW4s#I;IHyb$wa!FK0(-}cP zUcejWT8f?=JzHN{s6!du#=Xzw|KW zLVI`B_|mZV)NamJc)DlPIel(3$n6W~qDyRipHDA&V|6kzeGn2#cja((v~6&ra6s7{@KoW5Ty z^i|QW^B7gPW{u$TN>??Fh|<#RN1rElruv26m9nDQL0$S?t>C-DULH}kr89Aleuqux zP(?H&cMnuXcM-=>{F$N-`X@$)){$EN;2o$sW{%LUkA6Pm{SfrG>lF0xS?B);D@4Iu z4%@O|C^G$NQ22z%PHS5|uq0B2s~HZPjWXSZjI?gjT7_z7GAvKS3``S6DZ)( zZ^xx3<4>rG8*${a>GFKt$=Tbv3{clVwv@Pw#Ql)NNBY9$eN))9IyXjG2~7ksw-A^qQS#lpwu@2GA#q;OI|Pibg}POA1Pm>$=R8csT?Gi zos#;?AWHK}_8Irn%*eYUkev~-M|LFrPqGrq;P2s7Cz2!8&qb-%)U6B3Oa-W|#@cQl zhr}@6^;}`L4~?lAmuh&mu>Q*VKJ_vfB&Y(QtKnS?2ZvUl&|s8SbH1-3xB77(eOORB!CIrHX*Z0^`y>N314SCX%2XCR^>sT<~DQ+(RrovSmpl@&RhNXb^n4Na^ zvuihriq12njOM>p*b2FPy%4VZ4ST@JH2zsmawdPOZN7q?R#{z7D$*l^Thf!eQXs!) zaVSN@q|>R|8n0Al*@S0(YuqL|Fjc#49v0Rt=w1*I%&B#<;Nv!^sm5LgY-v*v^Z=5&L zzIX&djb57-+ueV9cqIiEUUUn{Rlf=Xp{e7@8vGI{z*zA-Oh2!yT@-2lrw)u?Ll94^ zfv-q(+%y04%SCYkXeSmEpcnz9c1ln*FfB6Dlfi3?3uUQHf5jDf{ zV+{D#L>XO%ICVI)g#fbS%*VPlOhYuzv@=Dw6{ukS$t}$V;EC85{4G)>K;;`5b9vU} zz+6HZ_7J!&=@1xhKG@)r`b(?%sW28pQKz+y_I8wIJ+QwCd?B|24am9EF?I{Q@x$qn zJRhZ$eYvXIqYT_zFO*-B<442ffg5H6PeP&Q`Uk*#UjGtab~AImF{tALQQy`piLf^O z{$nz+mpE@wwDzWrkgerTrEppD#IH@+?HPHv;T{ESpBL%x#WJ+cia$_-K2%yP*8u9u z#jr~N+;a@$4#~klYJtMz=e8wZDEN|KUYm;e4=TUFWjB7P_-NL}20as88 z4(}ILoH@meEYtVwd!b+V>ZM9<2*cWjZBi{&xVP;5dR)J18Ch zZq(F?)LjN|x%|ZP!Bd_|2?N!ZY(?*iE(&reqFm2j&zj{OQaw07QAn6!4EXwl*NY1+ zy|RnF%p{@FqY90IyY9U5e_eQIlHF7hi&i{;ZI{O%8LO7fD~vnA6O}_xNstb$7T7=n z1yJQ#NqPvNLn_oXDwzFM=cfsc)4m7Bx*vxp)|4zz*XJY!px+VchybZ-%duItM}=$WKNvL1`+lD-3sK-&p3sY#e)wv)Uzw}5l5_Pt`^l&E4|+Pf4S4_P z3dpq;E==G)?>Nu4_K)2Mkqm9%AyRz~s~_H!pof#c2fhO{RmS@ONMa5DPbU2xNQk{o zv)X|ueZ9HJjP7@x9{rRWdHxlfheYSAY50%e@_$#H&zaXUi3cwK`g26`Md5tK_)++6 zgj~JLfqfA^LR0qzPy;o`qEoa0q`klif(ry*v-_{Y`r*p?M`6x^<;5DI;ylB5JghN# z`jE$F-F0vB>N)J*`~qtPb|eG59Cu}D0!K_%h|!2Y*9D>Ywd*QO1OxuBsG(p*a;bYi zU4hCR2t=}yB&Y(z0j&y#EXN(uG$w0_@kXXN}XOMnGfApKe<58CbqUvhcra z%#FyK+yF(fo^{@}l7yrI+D&LWk0;Fjbpir$Agn+06rcrgW(IgWcNpR#mq>YkkxLvc z{Zp!1^J1*>OCvi6Z@rNKpE4ULcCi}q96%>crQH@{>k6=4xBXBP9$ttO6g&MOmx4QA zNU_a-D?EQoBY4aHTjAMjP05`3C=a`EN!HuhXBTc-FDkdWTjaC*!iffydJtRa;o|dh z9Bzt11Kl<<=Sr3Q!SmLEat_qxEI>4rvw}5}Z!^2K+*y=sMSG`-sNAm*5 zLLiYLLd^cEzfW&{z0Yt6TD?XOX!kH~0q5Sr@`+XGD&XEwhStE@&aX4b@b@P$uR8+G zC%2%C`O1)fxv^^Jb@4yIAQM7DGoA0oiSphk&;xOQRt%`IXP>l>0y!pGs%xEC#V>b( z32+L)gaYNu^Qnvh4yLA*6;$>%scxn8??)Q8!*Cxobdmt9gjWSO5b~AUyqJ@Az1Ied@2ryMqZ=iTM z(1LXN%W-8Ul&4VD7OtnUGWu7kY8<=lV<$EvF1pFEc9p`y-Z`G_@8Fr=TjsGaug%z8 zKY1hO(Iq4qQ7Y~1j@3V3!CCrnS~!mrruq%4}1rWo6}0HG)QG!cSid#RfvZZ(mf}5UG(n*FGDSr zXaviu?F+DSLsN|?;&`%}oOxv>(p))u=H#SLwe{G03md_3`{qy&r1wBdEg?>({$>QP2ygdm$T1T<{ zv57DSnBrV?KYu0Cka#V-eJrN8XJul2dT;eWdFq;u7+VgK#z?zzv|2GXlLVUQ)i{N>jE`J^^dn4fQe$s)Wp-z=Qa zuRYb4|M3~I%$3dCFRVG*Ly`+8aftfmv$#A15nRl~;5rB56z=jMXba#$mXlgum4xh3 z-ZBGVvC8;KT(W6Tup+007fKxL;ham8aL=ng#dF>?r?;kY250_dL2_KtJUU;U-biEH z1}l(y@A2@wB8Pj1N$il+D?1~u)Fa2NyTVrA31DehS<#5Mp#%*u5^{qF?tcQWdi@VpRJaJbt!$E#MOGU z=|x2}10--(Dp{pxgECTfW%*=C8K_dxBS{WaUMZ(c67}6mvL28-)_ZjB&|Y|JW#H0F zDQ8(3fjABLuu zFkgG)^@bP2Gc#;ya)WmJ#CAM|~!zzXU{LKEP#RK}c>DV7K z$qx-^fjHx~dE|#mvo4jdcsRPry2ooy3N___<_3H4O@1Y^FTd$RM9kO;mJ5eMx6b)O z1VOp6Tv*Ch&_zS*i{5_C*1}uonF@)6xmGq2Uq8%E%mc|wnwAI-6HlJRu&t6x8mpp& zv}7t>El7jOOC|TBBg8BRrrgAIAFF;6bK^>aAa+g-_koKC5)u z*M|_tQPY&4+5q=-gtY*akQ?&Pg{du`d;rx`3piLm z>wNeC3EMr*CsS6Xf>+hOF$L4kLFqcQ7^aTy!-Po*nVA!<7z|*tLf(?wo&#)lDfD(s zL7-Z^W`f|;<}m;J9MHep1jPPQ9Rq?`?Zzgh=Q&Yr=}NA4D*h&_v_Lmhsc^8!y|w#O z^yPJjmjsa&H;GcXYK{a-GRlQVbT9t!gVQ?(7Heaz$orQN{q0X8FfEk8`0X!~G8tSL zl?=6**$dWGHX$C{-g6xvWNGfdZmSh*g;w`n?XlNRuF;GPucE}oaAhW%wUym3I?Q@= zx4A0}VSR3r1#${?&I{v4)%SPs^x{LmCq0N0V2o5In|G_2Nk9vYhF;X#=2X_7yD{!g zl6>vE;}u`V5|r;&7z8~#6Y*!O7JTjNg*@XNg$(li2Y*ZR%2+V~1@-)iZ4b5}fy+43 zE|lXaO)lN`#l?muoT?!cnK1;iAqVa2eLLfJ5o?fjW;(i7d%AvqeSI<|v=%zOd1P&X ztLu?HZ@hCO15lA5udwoHA)t`)45+00a`;d$p{Qc#W(y_c1_nRhD6e)x8c2-AG19!Q zq%07-gO%h_yT&Nxk74zH2nOW&Z{Q0+$tKYRk8RAEX#v)+wMSxBpHdTY3A0)=$caUC-G*@#Gy40#9Buc z(x=2z)m0ifMzsxgpP<^S%Ysuy@CPINFSQunHAL6L%l`0eJc78S13G z*V|7n|8AdK0n3hM0dRwz37y$+CT9R(P-F*=erHL1!RpMD^2uL;1v9=@dZw0V!)7Rc z58D)i{MyTOueVBPw#EF9GT&oeJso)+yTFD#r=r42LCzA<-z$s;@^YljKh$aIxhoXR zdOI=#QQEbDa&e|~Up?7Z zVYht_4P(=ihr8qTOrx!0PEaE$5HdnQh!6xq1PLV^&$-$Nfofhn1%S=X(8%u`jvC2! zWM3)gE6(ZZCzvOYhbM{Glupk0i zauivCR6C$qZCxL9IT()0(f$#pHGHeULsre%Q3urYZJK*m2nm}NS^r=VUaq2-M3r!wZu5AT5t*oTq-TMB)WMwg--udSd34+8UE4umJ0#*XPC zV$BIo>Tw10;c3&wYh46okZuH=3e$LE_{S7E@E}kCk8O&`XykZdg{-!EmKsI&u;jq1H zb<6Q>*qeI|O;}n~a+;qfXU$wGLSK@E`A$zN^+ys`Momh&*a~qR`(Z&PE`nWQgg5WA zU7Y%lCV_FM8I!`A)PlJ~)w)j?VXPCo9FF}OkYX20!mRI9JS=pAgJgiILEezID>?cd z$Q3$(gpvO32ea8%dRcbH36Hk@Mw=N5QD94GY#uK9N_$hrEi-2WFl`GKW~f;GUt=rU z5rQ-SNu6O`LkZcx9P>H}3nvu2{pffON(+X!N(m~Xd22=6M%I?eEXamQ`t zDoNPDWY0N*WSdK9296-4l`FAGVnvY-e?R|ydh<*$1N9JGlkwA=sn}UEpnUPTM zwVu)t{JtU0R4SL#GKmpk%a=$5*q_^N6HeFnY^Z&d_e`(ylE^tvUw93at$YxW?QR!LY;oRLi-jW%+o8*nHB&Rl`K;WE2~G^ErWrW9l3NLbomz8U7_K?6B4{14x}Qi*(%(pvzP znfT9Rd~+vY2_osh6E>NlEXStr;K}I-BH!dOz`go00f)X)gX{+hwFnAqdAMZhde3y# zQY-9t*pJ`&x)+Ku>{C#@qM=QuCO8s>4M_{luno|d1dc!_M3iDt)EN3U2w&fa5qaBC z#Ki%q)DiYrZO$VS>jL^b>hCoq?^_3(E)XlTZv_kL6}B8=WPR?tB&?6Mh;tF7K8A`L zZ{9*ROCndNW>`GYl-A{H^q`M?p3<_e3PYTLe%4|!&wxSvXCvZ90oZV+Sp7|~Xx_qI{x!bp{I&CPbsF+3hGH5x$LRHiu&9*)-j)%LASX!2I8kB50 z>S}FF_;=IHTyrpi2Xd?8YADO7)78ii9AI6$>J$URcX)-@lM~I-zi-a}_dkLR;M!2B zi<3~$NhiSYN$ku{n@n^BjpPf%P}KAq`8oHyikh`pIbM76%r{t~wEUEUASx~0aF$$3 zw#KN(J0kPZh>|ONF06JWg9e|Qh&Ka`P-;Y$$CG6pJQT%T=WKRbyXSO)*6!Rk&gub# zG6ZO5UL3sNdofFWV4HvF9>N;DjjdCeLrq=1){?F9GRj`m?(GW~y%ufEx66n;?gK%+ z*I9;Df@6;|3NZ|0Iou|g6mheCws{h+uj#{O+&!IP5z-D;=z>y|=nXui7Vep8ni_bZG&z~%1;dsIQM@%Wc@vJ;k^msY5gPG%2_x37B(zDv=!a;jB zvvf2bt`HgX4h9{&7%kVT7_HxYzOnnyy!Iqm#Hl@3?v;O-`ni0h(s!;`w3FrY(cf#J zKmu_~1h>NNPADY@TZhMJ5=OEd%rx57DI!9c7@eto>FEoOzS3gv59z4kWHq8{kTb6q z>Ub4yV~7XGE!rLrbT6Ti%MY&l$B1|*-rKZWD)aoQ7HM!-HwW9pt6uajto;_{DC+aG zP8^Gb{+G-N2TOPv=C2Cm3@?t(Qj;Ceu8k5aU{|QEMy7Ii{<(TY!27iE?~$sI1~4i6 zma9=uAJ?7ln>h9yRKR?LUiZkr@kEN9hiZ0?N_rK+b;eKQco?xGviCt-cK<}59!PGy z#|geI(vm-cu=1|ipZ=`a(Ddi9kHuM<$dRb^u(40Lgb2Ig@6Vk9vkDtA<$ugiNZ`9A z20M03_WZ&{n!J<(B5#xV2U)c_8`NBo-}*t#7=t@>b_elDp^rOO`=|zK=WTGB&4KHb z1|OPmXUn&2vHc!wB{E{JcIx&}&CI}<_rkAfDv)+QJIinew}3oTv{Y4eRh;1;W0E9| z@MZN@p_+axNx=m{`rxAtUw`gzfr{qafr$I=?J6YAX3Bo{hCk`#z4jsXyCf>(hRJkvN#uC)+3yXML$SV(E@CScXyE+ zUA32%D4k++)Xa~Fi9KqLzR?7FvhznINk#$xBf%hlHQa1EZ-)paNx~`ht#YSnF9=o* zOTu>r%^>P;(h0c8@*w z{*hVNFz3k0Dvu^;fOHt(fdh%W6I@2l4u0{t)z3ddx4vR=8CBrgwK2aM*BpQ|NnNS3DA?Iwmu_9v92!nSF}nP+?A+6zLYs&N7itrqhS@< ze#B%g5|O0R@)bcEfE5G@vEya&%Z}LsOT-*oI1YWSsutEz@T~sa(5afiOMM)BjXnb{ z3N;Us2MVHiS%~=jA2JKhovqDtD0RVrs@8#J$5gEmd&9i5IE96>a*&sS&dj_o^7P#w z<1(C6;y!aO^W&kRQzl1Bdsvj<3|}#IZ!4~Q;_HR2=%!CW_NbuP_yH4U$%>BU4@MJS z0)-@m<-9FC@<}vcvx$xq(LAD6KSk(H+nv=yxeec*6|c%3Z3aQyN!WW;=qZzW}99 z0T40UGNWBtpF9cNd%%3U^tmFcibKZ^0G(6n@#nNR&fZY9H6*Uss6^ggx(UyBnQb!U zbz*Wze5Pm%^Gy4@{50_tQ}S7;;CHe9$_T7q2$^e=)EwU|Is*Cthj$i(Y!sBg*h_;l7jBACY?(qlHiAoQ%Bz{`osaf!o|iXzTC1HFtUh)+LL{QjSM!Y=?tKf1Ez1b`2vrmaRR_fdJT{h4W?p}E5s zp#S{1%K=Z4a8YZ`+bUAPZT)eZOqC2M2@@Sm(EhtqS=D7a?&y&oxgi`gEA#PR=`B7%a z0lZm3u`6H?T6(Rj(`pPeki)|H8XeXQxrrN@Tt_dtRhL~ za8mV}ShZ2u;zY7jJcnu*FVCoK%pc-thbDmun6-J{tCo?PyVWgM ziOHJ1_QxFJ{}_ggrVtDA*l-*Q9z9TjQ;|7R*PW(hZHB%|2q!RwR1rn?X>8{%s_T*b zA^X~M(I9L2a6(Yksi^kgLj<=~>B8HBgvEQ!ULWe8%F$gCJUVvNJox@X0^u%bxH+iu zWwKEqMF3v-73hz&ehJALT~Iqf(bKsP_-7c(qih`6bGu1>r6s{cNiDA69q7w(j%CH| zLZLGJCA(K2-_R&oCo@ew*fdP{yg3>)gGge8{M=4;lkv6q)(3WW2v^Idt87P%i)v%M zBd!D&`FWZgcX4ZvOSt19^{a8mupwMZ-(}crRL3&{7?^r5<+D941cXVO(cX1bbLd$& zoklhJF7|KMcI5;?bJgB>m7J-> zw2}psn-hMS1{gu5gECBht|7n+Y(P?6O$-Om?p%Vi$DT*-R&O|IB(!Hfkrq{54hR= zen19CRSazYf`sk)DW?N@3G#JY6+mq{fGfPY;xw4B(Ie(k0^PU?6d)xP(^Y#$p6+~x z!5;;VnsEMI{@8cQ1`nFK1LN>Em@S3eV@U|0wl<9`l(3TPIDq9e#M}Pp$fc@3`0)0T z{{vARRf@Y+&xAP^i}hGqKBOeGk-Bp1DAoD=7cD}ls-^9x{_o2oFume<`^zjP?Ry~) z1Y{Dbb-On!c0sBRbEO@5TQRE*x)+k$2s$b$esX-YU9p&`cW)qA9`8W7RITGS2=Q*~ z6ekPHq!G4@k@<1`eGY7VxFGd({n^$hMR17s5aLsRBNO8|cdo+P!oqCJ_CrKewUw}a z{*~5MY=1x+gvO=d;3?lambq5kQJaOLq`|C&mjObc{`uo(0WIKeAR3^`5EK;ytASkb h5$6-oU)b1#ocI28)giYF^kjm-F01{Wp=|W<-vAa3;~fA1 diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 9e2ee187cc..6e78ee5d45 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -53,8 +53,9 @@ OIIOTools transcoder plugin with configurable output presets. Any incoming repre Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. -- **`Colorspace`** - target colorspace, which must be available in used color config. -- **`Display & View`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both `Colorspace` and `Display & View` at the same time. +- **`Transcoding type`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both at the same time. +- **`Colorspace`** - target colorspace, which must be available in used color config. (If `Transcoding type` is `Use Colorspace` value in configuration is used OR if empty value collected on instance from DCC). +- **`Display & View`** - display and viewer colorspace. (If `Transcoding type` is `Use Display&View` values in configuration is used OR if empty values collected on instance from DCC). - **`Arguments`** - special additional command line arguments for `oiiotool`. From 1594d7753728820b4d84568c7c5fe80aaa0ebbab Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 17:42:07 +0100 Subject: [PATCH 263/483] OP-4643 - added use case for Maya to documentation --- .../assets/global_oiio_transcode2.png | Bin 0 -> 17960 bytes .../project_settings/settings_project_global.md | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 website/docs/project_settings/assets/global_oiio_transcode2.png diff --git a/website/docs/project_settings/assets/global_oiio_transcode2.png b/website/docs/project_settings/assets/global_oiio_transcode2.png new file mode 100644 index 0000000000000000000000000000000000000000..906f780830a96b4bc6f26d98484dd3f9cc3885f2 GIT binary patch literal 17960 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 F{{cSxQDp!C literal 0 HcmV?d00001 diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 6e78ee5d45..f58d2c2bf2 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -62,6 +62,9 @@ Notable parameters: Example here describes use case for creation of new color coded review of png image sequence. Original representation's files are kept intact, review is created from transcoded files, but these files are removed in cleanup process. ![global_oiio_transcode](assets/global_oiio_transcode.png) +Another use case is to transcode in Maya only `beauty` render layers and use collected `Display` and `View` colorspaces from DCC. +![global_oiio_transcode_in_Maya](assets/global_oiio_transcode.png) + ## Profile filters Many of the settings are using a concept of **Profile filters** From 9a44a8fd7d1d5cf4102066e12c56696769609f2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 14:00:14 +0100 Subject: [PATCH 264/483] save default settings with newline character at the end of file --- openpype/settings/entities/root_entities.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/root_entities.py b/openpype/settings/entities/root_entities.py index ff76fa5180..f2e24fb522 100644 --- a/openpype/settings/entities/root_entities.py +++ b/openpype/settings/entities/root_entities.py @@ -440,8 +440,9 @@ class RootEntity(BaseItemEntity): os.makedirs(dirpath) self.log.debug("Saving data to: {}\n{}".format(subpath, value)) + data = json.dumps(value, indent=4) + "\n" with open(output_path, "w") as file_stream: - json.dump(value, file_stream, indent=4) + file_stream.write(data) dynamic_values_item = self.collect_dynamic_schema_entities() dynamic_values_item.save_values() From 974510044611ea05c58f6ef2f5eb70095ea122de Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 14:50:56 +0100 Subject: [PATCH 265/483] resave default settings --- openpype/settings/defaults/project_anatomy/attributes.json | 2 +- openpype/settings/defaults/project_anatomy/imageio.json | 2 +- openpype/settings/defaults/project_anatomy/roots.json | 2 +- openpype/settings/defaults/project_anatomy/tasks.json | 2 +- openpype/settings/defaults/project_anatomy/templates.json | 2 +- .../settings/defaults/project_settings/aftereffects.json | 2 +- openpype/settings/defaults/project_settings/blender.json | 2 +- openpype/settings/defaults/project_settings/celaction.json | 2 +- openpype/settings/defaults/project_settings/flame.json | 2 +- openpype/settings/defaults/project_settings/ftrack.json | 2 +- openpype/settings/defaults/project_settings/fusion.json | 2 +- openpype/settings/defaults/project_settings/global.json | 2 +- openpype/settings/defaults/project_settings/harmony.json | 2 +- openpype/settings/defaults/project_settings/hiero.json | 2 +- openpype/settings/defaults/project_settings/houdini.json | 2 +- openpype/settings/defaults/project_settings/kitsu.json | 2 +- openpype/settings/defaults/project_settings/max.json | 2 +- openpype/settings/defaults/project_settings/maya.json | 2 +- openpype/settings/defaults/project_settings/nuke.json | 2 +- openpype/settings/defaults/project_settings/photoshop.json | 2 +- openpype/settings/defaults/project_settings/resolve.json | 2 +- .../settings/defaults/project_settings/royalrender.json | 2 +- openpype/settings/defaults/project_settings/shotgrid.json | 2 +- openpype/settings/defaults/project_settings/slack.json | 2 +- .../defaults/project_settings/standalonepublisher.json | 2 +- .../settings/defaults/project_settings/traypublisher.json | 2 +- openpype/settings/defaults/project_settings/tvpaint.json | 2 +- openpype/settings/defaults/project_settings/unreal.json | 2 +- .../settings/defaults/project_settings/webpublisher.json | 2 +- .../settings/defaults/system_settings/applications.json | 6 ++++-- openpype/settings/defaults/system_settings/general.json | 2 +- openpype/settings/defaults/system_settings/modules.json | 2 +- openpype/settings/defaults/system_settings/tools.json | 2 +- 33 files changed, 36 insertions(+), 34 deletions(-) diff --git a/openpype/settings/defaults/project_anatomy/attributes.json b/openpype/settings/defaults/project_anatomy/attributes.json index bf8bbef8de..0cc414fb69 100644 --- a/openpype/settings/defaults/project_anatomy/attributes.json +++ b/openpype/settings/defaults/project_anatomy/attributes.json @@ -23,4 +23,4 @@ ], "tools_env": [], "active": true -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_anatomy/imageio.json b/openpype/settings/defaults/project_anatomy/imageio.json index caa2a8a206..d38d0a0774 100644 --- a/openpype/settings/defaults/project_anatomy/imageio.json +++ b/openpype/settings/defaults/project_anatomy/imageio.json @@ -255,4 +255,4 @@ ] } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_anatomy/roots.json b/openpype/settings/defaults/project_anatomy/roots.json index ce295e946f..8171d17d56 100644 --- a/openpype/settings/defaults/project_anatomy/roots.json +++ b/openpype/settings/defaults/project_anatomy/roots.json @@ -4,4 +4,4 @@ "darwin": "/Volumes/path", "linux": "/mnt/share/projects" } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_anatomy/tasks.json b/openpype/settings/defaults/project_anatomy/tasks.json index 74504cc4d7..135462839f 100644 --- a/openpype/settings/defaults/project_anatomy/tasks.json +++ b/openpype/settings/defaults/project_anatomy/tasks.json @@ -41,4 +41,4 @@ "Compositing": { "short_name": "comp" } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 32230e0625..99a869963b 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -66,4 +66,4 @@ "source": "source" } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index e4b957fb85..669e1db0b8 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -33,4 +33,4 @@ "create_first_version": false, "custom_templates": [] } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 3585d2ad0a..fe05f94590 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -82,4 +82,4 @@ "active": false } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/celaction.json b/openpype/settings/defaults/project_settings/celaction.json index ad01e62d95..bdba6d7322 100644 --- a/openpype/settings/defaults/project_settings/celaction.json +++ b/openpype/settings/defaults/project_settings/celaction.json @@ -16,4 +16,4 @@ "anatomy_template_key_metadata": "render" } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/flame.json b/openpype/settings/defaults/project_settings/flame.json index cbd99c4560..3190bdb3bf 100644 --- a/openpype/settings/defaults/project_settings/flame.json +++ b/openpype/settings/defaults/project_settings/flame.json @@ -163,4 +163,4 @@ ] } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index ec48ba52ea..4ca4a35d1f 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -496,4 +496,4 @@ "farm_status_profiles": [] } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/fusion.json b/openpype/settings/defaults/project_settings/fusion.json index 720178e17a..954606820a 100644 --- a/openpype/settings/defaults/project_settings/fusion.json +++ b/openpype/settings/defaults/project_settings/fusion.json @@ -17,4 +17,4 @@ } } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 0e078dc157..cedc2d6876 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -607,4 +607,4 @@ "linux": [] }, "project_environments": {} -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/harmony.json b/openpype/settings/defaults/project_settings/harmony.json index 1f4ea88272..3f51a9c28b 100644 --- a/openpype/settings/defaults/project_settings/harmony.json +++ b/openpype/settings/defaults/project_settings/harmony.json @@ -50,4 +50,4 @@ "skip_timelines_check": [] } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/hiero.json b/openpype/settings/defaults/project_settings/hiero.json index c6180d0a58..0412967eaa 100644 --- a/openpype/settings/defaults/project_settings/hiero.json +++ b/openpype/settings/defaults/project_settings/hiero.json @@ -97,4 +97,4 @@ } ] } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/houdini.json b/openpype/settings/defaults/project_settings/houdini.json index 68cc8945fe..1b7faf8526 100644 --- a/openpype/settings/defaults/project_settings/houdini.json +++ b/openpype/settings/defaults/project_settings/houdini.json @@ -76,4 +76,4 @@ "active": true } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/kitsu.json b/openpype/settings/defaults/project_settings/kitsu.json index 3a9723b9c0..95b3da04ae 100644 --- a/openpype/settings/defaults/project_settings/kitsu.json +++ b/openpype/settings/defaults/project_settings/kitsu.json @@ -10,4 +10,4 @@ "note_status_shortname": "wfa" } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/max.json b/openpype/settings/defaults/project_settings/max.json index 84e0c7dba7..667b42411d 100644 --- a/openpype/settings/defaults/project_settings/max.json +++ b/openpype/settings/defaults/project_settings/max.json @@ -5,4 +5,4 @@ "image_format": "exr", "multipass": true } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 03c2d325bb..b590a56da6 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -1103,4 +1103,4 @@ "ValidateNoAnimation": false } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index 2999d1427d..d475c337d9 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -533,4 +533,4 @@ "profiles": [] }, "filters": {} -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/photoshop.json b/openpype/settings/defaults/project_settings/photoshop.json index cdfab0c439..bcf21f55dd 100644 --- a/openpype/settings/defaults/project_settings/photoshop.json +++ b/openpype/settings/defaults/project_settings/photoshop.json @@ -67,4 +67,4 @@ "create_first_version": false, "custom_templates": [] } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/resolve.json b/openpype/settings/defaults/project_settings/resolve.json index 66013c5ac7..264f3bd902 100644 --- a/openpype/settings/defaults/project_settings/resolve.json +++ b/openpype/settings/defaults/project_settings/resolve.json @@ -27,4 +27,4 @@ "handleEnd": 10 } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/royalrender.json b/openpype/settings/defaults/project_settings/royalrender.json index be267b11d8..b72fed8474 100644 --- a/openpype/settings/defaults/project_settings/royalrender.json +++ b/openpype/settings/defaults/project_settings/royalrender.json @@ -4,4 +4,4 @@ "review": true } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/shotgrid.json b/openpype/settings/defaults/project_settings/shotgrid.json index 774bce714b..83b6f69074 100644 --- a/openpype/settings/defaults/project_settings/shotgrid.json +++ b/openpype/settings/defaults/project_settings/shotgrid.json @@ -19,4 +19,4 @@ "step": "step" } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/slack.json b/openpype/settings/defaults/project_settings/slack.json index c156fed08e..910f099d04 100644 --- a/openpype/settings/defaults/project_settings/slack.json +++ b/openpype/settings/defaults/project_settings/slack.json @@ -17,4 +17,4 @@ ] } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/standalonepublisher.json b/openpype/settings/defaults/project_settings/standalonepublisher.json index b6e2e056a1..d923b4db43 100644 --- a/openpype/settings/defaults/project_settings/standalonepublisher.json +++ b/openpype/settings/defaults/project_settings/standalonepublisher.json @@ -304,4 +304,4 @@ } } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/traypublisher.json b/openpype/settings/defaults/project_settings/traypublisher.json index 8a222a6dd2..fdea4aeaba 100644 --- a/openpype/settings/defaults/project_settings/traypublisher.json +++ b/openpype/settings/defaults/project_settings/traypublisher.json @@ -321,4 +321,4 @@ "active": true } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 40603ed874..0b6d3d7e81 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -109,4 +109,4 @@ "custom_templates": [] }, "filters": {} -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/unreal.json b/openpype/settings/defaults/project_settings/unreal.json index b06bf28714..75cee11bd9 100644 --- a/openpype/settings/defaults/project_settings/unreal.json +++ b/openpype/settings/defaults/project_settings/unreal.json @@ -14,4 +14,4 @@ "project_setup": { "dev_mode": true } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/project_settings/webpublisher.json b/openpype/settings/defaults/project_settings/webpublisher.json index 27eac131b7..e830ba6a40 100644 --- a/openpype/settings/defaults/project_settings/webpublisher.json +++ b/openpype/settings/defaults/project_settings/webpublisher.json @@ -141,4 +141,4 @@ "layer_name_regex": "(?PL[0-9]{3}_\\w+)_(?P.+)" } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 936407a49b..f84d99e36b 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -1302,7 +1302,9 @@ "variant_label": "Current", "use_python_2": false, "executables": { - "windows": ["C:/Program Files/CelAction/CelAction2D Studio/CelAction2D.exe"], + "windows": [ + "C:/Program Files/CelAction/CelAction2D Studio/CelAction2D.exe" + ], "darwin": [], "linux": [] }, @@ -1365,4 +1367,4 @@ } }, "additional_apps": {} -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/system_settings/general.json b/openpype/settings/defaults/system_settings/general.json index 909ffc1ee4..d2994d1a62 100644 --- a/openpype/settings/defaults/system_settings/general.json +++ b/openpype/settings/defaults/system_settings/general.json @@ -18,4 +18,4 @@ "production_version": "", "staging_version": "", "version_check_interval": 5 -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/system_settings/modules.json b/openpype/settings/defaults/system_settings/modules.json index 703e72cb5d..1ddbfd2726 100644 --- a/openpype/settings/defaults/system_settings/modules.json +++ b/openpype/settings/defaults/system_settings/modules.json @@ -211,4 +211,4 @@ "linux": "" } } -} \ No newline at end of file +} diff --git a/openpype/settings/defaults/system_settings/tools.json b/openpype/settings/defaults/system_settings/tools.json index 243cde40cc..921e13af3a 100644 --- a/openpype/settings/defaults/system_settings/tools.json +++ b/openpype/settings/defaults/system_settings/tools.json @@ -87,4 +87,4 @@ "renderman": "Pixar Renderman" } } -} \ No newline at end of file +} From aa9817ae5272db128d66d009189811700b65b492 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 25 Jan 2023 11:09:19 +0000 Subject: [PATCH 266/483] Basic implementation of the new Creator --- openpype/hosts/unreal/api/__init__.py | 6 +- openpype/hosts/unreal/api/pipeline.py | 53 ++++++- openpype/hosts/unreal/api/plugin.py | 209 +++++++++++++++++++++++++- 3 files changed, 262 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/api/__init__.py b/openpype/hosts/unreal/api/__init__.py index ca9db259e6..2618a7677c 100644 --- a/openpype/hosts/unreal/api/__init__.py +++ b/openpype/hosts/unreal/api/__init__.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- """Unreal Editor OpenPype host API.""" -from .plugin import Loader +from .plugin import ( + UnrealActorCreator, + UnrealAssetCreator, + Loader +) from .pipeline import ( install, diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 2081c8fd13..7a21effcbc 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import os +import json import logging from typing import List from contextlib import contextmanager @@ -16,13 +17,14 @@ from openpype.pipeline import ( ) from openpype.tools.utils import host_tools import openpype.hosts.unreal -from openpype.host import HostBase, ILoadHost +from openpype.host import HostBase, ILoadHost, IPublishHost import unreal # noqa - logger = logging.getLogger("openpype.hosts.unreal") + OPENPYPE_CONTAINERS = "OpenPypeContainers" +CONTEXT_CONTAINER = "OpenPype/context.json" UNREAL_VERSION = semver.VersionInfo( *os.getenv("OPENPYPE_UNREAL_VERSION").split(".") ) @@ -35,7 +37,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") -class UnrealHost(HostBase, ILoadHost): +class UnrealHost(HostBase, ILoadHost, IPublishHost): """Unreal host implementation. For some time this class will re-use functions from module based @@ -60,6 +62,26 @@ class UnrealHost(HostBase, ILoadHost): show_tools_dialog() + def update_context_data(self, data, changes): + unreal.log_warning("update_context_data") + unreal.log_warning(data) + content_path = unreal.Paths.project_content_dir() + op_ctx = content_path + CONTEXT_CONTAINER + with open(op_ctx, "w+") as f: + json.dump(data, f) + with open(op_ctx, "r") as fp: + test = eval(json.load(fp)) + unreal.log_warning(test) + + def get_context_data(self): + content_path = unreal.Paths.project_content_dir() + op_ctx = content_path + CONTEXT_CONTAINER + if not os.path.isfile(op_ctx): + return {} + with open(op_ctx, "r") as fp: + data = eval(json.load(fp)) + return data + def install(): """Install Unreal configuration for OpenPype.""" @@ -133,6 +155,31 @@ def ls(): yield data +def lsinst(): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + # UE 5.1 changed how class name is specified + class_name = [ + "/Script/OpenPype", + "OpenPypePublishInstance" + ] if ( + UNREAL_VERSION.major == 5 + and UNREAL_VERSION.minor > 0 + ) else "OpenPypePublishInstance" # noqa + instances = ar.get_assets_by_class(class_name, True) + + # get_asset_by_class returns AssetData. To get all metadata we need to + # load asset. get_tag_values() work only on metadata registered in + # Asset Registry Project settings (and there is no way to set it with + # python short of editing ini configuration file). + for asset_data in instances: + asset = asset_data.get_asset() + data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) + data["objectName"] = asset_data.asset_name + data = cast_map_to_str_dict(data) + + yield data + + def parse_container(container): """To get data from container, AssetContainer must be loaded. diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 6fc00cb71c..f89ff153b1 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,7 +1,212 @@ # -*- coding: utf-8 -*- -from abc import ABC +import sys +import six +from abc import ( + ABC, + ABCMeta, + abstractmethod +) -from openpype.pipeline import LoaderPlugin +import unreal + +from .pipeline import ( + create_publish_instance, + imprint, + lsinst +) +from openpype.lib import BoolDef +from openpype.pipeline import ( + Creator, + LoaderPlugin, + CreatorError, + CreatedInstance +) + + +class OpenPypeCreatorError(CreatorError): + pass + + +@six.add_metaclass(ABCMeta) +class UnrealBaseCreator(Creator): + """Base class for Unreal creator plugins.""" + root = "/Game/OpenPype/PublishInstances" + suffix = "_INS" + + @staticmethod + def cache_subsets(shared_data): + """Cache instances for Creators to shared data. + + Create `unreal_cached_subsets` key when needed in shared data and + fill it with all collected instances from the scene under its + respective creator identifiers. + + If legacy instances are detected in the scene, create + `unreal_cached_legacy_subsets` there and fill it with + all legacy subsets under family as a key. + + Args: + Dict[str, Any]: Shared data. + + Return: + Dict[str, Any]: Shared data dictionary. + + """ + if shared_data.get("unreal_cached_subsets") is None: + shared_data["unreal_cached_subsets"] = {} + if shared_data.get("unreal_cached_legacy_subsets") is None: + shared_data["unreal_cached_legacy_subsets"] = {} + cached_instances = lsinst() + for i in cached_instances: + if not i.get("creator_identifier"): + # we have legacy instance + family = i.get("family") + if (family not in + shared_data["unreal_cached_legacy_subsets"]): + shared_data[ + "unreal_cached_legacy_subsets"][family] = [i] + else: + shared_data[ + "unreal_cached_legacy_subsets"][family].append(i) + continue + + creator_id = i.get("creator_identifier") + if creator_id not in shared_data["unreal_cached_subsets"]: + shared_data["unreal_cached_subsets"][creator_id] = [i] + else: + shared_data["unreal_cached_subsets"][creator_id].append(i) + return shared_data + + @abstractmethod + def create(self, subset_name, instance_data, pre_create_data): + pass + + def collect_instances(self): + # cache instances if missing + self.cache_subsets(self.collection_shared_data) + for instance in self.collection_shared_data[ + "unreal_cached_subsets"].get(self.identifier, []): + created_instance = CreatedInstance.from_existing(instance, self) + self._add_instance_to_context(created_instance) + + def update_instances(self, update_list): + unreal.log_warning(f"Update instances: {update_list}") + for created_inst, _changes in update_list: + instance_node = created_inst.get("instance_path", "") + + if not instance_node: + unreal.log_warning( + f"Instance node not found for {created_inst}") + + new_values = { + key: new_value + for key, (_old_value, new_value) in _changes.items() + } + imprint( + instance_node, + new_values + ) + + def remove_instances(self, instances): + for instance in instances: + instance_node = instance.data.get("instance_path", "") + if instance_node: + unreal.EditorAssetLibrary.delete_asset(instance_node) + + self._remove_instance_from_context(instance) + + def get_pre_create_attr_defs(self): + return [ + BoolDef("use_selection", label="Use selection") + ] + + +@six.add_metaclass(ABCMeta) +class UnrealAssetCreator(UnrealBaseCreator): + """Base class for Unreal creator plugins based on assets.""" + + def create(self, subset_name, instance_data, pre_create_data): + """Create instance of the asset. + + Args: + subset_name (str): Name of the subset. + instance_data (dict): Data for the instance. + pre_create_data (dict): Data for the instance. + + Returns: + CreatedInstance: Created instance. + """ + try: + selection = [] + + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + instance_name = f"{subset_name}{self.suffix}" + create_publish_instance(instance_name, self.root) + instance_data["members"] = selection + instance_data["subset"] = subset_name + instance_data["instance_path"] = f"{self.root}/{instance_name}" + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + self._add_instance_to_context(instance) + + imprint(f"{self.root}/{instance_name}", instance_data) + + except Exception as er: + six.reraise( + OpenPypeCreatorError, + OpenPypeCreatorError(f"Creator error: {er}"), + sys.exc_info()[2]) + + +@six.add_metaclass(ABCMeta) +class UnrealActorCreator(UnrealBaseCreator): + """Base class for Unreal creator plugins based on actors.""" + + def create(self, subset_name, instance_data, pre_create_data): + """Create instance of the asset. + + Args: + subset_name (str): Name of the subset. + instance_data (dict): Data for the instance. + pre_create_data (dict): Data for the instance. + + Returns: + CreatedInstance: Created instance. + """ + try: + selection = [] + + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() + selection = [a.get_path_name() for a in sel_objects] + + instance_name = f"{subset_name}{self.suffix}" + create_publish_instance(instance_name, self.root) + instance_data["members"] = selection + instance_data[ + "level"] = unreal.EditorLevelLibrary.get_editor_world() + instance_data["subset"] = subset_name + instance_data["instance_path"] = f"{self.root}/{instance_name}" + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + self._add_instance_to_context(instance) + + imprint(f"{self.root}/{instance_name}", instance_data) + + except Exception as er: + six.reraise( + OpenPypeCreatorError, + OpenPypeCreatorError(f"Creator error: {er}"), + sys.exc_info()[2]) class Loader(LoaderPlugin, ABC): From f0db455a09287223677c333b08a70a459120df6c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 26 Jan 2023 17:35:11 +0000 Subject: [PATCH 267/483] Improved basic creator --- openpype/hosts/unreal/api/plugin.py | 95 ++++++++++++++++++----------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index f89ff153b1..6a561420fa 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -4,7 +4,6 @@ import six from abc import ( ABC, ABCMeta, - abstractmethod ) import unreal @@ -12,7 +11,8 @@ import unreal from .pipeline import ( create_publish_instance, imprint, - lsinst + lsinst, + UNREAL_VERSION ) from openpype.lib import BoolDef from openpype.pipeline import ( @@ -77,9 +77,28 @@ class UnrealBaseCreator(Creator): shared_data["unreal_cached_subsets"][creator_id].append(i) return shared_data - @abstractmethod def create(self, subset_name, instance_data, pre_create_data): - pass + try: + instance_name = f"{subset_name}{self.suffix}" + create_publish_instance(instance_name, self.root) + + instance_data["subset"] = subset_name + instance_data["instance_path"] = f"{self.root}/{instance_name}" + + instance = CreatedInstance( + self.family, + subset_name, + instance_data, + self) + self._add_instance_to_context(instance) + + imprint(f"{self.root}/{instance_name}", instance_data) + + except Exception as er: + six.reraise( + OpenPypeCreatorError, + OpenPypeCreatorError(f"Creator error: {er}"), + sys.exc_info()[2]) def collect_instances(self): # cache instances if missing @@ -117,7 +136,7 @@ class UnrealBaseCreator(Creator): def get_pre_create_attr_defs(self): return [ - BoolDef("use_selection", label="Use selection") + BoolDef("use_selection", label="Use selection", default=True) ] @@ -137,25 +156,21 @@ class UnrealAssetCreator(UnrealBaseCreator): CreatedInstance: Created instance. """ try: - selection = [] + # Check if instance data has members, filled by the plugin. + # If not, use selection. + if not instance_data.get("members"): + selection = [] - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] - instance_name = f"{subset_name}{self.suffix}" - create_publish_instance(instance_name, self.root) - instance_data["members"] = selection - instance_data["subset"] = subset_name - instance_data["instance_path"] = f"{self.root}/{instance_name}" - instance = CreatedInstance( - self.family, + instance_data["members"] = selection + + super(UnrealAssetCreator, self).create( subset_name, instance_data, - self) - self._add_instance_to_context(instance) - - imprint(f"{self.root}/{instance_name}", instance_data) + pre_create_data) except Exception as er: six.reraise( @@ -180,27 +195,33 @@ class UnrealActorCreator(UnrealBaseCreator): CreatedInstance: Created instance. """ try: - selection = [] + if UNREAL_VERSION.major == 5: + world = unreal.UnrealEditorSubsystem().get_editor_world() + else: + world = unreal.EditorLevelLibrary.get_editor_world() - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() - selection = [a.get_path_name() for a in sel_objects] + # Check if the level is saved + if world.get_path_name().startswith("/Temp/"): + raise OpenPypeCreatorError( + "Level must be saved before creating instances.") - instance_name = f"{subset_name}{self.suffix}" - create_publish_instance(instance_name, self.root) - instance_data["members"] = selection - instance_data[ - "level"] = unreal.EditorLevelLibrary.get_editor_world() - instance_data["subset"] = subset_name - instance_data["instance_path"] = f"{self.root}/{instance_name}" - instance = CreatedInstance( - self.family, + # Check if instance data has members, filled by the plugin. + # If not, use selection. + if not instance_data.get("members"): + selection = [] + + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() + selection = [a.get_path_name() for a in sel_objects] + + instance_data["members"] = selection + + instance_data["level"] = world.get_path_name() + + super(UnrealActorCreator, self).create( subset_name, instance_data, - self) - self._add_instance_to_context(instance) - - imprint(f"{self.root}/{instance_name}", instance_data) + pre_create_data) except Exception as er: six.reraise( From 7284601d62c2304ce6fe07b538a858745e7d8869 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 26 Jan 2023 17:35:42 +0000 Subject: [PATCH 268/483] Updated creators to be compatible with new publisher --- .../unreal/plugins/create/create_camera.py | 44 +++---------- .../unreal/plugins/create/create_layout.py | 39 ++--------- .../unreal/plugins/create/create_look.py | 64 +++++++++---------- .../plugins/create/create_staticmeshfbx.py | 34 ++-------- .../unreal/plugins/create/create_uasset.py | 44 ++++--------- 5 files changed, 65 insertions(+), 160 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index bf1489d688..239dc87db5 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -1,41 +1,13 @@ -import unreal -from unreal import EditorAssetLibrary as eal -from unreal import EditorLevelLibrary as ell - -from openpype.hosts.unreal.api.pipeline import instantiate -from openpype.pipeline import LegacyCreator +# -*- coding: utf-8 -*- +from openpype.hosts.unreal.api.plugin import ( + UnrealActorCreator, +) -class CreateCamera(LegacyCreator): - """Layout output for character rigs""" +class CreateCamera(UnrealActorCreator): + """Create Camera.""" - name = "layoutMain" + identifier = "io.openpype.creators.unreal.camera" label = "Camera" family = "camera" - icon = "cubes" - - root = "/Game/OpenPype/Instances" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateCamera, self).__init__(*args, **kwargs) - - def process(self): - data = self.data - - name = data["subset"] - - data["level"] = ell.get_editor_world().get_path_name() - - if not eal.does_directory_exist(self.root): - eal.make_directory(self.root) - - factory = unreal.LevelSequenceFactoryNew() - tools = unreal.AssetToolsHelpers().get_asset_tools() - tools.create_asset(name, f"{self.root}/{name}", None, factory) - - asset_name = f"{self.root}/{name}/{name}.{name}" - - data["members"] = [asset_name] - - instantiate(f"{self.root}", name, data, None, self.suffix) + icon = "camera" diff --git a/openpype/hosts/unreal/plugins/create/create_layout.py b/openpype/hosts/unreal/plugins/create/create_layout.py index c1067b00d9..1d2e800a13 100644 --- a/openpype/hosts/unreal/plugins/create/create_layout.py +++ b/openpype/hosts/unreal/plugins/create/create_layout.py @@ -1,42 +1,13 @@ # -*- coding: utf-8 -*- -from unreal import EditorLevelLibrary - -from openpype.pipeline import LegacyCreator -from openpype.hosts.unreal.api.pipeline import instantiate +from openpype.hosts.unreal.api.plugin import ( + UnrealActorCreator, +) -class CreateLayout(LegacyCreator): +class CreateLayout(UnrealActorCreator): """Layout output for character rigs.""" - name = "layoutMain" + identifier = "io.openpype.creators.unreal.layout" label = "Layout" family = "layout" icon = "cubes" - - root = "/Game" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateLayout, self).__init__(*args, **kwargs) - - def process(self): - data = self.data - - name = data["subset"] - - selection = [] - # if (self.options or {}).get("useSelection"): - # sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - # selection = [a.get_path_name() for a in sel_objects] - - data["level"] = EditorLevelLibrary.get_editor_world().get_path_name() - - data["members"] = [] - - if (self.options or {}).get("useSelection"): - # Set as members the selected actors - for actor in EditorLevelLibrary.get_selected_level_actors(): - data["members"].append("{}.{}".format( - actor.get_outer().get_name(), actor.get_name())) - - instantiate(self.root, name, data, selection, self.suffix) diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 4abf3f6095..08d61ab9f8 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -1,56 +1,53 @@ # -*- coding: utf-8 -*- -"""Create look in Unreal.""" -import unreal # noqa -from openpype.hosts.unreal.api import pipeline, plugin -from openpype.pipeline import LegacyCreator +import unreal + +from openpype.hosts.unreal.api.pipeline import ( + create_folder +) +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator +) -class CreateLook(LegacyCreator): +class CreateLook(UnrealAssetCreator): """Shader connections defining shape look.""" - name = "unrealLook" - label = "Unreal - Look" + identifier = "io.openpype.creators.unreal.look" + label = "Look" family = "look" icon = "paint-brush" - root = "/Game/Avalon/Assets" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateLook, self).__init__(*args, **kwargs) - - def process(self): - name = self.data["subset"] - + def create(self, subset_name, instance_data, pre_create_data): selection = [] - if (self.options or {}).get("useSelection"): + if pre_create_data.get("use_selection"): sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] + if len(selection) != 1: + raise RuntimeError("Please select only one asset.") + + selected_asset = selection[0] + + look_directory = "/Game/OpenPype/Looks" + # Create the folder - path = f"{self.root}/{self.data['asset']}" - new_name = pipeline.create_folder(path, name) - full_path = f"{path}/{new_name}" + folder_name = create_folder(look_directory, subset_name) + path = f"{look_directory}/{folder_name}" # Create a new cube static mesh ar = unreal.AssetRegistryHelpers.get_asset_registry() cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube") - # Create the avalon publish instance object - container_name = f"{name}{self.suffix}" - pipeline.create_publish_instance( - instance=container_name, path=full_path) - # Get the mesh of the selected object - original_mesh = ar.get_asset_by_object_path(selection[0]).get_asset() - materials = original_mesh.get_editor_property('materials') + original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset() + materials = original_mesh.get_editor_property('static_materials') - self.data["members"] = [] + instance_data["members"] = [] # Add the materials to the cube for material in materials: - name = material.get_editor_property('material_slot_name') - object_path = f"{full_path}/{name}.{name}" + mat_name = material.get_editor_property('material_slot_name') + object_path = f"{path}/{mat_name}.{mat_name}" unreal_object = unreal.EditorAssetLibrary.duplicate_loaded_asset( cube.get_asset(), object_path ) @@ -61,8 +58,11 @@ class CreateLook(LegacyCreator): unreal_object.add_material( material.get_editor_property('material_interface')) - self.data["members"].append(object_path) + instance_data["members"].append(object_path) unreal.EditorAssetLibrary.save_asset(object_path) - pipeline.imprint(f"{full_path}/{container_name}", self.data) + super(CreateLook, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py index 45d517d27d..1acf7084d1 100644 --- a/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/create/create_staticmeshfbx.py @@ -1,35 +1,13 @@ # -*- coding: utf-8 -*- -"""Create Static Meshes as FBX geometry.""" -import unreal # noqa -from openpype.hosts.unreal.api.pipeline import ( - instantiate, +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, ) -from openpype.pipeline import LegacyCreator -class CreateStaticMeshFBX(LegacyCreator): - """Static FBX geometry.""" +class CreateStaticMeshFBX(UnrealAssetCreator): + """Create Static Meshes as FBX geometry.""" - name = "unrealStaticMeshMain" - label = "Unreal - Static Mesh" + identifier = "io.openpype.creators.unreal.staticmeshfbx" + label = "Static Mesh (FBX)" family = "unrealStaticMesh" icon = "cube" - asset_types = ["StaticMesh"] - - root = "/Game" - suffix = "_INS" - - def __init__(self, *args, **kwargs): - super(CreateStaticMeshFBX, self).__init__(*args, **kwargs) - - def process(self): - - name = self.data["subset"] - - selection = [] - if (self.options or {}).get("useSelection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - unreal.log("selection: {}".format(selection)) - instantiate(self.root, name, self.data, selection, self.suffix) diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index ee584ac00c..2d6fcc1d59 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -1,36 +1,25 @@ -"""Create UAsset.""" +# -*- coding: utf-8 -*- from pathlib import Path import unreal -from openpype.hosts.unreal.api import pipeline -from openpype.pipeline import LegacyCreator +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, +) -class CreateUAsset(LegacyCreator): - """UAsset.""" +class CreateUAsset(UnrealAssetCreator): + """Create UAsset.""" - name = "UAsset" + identifier = "io.openpype.creators.unreal.uasset" label = "UAsset" family = "uasset" icon = "cube" - root = "/Game/OpenPype" - suffix = "_INS" + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("use_selection"): + ar = unreal.AssetRegistryHelpers.get_asset_registry() - def __init__(self, *args, **kwargs): - super(CreateUAsset, self).__init__(*args, **kwargs) - - def process(self): - ar = unreal.AssetRegistryHelpers.get_asset_registry() - - subset = self.data["subset"] - path = f"{self.root}/PublishInstances/" - - unreal.EditorAssetLibrary.make_directory(path) - - selection = [] - if (self.options or {}).get("useSelection"): sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] @@ -50,12 +39,7 @@ class CreateUAsset(LegacyCreator): if Path(sys_path).suffix != ".uasset": raise RuntimeError(f"{Path(sys_path).name} is not a UAsset.") - unreal.log("selection: {}".format(selection)) - container_name = f"{subset}{self.suffix}" - pipeline.create_publish_instance( - instance=container_name, path=path) - - data = self.data.copy() - data["members"] = selection - - pipeline.imprint(f"{path}/{container_name}", data) + super(CreateUAsset, self).create( + subset_name, + instance_data, + pre_create_data) From ca7ae2a306218ab729e51af94e70b842aabfd671 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 27 Jan 2023 16:53:39 +0000 Subject: [PATCH 269/483] Updated render creator --- .../unreal/plugins/create/create_render.py | 174 ++++++++++-------- 1 file changed, 94 insertions(+), 80 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index a85d17421b..de3efdad74 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,117 +1,131 @@ +# -*- coding: utf-8 -*- import unreal -from openpype.hosts.unreal.api import pipeline -from openpype.pipeline import LegacyCreator +from openpype.hosts.unreal.api.pipeline import ( + get_subsequences +) +from openpype.hosts.unreal.api.plugin import ( + UnrealAssetCreator, +) -class CreateRender(LegacyCreator): +class CreateRender(UnrealAssetCreator): """Create instance for sequence for rendering""" - name = "unrealRender" - label = "Unreal - Render" + identifier = "io.openpype.creators.unreal.render" + label = "Render" family = "render" - icon = "cube" - asset_types = ["LevelSequence"] - - root = "/Game/OpenPype/PublishInstances" - suffix = "_INS" - - def process(self): - subset = self.data["subset"] + icon = "eye" + def create(self, subset_name, instance_data, pre_create_data): ar = unreal.AssetRegistryHelpers.get_asset_registry() - # The asset name is the the third element of the path which contains - # the map. - # The index of the split path is 3 because the first element is an - # empty string, as the path begins with "/Content". - a = unreal.EditorUtilityLibrary.get_selected_assets()[0] - asset_name = a.get_path_name().split("/")[3] - - # Get the master sequence and the master level. - # There should be only one sequence and one level in the directory. - filter = unreal.ARFilter( - class_names=["LevelSequence"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - sequences = ar.get_assets(filter) - ms = sequences[0].get_editor_property('object_path') - filter = unreal.ARFilter( - class_names=["World"], - package_paths=[f"/Game/OpenPype/{asset_name}"], - recursive_paths=False) - levels = ar.get_assets(filter) - ml = levels[0].get_editor_property('object_path') - - selection = [] - if (self.options or {}).get("useSelection"): + if pre_create_data.get("use_selection"): sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() selection = [ a.get_path_name() for a in sel_objects - if a.get_class().get_name() in self.asset_types] + if a.get_class().get_name() == "LevelSequence"] else: - selection.append(self.data['sequence']) + selection = [instance_data['sequence']] - unreal.log(f"selection: {selection}") + seq_data = None - path = f"{self.root}" - unreal.EditorAssetLibrary.make_directory(path) + for sel in selection: + selected_asset = ar.get_asset_by_object_path(sel).get_asset() + selected_asset_path = selected_asset.get_path_name() - ar = unreal.AssetRegistryHelpers.get_asset_registry() + # Check if the selected asset is a level sequence asset. + if selected_asset.get_class().get_name() != "LevelSequence": + unreal.log_warning( + f"Skipping {selected_asset.get_name()}. It isn't a Level " + "Sequence.") - for a in selection: - ms_obj = ar.get_asset_by_object_path(ms).get_asset() + # The asset name is the the third element of the path which + # contains the map. + # To take the asset name, we remove from the path the prefix + # "/Game/OpenPype/" and then we split the path by "/". + sel_path = selected_asset_path + asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0] - seq_data = None + # Get the master sequence and the master level. + # There should be only one sequence and one level in the directory. + ar_filter = unreal.ARFilter( + class_names=["LevelSequence"], + package_paths=[f"/Game/OpenPype/{asset_name}"], + recursive_paths=False) + sequences = ar.get_assets(ar_filter) + master_seq = sequences[0].get_asset().get_path_name() + master_seq_obj = sequences[0].get_asset() + ar_filter = unreal.ARFilter( + class_names=["World"], + package_paths=[f"/Game/OpenPype/{asset_name}"], + recursive_paths=False) + levels = ar.get_assets(ar_filter) + master_lvl = levels[0].get_asset().get_path_name() - if a == ms: - seq_data = { - "sequence": ms_obj, - "output": f"{ms_obj.get_name()}", - "frame_range": ( - ms_obj.get_playback_start(), ms_obj.get_playback_end()) - } + # If the selected asset is the master sequence, we get its data + # and then we create the instance for the master sequence. + # Otherwise, we cycle from the master sequence to find the selected + # sequence and we get its data. This data will be used to create + # the instance for the selected sequence. In particular, + # we get the frame range of the selected sequence and its final + # output path. + master_seq_data = { + "sequence": master_seq_obj, + "output": f"{master_seq_obj.get_name()}", + "frame_range": ( + master_seq_obj.get_playback_start(), + master_seq_obj.get_playback_end())} + + if selected_asset_path == master_seq: + seq_data = master_seq_data else: - seq_data_list = [{ - "sequence": ms_obj, - "output": f"{ms_obj.get_name()}", - "frame_range": ( - ms_obj.get_playback_start(), ms_obj.get_playback_end()) - }] + seq_data_list = [master_seq_data] - for s in seq_data_list: - subscenes = pipeline.get_subsequences(s.get('sequence')) + for seq in seq_data_list: + subscenes = get_subsequences(seq.get('sequence')) - for ss in subscenes: + for sub_seq in subscenes: + sub_seq_obj = sub_seq.get_sequence() curr_data = { - "sequence": ss.get_sequence(), - "output": (f"{s.get('output')}/" - f"{ss.get_sequence().get_name()}"), + "sequence": sub_seq_obj, + "output": (f"{seq.get('output')}/" + f"{sub_seq_obj.get_name()}"), "frame_range": ( - ss.get_start_frame(), ss.get_end_frame() - 1) - } + sub_seq.get_start_frame(), + sub_seq.get_end_frame() - 1)} - if ss.get_sequence().get_path_name() == a: + # If the selected asset is the current sub-sequence, + # we get its data and we break the loop. + # Otherwise, we add the current sub-sequence data to + # the list of sequences to check. + if sub_seq_obj.get_path_name() == selected_asset_path: seq_data = curr_data break + seq_data_list.append(curr_data) + # If we found the selected asset, we break the loop. if seq_data is not None: break + # If we didn't find the selected asset, we don't create the + # instance. if not seq_data: + unreal.log_warning( + f"Skipping {selected_asset.get_name()}. It isn't a " + "sub-sequence of the master sequence.") continue - d = self.data.copy() - d["members"] = [a] - d["sequence"] = a - d["master_sequence"] = ms - d["master_level"] = ml - d["output"] = seq_data.get('output') - d["frameStart"] = seq_data.get('frame_range')[0] - d["frameEnd"] = seq_data.get('frame_range')[1] + instance_data["members"] = [selected_asset_path] + instance_data["sequence"] = selected_asset_path + instance_data["master_sequence"] = master_seq + instance_data["master_level"] = master_lvl + instance_data["output"] = seq_data.get('output') + instance_data["frameStart"] = seq_data.get('frame_range')[0] + instance_data["frameEnd"] = seq_data.get('frame_range')[1] - container_name = f"{subset}{self.suffix}" - pipeline.create_publish_instance( - instance=container_name, path=path) - pipeline.imprint(f"{path}/{container_name}", d) + super(CreateRender, self).create( + subset_name, + instance_data, + pre_create_data) From a4c751b49ad43dfd4226fd3a99348877b8e5c084 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 30 Jan 2023 11:17:21 +0000 Subject: [PATCH 270/483] Hound fixes --- openpype/hosts/unreal/api/plugin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 6a561420fa..71ce0c18a7 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -104,7 +104,7 @@ class UnrealBaseCreator(Creator): # cache instances if missing self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data[ - "unreal_cached_subsets"].get(self.identifier, []): + "unreal_cached_subsets"].get(self.identifier, []): created_instance = CreatedInstance.from_existing(instance, self) self._add_instance_to_context(created_instance) @@ -162,7 +162,8 @@ class UnrealAssetCreator(UnrealBaseCreator): selection = [] if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + utility_lib = unreal.EditorUtilityLibrary + sel_objects = utility_lib.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] instance_data["members"] = selection @@ -211,7 +212,8 @@ class UnrealActorCreator(UnrealBaseCreator): selection = [] if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_actors() + utility_lib = unreal.EditorUtilityLibrary + sel_objects = utility_lib.get_selected_assets() selection = [a.get_path_name() for a in sel_objects] instance_data["members"] = selection From 833860468c90395601679941fdb0d69b05a7a472 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 31 Jan 2023 16:05:01 +0000 Subject: [PATCH 271/483] Collect instances is no longer needed with the new publisher --- .../plugins/publish/collect_instances.py | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 openpype/hosts/unreal/plugins/publish/collect_instances.py diff --git a/openpype/hosts/unreal/plugins/publish/collect_instances.py b/openpype/hosts/unreal/plugins/publish/collect_instances.py deleted file mode 100644 index 27b711cad6..0000000000 --- a/openpype/hosts/unreal/plugins/publish/collect_instances.py +++ /dev/null @@ -1,67 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect publishable instances in Unreal.""" -import ast -import unreal # noqa -import pyblish.api -from openpype.hosts.unreal.api.pipeline import UNREAL_VERSION -from openpype.pipeline.publish import KnownPublishError - - -class CollectInstances(pyblish.api.ContextPlugin): - """Gather instances by OpenPypePublishInstance class - - This collector finds all paths containing `OpenPypePublishInstance` class - asset - - Identifier: - id (str): "pyblish.avalon.instance" - - """ - - label = "Collect Instances" - order = pyblish.api.CollectorOrder - 0.1 - hosts = ["unreal"] - - def process(self, context): - - ar = unreal.AssetRegistryHelpers.get_asset_registry() - class_name = [ - "/Script/OpenPype", - "OpenPypePublishInstance" - ] if ( - UNREAL_VERSION.major == 5 - and UNREAL_VERSION.minor > 0 - ) else "OpenPypePublishInstance" # noqa - instance_containers = ar.get_assets_by_class(class_name, True) - - for container_data in instance_containers: - asset = container_data.get_asset() - data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset) - data["objectName"] = container_data.asset_name - # convert to strings - data = {str(key): str(value) for (key, value) in data.items()} - if not data.get("family"): - raise KnownPublishError("instance has no family") - - # content of container - members = ast.literal_eval(data.get("members")) - self.log.debug(members) - self.log.debug(asset.get_path_name()) - # remove instance container - self.log.info("Creating instance for {}".format(asset.get_name())) - - instance = context.create_instance(asset.get_name()) - instance[:] = members - - # Store the exact members of the object set - instance.data["setMembers"] = members - instance.data["families"] = [data.get("family")] - instance.data["level"] = data.get("level") - instance.data["parent"] = data.get("parent") - - label = "{0} ({1})".format(asset.get_name()[:-4], - data["asset"]) - - instance.data["label"] = label - - instance.data.update(data) From ba6d77b88ba5f808c9c6e2a33dfacdb5388ddf91 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 11:22:36 +0000 Subject: [PATCH 272/483] Use External Data in the Unreal Publish Instance to store members Not possible with all the families. Some families require to store actors in a scenes, and we cannot store them in the External Data. --- openpype/hosts/unreal/api/plugin.py | 24 ++++++--- .../unreal/plugins/create/create_look.py | 6 ++- .../publish/collect_instance_members.py | 49 +++++++++++++++++++ .../unreal/plugins/publish/extract_look.py | 4 +- .../unreal/plugins/publish/extract_uasset.py | 8 ++- 5 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 openpype/hosts/unreal/plugins/publish/collect_instance_members.py diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 71ce0c18a7..da571af9be 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -80,7 +80,7 @@ class UnrealBaseCreator(Creator): def create(self, subset_name, instance_data, pre_create_data): try: instance_name = f"{subset_name}{self.suffix}" - create_publish_instance(instance_name, self.root) + pub_instance = create_publish_instance(instance_name, self.root) instance_data["subset"] = subset_name instance_data["instance_path"] = f"{self.root}/{instance_name}" @@ -92,6 +92,15 @@ class UnrealBaseCreator(Creator): self) self._add_instance_to_context(instance) + pub_instance.set_editor_property('add_external_assets', True) + assets = pub_instance.get_editor_property('asset_data_external') + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + for member in pre_create_data.get("members", []): + obj = ar.get_asset_by_object_path(member).get_asset() + assets.add(obj) + imprint(f"{self.root}/{instance_name}", instance_data) except Exception as er: @@ -158,15 +167,14 @@ class UnrealAssetCreator(UnrealBaseCreator): try: # Check if instance data has members, filled by the plugin. # If not, use selection. - if not instance_data.get("members"): - selection = [] + if not pre_create_data.get("members"): + pre_create_data["members"] = [] if pre_create_data.get("use_selection"): - utility_lib = unreal.EditorUtilityLibrary - sel_objects = utility_lib.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] - - instance_data["members"] = selection + utilib = unreal.EditorUtilityLibrary + sel_objects = utilib.get_selected_assets() + pre_create_data["members"] = [ + a.get_path_name() for a in sel_objects] super(UnrealAssetCreator, self).create( subset_name, diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 08d61ab9f8..047764ef2a 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -34,6 +34,8 @@ class CreateLook(UnrealAssetCreator): folder_name = create_folder(look_directory, subset_name) path = f"{look_directory}/{folder_name}" + instance_data["look"] = path + # Create a new cube static mesh ar = unreal.AssetRegistryHelpers.get_asset_registry() cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube") @@ -42,7 +44,7 @@ class CreateLook(UnrealAssetCreator): original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset() materials = original_mesh.get_editor_property('static_materials') - instance_data["members"] = [] + pre_create_data["members"] = [] # Add the materials to the cube for material in materials: @@ -58,7 +60,7 @@ class CreateLook(UnrealAssetCreator): unreal_object.add_material( material.get_editor_property('material_interface')) - instance_data["members"].append(object_path) + pre_create_data["members"].append(object_path) unreal.EditorAssetLibrary.save_asset(object_path) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py new file mode 100644 index 0000000000..74969f5033 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py @@ -0,0 +1,49 @@ +import unreal + +import pyblish.api + + +class CollectInstanceMembers(pyblish.api.InstancePlugin): + """ + Collect members of instance. + + This collector will collect the assets for the families that support to + have them included as External Data, and will add them to the instance + as members. + """ + + order = pyblish.api.CollectorOrder + 0.1 + hosts = ["unreal"] + families = ["look", "unrealStaticMesh", "uasset"] + label = "Collect Instance Members" + + def process(self, instance): + """Collect members of instance.""" + self.log.info("Collecting instance members") + + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + inst_path = instance.data.get('instance_path') + inst_name = instance.data.get('objectName') + + pub_instance = ar.get_asset_by_object_path( + f"{inst_path}.{inst_name}").get_asset() + + if not pub_instance: + self.log.error(f"{inst_path}.{inst_name}") + raise RuntimeError(f"Instance {instance} not found.") + + if not pub_instance.get_editor_property("add_external_assets"): + # No external assets in the instance + return + + assets = pub_instance.get_editor_property('asset_data_external') + + members = [] + + for asset in assets: + members.append(asset.get_path_name()) + + self.log.debug(f"Members: {members}") + + instance.data["members"] = members diff --git a/openpype/hosts/unreal/plugins/publish/extract_look.py b/openpype/hosts/unreal/plugins/publish/extract_look.py index f999ad8651..4b32b4eb95 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_look.py +++ b/openpype/hosts/unreal/plugins/publish/extract_look.py @@ -29,13 +29,13 @@ class ExtractLook(publish.Extractor): for member in instance: asset = ar.get_asset_by_object_path(member) - object = asset.get_asset() + obj = asset.get_asset() name = asset.get_editor_property('asset_name') json_element = {'material': str(name)} - material_obj = object.get_editor_property('static_materials')[0] + material_obj = obj.get_editor_property('static_materials')[0] material = material_obj.material_interface base_color = mat_lib.get_material_property_input_node( diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py index 89d779d368..f719df2a82 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_uasset.py +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -22,7 +22,13 @@ class ExtractUAsset(publish.Extractor): staging_dir = self.staging_dir(instance) filename = "{}.uasset".format(instance.name) - obj = instance[0] + members = instance.data.get("members", []) + + if not members: + raise RuntimeError("No members found in instance.") + + # UAsset publishing supports only one member + obj = members[0] asset = ar.get_asset_by_object_path(obj).get_asset() sys_path = unreal.SystemLibrary.get_system_path(asset) From 6f15f39e4ffcbc5f4d8f09627901b5cc78daa13f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 12:18:03 +0000 Subject: [PATCH 273/483] Improved attributes for the creators --- openpype/hosts/unreal/api/plugin.py | 20 +++++++++++++------ .../unreal/plugins/create/create_render.py | 6 ++++++ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index da571af9be..7121aea20b 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -14,7 +14,10 @@ from .pipeline import ( lsinst, UNREAL_VERSION ) -from openpype.lib import BoolDef +from openpype.lib import ( + BoolDef, + UILabelDef +) from openpype.pipeline import ( Creator, LoaderPlugin, @@ -143,11 +146,6 @@ class UnrealBaseCreator(Creator): self._remove_instance_from_context(instance) - def get_pre_create_attr_defs(self): - return [ - BoolDef("use_selection", label="Use selection", default=True) - ] - @six.add_metaclass(ABCMeta) class UnrealAssetCreator(UnrealBaseCreator): @@ -187,6 +185,11 @@ class UnrealAssetCreator(UnrealBaseCreator): OpenPypeCreatorError(f"Creator error: {er}"), sys.exc_info()[2]) + def get_pre_create_attr_defs(self): + return [ + BoolDef("use_selection", label="Use selection", default=True) + ] + @six.add_metaclass(ABCMeta) class UnrealActorCreator(UnrealBaseCreator): @@ -239,6 +242,11 @@ class UnrealActorCreator(UnrealBaseCreator): OpenPypeCreatorError(f"Creator error: {er}"), sys.exc_info()[2]) + def get_pre_create_attr_defs(self): + return [ + UILabelDef("Select actors to create instance from them.") + ] + class Loader(LoaderPlugin, ABC): """This serves as skeleton for future OpenPype specific functionality""" diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index de3efdad74..8100a5016c 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -7,6 +7,7 @@ from openpype.hosts.unreal.api.pipeline import ( from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, ) +from openpype.lib import UILabelDef class CreateRender(UnrealAssetCreator): @@ -129,3 +130,8 @@ class CreateRender(UnrealAssetCreator): subset_name, instance_data, pre_create_data) + + def get_pre_create_attr_defs(self): + return [ + UILabelDef("Select the sequence to render.") + ] From f3834db1ae65566e91e4ba5532befbab637a333e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 16:15:03 +0000 Subject: [PATCH 274/483] Fix render creator problem with selection --- .../hosts/unreal/plugins/create/create_render.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index 8100a5016c..a1e3e43a78 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -6,6 +6,7 @@ from openpype.hosts.unreal.api.pipeline import ( ) from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, + OpenPypeCreatorError ) from openpype.lib import UILabelDef @@ -21,13 +22,13 @@ class CreateRender(UnrealAssetCreator): def create(self, subset_name, instance_data, pre_create_data): ar = unreal.AssetRegistryHelpers.get_asset_registry() - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [ - a.get_path_name() for a in sel_objects - if a.get_class().get_name() == "LevelSequence"] - else: - selection = [instance_data['sequence']] + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [ + a.get_path_name() for a in sel_objects + if a.get_class().get_name() == "LevelSequence"] + + if len(selection) == 0: + raise RuntimeError("Please select at least one Level Sequence.") seq_data = None From cae2186d402e6546e14b66c9a99d4bd772311cd3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 2 Feb 2023 16:17:23 +0000 Subject: [PATCH 275/483] Hound fixes --- openpype/hosts/unreal/plugins/create/create_render.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index a1e3e43a78..c957e50e29 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -5,8 +5,7 @@ from openpype.hosts.unreal.api.pipeline import ( get_subsequences ) from openpype.hosts.unreal.api.plugin import ( - UnrealAssetCreator, - OpenPypeCreatorError + UnrealAssetCreator ) from openpype.lib import UILabelDef From b54d83247800c601f5e120cd02ed69e37bd347ef Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Feb 2023 11:41:57 +0000 Subject: [PATCH 276/483] Implemented suggestions from review --- openpype/hosts/unreal/api/pipeline.py | 3 - openpype/hosts/unreal/api/plugin.py | 69 +++++++------------ .../unreal/plugins/create/create_camera.py | 2 +- .../unreal/plugins/create/create_look.py | 14 ++-- 4 files changed, 37 insertions(+), 51 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 7a21effcbc..0fe8c02ec5 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -69,9 +69,6 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): op_ctx = content_path + CONTEXT_CONTAINER with open(op_ctx, "w+") as f: json.dump(data, f) - with open(op_ctx, "r") as fp: - test = eval(json.load(fp)) - unreal.log_warning(test) def get_context_data(self): content_path = unreal.Paths.project_content_dir() diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 7121aea20b..fc724105b6 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- +import collections import sys import six -from abc import ( - ABC, - ABCMeta, -) +from abc import ABC import unreal @@ -26,11 +24,6 @@ from openpype.pipeline import ( ) -class OpenPypeCreatorError(CreatorError): - pass - - -@six.add_metaclass(ABCMeta) class UnrealBaseCreator(Creator): """Base class for Unreal creator plugins.""" root = "/Game/OpenPype/PublishInstances" @@ -56,28 +49,20 @@ class UnrealBaseCreator(Creator): """ if shared_data.get("unreal_cached_subsets") is None: - shared_data["unreal_cached_subsets"] = {} - if shared_data.get("unreal_cached_legacy_subsets") is None: - shared_data["unreal_cached_legacy_subsets"] = {} - cached_instances = lsinst() - for i in cached_instances: - if not i.get("creator_identifier"): - # we have legacy instance - family = i.get("family") - if (family not in - shared_data["unreal_cached_legacy_subsets"]): - shared_data[ - "unreal_cached_legacy_subsets"][family] = [i] - else: - shared_data[ - "unreal_cached_legacy_subsets"][family].append(i) - continue - - creator_id = i.get("creator_identifier") - if creator_id not in shared_data["unreal_cached_subsets"]: - shared_data["unreal_cached_subsets"][creator_id] = [i] + unreal_cached_subsets = collections.defaultdict(list) + unreal_cached_legacy_subsets = collections.defaultdict(list) + for instance in lsinst(): + creator_id = instance.get("creator_identifier") + if creator_id: + unreal_cached_subsets[creator_id].append(instance) else: - shared_data["unreal_cached_subsets"][creator_id].append(i) + family = instance.get("family") + unreal_cached_legacy_subsets[family].append(instance) + + shared_data["unreal_cached_subsets"] = unreal_cached_subsets + shared_data["unreal_cached_legacy_subsets"] = ( + unreal_cached_legacy_subsets + ) return shared_data def create(self, subset_name, instance_data, pre_create_data): @@ -108,8 +93,8 @@ class UnrealBaseCreator(Creator): except Exception as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError(f"Creator error: {er}"), + CreatorError, + CreatorError(f"Creator error: {er}"), sys.exc_info()[2]) def collect_instances(self): @@ -121,17 +106,17 @@ class UnrealBaseCreator(Creator): self._add_instance_to_context(created_instance) def update_instances(self, update_list): - unreal.log_warning(f"Update instances: {update_list}") - for created_inst, _changes in update_list: + for created_inst, changes in update_list: instance_node = created_inst.get("instance_path", "") if not instance_node: unreal.log_warning( f"Instance node not found for {created_inst}") + continue new_values = { - key: new_value - for key, (_old_value, new_value) in _changes.items() + key: changes[key].new_value + for key in changes.changed_keys } imprint( instance_node, @@ -147,7 +132,6 @@ class UnrealBaseCreator(Creator): self._remove_instance_from_context(instance) -@six.add_metaclass(ABCMeta) class UnrealAssetCreator(UnrealBaseCreator): """Base class for Unreal creator plugins based on assets.""" @@ -181,8 +165,8 @@ class UnrealAssetCreator(UnrealBaseCreator): except Exception as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError(f"Creator error: {er}"), + CreatorError, + CreatorError(f"Creator error: {er}"), sys.exc_info()[2]) def get_pre_create_attr_defs(self): @@ -191,7 +175,6 @@ class UnrealAssetCreator(UnrealBaseCreator): ] -@six.add_metaclass(ABCMeta) class UnrealActorCreator(UnrealBaseCreator): """Base class for Unreal creator plugins based on actors.""" @@ -214,7 +197,7 @@ class UnrealActorCreator(UnrealBaseCreator): # Check if the level is saved if world.get_path_name().startswith("/Temp/"): - raise OpenPypeCreatorError( + raise CreatorError( "Level must be saved before creating instances.") # Check if instance data has members, filled by the plugin. @@ -238,8 +221,8 @@ class UnrealActorCreator(UnrealBaseCreator): except Exception as er: six.reraise( - OpenPypeCreatorError, - OpenPypeCreatorError(f"Creator error: {er}"), + CreatorError, + CreatorError(f"Creator error: {er}"), sys.exc_info()[2]) def get_pre_create_attr_defs(self): diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 239dc87db5..00815e1ed4 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -10,4 +10,4 @@ class CreateCamera(UnrealActorCreator): identifier = "io.openpype.creators.unreal.camera" label = "Camera" family = "camera" - icon = "camera" + icon = "fa.camera" diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index 047764ef2a..cecb88bca3 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -7,6 +7,7 @@ from openpype.hosts.unreal.api.pipeline import ( from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator ) +from openpype.lib import UILabelDef class CreateLook(UnrealAssetCreator): @@ -18,10 +19,10 @@ class CreateLook(UnrealAssetCreator): icon = "paint-brush" def create(self, subset_name, instance_data, pre_create_data): - selection = [] - if pre_create_data.get("use_selection"): - sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] + # We need to set this to True for the parent class to work + pre_create_data["use_selection"] = True + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] if len(selection) != 1: raise RuntimeError("Please select only one asset.") @@ -68,3 +69,8 @@ class CreateLook(UnrealAssetCreator): subset_name, instance_data, pre_create_data) + + def get_pre_create_attr_defs(self): + return [ + UILabelDef("Select the asset from which to create the look.") + ] From 4f882b9b1989edaa53464ce3375ca85124d65489 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 15 Feb 2023 11:45:30 +0000 Subject: [PATCH 277/483] Fixed problem with the instance metadata --- openpype/hosts/unreal/api/pipeline.py | 2 +- openpype/hosts/unreal/api/plugin.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 0fe8c02ec5..0810ec7c07 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -76,7 +76,7 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): if not os.path.isfile(op_ctx): return {} with open(op_ctx, "r") as fp: - data = eval(json.load(fp)) + data = json.load(fp) return data diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index fc724105b6..a852ed9bb1 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import ast import collections import sys import six @@ -89,7 +90,9 @@ class UnrealBaseCreator(Creator): obj = ar.get_asset_by_object_path(member).get_asset() assets.add(obj) - imprint(f"{self.root}/{instance_name}", instance_data) + imprint(f"{self.root}/{instance_name}", instance.data_to_store()) + + return instance except Exception as er: six.reraise( @@ -102,6 +105,11 @@ class UnrealBaseCreator(Creator): self.cache_subsets(self.collection_shared_data) for instance in self.collection_shared_data[ "unreal_cached_subsets"].get(self.identifier, []): + # Unreal saves metadata as string, so we need to convert it back + instance['creator_attributes'] = ast.literal_eval( + instance.get('creator_attributes', '{}')) + instance['publish_attributes'] = ast.literal_eval( + instance.get('publish_attributes', '{}')) created_instance = CreatedInstance.from_existing(instance, self) self._add_instance_to_context(created_instance) From 606b3e3449e5f9ad0493ec3e93358bdd3a65f4c0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 20 Feb 2023 12:31:13 +0000 Subject: [PATCH 278/483] More changes from suggestions --- openpype/hosts/unreal/api/pipeline.py | 2 +- openpype/hosts/unreal/api/plugin.py | 12 +++++++++--- openpype/hosts/unreal/plugins/create/create_look.py | 3 ++- .../hosts/unreal/plugins/create/create_render.py | 7 ++++--- .../hosts/unreal/plugins/create/create_uasset.py | 7 ++++--- .../plugins/publish/collect_instance_members.py | 5 +---- 6 files changed, 21 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 0810ec7c07..4a22189c14 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -152,7 +152,7 @@ def ls(): yield data -def lsinst(): +def ls_inst(): ar = unreal.AssetRegistryHelpers.get_asset_registry() # UE 5.1 changed how class name is specified class_name = [ diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index a852ed9bb1..2498a249e8 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -3,14 +3,17 @@ import ast import collections import sys import six -from abc import ABC +from abc import ( + ABC, + ABCMeta, +) import unreal from .pipeline import ( create_publish_instance, imprint, - lsinst, + ls_inst, UNREAL_VERSION ) from openpype.lib import ( @@ -25,6 +28,7 @@ from openpype.pipeline import ( ) +@six.add_metaclass(ABCMeta) class UnrealBaseCreator(Creator): """Base class for Unreal creator plugins.""" root = "/Game/OpenPype/PublishInstances" @@ -52,7 +56,7 @@ class UnrealBaseCreator(Creator): if shared_data.get("unreal_cached_subsets") is None: unreal_cached_subsets = collections.defaultdict(list) unreal_cached_legacy_subsets = collections.defaultdict(list) - for instance in lsinst(): + for instance in ls_inst(): creator_id = instance.get("creator_identifier") if creator_id: unreal_cached_subsets[creator_id].append(instance) @@ -140,6 +144,7 @@ class UnrealBaseCreator(Creator): self._remove_instance_from_context(instance) +@six.add_metaclass(ABCMeta) class UnrealAssetCreator(UnrealBaseCreator): """Base class for Unreal creator plugins based on assets.""" @@ -183,6 +188,7 @@ class UnrealAssetCreator(UnrealBaseCreator): ] +@six.add_metaclass(ABCMeta) class UnrealActorCreator(UnrealBaseCreator): """Base class for Unreal creator plugins based on actors.""" diff --git a/openpype/hosts/unreal/plugins/create/create_look.py b/openpype/hosts/unreal/plugins/create/create_look.py index cecb88bca3..f6c73e47e6 100644 --- a/openpype/hosts/unreal/plugins/create/create_look.py +++ b/openpype/hosts/unreal/plugins/create/create_look.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import unreal +from openpype.pipeline import CreatorError from openpype.hosts.unreal.api.pipeline import ( create_folder ) @@ -25,7 +26,7 @@ class CreateLook(UnrealAssetCreator): selection = [a.get_path_name() for a in sel_objects] if len(selection) != 1: - raise RuntimeError("Please select only one asset.") + raise CreatorError("Please select only one asset.") selected_asset = selection[0] diff --git a/openpype/hosts/unreal/plugins/create/create_render.py b/openpype/hosts/unreal/plugins/create/create_render.py index c957e50e29..5834d2e7a7 100644 --- a/openpype/hosts/unreal/plugins/create/create_render.py +++ b/openpype/hosts/unreal/plugins/create/create_render.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import unreal +from openpype.pipeline import CreatorError from openpype.hosts.unreal.api.pipeline import ( get_subsequences ) @@ -26,8 +27,8 @@ class CreateRender(UnrealAssetCreator): a.get_path_name() for a in sel_objects if a.get_class().get_name() == "LevelSequence"] - if len(selection) == 0: - raise RuntimeError("Please select at least one Level Sequence.") + if not selection: + raise CreatorError("Please select at least one Level Sequence.") seq_data = None @@ -41,7 +42,7 @@ class CreateRender(UnrealAssetCreator): f"Skipping {selected_asset.get_name()}. It isn't a Level " "Sequence.") - # The asset name is the the third element of the path which + # The asset name is the third element of the path which # contains the map. # To take the asset name, we remove from the path the prefix # "/Game/OpenPype/" and then we split the path by "/". diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py index 2d6fcc1d59..70f17d478b 100644 --- a/openpype/hosts/unreal/plugins/create/create_uasset.py +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -3,6 +3,7 @@ from pathlib import Path import unreal +from openpype.pipeline import CreatorError from openpype.hosts.unreal.api.plugin import ( UnrealAssetCreator, ) @@ -24,7 +25,7 @@ class CreateUAsset(UnrealAssetCreator): selection = [a.get_path_name() for a in sel_objects] if len(selection) != 1: - raise RuntimeError("Please select only one object.") + raise CreatorError("Please select only one object.") obj = selection[0] @@ -32,12 +33,12 @@ class CreateUAsset(UnrealAssetCreator): sys_path = unreal.SystemLibrary.get_system_path(asset) if not sys_path: - raise RuntimeError( + raise CreatorError( f"{Path(obj).name} is not on the disk. Likely it needs to" "be saved first.") if Path(sys_path).suffix != ".uasset": - raise RuntimeError(f"{Path(sys_path).name} is not a UAsset.") + raise CreatorError(f"{Path(sys_path).name} is not a UAsset.") super(CreateUAsset, self).create( subset_name, diff --git a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py index 74969f5033..bd467a98fb 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py +++ b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py @@ -39,10 +39,7 @@ class CollectInstanceMembers(pyblish.api.InstancePlugin): assets = pub_instance.get_editor_property('asset_data_external') - members = [] - - for asset in assets: - members.append(asset.get_path_name()) + members = [asset.get_path_name() for asset in assets] self.log.debug(f"Members: {members}") From 637c1c08068a57a98972f8a0c29838f5628e6645 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Feb 2023 12:28:33 +0000 Subject: [PATCH 279/483] Addressing a concurrency issue when trying to access context --- openpype/hosts/unreal/api/pipeline.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/api/pipeline.py b/openpype/hosts/unreal/api/pipeline.py index 4a22189c14..8a5a459194 100644 --- a/openpype/hosts/unreal/api/pipeline.py +++ b/openpype/hosts/unreal/api/pipeline.py @@ -5,6 +5,7 @@ import logging from typing import List from contextlib import contextmanager import semver +import time import pyblish.api @@ -63,12 +64,21 @@ class UnrealHost(HostBase, ILoadHost, IPublishHost): show_tools_dialog() def update_context_data(self, data, changes): - unreal.log_warning("update_context_data") - unreal.log_warning(data) content_path = unreal.Paths.project_content_dir() op_ctx = content_path + CONTEXT_CONTAINER - with open(op_ctx, "w+") as f: - json.dump(data, f) + attempts = 3 + for i in range(attempts): + try: + with open(op_ctx, "w+") as f: + json.dump(data, f) + break + except IOError: + if i == attempts - 1: + raise Exception("Failed to write context data. Aborting.") + unreal.log_warning("Failed to write context data. Retrying...") + i += 1 + time.sleep(3) + continue def get_context_data(self): content_path = unreal.Paths.project_content_dir() From e958a692896027fe351e1b74266546eef5eb3fb3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Feb 2023 17:30:23 +0000 Subject: [PATCH 280/483] Fix UnrealActorCreator when getting actors from the scene --- openpype/hosts/unreal/api/plugin.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/api/plugin.py b/openpype/hosts/unreal/api/plugin.py index 2498a249e8..d60050a696 100644 --- a/openpype/hosts/unreal/api/plugin.py +++ b/openpype/hosts/unreal/api/plugin.py @@ -217,12 +217,9 @@ class UnrealActorCreator(UnrealBaseCreator): # Check if instance data has members, filled by the plugin. # If not, use selection. if not instance_data.get("members"): - selection = [] - - if pre_create_data.get("use_selection"): - utility_lib = unreal.EditorUtilityLibrary - sel_objects = utility_lib.get_selected_assets() - selection = [a.get_path_name() for a in sel_objects] + actor_subsystem = unreal.EditorActorSubsystem() + sel_actors = actor_subsystem.get_selected_level_actors() + selection = [a.get_path_name() for a in sel_actors] instance_data["members"] = selection From 8fb21d9a1444326ed7b43aa4efe7f4e7f7aeaa2b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Feb 2023 17:32:17 +0000 Subject: [PATCH 281/483] Fix Camera Publishing --- .../unreal/plugins/create/create_camera.py | 29 ++++++++- .../publish/collect_instance_members.py | 2 +- .../unreal/plugins/publish/extract_camera.py | 64 ++++++++++++++----- 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/unreal/plugins/create/create_camera.py b/openpype/hosts/unreal/plugins/create/create_camera.py index 00815e1ed4..642924e2d6 100644 --- a/openpype/hosts/unreal/plugins/create/create_camera.py +++ b/openpype/hosts/unreal/plugins/create/create_camera.py @@ -1,13 +1,38 @@ # -*- coding: utf-8 -*- +import unreal + +from openpype.pipeline import CreatorError +from openpype.hosts.unreal.api.pipeline import UNREAL_VERSION from openpype.hosts.unreal.api.plugin import ( - UnrealActorCreator, + UnrealAssetCreator, ) -class CreateCamera(UnrealActorCreator): +class CreateCamera(UnrealAssetCreator): """Create Camera.""" identifier = "io.openpype.creators.unreal.camera" label = "Camera" family = "camera" icon = "fa.camera" + + def create(self, subset_name, instance_data, pre_create_data): + if pre_create_data.get("use_selection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + if len(selection) != 1: + raise CreatorError("Please select only one object.") + + # Add the current level path to the metadata + if UNREAL_VERSION.major == 5: + world = unreal.UnrealEditorSubsystem().get_editor_world() + else: + world = unreal.EditorLevelLibrary.get_editor_world() + + instance_data["level"] = world.get_path_name() + + super(CreateCamera, self).create( + subset_name, + instance_data, + pre_create_data) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py index bd467a98fb..46ca51ab7e 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_instance_members.py +++ b/openpype/hosts/unreal/plugins/publish/collect_instance_members.py @@ -14,7 +14,7 @@ class CollectInstanceMembers(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.1 hosts = ["unreal"] - families = ["look", "unrealStaticMesh", "uasset"] + families = ["camera", "look", "unrealStaticMesh", "uasset"] label = "Collect Instance Members" def process(self, instance): diff --git a/openpype/hosts/unreal/plugins/publish/extract_camera.py b/openpype/hosts/unreal/plugins/publish/extract_camera.py index 4e37cc6a86..70a835aca2 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_camera.py +++ b/openpype/hosts/unreal/plugins/publish/extract_camera.py @@ -3,10 +3,9 @@ import os import unreal -from unreal import EditorAssetLibrary as eal -from unreal import EditorLevelLibrary as ell from openpype.pipeline import publish +from openpype.hosts.unreal.api.pipeline import UNREAL_VERSION class ExtractCamera(publish.Extractor): @@ -18,6 +17,8 @@ class ExtractCamera(publish.Extractor): optional = True def process(self, instance): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + # Define extract output file path staging_dir = self.staging_dir(instance) fbx_filename = "{}.fbx".format(instance.name) @@ -26,23 +27,54 @@ class ExtractCamera(publish.Extractor): self.log.info("Performing extraction..") # Check if the loaded level is the same of the instance - current_level = ell.get_editor_world().get_path_name() + if UNREAL_VERSION.major == 5: + world = unreal.UnrealEditorSubsystem().get_editor_world() + else: + world = unreal.EditorLevelLibrary.get_editor_world() + current_level = world.get_path_name() assert current_level == instance.data.get("level"), \ "Wrong level loaded" - for member in instance[:]: - data = eal.find_asset_data(member) - if data.asset_class == "LevelSequence": - ar = unreal.AssetRegistryHelpers.get_asset_registry() - sequence = ar.get_asset_by_object_path(member).get_asset() - unreal.SequencerTools.export_fbx( - ell.get_editor_world(), - sequence, - sequence.get_bindings(), - unreal.FbxExportOption(), - os.path.join(staging_dir, fbx_filename) - ) - break + for member in instance.data.get('members'): + data = ar.get_asset_by_object_path(member) + if UNREAL_VERSION.major == 5: + is_level_sequence = ( + data.asset_class_path.asset_name == "LevelSequence") + else: + is_level_sequence = (data.asset_class == "LevelSequence") + + if is_level_sequence: + sequence = data.get_asset() + if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor >= 1: + params = unreal.SequencerExportFBXParams( + world=world, + root_sequence=sequence, + sequence=sequence, + bindings=sequence.get_bindings(), + master_tracks=sequence.get_master_tracks(), + fbx_file_name=os.path.join(staging_dir, fbx_filename) + ) + unreal.SequencerTools.export_level_sequence_fbx(params) + elif UNREAL_VERSION.major == 4 and UNREAL_VERSION.minor == 26: + unreal.SequencerTools.export_fbx( + world, + sequence, + sequence.get_bindings(), + unreal.FbxExportOption(), + os.path.join(staging_dir, fbx_filename) + ) + else: + # Unreal 5.0 or 4.27 + unreal.SequencerTools.export_level_sequence_fbx( + world, + sequence, + sequence.get_bindings(), + unreal.FbxExportOption(), + os.path.join(staging_dir, fbx_filename) + ) + + if not os.path.isfile(os.path.join(staging_dir, fbx_filename)): + raise RuntimeError("Failed to extract camera") if "representations" not in instance.data: instance.data["representations"] = [] From 1dd41d072d5b038e03f20b0d19262ea34933a841 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Feb 2023 17:33:29 +0000 Subject: [PATCH 282/483] Hound fixes --- openpype/hosts/unreal/plugins/publish/extract_camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/publish/extract_camera.py b/openpype/hosts/unreal/plugins/publish/extract_camera.py index 70a835aca2..16e365ca96 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_camera.py +++ b/openpype/hosts/unreal/plugins/publish/extract_camera.py @@ -39,7 +39,7 @@ class ExtractCamera(publish.Extractor): data = ar.get_asset_by_object_path(member) if UNREAL_VERSION.major == 5: is_level_sequence = ( - data.asset_class_path.asset_name == "LevelSequence") + data.asset_class_path.asset_name == "LevelSequence") else: is_level_sequence = (data.asset_class == "LevelSequence") From 9f44aff3ed946e7158c8ba3be67ae084384fbd7c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 21 Feb 2023 17:40:10 +0000 Subject: [PATCH 283/483] Changed menu to remove Create and link Publish to new publisher --- openpype/hosts/unreal/api/tools_ui.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/unreal/api/tools_ui.py b/openpype/hosts/unreal/api/tools_ui.py index 708e167a65..8531472142 100644 --- a/openpype/hosts/unreal/api/tools_ui.py +++ b/openpype/hosts/unreal/api/tools_ui.py @@ -17,9 +17,8 @@ class ToolsBtnsWidget(QtWidgets.QWidget): def __init__(self, parent=None): super(ToolsBtnsWidget, self).__init__(parent) - create_btn = QtWidgets.QPushButton("Create...", self) load_btn = QtWidgets.QPushButton("Load...", self) - publish_btn = QtWidgets.QPushButton("Publish...", self) + publish_btn = QtWidgets.QPushButton("Publisher...", self) manage_btn = QtWidgets.QPushButton("Manage...", self) render_btn = QtWidgets.QPushButton("Render...", self) experimental_tools_btn = QtWidgets.QPushButton( @@ -28,7 +27,6 @@ class ToolsBtnsWidget(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(create_btn, 0) layout.addWidget(load_btn, 0) layout.addWidget(publish_btn, 0) layout.addWidget(manage_btn, 0) @@ -36,7 +34,6 @@ class ToolsBtnsWidget(QtWidgets.QWidget): layout.addWidget(experimental_tools_btn, 0) layout.addStretch(1) - create_btn.clicked.connect(self._on_create) load_btn.clicked.connect(self._on_load) publish_btn.clicked.connect(self._on_publish) manage_btn.clicked.connect(self._on_manage) @@ -50,7 +47,7 @@ class ToolsBtnsWidget(QtWidgets.QWidget): self.tool_required.emit("loader") def _on_publish(self): - self.tool_required.emit("publish") + self.tool_required.emit("publisher") def _on_manage(self): self.tool_required.emit("sceneinventory") From a07e76ebb56d9dc325f11b40a5b336f4125725e0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Feb 2023 11:05:26 +0100 Subject: [PATCH 284/483] OP-4643 - updates to documentation Co-authored-by: Roy Nieterau --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index f58d2c2bf2..d904080ad1 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -51,7 +51,7 @@ OIIOTools transcoder plugin with configurable output presets. Any incoming repre `oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. Notable parameters: -- **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. +- **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation loses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. - **`Transcoding type`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both at the same time. - **`Colorspace`** - target colorspace, which must be available in used color config. (If `Transcoding type` is `Use Colorspace` value in configuration is used OR if empty value collected on instance from DCC). From 2a912abbcebccdf16e21e4ac597b17f8b282f932 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 10:53:44 +0100 Subject: [PATCH 285/483] extract sequence can ignore layer's transparency --- .../plugins/publish/extract_sequence.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py index f2856c72a9..1a21715aa2 100644 --- a/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py +++ b/openpype/hosts/tvpaint/plugins/publish/extract_sequence.py @@ -59,6 +59,10 @@ class ExtractSequence(pyblish.api.Extractor): ) ) + ignore_layers_transparency = instance.data.get( + "ignoreLayersTransparency", False + ) + family_lowered = instance.data["family"].lower() mark_in = instance.context.data["sceneMarkIn"] mark_out = instance.context.data["sceneMarkOut"] @@ -114,7 +118,11 @@ class ExtractSequence(pyblish.api.Extractor): else: # Render output result = self.render( - output_dir, mark_in, mark_out, filtered_layers + output_dir, + mark_in, + mark_out, + filtered_layers, + ignore_layers_transparency ) output_filepaths_by_frame_idx, thumbnail_fullpath = result @@ -274,7 +282,9 @@ class ExtractSequence(pyblish.api.Extractor): return output_filepaths_by_frame_idx, thumbnail_filepath - def render(self, output_dir, mark_in, mark_out, layers): + def render( + self, output_dir, mark_in, mark_out, layers, ignore_layer_opacity + ): """ Export images from TVPaint. Args: @@ -282,6 +292,7 @@ class ExtractSequence(pyblish.api.Extractor): mark_in (int): Starting frame index from which export will begin. mark_out (int): On which frame index export will end. layers (list): List of layers to be exported. + ignore_layer_opacity (bool): Layer's opacity will be ignored. Returns: tuple: With 2 items first is list of filenames second is path to @@ -323,7 +334,7 @@ class ExtractSequence(pyblish.api.Extractor): for layer_id, render_data in extraction_data_by_layer_id.items(): layer = layers_by_id[layer_id] filepaths_by_layer_id[layer_id] = self._render_layer( - render_data, layer, output_dir + render_data, layer, output_dir, ignore_layer_opacity ) # Prepare final filepaths where compositing should store result @@ -380,7 +391,9 @@ class ExtractSequence(pyblish.api.Extractor): red, green, blue = self.review_bg return (red, green, blue) - def _render_layer(self, render_data, layer, output_dir): + def _render_layer( + self, render_data, layer, output_dir, ignore_layer_opacity + ): frame_references = render_data["frame_references"] filenames_by_frame_index = render_data["filenames_by_frame_index"] @@ -389,6 +402,12 @@ class ExtractSequence(pyblish.api.Extractor): "tv_layerset {}".format(layer_id), "tv_SaveMode \"PNG\"" ] + # Set density to 100 and store previous opacity + if ignore_layer_opacity: + george_script_lines.extend([ + "tv_layerdensity 100", + "orig_opacity = result", + ]) filepaths_by_frame = {} frames_to_render = [] @@ -409,6 +428,10 @@ class ExtractSequence(pyblish.api.Extractor): # Store image to output george_script_lines.append("tv_saveimage \"{}\"".format(dst_path)) + # Set density back to origin opacity + if ignore_layer_opacity: + george_script_lines.append("tv_layerdensity orig_opacity") + self.log.debug("Rendering Exposure frames {} of layer {} ({})".format( ",".join(frames_to_render), layer_id, layer["name"] )) From fd086776102236e9137a8d6d80d56b5d8def2a3a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 10:58:49 +0100 Subject: [PATCH 286/483] collect render instances can set 'ignoreLayersTransparency' --- .../tvpaint/plugins/publish/collect_render_instances.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py b/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py index ba89deac5d..e89fbf7882 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_render_instances.py @@ -9,6 +9,8 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): hosts = ["tvpaint"] families = ["render", "review"] + ignore_render_pass_transparency = False + def process(self, instance): context = instance.context creator_identifier = instance.data["creator_identifier"] @@ -63,6 +65,9 @@ class CollectRenderInstances(pyblish.api.InstancePlugin): for layer in layers_data if layer["name"] in layer_names ] + instance.data["ignoreLayersTransparency"] = ( + self.ignore_render_pass_transparency + ) render_layer_data = None render_layer_id = creator_attributes["render_layer_instance_id"] From 9c8a9412006a2c6a598cbb5d9253ca8dfb837126 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 10:59:03 +0100 Subject: [PATCH 287/483] added settings for Collect Render Instances --- .../defaults/project_settings/tvpaint.json | 3 +++ .../projects_schema/schema_project_tvpaint.json | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 0b6d3d7e81..87d3601ae4 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -49,6 +49,9 @@ } }, "publish": { + "CollectRenderInstances": { + "ignore_render_pass_transparency": true + }, "ExtractSequence": { "review_bg": [ 255, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 57016a8311..708b688ba5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -241,6 +241,20 @@ "key": "publish", "label": "Publish plugins", "children": [ + { + "type": "dict", + "collapsible": true, + "key": "CollectRenderInstances", + "label": "Collect Render Instances", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "ignore_render_pass_transparency", + "label": "Ignore Render Pass opacity" + } + ] + }, { "type": "dict", "collapsible": true, From 0cfc46f4140531e5646df353c81d8ec6895b1b44 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 11:19:49 +0100 Subject: [PATCH 288/483] disable ignore transparency by default --- openpype/settings/defaults/project_settings/tvpaint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 87d3601ae4..e06a67a254 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -50,7 +50,7 @@ }, "publish": { "CollectRenderInstances": { - "ignore_render_pass_transparency": true + "ignore_render_pass_transparency": false }, "ExtractSequence": { "review_bg": [ From ddf333fcdb502ecb8ce10a9b2d2152de198c686c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Feb 2023 10:39:10 +0100 Subject: [PATCH 289/483] fix access to not existing settings key --- openpype/hosts/tvpaint/plugins/create/create_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 40386efe91..7e85977b11 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -660,7 +660,6 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): ["create"] ["auto_detect_render"] ) - self.enabled = plugin_settings["enabled"] self.allow_group_rename = plugin_settings["allow_group_rename"] self.group_name_template = plugin_settings["group_name_template"] self.group_idx_offset = plugin_settings["group_idx_offset"] From bae2ded2f711d68291d6a6d14e60cb19f856d1c5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Feb 2023 11:30:15 +0100 Subject: [PATCH 290/483] Fix - check existence only if not None review_path might be None --- openpype/modules/slack/plugins/publish/integrate_slack_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/slack/plugins/publish/integrate_slack_api.py b/openpype/modules/slack/plugins/publish/integrate_slack_api.py index 4e2557ccc7..86c97586d2 100644 --- a/openpype/modules/slack/plugins/publish/integrate_slack_api.py +++ b/openpype/modules/slack/plugins/publish/integrate_slack_api.py @@ -187,7 +187,7 @@ class IntegrateSlackAPI(pyblish.api.InstancePlugin): repre_review_path = get_publish_repre_path( instance, repre, False ) - if os.path.exists(repre_review_path): + if repre_review_path and os.path.exists(repre_review_path): review_path = repre_review_path if "burnin" in tags: # burnin has precedence if exists break From d74e17a0e112c48714a87afeddf7750494fb6fca Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 21 Feb 2023 17:40:29 +0000 Subject: [PATCH 291/483] Implement get_multipart --- openpype/hosts/maya/api/lib_renderproducts.py | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 60090e9f6d..e635414029 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -196,12 +196,18 @@ class ARenderProducts: """Constructor.""" self.layer = layer self.render_instance = render_instance - self.multipart = False + self.multipart = self.get_multipart() # Initialize self.layer_data = self._get_layer_data() self.layer_data.products = self.get_render_products() + def get_multipart(self): + raise NotImplementedError( + "The render product implementation does not have a " + "\"get_multipart\" method." + ) + def has_camera_token(self): # type: () -> bool """Check if camera token is in image prefix. @@ -344,7 +350,6 @@ class ARenderProducts: separator = file_prefix[matches[0].end(1):matches[1].start(1)] return separator - def _get_layer_data(self): # type: () -> LayerMetadata # ______________________________________________ @@ -531,16 +536,20 @@ class RenderProductsArnold(ARenderProducts): return prefix - def _get_aov_render_products(self, aov, cameras=None): - """Return all render products for the AOV""" - - products = [] - aov_name = self._get_attr(aov, "name") + def get_multipart(self): multipart = False multilayer = bool(self._get_attr("defaultArnoldDriver.multipart")) merge_AOVs = bool(self._get_attr("defaultArnoldDriver.mergeAOVs")) if multilayer or merge_AOVs: multipart = True + + return multipart + + def _get_aov_render_products(self, aov, cameras=None): + """Return all render products for the AOV""" + + products = [] + aov_name = self._get_attr(aov, "name") ai_drivers = cmds.listConnections("{}.outputs".format(aov), source=True, destination=False, @@ -594,7 +603,7 @@ class RenderProductsArnold(ARenderProducts): ext=ext, aov=aov_name, driver=ai_driver, - multipart=multipart, + multipart=self.multipart, camera=camera) products.append(product) @@ -731,6 +740,14 @@ class RenderProductsVray(ARenderProducts): renderer = "vray" + def get_multipart(self): + multipart = False + image_format = self._get_attr("vraySettings.imageFormatStr") + if image_format == "exr (multichannel)": + multipart = True + + return multipart + def get_renderer_prefix(self): # type: () -> str """Get image prefix for V-Ray. @@ -797,11 +814,6 @@ class RenderProductsVray(ARenderProducts): if default_ext in {"exr (multichannel)", "exr (deep)"}: default_ext = "exr" - # Define multipart. - multipart = False - if image_format_str == "exr (multichannel)": - multipart = True - products = [] # add beauty as default when not disabled @@ -813,7 +825,7 @@ class RenderProductsVray(ARenderProducts): productName="", ext=default_ext, camera=camera, - multipart=multipart + multipart=self.multipart ) ) @@ -826,10 +838,10 @@ class RenderProductsVray(ARenderProducts): productName="Alpha", ext=default_ext, camera=camera, - multipart=multipart + multipart=self.multipart ) ) - if multipart: + if self.multipart: # AOVs are merged in m-channel file, only main layer is rendered return products @@ -989,6 +1001,19 @@ class RenderProductsRedshift(ARenderProducts): renderer = "redshift" unmerged_aovs = {"Cryptomatte"} + def get_multipart(self): + # For Redshift we don't directly return upon forcing multilayer + # due to some AOVs still being written into separate files, + # like Cryptomatte. + # AOVs are merged in multi-channel file + multipart = False + force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) # noqa + exMultipart = bool(self._get_attr("redshiftOptions.exrMultipart")) + if exMultipart or force_layer: + multipart = True + + return multipart + def get_renderer_prefix(self): """Get image prefix for Redshift. @@ -1028,16 +1053,6 @@ class RenderProductsRedshift(ARenderProducts): for c in self.get_renderable_cameras() ] - # For Redshift we don't directly return upon forcing multilayer - # due to some AOVs still being written into separate files, - # like Cryptomatte. - # AOVs are merged in multi-channel file - multipart = False - force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) # noqa - exMultipart = bool(self._get_attr("redshiftOptions.exrMultipart")) - if exMultipart or force_layer: - multipart = True - # Get Redshift Extension from image format image_format = self._get_attr("redshiftOptions.imageFormat") # integer ext = mel.eval("redshiftGetImageExtension(%i)" % image_format) @@ -1059,7 +1074,7 @@ class RenderProductsRedshift(ARenderProducts): continue aov_type = self._get_attr(aov, "aovType") - if multipart and aov_type not in self.unmerged_aovs: + if self.multipart and aov_type not in self.unmerged_aovs: continue # Any AOVs that still get processed, like Cryptomatte @@ -1094,7 +1109,7 @@ class RenderProductsRedshift(ARenderProducts): productName=aov_light_group_name, aov=aov_name, ext=ext, - multipart=multipart, + multipart=self.multipart, camera=camera) products.append(product) @@ -1108,7 +1123,7 @@ class RenderProductsRedshift(ARenderProducts): product = RenderProduct(productName=aov_name, aov=aov_name, ext=ext, - multipart=multipart, + multipart=self.multipart, camera=camera) products.append(product) @@ -1124,7 +1139,7 @@ class RenderProductsRedshift(ARenderProducts): products.insert(0, RenderProduct(productName=beauty_name, ext=ext, - multipart=multipart, + multipart=self.multipart, camera=camera)) return products @@ -1144,6 +1159,10 @@ class RenderProductsRenderman(ARenderProducts): renderer = "renderman" unmerged_aovs = {"PxrCryptomatte"} + def get_multipart(self): + # Implemented as display specific in "get_render_products". + return False + def get_render_products(self): """Get all AOVs. @@ -1283,6 +1302,9 @@ class RenderProductsMayaHardware(ARenderProducts): {"label": "EXR(exr)", "index": 40, "extension": "exr"} ] + def get_multipart(self): + return False + def _get_extension(self, value): result = None if isinstance(value, int): From 069e1eed21a360d8a01effdacd3b3684954ea83c Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 22 Feb 2023 18:00:11 +0000 Subject: [PATCH 292/483] Fix Redshift expected files. --- openpype/hosts/maya/api/lib_renderproducts.py | 26 +++++++++++++++---- .../maya/plugins/publish/collect_render.py | 7 ++++- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index e635414029..4e9e13d2a3 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1001,6 +1001,20 @@ class RenderProductsRedshift(ARenderProducts): renderer = "redshift" unmerged_aovs = {"Cryptomatte"} + def get_files(self, product): + # When outputting AOVs we need to replace Redshift specific AOV tokens + # with Maya render tokens for generating file sequences. We validate to + # a specific AOV fileprefix so we only need to accout for one + # replacement. + if not product.multipart and product.driver: + file_prefix = self._get_attr(product.driver + ".filePrefix") + self.layer_data.filePrefix = file_prefix.replace( + "/", + "//" + ) + + return super(RenderProductsRedshift, self).get_files(product) + def get_multipart(self): # For Redshift we don't directly return upon forcing multilayer # due to some AOVs still being written into separate files, @@ -1009,7 +1023,7 @@ class RenderProductsRedshift(ARenderProducts): multipart = False force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) # noqa exMultipart = bool(self._get_attr("redshiftOptions.exrMultipart")) - if exMultipart or force_layer: + if exMultipart and force_layer: multipart = True return multipart @@ -1109,8 +1123,9 @@ class RenderProductsRedshift(ARenderProducts): productName=aov_light_group_name, aov=aov_name, ext=ext, - multipart=self.multipart, - camera=camera) + multipart=False, + camera=camera, + driver=aov) products.append(product) if light_groups: @@ -1123,8 +1138,9 @@ class RenderProductsRedshift(ARenderProducts): product = RenderProduct(productName=aov_name, aov=aov_name, ext=ext, - multipart=self.multipart, - camera=camera) + multipart=False, + camera=camera, + driver=aov) products.append(product) # When a Beauty AOV is added manually, it will be rendered as diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index f2b5262187..338f148f85 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -42,6 +42,7 @@ Provides: import re import os import platform +import json from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup @@ -183,7 +184,11 @@ class CollectMayaRender(pyblish.api.ContextPlugin): self.log.info("multipart: {}".format( multipart)) assert exp_files, "no file names were generated, this is bug" - self.log.info(exp_files) + self.log.info( + "expected files: {}".format( + json.dumps(exp_files, indent=4, sort_keys=True) + ) + ) # if we want to attach render to subset, check if we have AOV's # in expectedFiles. If so, raise error as we cannot attach AOV From 43020a6c9d6a8366048a294969deb5d6e6a999e8 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Feb 2023 09:20:25 +0000 Subject: [PATCH 293/483] Only use force options as multipart identifier. --- openpype/hosts/maya/api/lib_renderproducts.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 4e9e13d2a3..02e55601b9 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1021,9 +1021,10 @@ class RenderProductsRedshift(ARenderProducts): # like Cryptomatte. # AOVs are merged in multi-channel file multipart = False - force_layer = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) # noqa - exMultipart = bool(self._get_attr("redshiftOptions.exrMultipart")) - if exMultipart and force_layer: + force_layer = bool( + self._get_attr("redshiftOptions.exrForceMultilayer") + ) + if force_layer: multipart = True return multipart From 9c688309a9883654c0fb66fab992e6f28323670c Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Thu, 23 Feb 2023 11:53:55 +0000 Subject: [PATCH 294/483] Update openpype/hosts/maya/api/lib_renderproducts.py --- openpype/hosts/maya/api/lib_renderproducts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py index 02e55601b9..463324284b 100644 --- a/openpype/hosts/maya/api/lib_renderproducts.py +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -1320,6 +1320,7 @@ class RenderProductsMayaHardware(ARenderProducts): ] def get_multipart(self): + # MayaHardware does not support multipart EXRs. return False def _get_extension(self, value): From d760cbab77c39f3a4ae9b72924ad222304a8aa65 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 31 Jan 2023 06:55:36 +0000 Subject: [PATCH 295/483] Batch script for running Openpype on Deadline. --- tools/openpype_console.bat | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 tools/openpype_console.bat diff --git a/tools/openpype_console.bat b/tools/openpype_console.bat new file mode 100644 index 0000000000..414b5fdf66 --- /dev/null +++ b/tools/openpype_console.bat @@ -0,0 +1,3 @@ +cd "%~dp0\.." +echo %OPENPYPE_MONGO% +.poetry\bin\poetry.exe run python start.py %* From 5d9fe60013148054b3cbd70d3701febc7f2fe3a4 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Feb 2023 16:25:35 +0000 Subject: [PATCH 296/483] Commenting for documentation --- tools/openpype_console.bat | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tools/openpype_console.bat b/tools/openpype_console.bat index 414b5fdf66..04b28c389f 100644 --- a/tools/openpype_console.bat +++ b/tools/openpype_console.bat @@ -1,3 +1,15 @@ +goto comment +SYNOPSIS + Helper script running scripts through the OpenPype environment. + +DESCRIPTION + This script is usually used as a replacement for building when tested farm integration like Deadline. + +EXAMPLE + +cmd> .\openpype_console.bat path/to/python_script.py +:comment + cd "%~dp0\.." echo %OPENPYPE_MONGO% .poetry\bin\poetry.exe run python start.py %* From 7c73995971a39ef5f77bfe277cd86cddc76b78cd Mon Sep 17 00:00:00 2001 From: Fabia Serra Arrizabalaga Date: Thu, 23 Feb 2023 00:52:40 +0100 Subject: [PATCH 297/483] Move get_workfile_build_placeholder_plugins to NukeHost class as workfile template builder expects --- openpype/hosts/nuke/api/__init__.py | 3 --- openpype/hosts/nuke/api/pipeline.py | 13 ++++++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index 3b00ca9f6f..1af5ff365d 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -30,7 +30,6 @@ from .pipeline import ( parse_container, update_container, - get_workfile_build_placeholder_plugins, ) from .lib import ( INSTANCE_DATA_KNOB, @@ -79,8 +78,6 @@ __all__ = ( "parse_container", "update_container", - "get_workfile_build_placeholder_plugins", - "INSTANCE_DATA_KNOB", "ROOT_DATA_KNOB", "maintained_selection", diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 6dec60d81a..d5289010cb 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -101,6 +101,12 @@ class NukeHost( def get_workfile_extensions(self): return file_extensions() + def get_workfile_build_placeholder_plugins(self): + return [ + NukePlaceholderLoadPlugin, + NukePlaceholderCreatePlugin + ] + def get_containers(self): return ls() @@ -200,13 +206,6 @@ def _show_workfiles(): host_tools.show_workfiles(parent=None, on_top=False) -def get_workfile_build_placeholder_plugins(): - return [ - NukePlaceholderLoadPlugin, - NukePlaceholderCreatePlugin - ] - - def _install_menu(): # uninstall original avalon menu main_window = get_main_window() From 2921579164112453343a580f8f358c124eb9c1d7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 11:57:51 +0100 Subject: [PATCH 298/483] OP-4643 - added Settings for ExtractColorTranscode --- .../defaults/project_settings/global.json | 4 + .../schemas/schema_global_publish.json | 73 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index cedc2d6876..8485bec67b 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -68,6 +68,10 @@ "output": [] } }, + "ExtractColorTranscode": { + "enabled": true, + "profiles": [] + }, "ExtractReview": { "enabled": true, "profiles": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5388d04bc9..46ae6ba554 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -197,6 +197,79 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractColorTranscode", + "label": "ExtractColorTranscode", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "hosts", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "key": "ext", + "label": "Output extension", + "type": "text" + }, + { + "key": "output_colorspace", + "label": "Output colorspace", + "type": "text" + }, + { + "key": "custom_tags", + "label": "Custom Tags", + "type": "list", + "object_type": "text" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From 8a67065fce1bd39de5870bb768365dffafdda8b2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 11:58:51 +0100 Subject: [PATCH 299/483] OP-4643 - added ExtractColorTranscode Added method to convert from one colorspace to another to transcoding lib --- openpype/lib/transcoding.py | 53 ++++++++ .../publish/extract_color_transcode.py | 124 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 openpype/plugins/publish/extract_color_transcode.py diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 039255d937..2fc662f2a4 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1045,3 +1045,56 @@ def convert_ffprobe_fps_to_float(value): if divisor == 0.0: return 0.0 return dividend / divisor + + +def convert_colorspace_for_input_paths( + input_paths, + output_dir, + source_color_space, + target_color_space, + logger=None +): + """Convert source files from one color space to another. + + Filenames of input files are kept so make sure that output directory + is not the same directory as input files have. + - This way it can handle gaps and can keep input filenames without handling + frame template + + Args: + input_paths (str): Paths that should be converted. It is expected that + contains single file or image sequence of samy type. + output_dir (str): Path to directory where output will be rendered. + Must not be same as input's directory. + source_color_space (str): ocio valid color space of source files + target_color_space (str): ocio valid target color space + logger (logging.Logger): Logger used for logging. + + """ + if logger is None: + logger = logging.getLogger(__name__) + + input_arg = "-i" + oiio_cmd = [ + get_oiio_tools_path(), + + # Don't add any additional attributes + "--nosoftwareattrib", + "--colorconvert", source_color_space, target_color_space + ] + for input_path in input_paths: + # Prepare subprocess arguments + + oiio_cmd.extend([ + input_arg, input_path, + ]) + + # Add last argument - path to output + base_filename = os.path.basename(input_path) + output_path = os.path.join(output_dir, base_filename) + oiio_cmd.extend([ + "-o", output_path + ]) + + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=logger) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py new file mode 100644 index 0000000000..58508ab18f --- /dev/null +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -0,0 +1,124 @@ +import pyblish.api + +from openpype.pipeline import publish +from openpype.lib import ( + + is_oiio_supported, +) + +from openpype.lib.transcoding import ( + convert_colorspace_for_input_paths, + get_transcode_temp_directory, +) + +from openpype.lib.profiles_filtering import filter_profiles + + +class ExtractColorTranscode(publish.Extractor): + """ + Extractor to convert colors from one colorspace to different. + """ + + label = "Transcode color spaces" + order = pyblish.api.ExtractorOrder + 0.01 + + optional = True + + # Configurable by Settings + profiles = None + options = None + + def process(self, instance): + if not self.profiles: + self.log.warning("No profiles present for create burnin") + return + + if "representations" not in instance.data: + self.log.warning("No representations, skipping.") + return + + if not is_oiio_supported(): + self.log.warning("OIIO not supported, no transcoding possible.") + return + + colorspace_data = instance.data.get("colorspaceData") + if not colorspace_data: + # TODO get_colorspace ?? + self.log.warning("Instance has not colorspace data, skipping") + return + source_color_space = colorspace_data["colorspace"] + + host_name = instance.context.data["hostName"] + family = instance.data["family"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + subset = instance.data["subset"] + + filtering_criteria = { + "hosts": host_name, + "families": family, + "task_names": task_name, + "task_types": task_type, + "subset": subset + } + profile = filter_profiles(self.profiles, filtering_criteria, + logger=self.log) + + if not profile: + self.log.info(( + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) + return + + self.log.debug("profile: {}".format(profile)) + + target_colorspace = profile["output_colorspace"] + if not target_colorspace: + raise RuntimeError("Target colorspace must be set") + + repres = instance.data.get("representations") or [] + for idx, repre in enumerate(repres): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self.repre_is_valid(repre): + continue + + new_staging_dir = get_transcode_temp_directory() + repre["stagingDir"] = new_staging_dir + files_to_remove = repre["files"] + if not isinstance(files_to_remove, list): + files_to_remove = [files_to_remove] + instance.context.data["cleanupFullPaths"].extend(files_to_remove) + + convert_colorspace_for_input_paths( + repre["files"], + new_staging_dir, + source_color_space, + target_colorspace, + self.log + ) + + def repre_is_valid(self, repre): + """Validation if representation should be processed. + + Args: + repre (dict): Representation which should be checked. + + Returns: + bool: False if can't be processed else True. + """ + + if "review" not in (repre.get("tags") or []): + self.log.info(( + "Representation \"{}\" don't have \"review\" tag. Skipped." + ).format(repre["name"])) + return False + + if not repre.get("files"): + self.log.warning(( + "Representation \"{}\" have empty files. Skipped." + ).format(repre["name"])) + return False + return True From 76c00f9fa8888e991c61074ed894b09c211b55f0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 12:05:57 +0100 Subject: [PATCH 300/483] OP-4643 - extractor must run just before ExtractReview Nuke render local is set to 0.01 --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 58508ab18f..5163cd4045 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -20,7 +20,7 @@ class ExtractColorTranscode(publish.Extractor): """ label = "Transcode color spaces" - order = pyblish.api.ExtractorOrder + 0.01 + order = pyblish.api.ExtractorOrder + 0.019 optional = True From 455eb379c26ac8494d1aff4fb257cd01e80dbdd5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:03:22 +0100 Subject: [PATCH 301/483] OP-4643 - fix for full file paths --- .../publish/extract_color_transcode.py | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 5163cd4045..6ad7599f2c 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,3 +1,4 @@ +import os import pyblish.api from openpype.pipeline import publish @@ -41,13 +42,6 @@ class ExtractColorTranscode(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return - colorspace_data = instance.data.get("colorspaceData") - if not colorspace_data: - # TODO get_colorspace ?? - self.log.warning("Instance has not colorspace data, skipping") - return - source_color_space = colorspace_data["colorspace"] - host_name = instance.context.data["hostName"] family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) @@ -82,18 +76,32 @@ class ExtractColorTranscode(publish.Extractor): repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - if not self.repre_is_valid(repre): + # if not self.repre_is_valid(repre): + # continue + + colorspace_data = repre.get("colorspaceData") + if not colorspace_data: + # TODO get_colorspace ?? + self.log.warning("Repre has not colorspace data, skipping") + continue + source_color_space = colorspace_data["colorspace"] + config_path = colorspace_data.get("configData", {}).get("path") + if not os.path.exists(config_path): + self.log.warning("Config file doesn't exist, skipping") continue new_staging_dir = get_transcode_temp_directory() + original_staging_dir = repre["stagingDir"] repre["stagingDir"] = new_staging_dir - files_to_remove = repre["files"] - if not isinstance(files_to_remove, list): - files_to_remove = [files_to_remove] - instance.context.data["cleanupFullPaths"].extend(files_to_remove) + files_to_convert = repre["files"] + if not isinstance(files_to_convert, list): + files_to_convert = [files_to_convert] + files_to_convert = [os.path.join(original_staging_dir, path) + for path in files_to_convert] + instance.context.data["cleanupFullPaths"].extend(files_to_convert) convert_colorspace_for_input_paths( - repre["files"], + files_to_convert, new_staging_dir, source_color_space, target_colorspace, From 52a5865341039d03da72dee00d4eb996334a4dbd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:04:06 +0100 Subject: [PATCH 302/483] OP-4643 - pass path for ocio config --- openpype/lib/transcoding.py | 3 +++ openpype/plugins/publish/extract_color_transcode.py | 1 + 2 files changed, 4 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 2fc662f2a4..ab86e44304 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1050,6 +1050,7 @@ def convert_ffprobe_fps_to_float(value): def convert_colorspace_for_input_paths( input_paths, output_dir, + config_path, source_color_space, target_color_space, logger=None @@ -1066,6 +1067,7 @@ def convert_colorspace_for_input_paths( contains single file or image sequence of samy type. output_dir (str): Path to directory where output will be rendered. Must not be same as input's directory. + config_path (str): path to OCIO config file source_color_space (str): ocio valid color space of source files target_color_space (str): ocio valid target color space logger (logging.Logger): Logger used for logging. @@ -1080,6 +1082,7 @@ def convert_colorspace_for_input_paths( # Don't add any additional attributes "--nosoftwareattrib", + "--colorconfig", config_path, "--colorconvert", source_color_space, target_color_space ] for input_path in input_paths: diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 6ad7599f2c..fdb13a47e8 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -103,6 +103,7 @@ class ExtractColorTranscode(publish.Extractor): convert_colorspace_for_input_paths( files_to_convert, new_staging_dir, + config_path, source_color_space, target_colorspace, self.log From 0f1dcb64eb5c98ca1d78b48b4e9e4a9dcfc86ffa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:15:33 +0100 Subject: [PATCH 303/483] OP-4643 - add custom_tags --- openpype/plugins/publish/extract_color_transcode.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index fdb13a47e8..ab932b2476 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -72,6 +72,7 @@ class ExtractColorTranscode(publish.Extractor): target_colorspace = profile["output_colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") + custom_tags = profile["custom_tags"] repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): @@ -109,6 +110,11 @@ class ExtractColorTranscode(publish.Extractor): self.log ) + if custom_tags: + if not repre.get("custom_tags"): + repre["custom_tags"] = [] + repre["custom_tags"].extend(custom_tags) + def repre_is_valid(self, repre): """Validation if representation should be processed. From 4d29e43a41a49dd805390fd03d3d7602d15ad38f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:18:38 +0100 Subject: [PATCH 304/483] OP-4643 - added docstring --- openpype/plugins/publish/extract_color_transcode.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index ab932b2476..88e2eed90f 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -18,6 +18,17 @@ from openpype.lib.profiles_filtering import filter_profiles class ExtractColorTranscode(publish.Extractor): """ Extractor to convert colors from one colorspace to different. + + Expects "colorspaceData" on representation. This dictionary is collected + previously and denotes that representation files should be converted. + This dict contains source colorspace information, collected by hosts. + + Target colorspace is selected by profiles in the Settings, based on: + - families + - host + - task types + - task names + - subset names """ label = "Transcode color spaces" From 454f65dc50253afdb7c74c7c8b851fcbe54c372d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:15:44 +0100 Subject: [PATCH 305/483] OP-4643 - updated Settings schema --- .../schemas/schema_global_publish.json | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 46ae6ba554..c2c911d7d6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -246,24 +246,44 @@ "type": "list", "object_type": "text" }, + { + "type": "boolean", + "key": "delete_original", + "label": "Delete Original Representation" + }, { "type": "splitter" }, { - "key": "ext", - "label": "Output extension", - "type": "text" - }, - { - "key": "output_colorspace", - "label": "Output colorspace", - "type": "text" - }, - { - "key": "custom_tags", - "label": "Custom Tags", - "type": "list", - "object_type": "text" + "key": "outputs", + "label": "Output Definitions", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "output_extension", + "label": "Output extension", + "type": "text" + }, + { + "key": "output_colorspace", + "label": "Output colorspace", + "type": "text" + }, + { + "type": "schema", + "name": "schema_representation_tags" + }, + { + "key": "custom_tags", + "label": "Custom Tags", + "type": "list", + "object_type": "text" + } + ] + } } ] } From a197c6820209db57dc3182ca392e51239be57bb5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:17:25 +0100 Subject: [PATCH 306/483] OP-4643 - skip video files Only frames currently supported. --- .../plugins/publish/extract_color_transcode.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 88e2eed90f..a0714c9a33 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -36,6 +36,9 @@ class ExtractColorTranscode(publish.Extractor): optional = True + # Supported extensions + supported_exts = ["exr", "jpg", "jpeg", "png", "dpx"] + # Configurable by Settings profiles = None options = None @@ -88,13 +91,7 @@ class ExtractColorTranscode(publish.Extractor): repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - # if not self.repre_is_valid(repre): - # continue - - colorspace_data = repre.get("colorspaceData") - if not colorspace_data: - # TODO get_colorspace ?? - self.log.warning("Repre has not colorspace data, skipping") + if not self._repre_is_valid(repre): continue source_color_space = colorspace_data["colorspace"] config_path = colorspace_data.get("configData", {}).get("path") @@ -136,9 +133,9 @@ class ExtractColorTranscode(publish.Extractor): bool: False if can't be processed else True. """ - if "review" not in (repre.get("tags") or []): - self.log.info(( - "Representation \"{}\" don't have \"review\" tag. Skipped." + if repre.get("ext") not in self.supported_exts: + self.log.warning(( + "Representation \"{}\" of unsupported extension. Skipped." ).format(repre["name"])) return False From bd1a9c7342098e05e095f422200090ce918a56b5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:19:08 +0100 Subject: [PATCH 307/483] OP-4643 - refactored profile, delete of original Implemented multiple outputs from single input representation --- .../publish/extract_color_transcode.py | 156 ++++++++++++------ 1 file changed, 109 insertions(+), 47 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index a0714c9a33..b0c851d5f4 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,4 +1,6 @@ import os +import copy + import pyblish.api from openpype.pipeline import publish @@ -56,13 +58,94 @@ class ExtractColorTranscode(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return + profile = self._get_profile(instance) + if not profile: + return + + repres = instance.data.get("representations") or [] + for idx, repre in enumerate(list(repres)): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self._repre_is_valid(repre): + continue + + colorspace_data = repre["colorspaceData"] + source_color_space = colorspace_data["colorspace"] + config_path = colorspace_data.get("configData", {}).get("path") + if not os.path.exists(config_path): + self.log.warning("Config file doesn't exist, skipping") + continue + + repre = self._handle_original_repre(repre, profile) + + for _, output_def in profile.get("outputs", {}).items(): + new_repre = copy.deepcopy(repre) + + new_staging_dir = get_transcode_temp_directory() + original_staging_dir = new_repre["stagingDir"] + new_repre["stagingDir"] = new_staging_dir + files_to_convert = new_repre["files"] + if not isinstance(files_to_convert, list): + files_to_convert = [files_to_convert] + + files_to_delete = copy.deepcopy(files_to_convert) + + output_extension = output_def["output_extension"] + files_to_convert = self._rename_output_files(files_to_convert, + output_extension) + + files_to_convert = [os.path.join(original_staging_dir, path) + for path in files_to_convert] + + target_colorspace = output_def["output_colorspace"] + if not target_colorspace: + raise RuntimeError("Target colorspace must be set") + + convert_colorspace_for_input_paths( + files_to_convert, + new_staging_dir, + config_path, + source_color_space, + target_colorspace, + self.log + ) + + instance.context.data["cleanupFullPaths"].extend( + files_to_delete) + + custom_tags = output_def.get("custom_tags") + if custom_tags: + if not new_repre.get("custom_tags"): + new_repre["custom_tags"] = [] + new_repre["custom_tags"].extend(custom_tags) + + # Add additional tags from output definition to representation + for tag in output_def["tags"]: + if tag not in new_repre["tags"]: + new_repre["tags"].append(tag) + + instance.data["representations"].append(new_repre) + + def _rename_output_files(self, files_to_convert, output_extension): + """Change extension of converted files.""" + if output_extension: + output_extension = output_extension.replace('.', '') + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + new_file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(new_file_name) + files_to_convert = renamed_files + return files_to_convert + + def _get_profile(self, instance): + """Returns profile if and how repre should be color transcoded.""" host_name = instance.context.data["hostName"] family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) task_name = task_data.get("name") task_type = task_data.get("type") subset = instance.data["subset"] - filtering_criteria = { "hosts": host_name, "families": family, @@ -75,55 +158,15 @@ class ExtractColorTranscode(publish.Extractor): if not profile: self.log.info(( - "Skipped instance. None of profiles in presets are for" - " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" - " | Task type \"{}\" | Subset \"{}\" " - ).format(host_name, family, task_name, task_type, subset)) - return + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) self.log.debug("profile: {}".format(profile)) + return profile - target_colorspace = profile["output_colorspace"] - if not target_colorspace: - raise RuntimeError("Target colorspace must be set") - custom_tags = profile["custom_tags"] - - repres = instance.data.get("representations") or [] - for idx, repre in enumerate(repres): - self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - if not self._repre_is_valid(repre): - continue - source_color_space = colorspace_data["colorspace"] - config_path = colorspace_data.get("configData", {}).get("path") - if not os.path.exists(config_path): - self.log.warning("Config file doesn't exist, skipping") - continue - - new_staging_dir = get_transcode_temp_directory() - original_staging_dir = repre["stagingDir"] - repre["stagingDir"] = new_staging_dir - files_to_convert = repre["files"] - if not isinstance(files_to_convert, list): - files_to_convert = [files_to_convert] - files_to_convert = [os.path.join(original_staging_dir, path) - for path in files_to_convert] - instance.context.data["cleanupFullPaths"].extend(files_to_convert) - - convert_colorspace_for_input_paths( - files_to_convert, - new_staging_dir, - config_path, - source_color_space, - target_colorspace, - self.log - ) - - if custom_tags: - if not repre.get("custom_tags"): - repre["custom_tags"] = [] - repre["custom_tags"].extend(custom_tags) - - def repre_is_valid(self, repre): + def _repre_is_valid(self, repre): """Validation if representation should be processed. Args: @@ -144,4 +187,23 @@ class ExtractColorTranscode(publish.Extractor): "Representation \"{}\" have empty files. Skipped." ).format(repre["name"])) return False + + if not repre.get("colorspaceData"): + self.log.warning("Repre has not colorspace data, skipping") + return False + return True + + def _handle_original_repre(self, repre, profile): + delete_original = profile["delete_original"] + + if delete_original: + if not repre.get("tags"): + repre["tags"] = [] + + if "review" in repre["tags"]: + repre["tags"].remove("review") + if "delete" not in repre["tags"]: + repre["tags"].append("delete") + + return repre From bb85bc330a514f088fd2fad6037f60976e0e993b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:23:01 +0100 Subject: [PATCH 308/483] OP-4643 - switched logging levels Do not use warning unnecessary. --- openpype/plugins/publish/extract_color_transcode.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index b0c851d5f4..4d38514b8b 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -47,11 +47,11 @@ class ExtractColorTranscode(publish.Extractor): def process(self, instance): if not self.profiles: - self.log.warning("No profiles present for create burnin") + self.log.debug("No profiles present for color transcode") return if "representations" not in instance.data: - self.log.warning("No representations, skipping.") + self.log.debug("No representations, skipping.") return if not is_oiio_supported(): @@ -177,19 +177,19 @@ class ExtractColorTranscode(publish.Extractor): """ if repre.get("ext") not in self.supported_exts: - self.log.warning(( + self.log.debug(( "Representation \"{}\" of unsupported extension. Skipped." ).format(repre["name"])) return False if not repre.get("files"): - self.log.warning(( + self.log.debug(( "Representation \"{}\" have empty files. Skipped." ).format(repre["name"])) return False if not repre.get("colorspaceData"): - self.log.warning("Repre has not colorspace data, skipping") + self.log.debug("Repre has no colorspace data. Skipped.") return False return True From 1fd17b9f597dfef41852572deae8edbec65c3280 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:46:14 +0100 Subject: [PATCH 309/483] OP-4643 - propagate new extension to representation --- .../publish/extract_color_transcode.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4d38514b8b..62cf8f0dee 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -90,8 +90,13 @@ class ExtractColorTranscode(publish.Extractor): files_to_delete = copy.deepcopy(files_to_convert) output_extension = output_def["output_extension"] - files_to_convert = self._rename_output_files(files_to_convert, - output_extension) + output_extension = output_extension.replace('.', '') + if output_extension: + new_repre["name"] = output_extension + new_repre["ext"] = output_extension + + files_to_convert = self._rename_output_files( + files_to_convert, output_extension) files_to_convert = [os.path.join(original_staging_dir, path) for path in files_to_convert] @@ -127,15 +132,13 @@ class ExtractColorTranscode(publish.Extractor): def _rename_output_files(self, files_to_convert, output_extension): """Change extension of converted files.""" - if output_extension: - output_extension = output_extension.replace('.', '') - renamed_files = [] - for file_name in files_to_convert: - file_name, _ = os.path.splitext(file_name) - new_file_name = '{}.{}'.format(file_name, - output_extension) - renamed_files.append(new_file_name) - files_to_convert = renamed_files + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + new_file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(new_file_name) + files_to_convert = renamed_files return files_to_convert def _get_profile(self, instance): From ffec1179ad1c9c07fae71db44ff2dc96b6fd7ec2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:46:35 +0100 Subject: [PATCH 310/483] OP-4643 - added label to Settings --- .../projects_schema/schemas/schema_global_publish.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index c2c911d7d6..7155510fef 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -201,10 +201,14 @@ "type": "dict", "collapsible": true, "key": "ExtractColorTranscode", - "label": "ExtractColorTranscode", + "label": "ExtractColorTranscode (ImageIO)", "checkbox_key": "enabled", "is_group": true, "children": [ + { + "type": "label", + "label": "Configure output format(s) and color spaces for matching representations. Empty 'Output extension' denotes keeping source extension." + }, { "type": "boolean", "key": "enabled", From 5a562dc821b2cbefc780509b44c084bba786e80c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jan 2023 18:22:08 +0100 Subject: [PATCH 311/483] OP-4643 - refactored according to review Function turned into single filepath input. --- openpype/lib/transcoding.py | 43 ++++++----- .../publish/extract_color_transcode.py | 72 ++++++++++--------- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index ab86e44304..e1bd22d109 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1047,12 +1047,12 @@ def convert_ffprobe_fps_to_float(value): return dividend / divisor -def convert_colorspace_for_input_paths( - input_paths, - output_dir, +def convert_colorspace( + input_path, + out_filepath, config_path, - source_color_space, - target_color_space, + source_colorspace, + target_colorspace, logger=None ): """Convert source files from one color space to another. @@ -1063,13 +1063,13 @@ def convert_colorspace_for_input_paths( frame template Args: - input_paths (str): Paths that should be converted. It is expected that + input_path (str): Paths that should be converted. It is expected that contains single file or image sequence of samy type. - output_dir (str): Path to directory where output will be rendered. + out_filepath (str): Path to directory where output will be rendered. Must not be same as input's directory. config_path (str): path to OCIO config file - source_color_space (str): ocio valid color space of source files - target_color_space (str): ocio valid target color space + source_colorspace (str): ocio valid color space of source files + target_colorspace (str): ocio valid target color space logger (logging.Logger): Logger used for logging. """ @@ -1083,21 +1083,18 @@ def convert_colorspace_for_input_paths( # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "--colorconvert", source_color_space, target_color_space + "--colorconvert", source_colorspace, target_colorspace ] - for input_path in input_paths: - # Prepare subprocess arguments + # Prepare subprocess arguments - oiio_cmd.extend([ - input_arg, input_path, - ]) + oiio_cmd.extend([ + input_arg, input_path, + ]) - # Add last argument - path to output - base_filename = os.path.basename(input_path) - output_path = os.path.join(output_dir, base_filename) - oiio_cmd.extend([ - "-o", output_path - ]) + # Add last argument - path to output + oiio_cmd.extend([ + "-o", out_filepath + ]) - logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) - run_subprocess(oiio_cmd, logger=logger) + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=logger) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 62cf8f0dee..3a05426432 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -10,7 +10,7 @@ from openpype.lib import ( ) from openpype.lib.transcoding import ( - convert_colorspace_for_input_paths, + convert_colorspace, get_transcode_temp_directory, ) @@ -69,7 +69,7 @@ class ExtractColorTranscode(publish.Extractor): continue colorspace_data = repre["colorspaceData"] - source_color_space = colorspace_data["colorspace"] + source_colorspace = colorspace_data["colorspace"] config_path = colorspace_data.get("configData", {}).get("path") if not os.path.exists(config_path): self.log.warning("Config file doesn't exist, skipping") @@ -80,8 +80,8 @@ class ExtractColorTranscode(publish.Extractor): for _, output_def in profile.get("outputs", {}).items(): new_repre = copy.deepcopy(repre) - new_staging_dir = get_transcode_temp_directory() original_staging_dir = new_repre["stagingDir"] + new_staging_dir = get_transcode_temp_directory() new_repre["stagingDir"] = new_staging_dir files_to_convert = new_repre["files"] if not isinstance(files_to_convert, list): @@ -92,27 +92,28 @@ class ExtractColorTranscode(publish.Extractor): output_extension = output_def["output_extension"] output_extension = output_extension.replace('.', '') if output_extension: - new_repre["name"] = output_extension + if new_repre["name"] == new_repre["ext"]: + new_repre["name"] = output_extension new_repre["ext"] = output_extension - files_to_convert = self._rename_output_files( - files_to_convert, output_extension) - - files_to_convert = [os.path.join(original_staging_dir, path) - for path in files_to_convert] - target_colorspace = output_def["output_colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") - convert_colorspace_for_input_paths( - files_to_convert, - new_staging_dir, - config_path, - source_color_space, - target_colorspace, - self.log - ) + for file_name in files_to_convert: + input_filepath = os.path.join(original_staging_dir, + file_name) + output_path = self._get_output_file_path(input_filepath, + new_staging_dir, + output_extension) + convert_colorspace( + input_filepath, + output_path, + config_path, + source_colorspace, + target_colorspace, + self.log + ) instance.context.data["cleanupFullPaths"].extend( files_to_delete) @@ -130,16 +131,16 @@ class ExtractColorTranscode(publish.Extractor): instance.data["representations"].append(new_repre) - def _rename_output_files(self, files_to_convert, output_extension): - """Change extension of converted files.""" - renamed_files = [] - for file_name in files_to_convert: - file_name, _ = os.path.splitext(file_name) - new_file_name = '{}.{}'.format(file_name, - output_extension) - renamed_files.append(new_file_name) - files_to_convert = renamed_files - return files_to_convert + def _get_output_file_path(self, input_filepath, output_dir, + output_extension): + """Create output file name path.""" + file_name = os.path.basename(input_filepath) + file_name, input_extension = os.path.splitext(file_name) + if not output_extension: + output_extension = input_extension + new_file_name = '{}.{}'.format(file_name, + output_extension) + return os.path.join(output_dir, new_file_name) def _get_profile(self, instance): """Returns profile if and how repre should be color transcoded.""" @@ -161,10 +162,10 @@ class ExtractColorTranscode(publish.Extractor): if not profile: self.log.info(( - "Skipped instance. None of profiles in presets are for" - " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" - " | Task type \"{}\" | Subset \"{}\" " - ).format(host_name, family, task_name, task_type, subset)) + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) self.log.debug("profile: {}".format(profile)) return profile @@ -181,18 +182,19 @@ class ExtractColorTranscode(publish.Extractor): if repre.get("ext") not in self.supported_exts: self.log.debug(( - "Representation \"{}\" of unsupported extension. Skipped." + "Representation '{}' of unsupported extension. Skipped." ).format(repre["name"])) return False if not repre.get("files"): self.log.debug(( - "Representation \"{}\" have empty files. Skipped." + "Representation '{}' have empty files. Skipped." ).format(repre["name"])) return False if not repre.get("colorspaceData"): - self.log.debug("Repre has no colorspace data. Skipped.") + self.log.debug("Representation '{}' has no colorspace data. " + "Skipped.") return False return True From e0a163bc2e836c9292e52757abab43d26bc44c07 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:53:02 +0100 Subject: [PATCH 312/483] OP-4643 - updated schema Co-authored-by: Toke Jepsen --- .../projects_schema/schemas/schema_global_publish.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 7155510fef..80c18ce118 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -267,8 +267,8 @@ "type": "dict", "children": [ { - "key": "output_extension", - "label": "Output extension", + "key": "extension", + "label": "Extension", "type": "text" }, { From 657c3156dfc046405cee116c6ccb141e60901d23 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:54:46 +0100 Subject: [PATCH 313/483] OP-4643 - updated plugin name in schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- .../projects_schema/schemas/schema_global_publish.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 80c18ce118..357cbfb287 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -200,8 +200,8 @@ { "type": "dict", "collapsible": true, - "key": "ExtractColorTranscode", - "label": "ExtractColorTranscode (ImageIO)", + "key": "ExtractOIIOTranscode", + "label": "Extract OIIO Transcode", "checkbox_key": "enabled", "is_group": true, "children": [ From 4b5c14e46686e471f20bef6909d6287b2eae6859 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:55:57 +0100 Subject: [PATCH 314/483] OP-4643 - updated key in schema Co-authored-by: Toke Jepsen --- .../projects_schema/schemas/schema_global_publish.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 357cbfb287..0281b0ded6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -272,8 +272,8 @@ "type": "text" }, { - "key": "output_colorspace", - "label": "Output colorspace", + "key": "colorspace", + "label": "Colorspace", "type": "text" }, { From 756661f71b762fb738bd793984787941f2051e4d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:57:03 +0100 Subject: [PATCH 315/483] OP-4643 - changed oiio_cmd creation Co-authored-by: Toke Jepsen --- openpype/lib/transcoding.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index e1bd22d109..f22628dd28 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1076,25 +1076,15 @@ def convert_colorspace( if logger is None: logger = logging.getLogger(__name__) - input_arg = "-i" oiio_cmd = [ get_oiio_tools_path(), - + input_path, # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "--colorconvert", source_colorspace, target_colorspace - ] - # Prepare subprocess arguments - - oiio_cmd.extend([ - input_arg, input_path, - ]) - - # Add last argument - path to output - oiio_cmd.extend([ + "--colorconvert", source_colorspace, target_colorspace, "-o", out_filepath - ]) + ] logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) From c3bf4734fdabc019c966bae93e131235f2560f34 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 13:44:45 +0100 Subject: [PATCH 316/483] OP-4643 - updated new keys into settings --- .../settings/defaults/project_settings/global.json | 2 +- .../projects_schema/schemas/schema_global_publish.json | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 8485bec67b..a5e2d25a88 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -68,7 +68,7 @@ "output": [] } }, - "ExtractColorTranscode": { + "ExtractOIIOTranscode": { "enabled": true, "profiles": [] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 0281b0ded6..74b81b13af 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -276,6 +276,16 @@ "label": "Colorspace", "type": "text" }, + { + "key": "display", + "label": "Display", + "type": "text" + }, + { + "key": "view", + "label": "View", + "type": "text" + }, { "type": "schema", "name": "schema_representation_tags" From 2b20ede6d86924a19e5bf5ad9697f451a85a757e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 13:45:42 +0100 Subject: [PATCH 317/483] OP-4643 - renanmed plugin, added new keys into outputs --- openpype/plugins/publish/extract_color_transcode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 3a05426432..cc63b35988 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -17,7 +17,7 @@ from openpype.lib.transcoding import ( from openpype.lib.profiles_filtering import filter_profiles -class ExtractColorTranscode(publish.Extractor): +class ExtractOIIOTranscode(publish.Extractor): """ Extractor to convert colors from one colorspace to different. @@ -89,14 +89,14 @@ class ExtractColorTranscode(publish.Extractor): files_to_delete = copy.deepcopy(files_to_convert) - output_extension = output_def["output_extension"] + output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') if output_extension: if new_repre["name"] == new_repre["ext"]: new_repre["name"] = output_extension new_repre["ext"] = output_extension - target_colorspace = output_def["output_colorspace"] + target_colorspace = output_def["colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") From 8b47a44d04aace508cc05608a18c4f7cce23cb9b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:03:13 +0100 Subject: [PATCH 318/483] OP-4643 - fixed config path key --- openpype/plugins/publish/extract_color_transcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index cc63b35988..245faeb306 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -70,8 +70,8 @@ class ExtractOIIOTranscode(publish.Extractor): colorspace_data = repre["colorspaceData"] source_colorspace = colorspace_data["colorspace"] - config_path = colorspace_data.get("configData", {}).get("path") - if not os.path.exists(config_path): + config_path = colorspace_data.get("config", {}).get("path") + if not config_path or not os.path.exists(config_path): self.log.warning("Config file doesn't exist, skipping") continue From ed7faeef8f0a7678e99f70645d57a72864cc9461 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:03:42 +0100 Subject: [PATCH 319/483] OP-4643 - fixed renaming files --- openpype/plugins/publish/extract_color_transcode.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 245faeb306..c079dcf70e 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -96,6 +96,14 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["name"] = output_extension new_repre["ext"] = output_extension + renamed_files = [] + _, orig_ext = os.path.splitext(files_to_convert[0]) + for file_name in files_to_convert: + file_name = file_name.replace(orig_ext, + "."+output_extension) + renamed_files.append(file_name) + new_repre["files"] = renamed_files + target_colorspace = output_def["colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") From 4b1418a79242eb63cfd4295d70d474b992c276ea Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:04:44 +0100 Subject: [PATCH 320/483] OP-4643 - updated to calculate sequence format --- .../publish/extract_color_transcode.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index c079dcf70e..09c86909cb 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,5 +1,6 @@ import os import copy +import clique import pyblish.api @@ -108,6 +109,8 @@ class ExtractOIIOTranscode(publish.Extractor): if not target_colorspace: raise RuntimeError("Target colorspace must be set") + files_to_convert = self._translate_to_sequence( + files_to_convert) for file_name in files_to_convert: input_filepath = os.path.join(original_staging_dir, file_name) @@ -139,6 +142,40 @@ class ExtractOIIOTranscode(publish.Extractor): instance.data["representations"].append(new_repre) + def _translate_to_sequence(self, files_to_convert): + """Returns original list of files or single sequence format filename. + + Uses clique to find frame sequence, in this case it merges all frames + into sequence format (%0X) and returns it. + If sequence not found, it returns original list + + Args: + files_to_convert (list): list of file names + Returns: + (list) of [file.%04.exr] or [fileA.exr, fileB.exr] + """ + pattern = [clique.PATTERNS["frames"]] + collections, remainder = clique.assemble( + files_to_convert, patterns=pattern, + assume_padded_when_ambiguous=True) + + if collections: + if len(collections) > 1: + raise ValueError( + "Too many collections {}".format(collections)) + + collection = collections[0] + padding = collection.padding + padding_str = "%0{}".format(padding) + frames = list(collection.indexes) + frame_str = "{}-{}#".format(frames[0], frames[-1]) + file_name = "{}{}{}".format(collection.head, frame_str, + collection.tail) + + files_to_convert = [file_name] + + return files_to_convert + def _get_output_file_path(self, input_filepath, output_dir, output_extension): """Create output file name path.""" From 382074b54cc0eb7a7c6d403853f4350d485fa00a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:54:02 +0100 Subject: [PATCH 321/483] OP-4643 - implemented display and viewer color space --- openpype/lib/transcoding.py | 23 +++++++++++++++++-- .../publish/extract_color_transcode.py | 13 +++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index f22628dd28..cc9cd4e1eb 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1053,6 +1053,8 @@ def convert_colorspace( config_path, source_colorspace, target_colorspace, + view, + display, logger=None ): """Convert source files from one color space to another. @@ -1070,8 +1072,11 @@ def convert_colorspace( config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space + view (str): name for viewer space (ocio valid) + display (str): name for display-referred reference space (ocio valid) logger (logging.Logger): Logger used for logging. - + Raises: + ValueError: if misconfigured """ if logger is None: logger = logging.getLogger(__name__) @@ -1082,9 +1087,23 @@ def convert_colorspace( # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "--colorconvert", source_colorspace, target_colorspace, "-o", out_filepath ] + if all([target_colorspace, view, display]): + raise ValueError("Colorspace and both screen and display" + " cannot be set together." + "Choose colorspace or screen and display") + if not target_colorspace and not all([view, display]): + raise ValueError("Both screen and display must be set.") + + if target_colorspace: + oiio_cmd.extend(["--colorconvert", + source_colorspace, + target_colorspace]) + if view and display: + oiio_cmd.extend(["--iscolorspace", source_colorspace]) + oiio_cmd.extend(["--ociodisplay", display, view]) + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 09c86909cb..cd8421c0cd 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -106,8 +106,15 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["files"] = renamed_files target_colorspace = output_def["colorspace"] - if not target_colorspace: - raise RuntimeError("Target colorspace must be set") + view = output_def["view"] or colorspace_data.get("view") + display = (output_def["display"] or + colorspace_data.get("display")) + # both could be already collected by DCC, + # but could be overwritten + if view: + new_repre["colorspaceData"]["view"] = view + if display: + new_repre["colorspaceData"]["view"] = display files_to_convert = self._translate_to_sequence( files_to_convert) @@ -123,6 +130,8 @@ class ExtractOIIOTranscode(publish.Extractor): config_path, source_colorspace, target_colorspace, + view, + display, self.log ) From 8873c9f9056b9b672fe95be0707c9ab8753726b8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 19:03:10 +0100 Subject: [PATCH 322/483] OP-4643 - fix wrong order of deletion of representation --- openpype/plugins/publish/extract_color_transcode.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index cd8421c0cd..9cca5cc969 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -69,6 +69,8 @@ class ExtractOIIOTranscode(publish.Extractor): if not self._repre_is_valid(repre): continue + added_representations = False + colorspace_data = repre["colorspaceData"] source_colorspace = colorspace_data["colorspace"] config_path = colorspace_data.get("config", {}).get("path") @@ -76,8 +78,6 @@ class ExtractOIIOTranscode(publish.Extractor): self.log.warning("Config file doesn't exist, skipping") continue - repre = self._handle_original_repre(repre, profile) - for _, output_def in profile.get("outputs", {}).items(): new_repre = copy.deepcopy(repre) @@ -150,6 +150,10 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["tags"].append(tag) instance.data["representations"].append(new_repre) + added_representations = True + + if added_representations: + self._mark_original_repre_for_deletion(repre, profile) def _translate_to_sequence(self, files_to_convert): """Returns original list of files or single sequence format filename. @@ -253,7 +257,8 @@ class ExtractOIIOTranscode(publish.Extractor): return True - def _handle_original_repre(self, repre, profile): + def _mark_original_repre_for_deletion(self, repre, profile): + """If new transcoded representation created, delete old.""" delete_original = profile["delete_original"] if delete_original: @@ -264,5 +269,3 @@ class ExtractOIIOTranscode(publish.Extractor): repre["tags"].remove("review") if "delete" not in repre["tags"]: repre["tags"].append("delete") - - return repre From 176f53117fe49410135121dc0b858eadb8041270 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 11:26:27 +0100 Subject: [PATCH 323/483] OP-4643 - updated docstring, standardized arguments --- openpype/lib/transcoding.py | 19 +++++++---------- .../publish/extract_color_transcode.py | 21 +++++++++---------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index cc9cd4e1eb..0f6d35affe 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1049,7 +1049,7 @@ def convert_ffprobe_fps_to_float(value): def convert_colorspace( input_path, - out_filepath, + output_path, config_path, source_colorspace, target_colorspace, @@ -1057,18 +1057,13 @@ def convert_colorspace( display, logger=None ): - """Convert source files from one color space to another. - - Filenames of input files are kept so make sure that output directory - is not the same directory as input files have. - - This way it can handle gaps and can keep input filenames without handling - frame template + """Convert source file from one color space to another. Args: - input_path (str): Paths that should be converted. It is expected that - contains single file or image sequence of samy type. - out_filepath (str): Path to directory where output will be rendered. - Must not be same as input's directory. + input_path (str): Path that should be converted. It is expected that + contains single file or image sequence of same type + (sequence in format 'file.FRAMESTART-FRAMEEND#.exr', see oiio docs) + output_path (str): Path to output filename. config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space @@ -1087,7 +1082,7 @@ def convert_colorspace( # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "-o", out_filepath + "-o", output_path ] if all([target_colorspace, view, display]): diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 9cca5cc969..c4cef15ea6 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -119,13 +119,13 @@ class ExtractOIIOTranscode(publish.Extractor): files_to_convert = self._translate_to_sequence( files_to_convert) for file_name in files_to_convert: - input_filepath = os.path.join(original_staging_dir, - file_name) - output_path = self._get_output_file_path(input_filepath, + input_path = os.path.join(original_staging_dir, + file_name) + output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) convert_colorspace( - input_filepath, + input_path, output_path, config_path, source_colorspace, @@ -156,16 +156,17 @@ class ExtractOIIOTranscode(publish.Extractor): self._mark_original_repre_for_deletion(repre, profile) def _translate_to_sequence(self, files_to_convert): - """Returns original list of files or single sequence format filename. + """Returns original list or list with filename formatted in single + sequence format. Uses clique to find frame sequence, in this case it merges all frames - into sequence format (%0X) and returns it. + into sequence format (FRAMESTART-FRAMEEND#) and returns it. If sequence not found, it returns original list Args: files_to_convert (list): list of file names Returns: - (list) of [file.%04.exr] or [fileA.exr, fileB.exr] + (list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr] """ pattern = [clique.PATTERNS["frames"]] collections, remainder = clique.assemble( @@ -178,8 +179,6 @@ class ExtractOIIOTranscode(publish.Extractor): "Too many collections {}".format(collections)) collection = collections[0] - padding = collection.padding - padding_str = "%0{}".format(padding) frames = list(collection.indexes) frame_str = "{}-{}#".format(frames[0], frames[-1]) file_name = "{}{}{}".format(collection.head, frame_str, @@ -189,10 +188,10 @@ class ExtractOIIOTranscode(publish.Extractor): return files_to_convert - def _get_output_file_path(self, input_filepath, output_dir, + def _get_output_file_path(self, input_path, output_dir, output_extension): """Create output file name path.""" - file_name = os.path.basename(input_filepath) + file_name = os.path.basename(input_path) file_name, input_extension = os.path.splitext(file_name) if not output_extension: output_extension = input_extension From 04ae5a28b415197f36d198e409e8343bc4bc6022 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 11:27:06 +0100 Subject: [PATCH 324/483] OP-4643 - fix wrong assignment --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index c4cef15ea6..4e899a519c 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -114,7 +114,7 @@ class ExtractOIIOTranscode(publish.Extractor): if view: new_repre["colorspaceData"]["view"] = view if display: - new_repre["colorspaceData"]["view"] = display + new_repre["colorspaceData"]["display"] = display files_to_convert = self._translate_to_sequence( files_to_convert) From c6571b9dfd520a77877c84a86c60999ab5257a3a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 11:59:13 +0100 Subject: [PATCH 325/483] OP-4643 - fix files to delete --- .../publish/extract_color_transcode.py | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4e899a519c..99e684ba21 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -84,26 +84,18 @@ class ExtractOIIOTranscode(publish.Extractor): original_staging_dir = new_repre["stagingDir"] new_staging_dir = get_transcode_temp_directory() new_repre["stagingDir"] = new_staging_dir - files_to_convert = new_repre["files"] - if not isinstance(files_to_convert, list): - files_to_convert = [files_to_convert] - files_to_delete = copy.deepcopy(files_to_convert) + if isinstance(new_repre["files"], list): + files_to_convert = copy.deepcopy(new_repre["files"]) + else: + files_to_convert = [new_repre["files"]] output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') if output_extension: - if new_repre["name"] == new_repre["ext"]: - new_repre["name"] = output_extension - new_repre["ext"] = output_extension - - renamed_files = [] - _, orig_ext = os.path.splitext(files_to_convert[0]) - for file_name in files_to_convert: - file_name = file_name.replace(orig_ext, - "."+output_extension) - renamed_files.append(file_name) - new_repre["files"] = renamed_files + self._rename_in_representation(new_repre, + files_to_convert, + output_extension) target_colorspace = output_def["colorspace"] view = output_def["view"] or colorspace_data.get("view") @@ -135,8 +127,12 @@ class ExtractOIIOTranscode(publish.Extractor): self.log ) - instance.context.data["cleanupFullPaths"].extend( - files_to_delete) + # cleanup temporary transcoded files + for file_name in new_repre["files"]: + transcoded_file_path = os.path.join(new_staging_dir, + file_name) + instance.context.data["cleanupFullPaths"].append( + transcoded_file_path) custom_tags = output_def.get("custom_tags") if custom_tags: @@ -155,6 +151,21 @@ class ExtractOIIOTranscode(publish.Extractor): if added_representations: self._mark_original_repre_for_deletion(repre, profile) + def _rename_in_representation(self, new_repre, files_to_convert, + output_extension): + """Replace old extension with new one everywhere in representation.""" + if new_repre["name"] == new_repre["ext"]: + new_repre["name"] = output_extension + new_repre["ext"] = output_extension + + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(file_name) + new_repre["files"] = renamed_files + def _translate_to_sequence(self, files_to_convert): """Returns original list or list with filename formatted in single sequence format. From 8598c1ec393a1c8c0f05ad12a2a66bbf7eb89136 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 13:17:59 +0100 Subject: [PATCH 326/483] OP-4643 - moved output argument to the end --- openpype/lib/transcoding.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 0f6d35affe..e74dab4ccc 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1081,8 +1081,7 @@ def convert_colorspace( input_path, # Don't add any additional attributes "--nosoftwareattrib", - "--colorconfig", config_path, - "-o", output_path + "--colorconfig", config_path ] if all([target_colorspace, view, display]): @@ -1100,5 +1099,7 @@ def convert_colorspace( oiio_cmd.extend(["--iscolorspace", source_colorspace]) oiio_cmd.extend(["--ociodisplay", display, view]) + oiio_cmd.extend(["-o", output_path]) + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) From afe0a97bc5ace60a5f7740d22b3d1937e1b69fdf Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 13:18:33 +0100 Subject: [PATCH 327/483] OP-4643 - fix no tags in repre --- openpype/plugins/publish/extract_color_transcode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 99e684ba21..3d897c6d9f 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -142,6 +142,8 @@ class ExtractOIIOTranscode(publish.Extractor): # Add additional tags from output definition to representation for tag in output_def["tags"]: + if not new_repre.get("tags"): + new_repre["tags"] = [] if tag not in new_repre["tags"]: new_repre["tags"].append(tag) From 190a79a836d6e981e57f9f23be58d146193c4d6c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 13:26:07 +0100 Subject: [PATCH 328/483] OP-4643 - changed docstring Elaborated more that 'target_colorspace' and ('view', 'display') are disjunctive. --- openpype/lib/transcoding.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index e74dab4ccc..f7d5e222c8 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1053,8 +1053,8 @@ def convert_colorspace( config_path, source_colorspace, target_colorspace, - view, - display, + view=None, + display=None, logger=None ): """Convert source file from one color space to another. @@ -1067,7 +1067,9 @@ def convert_colorspace( config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space + if filled, 'view' and 'display' must be empty view (str): name for viewer space (ocio valid) + both 'view' and 'display' must be filled (if 'target_colorspace') display (str): name for display-referred reference space (ocio valid) logger (logging.Logger): Logger used for logging. Raises: From 4967d91010dc03df5640364350095f0fae9aa52c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 11:14:07 +0100 Subject: [PATCH 329/483] OP-4663 - fix double dots in extension Co-authored-by: Toke Jepsen --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 3d897c6d9f..bfed69c300 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -207,7 +207,7 @@ class ExtractOIIOTranscode(publish.Extractor): file_name = os.path.basename(input_path) file_name, input_extension = os.path.splitext(file_name) if not output_extension: - output_extension = input_extension + output_extension = input_extension.replace(".", "") new_file_name = '{}.{}'.format(file_name, output_extension) return os.path.join(output_dir, new_file_name) From 43df616692568352b9734715e3a48b71a60e2221 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 16:11:45 +0100 Subject: [PATCH 330/483] OP-4643 - update documentation in Settings schema --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 74b81b13af..3956f403f4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -207,7 +207,7 @@ "children": [ { "type": "label", - "label": "Configure output format(s) and color spaces for matching representations. Empty 'Output extension' denotes keeping source extension." + "label": "Configure Output Definition(s) for new representation(s). \nEmpty 'Extension' denotes keeping source extension. \nName(key) of output definition will be used as new representation name \nunless 'passthrough' value is used to keep existing name. \nFill either 'Colorspace' (for target colorspace) or \nboth 'Display' and 'View' (for display and viewer colorspaces)." }, { "type": "boolean", From 5b72bafcfc9a33302f588e29d314d91b98186f87 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 16:13:59 +0100 Subject: [PATCH 331/483] OP-4643 - name of new representation from output definition key --- .../publish/extract_color_transcode.py | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index bfed69c300..e39ea3add9 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -32,6 +32,25 @@ class ExtractOIIOTranscode(publish.Extractor): - task types - task names - subset names + + Can produce one or more representations (with different extensions) based + on output definition in format: + "output_name: { + "extension": "png", + "colorspace": "ACES - ACEScg", + "display": "", + "view": "", + "tags": [], + "custom_tags": [] + } + + If 'extension' is empty original representation extension is used. + 'output_name' will be used as name of new representation. In case of value + 'passthrough' name of original representation will be used. + + 'colorspace' denotes target colorspace to be transcoded into. Could be + empty if transcoding should be only into display and viewer colorspace. + (In that case both 'display' and 'view' must be filled.) """ label = "Transcode color spaces" @@ -78,7 +97,7 @@ class ExtractOIIOTranscode(publish.Extractor): self.log.warning("Config file doesn't exist, skipping") continue - for _, output_def in profile.get("outputs", {}).items(): + for output_name, output_def in profile.get("outputs", {}).items(): new_repre = copy.deepcopy(repre) original_staging_dir = new_repre["stagingDir"] @@ -92,10 +111,10 @@ class ExtractOIIOTranscode(publish.Extractor): output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') - if output_extension: - self._rename_in_representation(new_repre, - files_to_convert, - output_extension) + self._rename_in_representation(new_repre, + files_to_convert, + output_name, + output_extension) target_colorspace = output_def["colorspace"] view = output_def["view"] or colorspace_data.get("view") @@ -154,10 +173,22 @@ class ExtractOIIOTranscode(publish.Extractor): self._mark_original_repre_for_deletion(repre, profile) def _rename_in_representation(self, new_repre, files_to_convert, - output_extension): - """Replace old extension with new one everywhere in representation.""" - if new_repre["name"] == new_repre["ext"]: - new_repre["name"] = output_extension + output_name, output_extension): + """Replace old extension with new one everywhere in representation. + + Args: + new_repre (dict) + files_to_convert (list): of filenames from repre["files"], + standardized to always list + output_name (str): key of output definition from Settings, + if "" token used, keep original repre name + output_extension (str): extension from output definition + """ + if output_name != "passthrough": + new_repre["name"] = output_name + if not output_extension: + return + new_repre["ext"] = output_extension renamed_files = [] From 7eacd1f30f2ce92adcec6ce8e48fb62bfb92a148 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 16:42:48 +0100 Subject: [PATCH 332/483] OP-4643 - updated docstring for convert_colorspace --- openpype/lib/transcoding.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index f7d5e222c8..b6edd863f8 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1062,8 +1062,11 @@ def convert_colorspace( Args: input_path (str): Path that should be converted. It is expected that contains single file or image sequence of same type - (sequence in format 'file.FRAMESTART-FRAMEEND#.exr', see oiio docs) + (sequence in format 'file.FRAMESTART-FRAMEEND#.ext', see oiio docs, + eg `big.1-3#.tif`) output_path (str): Path to output filename. + (must follow format of 'input_path', eg. single file or + sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`) config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space From 440c4e0c100a413f62aaaf582201384124ba916b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Feb 2023 18:22:10 +0100 Subject: [PATCH 333/483] OP-4643 - remove review from old representation If new representation gets created and adds 'review' tag it becomes new reviewable representation. --- .../publish/extract_color_transcode.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index e39ea3add9..d10b887a0b 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -89,6 +89,7 @@ class ExtractOIIOTranscode(publish.Extractor): continue added_representations = False + added_review = False colorspace_data = repre["colorspaceData"] source_colorspace = colorspace_data["colorspace"] @@ -166,11 +167,15 @@ class ExtractOIIOTranscode(publish.Extractor): if tag not in new_repre["tags"]: new_repre["tags"].append(tag) + if tag == "review": + added_review = True + instance.data["representations"].append(new_repre) added_representations = True if added_representations: - self._mark_original_repre_for_deletion(repre, profile) + self._mark_original_repre_for_deletion(repre, profile, + added_review) def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): @@ -300,15 +305,16 @@ class ExtractOIIOTranscode(publish.Extractor): return True - def _mark_original_repre_for_deletion(self, repre, profile): + def _mark_original_repre_for_deletion(self, repre, profile, added_review): """If new transcoded representation created, delete old.""" + if not repre.get("tags"): + repre["tags"] = [] + delete_original = profile["delete_original"] if delete_original: - if not repre.get("tags"): - repre["tags"] = [] - - if "review" in repre["tags"]: - repre["tags"].remove("review") if "delete" not in repre["tags"]: repre["tags"].append("delete") + + if added_review and "review" in repre["tags"]: + repre["tags"].remove("review") From 212cbe79a2eb05f3b426a01d836d42964838bbbb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Feb 2023 18:23:42 +0100 Subject: [PATCH 334/483] OP-4643 - remove representation that should be deleted Or old revieable representation would be reviewed too. --- openpype/plugins/publish/extract_color_transcode.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index d10b887a0b..93ee1ec44d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -177,6 +177,11 @@ class ExtractOIIOTranscode(publish.Extractor): self._mark_original_repre_for_deletion(repre, profile, added_review) + for repre in tuple(instance.data["representations"]): + tags = repre.get("tags") or [] + if "delete" in tags and "thumbnail" not in tags: + instance.data["representations"].remove(repre) + def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): """Replace old extension with new one everywhere in representation. From 6295d0f34a9efc8e86ebbbf1dbe4b40a391dbf7b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Feb 2023 14:54:25 +0100 Subject: [PATCH 335/483] OP-4643 - fix logging Wrong variable used --- openpype/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index dcb43d7fa2..0f6dacba18 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -169,7 +169,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "Skipped representation. All output definitions from" " selected profile does not match to representation's" " custom tags. \"{}\"" - ).format(str(tags))) + ).format(str(custom_tags))) continue outputs_per_representations.append((repre, outputs)) From 03e8661323622c39d91c647d2f6cdd615a4ca99c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Feb 2023 15:14:14 +0100 Subject: [PATCH 336/483] OP-4643 - allow new repre to stay One might want to delete outputs with 'delete' tag, but repre must stay there at least until extract_review. More universal new tag might be created for this. --- openpype/plugins/publish/extract_color_transcode.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 93ee1ec44d..4a03e623fd 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -161,15 +161,17 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["custom_tags"].extend(custom_tags) # Add additional tags from output definition to representation + if not new_repre.get("tags"): + new_repre["tags"] = [] for tag in output_def["tags"]: - if not new_repre.get("tags"): - new_repre["tags"] = [] if tag not in new_repre["tags"]: new_repre["tags"].append(tag) if tag == "review": added_review = True + new_repre["tags"].append("newly_added") + instance.data["representations"].append(new_repre) added_representations = True @@ -179,6 +181,12 @@ class ExtractOIIOTranscode(publish.Extractor): for repre in tuple(instance.data["representations"]): tags = repre.get("tags") or [] + # TODO implement better way, for now do not delete new repre + # new repre might have 'delete' tag to removed, but it first must + # be there for review to be created + if "newly_added" in tags: + tags.remove("newly_added") + continue if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) From f4140d7664cb3a36c032f4ae4b6d0a240fc01d3a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:04:59 +0100 Subject: [PATCH 337/483] OP-4642 - added additional command arguments to Settings --- .../schemas/schema_global_publish.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 3956f403f4..5333d514b5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -286,6 +286,20 @@ "label": "View", "type": "text" }, + { + "key": "oiiotool_args", + "label": "OIIOtool arguments", + "type": "dict", + "highlight_content": true, + "children": [ + { + "key": "additional_command_args", + "label": "Additional command line arguments", + "type": "list", + "object_type": "text" + } + ] + }, { "type": "schema", "name": "schema_representation_tags" From b1d30058b0cdb26e46618d37807b01d400c89e33 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:08:06 +0100 Subject: [PATCH 338/483] OP-4642 - added additional command arguments for oiiotool Some extension requires special command line arguments (.dpx and binary depth). --- openpype/lib/transcoding.py | 6 ++++++ openpype/plugins/publish/extract_color_transcode.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index b6edd863f8..982cee7a46 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1055,6 +1055,7 @@ def convert_colorspace( target_colorspace, view=None, display=None, + additional_command_args=None, logger=None ): """Convert source file from one color space to another. @@ -1074,6 +1075,8 @@ def convert_colorspace( view (str): name for viewer space (ocio valid) both 'view' and 'display' must be filled (if 'target_colorspace') display (str): name for display-referred reference space (ocio valid) + additional_command_args (list): arguments for oiiotool (like binary + depth for .dpx) logger (logging.Logger): Logger used for logging. Raises: ValueError: if misconfigured @@ -1096,6 +1099,9 @@ def convert_colorspace( if not target_colorspace and not all([view, display]): raise ValueError("Both screen and display must be set.") + if additional_command_args: + oiio_cmd.extend(additional_command_args) + if target_colorspace: oiio_cmd.extend(["--colorconvert", source_colorspace, diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4a03e623fd..3de404125d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -128,6 +128,9 @@ class ExtractOIIOTranscode(publish.Extractor): if display: new_repre["colorspaceData"]["display"] = display + additional_command_args = (output_def["oiiotool_args"] + ["additional_command_args"]) + files_to_convert = self._translate_to_sequence( files_to_convert) for file_name in files_to_convert: @@ -144,6 +147,7 @@ class ExtractOIIOTranscode(publish.Extractor): target_colorspace, view, display, + additional_command_args, self.log ) From e8a79b4f7673a37657b54a62f74c81985a2b5636 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:21:25 +0100 Subject: [PATCH 339/483] OP-4642 - refactored newly added representations --- openpype/plugins/publish/extract_color_transcode.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 3de404125d..8c4ef59de9 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -82,6 +82,7 @@ class ExtractOIIOTranscode(publish.Extractor): if not profile: return + new_representations = [] repres = instance.data.get("representations") or [] for idx, repre in enumerate(list(repres)): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) @@ -174,9 +175,7 @@ class ExtractOIIOTranscode(publish.Extractor): if tag == "review": added_review = True - new_repre["tags"].append("newly_added") - - instance.data["representations"].append(new_repre) + new_representations.append(new_repre) added_representations = True if added_representations: @@ -185,15 +184,11 @@ class ExtractOIIOTranscode(publish.Extractor): for repre in tuple(instance.data["representations"]): tags = repre.get("tags") or [] - # TODO implement better way, for now do not delete new repre - # new repre might have 'delete' tag to removed, but it first must - # be there for review to be created - if "newly_added" in tags: - tags.remove("newly_added") - continue if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) + instance.data["representations"].extend(new_representations) + def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): """Replace old extension with new one everywhere in representation. From e5ec6c4812aa5d5c59a04d834b631afb0917bd2e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:24:53 +0100 Subject: [PATCH 340/483] OP-4642 - refactored query of representations line 73 returns if no representations. --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 8c4ef59de9..de36ea7d5f 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -83,7 +83,7 @@ class ExtractOIIOTranscode(publish.Extractor): return new_representations = [] - repres = instance.data.get("representations") or [] + repres = instance.data["representations"] for idx, repre in enumerate(list(repres)): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) if not self._repre_is_valid(repre): From 6235faab82af4ac7d3a50315b4c1dc9223a73c0a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 10:44:10 +0100 Subject: [PATCH 341/483] OP-4643 - fixed subset filtering Co-authored-by: Toke Jepsen --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index de36ea7d5f..71124b527a 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -273,7 +273,7 @@ class ExtractOIIOTranscode(publish.Extractor): "families": family, "task_names": task_name, "task_types": task_type, - "subset": subset + "subsets": subset } profile = filter_profiles(self.profiles, filtering_criteria, logger=self.log) From b304d63461704f7c2e0709bd5165683a0d890a10 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 12:12:35 +0100 Subject: [PATCH 342/483] OP-4643 - split command line arguments to separate items Reuse existing method from ExtractReview, put it into transcoding.py --- openpype/lib/transcoding.py | 29 +++++++++++++++++++++- openpype/plugins/publish/extract_review.py | 27 +++----------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 982cee7a46..4d2f72fc41 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1100,7 +1100,7 @@ def convert_colorspace( raise ValueError("Both screen and display must be set.") if additional_command_args: - oiio_cmd.extend(additional_command_args) + oiio_cmd.extend(split_cmd_args(additional_command_args)) if target_colorspace: oiio_cmd.extend(["--colorconvert", @@ -1114,3 +1114,30 @@ def convert_colorspace( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) + + +def split_cmd_args(in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + Args: + in_args (list): of arguments ['-n', '-d uint10'] + Returns + (list): ['-n', '-d', 'unint10'] + """ + splitted_args = [] + for arg in in_args: + sub_args = arg.split(" -") + if len(sub_args) == 1: + if arg and arg not in splitted_args: + splitted_args.append(arg) + continue + + for idx, arg in enumerate(sub_args): + if idx != 0: + arg = "-" + arg + + if arg and arg not in splitted_args: + splitted_args.append(arg) + return splitted_args diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 0f6dacba18..e80141fc4a 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -22,6 +22,7 @@ from openpype.lib.transcoding import ( should_convert_for_ffmpeg, convert_input_paths_for_ffmpeg, get_transcode_temp_directory, + split_cmd_args ) @@ -670,7 +671,7 @@ class ExtractReview(pyblish.api.InstancePlugin): res_filters = self.rescaling_filters(temp_data, output_def, new_repre) ffmpeg_video_filters.extend(res_filters) - ffmpeg_input_args = self.split_ffmpeg_args(ffmpeg_input_args) + ffmpeg_input_args = split_cmd_args(ffmpeg_input_args) lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args) ffmpeg_video_filters.extend(lut_filters) @@ -723,28 +724,6 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_output_args ) - def split_ffmpeg_args(self, in_args): - """Makes sure all entered arguments are separated in individual items. - - Split each argument string with " -" to identify if string contains - one or more arguments. - """ - splitted_args = [] - for arg in in_args: - sub_args = arg.split(" -") - if len(sub_args) == 1: - if arg and arg not in splitted_args: - splitted_args.append(arg) - continue - - for idx, arg in enumerate(sub_args): - if idx != 0: - arg = "-" + arg - - if arg and arg not in splitted_args: - splitted_args.append(arg) - return splitted_args - def ffmpeg_full_args( self, input_args, video_filters, audio_filters, output_args ): @@ -764,7 +743,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Returns: list: Containing all arguments ready to run in subprocess. """ - output_args = self.split_ffmpeg_args(output_args) + output_args = split_cmd_args(output_args) video_args_dentifiers = ["-vf", "-filter:v"] audio_args_dentifiers = ["-af", "-filter:a"] From 5d0dc43494452813d19f7ffa511b841e59b64209 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 13:02:41 +0100 Subject: [PATCH 343/483] OP-4643 - refactor - changed existence check --- openpype/plugins/publish/extract_color_transcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 71124b527a..456e40008d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -161,12 +161,12 @@ class ExtractOIIOTranscode(publish.Extractor): custom_tags = output_def.get("custom_tags") if custom_tags: - if not new_repre.get("custom_tags"): + if new_repre.get("custom_tags") is None: new_repre["custom_tags"] = [] new_repre["custom_tags"].extend(custom_tags) # Add additional tags from output definition to representation - if not new_repre.get("tags"): + if new_repre.get("tags") is None: new_repre["tags"] = [] for tag in output_def["tags"]: if tag not in new_repre["tags"]: From 8651a693f990d52e43c14845e82efc3033b5a054 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 13:11:11 +0100 Subject: [PATCH 344/483] Revert "Fix - added missed scopes for Slack bot" This reverts commit 5e0c4a3ab1432e120b8f0c324f899070f1a5f831. --- openpype/modules/slack/manifest.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml index 233c39fbaf..7a65cc5915 100644 --- a/openpype/modules/slack/manifest.yml +++ b/openpype/modules/slack/manifest.yml @@ -19,8 +19,6 @@ oauth_config: - chat:write.public - files:write - channels:read - - users:read - - usergroups:read settings: org_deploy_enabled: false socket_mode_enabled: false From 68313f9215a54d94f4c18119a56679b7c277f937 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 13:10:11 +0100 Subject: [PATCH 345/483] OP-4643 - changed label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5333d514b5..3e9467af61 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -294,7 +294,7 @@ "children": [ { "key": "additional_command_args", - "label": "Additional command line arguments", + "label": "Arguments", "type": "list", "object_type": "text" } From 68a0892a1d20a17f2504382a3c16586de5039108 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 15:06:16 +0100 Subject: [PATCH 346/483] OP-4643 - added documentation --- .../assets/global_oiio_transcode.png | Bin 0 -> 29010 bytes .../project_settings/settings_project_global.md | 15 +++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 website/docs/project_settings/assets/global_oiio_transcode.png diff --git a/website/docs/project_settings/assets/global_oiio_transcode.png b/website/docs/project_settings/assets/global_oiio_transcode.png new file mode 100644 index 0000000000000000000000000000000000000000..99396d5bb3f16d434a92c6079515128488b82489 GIT binary patch literal 29010 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 literal 0 HcmV?d00001 diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 37fed93e69..52671d2db6 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -45,6 +45,21 @@ The _input pattern_ matching uses REGEX expression syntax (try [regexr.com](http The **colorspace name** value is a raw string input and no validation is run after saving project settings. We recommend to open the specified `config.ocio` file and copy pasting the exact colorspace names. ::: +### Extract OIIO Transcode +There is profile configurable (see lower) plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. +Plugin expects instances with filled dictionary `colorspaceData` on a representation. This data contains information about source colorspace and must be collected for transcoding. +`oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. + +Notable parameters: +- **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. +- **`Extension`** - target extension, could be empty - original extension is used +- **`Colorspace`** - target colorspace - must be available in used color config +- **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) +- **`Arguments`** - special additional command line arguments for `oiiotool` + + +Example here describes use case for creation of new color coded review of png image sequence. Original representation's files are kept intact, review is created from transcoded files, but these files are removed in cleanup process. +![global_oiio_transcode](assets/global_oiio_transcode.png) ## Profile filters From 265a08abcdf88d3180fc3a1da362351c28083b9b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:41:42 +0100 Subject: [PATCH 347/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 52671d2db6..cc661a21fa 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -46,7 +46,7 @@ The **colorspace name** value is a raw string input and no validation is run aft ::: ### Extract OIIO Transcode -There is profile configurable (see lower) plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. +There is profile configurable plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. Plugin expects instances with filled dictionary `colorspaceData` on a representation. This data contains information about source colorspace and must be collected for transcoding. `oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. From b2d40c1cc39fbc42b0365a3edc9fb638db5c3584 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:42:06 +0100 Subject: [PATCH 348/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index cc661a21fa..8e557a381c 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -52,7 +52,7 @@ Plugin expects instances with filled dictionary `colorspaceData` on a representa Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. -- **`Extension`** - target extension, could be empty - original extension is used +- **`Extension`** - target extension. If left empty, original extension is used. - **`Colorspace`** - target colorspace - must be available in used color config - **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) - **`Arguments`** - special additional command line arguments for `oiiotool` From 2d601023f7db52033736e770b6ec3879fda04de5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:42:29 +0100 Subject: [PATCH 349/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 8e557a381c..166400cb7f 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -53,7 +53,7 @@ Plugin expects instances with filled dictionary `colorspaceData` on a representa Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. -- **`Colorspace`** - target colorspace - must be available in used color config +- **`Colorspace`** - target colorspace, which must be available in used color config. - **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) - **`Arguments`** - special additional command line arguments for `oiiotool` From 6260ea0a91cce8f1a67707aa55553e6ea6afbcab Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:42:48 +0100 Subject: [PATCH 350/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 166400cb7f..908191f122 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -54,7 +54,7 @@ Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. - **`Colorspace`** - target colorspace, which must be available in used color config. -- **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) +- **`Display & View`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both `Colorspace` and `Display & View` at the same time. - **`Arguments`** - special additional command line arguments for `oiiotool` From c1c8ca234f97c6b3f64bd91c52f91969656077e1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:43:06 +0100 Subject: [PATCH 351/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 908191f122..0a73868d2d 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -55,7 +55,7 @@ Notable parameters: - **`Extension`** - target extension. If left empty, original extension is used. - **`Colorspace`** - target colorspace, which must be available in used color config. - **`Display & View`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both `Colorspace` and `Display & View` at the same time. -- **`Arguments`** - special additional command line arguments for `oiiotool` +- **`Arguments`** - special additional command line arguments for `oiiotool`. Example here describes use case for creation of new color coded review of png image sequence. Original representation's files are kept intact, review is created from transcoded files, but these files are removed in cleanup process. From 92768e004993311fae5e74ccc9e552ba8f171a2c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 17:21:20 +0100 Subject: [PATCH 352/483] OP-4643 - updates to documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- website/docs/project_settings/settings_project_global.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 0a73868d2d..9e2ee187cc 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -46,8 +46,8 @@ The **colorspace name** value is a raw string input and no validation is run aft ::: ### Extract OIIO Transcode -There is profile configurable plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. -Plugin expects instances with filled dictionary `colorspaceData` on a representation. This data contains information about source colorspace and must be collected for transcoding. +OIIOTools transcoder plugin with configurable output presets. Any incoming representation with `colorspaceData` is convertable to single or multiple representations with different target colorspaces or display and viewer names found in linked **config.ocio** file. + `oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. Notable parameters: From 94ee02879286ef75ce60001f8c94d818a24aea57 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 17:56:21 +0100 Subject: [PATCH 353/483] Revert "OP-4643 - split command line arguments to separate items" This reverts commit deaad39437501f18fc3ba4be8b1fc5f0ee3be65d. --- openpype/lib/transcoding.py | 29 +--------------------- openpype/plugins/publish/extract_review.py | 27 +++++++++++++++++--- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 4d2f72fc41..982cee7a46 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1100,7 +1100,7 @@ def convert_colorspace( raise ValueError("Both screen and display must be set.") if additional_command_args: - oiio_cmd.extend(split_cmd_args(additional_command_args)) + oiio_cmd.extend(additional_command_args) if target_colorspace: oiio_cmd.extend(["--colorconvert", @@ -1114,30 +1114,3 @@ def convert_colorspace( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) - - -def split_cmd_args(in_args): - """Makes sure all entered arguments are separated in individual items. - - Split each argument string with " -" to identify if string contains - one or more arguments. - Args: - in_args (list): of arguments ['-n', '-d uint10'] - Returns - (list): ['-n', '-d', 'unint10'] - """ - splitted_args = [] - for arg in in_args: - sub_args = arg.split(" -") - if len(sub_args) == 1: - if arg and arg not in splitted_args: - splitted_args.append(arg) - continue - - for idx, arg in enumerate(sub_args): - if idx != 0: - arg = "-" + arg - - if arg and arg not in splitted_args: - splitted_args.append(arg) - return splitted_args diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index e80141fc4a..0f6dacba18 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -22,7 +22,6 @@ from openpype.lib.transcoding import ( should_convert_for_ffmpeg, convert_input_paths_for_ffmpeg, get_transcode_temp_directory, - split_cmd_args ) @@ -671,7 +670,7 @@ class ExtractReview(pyblish.api.InstancePlugin): res_filters = self.rescaling_filters(temp_data, output_def, new_repre) ffmpeg_video_filters.extend(res_filters) - ffmpeg_input_args = split_cmd_args(ffmpeg_input_args) + ffmpeg_input_args = self.split_ffmpeg_args(ffmpeg_input_args) lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args) ffmpeg_video_filters.extend(lut_filters) @@ -724,6 +723,28 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_output_args ) + def split_ffmpeg_args(self, in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + """ + splitted_args = [] + for arg in in_args: + sub_args = arg.split(" -") + if len(sub_args) == 1: + if arg and arg not in splitted_args: + splitted_args.append(arg) + continue + + for idx, arg in enumerate(sub_args): + if idx != 0: + arg = "-" + arg + + if arg and arg not in splitted_args: + splitted_args.append(arg) + return splitted_args + def ffmpeg_full_args( self, input_args, video_filters, audio_filters, output_args ): @@ -743,7 +764,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Returns: list: Containing all arguments ready to run in subprocess. """ - output_args = split_cmd_args(output_args) + output_args = self.split_ffmpeg_args(output_args) video_args_dentifiers = ["-vf", "-filter:v"] audio_args_dentifiers = ["-af", "-filter:a"] From 840f6811345241dd4e058d2d5940847c5b31c69f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 18:02:17 +0100 Subject: [PATCH 354/483] OP-4643 - different splitting for oiio It seems that logic in ExtractReview does different thing. --- openpype/lib/transcoding.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 982cee7a46..376297ff32 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1100,7 +1100,7 @@ def convert_colorspace( raise ValueError("Both screen and display must be set.") if additional_command_args: - oiio_cmd.extend(additional_command_args) + oiio_cmd.extend(split_cmd_args(additional_command_args)) if target_colorspace: oiio_cmd.extend(["--colorconvert", @@ -1114,3 +1114,21 @@ def convert_colorspace( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) + + +def split_cmd_args(in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + Args: + in_args (list): of arguments ['-n', '-d uint10'] + Returns + (list): ['-n', '-d', 'unint10'] + """ + splitted_args = [] + for arg in in_args: + if not arg.strip(): + continue + splitted_args.extend(arg.split(" ")) + return splitted_args From 5a7e84ab90042c3565e5ba13f11cf5674f02e4f8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 18:14:57 +0100 Subject: [PATCH 355/483] OP-4643 - allow colorspace to be empty and collected from DCC --- openpype/plugins/publish/extract_color_transcode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 456e40008d..82b92ec93e 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -118,7 +118,8 @@ class ExtractOIIOTranscode(publish.Extractor): output_name, output_extension) - target_colorspace = output_def["colorspace"] + target_colorspace = (output_def["colorspace"] or + colorspace_data.get("colorspace")) view = output_def["view"] or colorspace_data.get("view") display = (output_def["display"] or colorspace_data.get("display")) From 4abac5ec783c29465d4eb6347ffbd87b6315c2df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 12:22:53 +0100 Subject: [PATCH 356/483] OP-4643 - fix colorspace from DCC representation["colorspaceData"]["colorspace"] is only input colorspace --- openpype/plugins/publish/extract_color_transcode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 82b92ec93e..456e40008d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -118,8 +118,7 @@ class ExtractOIIOTranscode(publish.Extractor): output_name, output_extension) - target_colorspace = (output_def["colorspace"] or - colorspace_data.get("colorspace")) + target_colorspace = output_def["colorspace"] view = output_def["view"] or colorspace_data.get("view") display = (output_def["display"] or colorspace_data.get("display")) From 6cb8cbd6fc03b2d63f09bac0b25634cb1ef3f827 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 17:33:20 +0100 Subject: [PATCH 357/483] OP-4643 - added explicit enum for transcoding type As transcoding info (colorspace, display) might be collected from DCC, it must be explicit which should be used. --- openpype/lib/transcoding.py | 2 +- .../plugins/publish/extract_color_transcode.py | 15 +++++++++++---- .../schemas/schema_global_publish.json | 9 +++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 376297ff32..c0bda2aa37 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1052,7 +1052,7 @@ def convert_colorspace( output_path, config_path, source_colorspace, - target_colorspace, + target_colorspace=None, view=None, display=None, additional_command_args=None, diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 456e40008d..b0921688e9 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -118,10 +118,17 @@ class ExtractOIIOTranscode(publish.Extractor): output_name, output_extension) - target_colorspace = output_def["colorspace"] - view = output_def["view"] or colorspace_data.get("view") - display = (output_def["display"] or - colorspace_data.get("display")) + transcoding_type = output_def["transcoding_type"] + + target_colorspace = view = display = None + if transcoding_type == "colorspace": + target_colorspace = (output_def["colorspace"] or + colorspace_data.get("colorspace")) + else: + view = output_def["view"] or colorspace_data.get("view") + display = (output_def["display"] or + colorspace_data.get("display")) + # both could be already collected by DCC, # but could be overwritten if view: diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 3e9467af61..76574e8b9b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -271,6 +271,15 @@ "label": "Extension", "type": "text" }, + { + "type": "enum", + "key": "transcoding_type", + "label": "Transcoding type", + "enum_items": [ + { "colorspace": "Use Colorspace" }, + { "display": "Use Display&View" } + ] + }, { "key": "colorspace", "label": "Colorspace", From 58ae146027ebe31b9fe2d7fcc6d4323efb93c883 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 17:34:03 +0100 Subject: [PATCH 358/483] OP-4643 - added explicit enum for transcoding type As transcoding info (colorspace, display) might be collected from DCC, it must be explicit which should be used. --- .../assets/global_oiio_transcode.png | Bin 29010 -> 17936 bytes .../settings_project_global.md | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/website/docs/project_settings/assets/global_oiio_transcode.png b/website/docs/project_settings/assets/global_oiio_transcode.png index 99396d5bb3f16d434a92c6079515128488b82489..d818ecfe19f93366e5f4664734d68c7b448d7b4f 100644 GIT binary patch literal 17936 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 DbV~re literal 29010 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 diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 9e2ee187cc..6e78ee5d45 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -53,8 +53,9 @@ OIIOTools transcoder plugin with configurable output presets. Any incoming repre Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. -- **`Colorspace`** - target colorspace, which must be available in used color config. -- **`Display & View`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both `Colorspace` and `Display & View` at the same time. +- **`Transcoding type`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both at the same time. +- **`Colorspace`** - target colorspace, which must be available in used color config. (If `Transcoding type` is `Use Colorspace` value in configuration is used OR if empty value collected on instance from DCC). +- **`Display & View`** - display and viewer colorspace. (If `Transcoding type` is `Use Display&View` values in configuration is used OR if empty values collected on instance from DCC). - **`Arguments`** - special additional command line arguments for `oiiotool`. From aae1430904962212e3c31345eb317d74cbd2385b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 17:42:07 +0100 Subject: [PATCH 359/483] OP-4643 - added use case for Maya to documentation --- .../assets/global_oiio_transcode2.png | Bin 0 -> 17960 bytes .../project_settings/settings_project_global.md | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 website/docs/project_settings/assets/global_oiio_transcode2.png diff --git a/website/docs/project_settings/assets/global_oiio_transcode2.png b/website/docs/project_settings/assets/global_oiio_transcode2.png new file mode 100644 index 0000000000000000000000000000000000000000..906f780830a96b4bc6f26d98484dd3f9cc3885f2 GIT binary patch literal 17960 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 F{{cSxQDp!C literal 0 HcmV?d00001 diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 6e78ee5d45..f58d2c2bf2 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -62,6 +62,9 @@ Notable parameters: Example here describes use case for creation of new color coded review of png image sequence. Original representation's files are kept intact, review is created from transcoded files, but these files are removed in cleanup process. ![global_oiio_transcode](assets/global_oiio_transcode.png) +Another use case is to transcode in Maya only `beauty` render layers and use collected `Display` and `View` colorspaces from DCC. +![global_oiio_transcode_in_Maya](assets/global_oiio_transcode.png) + ## Profile filters Many of the settings are using a concept of **Profile filters** From 82e4e3e5b76d195be63bfce7017e3cc2ede23704 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Feb 2023 11:05:26 +0100 Subject: [PATCH 360/483] OP-4643 - updates to documentation Co-authored-by: Roy Nieterau --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index f58d2c2bf2..d904080ad1 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -51,7 +51,7 @@ OIIOTools transcoder plugin with configurable output presets. Any incoming repre `oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. Notable parameters: -- **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. +- **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation loses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. - **`Transcoding type`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both at the same time. - **`Colorspace`** - target colorspace, which must be available in used color config. (If `Transcoding type` is `Use Colorspace` value in configuration is used OR if empty value collected on instance from DCC). From 04109103303c436873a1898de53a54735c524f10 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 11:57:51 +0100 Subject: [PATCH 361/483] OP-4643 - added Settings for ExtractColorTranscode --- .../defaults/project_settings/global.json | 4 + .../schemas/schema_global_publish.json | 73 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index cedc2d6876..8485bec67b 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -68,6 +68,10 @@ "output": [] } }, + "ExtractColorTranscode": { + "enabled": true, + "profiles": [] + }, "ExtractReview": { "enabled": true, "profiles": [ diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5388d04bc9..46ae6ba554 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -197,6 +197,79 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractColorTranscode", + "label": "ExtractColorTranscode", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "hosts", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "splitter" + }, + { + "key": "ext", + "label": "Output extension", + "type": "text" + }, + { + "key": "output_colorspace", + "label": "Output colorspace", + "type": "text" + }, + { + "key": "custom_tags", + "label": "Custom Tags", + "type": "list", + "object_type": "text" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From 2f1888bbfbd8dbabcd50ed4d48ab2230d810ba53 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 11:58:51 +0100 Subject: [PATCH 362/483] OP-4643 - added ExtractColorTranscode Added method to convert from one colorspace to another to transcoding lib --- openpype/lib/transcoding.py | 53 ++++++++ .../publish/extract_color_transcode.py | 124 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 openpype/plugins/publish/extract_color_transcode.py diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 039255d937..2fc662f2a4 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1045,3 +1045,56 @@ def convert_ffprobe_fps_to_float(value): if divisor == 0.0: return 0.0 return dividend / divisor + + +def convert_colorspace_for_input_paths( + input_paths, + output_dir, + source_color_space, + target_color_space, + logger=None +): + """Convert source files from one color space to another. + + Filenames of input files are kept so make sure that output directory + is not the same directory as input files have. + - This way it can handle gaps and can keep input filenames without handling + frame template + + Args: + input_paths (str): Paths that should be converted. It is expected that + contains single file or image sequence of samy type. + output_dir (str): Path to directory where output will be rendered. + Must not be same as input's directory. + source_color_space (str): ocio valid color space of source files + target_color_space (str): ocio valid target color space + logger (logging.Logger): Logger used for logging. + + """ + if logger is None: + logger = logging.getLogger(__name__) + + input_arg = "-i" + oiio_cmd = [ + get_oiio_tools_path(), + + # Don't add any additional attributes + "--nosoftwareattrib", + "--colorconvert", source_color_space, target_color_space + ] + for input_path in input_paths: + # Prepare subprocess arguments + + oiio_cmd.extend([ + input_arg, input_path, + ]) + + # Add last argument - path to output + base_filename = os.path.basename(input_path) + output_path = os.path.join(output_dir, base_filename) + oiio_cmd.extend([ + "-o", output_path + ]) + + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=logger) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py new file mode 100644 index 0000000000..58508ab18f --- /dev/null +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -0,0 +1,124 @@ +import pyblish.api + +from openpype.pipeline import publish +from openpype.lib import ( + + is_oiio_supported, +) + +from openpype.lib.transcoding import ( + convert_colorspace_for_input_paths, + get_transcode_temp_directory, +) + +from openpype.lib.profiles_filtering import filter_profiles + + +class ExtractColorTranscode(publish.Extractor): + """ + Extractor to convert colors from one colorspace to different. + """ + + label = "Transcode color spaces" + order = pyblish.api.ExtractorOrder + 0.01 + + optional = True + + # Configurable by Settings + profiles = None + options = None + + def process(self, instance): + if not self.profiles: + self.log.warning("No profiles present for create burnin") + return + + if "representations" not in instance.data: + self.log.warning("No representations, skipping.") + return + + if not is_oiio_supported(): + self.log.warning("OIIO not supported, no transcoding possible.") + return + + colorspace_data = instance.data.get("colorspaceData") + if not colorspace_data: + # TODO get_colorspace ?? + self.log.warning("Instance has not colorspace data, skipping") + return + source_color_space = colorspace_data["colorspace"] + + host_name = instance.context.data["hostName"] + family = instance.data["family"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + subset = instance.data["subset"] + + filtering_criteria = { + "hosts": host_name, + "families": family, + "task_names": task_name, + "task_types": task_type, + "subset": subset + } + profile = filter_profiles(self.profiles, filtering_criteria, + logger=self.log) + + if not profile: + self.log.info(( + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) + return + + self.log.debug("profile: {}".format(profile)) + + target_colorspace = profile["output_colorspace"] + if not target_colorspace: + raise RuntimeError("Target colorspace must be set") + + repres = instance.data.get("representations") or [] + for idx, repre in enumerate(repres): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self.repre_is_valid(repre): + continue + + new_staging_dir = get_transcode_temp_directory() + repre["stagingDir"] = new_staging_dir + files_to_remove = repre["files"] + if not isinstance(files_to_remove, list): + files_to_remove = [files_to_remove] + instance.context.data["cleanupFullPaths"].extend(files_to_remove) + + convert_colorspace_for_input_paths( + repre["files"], + new_staging_dir, + source_color_space, + target_colorspace, + self.log + ) + + def repre_is_valid(self, repre): + """Validation if representation should be processed. + + Args: + repre (dict): Representation which should be checked. + + Returns: + bool: False if can't be processed else True. + """ + + if "review" not in (repre.get("tags") or []): + self.log.info(( + "Representation \"{}\" don't have \"review\" tag. Skipped." + ).format(repre["name"])) + return False + + if not repre.get("files"): + self.log.warning(( + "Representation \"{}\" have empty files. Skipped." + ).format(repre["name"])) + return False + return True From b932994e15ab43e5df93bcf3e81e71622594c6a2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 12:05:57 +0100 Subject: [PATCH 363/483] OP-4643 - extractor must run just before ExtractReview Nuke render local is set to 0.01 --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 58508ab18f..5163cd4045 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -20,7 +20,7 @@ class ExtractColorTranscode(publish.Extractor): """ label = "Transcode color spaces" - order = pyblish.api.ExtractorOrder + 0.01 + order = pyblish.api.ExtractorOrder + 0.019 optional = True From 48f24ef17d8929e84ac16868ad2d6a733d47b1f1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:03:22 +0100 Subject: [PATCH 364/483] OP-4643 - fix for full file paths --- .../publish/extract_color_transcode.py | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 5163cd4045..6ad7599f2c 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,3 +1,4 @@ +import os import pyblish.api from openpype.pipeline import publish @@ -41,13 +42,6 @@ class ExtractColorTranscode(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return - colorspace_data = instance.data.get("colorspaceData") - if not colorspace_data: - # TODO get_colorspace ?? - self.log.warning("Instance has not colorspace data, skipping") - return - source_color_space = colorspace_data["colorspace"] - host_name = instance.context.data["hostName"] family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) @@ -82,18 +76,32 @@ class ExtractColorTranscode(publish.Extractor): repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - if not self.repre_is_valid(repre): + # if not self.repre_is_valid(repre): + # continue + + colorspace_data = repre.get("colorspaceData") + if not colorspace_data: + # TODO get_colorspace ?? + self.log.warning("Repre has not colorspace data, skipping") + continue + source_color_space = colorspace_data["colorspace"] + config_path = colorspace_data.get("configData", {}).get("path") + if not os.path.exists(config_path): + self.log.warning("Config file doesn't exist, skipping") continue new_staging_dir = get_transcode_temp_directory() + original_staging_dir = repre["stagingDir"] repre["stagingDir"] = new_staging_dir - files_to_remove = repre["files"] - if not isinstance(files_to_remove, list): - files_to_remove = [files_to_remove] - instance.context.data["cleanupFullPaths"].extend(files_to_remove) + files_to_convert = repre["files"] + if not isinstance(files_to_convert, list): + files_to_convert = [files_to_convert] + files_to_convert = [os.path.join(original_staging_dir, path) + for path in files_to_convert] + instance.context.data["cleanupFullPaths"].extend(files_to_convert) convert_colorspace_for_input_paths( - repre["files"], + files_to_convert, new_staging_dir, source_color_space, target_colorspace, From ec299f0d3ca379f73e6f06949506148e78ca5fa1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:04:06 +0100 Subject: [PATCH 365/483] OP-4643 - pass path for ocio config --- openpype/lib/transcoding.py | 3 +++ openpype/plugins/publish/extract_color_transcode.py | 1 + 2 files changed, 4 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 2fc662f2a4..ab86e44304 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1050,6 +1050,7 @@ def convert_ffprobe_fps_to_float(value): def convert_colorspace_for_input_paths( input_paths, output_dir, + config_path, source_color_space, target_color_space, logger=None @@ -1066,6 +1067,7 @@ def convert_colorspace_for_input_paths( contains single file or image sequence of samy type. output_dir (str): Path to directory where output will be rendered. Must not be same as input's directory. + config_path (str): path to OCIO config file source_color_space (str): ocio valid color space of source files target_color_space (str): ocio valid target color space logger (logging.Logger): Logger used for logging. @@ -1080,6 +1082,7 @@ def convert_colorspace_for_input_paths( # Don't add any additional attributes "--nosoftwareattrib", + "--colorconfig", config_path, "--colorconvert", source_color_space, target_color_space ] for input_path in input_paths: diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 6ad7599f2c..fdb13a47e8 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -103,6 +103,7 @@ class ExtractColorTranscode(publish.Extractor): convert_colorspace_for_input_paths( files_to_convert, new_staging_dir, + config_path, source_color_space, target_colorspace, self.log From 2bc8377dbcf856012489a49051da9952ab75546b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:15:33 +0100 Subject: [PATCH 366/483] OP-4643 - add custom_tags --- openpype/plugins/publish/extract_color_transcode.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index fdb13a47e8..ab932b2476 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -72,6 +72,7 @@ class ExtractColorTranscode(publish.Extractor): target_colorspace = profile["output_colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") + custom_tags = profile["custom_tags"] repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): @@ -109,6 +110,11 @@ class ExtractColorTranscode(publish.Extractor): self.log ) + if custom_tags: + if not repre.get("custom_tags"): + repre["custom_tags"] = [] + repre["custom_tags"].extend(custom_tags) + def repre_is_valid(self, repre): """Validation if representation should be processed. From 4a80b7bb34efdf1dbd7c8f554d42f1caff035385 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:18:38 +0100 Subject: [PATCH 367/483] OP-4643 - added docstring --- openpype/plugins/publish/extract_color_transcode.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index ab932b2476..88e2eed90f 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -18,6 +18,17 @@ from openpype.lib.profiles_filtering import filter_profiles class ExtractColorTranscode(publish.Extractor): """ Extractor to convert colors from one colorspace to different. + + Expects "colorspaceData" on representation. This dictionary is collected + previously and denotes that representation files should be converted. + This dict contains source colorspace information, collected by hosts. + + Target colorspace is selected by profiles in the Settings, based on: + - families + - host + - task types + - task names + - subset names """ label = "Transcode color spaces" From f92c74605b793db31bbee90ccbed656571e69c39 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:15:44 +0100 Subject: [PATCH 368/483] OP-4643 - updated Settings schema --- .../schemas/schema_global_publish.json | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 46ae6ba554..c2c911d7d6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -246,24 +246,44 @@ "type": "list", "object_type": "text" }, + { + "type": "boolean", + "key": "delete_original", + "label": "Delete Original Representation" + }, { "type": "splitter" }, { - "key": "ext", - "label": "Output extension", - "type": "text" - }, - { - "key": "output_colorspace", - "label": "Output colorspace", - "type": "text" - }, - { - "key": "custom_tags", - "label": "Custom Tags", - "type": "list", - "object_type": "text" + "key": "outputs", + "label": "Output Definitions", + "type": "dict-modifiable", + "highlight_content": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "output_extension", + "label": "Output extension", + "type": "text" + }, + { + "key": "output_colorspace", + "label": "Output colorspace", + "type": "text" + }, + { + "type": "schema", + "name": "schema_representation_tags" + }, + { + "key": "custom_tags", + "label": "Custom Tags", + "type": "list", + "object_type": "text" + } + ] + } } ] } From 2f79021aca117bf3ebea7a2bd1103a6c7e958525 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:17:25 +0100 Subject: [PATCH 369/483] OP-4643 - skip video files Only frames currently supported. --- .../plugins/publish/extract_color_transcode.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 88e2eed90f..a0714c9a33 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -36,6 +36,9 @@ class ExtractColorTranscode(publish.Extractor): optional = True + # Supported extensions + supported_exts = ["exr", "jpg", "jpeg", "png", "dpx"] + # Configurable by Settings profiles = None options = None @@ -88,13 +91,7 @@ class ExtractColorTranscode(publish.Extractor): repres = instance.data.get("representations") or [] for idx, repre in enumerate(repres): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - # if not self.repre_is_valid(repre): - # continue - - colorspace_data = repre.get("colorspaceData") - if not colorspace_data: - # TODO get_colorspace ?? - self.log.warning("Repre has not colorspace data, skipping") + if not self._repre_is_valid(repre): continue source_color_space = colorspace_data["colorspace"] config_path = colorspace_data.get("configData", {}).get("path") @@ -136,9 +133,9 @@ class ExtractColorTranscode(publish.Extractor): bool: False if can't be processed else True. """ - if "review" not in (repre.get("tags") or []): - self.log.info(( - "Representation \"{}\" don't have \"review\" tag. Skipped." + if repre.get("ext") not in self.supported_exts: + self.log.warning(( + "Representation \"{}\" of unsupported extension. Skipped." ).format(repre["name"])) return False From e63dc4075629efe7499a510742c61a0f5e9c46fd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:19:08 +0100 Subject: [PATCH 370/483] OP-4643 - refactored profile, delete of original Implemented multiple outputs from single input representation --- .../publish/extract_color_transcode.py | 156 ++++++++++++------ 1 file changed, 109 insertions(+), 47 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index a0714c9a33..b0c851d5f4 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,4 +1,6 @@ import os +import copy + import pyblish.api from openpype.pipeline import publish @@ -56,13 +58,94 @@ class ExtractColorTranscode(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return + profile = self._get_profile(instance) + if not profile: + return + + repres = instance.data.get("representations") or [] + for idx, repre in enumerate(list(repres)): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self._repre_is_valid(repre): + continue + + colorspace_data = repre["colorspaceData"] + source_color_space = colorspace_data["colorspace"] + config_path = colorspace_data.get("configData", {}).get("path") + if not os.path.exists(config_path): + self.log.warning("Config file doesn't exist, skipping") + continue + + repre = self._handle_original_repre(repre, profile) + + for _, output_def in profile.get("outputs", {}).items(): + new_repre = copy.deepcopy(repre) + + new_staging_dir = get_transcode_temp_directory() + original_staging_dir = new_repre["stagingDir"] + new_repre["stagingDir"] = new_staging_dir + files_to_convert = new_repre["files"] + if not isinstance(files_to_convert, list): + files_to_convert = [files_to_convert] + + files_to_delete = copy.deepcopy(files_to_convert) + + output_extension = output_def["output_extension"] + files_to_convert = self._rename_output_files(files_to_convert, + output_extension) + + files_to_convert = [os.path.join(original_staging_dir, path) + for path in files_to_convert] + + target_colorspace = output_def["output_colorspace"] + if not target_colorspace: + raise RuntimeError("Target colorspace must be set") + + convert_colorspace_for_input_paths( + files_to_convert, + new_staging_dir, + config_path, + source_color_space, + target_colorspace, + self.log + ) + + instance.context.data["cleanupFullPaths"].extend( + files_to_delete) + + custom_tags = output_def.get("custom_tags") + if custom_tags: + if not new_repre.get("custom_tags"): + new_repre["custom_tags"] = [] + new_repre["custom_tags"].extend(custom_tags) + + # Add additional tags from output definition to representation + for tag in output_def["tags"]: + if tag not in new_repre["tags"]: + new_repre["tags"].append(tag) + + instance.data["representations"].append(new_repre) + + def _rename_output_files(self, files_to_convert, output_extension): + """Change extension of converted files.""" + if output_extension: + output_extension = output_extension.replace('.', '') + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + new_file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(new_file_name) + files_to_convert = renamed_files + return files_to_convert + + def _get_profile(self, instance): + """Returns profile if and how repre should be color transcoded.""" host_name = instance.context.data["hostName"] family = instance.data["family"] task_data = instance.data["anatomyData"].get("task", {}) task_name = task_data.get("name") task_type = task_data.get("type") subset = instance.data["subset"] - filtering_criteria = { "hosts": host_name, "families": family, @@ -75,55 +158,15 @@ class ExtractColorTranscode(publish.Extractor): if not profile: self.log.info(( - "Skipped instance. None of profiles in presets are for" - " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" - " | Task type \"{}\" | Subset \"{}\" " - ).format(host_name, family, task_name, task_type, subset)) - return + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) self.log.debug("profile: {}".format(profile)) + return profile - target_colorspace = profile["output_colorspace"] - if not target_colorspace: - raise RuntimeError("Target colorspace must be set") - custom_tags = profile["custom_tags"] - - repres = instance.data.get("representations") or [] - for idx, repre in enumerate(repres): - self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - if not self._repre_is_valid(repre): - continue - source_color_space = colorspace_data["colorspace"] - config_path = colorspace_data.get("configData", {}).get("path") - if not os.path.exists(config_path): - self.log.warning("Config file doesn't exist, skipping") - continue - - new_staging_dir = get_transcode_temp_directory() - original_staging_dir = repre["stagingDir"] - repre["stagingDir"] = new_staging_dir - files_to_convert = repre["files"] - if not isinstance(files_to_convert, list): - files_to_convert = [files_to_convert] - files_to_convert = [os.path.join(original_staging_dir, path) - for path in files_to_convert] - instance.context.data["cleanupFullPaths"].extend(files_to_convert) - - convert_colorspace_for_input_paths( - files_to_convert, - new_staging_dir, - config_path, - source_color_space, - target_colorspace, - self.log - ) - - if custom_tags: - if not repre.get("custom_tags"): - repre["custom_tags"] = [] - repre["custom_tags"].extend(custom_tags) - - def repre_is_valid(self, repre): + def _repre_is_valid(self, repre): """Validation if representation should be processed. Args: @@ -144,4 +187,23 @@ class ExtractColorTranscode(publish.Extractor): "Representation \"{}\" have empty files. Skipped." ).format(repre["name"])) return False + + if not repre.get("colorspaceData"): + self.log.warning("Repre has not colorspace data, skipping") + return False + return True + + def _handle_original_repre(self, repre, profile): + delete_original = profile["delete_original"] + + if delete_original: + if not repre.get("tags"): + repre["tags"] = [] + + if "review" in repre["tags"]: + repre["tags"].remove("review") + if "delete" not in repre["tags"]: + repre["tags"].append("delete") + + return repre From 7341c618274ccdc8f469473ff05698499f6d5b72 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:23:01 +0100 Subject: [PATCH 371/483] OP-4643 - switched logging levels Do not use warning unnecessary. --- openpype/plugins/publish/extract_color_transcode.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index b0c851d5f4..4d38514b8b 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -47,11 +47,11 @@ class ExtractColorTranscode(publish.Extractor): def process(self, instance): if not self.profiles: - self.log.warning("No profiles present for create burnin") + self.log.debug("No profiles present for color transcode") return if "representations" not in instance.data: - self.log.warning("No representations, skipping.") + self.log.debug("No representations, skipping.") return if not is_oiio_supported(): @@ -177,19 +177,19 @@ class ExtractColorTranscode(publish.Extractor): """ if repre.get("ext") not in self.supported_exts: - self.log.warning(( + self.log.debug(( "Representation \"{}\" of unsupported extension. Skipped." ).format(repre["name"])) return False if not repre.get("files"): - self.log.warning(( + self.log.debug(( "Representation \"{}\" have empty files. Skipped." ).format(repre["name"])) return False if not repre.get("colorspaceData"): - self.log.warning("Repre has not colorspace data, skipping") + self.log.debug("Repre has no colorspace data. Skipped.") return False return True From d5cc450e9cd18f7360d65140e48ea5eab810c8e2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:46:14 +0100 Subject: [PATCH 372/483] OP-4643 - propagate new extension to representation --- .../publish/extract_color_transcode.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4d38514b8b..62cf8f0dee 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -90,8 +90,13 @@ class ExtractColorTranscode(publish.Extractor): files_to_delete = copy.deepcopy(files_to_convert) output_extension = output_def["output_extension"] - files_to_convert = self._rename_output_files(files_to_convert, - output_extension) + output_extension = output_extension.replace('.', '') + if output_extension: + new_repre["name"] = output_extension + new_repre["ext"] = output_extension + + files_to_convert = self._rename_output_files( + files_to_convert, output_extension) files_to_convert = [os.path.join(original_staging_dir, path) for path in files_to_convert] @@ -127,15 +132,13 @@ class ExtractColorTranscode(publish.Extractor): def _rename_output_files(self, files_to_convert, output_extension): """Change extension of converted files.""" - if output_extension: - output_extension = output_extension.replace('.', '') - renamed_files = [] - for file_name in files_to_convert: - file_name, _ = os.path.splitext(file_name) - new_file_name = '{}.{}'.format(file_name, - output_extension) - renamed_files.append(new_file_name) - files_to_convert = renamed_files + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + new_file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(new_file_name) + files_to_convert = renamed_files return files_to_convert def _get_profile(self, instance): From 65b454c42c77fb75afc06dd34cf36c40ea52a751 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 18:46:35 +0100 Subject: [PATCH 373/483] OP-4643 - added label to Settings --- .../projects_schema/schemas/schema_global_publish.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index c2c911d7d6..7155510fef 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -201,10 +201,14 @@ "type": "dict", "collapsible": true, "key": "ExtractColorTranscode", - "label": "ExtractColorTranscode", + "label": "ExtractColorTranscode (ImageIO)", "checkbox_key": "enabled", "is_group": true, "children": [ + { + "type": "label", + "label": "Configure output format(s) and color spaces for matching representations. Empty 'Output extension' denotes keeping source extension." + }, { "type": "boolean", "key": "enabled", From 9fc4070e066499a89898c450263119dbf8e2f99a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 16 Jan 2023 18:22:08 +0100 Subject: [PATCH 374/483] OP-4643 - refactored according to review Function turned into single filepath input. --- openpype/lib/transcoding.py | 43 ++++++----- .../publish/extract_color_transcode.py | 72 ++++++++++--------- 2 files changed, 57 insertions(+), 58 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index ab86e44304..e1bd22d109 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1047,12 +1047,12 @@ def convert_ffprobe_fps_to_float(value): return dividend / divisor -def convert_colorspace_for_input_paths( - input_paths, - output_dir, +def convert_colorspace( + input_path, + out_filepath, config_path, - source_color_space, - target_color_space, + source_colorspace, + target_colorspace, logger=None ): """Convert source files from one color space to another. @@ -1063,13 +1063,13 @@ def convert_colorspace_for_input_paths( frame template Args: - input_paths (str): Paths that should be converted. It is expected that + input_path (str): Paths that should be converted. It is expected that contains single file or image sequence of samy type. - output_dir (str): Path to directory where output will be rendered. + out_filepath (str): Path to directory where output will be rendered. Must not be same as input's directory. config_path (str): path to OCIO config file - source_color_space (str): ocio valid color space of source files - target_color_space (str): ocio valid target color space + source_colorspace (str): ocio valid color space of source files + target_colorspace (str): ocio valid target color space logger (logging.Logger): Logger used for logging. """ @@ -1083,21 +1083,18 @@ def convert_colorspace_for_input_paths( # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "--colorconvert", source_color_space, target_color_space + "--colorconvert", source_colorspace, target_colorspace ] - for input_path in input_paths: - # Prepare subprocess arguments + # Prepare subprocess arguments - oiio_cmd.extend([ - input_arg, input_path, - ]) + oiio_cmd.extend([ + input_arg, input_path, + ]) - # Add last argument - path to output - base_filename = os.path.basename(input_path) - output_path = os.path.join(output_dir, base_filename) - oiio_cmd.extend([ - "-o", output_path - ]) + # Add last argument - path to output + oiio_cmd.extend([ + "-o", out_filepath + ]) - logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) - run_subprocess(oiio_cmd, logger=logger) + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=logger) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 62cf8f0dee..3a05426432 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -10,7 +10,7 @@ from openpype.lib import ( ) from openpype.lib.transcoding import ( - convert_colorspace_for_input_paths, + convert_colorspace, get_transcode_temp_directory, ) @@ -69,7 +69,7 @@ class ExtractColorTranscode(publish.Extractor): continue colorspace_data = repre["colorspaceData"] - source_color_space = colorspace_data["colorspace"] + source_colorspace = colorspace_data["colorspace"] config_path = colorspace_data.get("configData", {}).get("path") if not os.path.exists(config_path): self.log.warning("Config file doesn't exist, skipping") @@ -80,8 +80,8 @@ class ExtractColorTranscode(publish.Extractor): for _, output_def in profile.get("outputs", {}).items(): new_repre = copy.deepcopy(repre) - new_staging_dir = get_transcode_temp_directory() original_staging_dir = new_repre["stagingDir"] + new_staging_dir = get_transcode_temp_directory() new_repre["stagingDir"] = new_staging_dir files_to_convert = new_repre["files"] if not isinstance(files_to_convert, list): @@ -92,27 +92,28 @@ class ExtractColorTranscode(publish.Extractor): output_extension = output_def["output_extension"] output_extension = output_extension.replace('.', '') if output_extension: - new_repre["name"] = output_extension + if new_repre["name"] == new_repre["ext"]: + new_repre["name"] = output_extension new_repre["ext"] = output_extension - files_to_convert = self._rename_output_files( - files_to_convert, output_extension) - - files_to_convert = [os.path.join(original_staging_dir, path) - for path in files_to_convert] - target_colorspace = output_def["output_colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") - convert_colorspace_for_input_paths( - files_to_convert, - new_staging_dir, - config_path, - source_color_space, - target_colorspace, - self.log - ) + for file_name in files_to_convert: + input_filepath = os.path.join(original_staging_dir, + file_name) + output_path = self._get_output_file_path(input_filepath, + new_staging_dir, + output_extension) + convert_colorspace( + input_filepath, + output_path, + config_path, + source_colorspace, + target_colorspace, + self.log + ) instance.context.data["cleanupFullPaths"].extend( files_to_delete) @@ -130,16 +131,16 @@ class ExtractColorTranscode(publish.Extractor): instance.data["representations"].append(new_repre) - def _rename_output_files(self, files_to_convert, output_extension): - """Change extension of converted files.""" - renamed_files = [] - for file_name in files_to_convert: - file_name, _ = os.path.splitext(file_name) - new_file_name = '{}.{}'.format(file_name, - output_extension) - renamed_files.append(new_file_name) - files_to_convert = renamed_files - return files_to_convert + def _get_output_file_path(self, input_filepath, output_dir, + output_extension): + """Create output file name path.""" + file_name = os.path.basename(input_filepath) + file_name, input_extension = os.path.splitext(file_name) + if not output_extension: + output_extension = input_extension + new_file_name = '{}.{}'.format(file_name, + output_extension) + return os.path.join(output_dir, new_file_name) def _get_profile(self, instance): """Returns profile if and how repre should be color transcoded.""" @@ -161,10 +162,10 @@ class ExtractColorTranscode(publish.Extractor): if not profile: self.log.info(( - "Skipped instance. None of profiles in presets are for" - " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" - " | Task type \"{}\" | Subset \"{}\" " - ).format(host_name, family, task_name, task_type, subset)) + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Families: \"{}\" | Task \"{}\"" + " | Task type \"{}\" | Subset \"{}\" " + ).format(host_name, family, task_name, task_type, subset)) self.log.debug("profile: {}".format(profile)) return profile @@ -181,18 +182,19 @@ class ExtractColorTranscode(publish.Extractor): if repre.get("ext") not in self.supported_exts: self.log.debug(( - "Representation \"{}\" of unsupported extension. Skipped." + "Representation '{}' of unsupported extension. Skipped." ).format(repre["name"])) return False if not repre.get("files"): self.log.debug(( - "Representation \"{}\" have empty files. Skipped." + "Representation '{}' have empty files. Skipped." ).format(repre["name"])) return False if not repre.get("colorspaceData"): - self.log.debug("Repre has no colorspace data. Skipped.") + self.log.debug("Representation '{}' has no colorspace data. " + "Skipped.") return False return True From 5eb771333b9d0b0a7b2abf5a2487284f5043f478 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:53:02 +0100 Subject: [PATCH 375/483] OP-4643 - updated schema Co-authored-by: Toke Jepsen --- .../projects_schema/schemas/schema_global_publish.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 7155510fef..80c18ce118 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -267,8 +267,8 @@ "type": "dict", "children": [ { - "key": "output_extension", - "label": "Output extension", + "key": "extension", + "label": "Extension", "type": "text" }, { From 49a06f873bc1a77e1dde35a9e6c7f1603508e6e4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:54:46 +0100 Subject: [PATCH 376/483] OP-4643 - updated plugin name in schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- .../projects_schema/schemas/schema_global_publish.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 80c18ce118..357cbfb287 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -200,8 +200,8 @@ { "type": "dict", "collapsible": true, - "key": "ExtractColorTranscode", - "label": "ExtractColorTranscode (ImageIO)", + "key": "ExtractOIIOTranscode", + "label": "Extract OIIO Transcode", "checkbox_key": "enabled", "is_group": true, "children": [ From 2ec5221b282b4222f5ac179f63f5650050f3b331 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:55:57 +0100 Subject: [PATCH 377/483] OP-4643 - updated key in schema Co-authored-by: Toke Jepsen --- .../projects_schema/schemas/schema_global_publish.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 357cbfb287..0281b0ded6 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -272,8 +272,8 @@ "type": "text" }, { - "key": "output_colorspace", - "label": "Output colorspace", + "key": "colorspace", + "label": "Colorspace", "type": "text" }, { From 83d21d9d7793350d6374c2c240f4f70e688c2e88 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 12:57:03 +0100 Subject: [PATCH 378/483] OP-4643 - changed oiio_cmd creation Co-authored-by: Toke Jepsen --- openpype/lib/transcoding.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index e1bd22d109..f22628dd28 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1076,25 +1076,15 @@ def convert_colorspace( if logger is None: logger = logging.getLogger(__name__) - input_arg = "-i" oiio_cmd = [ get_oiio_tools_path(), - + input_path, # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "--colorconvert", source_colorspace, target_colorspace - ] - # Prepare subprocess arguments - - oiio_cmd.extend([ - input_arg, input_path, - ]) - - # Add last argument - path to output - oiio_cmd.extend([ + "--colorconvert", source_colorspace, target_colorspace, "-o", out_filepath - ]) + ] logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) From 40f8cd4a93bfe76dcb91ee683c78033417f40525 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 13:44:45 +0100 Subject: [PATCH 379/483] OP-4643 - updated new keys into settings --- .../settings/defaults/project_settings/global.json | 2 +- .../projects_schema/schemas/schema_global_publish.json | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index 8485bec67b..a5e2d25a88 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -68,7 +68,7 @@ "output": [] } }, - "ExtractColorTranscode": { + "ExtractOIIOTranscode": { "enabled": true, "profiles": [] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 0281b0ded6..74b81b13af 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -276,6 +276,16 @@ "label": "Colorspace", "type": "text" }, + { + "key": "display", + "label": "Display", + "type": "text" + }, + { + "key": "view", + "label": "View", + "type": "text" + }, { "type": "schema", "name": "schema_representation_tags" From e64389f11be2d072e9d8d59dce5334eebb727776 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 13:45:42 +0100 Subject: [PATCH 380/483] OP-4643 - renanmed plugin, added new keys into outputs --- openpype/plugins/publish/extract_color_transcode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 3a05426432..cc63b35988 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -17,7 +17,7 @@ from openpype.lib.transcoding import ( from openpype.lib.profiles_filtering import filter_profiles -class ExtractColorTranscode(publish.Extractor): +class ExtractOIIOTranscode(publish.Extractor): """ Extractor to convert colors from one colorspace to different. @@ -89,14 +89,14 @@ class ExtractColorTranscode(publish.Extractor): files_to_delete = copy.deepcopy(files_to_convert) - output_extension = output_def["output_extension"] + output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') if output_extension: if new_repre["name"] == new_repre["ext"]: new_repre["name"] = output_extension new_repre["ext"] = output_extension - target_colorspace = output_def["output_colorspace"] + target_colorspace = output_def["colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") From 99d687c9a1366984116b5ef43d65cab465886b12 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:03:13 +0100 Subject: [PATCH 381/483] OP-4643 - fixed config path key --- openpype/plugins/publish/extract_color_transcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index cc63b35988..245faeb306 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -70,8 +70,8 @@ class ExtractOIIOTranscode(publish.Extractor): colorspace_data = repre["colorspaceData"] source_colorspace = colorspace_data["colorspace"] - config_path = colorspace_data.get("configData", {}).get("path") - if not os.path.exists(config_path): + config_path = colorspace_data.get("config", {}).get("path") + if not config_path or not os.path.exists(config_path): self.log.warning("Config file doesn't exist, skipping") continue From 2a7fd01aada28eebf603e6bd8cc6d6b1a8a560a6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:03:42 +0100 Subject: [PATCH 382/483] OP-4643 - fixed renaming files --- openpype/plugins/publish/extract_color_transcode.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 245faeb306..c079dcf70e 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -96,6 +96,14 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["name"] = output_extension new_repre["ext"] = output_extension + renamed_files = [] + _, orig_ext = os.path.splitext(files_to_convert[0]) + for file_name in files_to_convert: + file_name = file_name.replace(orig_ext, + "."+output_extension) + renamed_files.append(file_name) + new_repre["files"] = renamed_files + target_colorspace = output_def["colorspace"] if not target_colorspace: raise RuntimeError("Target colorspace must be set") From 34d519524e9998c5436baf5b53f8740bf2eceff3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:04:44 +0100 Subject: [PATCH 383/483] OP-4643 - updated to calculate sequence format --- .../publish/extract_color_transcode.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index c079dcf70e..09c86909cb 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,5 +1,6 @@ import os import copy +import clique import pyblish.api @@ -108,6 +109,8 @@ class ExtractOIIOTranscode(publish.Extractor): if not target_colorspace: raise RuntimeError("Target colorspace must be set") + files_to_convert = self._translate_to_sequence( + files_to_convert) for file_name in files_to_convert: input_filepath = os.path.join(original_staging_dir, file_name) @@ -139,6 +142,40 @@ class ExtractOIIOTranscode(publish.Extractor): instance.data["representations"].append(new_repre) + def _translate_to_sequence(self, files_to_convert): + """Returns original list of files or single sequence format filename. + + Uses clique to find frame sequence, in this case it merges all frames + into sequence format (%0X) and returns it. + If sequence not found, it returns original list + + Args: + files_to_convert (list): list of file names + Returns: + (list) of [file.%04.exr] or [fileA.exr, fileB.exr] + """ + pattern = [clique.PATTERNS["frames"]] + collections, remainder = clique.assemble( + files_to_convert, patterns=pattern, + assume_padded_when_ambiguous=True) + + if collections: + if len(collections) > 1: + raise ValueError( + "Too many collections {}".format(collections)) + + collection = collections[0] + padding = collection.padding + padding_str = "%0{}".format(padding) + frames = list(collection.indexes) + frame_str = "{}-{}#".format(frames[0], frames[-1]) + file_name = "{}{}{}".format(collection.head, frame_str, + collection.tail) + + files_to_convert = [file_name] + + return files_to_convert + def _get_output_file_path(self, input_filepath, output_dir, output_extension): """Create output file name path.""" From be176bbeb2feb40751be9c208fb4b0dd236f66ae Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 18:54:02 +0100 Subject: [PATCH 384/483] OP-4643 - implemented display and viewer color space --- openpype/lib/transcoding.py | 23 +++++++++++++++++-- .../publish/extract_color_transcode.py | 13 +++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index f22628dd28..cc9cd4e1eb 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1053,6 +1053,8 @@ def convert_colorspace( config_path, source_colorspace, target_colorspace, + view, + display, logger=None ): """Convert source files from one color space to another. @@ -1070,8 +1072,11 @@ def convert_colorspace( config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space + view (str): name for viewer space (ocio valid) + display (str): name for display-referred reference space (ocio valid) logger (logging.Logger): Logger used for logging. - + Raises: + ValueError: if misconfigured """ if logger is None: logger = logging.getLogger(__name__) @@ -1082,9 +1087,23 @@ def convert_colorspace( # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "--colorconvert", source_colorspace, target_colorspace, "-o", out_filepath ] + if all([target_colorspace, view, display]): + raise ValueError("Colorspace and both screen and display" + " cannot be set together." + "Choose colorspace or screen and display") + if not target_colorspace and not all([view, display]): + raise ValueError("Both screen and display must be set.") + + if target_colorspace: + oiio_cmd.extend(["--colorconvert", + source_colorspace, + target_colorspace]) + if view and display: + oiio_cmd.extend(["--iscolorspace", source_colorspace]) + oiio_cmd.extend(["--ociodisplay", display, view]) + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 09c86909cb..cd8421c0cd 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -106,8 +106,15 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["files"] = renamed_files target_colorspace = output_def["colorspace"] - if not target_colorspace: - raise RuntimeError("Target colorspace must be set") + view = output_def["view"] or colorspace_data.get("view") + display = (output_def["display"] or + colorspace_data.get("display")) + # both could be already collected by DCC, + # but could be overwritten + if view: + new_repre["colorspaceData"]["view"] = view + if display: + new_repre["colorspaceData"]["view"] = display files_to_convert = self._translate_to_sequence( files_to_convert) @@ -123,6 +130,8 @@ class ExtractOIIOTranscode(publish.Extractor): config_path, source_colorspace, target_colorspace, + view, + display, self.log ) From 3dba4f3eb14c545038bb340614023f974c2c405a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 26 Jan 2023 19:03:10 +0100 Subject: [PATCH 385/483] OP-4643 - fix wrong order of deletion of representation --- openpype/plugins/publish/extract_color_transcode.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index cd8421c0cd..9cca5cc969 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -69,6 +69,8 @@ class ExtractOIIOTranscode(publish.Extractor): if not self._repre_is_valid(repre): continue + added_representations = False + colorspace_data = repre["colorspaceData"] source_colorspace = colorspace_data["colorspace"] config_path = colorspace_data.get("config", {}).get("path") @@ -76,8 +78,6 @@ class ExtractOIIOTranscode(publish.Extractor): self.log.warning("Config file doesn't exist, skipping") continue - repre = self._handle_original_repre(repre, profile) - for _, output_def in profile.get("outputs", {}).items(): new_repre = copy.deepcopy(repre) @@ -150,6 +150,10 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["tags"].append(tag) instance.data["representations"].append(new_repre) + added_representations = True + + if added_representations: + self._mark_original_repre_for_deletion(repre, profile) def _translate_to_sequence(self, files_to_convert): """Returns original list of files or single sequence format filename. @@ -253,7 +257,8 @@ class ExtractOIIOTranscode(publish.Extractor): return True - def _handle_original_repre(self, repre, profile): + def _mark_original_repre_for_deletion(self, repre, profile): + """If new transcoded representation created, delete old.""" delete_original = profile["delete_original"] if delete_original: @@ -264,5 +269,3 @@ class ExtractOIIOTranscode(publish.Extractor): repre["tags"].remove("review") if "delete" not in repre["tags"]: repre["tags"].append("delete") - - return repre From 7e9d707226dd1956e457e1d29a0d2df58334e26d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 11:26:27 +0100 Subject: [PATCH 386/483] OP-4643 - updated docstring, standardized arguments --- openpype/lib/transcoding.py | 19 +++++++---------- .../publish/extract_color_transcode.py | 21 +++++++++---------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index cc9cd4e1eb..0f6d35affe 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1049,7 +1049,7 @@ def convert_ffprobe_fps_to_float(value): def convert_colorspace( input_path, - out_filepath, + output_path, config_path, source_colorspace, target_colorspace, @@ -1057,18 +1057,13 @@ def convert_colorspace( display, logger=None ): - """Convert source files from one color space to another. - - Filenames of input files are kept so make sure that output directory - is not the same directory as input files have. - - This way it can handle gaps and can keep input filenames without handling - frame template + """Convert source file from one color space to another. Args: - input_path (str): Paths that should be converted. It is expected that - contains single file or image sequence of samy type. - out_filepath (str): Path to directory where output will be rendered. - Must not be same as input's directory. + input_path (str): Path that should be converted. It is expected that + contains single file or image sequence of same type + (sequence in format 'file.FRAMESTART-FRAMEEND#.exr', see oiio docs) + output_path (str): Path to output filename. config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space @@ -1087,7 +1082,7 @@ def convert_colorspace( # Don't add any additional attributes "--nosoftwareattrib", "--colorconfig", config_path, - "-o", out_filepath + "-o", output_path ] if all([target_colorspace, view, display]): diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 9cca5cc969..c4cef15ea6 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -119,13 +119,13 @@ class ExtractOIIOTranscode(publish.Extractor): files_to_convert = self._translate_to_sequence( files_to_convert) for file_name in files_to_convert: - input_filepath = os.path.join(original_staging_dir, - file_name) - output_path = self._get_output_file_path(input_filepath, + input_path = os.path.join(original_staging_dir, + file_name) + output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) convert_colorspace( - input_filepath, + input_path, output_path, config_path, source_colorspace, @@ -156,16 +156,17 @@ class ExtractOIIOTranscode(publish.Extractor): self._mark_original_repre_for_deletion(repre, profile) def _translate_to_sequence(self, files_to_convert): - """Returns original list of files or single sequence format filename. + """Returns original list or list with filename formatted in single + sequence format. Uses clique to find frame sequence, in this case it merges all frames - into sequence format (%0X) and returns it. + into sequence format (FRAMESTART-FRAMEEND#) and returns it. If sequence not found, it returns original list Args: files_to_convert (list): list of file names Returns: - (list) of [file.%04.exr] or [fileA.exr, fileB.exr] + (list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr] """ pattern = [clique.PATTERNS["frames"]] collections, remainder = clique.assemble( @@ -178,8 +179,6 @@ class ExtractOIIOTranscode(publish.Extractor): "Too many collections {}".format(collections)) collection = collections[0] - padding = collection.padding - padding_str = "%0{}".format(padding) frames = list(collection.indexes) frame_str = "{}-{}#".format(frames[0], frames[-1]) file_name = "{}{}{}".format(collection.head, frame_str, @@ -189,10 +188,10 @@ class ExtractOIIOTranscode(publish.Extractor): return files_to_convert - def _get_output_file_path(self, input_filepath, output_dir, + def _get_output_file_path(self, input_path, output_dir, output_extension): """Create output file name path.""" - file_name = os.path.basename(input_filepath) + file_name = os.path.basename(input_path) file_name, input_extension = os.path.splitext(file_name) if not output_extension: output_extension = input_extension From c50d9917a4428270db3cc0797da1c4f941d2022e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 11:27:06 +0100 Subject: [PATCH 387/483] OP-4643 - fix wrong assignment --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index c4cef15ea6..4e899a519c 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -114,7 +114,7 @@ class ExtractOIIOTranscode(publish.Extractor): if view: new_repre["colorspaceData"]["view"] = view if display: - new_repre["colorspaceData"]["view"] = display + new_repre["colorspaceData"]["display"] = display files_to_convert = self._translate_to_sequence( files_to_convert) From f226dc60cf055d836614b540b0eca31611f568ce Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 11:59:13 +0100 Subject: [PATCH 388/483] OP-4643 - fix files to delete --- .../publish/extract_color_transcode.py | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4e899a519c..99e684ba21 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -84,26 +84,18 @@ class ExtractOIIOTranscode(publish.Extractor): original_staging_dir = new_repre["stagingDir"] new_staging_dir = get_transcode_temp_directory() new_repre["stagingDir"] = new_staging_dir - files_to_convert = new_repre["files"] - if not isinstance(files_to_convert, list): - files_to_convert = [files_to_convert] - files_to_delete = copy.deepcopy(files_to_convert) + if isinstance(new_repre["files"], list): + files_to_convert = copy.deepcopy(new_repre["files"]) + else: + files_to_convert = [new_repre["files"]] output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') if output_extension: - if new_repre["name"] == new_repre["ext"]: - new_repre["name"] = output_extension - new_repre["ext"] = output_extension - - renamed_files = [] - _, orig_ext = os.path.splitext(files_to_convert[0]) - for file_name in files_to_convert: - file_name = file_name.replace(orig_ext, - "."+output_extension) - renamed_files.append(file_name) - new_repre["files"] = renamed_files + self._rename_in_representation(new_repre, + files_to_convert, + output_extension) target_colorspace = output_def["colorspace"] view = output_def["view"] or colorspace_data.get("view") @@ -135,8 +127,12 @@ class ExtractOIIOTranscode(publish.Extractor): self.log ) - instance.context.data["cleanupFullPaths"].extend( - files_to_delete) + # cleanup temporary transcoded files + for file_name in new_repre["files"]: + transcoded_file_path = os.path.join(new_staging_dir, + file_name) + instance.context.data["cleanupFullPaths"].append( + transcoded_file_path) custom_tags = output_def.get("custom_tags") if custom_tags: @@ -155,6 +151,21 @@ class ExtractOIIOTranscode(publish.Extractor): if added_representations: self._mark_original_repre_for_deletion(repre, profile) + def _rename_in_representation(self, new_repre, files_to_convert, + output_extension): + """Replace old extension with new one everywhere in representation.""" + if new_repre["name"] == new_repre["ext"]: + new_repre["name"] = output_extension + new_repre["ext"] = output_extension + + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(file_name) + new_repre["files"] = renamed_files + def _translate_to_sequence(self, files_to_convert): """Returns original list or list with filename formatted in single sequence format. From 97a2014c125d7b8f766754e14ac4d74889a5f469 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 13:17:59 +0100 Subject: [PATCH 389/483] OP-4643 - moved output argument to the end --- openpype/lib/transcoding.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 0f6d35affe..e74dab4ccc 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1081,8 +1081,7 @@ def convert_colorspace( input_path, # Don't add any additional attributes "--nosoftwareattrib", - "--colorconfig", config_path, - "-o", output_path + "--colorconfig", config_path ] if all([target_colorspace, view, display]): @@ -1100,5 +1099,7 @@ def convert_colorspace( oiio_cmd.extend(["--iscolorspace", source_colorspace]) oiio_cmd.extend(["--ociodisplay", display, view]) + oiio_cmd.extend(["-o", output_path]) + logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) From 3d2f4319369d6459263cd79e69bec3898ee86efa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 13:18:33 +0100 Subject: [PATCH 390/483] OP-4643 - fix no tags in repre --- openpype/plugins/publish/extract_color_transcode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 99e684ba21..3d897c6d9f 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -142,6 +142,8 @@ class ExtractOIIOTranscode(publish.Extractor): # Add additional tags from output definition to representation for tag in output_def["tags"]: + if not new_repre.get("tags"): + new_repre["tags"] = [] if tag not in new_repre["tags"]: new_repre["tags"].append(tag) From 016111ab3381e2ba6c9b5b47fe361a25208c1a6b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 13:26:07 +0100 Subject: [PATCH 391/483] OP-4643 - changed docstring Elaborated more that 'target_colorspace' and ('view', 'display') are disjunctive. --- openpype/lib/transcoding.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index e74dab4ccc..f7d5e222c8 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1053,8 +1053,8 @@ def convert_colorspace( config_path, source_colorspace, target_colorspace, - view, - display, + view=None, + display=None, logger=None ): """Convert source file from one color space to another. @@ -1067,7 +1067,9 @@ def convert_colorspace( config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space + if filled, 'view' and 'display' must be empty view (str): name for viewer space (ocio valid) + both 'view' and 'display' must be filled (if 'target_colorspace') display (str): name for display-referred reference space (ocio valid) logger (logging.Logger): Logger used for logging. Raises: From e1d68ec387572f180844ca8677110cc32d8cf9df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 11:14:07 +0100 Subject: [PATCH 392/483] OP-4663 - fix double dots in extension Co-authored-by: Toke Jepsen --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 3d897c6d9f..bfed69c300 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -207,7 +207,7 @@ class ExtractOIIOTranscode(publish.Extractor): file_name = os.path.basename(input_path) file_name, input_extension = os.path.splitext(file_name) if not output_extension: - output_extension = input_extension + output_extension = input_extension.replace(".", "") new_file_name = '{}.{}'.format(file_name, output_extension) return os.path.join(output_dir, new_file_name) From b5246cdf6587975cec5638666e82196e84854de3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 16:11:45 +0100 Subject: [PATCH 393/483] OP-4643 - update documentation in Settings schema --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 74b81b13af..3956f403f4 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -207,7 +207,7 @@ "children": [ { "type": "label", - "label": "Configure output format(s) and color spaces for matching representations. Empty 'Output extension' denotes keeping source extension." + "label": "Configure Output Definition(s) for new representation(s). \nEmpty 'Extension' denotes keeping source extension. \nName(key) of output definition will be used as new representation name \nunless 'passthrough' value is used to keep existing name. \nFill either 'Colorspace' (for target colorspace) or \nboth 'Display' and 'View' (for display and viewer colorspaces)." }, { "type": "boolean", From b4085288c34f0a035f5be5a536bb6cfdcc4f1a2c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 16:13:59 +0100 Subject: [PATCH 394/483] OP-4643 - name of new representation from output definition key --- .../publish/extract_color_transcode.py | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index bfed69c300..e39ea3add9 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -32,6 +32,25 @@ class ExtractOIIOTranscode(publish.Extractor): - task types - task names - subset names + + Can produce one or more representations (with different extensions) based + on output definition in format: + "output_name: { + "extension": "png", + "colorspace": "ACES - ACEScg", + "display": "", + "view": "", + "tags": [], + "custom_tags": [] + } + + If 'extension' is empty original representation extension is used. + 'output_name' will be used as name of new representation. In case of value + 'passthrough' name of original representation will be used. + + 'colorspace' denotes target colorspace to be transcoded into. Could be + empty if transcoding should be only into display and viewer colorspace. + (In that case both 'display' and 'view' must be filled.) """ label = "Transcode color spaces" @@ -78,7 +97,7 @@ class ExtractOIIOTranscode(publish.Extractor): self.log.warning("Config file doesn't exist, skipping") continue - for _, output_def in profile.get("outputs", {}).items(): + for output_name, output_def in profile.get("outputs", {}).items(): new_repre = copy.deepcopy(repre) original_staging_dir = new_repre["stagingDir"] @@ -92,10 +111,10 @@ class ExtractOIIOTranscode(publish.Extractor): output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') - if output_extension: - self._rename_in_representation(new_repre, - files_to_convert, - output_extension) + self._rename_in_representation(new_repre, + files_to_convert, + output_name, + output_extension) target_colorspace = output_def["colorspace"] view = output_def["view"] or colorspace_data.get("view") @@ -154,10 +173,22 @@ class ExtractOIIOTranscode(publish.Extractor): self._mark_original_repre_for_deletion(repre, profile) def _rename_in_representation(self, new_repre, files_to_convert, - output_extension): - """Replace old extension with new one everywhere in representation.""" - if new_repre["name"] == new_repre["ext"]: - new_repre["name"] = output_extension + output_name, output_extension): + """Replace old extension with new one everywhere in representation. + + Args: + new_repre (dict) + files_to_convert (list): of filenames from repre["files"], + standardized to always list + output_name (str): key of output definition from Settings, + if "" token used, keep original repre name + output_extension (str): extension from output definition + """ + if output_name != "passthrough": + new_repre["name"] = output_name + if not output_extension: + return + new_repre["ext"] = output_extension renamed_files = [] From 925c7a9564fa1e2c8cc61d025de35d75af155939 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 16:42:48 +0100 Subject: [PATCH 395/483] OP-4643 - updated docstring for convert_colorspace --- openpype/lib/transcoding.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index f7d5e222c8..b6edd863f8 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1062,8 +1062,11 @@ def convert_colorspace( Args: input_path (str): Path that should be converted. It is expected that contains single file or image sequence of same type - (sequence in format 'file.FRAMESTART-FRAMEEND#.exr', see oiio docs) + (sequence in format 'file.FRAMESTART-FRAMEEND#.ext', see oiio docs, + eg `big.1-3#.tif`) output_path (str): Path to output filename. + (must follow format of 'input_path', eg. single file or + sequence in 'file.FRAMESTART-FRAMEEND#.ext', `output.1-3#.tif`) config_path (str): path to OCIO config file source_colorspace (str): ocio valid color space of source files target_colorspace (str): ocio valid target color space From d96775867a333566ff0681dbdb86688c3bda7f3d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Feb 2023 18:22:10 +0100 Subject: [PATCH 396/483] OP-4643 - remove review from old representation If new representation gets created and adds 'review' tag it becomes new reviewable representation. --- .../publish/extract_color_transcode.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index e39ea3add9..d10b887a0b 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -89,6 +89,7 @@ class ExtractOIIOTranscode(publish.Extractor): continue added_representations = False + added_review = False colorspace_data = repre["colorspaceData"] source_colorspace = colorspace_data["colorspace"] @@ -166,11 +167,15 @@ class ExtractOIIOTranscode(publish.Extractor): if tag not in new_repre["tags"]: new_repre["tags"].append(tag) + if tag == "review": + added_review = True + instance.data["representations"].append(new_repre) added_representations = True if added_representations: - self._mark_original_repre_for_deletion(repre, profile) + self._mark_original_repre_for_deletion(repre, profile, + added_review) def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): @@ -300,15 +305,16 @@ class ExtractOIIOTranscode(publish.Extractor): return True - def _mark_original_repre_for_deletion(self, repre, profile): + def _mark_original_repre_for_deletion(self, repre, profile, added_review): """If new transcoded representation created, delete old.""" + if not repre.get("tags"): + repre["tags"] = [] + delete_original = profile["delete_original"] if delete_original: - if not repre.get("tags"): - repre["tags"] = [] - - if "review" in repre["tags"]: - repre["tags"].remove("review") if "delete" not in repre["tags"]: repre["tags"].append("delete") + + if added_review and "review" in repre["tags"]: + repre["tags"].remove("review") From 7540f61791958b6043ad1a466900a41fc8b27e4c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 7 Feb 2023 18:23:42 +0100 Subject: [PATCH 397/483] OP-4643 - remove representation that should be deleted Or old revieable representation would be reviewed too. --- openpype/plugins/publish/extract_color_transcode.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index d10b887a0b..93ee1ec44d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -177,6 +177,11 @@ class ExtractOIIOTranscode(publish.Extractor): self._mark_original_repre_for_deletion(repre, profile, added_review) + for repre in tuple(instance.data["representations"]): + tags = repre.get("tags") or [] + if "delete" in tags and "thumbnail" not in tags: + instance.data["representations"].remove(repre) + def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): """Replace old extension with new one everywhere in representation. From 82b44da739625242d4e2a0ffddec317cad25e806 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Feb 2023 14:54:25 +0100 Subject: [PATCH 398/483] OP-4643 - fix logging Wrong variable used --- openpype/plugins/publish/extract_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index dcb43d7fa2..0f6dacba18 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -169,7 +169,7 @@ class ExtractReview(pyblish.api.InstancePlugin): "Skipped representation. All output definitions from" " selected profile does not match to representation's" " custom tags. \"{}\"" - ).format(str(tags))) + ).format(str(custom_tags))) continue outputs_per_representations.append((repre, outputs)) From 1d12316ee18889a2b18e02db06edf082e6a61d70 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Feb 2023 15:14:14 +0100 Subject: [PATCH 399/483] OP-4643 - allow new repre to stay One might want to delete outputs with 'delete' tag, but repre must stay there at least until extract_review. More universal new tag might be created for this. --- openpype/plugins/publish/extract_color_transcode.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 93ee1ec44d..4a03e623fd 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -161,15 +161,17 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["custom_tags"].extend(custom_tags) # Add additional tags from output definition to representation + if not new_repre.get("tags"): + new_repre["tags"] = [] for tag in output_def["tags"]: - if not new_repre.get("tags"): - new_repre["tags"] = [] if tag not in new_repre["tags"]: new_repre["tags"].append(tag) if tag == "review": added_review = True + new_repre["tags"].append("newly_added") + instance.data["representations"].append(new_repre) added_representations = True @@ -179,6 +181,12 @@ class ExtractOIIOTranscode(publish.Extractor): for repre in tuple(instance.data["representations"]): tags = repre.get("tags") or [] + # TODO implement better way, for now do not delete new repre + # new repre might have 'delete' tag to removed, but it first must + # be there for review to be created + if "newly_added" in tags: + tags.remove("newly_added") + continue if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) From b5c3e0931e0dc1d13d09cd2b8579e1fc86b0169e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:04:59 +0100 Subject: [PATCH 400/483] OP-4642 - added additional command arguments to Settings --- .../schemas/schema_global_publish.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 3956f403f4..5333d514b5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -286,6 +286,20 @@ "label": "View", "type": "text" }, + { + "key": "oiiotool_args", + "label": "OIIOtool arguments", + "type": "dict", + "highlight_content": true, + "children": [ + { + "key": "additional_command_args", + "label": "Additional command line arguments", + "type": "list", + "object_type": "text" + } + ] + }, { "type": "schema", "name": "schema_representation_tags" From 3921982365792bb92912777c0cc130462c0e6e14 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:08:06 +0100 Subject: [PATCH 401/483] OP-4642 - added additional command arguments for oiiotool Some extension requires special command line arguments (.dpx and binary depth). --- openpype/lib/transcoding.py | 6 ++++++ openpype/plugins/publish/extract_color_transcode.py | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index b6edd863f8..982cee7a46 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1055,6 +1055,7 @@ def convert_colorspace( target_colorspace, view=None, display=None, + additional_command_args=None, logger=None ): """Convert source file from one color space to another. @@ -1074,6 +1075,8 @@ def convert_colorspace( view (str): name for viewer space (ocio valid) both 'view' and 'display' must be filled (if 'target_colorspace') display (str): name for display-referred reference space (ocio valid) + additional_command_args (list): arguments for oiiotool (like binary + depth for .dpx) logger (logging.Logger): Logger used for logging. Raises: ValueError: if misconfigured @@ -1096,6 +1099,9 @@ def convert_colorspace( if not target_colorspace and not all([view, display]): raise ValueError("Both screen and display must be set.") + if additional_command_args: + oiio_cmd.extend(additional_command_args) + if target_colorspace: oiio_cmd.extend(["--colorconvert", source_colorspace, diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4a03e623fd..3de404125d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -128,6 +128,9 @@ class ExtractOIIOTranscode(publish.Extractor): if display: new_repre["colorspaceData"]["display"] = display + additional_command_args = (output_def["oiiotool_args"] + ["additional_command_args"]) + files_to_convert = self._translate_to_sequence( files_to_convert) for file_name in files_to_convert: @@ -144,6 +147,7 @@ class ExtractOIIOTranscode(publish.Extractor): target_colorspace, view, display, + additional_command_args, self.log ) From 0834b7564b842832cba47a80a2b8c933d8bce918 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:21:25 +0100 Subject: [PATCH 402/483] OP-4642 - refactored newly added representations --- openpype/plugins/publish/extract_color_transcode.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 3de404125d..8c4ef59de9 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -82,6 +82,7 @@ class ExtractOIIOTranscode(publish.Extractor): if not profile: return + new_representations = [] repres = instance.data.get("representations") or [] for idx, repre in enumerate(list(repres)): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) @@ -174,9 +175,7 @@ class ExtractOIIOTranscode(publish.Extractor): if tag == "review": added_review = True - new_repre["tags"].append("newly_added") - - instance.data["representations"].append(new_repre) + new_representations.append(new_repre) added_representations = True if added_representations: @@ -185,15 +184,11 @@ class ExtractOIIOTranscode(publish.Extractor): for repre in tuple(instance.data["representations"]): tags = repre.get("tags") or [] - # TODO implement better way, for now do not delete new repre - # new repre might have 'delete' tag to removed, but it first must - # be there for review to be created - if "newly_added" in tags: - tags.remove("newly_added") - continue if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) + instance.data["representations"].extend(new_representations) + def _rename_in_representation(self, new_repre, files_to_convert, output_name, output_extension): """Replace old extension with new one everywhere in representation. From 263d3dccc2bb2f2ddc3d18d725a9d16101404cbb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:24:53 +0100 Subject: [PATCH 403/483] OP-4642 - refactored query of representations line 73 returns if no representations. --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 8c4ef59de9..de36ea7d5f 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -83,7 +83,7 @@ class ExtractOIIOTranscode(publish.Extractor): return new_representations = [] - repres = instance.data.get("representations") or [] + repres = instance.data["representations"] for idx, repre in enumerate(list(repres)): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) if not self._repre_is_valid(repre): From cf066d1441d5d356e4be59591aa8fadfc24bcd0b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 10:44:10 +0100 Subject: [PATCH 404/483] OP-4643 - fixed subset filtering Co-authored-by: Toke Jepsen --- openpype/plugins/publish/extract_color_transcode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index de36ea7d5f..71124b527a 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -273,7 +273,7 @@ class ExtractOIIOTranscode(publish.Extractor): "families": family, "task_names": task_name, "task_types": task_type, - "subset": subset + "subsets": subset } profile = filter_profiles(self.profiles, filtering_criteria, logger=self.log) From 984974d7e01a48ebcd704e167c1e3fef42418d82 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 12:12:35 +0100 Subject: [PATCH 405/483] OP-4643 - split command line arguments to separate items Reuse existing method from ExtractReview, put it into transcoding.py --- openpype/lib/transcoding.py | 29 +++++++++++++++++++++- openpype/plugins/publish/extract_review.py | 27 +++----------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 982cee7a46..4d2f72fc41 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1100,7 +1100,7 @@ def convert_colorspace( raise ValueError("Both screen and display must be set.") if additional_command_args: - oiio_cmd.extend(additional_command_args) + oiio_cmd.extend(split_cmd_args(additional_command_args)) if target_colorspace: oiio_cmd.extend(["--colorconvert", @@ -1114,3 +1114,30 @@ def convert_colorspace( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) + + +def split_cmd_args(in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + Args: + in_args (list): of arguments ['-n', '-d uint10'] + Returns + (list): ['-n', '-d', 'unint10'] + """ + splitted_args = [] + for arg in in_args: + sub_args = arg.split(" -") + if len(sub_args) == 1: + if arg and arg not in splitted_args: + splitted_args.append(arg) + continue + + for idx, arg in enumerate(sub_args): + if idx != 0: + arg = "-" + arg + + if arg and arg not in splitted_args: + splitted_args.append(arg) + return splitted_args diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 0f6dacba18..e80141fc4a 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -22,6 +22,7 @@ from openpype.lib.transcoding import ( should_convert_for_ffmpeg, convert_input_paths_for_ffmpeg, get_transcode_temp_directory, + split_cmd_args ) @@ -670,7 +671,7 @@ class ExtractReview(pyblish.api.InstancePlugin): res_filters = self.rescaling_filters(temp_data, output_def, new_repre) ffmpeg_video_filters.extend(res_filters) - ffmpeg_input_args = self.split_ffmpeg_args(ffmpeg_input_args) + ffmpeg_input_args = split_cmd_args(ffmpeg_input_args) lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args) ffmpeg_video_filters.extend(lut_filters) @@ -723,28 +724,6 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_output_args ) - def split_ffmpeg_args(self, in_args): - """Makes sure all entered arguments are separated in individual items. - - Split each argument string with " -" to identify if string contains - one or more arguments. - """ - splitted_args = [] - for arg in in_args: - sub_args = arg.split(" -") - if len(sub_args) == 1: - if arg and arg not in splitted_args: - splitted_args.append(arg) - continue - - for idx, arg in enumerate(sub_args): - if idx != 0: - arg = "-" + arg - - if arg and arg not in splitted_args: - splitted_args.append(arg) - return splitted_args - def ffmpeg_full_args( self, input_args, video_filters, audio_filters, output_args ): @@ -764,7 +743,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Returns: list: Containing all arguments ready to run in subprocess. """ - output_args = self.split_ffmpeg_args(output_args) + output_args = split_cmd_args(output_args) video_args_dentifiers = ["-vf", "-filter:v"] audio_args_dentifiers = ["-af", "-filter:a"] From 51c54e1aa1bf602fe4cc2ba0ff1110dcfe5f5539 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 13:02:41 +0100 Subject: [PATCH 406/483] OP-4643 - refactor - changed existence check --- openpype/plugins/publish/extract_color_transcode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 71124b527a..456e40008d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -161,12 +161,12 @@ class ExtractOIIOTranscode(publish.Extractor): custom_tags = output_def.get("custom_tags") if custom_tags: - if not new_repre.get("custom_tags"): + if new_repre.get("custom_tags") is None: new_repre["custom_tags"] = [] new_repre["custom_tags"].extend(custom_tags) # Add additional tags from output definition to representation - if not new_repre.get("tags"): + if new_repre.get("tags") is None: new_repre["tags"] = [] for tag in output_def["tags"]: if tag not in new_repre["tags"]: From cb551fe83acde2ea43e61706273bf33fec1c37d3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 13:11:11 +0100 Subject: [PATCH 407/483] Revert "Fix - added missed scopes for Slack bot" This reverts commit 5e0c4a3ab1432e120b8f0c324f899070f1a5f831. --- openpype/modules/slack/manifest.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml index 233c39fbaf..7a65cc5915 100644 --- a/openpype/modules/slack/manifest.yml +++ b/openpype/modules/slack/manifest.yml @@ -19,8 +19,6 @@ oauth_config: - chat:write.public - files:write - channels:read - - users:read - - usergroups:read settings: org_deploy_enabled: false socket_mode_enabled: false From e2ebbae14772b4f24b169c104cdabfcabfab6b66 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 13:10:11 +0100 Subject: [PATCH 408/483] OP-4643 - changed label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- .../schemas/projects_schema/schemas/schema_global_publish.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 5333d514b5..3e9467af61 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -294,7 +294,7 @@ "children": [ { "key": "additional_command_args", - "label": "Additional command line arguments", + "label": "Arguments", "type": "list", "object_type": "text" } From 4e755e193a6c1e6f2d074d98d2684b3e18cf5282 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 15:06:16 +0100 Subject: [PATCH 409/483] OP-4643 - added documentation --- .../assets/global_oiio_transcode.png | Bin 0 -> 29010 bytes .../project_settings/settings_project_global.md | 15 +++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 website/docs/project_settings/assets/global_oiio_transcode.png diff --git a/website/docs/project_settings/assets/global_oiio_transcode.png b/website/docs/project_settings/assets/global_oiio_transcode.png new file mode 100644 index 0000000000000000000000000000000000000000..99396d5bb3f16d434a92c6079515128488b82489 GIT binary patch literal 29010 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 literal 0 HcmV?d00001 diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 37fed93e69..52671d2db6 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -45,6 +45,21 @@ The _input pattern_ matching uses REGEX expression syntax (try [regexr.com](http The **colorspace name** value is a raw string input and no validation is run after saving project settings. We recommend to open the specified `config.ocio` file and copy pasting the exact colorspace names. ::: +### Extract OIIO Transcode +There is profile configurable (see lower) plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. +Plugin expects instances with filled dictionary `colorspaceData` on a representation. This data contains information about source colorspace and must be collected for transcoding. +`oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. + +Notable parameters: +- **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. +- **`Extension`** - target extension, could be empty - original extension is used +- **`Colorspace`** - target colorspace - must be available in used color config +- **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) +- **`Arguments`** - special additional command line arguments for `oiiotool` + + +Example here describes use case for creation of new color coded review of png image sequence. Original representation's files are kept intact, review is created from transcoded files, but these files are removed in cleanup process. +![global_oiio_transcode](assets/global_oiio_transcode.png) ## Profile filters From 54e92f02b9f1a31fd9b05005864b6f1e8e5b99ff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:41:42 +0100 Subject: [PATCH 410/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 52671d2db6..cc661a21fa 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -46,7 +46,7 @@ The **colorspace name** value is a raw string input and no validation is run aft ::: ### Extract OIIO Transcode -There is profile configurable (see lower) plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. +There is profile configurable plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. Plugin expects instances with filled dictionary `colorspaceData` on a representation. This data contains information about source colorspace and must be collected for transcoding. `oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. From 665132d9feb19a6d566e2541ccb1207bef1dcc53 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:42:06 +0100 Subject: [PATCH 411/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index cc661a21fa..8e557a381c 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -52,7 +52,7 @@ Plugin expects instances with filled dictionary `colorspaceData` on a representa Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. -- **`Extension`** - target extension, could be empty - original extension is used +- **`Extension`** - target extension. If left empty, original extension is used. - **`Colorspace`** - target colorspace - must be available in used color config - **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) - **`Arguments`** - special additional command line arguments for `oiiotool` From ceca9a5bb89c8da7915d7c4772cbd23448b49714 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:42:29 +0100 Subject: [PATCH 412/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 8e557a381c..166400cb7f 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -53,7 +53,7 @@ Plugin expects instances with filled dictionary `colorspaceData` on a representa Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. -- **`Colorspace`** - target colorspace - must be available in used color config +- **`Colorspace`** - target colorspace, which must be available in used color config. - **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) - **`Arguments`** - special additional command line arguments for `oiiotool` From 0a4cae9db2eaf3beea496e57065240a7b4eec1c1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:42:48 +0100 Subject: [PATCH 413/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 166400cb7f..908191f122 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -54,7 +54,7 @@ Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. - **`Colorspace`** - target colorspace, which must be available in used color config. -- **`Display & View`** - transcoding into colorspace OR into display and viewer space could be used. (It is disjunctive: Colorspace & nothing in Display and View or opposite) +- **`Display & View`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both `Colorspace` and `Display & View` at the same time. - **`Arguments`** - special additional command line arguments for `oiiotool` From 3a0e9dc78ce48bf9d964eed90b5afea24853e8a7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 16:43:06 +0100 Subject: [PATCH 414/483] OP-4643 - updates to documentation Co-authored-by: Toke Jepsen --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 908191f122..0a73868d2d 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -55,7 +55,7 @@ Notable parameters: - **`Extension`** - target extension. If left empty, original extension is used. - **`Colorspace`** - target colorspace, which must be available in used color config. - **`Display & View`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both `Colorspace` and `Display & View` at the same time. -- **`Arguments`** - special additional command line arguments for `oiiotool` +- **`Arguments`** - special additional command line arguments for `oiiotool`. Example here describes use case for creation of new color coded review of png image sequence. Original representation's files are kept intact, review is created from transcoded files, but these files are removed in cleanup process. From 7f8a766c6a294991c379a356e63ab3b2fd9e53a7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 17:21:20 +0100 Subject: [PATCH 415/483] OP-4643 - updates to documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Ježek --- website/docs/project_settings/settings_project_global.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 0a73868d2d..9e2ee187cc 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -46,8 +46,8 @@ The **colorspace name** value is a raw string input and no validation is run aft ::: ### Extract OIIO Transcode -There is profile configurable plugin which allows to transcode any incoming representation to one or multiple new representations (configured in `Output Definitions`) with different target colorspaces. -Plugin expects instances with filled dictionary `colorspaceData` on a representation. This data contains information about source colorspace and must be collected for transcoding. +OIIOTools transcoder plugin with configurable output presets. Any incoming representation with `colorspaceData` is convertable to single or multiple representations with different target colorspaces or display and viewer names found in linked **config.ocio** file. + `oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. Notable parameters: From 559d54c3a1b061f3234b0491f6abbb8b22aee6c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 17:56:21 +0100 Subject: [PATCH 416/483] Revert "OP-4643 - split command line arguments to separate items" This reverts commit deaad39437501f18fc3ba4be8b1fc5f0ee3be65d. --- openpype/lib/transcoding.py | 29 +--------------------- openpype/plugins/publish/extract_review.py | 27 +++++++++++++++++--- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 4d2f72fc41..982cee7a46 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1100,7 +1100,7 @@ def convert_colorspace( raise ValueError("Both screen and display must be set.") if additional_command_args: - oiio_cmd.extend(split_cmd_args(additional_command_args)) + oiio_cmd.extend(additional_command_args) if target_colorspace: oiio_cmd.extend(["--colorconvert", @@ -1114,30 +1114,3 @@ def convert_colorspace( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) - - -def split_cmd_args(in_args): - """Makes sure all entered arguments are separated in individual items. - - Split each argument string with " -" to identify if string contains - one or more arguments. - Args: - in_args (list): of arguments ['-n', '-d uint10'] - Returns - (list): ['-n', '-d', 'unint10'] - """ - splitted_args = [] - for arg in in_args: - sub_args = arg.split(" -") - if len(sub_args) == 1: - if arg and arg not in splitted_args: - splitted_args.append(arg) - continue - - for idx, arg in enumerate(sub_args): - if idx != 0: - arg = "-" + arg - - if arg and arg not in splitted_args: - splitted_args.append(arg) - return splitted_args diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index e80141fc4a..0f6dacba18 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -22,7 +22,6 @@ from openpype.lib.transcoding import ( should_convert_for_ffmpeg, convert_input_paths_for_ffmpeg, get_transcode_temp_directory, - split_cmd_args ) @@ -671,7 +670,7 @@ class ExtractReview(pyblish.api.InstancePlugin): res_filters = self.rescaling_filters(temp_data, output_def, new_repre) ffmpeg_video_filters.extend(res_filters) - ffmpeg_input_args = split_cmd_args(ffmpeg_input_args) + ffmpeg_input_args = self.split_ffmpeg_args(ffmpeg_input_args) lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args) ffmpeg_video_filters.extend(lut_filters) @@ -724,6 +723,28 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_output_args ) + def split_ffmpeg_args(self, in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + """ + splitted_args = [] + for arg in in_args: + sub_args = arg.split(" -") + if len(sub_args) == 1: + if arg and arg not in splitted_args: + splitted_args.append(arg) + continue + + for idx, arg in enumerate(sub_args): + if idx != 0: + arg = "-" + arg + + if arg and arg not in splitted_args: + splitted_args.append(arg) + return splitted_args + def ffmpeg_full_args( self, input_args, video_filters, audio_filters, output_args ): @@ -743,7 +764,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Returns: list: Containing all arguments ready to run in subprocess. """ - output_args = split_cmd_args(output_args) + output_args = self.split_ffmpeg_args(output_args) video_args_dentifiers = ["-vf", "-filter:v"] audio_args_dentifiers = ["-af", "-filter:a"] From 5678bdbf065922d1e065782ce419149bdd29cbae Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 18:02:17 +0100 Subject: [PATCH 417/483] OP-4643 - different splitting for oiio It seems that logic in ExtractReview does different thing. --- openpype/lib/transcoding.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 982cee7a46..376297ff32 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1100,7 +1100,7 @@ def convert_colorspace( raise ValueError("Both screen and display must be set.") if additional_command_args: - oiio_cmd.extend(additional_command_args) + oiio_cmd.extend(split_cmd_args(additional_command_args)) if target_colorspace: oiio_cmd.extend(["--colorconvert", @@ -1114,3 +1114,21 @@ def convert_colorspace( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) + + +def split_cmd_args(in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + Args: + in_args (list): of arguments ['-n', '-d uint10'] + Returns + (list): ['-n', '-d', 'unint10'] + """ + splitted_args = [] + for arg in in_args: + if not arg.strip(): + continue + splitted_args.extend(arg.split(" ")) + return splitted_args From f30b3c52307e4db5e6715a56ba9532c89dcfceb8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 18:14:57 +0100 Subject: [PATCH 418/483] OP-4643 - allow colorspace to be empty and collected from DCC --- openpype/plugins/publish/extract_color_transcode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 456e40008d..82b92ec93e 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -118,7 +118,8 @@ class ExtractOIIOTranscode(publish.Extractor): output_name, output_extension) - target_colorspace = output_def["colorspace"] + target_colorspace = (output_def["colorspace"] or + colorspace_data.get("colorspace")) view = output_def["view"] or colorspace_data.get("view") display = (output_def["display"] or colorspace_data.get("display")) From 7ecf6fde48aebc705f13ac965e2a9819239b2c87 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 12:22:53 +0100 Subject: [PATCH 419/483] OP-4643 - fix colorspace from DCC representation["colorspaceData"]["colorspace"] is only input colorspace --- openpype/plugins/publish/extract_color_transcode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 82b92ec93e..456e40008d 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -118,8 +118,7 @@ class ExtractOIIOTranscode(publish.Extractor): output_name, output_extension) - target_colorspace = (output_def["colorspace"] or - colorspace_data.get("colorspace")) + target_colorspace = output_def["colorspace"] view = output_def["view"] or colorspace_data.get("view") display = (output_def["display"] or colorspace_data.get("display")) From 945f1dfe55ad1f150cf0f71baacaea80857a0e4e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 17:33:20 +0100 Subject: [PATCH 420/483] OP-4643 - added explicit enum for transcoding type As transcoding info (colorspace, display) might be collected from DCC, it must be explicit which should be used. --- openpype/lib/transcoding.py | 2 +- .../plugins/publish/extract_color_transcode.py | 15 +++++++++++---- .../schemas/schema_global_publish.json | 9 +++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index 376297ff32..c0bda2aa37 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1052,7 +1052,7 @@ def convert_colorspace( output_path, config_path, source_colorspace, - target_colorspace, + target_colorspace=None, view=None, display=None, additional_command_args=None, diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 456e40008d..b0921688e9 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -118,10 +118,17 @@ class ExtractOIIOTranscode(publish.Extractor): output_name, output_extension) - target_colorspace = output_def["colorspace"] - view = output_def["view"] or colorspace_data.get("view") - display = (output_def["display"] or - colorspace_data.get("display")) + transcoding_type = output_def["transcoding_type"] + + target_colorspace = view = display = None + if transcoding_type == "colorspace": + target_colorspace = (output_def["colorspace"] or + colorspace_data.get("colorspace")) + else: + view = output_def["view"] or colorspace_data.get("view") + display = (output_def["display"] or + colorspace_data.get("display")) + # both could be already collected by DCC, # but could be overwritten if view: diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json index 3e9467af61..76574e8b9b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_global_publish.json @@ -271,6 +271,15 @@ "label": "Extension", "type": "text" }, + { + "type": "enum", + "key": "transcoding_type", + "label": "Transcoding type", + "enum_items": [ + { "colorspace": "Use Colorspace" }, + { "display": "Use Display&View" } + ] + }, { "key": "colorspace", "label": "Colorspace", From 25fb38bd4c210770c711d725ed1b88f0c1b8731b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 17:34:03 +0100 Subject: [PATCH 421/483] OP-4643 - added explicit enum for transcoding type As transcoding info (colorspace, display) might be collected from DCC, it must be explicit which should be used. --- .../assets/global_oiio_transcode.png | Bin 29010 -> 17936 bytes .../settings_project_global.md | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/website/docs/project_settings/assets/global_oiio_transcode.png b/website/docs/project_settings/assets/global_oiio_transcode.png index 99396d5bb3f16d434a92c6079515128488b82489..d818ecfe19f93366e5f4664734d68c7b448d7b4f 100644 GIT binary patch literal 17936 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 DbV~re literal 29010 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 diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 9e2ee187cc..6e78ee5d45 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -53,8 +53,9 @@ OIIOTools transcoder plugin with configurable output presets. Any incoming repre Notable parameters: - **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. -- **`Colorspace`** - target colorspace, which must be available in used color config. -- **`Display & View`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both `Colorspace` and `Display & View` at the same time. +- **`Transcoding type`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both at the same time. +- **`Colorspace`** - target colorspace, which must be available in used color config. (If `Transcoding type` is `Use Colorspace` value in configuration is used OR if empty value collected on instance from DCC). +- **`Display & View`** - display and viewer colorspace. (If `Transcoding type` is `Use Display&View` values in configuration is used OR if empty values collected on instance from DCC). - **`Arguments`** - special additional command line arguments for `oiiotool`. From b8f8fd9a5a5342376b28b18f4a9d394ddf3d6b42 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Feb 2023 17:42:07 +0100 Subject: [PATCH 422/483] OP-4643 - added use case for Maya to documentation --- .../assets/global_oiio_transcode2.png | Bin 0 -> 17960 bytes .../project_settings/settings_project_global.md | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 website/docs/project_settings/assets/global_oiio_transcode2.png diff --git a/website/docs/project_settings/assets/global_oiio_transcode2.png b/website/docs/project_settings/assets/global_oiio_transcode2.png new file mode 100644 index 0000000000000000000000000000000000000000..906f780830a96b4bc6f26d98484dd3f9cc3885f2 GIT binary patch literal 17960 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 F{{cSxQDp!C literal 0 HcmV?d00001 diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index 6e78ee5d45..f58d2c2bf2 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -62,6 +62,9 @@ Notable parameters: Example here describes use case for creation of new color coded review of png image sequence. Original representation's files are kept intact, review is created from transcoded files, but these files are removed in cleanup process. ![global_oiio_transcode](assets/global_oiio_transcode.png) +Another use case is to transcode in Maya only `beauty` render layers and use collected `Display` and `View` colorspaces from DCC. +![global_oiio_transcode_in_Maya](assets/global_oiio_transcode.png) + ## Profile filters Many of the settings are using a concept of **Profile filters** From 539ba60eb4c21e310e716a806683acbc7a0284a5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Feb 2023 11:05:26 +0100 Subject: [PATCH 423/483] OP-4643 - updates to documentation Co-authored-by: Roy Nieterau --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index f58d2c2bf2..d904080ad1 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -51,7 +51,7 @@ OIIOTools transcoder plugin with configurable output presets. Any incoming repre `oiiotool` is used for transcoding, eg. `oiiotool` must be present in `vendor/bin/oiio` or environment variable `OPENPYPE_OIIO_PATHS` must be provided for custom oiio installation. Notable parameters: -- **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation looses its 'review' tag if present. +- **`Delete Original Representation`** - keep or remove original representation. If old representation is kept, but there is new transcoded representation with 'Create review' tag, original representation loses its 'review' tag if present. - **`Extension`** - target extension. If left empty, original extension is used. - **`Transcoding type`** - transcoding into colorspace or into display and viewer space could be used. Cannot use both at the same time. - **`Colorspace`** - target colorspace, which must be available in used color config. (If `Transcoding type` is `Use Colorspace` value in configuration is used OR if empty value collected on instance from DCC). From 6ad3421f7f4c6aa7d3cd21d7e63dca19df3d8e41 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 22 Feb 2023 15:40:55 +0100 Subject: [PATCH 424/483] improving deprecation --- openpype/hosts/nuke/api/lib.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 0325838e78..b13c592fbf 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -214,8 +214,9 @@ def update_node_data(node, knobname, data): knob.setValue(knob_value) +@deprecated class Knobby(object): - """[DEPRICATED] For creating knob which it's type isn't + """[DEPRECATED] For creating knob which it's type isn't mapped in `create_knobs` Args: @@ -248,8 +249,9 @@ class Knobby(object): return " ".join(words) +@deprecated def create_knobs(data, tab=None): - """[DEPRICATED] Create knobs by data + """[DEPRECATED] Create knobs by data Depending on the type of each dict value and creates the correct Knob. @@ -342,8 +344,9 @@ def create_knobs(data, tab=None): return knobs +@deprecated def imprint(node, data, tab=None): - """[DEPRICATED] Store attributes with value on node + """[DEPRECATED] Store attributes with value on node Parse user data into Node knobs. Use `collections.OrderedDict` to ensure knob order. @@ -398,8 +401,9 @@ def imprint(node, data, tab=None): node.addKnob(knob) +@deprecated def add_publish_knob(node): - """[DEPRICATED] Add Publish knob to node + """[DEPRECATED] Add Publish knob to node Arguments: node (nuke.Node): nuke node to be processed @@ -416,8 +420,9 @@ def add_publish_knob(node): return node +@deprecated def set_avalon_knob_data(node, data=None, prefix="avalon:"): - """[DEPRICATED] Sets data into nodes's avalon knob + """[DEPRECATED] Sets data into nodes's avalon knob Arguments: node (nuke.Node): Nuke node to imprint with data, @@ -478,8 +483,9 @@ def set_avalon_knob_data(node, data=None, prefix="avalon:"): return node +@deprecated def get_avalon_knob_data(node, prefix="avalon:", create=True): - """[DEPRICATED] Gets a data from nodes's avalon knob + """[DEPRECATED] Gets a data from nodes's avalon knob Arguments: node (obj): Nuke node to search for data, @@ -521,8 +527,9 @@ def get_avalon_knob_data(node, prefix="avalon:", create=True): return data +@deprecated def fix_data_for_node_create(data): - """[DEPRICATED] Fixing data to be used for nuke knobs + """[DEPRECATED] Fixing data to be used for nuke knobs """ for k, v in data.items(): if isinstance(v, six.text_type): @@ -532,8 +539,9 @@ def fix_data_for_node_create(data): return data +@deprecated def add_write_node_legacy(name, **kwarg): - """[DEPRICATED] Adding nuke write node + """[DEPRECATED] Adding nuke write node Arguments: name (str): nuke node name kwarg (attrs): data for nuke knobs @@ -697,7 +705,7 @@ def get_nuke_imageio_settings(): @deprecated("openpype.hosts.nuke.api.lib.get_nuke_imageio_settings") def get_created_node_imageio_setting_legacy(nodeclass, creator, subset): - '''[DEPRICATED] Get preset data for dataflow (fileType, compression, bitDepth) + '''[DEPRECATED] Get preset data for dataflow (fileType, compression, bitDepth) ''' assert any([creator, nodeclass]), nuke.message( From 2e07aa33fa398148dd51ed103cf4da553c4f2379 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 22 Feb 2023 17:21:17 +0100 Subject: [PATCH 425/483] Nuke: baking with multiple reposition nodes also with settings and defaults --- openpype/hosts/nuke/api/plugin.py | 96 ++++++++++++------- .../defaults/project_settings/nuke.json | 35 +++++++ .../schemas/schema_nuke_publish.json | 47 +++++++++ 3 files changed, 144 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index d3f8357f7d..5521db99c0 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -558,9 +558,7 @@ class ExporterReview(object): self.path_in = self.instance.data.get("path", None) self.staging_dir = self.instance.data["stagingDir"] self.collection = self.instance.data.get("collection", None) - self.data = dict({ - "representations": list() - }) + self.data = {"representations": []} def get_file_info(self): if self.collection: @@ -626,7 +624,7 @@ class ExporterReview(object): nuke_imageio = opnlib.get_nuke_imageio_settings() # TODO: this is only securing backward compatibility lets remove - # this once all projects's anotomy are updated to newer config + # this once all projects's anatomy are updated to newer config if "baking" in nuke_imageio.keys(): return nuke_imageio["baking"]["viewerProcess"] else: @@ -823,8 +821,41 @@ class ExporterReviewMov(ExporterReview): add_tags = [] self.publish_on_farm = farm read_raw = kwargs["read_raw"] + + # TODO: remove this when `reformat_nodes_config` + # is changed in settings reformat_node_add = kwargs["reformat_node_add"] reformat_node_config = kwargs["reformat_node_config"] + + # TODO: make this required in future + reformat_nodes_config = kwargs.get("reformat_nodes_config", {}) + + # TODO: remove this once deprecated is removed + # make sure only reformat_nodes_config is used in future + if reformat_node_add and reformat_nodes_config.get("enabled"): + self.log.warning( + "`reformat_node_add` is deprecated. " + "Please use only `reformat_nodes_config` instead.") + reformat_nodes_config = None + + # TODO: reformat code when backward compatibility is not needed + # warning if reformat_nodes_config is not set + if not reformat_nodes_config: + self.log.warning( + "Please set `reformat_nodes_config` in settings.") + self.log.warning( + "Using `reformat_node_config` instead.") + reformat_nodes_config = { + "enabled": reformat_node_add, + "reposition_nodes": [ + { + "node_class": "Reformat", + "knobs": reformat_node_config + } + ] + } + + bake_viewer_process = kwargs["bake_viewer_process"] bake_viewer_input_process_node = kwargs[ "bake_viewer_input_process"] @@ -846,7 +877,6 @@ class ExporterReviewMov(ExporterReview): subset = self.instance.data["subset"] self._temp_nodes[subset] = [] - # ---------- start nodes creation # Read node r_node = nuke.createNode("Read") @@ -860,44 +890,39 @@ class ExporterReviewMov(ExporterReview): if read_raw: r_node["raw"].setValue(1) - # connect - self._temp_nodes[subset].append(r_node) - self.previous_node = r_node - self.log.debug("Read... `{}`".format(self._temp_nodes[subset])) + # connect to Read node + self._shift_to_previous_node_and_temp(subset, r_node, "Read... `{}`") # add reformat node - if reformat_node_add: + if reformat_nodes_config["enabled"]: + reposition_nodes = reformat_nodes_config["reposition_nodes"] + for reposition_node in reposition_nodes: + node_class = reposition_node["node_class"] + knobs = reposition_node["knobs"] + node = nuke.createNode(node_class) + set_node_knobs_from_settings(node, knobs) + + # connect in order + self._connect_to_above_nodes( + node, subset, "Reposition node... `{}`" + ) # append reformated tag add_tags.append("reformated") - rf_node = nuke.createNode("Reformat") - set_node_knobs_from_settings(rf_node, reformat_node_config) - - # connect - rf_node.setInput(0, self.previous_node) - self._temp_nodes[subset].append(rf_node) - self.previous_node = rf_node - self.log.debug( - "Reformat... `{}`".format(self._temp_nodes[subset])) - # only create colorspace baking if toggled on if bake_viewer_process: if bake_viewer_input_process_node: # View Process node ipn = get_view_process_node() if ipn is not None: - # connect - ipn.setInput(0, self.previous_node) - self._temp_nodes[subset].append(ipn) - self.previous_node = ipn - self.log.debug( - "ViewProcess... `{}`".format( - self._temp_nodes[subset])) + # connect to ViewProcess node + self._connect_to_above_nodes(ipn, subset, "ViewProcess... `{}`") if not self.viewer_lut_raw: # OCIODisplay dag_node = nuke.createNode("OCIODisplay") + # assign display display, viewer = get_viewer_config_from_string( str(baking_view_profile) ) @@ -907,13 +932,7 @@ class ExporterReviewMov(ExporterReview): # assign viewer dag_node["view"].setValue(viewer) - # connect - dag_node.setInput(0, self.previous_node) - self._temp_nodes[subset].append(dag_node) - self.previous_node = dag_node - self.log.debug("OCIODisplay... `{}`".format( - self._temp_nodes[subset])) - + self._connect_to_above_nodes(dag_node, subset, "OCIODisplay... `{}`") # Write node write_node = nuke.createNode("Write") self.log.debug("Path: {}".format(self.path)) @@ -967,6 +986,15 @@ class ExporterReviewMov(ExporterReview): return self.data + def _shift_to_previous_node_and_temp(self, subset, node, message): + self._temp_nodes[subset].append(node) + self.previous_node = node + self.log.debug(message.format(self._temp_nodes[subset])) + + def _connect_to_above_nodes(self, node, subset, message): + node.setInput(0, self.previous_node) + self._shift_to_previous_node_and_temp(subset, node, message) + @deprecated("openpype.hosts.nuke.api.plugin.NukeWriteCreator") class AbstractWriteRender(OpenPypeCreator): diff --git a/openpype/settings/defaults/project_settings/nuke.json b/openpype/settings/defaults/project_settings/nuke.json index d475c337d9..2545411e0a 100644 --- a/openpype/settings/defaults/project_settings/nuke.json +++ b/openpype/settings/defaults/project_settings/nuke.json @@ -446,6 +446,41 @@ "value": false } ], + "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": [] } 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 5b9145e7d9..1c542279fc 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 @@ -271,6 +271,10 @@ { "type": "separator" }, + { + "type": "label", + "label": "Currently we are supporting also multiple reposition nodes.
Older single reformat node is still supported
and if it is activated then preference will
be on it. If you want to use multiple reformat
nodes then you need to disable single reformat
node and enable multiple Reformat nodes
here." + }, { "type": "boolean", "key": "reformat_node_add", @@ -287,6 +291,49 @@ } ] }, + { + "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" }, From fb3bda7c80c908dc052f50092e6d94a62947a3ad Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Thu, 23 Feb 2023 10:57:00 +0100 Subject: [PATCH 426/483] little fixes --- openpype/hosts/nuke/api/lib.py | 9 +++------ openpype/hosts/nuke/api/plugin.py | 6 +++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index b13c592fbf..73d4986b64 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -214,7 +214,6 @@ def update_node_data(node, knobname, data): knob.setValue(knob_value) -@deprecated class Knobby(object): """[DEPRECATED] For creating knob which it's type isn't mapped in `create_knobs` @@ -249,9 +248,8 @@ class Knobby(object): return " ".join(words) -@deprecated def create_knobs(data, tab=None): - """[DEPRECATED] Create knobs by data + """Create knobs by data Depending on the type of each dict value and creates the correct Knob. @@ -344,9 +342,8 @@ def create_knobs(data, tab=None): return knobs -@deprecated def imprint(node, data, tab=None): - """[DEPRECATED] Store attributes with value on node + """Store attributes with value on node Parse user data into Node knobs. Use `collections.OrderedDict` to ensure knob order. @@ -1249,7 +1246,7 @@ def create_write_node( nodes to be created before write with dependency review (bool)[optional]: adding review knob farm (bool)[optional]: rendering workflow target - kwargs (dict)[optional]: additional key arguments for formating + kwargs (dict)[optional]: additional key arguments for formatting Example: prenodes = { diff --git a/openpype/hosts/nuke/api/plugin.py b/openpype/hosts/nuke/api/plugin.py index 5521db99c0..160ca820a4 100644 --- a/openpype/hosts/nuke/api/plugin.py +++ b/openpype/hosts/nuke/api/plugin.py @@ -842,9 +842,9 @@ class ExporterReviewMov(ExporterReview): # warning if reformat_nodes_config is not set if not reformat_nodes_config: self.log.warning( - "Please set `reformat_nodes_config` in settings.") - self.log.warning( - "Using `reformat_node_config` instead.") + "Please set `reformat_nodes_config` in settings. " + "Using `reformat_node_config` instead." + ) reformat_nodes_config = { "enabled": reformat_node_add, "reposition_nodes": [ From b4541d29fe823416c27b9a07424d431a738d3e8a Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Feb 2023 11:47:32 +0100 Subject: [PATCH 427/483] nuke assist kickoff --- .../system_settings/applications.json | 128 ++++++++++++++++++ .../system_schema/schema_applications.json | 8 ++ 2 files changed, 136 insertions(+) diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index f84d99e36b..5fd9b926fb 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -337,6 +337,134 @@ } } }, + "nukeassist": { + "enabled": true, + "label": "Nuke Assist", + "icon": "{}/app_icons/nuke.png", + "host_name": "nuke", + "environment": { + "NUKE_PATH": [ + "{NUKE_PATH}", + "{OPENPYPE_STUDIO_PLUGINS}/nuke" + ] + }, + "variants": { + "13-2": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.2v1\\Nuke13.2.exe" + ], + "darwin": [], + "linux": [ + "/usr/local/Nuke13.2v1/Nuke13.2" + ] + }, + "arguments": { + "windows": ["--nukeassist"], + "darwin": ["--nukeassist"], + "linux": ["--nukeassist"] + }, + "environment": {} + }, + "13-0": { + "use_python_2": false, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke13.0v1\\Nuke13.0.exe" + ], + "darwin": [], + "linux": [ + "/usr/local/Nuke13.0v1/Nuke13.0" + ] + }, + "arguments": { + "windows": ["--nukeassist"], + "darwin": ["--nukeassist"], + "linux": ["--nukeassist"] + }, + "environment": {} + }, + "12-2": { + "use_python_2": true, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke12.2v3\\Nuke12.2.exe" + ], + "darwin": [], + "linux": [ + "/usr/local/Nuke12.2v3Nuke12.2" + ] + }, + "arguments": { + "windows": ["--nukeassist"], + "darwin": ["--nukeassist"], + "linux": ["--nukeassist"] + }, + "environment": {} + }, + "12-0": { + "use_python_2": true, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke12.0v1\\Nuke12.0.exe" + ], + "darwin": [], + "linux": [ + "/usr/local/Nuke12.0v1/Nuke12.0" + ] + }, + "arguments": { + "windows": ["--nukeassist"], + "darwin": ["--nukeassist"], + "linux": ["--nukeassist"] + }, + "environment": {} + }, + "11-3": { + "use_python_2": true, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke11.3v1\\Nuke11.3.exe" + ], + "darwin": [], + "linux": [ + "/usr/local/Nuke11.3v5/Nuke11.3" + ] + }, + "arguments": { + "windows": ["--nukeassist"], + "darwin": ["--nukeassist"], + "linux": ["--nukeassist"] + }, + "environment": {} + }, + "11-2": { + "use_python_2": true, + "executables": { + "windows": [ + "C:\\Program Files\\Nuke11.2v2\\Nuke11.2.exe" + ], + "darwin": [], + "linux": [] + }, + "arguments": { + "windows": ["--nukeassist"], + "darwin": ["--nukeassist"], + "linux": ["--nukeassist"] + }, + "environment": {} + }, + "__dynamic_keys_labels__": { + "13-2": "13.2", + "13-0": "13.0", + "12-2": "12.2", + "12-0": "12.0", + "11-3": "11.3", + "11-2": "11.2" + } + } + }, "nukex": { "enabled": true, "label": "Nuke X", diff --git a/openpype/settings/entities/schemas/system_schema/schema_applications.json b/openpype/settings/entities/schemas/system_schema/schema_applications.json index 36c5811496..b17687cf71 100644 --- a/openpype/settings/entities/schemas/system_schema/schema_applications.json +++ b/openpype/settings/entities/schemas/system_schema/schema_applications.json @@ -25,6 +25,14 @@ "nuke_label": "Nuke" } }, + { + "type": "schema_template", + "name": "template_nuke", + "template_data": { + "nuke_type": "nukeassist", + "nuke_label": "Nuke Assist" + } + }, { "type": "schema_template", "name": "template_nuke", From e57c9e8dd6dc9b08d914f43805b3583e5c3bbe1c Mon Sep 17 00:00:00 2001 From: ynput Date: Tue, 21 Feb 2023 14:23:26 +0200 Subject: [PATCH 428/483] adding appgroup to prelaunch hook --- openpype/hooks/pre_foundry_apps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hooks/pre_foundry_apps.py b/openpype/hooks/pre_foundry_apps.py index 85f68c6b60..2092d5025d 100644 --- a/openpype/hooks/pre_foundry_apps.py +++ b/openpype/hooks/pre_foundry_apps.py @@ -13,7 +13,7 @@ class LaunchFoundryAppsWindows(PreLaunchHook): # Should be as last hook because must change launch arguments to string order = 1000 - app_groups = ["nuke", "nukex", "hiero", "nukestudio"] + app_groups = ["nuke", "nukeassist", "nukex", "hiero", "nukestudio"] platforms = ["windows"] def execute(self): From fc6e5ca854fad80a6687befba10422a187724bfd Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Feb 2023 13:39:02 +0100 Subject: [PATCH 429/483] adding nukeassist hook --- openpype/hosts/nuke/hooks/__init__.py | 0 openpype/hosts/nuke/hooks/pre_nukeassist_setup.py | 10 ++++++++++ 2 files changed, 10 insertions(+) create mode 100644 openpype/hosts/nuke/hooks/__init__.py create mode 100644 openpype/hosts/nuke/hooks/pre_nukeassist_setup.py diff --git a/openpype/hosts/nuke/hooks/__init__.py b/openpype/hosts/nuke/hooks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py new file mode 100644 index 0000000000..80696c34e5 --- /dev/null +++ b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py @@ -0,0 +1,10 @@ +from openpype.lib import PreLaunchHook + +class PrelaunchNukeAssistHook(PreLaunchHook): + """ + Adding flag when nukeassist + """ + app_groups = ["nukeassist"] + + def execute(self): + self.launch_context.env["NUKEASSIST"] = True From a1c9e396640e9d6691af68e21ac1fa3ff78e9bf3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Feb 2023 13:56:17 +0100 Subject: [PATCH 430/483] adding hook to host --- openpype/hosts/nuke/addon.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/nuke/addon.py b/openpype/hosts/nuke/addon.py index 9d25afe2b6..6a4b91a76d 100644 --- a/openpype/hosts/nuke/addon.py +++ b/openpype/hosts/nuke/addon.py @@ -63,5 +63,12 @@ class NukeAddon(OpenPypeModule, IHostAddon): path_paths.append(quick_time_path) env["PATH"] = os.pathsep.join(path_paths) + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(NUKE_ROOT_DIR, "hooks") + ] + def get_workfile_extensions(self): return [".nk"] From 71745e185cae75de715a0962fcf34262d4d7df9d Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Feb 2023 14:54:51 +0100 Subject: [PATCH 431/483] adding menu assist variant conditions --- openpype/hosts/nuke/api/pipeline.py | 50 +++++++++++-------- openpype/hosts/nuke/hooks/__init__.py | 0 .../hosts/nuke/hooks/pre_nukeassist_setup.py | 2 +- 3 files changed, 30 insertions(+), 22 deletions(-) delete mode 100644 openpype/hosts/nuke/hooks/__init__.py diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index d5289010cb..306fa50de9 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -71,7 +71,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") MENU_LABEL = os.environ["AVALON_LABEL"] - +ASSIST = bool(os.getenv("NUKEASSIST")) # registering pyblish gui regarding settings in presets if os.getenv("PYBLISH_GUI", None): @@ -207,6 +207,7 @@ def _show_workfiles(): def _install_menu(): + # uninstall original avalon menu main_window = get_main_window() menubar = nuke.menu("Nuke") @@ -217,7 +218,9 @@ def _install_menu(): ) Context.context_label = label context_action = menu.addCommand(label) - context_action.setEnabled(False) + + if not ASSIST: + context_action.setEnabled(False) menu.addSeparator() menu.addCommand( @@ -226,18 +229,20 @@ def _install_menu(): ) menu.addSeparator() - menu.addCommand( - "Create...", - lambda: host_tools.show_publisher( - tab="create" + if not ASSIST: + menu.addCommand( + "Create...", + lambda: host_tools.show_publisher( + tab="create" + ) ) - ) - menu.addCommand( - "Publish...", - lambda: host_tools.show_publisher( - tab="publish" + menu.addCommand( + "Publish...", + lambda: host_tools.show_publisher( + tab="publish" + ) ) - ) + menu.addCommand( "Load...", lambda: host_tools.show_loader( @@ -285,15 +290,18 @@ def _install_menu(): "Build Workfile from template", lambda: build_workfile_template() ) - menu_template.addSeparator() - menu_template.addCommand( - "Create Place Holder", - lambda: create_placeholder() - ) - menu_template.addCommand( - "Update Place Holder", - lambda: update_placeholder() - ) + + if not ASSIST: + menu_template.addSeparator() + menu_template.addCommand( + "Create Place Holder", + lambda: create_placeholder() + ) + menu_template.addCommand( + "Update Place Holder", + lambda: update_placeholder() + ) + menu.addSeparator() menu.addCommand( "Experimental tools...", diff --git a/openpype/hosts/nuke/hooks/__init__.py b/openpype/hosts/nuke/hooks/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py index 80696c34e5..054bd677a7 100644 --- a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py +++ b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py @@ -7,4 +7,4 @@ class PrelaunchNukeAssistHook(PreLaunchHook): app_groups = ["nukeassist"] def execute(self): - self.launch_context.env["NUKEASSIST"] = True + self.launch_context.env["NUKEASSIST"] = "1" From f3431c62792cea3a65c95366d105bd782fe46376 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Feb 2023 15:26:07 +0100 Subject: [PATCH 432/483] moving Assist switch to api level condition for updating nodes --- openpype/hosts/nuke/api/__init__.py | 7 ++++++- openpype/hosts/nuke/api/lib.py | 21 ++++++++++++--------- openpype/hosts/nuke/api/pipeline.py | 2 +- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index 1af5ff365d..7766a94140 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -1,3 +1,4 @@ +import os from .workio import ( file_extensions, has_unsaved_changes, @@ -50,6 +51,8 @@ from .utils import ( get_colorspace_list ) +ASSIST = bool(os.getenv("NUKEASSIST")) + __all__ = ( "file_extensions", "has_unsaved_changes", @@ -92,5 +95,7 @@ __all__ = ( "create_write_node", "colorspace_exists_on_node", - "get_colorspace_list" + "get_colorspace_list", + + "ASSIST" ) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 73d4986b64..ec5bc58f9f 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -49,7 +49,7 @@ from openpype.pipeline.colorspace import ( ) from openpype.pipeline.workfile import BuildWorkfile -from . import gizmo_menu +from . import gizmo_menu, ASSIST from .workio import ( save_file, @@ -2263,14 +2263,17 @@ class WorkfileSettings(object): node['frame_range'].setValue(range) node['frame_range_lock'].setValue(True) - set_node_data( - self._root_node, - INSTANCE_DATA_KNOB, - { - "handleStart": int(handle_start), - "handleEnd": int(handle_end) - } - ) + if not ASSIST: + set_node_data( + self._root_node, + INSTANCE_DATA_KNOB, + { + "handleStart": int(handle_start), + "handleEnd": int(handle_end) + } + ) + else: + log.warning("NukeAssist mode is not allowing updating custom knobs...") def reset_resolution(self): """Set resolution to project resolution.""" diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 306fa50de9..94c4518664 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -60,6 +60,7 @@ from .workio import ( work_root, current_file ) +from . import ASSIST log = Logger.get_logger(__name__) @@ -71,7 +72,6 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") MENU_LABEL = os.environ["AVALON_LABEL"] -ASSIST = bool(os.getenv("NUKEASSIST")) # registering pyblish gui regarding settings in presets if os.getenv("PYBLISH_GUI", None): From fe163ab19b43bbde9c9ee297cf02ca2d42d998b5 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Feb 2023 16:06:39 +0100 Subject: [PATCH 433/483] moving ASSIST switch to utils --- openpype/hosts/nuke/api/__init__.py | 7 +------ openpype/hosts/nuke/api/lib.py | 3 ++- openpype/hosts/nuke/api/pipeline.py | 2 +- openpype/hosts/nuke/api/utils.py | 1 + 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/nuke/api/__init__.py b/openpype/hosts/nuke/api/__init__.py index 7766a94140..1af5ff365d 100644 --- a/openpype/hosts/nuke/api/__init__.py +++ b/openpype/hosts/nuke/api/__init__.py @@ -1,4 +1,3 @@ -import os from .workio import ( file_extensions, has_unsaved_changes, @@ -51,8 +50,6 @@ from .utils import ( get_colorspace_list ) -ASSIST = bool(os.getenv("NUKEASSIST")) - __all__ = ( "file_extensions", "has_unsaved_changes", @@ -95,7 +92,5 @@ __all__ = ( "create_write_node", "colorspace_exists_on_node", - "get_colorspace_list", - - "ASSIST" + "get_colorspace_list" ) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index ec5bc58f9f..dfc647872b 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -49,7 +49,8 @@ from openpype.pipeline.colorspace import ( ) from openpype.pipeline.workfile import BuildWorkfile -from . import gizmo_menu, ASSIST +from . import gizmo_menu +from .utils import ASSIST from .workio import ( save_file, diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 94c4518664..55cb77bafe 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -60,7 +60,7 @@ from .workio import ( work_root, current_file ) -from . import ASSIST +from .utils import ASSIST log = Logger.get_logger(__name__) diff --git a/openpype/hosts/nuke/api/utils.py b/openpype/hosts/nuke/api/utils.py index 6bcb752dd1..261eba8401 100644 --- a/openpype/hosts/nuke/api/utils.py +++ b/openpype/hosts/nuke/api/utils.py @@ -4,6 +4,7 @@ import nuke from openpype import resources from .lib import maintained_selection +ASSIST = bool(os.getenv("NUKEASSIST")) def set_context_favorites(favorites=None): """ Adding favorite folders to nuke's browser From 360a5b3b68de7e3f735078173c897e21196adec4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Feb 2023 16:38:54 +0100 Subject: [PATCH 434/483] move ASSIST to constant --- openpype/hosts/nuke/api/constants.py | 4 ++++ openpype/hosts/nuke/api/lib.py | 2 +- openpype/hosts/nuke/api/pipeline.py | 2 +- openpype/hosts/nuke/api/utils.py | 1 - 4 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 openpype/hosts/nuke/api/constants.py diff --git a/openpype/hosts/nuke/api/constants.py b/openpype/hosts/nuke/api/constants.py new file mode 100644 index 0000000000..110199720f --- /dev/null +++ b/openpype/hosts/nuke/api/constants.py @@ -0,0 +1,4 @@ +import os + + +ASSIST = bool(os.getenv("NUKEASSIST")) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index dfc647872b..9e36fb147b 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -50,7 +50,7 @@ from openpype.pipeline.colorspace import ( from openpype.pipeline.workfile import BuildWorkfile from . import gizmo_menu -from .utils import ASSIST +from .constants import ASSIST from .workio import ( save_file, diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 55cb77bafe..f07d150ba5 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -60,7 +60,7 @@ from .workio import ( work_root, current_file ) -from .utils import ASSIST +from .constants import ASSIST log = Logger.get_logger(__name__) diff --git a/openpype/hosts/nuke/api/utils.py b/openpype/hosts/nuke/api/utils.py index 261eba8401..6bcb752dd1 100644 --- a/openpype/hosts/nuke/api/utils.py +++ b/openpype/hosts/nuke/api/utils.py @@ -4,7 +4,6 @@ import nuke from openpype import resources from .lib import maintained_selection -ASSIST = bool(os.getenv("NUKEASSIST")) def set_context_favorites(favorites=None): """ Adding favorite folders to nuke's browser From 132b616f1f9b4c802f7c985b5eb73948d3ae22a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Wed, 22 Feb 2023 14:41:36 +0100 Subject: [PATCH 435/483] Update openpype/hosts/nuke/hooks/pre_nukeassist_setup.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/nuke/hooks/pre_nukeassist_setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py index 054bd677a7..3a0f00413a 100644 --- a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py +++ b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py @@ -8,3 +8,4 @@ class PrelaunchNukeAssistHook(PreLaunchHook): def execute(self): self.launch_context.env["NUKEASSIST"] = "1" + From b3b488c2a6235d71d1e15eb2d058d22c439f5303 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 22 Feb 2023 14:44:22 +0100 Subject: [PATCH 436/483] removing line --- openpype/hosts/nuke/hooks/pre_nukeassist_setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py index 3a0f00413a..054bd677a7 100644 --- a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py +++ b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py @@ -8,4 +8,3 @@ class PrelaunchNukeAssistHook(PreLaunchHook): def execute(self): self.launch_context.env["NUKEASSIST"] = "1" - From 363551af73a5703a08af4a5881448e9bc61885d3 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 22 Feb 2023 14:52:57 +0100 Subject: [PATCH 437/483] context label is not needed in nukeassist --- openpype/hosts/nuke/api/pipeline.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index f07d150ba5..4c0f169ade 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -207,22 +207,25 @@ def _show_workfiles(): def _install_menu(): + """Install Avalon menu into Nuke's main menu bar.""" # uninstall original avalon menu main_window = get_main_window() menubar = nuke.menu("Nuke") menu = menubar.addMenu(MENU_LABEL) - label = "{0}, {1}".format( - os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] - ) - Context.context_label = label - context_action = menu.addCommand(label) if not ASSIST: + label = "{0}, {1}".format( + os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] + ) + Context.context_label = label + context_action = menu.addCommand(label) context_action.setEnabled(False) - menu.addSeparator() + # add separator after context label + menu.addSeparator() + menu.addCommand( "Work Files...", _show_workfiles From cb87097f74af6d6c616486867e89a9c0afada6c0 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 22 Feb 2023 14:56:27 +0100 Subject: [PATCH 438/483] adding empty lines --- openpype/hosts/nuke/api/pipeline.py | 1 - openpype/hosts/nuke/hooks/pre_nukeassist_setup.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/pipeline.py b/openpype/hosts/nuke/api/pipeline.py index 4c0f169ade..2496d66c1d 100644 --- a/openpype/hosts/nuke/api/pipeline.py +++ b/openpype/hosts/nuke/api/pipeline.py @@ -214,7 +214,6 @@ def _install_menu(): menubar = nuke.menu("Nuke") menu = menubar.addMenu(MENU_LABEL) - if not ASSIST: label = "{0}, {1}".format( os.environ["AVALON_ASSET"], os.environ["AVALON_TASK"] diff --git a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py index 054bd677a7..3948a665c6 100644 --- a/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py +++ b/openpype/hosts/nuke/hooks/pre_nukeassist_setup.py @@ -1,5 +1,6 @@ from openpype.lib import PreLaunchHook + class PrelaunchNukeAssistHook(PreLaunchHook): """ Adding flag when nukeassist From 3218dd021ad1f471215fd9e7b1f2ce1972e42d74 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 22 Feb 2023 14:59:30 +0100 Subject: [PATCH 439/483] hound comments --- openpype/hosts/nuke/api/lib.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/nuke/api/lib.py b/openpype/hosts/nuke/api/lib.py index 9e36fb147b..c08db978d3 100644 --- a/openpype/hosts/nuke/api/lib.py +++ b/openpype/hosts/nuke/api/lib.py @@ -2274,7 +2274,10 @@ class WorkfileSettings(object): } ) else: - log.warning("NukeAssist mode is not allowing updating custom knobs...") + log.warning( + "NukeAssist mode is not allowing " + "updating custom knobs..." + ) def reset_resolution(self): """Set resolution to project resolution.""" From 7ec1cb77a46675a2e986a78f6081594d67f996a5 Mon Sep 17 00:00:00 2001 From: Kayla Man <64118225+moonyuet@users.noreply.github.com> Date: Thu, 23 Feb 2023 23:00:16 +0800 Subject: [PATCH 440/483] maya gltf texture convertor and validator (#4261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ondřej Samohel <33513211+antirotor@users.noreply.github.com> --- .../maya/plugins/publish/extract_gltf.py | 3 + .../plugins/publish/validate_glsl_material.py | 207 ++++++++++++++++++ .../plugins/publish/validate_glsl_plugin.py | 31 +++ .../defaults/project_settings/maya.json | 15 ++ .../schemas/schema_maya_publish.json | 32 +++ 5 files changed, 288 insertions(+) create mode 100644 openpype/hosts/maya/plugins/publish/validate_glsl_material.py create mode 100644 openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py diff --git a/openpype/hosts/maya/plugins/publish/extract_gltf.py b/openpype/hosts/maya/plugins/publish/extract_gltf.py index f5ceed5f33..ac258ffb3d 100644 --- a/openpype/hosts/maya/plugins/publish/extract_gltf.py +++ b/openpype/hosts/maya/plugins/publish/extract_gltf.py @@ -22,6 +22,8 @@ class ExtractGLB(publish.Extractor): self.log.info("Extracting GLB to: {}".format(path)) + cmds.loadPlugin("maya2glTF", quiet=True) + nodes = instance[:] self.log.info("Instance: {0}".format(nodes)) @@ -45,6 +47,7 @@ class ExtractGLB(publish.Extractor): "glb": True, "vno": True # visibleNodeOnly } + with lib.maintained_selection(): cmds.select(nodes, hi=True, noExpand=True) extract_gltf(staging_dir, diff --git a/openpype/hosts/maya/plugins/publish/validate_glsl_material.py b/openpype/hosts/maya/plugins/publish/validate_glsl_material.py new file mode 100644 index 0000000000..10c48da404 --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_glsl_material.py @@ -0,0 +1,207 @@ +import os +from maya import cmds + +import pyblish.api +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder +) +from openpype.pipeline import PublishValidationError + + +class ValidateGLSLMaterial(pyblish.api.InstancePlugin): + """ + Validate if the asset uses GLSL Shader + """ + + order = ValidateContentsOrder + 0.1 + families = ['gltf'] + hosts = ['maya'] + label = 'GLSL Shader for GLTF' + actions = [RepairAction] + optional = True + active = True + + def process(self, instance): + shading_grp = self.get_material_from_shapes(instance) + if not shading_grp: + raise PublishValidationError("No shading group found") + invalid = self.get_texture_shader_invalid(instance) + if invalid: + raise PublishValidationError("Non GLSL Shader found: " + "{0}".format(invalid)) + + def get_material_from_shapes(self, instance): + shapes = cmds.ls(instance, type="mesh", long=True) + for shape in shapes: + shading_grp = cmds.listConnections(shape, + destination=True, + type="shadingEngine") + + return shading_grp or [] + + def get_texture_shader_invalid(self, instance): + + invalid = set() + shading_grp = self.get_material_from_shapes(instance) + for shading_group in shading_grp: + material_name = "{}.surfaceShader".format(shading_group) + material = cmds.listConnections(material_name, + source=True, + destination=False, + type="GLSLShader") + + if not material: + # add material name + material = cmds.listConnections(material_name)[0] + invalid.add(material) + + return list(invalid) + + @classmethod + def repair(cls, instance): + """ + Repair instance by assigning GLSL Shader + to the material + """ + cls.assign_glsl_shader(instance) + return + + @classmethod + def assign_glsl_shader(cls, instance): + """ + Converting StingrayPBS material to GLSL Shaders + for the glb export through Maya2GLTF plugin + """ + + meshes = cmds.ls(instance, type="mesh", long=True) + cls.log.info("meshes: {}".format(meshes)) + # load the glsl shader plugin + cmds.loadPlugin("glslShader", quiet=True) + + for mesh in meshes: + # create glsl shader + glsl = cmds.createNode('GLSLShader') + glsl_shading_grp = cmds.sets(name=glsl + "SG", empty=True, + renderable=True, noSurfaceShader=True) + cmds.connectAttr(glsl + ".outColor", + glsl_shading_grp + ".surfaceShader") + + # load the maya2gltf shader + ogsfx_path = instance.context.data["project_settings"]["maya"]["publish"]["ExtractGLB"]["ogsfx_path"] # noqa + if not os.path.exists(ogsfx_path): + if ogsfx_path: + # if custom ogsfx path is not specified + # the log below is the warning for the user + cls.log.warning("ogsfx shader file " + "not found in {}".format(ogsfx_path)) + + cls.log.info("Find the ogsfx shader file in " + "default maya directory...") + # re-direct to search the ogsfx path in maya_dir + ogsfx_path = os.getenv("MAYA_APP_DIR") + ogsfx_path + if not os.path.exists(ogsfx_path): + raise PublishValidationError("The ogsfx shader file does not " # noqa + "exist: {}".format(ogsfx_path)) # noqa + + cmds.setAttr(glsl + ".shader", ogsfx_path, typ="string") + # list the materials used for the assets + shading_grp = cmds.listConnections(mesh, + destination=True, + type="shadingEngine") + + # get the materials related to the selected assets + for material in shading_grp: + pbs_shader = cmds.listConnections(material, + destination=True, + type="StingrayPBS") + if pbs_shader: + cls.pbs_shader_conversion(pbs_shader, glsl) + # setting up to relink the texture if + # the mesh is with aiStandardSurface + arnold_shader = cmds.listConnections(material, + destination=True, + type="aiStandardSurface") + if arnold_shader: + cls.arnold_shader_conversion(arnold_shader, glsl) + + cmds.sets(mesh, forceElement=str(glsl_shading_grp)) + + @classmethod + def pbs_shader_conversion(cls, main_shader, glsl): + + cls.log.info("StringrayPBS detected " + "-> Can do texture conversion") + + for shader in main_shader: + # get the file textures related to the PBS Shader + albedo = cmds.listConnections(shader + + ".TEX_color_map") + if albedo: + dif_output = albedo[0] + ".outColor" + # get the glsl_shader input + # reconnect the file nodes to maya2gltf shader + glsl_dif = glsl + ".u_BaseColorTexture" + cmds.connectAttr(dif_output, glsl_dif) + + # connect orm map if there is one + orm_packed = cmds.listConnections(shader + + ".TEX_ao_map") + if orm_packed: + orm_output = orm_packed[0] + ".outColor" + + mtl = glsl + ".u_MetallicTexture" + ao = glsl + ".u_OcclusionTexture" + rough = glsl + ".u_RoughnessTexture" + + cmds.connectAttr(orm_output, mtl) + cmds.connectAttr(orm_output, ao) + cmds.connectAttr(orm_output, rough) + + # connect nrm map if there is one + nrm = cmds.listConnections(shader + + ".TEX_normal_map") + if nrm: + nrm_output = nrm[0] + ".outColor" + glsl_nrm = glsl + ".u_NormalTexture" + cmds.connectAttr(nrm_output, glsl_nrm) + + @classmethod + def arnold_shader_conversion(cls, main_shader, glsl): + cls.log.info("aiStandardSurface detected " + "-> Can do texture conversion") + + for shader in main_shader: + # get the file textures related to the PBS Shader + albedo = cmds.listConnections(shader + ".baseColor") + if albedo: + dif_output = albedo[0] + ".outColor" + # get the glsl_shader input + # reconnect the file nodes to maya2gltf shader + glsl_dif = glsl + ".u_BaseColorTexture" + cmds.connectAttr(dif_output, glsl_dif) + + orm_packed = cmds.listConnections(shader + + ".specularRoughness") + if orm_packed: + orm_output = orm_packed[0] + ".outColor" + + mtl = glsl + ".u_MetallicTexture" + ao = glsl + ".u_OcclusionTexture" + rough = glsl + ".u_RoughnessTexture" + + cmds.connectAttr(orm_output, mtl) + cmds.connectAttr(orm_output, ao) + cmds.connectAttr(orm_output, rough) + + # connect nrm map if there is one + bump_node = cmds.listConnections(shader + + ".normalCamera") + if bump_node: + for bump in bump_node: + nrm = cmds.listConnections(bump + + ".bumpValue") + if nrm: + nrm_output = nrm[0] + ".outColor" + glsl_nrm = glsl + ".u_NormalTexture" + cmds.connectAttr(nrm_output, glsl_nrm) diff --git a/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py b/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py new file mode 100644 index 0000000000..53c2cf548a --- /dev/null +++ b/openpype/hosts/maya/plugins/publish/validate_glsl_plugin.py @@ -0,0 +1,31 @@ + +from maya import cmds + +import pyblish.api +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder +) + + +class ValidateGLSLPlugin(pyblish.api.InstancePlugin): + """ + Validate if the asset uses GLSL Shader + """ + + order = ValidateContentsOrder + 0.15 + families = ['gltf'] + hosts = ['maya'] + label = 'maya2glTF plugin' + actions = [RepairAction] + + def process(self, instance): + if not cmds.pluginInfo("maya2glTF", query=True, loaded=True): + raise RuntimeError("maya2glTF is not loaded") + + @classmethod + def repair(cls, instance): + """ + Repair instance by enabling the plugin + """ + return cmds.loadPlugin("maya2glTF", quiet=True) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index b590a56da6..32b141566b 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -407,6 +407,16 @@ "optional": false, "active": true }, + "ValidateGLSLMaterial": { + "enabled": true, + "optional": false, + "active": true + }, + "ValidateGLSLPlugin": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateRenderImageRule": { "enabled": true, "optional": false, @@ -898,6 +908,11 @@ "optional": true, "active": true, "bake_attributes": [] + }, + "ExtractGLB": { + "enabled": true, + "active": true, + "ogsfx_path": "/maya2glTF/PBR/shaders/glTF_PBR.ogsfx" } }, "load": { 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 873bb79c95..994e2d0032 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 @@ -408,6 +408,14 @@ "key": "ValidateCurrentRenderLayerIsRenderable", "label": "Validate Current Render Layer Has Renderable Camera" }, + { + "key": "ValidateGLSLMaterial", + "label": "Validate GLSL Material" + }, + { + "key": "ValidateGLSLPlugin", + "label": "Validate GLSL Plugin" + }, { "key": "ValidateRenderImageRule", "label": "Validate Images File Rule (Workspace)" @@ -956,6 +964,30 @@ "is_list": true } ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractGLB", + "label": "Extract GLB", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "text", + "key": "ogsfx_path", + "label": "GLSL Shader Directory" + } + ] } ] } From 64a142ef6482b61eb0967806882ccb6c70ecd91c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 6 Jan 2023 14:03:22 +0100 Subject: [PATCH 441/483] OP-4643 - fix for full file paths --- openpype/plugins/publish/extract_color_transcode.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index b0921688e9..99c8c87e51 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -1,7 +1,6 @@ import os import copy import clique - import pyblish.api from openpype.pipeline import publish From d2f8407111905b621e29b27202ef3e59afa63983 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 11:59:13 +0100 Subject: [PATCH 442/483] OP-4643 - fix files to delete --- .../plugins/publish/extract_color_transcode.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 99c8c87e51..61e29697d6 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -222,6 +222,21 @@ class ExtractOIIOTranscode(publish.Extractor): renamed_files.append(file_name) new_repre["files"] = renamed_files + def _rename_in_representation(self, new_repre, files_to_convert, + output_extension): + """Replace old extension with new one everywhere in representation.""" + if new_repre["name"] == new_repre["ext"]: + new_repre["name"] = output_extension + new_repre["ext"] = output_extension + + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(file_name) + new_repre["files"] = renamed_files + def _translate_to_sequence(self, files_to_convert): """Returns original list or list with filename formatted in single sequence format. From c038fbf884f872ec717588290b9ba9273f219d36 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 27 Jan 2023 13:18:33 +0100 Subject: [PATCH 443/483] OP-4643 - fix no tags in repre --- openpype/plugins/publish/extract_color_transcode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 61e29697d6..aca4adc40c 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -175,6 +175,8 @@ class ExtractOIIOTranscode(publish.Extractor): if new_repre.get("tags") is None: new_repre["tags"] = [] for tag in output_def["tags"]: + if not new_repre.get("tags"): + new_repre["tags"] = [] if tag not in new_repre["tags"]: new_repre["tags"].append(tag) From 195e9b436047a797bd9c99ac37ba5080690e3943 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 1 Feb 2023 16:13:59 +0100 Subject: [PATCH 444/483] OP-4643 - name of new representation from output definition key --- .../publish/extract_color_transcode.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index aca4adc40c..bd81dd6087 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -225,10 +225,22 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["files"] = renamed_files def _rename_in_representation(self, new_repre, files_to_convert, - output_extension): - """Replace old extension with new one everywhere in representation.""" - if new_repre["name"] == new_repre["ext"]: - new_repre["name"] = output_extension + output_name, output_extension): + """Replace old extension with new one everywhere in representation. + + Args: + new_repre (dict) + files_to_convert (list): of filenames from repre["files"], + standardized to always list + output_name (str): key of output definition from Settings, + if "" token used, keep original repre name + output_extension (str): extension from output definition + """ + if output_name != "passthrough": + new_repre["name"] = output_name + if not output_extension: + return + new_repre["ext"] = output_extension renamed_files = [] From 4a70ec9c54fbf7caec8ac8bd1f17adecbd9dd6b0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 8 Feb 2023 12:07:00 +0100 Subject: [PATCH 445/483] Fix - added missed scopes for Slack bot --- openpype/modules/slack/manifest.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml index 7a65cc5915..233c39fbaf 100644 --- a/openpype/modules/slack/manifest.yml +++ b/openpype/modules/slack/manifest.yml @@ -19,6 +19,8 @@ oauth_config: - chat:write.public - files:write - channels:read + - users:read + - usergroups:read settings: org_deploy_enabled: false socket_mode_enabled: false From 7f94f7ef7183a51bdfa1bd40078a9183ee50e1bd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 14 Feb 2023 15:14:14 +0100 Subject: [PATCH 446/483] OP-4643 - allow new repre to stay One might want to delete outputs with 'delete' tag, but repre must stay there at least until extract_review. More universal new tag might be created for this. --- openpype/plugins/publish/extract_color_transcode.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index bd81dd6087..4892a00fbe 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -175,8 +175,6 @@ class ExtractOIIOTranscode(publish.Extractor): if new_repre.get("tags") is None: new_repre["tags"] = [] for tag in output_def["tags"]: - if not new_repre.get("tags"): - new_repre["tags"] = [] if tag not in new_repre["tags"]: new_repre["tags"].append(tag) @@ -192,6 +190,12 @@ class ExtractOIIOTranscode(publish.Extractor): for repre in tuple(instance.data["representations"]): tags = repre.get("tags") or [] + # TODO implement better way, for now do not delete new repre + # new repre might have 'delete' tag to removed, but it first must + # be there for review to be created + if "newly_added" in tags: + tags.remove("newly_added") + continue if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) From cce048fd3e0f50441fa9f893dfb9efebe43d95a8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 15 Feb 2023 16:21:25 +0100 Subject: [PATCH 447/483] OP-4642 - refactored newly added representations --- openpype/plugins/publish/extract_color_transcode.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/plugins/publish/extract_color_transcode.py b/openpype/plugins/publish/extract_color_transcode.py index 4892a00fbe..a6fa710425 100644 --- a/openpype/plugins/publish/extract_color_transcode.py +++ b/openpype/plugins/publish/extract_color_transcode.py @@ -190,12 +190,6 @@ class ExtractOIIOTranscode(publish.Extractor): for repre in tuple(instance.data["representations"]): tags = repre.get("tags") or [] - # TODO implement better way, for now do not delete new repre - # new repre might have 'delete' tag to removed, but it first must - # be there for review to be created - if "newly_added" in tags: - tags.remove("newly_added") - continue if "delete" in tags and "thumbnail" not in tags: instance.data["representations"].remove(repre) From 9eaa0d1ff8e0882a2778fce44f09fba3f2cccecd Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 12:12:35 +0100 Subject: [PATCH 448/483] OP-4643 - split command line arguments to separate items Reuse existing method from ExtractReview, put it into transcoding.py --- openpype/plugins/publish/extract_review.py | 27 +++------------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 0f6dacba18..e80141fc4a 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -22,6 +22,7 @@ from openpype.lib.transcoding import ( should_convert_for_ffmpeg, convert_input_paths_for_ffmpeg, get_transcode_temp_directory, + split_cmd_args ) @@ -670,7 +671,7 @@ class ExtractReview(pyblish.api.InstancePlugin): res_filters = self.rescaling_filters(temp_data, output_def, new_repre) ffmpeg_video_filters.extend(res_filters) - ffmpeg_input_args = self.split_ffmpeg_args(ffmpeg_input_args) + ffmpeg_input_args = split_cmd_args(ffmpeg_input_args) lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args) ffmpeg_video_filters.extend(lut_filters) @@ -723,28 +724,6 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_output_args ) - def split_ffmpeg_args(self, in_args): - """Makes sure all entered arguments are separated in individual items. - - Split each argument string with " -" to identify if string contains - one or more arguments. - """ - splitted_args = [] - for arg in in_args: - sub_args = arg.split(" -") - if len(sub_args) == 1: - if arg and arg not in splitted_args: - splitted_args.append(arg) - continue - - for idx, arg in enumerate(sub_args): - if idx != 0: - arg = "-" + arg - - if arg and arg not in splitted_args: - splitted_args.append(arg) - return splitted_args - def ffmpeg_full_args( self, input_args, video_filters, audio_filters, output_args ): @@ -764,7 +743,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Returns: list: Containing all arguments ready to run in subprocess. """ - output_args = self.split_ffmpeg_args(output_args) + output_args = split_cmd_args(output_args) video_args_dentifiers = ["-vf", "-filter:v"] audio_args_dentifiers = ["-af", "-filter:a"] From 751586fd415ea6aa10327cca8fb57f2f1657b9d7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 13:11:11 +0100 Subject: [PATCH 449/483] Revert "Fix - added missed scopes for Slack bot" This reverts commit 5e0c4a3ab1432e120b8f0c324f899070f1a5f831. --- openpype/modules/slack/manifest.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/modules/slack/manifest.yml b/openpype/modules/slack/manifest.yml index 233c39fbaf..7a65cc5915 100644 --- a/openpype/modules/slack/manifest.yml +++ b/openpype/modules/slack/manifest.yml @@ -19,8 +19,6 @@ oauth_config: - chat:write.public - files:write - channels:read - - users:read - - usergroups:read settings: org_deploy_enabled: false socket_mode_enabled: false From 84ccfa60f43c1fa5a575b62c79779d66dfc2951d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 15:06:16 +0100 Subject: [PATCH 450/483] OP-4643 - added documentation --- website/docs/project_settings/settings_project_global.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/project_settings/settings_project_global.md b/website/docs/project_settings/settings_project_global.md index d904080ad1..b320b5502f 100644 --- a/website/docs/project_settings/settings_project_global.md +++ b/website/docs/project_settings/settings_project_global.md @@ -63,7 +63,7 @@ Example here describes use case for creation of new color coded review of png im ![global_oiio_transcode](assets/global_oiio_transcode.png) Another use case is to transcode in Maya only `beauty` render layers and use collected `Display` and `View` colorspaces from DCC. -![global_oiio_transcode_in_Maya](assets/global_oiio_transcode.png) +![global_oiio_transcode_in_Maya](assets/global_oiio_transcode.png)n ## Profile filters From 72a3572d9527093290ebd600b538e2375e690861 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 21 Feb 2023 17:56:21 +0100 Subject: [PATCH 451/483] Revert "OP-4643 - split command line arguments to separate items" This reverts commit deaad39437501f18fc3ba4be8b1fc5f0ee3be65d. --- openpype/lib/transcoding.py | 3 ++- openpype/plugins/publish/extract_review.py | 27 +++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/openpype/lib/transcoding.py b/openpype/lib/transcoding.py index c0bda2aa37..e5e21195e5 100644 --- a/openpype/lib/transcoding.py +++ b/openpype/lib/transcoding.py @@ -1100,7 +1100,7 @@ def convert_colorspace( raise ValueError("Both screen and display must be set.") if additional_command_args: - oiio_cmd.extend(split_cmd_args(additional_command_args)) + oiio_cmd.extend(additional_command_args) if target_colorspace: oiio_cmd.extend(["--colorconvert", @@ -1132,3 +1132,4 @@ def split_cmd_args(in_args): continue splitted_args.extend(arg.split(" ")) return splitted_args + diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index e80141fc4a..0f6dacba18 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -22,7 +22,6 @@ from openpype.lib.transcoding import ( should_convert_for_ffmpeg, convert_input_paths_for_ffmpeg, get_transcode_temp_directory, - split_cmd_args ) @@ -671,7 +670,7 @@ class ExtractReview(pyblish.api.InstancePlugin): res_filters = self.rescaling_filters(temp_data, output_def, new_repre) ffmpeg_video_filters.extend(res_filters) - ffmpeg_input_args = split_cmd_args(ffmpeg_input_args) + ffmpeg_input_args = self.split_ffmpeg_args(ffmpeg_input_args) lut_filters = self.lut_filters(new_repre, instance, ffmpeg_input_args) ffmpeg_video_filters.extend(lut_filters) @@ -724,6 +723,28 @@ class ExtractReview(pyblish.api.InstancePlugin): ffmpeg_output_args ) + def split_ffmpeg_args(self, in_args): + """Makes sure all entered arguments are separated in individual items. + + Split each argument string with " -" to identify if string contains + one or more arguments. + """ + splitted_args = [] + for arg in in_args: + sub_args = arg.split(" -") + if len(sub_args) == 1: + if arg and arg not in splitted_args: + splitted_args.append(arg) + continue + + for idx, arg in enumerate(sub_args): + if idx != 0: + arg = "-" + arg + + if arg and arg not in splitted_args: + splitted_args.append(arg) + return splitted_args + def ffmpeg_full_args( self, input_args, video_filters, audio_filters, output_args ): @@ -743,7 +764,7 @@ class ExtractReview(pyblish.api.InstancePlugin): Returns: list: Containing all arguments ready to run in subprocess. """ - output_args = split_cmd_args(output_args) + output_args = self.split_ffmpeg_args(output_args) video_args_dentifiers = ["-vf", "-filter:v"] audio_args_dentifiers = ["-af", "-filter:a"] From 59c3510a0219fe726e52816e4d1ff20015455794 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 11:26:06 +0100 Subject: [PATCH 452/483] don't add opacity to layers info --- openpype/hosts/tvpaint/api/lib.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/api/lib.py b/openpype/hosts/tvpaint/api/lib.py index 312a211d49..a0cf761870 100644 --- a/openpype/hosts/tvpaint/api/lib.py +++ b/openpype/hosts/tvpaint/api/lib.py @@ -50,7 +50,8 @@ def parse_layers_data(data): "group_id": int(group_id), "visible": visible == "ON", "position": int(position), - "opacity": int(opacity), + # Opacity from 'tv_layerinfo' is always set to '0' so it's unusable + # "opacity": int(opacity), "name": name, "type": layer_type, "frame_start": int(frame_start), From 0f8a9c58a758fda407d777232112b7723d0e4332 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 11:26:22 +0100 Subject: [PATCH 453/483] added is_current information to layers info --- openpype/hosts/tvpaint/api/lib.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/api/lib.py b/openpype/hosts/tvpaint/api/lib.py index a0cf761870..49846d7f29 100644 --- a/openpype/hosts/tvpaint/api/lib.py +++ b/openpype/hosts/tvpaint/api/lib.py @@ -43,7 +43,7 @@ def parse_layers_data(data): layer_id, group_id, visible, position, opacity, name, layer_type, frame_start, frame_end, prelighttable, postlighttable, - selected, editable, sencil_state + selected, editable, sencil_state, is_current ) = layer_raw.split("|") layer = { "layer_id": int(layer_id), @@ -60,7 +60,8 @@ def parse_layers_data(data): "postlighttable": postlighttable == "1", "selected": selected == "1", "editable": editable == "1", - "sencil_state": sencil_state + "sencil_state": sencil_state, + "is_current": is_current == "1" } layers.append(layer) return layers @@ -88,15 +89,17 @@ def get_layers_data_george_script(output_filepath, layer_ids=None): " selected editable sencilState" ), # Check if layer ID match `tv_LayerCurrentID` + "is_current=0", "IF CMP(current_layer_id, layer_id)==1", # - mark layer as selected if layer id match to current layer id + "is_current=1", "selected=1", "END", # Prepare line with data separated by "|" ( "line = layer_id'|'group_id'|'visible'|'position'|'opacity'|'" "name'|'type'|'startFrame'|'endFrame'|'prelighttable'|'" - "postlighttable'|'selected'|'editable'|'sencilState" + "postlighttable'|'selected'|'editable'|'sencilState'|'is_current" ), # Write data to output file "tv_writetextfile \"strict\" \"append\" '\"'output_path'\"' line", From 80151ca8800ca3fdcbcdebc092f27f6277201aff Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 11:39:50 +0100 Subject: [PATCH 454/483] disable review on render layer/pass by default --- openpype/settings/defaults/project_settings/tvpaint.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index e06a67a254..9173a8c3d5 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -31,13 +31,13 @@ "default_variants": [] }, "create_render_layer": { - "mark_for_review": true, + "mark_for_review": false, "default_pass_name": "beauty", "default_variant": "Main", "default_variants": [] }, "create_render_pass": { - "mark_for_review": true, + "mark_for_review": false, "default_variant": "Main", "default_variants": [] }, From abc36c8357e0773e35b5c312663d80ac3afbbfb1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 22 Feb 2023 12:08:08 +0100 Subject: [PATCH 455/483] fix detailed descriptions --- openpype/hosts/tvpaint/plugins/create/create_render.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 7e85977b11..d996c06818 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -66,7 +66,7 @@ RENDER_LAYER_DETAILED_DESCRIPTIONS = ( Be aware Render Layer is not TVPaint layer. All TVPaint layers in the scene with the color group id are rendered in the -beauty pass. To create sub passes use Render Layer creator which is +beauty pass. To create sub passes use Render Pass creator which is dependent on existence of render layer instance. The group can represent an asset (tree) or different part of scene that consist @@ -82,8 +82,8 @@ could be Render Layer which has 'Arm', 'Head' and 'Body' as Render Passes. RENDER_PASS_DETAILED_DESCRIPTIONS = ( """Render Pass is sub part of Render Layer. -Render Pass can consist of one or more TVPaint layers. Render Layers must -belong to a Render Layer. Marker TVPaint layers will change it's group color +Render Pass can consist of one or more TVPaint layers. Render Pass must +belong to a Render Layer. Marked TVPaint layers will change it's group color to match group color of Render Layer. """ ) From 79a40902e0a16518097240dd8bffd19c90fecbdd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Feb 2023 14:49:02 +0100 Subject: [PATCH 456/483] Better error message --- openpype/hosts/tvpaint/plugins/create/create_render.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index d996c06818..563d16f847 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -461,7 +461,10 @@ class CreateRenderPass(TVPaintCreator): "render_layer_instance_id" ) if not render_layer_instance_id: - raise CreatorError("Missing RenderLayer instance") + raise CreatorError(( + "You cannot create a Render Pass without a Render Layer." + " Please select one first" + )) render_layer_instance = self.create_context.instances_by_id.get( render_layer_instance_id From ba792f648b6aeea6299a5137afd23de4dc677a3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Feb 2023 14:52:30 +0100 Subject: [PATCH 457/483] use 'list_instances' instead of existing instances --- .../hosts/tvpaint/plugins/create/create_render.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 563d16f847..6a3788cc08 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -601,13 +601,16 @@ class CreateRenderPass(TVPaintCreator): ] def get_pre_create_attr_defs(self): + # Find available Render Layers + # - instances are created after creators reset + current_instances = self.host.list_instances() render_layers = [ { - "value": instance.id, - "label": instance.label + "value": instance["instance_id"], + "label": instance["subset"] } - for instance in self.create_context.instances - if instance.creator_identifier == CreateRenderlayer.identifier + for instance in current_instances + if instance["creator_identifier"] == CreateRenderlayer.identifier ] if not render_layers: render_layers.append({"value": None, "label": "N/A"}) From 8218e18edf352d377c901197ecf9408988e974e2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Feb 2023 14:52:53 +0100 Subject: [PATCH 458/483] added label with hint for artist --- openpype/hosts/tvpaint/plugins/create/create_render.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 6a3788cc08..d356a07687 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -41,6 +41,7 @@ from openpype.client import get_asset_by_name from openpype.lib import ( prepare_template_data, AbstractAttrDef, + UILabelDef, UISeparatorDef, EnumDef, TextDef, @@ -621,6 +622,9 @@ class CreateRenderPass(TVPaintCreator): label="Render Layer", items=render_layers ), + UILabelDef( + "NOTE: Try to hit refresh if you don't see a Render Layer" + ), BoolDef( "mark_for_review", label="Review", From 227f1402fe03590b4350124db0a3894335967c04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 23 Feb 2023 14:57:34 +0100 Subject: [PATCH 459/483] change how instance attributes are filled --- .../tvpaint/plugins/create/create_render.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index d356a07687..9711024c79 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -633,7 +633,34 @@ class CreateRenderPass(TVPaintCreator): ] def get_instance_attr_defs(self): - return self.get_pre_create_attr_defs() + # Find available Render Layers + current_instances = self.create_context.instances + render_layers = [ + { + "value": instance.id, + "label": instance.label + } + for instance in current_instances + if instance.creator_identifier == CreateRenderlayer.identifier + ] + if not render_layers: + render_layers.append({"value": None, "label": "N/A"}) + + return [ + EnumDef( + "render_layer_instance_id", + label="Render Layer", + items=render_layers + ), + UILabelDef( + "NOTE: Try to hit refresh if you don't see a Render Layer" + ), + BoolDef( + "mark_for_review", + label="Review", + default=self.mark_for_review + ) + ] class TVPaintAutoDetectRenderCreator(TVPaintCreator): From eae25bbf0b4dcf4b62bbdeab8d61ce9f4f82cfd9 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Feb 2023 11:24:36 +0000 Subject: [PATCH 460/483] Publish user defined attributes option. --- .../maya/plugins/create/create_pointcache.py | 1 + .../maya/plugins/publish/collect_pointcache.py | 18 ++++++++++++++++++ .../maya/plugins/publish/extract_pointcache.py | 3 +-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index 63c0490dc7..51eab94a1a 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -33,6 +33,7 @@ class CreatePointCache(plugin.Creator): self.data["refresh"] = False # Default to suspend refresh. # Add options for custom attributes + self.data["includeUserDefinedAttributes"] = True self.data["attr"] = "" self.data["attrPrefix"] = "" diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py index 332992ca92..72aa37fc11 100644 --- a/openpype/hosts/maya/plugins/publish/collect_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/collect_pointcache.py @@ -42,3 +42,21 @@ class CollectPointcache(pyblish.api.InstancePlugin): if proxy_set: instance.remove(proxy_set) instance.data["setMembers"].remove(proxy_set) + + # Collect user defined attributes. + if not instance.data.get("includeUserDefinedAttributes", False): + return + + all_nodes = ( + instance.data["setMembers"] + instance.data.get("proxy", []) + ) + user_defined_attributes = set() + for node in all_nodes: + attrs = cmds.listAttr(node, userDefined=True) or list() + shapes = cmds.listRelatives(node, shapes=True) or list() + for shape in shapes: + attrs.extend(cmds.listAttr(shape, userDefined=True) or list()) + + user_defined_attributes.update(attrs) + + instance.data["userDefinedAttributes"] = list(user_defined_attributes) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 0eb65e4226..8e794d4e17 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -39,8 +39,7 @@ class ExtractAlembic(publish.Extractor): start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - attrs = instance.data.get("attr", "").split(";") - attrs = [value for value in attrs if value.strip()] + attrs = instance.data.get("userDefinedAttributes", []) attrs += ["cbId"] attr_prefixes = instance.data.get("attrPrefix", "").split(";") From 80e19b6d430f239b95d8b14e9965ba3821e28867 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Feb 2023 12:22:49 +0000 Subject: [PATCH 461/483] Include animation family --- .../maya/plugins/create/create_animation.py | 4 ++++ .../maya/plugins/publish/collect_animation.py | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_animation.py b/openpype/hosts/maya/plugins/create/create_animation.py index e54c12315c..a4b6e86598 100644 --- a/openpype/hosts/maya/plugins/create/create_animation.py +++ b/openpype/hosts/maya/plugins/create/create_animation.py @@ -13,6 +13,7 @@ class CreateAnimation(plugin.Creator): icon = "male" write_color_sets = False write_face_sets = False + include_user_defined_attributes = False def __init__(self, *args, **kwargs): super(CreateAnimation, self).__init__(*args, **kwargs) @@ -47,3 +48,6 @@ class CreateAnimation(plugin.Creator): # Default to write normals. self.data["writeNormals"] = True + + value = self.include_user_defined_attributes + self.data["includeUserDefinedAttributes"] = value diff --git a/openpype/hosts/maya/plugins/publish/collect_animation.py b/openpype/hosts/maya/plugins/publish/collect_animation.py index 549098863f..8f523f770b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_animation.py +++ b/openpype/hosts/maya/plugins/publish/collect_animation.py @@ -46,7 +46,6 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): hierarchy = members + descendants - # Ignore certain node types (e.g. constraints) ignore = cmds.ls(hierarchy, type=self.ignore_type, long=True) if ignore: @@ -58,3 +57,18 @@ class CollectAnimationOutputGeometry(pyblish.api.InstancePlugin): if instance.data.get("farm"): instance.data["families"].append("publish.farm") + + # Collect user defined attributes. + if not instance.data.get("includeUserDefinedAttributes", False): + return + + user_defined_attributes = set() + for node in hierarchy: + attrs = cmds.listAttr(node, userDefined=True) or list() + shapes = cmds.listRelatives(node, shapes=True) or list() + for shape in shapes: + attrs.extend(cmds.listAttr(shape, userDefined=True) or list()) + + user_defined_attributes.update(attrs) + + instance.data["userDefinedAttributes"] = list(user_defined_attributes) From b12d1e9ff99f26435a167c6f17c9a9f9e2b79cdb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Feb 2023 12:23:10 +0000 Subject: [PATCH 462/483] Add project settings. --- .../hosts/maya/plugins/create/create_pointcache.py | 4 +++- openpype/settings/defaults/project_settings/maya.json | 2 ++ .../projects_schema/schemas/schema_maya_create.json | 10 ++++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/create/create_pointcache.py b/openpype/hosts/maya/plugins/create/create_pointcache.py index 51eab94a1a..1b8d5e6850 100644 --- a/openpype/hosts/maya/plugins/create/create_pointcache.py +++ b/openpype/hosts/maya/plugins/create/create_pointcache.py @@ -15,6 +15,7 @@ class CreatePointCache(plugin.Creator): icon = "gears" write_color_sets = False write_face_sets = False + include_user_defined_attributes = False def __init__(self, *args, **kwargs): super(CreatePointCache, self).__init__(*args, **kwargs) @@ -33,7 +34,8 @@ class CreatePointCache(plugin.Creator): self.data["refresh"] = False # Default to suspend refresh. # Add options for custom attributes - self.data["includeUserDefinedAttributes"] = True + value = self.include_user_defined_attributes + self.data["includeUserDefinedAttributes"] = value self.data["attr"] = "" self.data["attrPrefix"] = "" diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 32b141566b..2559448900 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -147,6 +147,7 @@ "enabled": true, "write_color_sets": false, "write_face_sets": false, + "include_user_defined_attributes": false, "defaults": [ "Main" ] @@ -165,6 +166,7 @@ "enabled": true, "write_color_sets": false, "write_face_sets": false, + "include_user_defined_attributes": false, "defaults": [ "Main" ] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json index e1a3082616..77a39f692f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_create.json @@ -132,6 +132,11 @@ "key": "write_face_sets", "label": "Write Face Sets" }, + { + "type": "boolean", + "key": "include_user_defined_attributes", + "label": "Include User Defined Attributes" + }, { "type": "list", "key": "defaults", @@ -192,6 +197,11 @@ "key": "write_face_sets", "label": "Write Face Sets" }, + { + "type": "boolean", + "key": "include_user_defined_attributes", + "label": "Include User Defined Attributes" + }, { "type": "list", "key": "defaults", From ba927f068f115521d9a51564e1e3b7cb0f4a52e6 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Feb 2023 15:39:15 +0000 Subject: [PATCH 463/483] BigRoy feedback --- openpype/hosts/maya/plugins/publish/collect_pointcache.py | 5 +---- openpype/hosts/maya/plugins/publish/extract_pointcache.py | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_pointcache.py b/openpype/hosts/maya/plugins/publish/collect_pointcache.py index 72aa37fc11..d0430c5612 100644 --- a/openpype/hosts/maya/plugins/publish/collect_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/collect_pointcache.py @@ -47,11 +47,8 @@ class CollectPointcache(pyblish.api.InstancePlugin): if not instance.data.get("includeUserDefinedAttributes", False): return - all_nodes = ( - instance.data["setMembers"] + instance.data.get("proxy", []) - ) user_defined_attributes = set() - for node in all_nodes: + for node in instance: attrs = cmds.listAttr(node, userDefined=True) or list() shapes = cmds.listRelatives(node, shapes=True) or list() for shape in shapes: diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 8e794d4e17..e551858d48 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -39,7 +39,9 @@ class ExtractAlembic(publish.Extractor): start = float(instance.data.get("frameStartHandle", 1)) end = float(instance.data.get("frameEndHandle", 1)) - attrs = instance.data.get("userDefinedAttributes", []) + attrs = instance.data.get("attr", "").split(";") + attrs = [value for value in attrs if value.strip()] + attrs += instance.data.get("userDefinedAttributes", []) attrs += ["cbId"] attr_prefixes = instance.data.get("attrPrefix", "").split(";") From 421e24055b03d3c3a910ebf74e102ef5b6144f3b Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 23 Feb 2023 17:14:36 +0000 Subject: [PATCH 464/483] Documentation --- website/docs/artist_hosts_maya.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index 9fab845e62..73bff286b9 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -314,10 +314,18 @@ Example setup: ![Maya - Point Cache Example](assets/maya-pointcache_setup.png) -:::note Publish on farm -If your studio has Deadline configured, artists could choose to offload potentially long running export of pointache and publish it to the farm. -Only thing that is necessary is to toggle `Farm` property in created pointcache instance to True. -::: +#### Options + +- **Frame Start**: which frame to start the export at. +- **Frame End**: which frame to end the export at. +- **Handle Start**: additional frames to export at frame start. Ei. frame start - handle start = export start. +- **Handle Start**: additional frames to export at frame end. Ei. frame end + handle end = export end. +- **Step**: frequency of sampling the export. For example when dealing with quick movements for motion blur, a step size of less than 1 might be better. +- **Refresh**: refresh the viewport when exporting the pointcache. For performance is best to leave off, but certain situations can require to refresh the viewport, for example using the Bullet plugin. +- **Attr**: specific attributes to publish separated by `;` +- **Include User Defined Attribudes**: include all user defined attributes in the publish. +- **Farm**: if your studio has Deadline configured, artists could choose to offload potentially long running export of pointache and publish it to the farm. Only thing that is necessary is to toggle this attribute in created pointcache instance to True. +- **Priority**: Farm priority. ### Loading Point Caches From aee263d24da72655d48942ee198cf95cd94054fc Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 24 Feb 2023 08:56:52 +0000 Subject: [PATCH 465/483] Update website/docs/artist_hosts_maya.md --- website/docs/artist_hosts_maya.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index 73bff286b9..c2100204b8 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -322,7 +322,8 @@ Example setup: - **Handle Start**: additional frames to export at frame end. Ei. frame end + handle end = export end. - **Step**: frequency of sampling the export. For example when dealing with quick movements for motion blur, a step size of less than 1 might be better. - **Refresh**: refresh the viewport when exporting the pointcache. For performance is best to leave off, but certain situations can require to refresh the viewport, for example using the Bullet plugin. -- **Attr**: specific attributes to publish separated by `;` +- **Attr**: specific attributes to publish separated by `;`. +- **AttrPrefix**: Prefix filter for determining which geometric attributes to write out, separated by `;`. - **Include User Defined Attribudes**: include all user defined attributes in the publish. - **Farm**: if your studio has Deadline configured, artists could choose to offload potentially long running export of pointache and publish it to the farm. Only thing that is necessary is to toggle this attribute in created pointcache instance to True. - **Priority**: Farm priority. From 30768de8502db34e2d03d2440262733e61002949 Mon Sep 17 00:00:00 2001 From: Toke Jepsen Date: Fri, 24 Feb 2023 10:15:01 +0000 Subject: [PATCH 466/483] Update website/docs/artist_hosts_maya.md Co-authored-by: Roy Nieterau --- website/docs/artist_hosts_maya.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/artist_hosts_maya.md b/website/docs/artist_hosts_maya.md index c2100204b8..07495fbc96 100644 --- a/website/docs/artist_hosts_maya.md +++ b/website/docs/artist_hosts_maya.md @@ -323,7 +323,7 @@ Example setup: - **Step**: frequency of sampling the export. For example when dealing with quick movements for motion blur, a step size of less than 1 might be better. - **Refresh**: refresh the viewport when exporting the pointcache. For performance is best to leave off, but certain situations can require to refresh the viewport, for example using the Bullet plugin. - **Attr**: specific attributes to publish separated by `;`. -- **AttrPrefix**: Prefix filter for determining which geometric attributes to write out, separated by `;`. +- **AttrPrefix**: specific attributes which start with this prefix to publish separated by `;`. - **Include User Defined Attribudes**: include all user defined attributes in the publish. - **Farm**: if your studio has Deadline configured, artists could choose to offload potentially long running export of pointache and publish it to the farm. Only thing that is necessary is to toggle this attribute in created pointcache instance to True. - **Priority**: Farm priority. From 90955e577900ad86072468f33cbf26af3b2a5116 Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 25 Jan 2023 11:31:27 +0100 Subject: [PATCH 467/483] Refactored the generation of UE projects, plugin is now being installed in the engine. --- openpype/hosts/unreal/addon.py | 2 +- .../unreal/hooks/pre_workfile_preparation.py | 28 ++- .../UE_4.7/CommandletProject/.gitignore | 6 + .../CommandletProject.uproject | 12 + .../Config/DefaultEditor.ini | 0 .../Config/DefaultEngine.ini | 16 ++ .../CommandletProject/Config/DefaultGame.ini | 4 + .../UE_4.7/{ => OpenPype}/.gitignore | 0 .../Config/DefaultOpenPypeSettings.ini | 0 .../UE_4.7/OpenPype/Config/FilterPlugin.ini | 8 + .../Content/Python/init_unreal.py | 0 .../OpenPype}/OpenPype.uplugin | 4 +- .../UE_4.7/{ => OpenPype}/README.md | 0 .../{ => OpenPype}/Resources/openpype128.png | Bin .../{ => OpenPype}/Resources/openpype40.png | Bin .../{ => OpenPype}/Resources/openpype512.png | Bin .../Source/OpenPype/OpenPype.Build.cs | 5 +- .../OPGenerateProjectCommandlet.cpp | 140 +++++++++++ .../Private/Commandlets/OPActionResult.cpp | 41 ++++ .../OpenPype/Private/Logging/OP_Log.cpp | 1 + .../Source/OpenPype/Private/OpenPype.cpp | 42 ++-- .../Source/OpenPype/Private/OpenPypeLib.cpp | 0 .../Private/OpenPypePublishInstance.cpp | 4 +- .../OpenPypePublishInstanceFactory.cpp | 0 .../OpenPype/Private/OpenPypePythonBridge.cpp | 0 .../OpenPype/Private/OpenPypeSettings.cpp | 3 +- .../Source/OpenPype/Private/OpenPypeStyle.cpp | 3 +- .../OPGenerateProjectCommandlet.h | 60 +++++ .../Public/Commandlets/OPActionResult.h | 83 +++++++ .../Source/OpenPype/Public/Logging/OP_Log.h | 3 + .../Source/OpenPype/Public/OPConstants.h | 12 + .../Source/OpenPype/Public/OpenPype.h | 0 .../Source/OpenPype/Public/OpenPypeLib.h | 0 .../OpenPype/Public/OpenPypePublishInstance.h | 6 +- .../Public/OpenPypePublishInstanceFactory.h | 0 .../OpenPype/Public/OpenPypePythonBridge.h | 0 .../Source/OpenPype/Public/OpenPypeSettings.h | 1 - .../Source/OpenPype/Public/OpenPypeStyle.h | 0 .../OpenPype/Private/AssetContainer.cpp | 115 --------- .../Private/AssetContainerFactory.cpp | 20 -- .../Source/OpenPype/Public/AssetContainer.h | 39 --- .../OpenPype/Public/AssetContainerFactory.h | 21 -- .../UE_5.0/CommandletProject/.gitignore | 6 + .../CommandletProject.uproject | 20 ++ .../Config/DefaultEditor.ini | 0 .../Config/DefaultEngine.ini | 42 ++++ .../CommandletProject/Config/DefaultGame.ini | 4 + .../UE_5.0/{ => OpenPype}/.gitignore | 0 .../Config/DefaultOpenPypeSettings.ini | 0 .../UE_5.0/OpenPype/Config/FilterPlugin.ini | 8 + .../Content/Python/init_unreal.py | 0 .../OpenPype}/OpenPype.uplugin | 1 + .../UE_5.0/{ => OpenPype}/README.md | 0 .../{ => OpenPype}/Resources/openpype128.png | Bin .../{ => OpenPype}/Resources/openpype40.png | Bin .../{ => OpenPype}/Resources/openpype512.png | Bin .../Source/OpenPype/OpenPype.Build.cs | 5 +- .../OPGenerateProjectCommandlet.cpp | 140 +++++++++++ .../Private/Commandlets/OPActionResult.cpp | 41 ++++ .../OpenPype/Private/Logging/OP_Log.cpp | 1 + .../Source/OpenPype/Private/OpenPype.cpp | 0 .../OpenPype/Private/OpenPypeCommands.cpp | 0 .../Source/OpenPype/Private/OpenPypeLib.cpp | 0 .../Private/OpenPypePublishInstance.cpp | 2 +- .../OpenPypePublishInstanceFactory.cpp | 0 .../OpenPype/Private/OpenPypePythonBridge.cpp | 0 .../OpenPype/Private/OpenPypeSettings.cpp | 0 .../Source/OpenPype/Private/OpenPypeStyle.cpp | 0 .../OPGenerateProjectCommandlet.h | 60 +++++ .../Public/Commandlets/OPActionResult.h | 83 +++++++ .../Source/OpenPype/Public/Logging/OP_Log.h | 3 + .../Source/OpenPype/Public/OPConstants.h | 12 + .../Source/OpenPype/Public/OpenPype.h | 0 .../Source/OpenPype/Public/OpenPypeCommands.h | 0 .../Source/OpenPype/Public/OpenPypeLib.h | 0 .../OpenPype/Public/OpenPypePublishInstance.h | 6 +- .../Public/OpenPypePublishInstanceFactory.h | 0 .../OpenPype/Public/OpenPypePythonBridge.h | 0 .../Source/OpenPype/Public/OpenPypeSettings.h | 0 .../Source/OpenPype/Public/OpenPypeStyle.h | 0 .../OpenPype/Private/AssetContainer.cpp | 115 --------- .../Private/AssetContainerFactory.cpp | 20 -- .../Source/OpenPype/Public/AssetContainer.h | 39 --- .../OpenPype/Public/AssetContainerFactory.h | 21 -- openpype/hosts/unreal/lib.py | 230 +++++++++++++----- 85 files changed, 1029 insertions(+), 509 deletions(-) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore create mode 100644 openpype/hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject create mode 100644 openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEditor.ini create mode 100644 openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEngine.ini create mode 100644 openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultGame.ini rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/.gitignore (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Config/DefaultOpenPypeSettings.ini (100%) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/FilterPlugin.ini rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Content/Python/init_unreal.py (100%) rename openpype/hosts/unreal/integration/{UE_5.0 => UE_4.7/OpenPype}/OpenPype.uplugin (90%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/README.md (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Resources/openpype128.png (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Resources/openpype40.png (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Resources/openpype512.png (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/OpenPype.Build.cs (92%) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPype.cpp (79%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypeLib.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypePublishInstance.cpp (98%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypePythonBridge.cpp (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypeSettings.cpp (91%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Private/OpenPypeStyle.cpp (93%) create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h create mode 100644 openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPype.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypeLib.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypePublishInstance.h (94%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypePythonBridge.h (100%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypeSettings.h (97%) rename openpype/hosts/unreal/integration/UE_4.7/{ => OpenPype}/Source/OpenPype/Public/OpenPypeStyle.h (100%) delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainer.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainer.h delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainerFactory.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore create mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject create mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEditor.ini create mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEngine.ini create mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultGame.ini rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/.gitignore (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Config/DefaultOpenPypeSettings.ini (100%) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/FilterPlugin.ini rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Content/Python/init_unreal.py (100%) rename openpype/hosts/unreal/integration/{UE_4.7 => UE_5.0/OpenPype}/OpenPype.uplugin (95%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/README.md (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Resources/openpype128.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Resources/openpype40.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Resources/openpype512.png (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/OpenPype.Build.cs (92%) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPype.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypeCommands.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypeLib.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypePublishInstance.cpp (99%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypePythonBridge.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypeSettings.cpp (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Private/OpenPypeStyle.cpp (100%) create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h create mode 100644 openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPype.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypeCommands.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypeLib.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypePublishInstance.h (94%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypePythonBridge.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypeSettings.h (100%) rename openpype/hosts/unreal/integration/UE_5.0/{ => OpenPype}/Source/OpenPype/Public/OpenPypeStyle.h (100%) delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainer.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainerFactory.cpp delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainer.h delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainerFactory.h diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index e2c8484651..c92a44870f 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -17,7 +17,7 @@ class UnrealAddon(OpenPypeModule, IHostAddon): ue_plugin = "UE_5.0" if app.name[:1] == "5" else "UE_4.7" unreal_plugin_path = os.path.join( - UNREAL_ROOT_DIR, "integration", ue_plugin + UNREAL_ROOT_DIR, "integration", ue_plugin, "OpenPype" ) if not env.get("OPENPYPE_UNREAL_PLUGIN"): env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 2dc6fb9f42..821018ba9d 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -119,29 +119,33 @@ class UnrealPrelaunchHook(PreLaunchHook): f"detected [ {engine_version} ]" )) - ue_path = unreal_lib.get_editor_executable_path( + ue_path = unreal_lib.get_editor_exe_path( Path(detected[engine_version]), engine_version) self.launch_context.launch_args = [ue_path.as_posix()] project_path.mkdir(parents=True, exist_ok=True) + # Set "OPENPYPE_UNREAL_PLUGIN" to current process environment for + # execution of `create_unreal_project` + if self.launch_context.env.get("OPENPYPE_UNREAL_PLUGIN"): + self.log.info(( + f"{self.signature} using OpenPype plugin from " + f"{self.launch_context.env.get('OPENPYPE_UNREAL_PLUGIN')}" + )) + env_key = "OPENPYPE_UNREAL_PLUGIN" + if self.launch_context.env.get(env_key): + os.environ[env_key] = self.launch_context.env[env_key] + + engine_path = detected[engine_version] + + unreal_lib.try_installing_plugin(Path(engine_path), engine_version) + project_file = project_path / unreal_project_filename if not project_file.is_file(): - engine_path = detected[engine_version] self.log.info(( f"{self.signature} creating unreal " f"project [ {unreal_project_name} ]" )) - # Set "OPENPYPE_UNREAL_PLUGIN" to current process environment for - # execution of `create_unreal_project` - if self.launch_context.env.get("OPENPYPE_UNREAL_PLUGIN"): - self.log.info(( - f"{self.signature} using OpenPype plugin from " - f"{self.launch_context.env.get('OPENPYPE_UNREAL_PLUGIN')}" - )) - env_key = "OPENPYPE_UNREAL_PLUGIN" - if self.launch_context.env.get(env_key): - os.environ[env_key] = self.launch_context.env[env_key] unreal_lib.create_unreal_project( unreal_project_name, diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore new file mode 100644 index 0000000000..1004610e4f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore @@ -0,0 +1,6 @@ +/Saved +/DerivedDataCache +/Intermediate +/Binaries +/.idea +/.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject new file mode 100644 index 0000000000..4d75e03bf3 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject @@ -0,0 +1,12 @@ +{ + "FileVersion": 3, + "EngineAssociation": "4.27", + "Category": "", + "Description": "", + "Plugins": [ + { + "Name": "OpenPype", + "Enabled": true + } + ] +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEditor.ini b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEditor.ini new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEngine.ini b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEngine.ini new file mode 100644 index 0000000000..2845baccca --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEngine.ini @@ -0,0 +1,16 @@ + + +[/Script/EngineSettings.GameMapsSettings] +GameDefaultMap=/Engine/Maps/Templates/Template_Default.Template_Default + + +[/Script/HardwareTargeting.HardwareTargetingSettings] +TargetedHardwareClass=Desktop +AppliedTargetedHardwareClass=Desktop +DefaultGraphicsPerformance=Maximum +AppliedDefaultGraphicsPerformance=Maximum + +[/Script/Engine.Engine] ++ActiveGameNameRedirects=(OldGameName="TP_BlankBP",NewGameName="/Script/CommandletProject") ++ActiveGameNameRedirects=(OldGameName="/Script/TP_BlankBP",NewGameName="/Script/CommandletProject") + diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultGame.ini b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultGame.ini new file mode 100644 index 0000000000..40956de961 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultGame.ini @@ -0,0 +1,4 @@ + + +[/Script/EngineSettings.GeneralProjectSettings] +ProjectID=95AED0BF45A918DF73ABB3BB27D25356 diff --git a/openpype/hosts/unreal/integration/UE_4.7/.gitignore b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/.gitignore rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore diff --git a/openpype/hosts/unreal/integration/UE_4.7/Config/DefaultOpenPypeSettings.ini b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultOpenPypeSettings.ini similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Config/DefaultOpenPypeSettings.ini rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/DefaultOpenPypeSettings.ini diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/FilterPlugin.ini new file mode 100644 index 0000000000..ccebca2f32 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Config/FilterPlugin.ini @@ -0,0 +1,8 @@ +[FilterPlugin] +; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and +; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. +; +; Examples: +; /README.txt +; /Extras/... +; /Binaries/ThirdParty/*.dll diff --git a/openpype/hosts/unreal/integration/UE_4.7/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Content/Python/init_unreal.py similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Content/Python/init_unreal.py rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Content/Python/init_unreal.py diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin similarity index 90% rename from openpype/hosts/unreal/integration/UE_5.0/OpenPype.uplugin rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin index 4c7a74403c..23155cb74d 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin @@ -10,10 +10,10 @@ "DocsURL": "https://openpype.io/docs/artist_hosts_unreal", "MarketplaceURL": "", "SupportURL": "https://pype.club/", + "EngineVersion": "4.27", "CanContainContent": true, "IsBetaVersion": true, - "IsExperimentalVersion": false, - "Installed": false, + "Installed": true, "Modules": [ { "Name": "OpenPype", diff --git a/openpype/hosts/unreal/integration/UE_4.7/README.md b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/README.md similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/README.md rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/README.md diff --git a/openpype/hosts/unreal/integration/UE_4.7/Resources/openpype128.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype128.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Resources/openpype128.png rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype128.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/Resources/openpype40.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype40.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Resources/openpype40.png rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype40.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/Resources/openpype512.png b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype512.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Resources/openpype512.png rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Resources/openpype512.png diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs similarity index 92% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs index 46e5dcb2df..13afb11003 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/OpenPype.Build.cs +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs @@ -6,8 +6,8 @@ public class OpenPype : ModuleRules { public OpenPype(ReadOnlyTargetRules Target) : base(Target) { - PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - + PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; + PublicIncludePaths.AddRange( new string[] { // ... add public include paths required here ... @@ -34,6 +34,7 @@ public class OpenPype : ModuleRules PrivateDependencyModuleNames.AddRange( new string[] { + "GameProjectGeneration", "Projects", "InputCore", "UnrealEd", diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp new file mode 100644 index 0000000000..024a6097b3 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp @@ -0,0 +1,140 @@ +#include "Commandlets/Implementations/OPGenerateProjectCommandlet.h" + +#include "Editor.h" +#include "GameProjectUtils.h" +#include "OPConstants.h" +#include "Commandlets/OPActionResult.h" +#include "ProjectDescriptor.h" + +int32 UOPGenerateProjectCommandlet::Main(const FString& CommandLineParams) +{ + //Parses command line parameters & creates structure FProjectInformation + const FOPGenerateProjectParams ParsedParams = FOPGenerateProjectParams(CommandLineParams); + ProjectInformation = ParsedParams.GenerateUEProjectInformation(); + + //Creates .uproject & other UE files + EVALUATE_OP_ACTION_RESULT(TryCreateProject()); + + //Loads created .uproject + EVALUATE_OP_ACTION_RESULT(TryLoadProjectDescriptor()); + + //Adds needed plugin to .uproject + AttachPluginsToProjectDescriptor(); + + //Saves .uproject + EVALUATE_OP_ACTION_RESULT(TrySave()); + + //When we are here, there should not be problems in generating Unreal Project for OpenPype + return 0; +} + + +FOPGenerateProjectParams::FOPGenerateProjectParams(): FOPGenerateProjectParams("") +{ +} + +FOPGenerateProjectParams::FOPGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( + CommandLineParams) +{ + UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); +} + +FProjectInformation FOPGenerateProjectParams::GenerateUEProjectInformation() const +{ + FProjectInformation ProjectInformation = FProjectInformation(); + ProjectInformation.ProjectFilename = GetProjectFileName(); + + ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); + + return ProjectInformation; +} + +FString FOPGenerateProjectParams::TryGetToken(const int32 Index) const +{ + return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; +} + +FString FOPGenerateProjectParams::GetProjectFileName() const +{ + return TryGetToken(0); +} + +bool FOPGenerateProjectParams::IsSwitchPresent(const FString& Switch) const +{ + return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool + { + return Item.Equals(Switch); + } + ); +} + + +UOPGenerateProjectCommandlet::UOPGenerateProjectCommandlet() +{ + LogToConsole = true; +} + +FOP_ActionResult UOPGenerateProjectCommandlet::TryCreateProject() const +{ + FText FailReason; + FText FailLog; + TArray OutCreatedFiles; + + if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) + return FOP_ActionResult(EOP_ActionResult::ProjectNotCreated, FailReason); + return FOP_ActionResult(); +} + +FOP_ActionResult UOPGenerateProjectCommandlet::TryLoadProjectDescriptor() +{ + FText FailReason; + const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); + + return FOP_ActionResult(bLoaded ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotLoaded, FailReason); +} + +void UOPGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() +{ + FPluginReferenceDescriptor OPPluginDescriptor; + OPPluginDescriptor.bEnabled = true; + OPPluginDescriptor.Name = OPConstants::OP_PluginName; + ProjectDescriptor.Plugins.Add(OPPluginDescriptor); + + FPluginReferenceDescriptor PythonPluginDescriptor; + PythonPluginDescriptor.bEnabled = true; + PythonPluginDescriptor.Name = OPConstants::PythonScript_PluginName; + ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); + + FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; + SequencerScriptingPluginDescriptor.bEnabled = true; + SequencerScriptingPluginDescriptor.Name = OPConstants::SequencerScripting_PluginName; + ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); + + FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; + MovieRenderPipelinePluginDescriptor.bEnabled = true; + MovieRenderPipelinePluginDescriptor.Name = OPConstants::MovieRenderPipeline_PluginName; + ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); + + FPluginReferenceDescriptor EditorScriptingPluginDescriptor; + EditorScriptingPluginDescriptor.bEnabled = true; + EditorScriptingPluginDescriptor.Name = OPConstants::EditorScriptingUtils_PluginName; + ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); +} + +FOP_ActionResult UOPGenerateProjectCommandlet::TrySave() +{ + FText FailReason; + const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); + + return FOP_ActionResult(bSaved ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotSaved, FailReason); +} + +FOPGenerateProjectParams UOPGenerateProjectCommandlet::ParseParameters(const FString& Params) const +{ + FOPGenerateProjectParams ParamsResult; + + TArray Tokens, Switches; + ParseCommandLine(*Params, Tokens, Switches); + + return ParamsResult; +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp new file mode 100644 index 0000000000..9236fbb057 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp @@ -0,0 +1,41 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "Commandlets/OPActionResult.h" +#include "Logging/OP_Log.h" + +EOP_ActionResult::Type& FOP_ActionResult::GetStatus() +{ + return Status; +} + +FText& FOP_ActionResult::GetReason() +{ + return Reason; +} + +FOP_ActionResult::FOP_ActionResult():Status(EOP_ActionResult::Type::Ok) +{ + +} + +FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum):Status(InEnum) +{ + TryLog(); +} + +FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) +{ + TryLog(); +}; + +bool FOP_ActionResult::IsProblem() const +{ + return Status != EOP_ActionResult::Ok; +} + +void FOP_ActionResult::TryLog() const +{ + if(IsProblem()) + UE_LOG(LogCommandletOPGenerateProject, Error, TEXT("%s"), *Reason.ToString()); +} diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp new file mode 100644 index 0000000000..29b1068c21 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp @@ -0,0 +1 @@ +#include "Logging/OP_Log.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp similarity index 79% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp index d06a08eb43..a510a5e3bf 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPype.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp @@ -16,32 +16,34 @@ static const FName OpenPypeTabName("OpenPype"); // This function is triggered when the plugin is staring up void FOpenPypeModule::StartupModule() { - FOpenPypeStyle::Initialize(); - FOpenPypeStyle::SetIcon("Logo", "openpype40"); + if (!IsRunningCommandlet()) { + FOpenPypeStyle::Initialize(); + FOpenPypeStyle::SetIcon("Logo", "openpype40"); - // Create the Extender that will add content to the menu - FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); + // Create the Extender that will add content to the menu + FLevelEditorModule& LevelEditorModule = FModuleManager::LoadModuleChecked("LevelEditor"); - TSharedPtr MenuExtender = MakeShareable(new FExtender()); - TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); + TSharedPtr MenuExtender = MakeShareable(new FExtender()); + TSharedPtr ToolbarExtender = MakeShareable(new FExtender()); - MenuExtender->AddMenuExtension( - "LevelEditor", - EExtensionHook::After, - NULL, - FMenuExtensionDelegate::CreateRaw(this, &FOpenPypeModule::AddMenuEntry) - ); - ToolbarExtender->AddToolBarExtension( - "Settings", - EExtensionHook::After, - NULL, - FToolBarExtensionDelegate::CreateRaw(this, &FOpenPypeModule::AddToobarEntry)); + MenuExtender->AddMenuExtension( + "LevelEditor", + EExtensionHook::After, + NULL, + FMenuExtensionDelegate::CreateRaw(this, &FOpenPypeModule::AddMenuEntry) + ); + ToolbarExtender->AddToolBarExtension( + "Settings", + EExtensionHook::After, + NULL, + FToolBarExtensionDelegate::CreateRaw(this, &FOpenPypeModule::AddToobarEntry)); - LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); - LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); + LevelEditorModule.GetMenuExtensibilityManager()->AddExtender(MenuExtender); + LevelEditorModule.GetToolBarExtensibilityManager()->AddExtender(ToolbarExtender); - RegisterSettings(); + RegisterSettings(); + } } void FOpenPypeModule::ShutdownModule() diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeLib.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp similarity index 98% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp index 38740f1cbd..424c4ed491 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp @@ -2,10 +2,10 @@ #include "OpenPypePublishInstance.h" #include "AssetRegistryModule.h" -#include "NotificationManager.h" #include "OpenPypeLib.h" #include "OpenPypeSettings.h" -#include "SNotificationList.h" +#include "Framework/Notifications/NotificationManager.h" +#include "Widgets/Notifications/SNotificationList.h" //Moves all the invalid pointers to the end to prepare them for the shrinking #define REMOVE_INVALID_ENTRIES(VAR) VAR.CompactStable(); \ diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypePythonBridge.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeSettings.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp similarity index 91% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeSettings.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp index 7134614d22..951b522308 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeSettings.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp @@ -2,8 +2,7 @@ #include "OpenPypeSettings.h" -#include "IPluginManager.h" -#include "UObjectGlobals.h" +#include "Interfaces/IPluginManager.h" /** * Mainly is used for initializing default values if the DefaultOpenPypeSettings.ini file does not exist in the saved config diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp similarity index 93% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeStyle.cpp rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp index a51c2d6aa5..b7abc38156 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/OpenPypeStyle.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp @@ -43,7 +43,7 @@ const FVector2D Icon40x40(40.0f, 40.0f); TUniquePtr< FSlateStyleSet > FOpenPypeStyle::Create() { TUniquePtr< FSlateStyleSet > Style = MakeUnique(GetStyleSetName()); - Style->SetContentRoot(FPaths::ProjectPluginsDir() / TEXT("OpenPype/Resources")); + Style->SetContentRoot(FPaths::EnginePluginsDir() / TEXT("Marketplace/OpenPype/Resources")); return Style; } @@ -66,5 +66,4 @@ const ISlateStyle& FOpenPypeStyle::Get() { check(OpenPypeStyleInstance); return *OpenPypeStyleInstance; - return *OpenPypeStyleInstance; } diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h new file mode 100644 index 0000000000..8738de6d4a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h @@ -0,0 +1,60 @@ +#pragma once + + +#include "GameProjectUtils.h" +#include "Commandlets/OPActionResult.h" +#include "ProjectDescriptor.h" +#include "Commandlets/Commandlet.h" +#include "OPGenerateProjectCommandlet.generated.h" + +struct FProjectDescriptor; +struct FProjectInformation; + +/** +* @brief Structure which parses command line parameters and generates FProjectInformation +*/ +USTRUCT() +struct FOPGenerateProjectParams +{ + GENERATED_BODY() + +private: + FString CommandLineParams; + TArray Tokens; + TArray Switches; + +public: + FOPGenerateProjectParams(); + FOPGenerateProjectParams(const FString& CommandLineParams); + + FProjectInformation GenerateUEProjectInformation() const; + +private: + FString TryGetToken(const int32 Index) const; + FString GetProjectFileName() const; + + bool IsSwitchPresent(const FString& Switch) const; +}; + +UCLASS() +class OPENPYPE_API UOPGenerateProjectCommandlet : public UCommandlet +{ + GENERATED_BODY() + +private: + FProjectInformation ProjectInformation; + FProjectDescriptor ProjectDescriptor; + +public: + UOPGenerateProjectCommandlet(); + + virtual int32 Main(const FString& CommandLineParams) override; + +private: + FOPGenerateProjectParams ParseParameters(const FString& Params) const; + FOP_ActionResult TryCreateProject() const; + FOP_ActionResult TryLoadProjectDescriptor(); + void AttachPluginsToProjectDescriptor(); + FOP_ActionResult TrySave(); +}; + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h new file mode 100644 index 0000000000..f46ba9c62a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h @@ -0,0 +1,83 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "OPActionResult.generated.h" + +/** + * @brief This macro returns error code when is problem or does nothing when there is no problem. + * @param ActionResult FOP_ActionResult structure + */ +#define EVALUATE_OP_ACTION_RESULT(ActionResult) \ + if(ActionResult.IsProblem()) \ + return ActionResult.GetStatus(); + +/** +* @brief This enum values are humanly readable mapping of error codes. +* Here should be all error codes to be possible find what went wrong. +* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... +*/ +UENUM() +namespace EOP_ActionResult +{ + enum Type + { + Ok, + ProjectNotCreated, + ProjectNotLoaded, + ProjectNotSaved, + //....Here insert another values + + //Do not remove! + //Usable for looping through enum values + __Last UMETA(Hidden) + }; +} + + +/** + * @brief This struct holds action result enum and optionally reason of fail + */ +USTRUCT() +struct FOP_ActionResult +{ + GENERATED_BODY() + +public: + /** @brief Default constructor usable when there is no problem */ + FOP_ActionResult(); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + */ + FOP_ActionResult(const EOP_ActionResult::Type& InEnum); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + * @param InReason Reason of potential fail + */ + FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason); + +private: + /** @brief Action status */ + EOP_ActionResult::Type Status; + + /** @brief Optional reason of fail */ + FText Reason; + +public: + /** + * @brief Checks if there is problematic state + * @return true when status is not equal to EOP_ActionResult::Ok + */ + bool IsProblem() const; + EOP_ActionResult::Type& GetStatus(); + FText& GetReason(); + +private: + void TryLog() const; +}; + diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h new file mode 100644 index 0000000000..4f8af3e2e6 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h @@ -0,0 +1,3 @@ +#pragma once + +DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h new file mode 100644 index 0000000000..21a033e426 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h @@ -0,0 +1,12 @@ +#pragma once + +namespace OPConstants +{ + const FString OP_PluginName = "OpenPype"; + const FString PythonScript_PluginName = "PythonScriptPlugin"; + const FString SequencerScripting_PluginName = "SequencerScripting"; + const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; + const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; +} + + diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPype.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeLib.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h similarity index 94% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstance.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h index cd414fe2cc..16b3194b96 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h @@ -16,7 +16,7 @@ public: * * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 */ - UFUNCTION(BlueprintCallable, BlueprintPure) + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") TSet GetInternalAssets() const { //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. @@ -33,7 +33,7 @@ public: * * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 */ - UFUNCTION(BlueprintCallable, BlueprintPure) + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") TSet GetExternalAssets() const { //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. @@ -53,7 +53,7 @@ public: * * @attention If the bAddExternalAssets variable is false, external assets won't be included! */ - UFUNCTION(BlueprintCallable, BlueprintPure) + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") TSet GetAllAssets() const { const TSet>& IteratedSet = bAddExternalAssets diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypePythonBridge.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeSettings.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h similarity index 97% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeSettings.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h index 2df6c887cf..9bdcfb2399 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeSettings.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h @@ -3,7 +3,6 @@ #pragma once #include "CoreMinimal.h" -#include "Object.h" #include "OpenPypeSettings.generated.h" #define OPENPYPE_SETTINGS_FILEPATH IPluginManager::Get().FindPlugin("OpenPype")->GetBaseDir() / TEXT("Config") / TEXT("DefaultOpenPypeSettings.ini") diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/OpenPypeStyle.h rename to openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainer.cpp deleted file mode 100644 index c766f87a8e..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainer.cpp +++ /dev/null @@ -1,115 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#include "AssetContainer.h" -#include "AssetRegistryModule.h" -#include "Misc/PackageName.h" -#include "Engine.h" -#include "Containers/UnrealString.h" - -UAssetContainer::UAssetContainer(const FObjectInitializer& ObjectInitializer) -: UAssetUserData(ObjectInitializer) -{ - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAssetContainer %s"), *path); - FARFilter Filter; - Filter.PackagePaths.Add(FName(*path)); - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAssetContainer::OnAssetAdded); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAssetContainer::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAssetContainer::OnAssetRenamed); -} - -void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClass.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - assets.Add(assetPath); - assetsData.Add(AssetData); - UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); - } - } -} - -void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClass.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - FString path = UAssetContainer::GetPathName(); - FString lpp = FPackageName::GetLongPackagePath(*path); - - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); - assets.Remove(assetPath); - assetsData.Remove(AssetData); - } - } -} - -void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClass.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - - assets.Remove(str); - assets.Add(assetPath); - assetsData.Remove(AssetData); - // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); - } - } -} - diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainerFactory.cpp deleted file mode 100644 index b943150bdd..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Private/AssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "AssetContainerFactory.h" -#include "AssetContainer.h" - -UAssetContainerFactory::UAssetContainerFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAssetContainer::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - UAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); - return AssetContainer; -} - -bool UAssetContainerFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainer.h deleted file mode 100644 index 3c2a360c78..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainer.h +++ /dev/null @@ -1,39 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/NoExportTypes.h" -#include "Engine/AssetUserData.h" -#include "AssetData.h" -#include "AssetContainer.generated.h" - -/** - * - */ -UCLASS(Blueprintable) -class OPENPYPE_API UAssetContainer : public UAssetUserData -{ - GENERATED_BODY() - -public: - - UAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAssetContainer(); - - UPROPERTY(EditAnywhere, BlueprintReadOnly) - TArray assets; - - // There seems to be no reflection option to expose array of FAssetData - /* - UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) - TArray assetsData; - */ -private: - TArray assetsData; - void OnAssetAdded(const FAssetData& AssetData); - void OnAssetRemoved(const FAssetData& AssetData); - void OnAssetRenamed(const FAssetData& AssetData, const FString& str); -}; - - diff --git a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainerFactory.h deleted file mode 100644 index 331ce6bb50..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/Source/OpenPype/Public/AssetContainerFactory.h +++ /dev/null @@ -1,21 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AssetContainerFactory.generated.h" - -/** - * - */ -UCLASS() -class OPENPYPE_API UAssetContainerFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAssetContainerFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore new file mode 100644 index 0000000000..1004610e4f --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore @@ -0,0 +1,6 @@ +/Saved +/DerivedDataCache +/Intermediate +/Binaries +/.idea +/.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject new file mode 100644 index 0000000000..c8dc1c673e --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject @@ -0,0 +1,20 @@ +{ + "FileVersion": 3, + "EngineAssociation": "5.0", + "Category": "", + "Description": "", + "Plugins": [ + { + "Name": "ModelingToolsEditorMode", + "Enabled": true, + "TargetAllowList": [ + "Editor" + ] + }, + { + "Name": "OpenPype", + "Enabled": true, + "Type": "Editor" + } + ] +} \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEditor.ini b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEditor.ini new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEngine.ini b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEngine.ini new file mode 100644 index 0000000000..3f5357dac4 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEngine.ini @@ -0,0 +1,42 @@ + + +[/Script/EngineSettings.GameMapsSettings] +GameDefaultMap=/Engine/Maps/Templates/OpenWorld + + +[/Script/HardwareTargeting.HardwareTargetingSettings] +TargetedHardwareClass=Desktop +AppliedTargetedHardwareClass=Desktop +DefaultGraphicsPerformance=Maximum +AppliedDefaultGraphicsPerformance=Maximum + +[/Script/WindowsTargetPlatform.WindowsTargetSettings] +DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 + +[/Script/Engine.RendererSettings] +r.GenerateMeshDistanceFields=True +r.DynamicGlobalIlluminationMethod=1 +r.ReflectionMethod=1 +r.Shadow.Virtual.Enable=1 + +[/Script/WorldPartitionEditor.WorldPartitionEditorSettings] +CommandletClass=Class'/Script/UnrealEd.WorldPartitionConvertCommandlet' + +[/Script/Engine.Engine] ++ActiveGameNameRedirects=(OldGameName="TP_BlankBP",NewGameName="/Script/CommandletProject") ++ActiveGameNameRedirects=(OldGameName="/Script/TP_BlankBP",NewGameName="/Script/CommandletProject") + +[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] +bEnablePlugin=True +bAllowNetworkConnection=True +SecurityToken=684C16AF4BD96F1D6828A6B067693175 +bIncludeInShipping=False +bAllowExternalStartInShipping=False +bCompileAFSProject=False +bUseCompression=False +bLogFiles=False +bReportStats=False +ConnectionType=USBOnly +bUseManualIPAddress=False +ManualIPAddress= + diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultGame.ini b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultGame.ini new file mode 100644 index 0000000000..c661b739ab --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultGame.ini @@ -0,0 +1,4 @@ + + +[/Script/EngineSettings.GeneralProjectSettings] +ProjectID=D528076140C577E5807BA5BA135366BB diff --git a/openpype/hosts/unreal/integration/UE_5.0/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/.gitignore similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/.gitignore rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/.gitignore diff --git a/openpype/hosts/unreal/integration/UE_5.0/Config/DefaultOpenPypeSettings.ini b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultOpenPypeSettings.ini similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Config/DefaultOpenPypeSettings.ini rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/DefaultOpenPypeSettings.ini diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/FilterPlugin.ini b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/FilterPlugin.ini new file mode 100644 index 0000000000..ccebca2f32 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Config/FilterPlugin.ini @@ -0,0 +1,8 @@ +[FilterPlugin] +; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and +; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. +; +; Examples: +; /README.txt +; /Extras/... +; /Binaries/ThirdParty/*.dll diff --git a/openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Content/Python/init_unreal.py rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Content/Python/init_unreal.py diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin similarity index 95% rename from openpype/hosts/unreal/integration/UE_4.7/OpenPype.uplugin rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin index 4c7a74403c..b89eb43949 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin @@ -11,6 +11,7 @@ "MarketplaceURL": "", "SupportURL": "https://pype.club/", "CanContainContent": true, + "EngineVersion": "5.0", "IsBetaVersion": true, "IsExperimentalVersion": false, "Installed": false, diff --git a/openpype/hosts/unreal/integration/UE_5.0/README.md b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/README.md similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/README.md rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/README.md diff --git a/openpype/hosts/unreal/integration/UE_5.0/Resources/openpype128.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype128.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Resources/openpype128.png rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype128.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/Resources/openpype40.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype40.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Resources/openpype40.png rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype40.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/Resources/openpype512.png b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype512.png similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Resources/openpype512.png rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Resources/openpype512.png diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs similarity index 92% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs index d853ec028f..99c1c7b306 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/OpenPype.Build.cs +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs @@ -10,7 +10,7 @@ public class OpenPype : ModuleRules bLegacyPublicIncludePaths = false; ShadowVariableWarningLevel = WarningLevel.Error; PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; - IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_0; + //IncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_0; PublicIncludePaths.AddRange( new string[] { @@ -30,14 +30,15 @@ public class OpenPype : ModuleRules new string[] { "Core", + "CoreUObject" // ... add other public dependencies that you statically link with here ... } ); - PrivateDependencyModuleNames.AddRange( new string[] { + "GameProjectGeneration", "Projects", "InputCore", "EditorFramework", diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp new file mode 100644 index 0000000000..024a6097b3 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp @@ -0,0 +1,140 @@ +#include "Commandlets/Implementations/OPGenerateProjectCommandlet.h" + +#include "Editor.h" +#include "GameProjectUtils.h" +#include "OPConstants.h" +#include "Commandlets/OPActionResult.h" +#include "ProjectDescriptor.h" + +int32 UOPGenerateProjectCommandlet::Main(const FString& CommandLineParams) +{ + //Parses command line parameters & creates structure FProjectInformation + const FOPGenerateProjectParams ParsedParams = FOPGenerateProjectParams(CommandLineParams); + ProjectInformation = ParsedParams.GenerateUEProjectInformation(); + + //Creates .uproject & other UE files + EVALUATE_OP_ACTION_RESULT(TryCreateProject()); + + //Loads created .uproject + EVALUATE_OP_ACTION_RESULT(TryLoadProjectDescriptor()); + + //Adds needed plugin to .uproject + AttachPluginsToProjectDescriptor(); + + //Saves .uproject + EVALUATE_OP_ACTION_RESULT(TrySave()); + + //When we are here, there should not be problems in generating Unreal Project for OpenPype + return 0; +} + + +FOPGenerateProjectParams::FOPGenerateProjectParams(): FOPGenerateProjectParams("") +{ +} + +FOPGenerateProjectParams::FOPGenerateProjectParams(const FString& CommandLineParams): CommandLineParams( + CommandLineParams) +{ + UCommandlet::ParseCommandLine(*CommandLineParams, Tokens, Switches); +} + +FProjectInformation FOPGenerateProjectParams::GenerateUEProjectInformation() const +{ + FProjectInformation ProjectInformation = FProjectInformation(); + ProjectInformation.ProjectFilename = GetProjectFileName(); + + ProjectInformation.bShouldGenerateCode = IsSwitchPresent("GenerateCode"); + + return ProjectInformation; +} + +FString FOPGenerateProjectParams::TryGetToken(const int32 Index) const +{ + return Tokens.IsValidIndex(Index) ? Tokens[Index] : ""; +} + +FString FOPGenerateProjectParams::GetProjectFileName() const +{ + return TryGetToken(0); +} + +bool FOPGenerateProjectParams::IsSwitchPresent(const FString& Switch) const +{ + return INDEX_NONE != Switches.IndexOfByPredicate([&Switch](const FString& Item) -> bool + { + return Item.Equals(Switch); + } + ); +} + + +UOPGenerateProjectCommandlet::UOPGenerateProjectCommandlet() +{ + LogToConsole = true; +} + +FOP_ActionResult UOPGenerateProjectCommandlet::TryCreateProject() const +{ + FText FailReason; + FText FailLog; + TArray OutCreatedFiles; + + if (!GameProjectUtils::CreateProject(ProjectInformation, FailReason, FailLog, &OutCreatedFiles)) + return FOP_ActionResult(EOP_ActionResult::ProjectNotCreated, FailReason); + return FOP_ActionResult(); +} + +FOP_ActionResult UOPGenerateProjectCommandlet::TryLoadProjectDescriptor() +{ + FText FailReason; + const bool bLoaded = ProjectDescriptor.Load(ProjectInformation.ProjectFilename, FailReason); + + return FOP_ActionResult(bLoaded ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotLoaded, FailReason); +} + +void UOPGenerateProjectCommandlet::AttachPluginsToProjectDescriptor() +{ + FPluginReferenceDescriptor OPPluginDescriptor; + OPPluginDescriptor.bEnabled = true; + OPPluginDescriptor.Name = OPConstants::OP_PluginName; + ProjectDescriptor.Plugins.Add(OPPluginDescriptor); + + FPluginReferenceDescriptor PythonPluginDescriptor; + PythonPluginDescriptor.bEnabled = true; + PythonPluginDescriptor.Name = OPConstants::PythonScript_PluginName; + ProjectDescriptor.Plugins.Add(PythonPluginDescriptor); + + FPluginReferenceDescriptor SequencerScriptingPluginDescriptor; + SequencerScriptingPluginDescriptor.bEnabled = true; + SequencerScriptingPluginDescriptor.Name = OPConstants::SequencerScripting_PluginName; + ProjectDescriptor.Plugins.Add(SequencerScriptingPluginDescriptor); + + FPluginReferenceDescriptor MovieRenderPipelinePluginDescriptor; + MovieRenderPipelinePluginDescriptor.bEnabled = true; + MovieRenderPipelinePluginDescriptor.Name = OPConstants::MovieRenderPipeline_PluginName; + ProjectDescriptor.Plugins.Add(MovieRenderPipelinePluginDescriptor); + + FPluginReferenceDescriptor EditorScriptingPluginDescriptor; + EditorScriptingPluginDescriptor.bEnabled = true; + EditorScriptingPluginDescriptor.Name = OPConstants::EditorScriptingUtils_PluginName; + ProjectDescriptor.Plugins.Add(EditorScriptingPluginDescriptor); +} + +FOP_ActionResult UOPGenerateProjectCommandlet::TrySave() +{ + FText FailReason; + const bool bSaved = ProjectDescriptor.Save(ProjectInformation.ProjectFilename, FailReason); + + return FOP_ActionResult(bSaved ? EOP_ActionResult::Ok : EOP_ActionResult::ProjectNotSaved, FailReason); +} + +FOPGenerateProjectParams UOPGenerateProjectCommandlet::ParseParameters(const FString& Params) const +{ + FOPGenerateProjectParams ParamsResult; + + TArray Tokens, Switches; + ParseCommandLine(*Params, Tokens, Switches); + + return ParamsResult; +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp new file mode 100644 index 0000000000..9236fbb057 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp @@ -0,0 +1,41 @@ +// Fill out your copyright notice in the Description page of Project Settings. + + +#include "Commandlets/OPActionResult.h" +#include "Logging/OP_Log.h" + +EOP_ActionResult::Type& FOP_ActionResult::GetStatus() +{ + return Status; +} + +FText& FOP_ActionResult::GetReason() +{ + return Reason; +} + +FOP_ActionResult::FOP_ActionResult():Status(EOP_ActionResult::Type::Ok) +{ + +} + +FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum):Status(InEnum) +{ + TryLog(); +} + +FOP_ActionResult::FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason):Status(InEnum), Reason(InReason) +{ + TryLog(); +}; + +bool FOP_ActionResult::IsProblem() const +{ + return Status != EOP_ActionResult::Ok; +} + +void FOP_ActionResult::TryLog() const +{ + if(IsProblem()) + UE_LOG(LogCommandletOPGenerateProject, Error, TEXT("%s"), *Reason.ToString()); +} diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp new file mode 100644 index 0000000000..29b1068c21 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp @@ -0,0 +1 @@ +#include "Logging/OP_Log.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPype.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPype.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPype.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeCommands.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeLib.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp similarity index 99% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstance.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp index 0b56111a49..e6a85002c7 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp @@ -55,7 +55,7 @@ void UOpenPypePublishInstance::OnAssetCreated(const FAssetData& InAssetData) if (!IsValid(Asset)) { UE_LOG(LogAssetData, Warning, TEXT("Asset \"%s\" is not valid! Skipping the addition."), - *InAssetData.GetObjectPathString()); + *InAssetData.ObjectPath.ToString()); return; } diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypePythonBridge.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeSettings.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeSettings.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/OpenPypeStyle.cpp rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h new file mode 100644 index 0000000000..8738de6d4a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h @@ -0,0 +1,60 @@ +#pragma once + + +#include "GameProjectUtils.h" +#include "Commandlets/OPActionResult.h" +#include "ProjectDescriptor.h" +#include "Commandlets/Commandlet.h" +#include "OPGenerateProjectCommandlet.generated.h" + +struct FProjectDescriptor; +struct FProjectInformation; + +/** +* @brief Structure which parses command line parameters and generates FProjectInformation +*/ +USTRUCT() +struct FOPGenerateProjectParams +{ + GENERATED_BODY() + +private: + FString CommandLineParams; + TArray Tokens; + TArray Switches; + +public: + FOPGenerateProjectParams(); + FOPGenerateProjectParams(const FString& CommandLineParams); + + FProjectInformation GenerateUEProjectInformation() const; + +private: + FString TryGetToken(const int32 Index) const; + FString GetProjectFileName() const; + + bool IsSwitchPresent(const FString& Switch) const; +}; + +UCLASS() +class OPENPYPE_API UOPGenerateProjectCommandlet : public UCommandlet +{ + GENERATED_BODY() + +private: + FProjectInformation ProjectInformation; + FProjectDescriptor ProjectDescriptor; + +public: + UOPGenerateProjectCommandlet(); + + virtual int32 Main(const FString& CommandLineParams) override; + +private: + FOPGenerateProjectParams ParseParameters(const FString& Params) const; + FOP_ActionResult TryCreateProject() const; + FOP_ActionResult TryLoadProjectDescriptor(); + void AttachPluginsToProjectDescriptor(); + FOP_ActionResult TrySave(); +}; + diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h new file mode 100644 index 0000000000..f46ba9c62a --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h @@ -0,0 +1,83 @@ +// Fill out your copyright notice in the Description page of Project Settings. + +#pragma once + +#include "CoreMinimal.h" +#include "OPActionResult.generated.h" + +/** + * @brief This macro returns error code when is problem or does nothing when there is no problem. + * @param ActionResult FOP_ActionResult structure + */ +#define EVALUATE_OP_ACTION_RESULT(ActionResult) \ + if(ActionResult.IsProblem()) \ + return ActionResult.GetStatus(); + +/** +* @brief This enum values are humanly readable mapping of error codes. +* Here should be all error codes to be possible find what went wrong. +* TODO: In the future should exists an web document where is mapped error code & what problem occured & how to repair it... +*/ +UENUM() +namespace EOP_ActionResult +{ + enum Type + { + Ok, + ProjectNotCreated, + ProjectNotLoaded, + ProjectNotSaved, + //....Here insert another values + + //Do not remove! + //Usable for looping through enum values + __Last UMETA(Hidden) + }; +} + + +/** + * @brief This struct holds action result enum and optionally reason of fail + */ +USTRUCT() +struct FOP_ActionResult +{ + GENERATED_BODY() + +public: + /** @brief Default constructor usable when there is no problem */ + FOP_ActionResult(); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + */ + FOP_ActionResult(const EOP_ActionResult::Type& InEnum); + + /** + * @brief This constructor initializes variables & attempts to log when is error + * @param InEnum Status + * @param InReason Reason of potential fail + */ + FOP_ActionResult(const EOP_ActionResult::Type& InEnum, const FText& InReason); + +private: + /** @brief Action status */ + EOP_ActionResult::Type Status; + + /** @brief Optional reason of fail */ + FText Reason; + +public: + /** + * @brief Checks if there is problematic state + * @return true when status is not equal to EOP_ActionResult::Ok + */ + bool IsProblem() const; + EOP_ActionResult::Type& GetStatus(); + FText& GetReason(); + +private: + void TryLog() const; +}; + diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h new file mode 100644 index 0000000000..4f8af3e2e6 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h @@ -0,0 +1,3 @@ +#pragma once + +DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h new file mode 100644 index 0000000000..21a033e426 --- /dev/null +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h @@ -0,0 +1,12 @@ +#pragma once + +namespace OPConstants +{ + const FString OP_PluginName = "OpenPype"; + const FString PythonScript_PluginName = "PythonScriptPlugin"; + const FString SequencerScripting_PluginName = "SequencerScripting"; + const FString MovieRenderPipeline_PluginName = "MovieRenderPipeline"; + const FString EditorScriptingUtils_PluginName = "EditorScriptingUtilities"; +} + + diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPype.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeCommands.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeLib.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h similarity index 94% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstance.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h index 146025bd6d..c221f64135 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h @@ -17,7 +17,7 @@ public: * * @return - Set of UObjects. Careful! They are returning raw pointers. Seems like an issue in UE5 */ - UFUNCTION(BlueprintCallable, BlueprintPure) + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") TSet GetInternalAssets() const { //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. @@ -34,7 +34,7 @@ public: * * @return - TSet of assets (UObjects). Careful! They are returning raw pointers. Seems like an issue in UE5 */ - UFUNCTION(BlueprintCallable, BlueprintPure) + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") TSet GetExternalAssets() const { //For some reason it can only return Raw Pointers? Seems like an issue which they haven't fixed. @@ -54,7 +54,7 @@ public: * * @attention If the bAddExternalAssets variable is false, external assets won't be included! */ - UFUNCTION(BlueprintCallable, BlueprintPure) + UFUNCTION(BlueprintCallable, BlueprintPure, Category="Python") TSet GetAllAssets() const { const TSet>& IteratedSet = bAddExternalAssets diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypePythonBridge.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeSettings.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeSettings.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h similarity index 100% rename from openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/OpenPypeStyle.h rename to openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainer.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainer.cpp deleted file mode 100644 index 61e563f729..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainer.cpp +++ /dev/null @@ -1,115 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#include "AssetContainer.h" -#include "AssetRegistry/AssetRegistryModule.h" -#include "Misc/PackageName.h" -#include "Engine.h" -#include "Containers/UnrealString.h" - -UAssetContainer::UAssetContainer(const FObjectInitializer& ObjectInitializer) -: UAssetUserData(ObjectInitializer) -{ - FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); - FString path = UAssetContainer::GetPathName(); - UE_LOG(LogTemp, Warning, TEXT("UAssetContainer %s"), *path); - FARFilter Filter; - Filter.PackagePaths.Add(FName(*path)); - - AssetRegistryModule.Get().OnAssetAdded().AddUObject(this, &UAssetContainer::OnAssetAdded); - AssetRegistryModule.Get().OnAssetRemoved().AddUObject(this, &UAssetContainer::OnAssetRemoved); - AssetRegistryModule.Get().OnAssetRenamed().AddUObject(this, &UAssetContainer::OnAssetRenamed); -} - -void UAssetContainer::OnAssetAdded(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClassPath.ToString(); - UE_LOG(LogTemp, Log, TEXT("asset name %s"), *assetFName); - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - assets.Add(assetPath); - assetsData.Add(AssetData); - UE_LOG(LogTemp, Log, TEXT("%s: asset added to %s"), *selfFullPath, *selfDir); - } - } -} - -void UAssetContainer::OnAssetRemoved(const FAssetData& AssetData) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClassPath.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - - // take interest only in paths starting with path of current container - FString path = UAssetContainer::GetPathName(); - FString lpp = FPackageName::GetLongPackagePath(*path); - - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - // UE_LOG(LogTemp, Warning, TEXT("%s: asset removed"), *lpp); - assets.Remove(assetPath); - assetsData.Remove(AssetData); - } - } -} - -void UAssetContainer::OnAssetRenamed(const FAssetData& AssetData, const FString& str) -{ - TArray split; - - // get directory of current container - FString selfFullPath = UAssetContainer::GetPathName(); - FString selfDir = FPackageName::GetLongPackagePath(*selfFullPath); - - // get asset path and class - FString assetPath = AssetData.GetFullName(); - FString assetFName = AssetData.AssetClassPath.ToString(); - - // split path - assetPath.ParseIntoArray(split, TEXT(" "), true); - - FString assetDir = FPackageName::GetLongPackagePath(*split[1]); - if (assetDir.StartsWith(*selfDir)) - { - // exclude self - if (assetFName != "AssetContainer") - { - - assets.Remove(str); - assets.Add(assetPath); - assetsData.Remove(AssetData); - // UE_LOG(LogTemp, Warning, TEXT("%s: asset renamed %s"), *lpp, *str); - } - } -} - diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainerFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainerFactory.cpp deleted file mode 100644 index b943150bdd..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Private/AssetContainerFactory.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "AssetContainerFactory.h" -#include "AssetContainer.h" - -UAssetContainerFactory::UAssetContainerFactory(const FObjectInitializer& ObjectInitializer) - : UFactory(ObjectInitializer) -{ - SupportedClass = UAssetContainer::StaticClass(); - bCreateNew = false; - bEditorImport = true; -} - -UObject* UAssetContainerFactory::FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) -{ - UAssetContainer* AssetContainer = NewObject(InParent, Class, Name, Flags); - return AssetContainer; -} - -bool UAssetContainerFactory::ShouldShowInNewMenu() const { - return false; -} diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainer.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainer.h deleted file mode 100644 index 2c06e59d6f..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainer.h +++ /dev/null @@ -1,39 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "UObject/NoExportTypes.h" -#include "Engine/AssetUserData.h" -#include "AssetRegistry/AssetData.h" -#include "AssetContainer.generated.h" - -/** - * - */ -UCLASS(Blueprintable) -class OPENPYPE_API UAssetContainer : public UAssetUserData -{ - GENERATED_BODY() - -public: - - UAssetContainer(const FObjectInitializer& ObjectInitalizer); - // ~UAssetContainer(); - - UPROPERTY(EditAnywhere, BlueprintReadOnly) - TArray assets; - - // There seems to be no reflection option to expose array of FAssetData - /* - UPROPERTY(Transient, BlueprintReadOnly, Category = "Python", meta=(DisplayName="Assets Data")) - TArray assetsData; - */ -private: - TArray assetsData; - void OnAssetAdded(const FAssetData& AssetData); - void OnAssetRemoved(const FAssetData& AssetData); - void OnAssetRenamed(const FAssetData& AssetData, const FString& str); -}; - - diff --git a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainerFactory.h b/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainerFactory.h deleted file mode 100644 index 331ce6bb50..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/Source/OpenPype/Public/AssetContainerFactory.h +++ /dev/null @@ -1,21 +0,0 @@ -// Fill out your copyright notice in the Description page of Project Settings. - -#pragma once - -#include "CoreMinimal.h" -#include "Factories/Factory.h" -#include "AssetContainerFactory.generated.h" - -/** - * - */ -UCLASS() -class OPENPYPE_API UAssetContainerFactory : public UFactory -{ - GENERATED_BODY() - -public: - UAssetContainerFactory(const FObjectInitializer& ObjectInitializer); - virtual UObject* FactoryCreateNew(UClass* Class, UObject* InParent, FName Name, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override; - virtual bool ShouldShowInNewMenu() const override; -}; \ No newline at end of file diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 095f5e414b..5bde65edb6 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -4,6 +4,10 @@ import os import platform import json + +from typing import List + +import openpype from distutils import dir_util import subprocess import re @@ -73,7 +77,7 @@ def get_engine_versions(env=None): return OrderedDict() -def get_editor_executable_path(engine_path: Path, engine_version: str) -> Path: +def get_editor_exe_path(engine_path: Path, engine_version: str) -> Path: """Get UE Editor executable path.""" ue_path = engine_path / "Engine/Binaries" if platform.system().lower() == "windows": @@ -214,77 +218,58 @@ def create_unreal_project(project_name: str, # created in different UE4 version. When user convert such project # to his UE4 version, Engine ID is replaced in uproject file. If some # other user tries to open it, it will present him with similar error. - ue_modules = Path() - if platform.system().lower() == "windows": - ue_modules_path = engine_path / "Engine/Binaries/Win64" - if ue_version.split(".")[0] == "4": - ue_modules_path /= "UE4Editor.modules" - elif ue_version.split(".")[0] == "5": - ue_modules_path /= "UnrealEditor.modules" - ue_modules = Path(ue_modules_path) - if platform.system().lower() == "linux": - ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries", - "Linux", "UE4Editor.modules")) + # engine_path should be the location of UE_X.X folder - if platform.system().lower() == "darwin": - ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries", - "Mac", "UE4Editor.modules")) + ue_editor_exe_path: Path = get_editor_exe_path(engine_path, ue_version) + cmdlet_project_path = get_path_to_cmdlet_project(ue_version) - if ue_modules.exists(): - print("--- Loading Engine ID from modules file ...") - with open(ue_modules, "r") as mp: - loaded_modules = json.load(mp) + project_file = pr_dir / f"{project_name}.uproject" - if loaded_modules.get("BuildId"): - ue_id = "{" + loaded_modules.get("BuildId") + "}" - - plugins_path = None - if os.path.isdir(env.get("OPENPYPE_UNREAL_PLUGIN", "")): - # copy plugin to correct path under project - plugins_path = pr_dir / "Plugins" - openpype_plugin_path = plugins_path / "OpenPype" - if not openpype_plugin_path.is_dir(): - openpype_plugin_path.mkdir(parents=True, exist_ok=True) - dir_util._path_created = {} - dir_util.copy_tree(os.environ.get("OPENPYPE_UNREAL_PLUGIN"), - openpype_plugin_path.as_posix()) - - if not (openpype_plugin_path / "Binaries").is_dir() \ - or not (openpype_plugin_path / "Intermediate").is_dir(): - dev_mode = True - - # data for project file - data = { - "FileVersion": 3, - "EngineAssociation": ue_id, - "Category": "", - "Description": "", - "Plugins": [ - {"Name": "PythonScriptPlugin", "Enabled": True}, - {"Name": "EditorScriptingUtilities", "Enabled": True}, - {"Name": "SequencerScripting", "Enabled": True}, - {"Name": "MovieRenderPipeline", "Enabled": True}, - {"Name": "OpenPype", "Enabled": True} - ] - } + print("--- Generating a new project ...") + commandlet_cmd = [f'{ue_editor_exe_path.as_posix()}', + f'{cmdlet_project_path.as_posix()}', + f'-run=OPGenerateProject', + f'{project_file.resolve().as_posix()}'] if dev_mode or preset["dev_mode"]: - # this will add the project module and necessary source file to - # make it a C++ project and to (hopefully) make Unreal Editor to - # compile all # sources at start + commandlet_cmd.append('-GenerateCode') - data["Modules"] = [{ - "Name": project_name, - "Type": "Runtime", - "LoadingPhase": "Default", - "AdditionalDependencies": ["Engine"], - }] + subprocess.run(commandlet_cmd) - # write project file - project_file = pr_dir / f"{project_name}.uproject" - with open(project_file, mode="w") as pf: - json.dump(data, pf, indent=4) + with open(project_file, mode="r+") as pf: + pf_json = json.load(pf) + pf_json["EngineAssociation"] = _get_build_id(engine_path, ue_version) + pf.seek(0) + json.dump(pf_json, pf, indent=4) + pf.truncate() + print(f'--- Engine ID has been writen into the project file') + + if dev_mode or preset["dev_mode"]: + u_build_tool = get_path_to_ubt(engine_path, ue_version) + + arch = "Win64" + if platform.system().lower() == "windows": + arch = "Win64" + elif platform.system().lower() == "linux": + arch = "Linux" + elif platform.system().lower() == "darwin": + # we need to test this out + arch = "Mac" + + command1 = [u_build_tool.as_posix(), "-projectfiles", + f"-project={project_file}", "-progress"] + + subprocess.run(command1) + + command2 = [u_build_tool.as_posix(), + f"-ModuleWithSuffix={project_name},3555", arch, + "Development", "-TargetType=Editor", + f'-Project={project_file}', + f'{project_file}', + "-IgnoreJunk"] + + subprocess.run(command2) # ensure we have PySide2 installed in engine python_path = None @@ -307,8 +292,121 @@ def create_unreal_project(project_name: str, subprocess.check_call( [python_path.as_posix(), "-m", "pip", "install", "pyside2"]) - if dev_mode or preset["dev_mode"]: - _prepare_cpp_project(project_file, engine_path, ue_version) + +def get_path_to_uat(engine_path: Path) -> Path: + if platform.system().lower() == "windows": + return engine_path / "Engine/Build/BatchFiles/RunUAT.bat" + + if platform.system().lower() == "linux" or platform.system().lower() == "darwin": + return engine_path / "Engine/Build/BatchFiles/RunUAT.sh" + + +def get_path_to_cmdlet_project(ue_version: str) -> Path: + commandlet_project_path: Path = Path(os.path.dirname(os.path.abspath(openpype.__file__))) + + # For now, only tested on Windows (For Linux and Mac it has to be implemented) + if ue_version.split(".")[0] == "4": + return commandlet_project_path / "hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject" + elif ue_version.split(".")[0] == "5": + return commandlet_project_path / "hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject" + + +def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path: + u_build_tool_path = engine_path / "Engine/Binaries/DotNET" + + if ue_version.split(".")[0] == "4": + u_build_tool_path /= "UnrealBuildTool.exe" + elif ue_version.split(".")[0] == "5": + u_build_tool_path /= "UnrealBuildTool/UnrealBuildTool.exe" + + return Path(u_build_tool_path) + + +def _get_build_id(engine_path: Path, ue_version: str) -> str: + ue_modules = Path() + if platform.system().lower() == "windows": + ue_modules_path = engine_path / "Engine/Binaries/Win64" + if ue_version.split(".")[0] == "4": + ue_modules_path /= "UE4Editor.modules" + elif ue_version.split(".")[0] == "5": + ue_modules_path /= "UnrealEditor.modules" + ue_modules = Path(ue_modules_path) + + if platform.system().lower() == "linux": + ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries", + "Linux", "UE4Editor.modules")) + + if platform.system().lower() == "darwin": + ue_modules = Path(os.path.join(engine_path, "Engine", "Binaries", + "Mac", "UE4Editor.modules")) + + if ue_modules.exists(): + print("--- Loading Engine ID from modules file ...") + with open(ue_modules, "r") as mp: + loaded_modules = json.load(mp) + + if loaded_modules.get("BuildId"): + return "{" + loaded_modules.get("BuildId") + "}" + + +def try_installing_plugin(engine_path: Path, + ue_version: str, + env: dict = None) -> None: + env = env or os.environ + + integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) + + if not os.path.isdir(integration_plugin_path): + raise RuntimeError("Path to the integration plugin is null!") + + # Create a path to the plugin in the engine + openpype_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" + + if not openpype_plugin_path.is_dir(): + print("--- OpenPype Plugin is not present. Creating a new plugin directory ...") + openpype_plugin_path.mkdir(parents=True, exist_ok=True) + + engine_plugin_config_path: Path = openpype_plugin_path / "Config" + engine_plugin_config_path.mkdir(exist_ok=True) + + dir_util._path_created = {} + + if not (openpype_plugin_path / "Binaries").is_dir() \ + or not (openpype_plugin_path / "Intermediate").is_dir(): + print("--- Binaries are not present. Building the plugin ...") + _build_and_move_integration_plugin(engine_path, openpype_plugin_path, env) + + +def _build_and_move_integration_plugin(engine_path: Path, + plugin_build_path: Path, + env: dict = None) -> None: + uat_path: Path = get_path_to_uat(engine_path) + + env = env or os.environ + integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) + + if uat_path.is_file(): + temp_dir: Path = integration_plugin_path.parent / "Temp" + temp_dir.mkdir(exist_ok=True) + uplugin_path: Path = integration_plugin_path / "OpenPype.uplugin" + + # in order to successfully build the plugin, It must be built outside the Engine directory and then moved + build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}', + 'BuildPlugin', + f'-Plugin={uplugin_path.as_posix()}', + f'-Package={temp_dir.as_posix()}'] + subprocess.run(build_plugin_cmd) + + # Copy the contents of the 'Temp' dir into the 'OpenPype' directory in the engine + dir_util.copy_tree(temp_dir.as_posix(), plugin_build_path.as_posix()) + + # We need to also copy the config folder. The UAT doesn't include the Config folder in the build + plugin_install_config_path: Path = plugin_build_path / "Config" + integration_plugin_config_path = integration_plugin_path / "Config" + + dir_util.copy_tree(integration_plugin_config_path.as_posix(), plugin_install_config_path.as_posix()) + + dir_util.remove_tree(temp_dir.as_posix()) def _prepare_cpp_project( From 222a2a0631293f5c9640af10c99f15fc85372ada Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 25 Jan 2023 13:25:57 +0100 Subject: [PATCH 468/483] Cleaning up and refactoring the code. --- openpype/hosts/unreal/lib.py | 210 ++++------------------------------- 1 file changed, 23 insertions(+), 187 deletions(-) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 5bde65edb6..8c4299be53 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -221,14 +221,14 @@ def create_unreal_project(project_name: str, # engine_path should be the location of UE_X.X folder - ue_editor_exe_path: Path = get_editor_exe_path(engine_path, ue_version) - cmdlet_project_path = get_path_to_cmdlet_project(ue_version) + ue_editor_exe: Path = get_editor_exe_path(engine_path, ue_version) + cmdlet_project: Path = get_path_to_cmdlet_project(ue_version) project_file = pr_dir / f"{project_name}.uproject" print("--- Generating a new project ...") - commandlet_cmd = [f'{ue_editor_exe_path.as_posix()}', - f'{cmdlet_project_path.as_posix()}', + commandlet_cmd = [f'{ue_editor_exe.as_posix()}', + f'{cmdlet_project.as_posix()}', f'-run=OPGenerateProject', f'{project_file.resolve().as_posix()}'] @@ -297,18 +297,21 @@ def get_path_to_uat(engine_path: Path) -> Path: if platform.system().lower() == "windows": return engine_path / "Engine/Build/BatchFiles/RunUAT.bat" - if platform.system().lower() == "linux" or platform.system().lower() == "darwin": + if platform.system().lower() == "linux" \ + or platform.system().lower() == "darwin": return engine_path / "Engine/Build/BatchFiles/RunUAT.sh" def get_path_to_cmdlet_project(ue_version: str) -> Path: - commandlet_project_path: Path = Path(os.path.dirname(os.path.abspath(openpype.__file__))) + cmdlet_project: Path = Path(os.path.dirname(os.path.abspath(openpype.__file__))) # For now, only tested on Windows (For Linux and Mac it has to be implemented) if ue_version.split(".")[0] == "4": - return commandlet_project_path / "hosts/unreal/integration/UE_4.7/CommandletProject/CommandletProject.uproject" + cmdlet_project /= "hosts/unreal/integration/UE_4.7" elif ue_version.split(".")[0] == "5": - return commandlet_project_path / "hosts/unreal/integration/UE_5.0/CommandletProject/CommandletProject.uproject" + cmdlet_project /= "hosts/unreal/integration/UE_5.0" + + return cmdlet_project / "CommandletProject/CommandletProject.uproject" def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path: @@ -374,12 +377,12 @@ def try_installing_plugin(engine_path: Path, if not (openpype_plugin_path / "Binaries").is_dir() \ or not (openpype_plugin_path / "Intermediate").is_dir(): print("--- Binaries are not present. Building the plugin ...") - _build_and_move_integration_plugin(engine_path, openpype_plugin_path, env) + _build_and_move_plugin(engine_path, openpype_plugin_path, env) -def _build_and_move_integration_plugin(engine_path: Path, - plugin_build_path: Path, - env: dict = None) -> None: +def _build_and_move_plugin(engine_path: Path, + plugin_build_path: Path, + env: dict = None) -> None: uat_path: Path = get_path_to_uat(engine_path) env = env or os.environ @@ -390,191 +393,24 @@ def _build_and_move_integration_plugin(engine_path: Path, temp_dir.mkdir(exist_ok=True) uplugin_path: Path = integration_plugin_path / "OpenPype.uplugin" - # in order to successfully build the plugin, It must be built outside the Engine directory and then moved + # in order to successfully build the plugin, + # It must be built outside the Engine directory and then moved build_plugin_cmd: List[str] = [f'{uat_path.as_posix()}', 'BuildPlugin', f'-Plugin={uplugin_path.as_posix()}', f'-Package={temp_dir.as_posix()}'] subprocess.run(build_plugin_cmd) - # Copy the contents of the 'Temp' dir into the 'OpenPype' directory in the engine + # Copy the contents of the 'Temp' dir into the + # 'OpenPype' directory in the engine dir_util.copy_tree(temp_dir.as_posix(), plugin_build_path.as_posix()) - # We need to also copy the config folder. The UAT doesn't include the Config folder in the build + # We need to also copy the config folder. + # The UAT doesn't include the Config folder in the build plugin_install_config_path: Path = plugin_build_path / "Config" integration_plugin_config_path = integration_plugin_path / "Config" - dir_util.copy_tree(integration_plugin_config_path.as_posix(), plugin_install_config_path.as_posix()) + dir_util.copy_tree(integration_plugin_config_path.as_posix(), + plugin_install_config_path.as_posix()) dir_util.remove_tree(temp_dir.as_posix()) - - -def _prepare_cpp_project( - project_file: Path, engine_path: Path, ue_version: str) -> None: - """Prepare CPP Unreal Project. - - This function will add source files needed for project to be - rebuild along with the OpenPype integration plugin. - - There seems not to be automated way to do it from command line. - But there might be way to create at least those target and build files - by some generator. This needs more research as manually writing - those files is rather hackish. :skull_and_crossbones: - - - Args: - project_file (str): Path to .uproject file. - engine_path (str): Path to unreal engine associated with project. - - """ - project_name = project_file.stem - project_dir = project_file.parent - targets_dir = project_dir / "Source" - sources_dir = targets_dir / project_name - - sources_dir.mkdir(parents=True, exist_ok=True) - (project_dir / "Content").mkdir(parents=True, exist_ok=True) - - module_target = ''' -using UnrealBuildTool; -using System.Collections.Generic; - -public class {0}Target : TargetRules -{{ - public {0}Target( TargetInfo Target) : base(Target) - {{ - Type = TargetType.Game; - ExtraModuleNames.AddRange( new string[] {{ "{0}" }} ); - }} -}} -'''.format(project_name) - - editor_module_target = ''' -using UnrealBuildTool; -using System.Collections.Generic; - -public class {0}EditorTarget : TargetRules -{{ - public {0}EditorTarget( TargetInfo Target) : base(Target) - {{ - Type = TargetType.Editor; - - ExtraModuleNames.AddRange( new string[] {{ "{0}" }} ); - }} -}} -'''.format(project_name) - - module_build = ''' -using UnrealBuildTool; -public class {0} : ModuleRules -{{ - public {0}(ReadOnlyTargetRules Target) : base(Target) - {{ - PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; - PublicDependencyModuleNames.AddRange(new string[] {{ "Core", - "CoreUObject", "Engine", "InputCore" }}); - PrivateDependencyModuleNames.AddRange(new string[] {{ }}); - }} -}} -'''.format(project_name) - - module_cpp = ''' -#include "{0}.h" -#include "Modules/ModuleManager.h" - -IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, {0}, "{0}" ); -'''.format(project_name) - - module_header = ''' -#pragma once -#include "CoreMinimal.h" -''' - - game_mode_cpp = ''' -#include "{0}GameModeBase.h" -'''.format(project_name) - - game_mode_h = ''' -#pragma once - -#include "CoreMinimal.h" -#include "GameFramework/GameModeBase.h" -#include "{0}GameModeBase.generated.h" - -UCLASS() -class {1}_API A{0}GameModeBase : public AGameModeBase -{{ - GENERATED_BODY() -}}; -'''.format(project_name, project_name.upper()) - - with open(targets_dir / f"{project_name}.Target.cs", mode="w") as f: - f.write(module_target) - - with open(targets_dir / f"{project_name}Editor.Target.cs", mode="w") as f: - f.write(editor_module_target) - - with open(sources_dir / f"{project_name}.Build.cs", mode="w") as f: - f.write(module_build) - - with open(sources_dir / f"{project_name}.cpp", mode="w") as f: - f.write(module_cpp) - - with open(sources_dir / f"{project_name}.h", mode="w") as f: - f.write(module_header) - - with open(sources_dir / f"{project_name}GameModeBase.cpp", mode="w") as f: - f.write(game_mode_cpp) - - with open(sources_dir / f"{project_name}GameModeBase.h", mode="w") as f: - f.write(game_mode_h) - - u_build_tool_path = engine_path / "Engine/Binaries/DotNET" - if ue_version.split(".")[0] == "4": - u_build_tool_path /= "UnrealBuildTool.exe" - elif ue_version.split(".")[0] == "5": - u_build_tool_path /= "UnrealBuildTool/UnrealBuildTool.exe" - u_build_tool = Path(u_build_tool_path) - u_header_tool = None - - arch = "Win64" - if platform.system().lower() == "windows": - arch = "Win64" - u_header_tool = Path( - engine_path / "Engine/Binaries/Win64/UnrealHeaderTool.exe") - elif platform.system().lower() == "linux": - arch = "Linux" - u_header_tool = Path( - engine_path / "Engine/Binaries/Linux/UnrealHeaderTool") - elif platform.system().lower() == "darwin": - # we need to test this out - arch = "Mac" - u_header_tool = Path( - engine_path / "Engine/Binaries/Mac/UnrealHeaderTool") - - if not u_header_tool: - raise NotImplementedError("Unsupported platform") - - command1 = [u_build_tool.as_posix(), "-projectfiles", - f"-project={project_file}", "-progress"] - - subprocess.run(command1) - - command2 = [u_build_tool.as_posix(), - f"-ModuleWithSuffix={project_name},3555", arch, - "Development", "-TargetType=Editor", - f'-Project={project_file}', - f'{project_file}', - "-IgnoreJunk"] - - subprocess.run(command2) - - """ - uhtmanifest = os.path.join(os.path.dirname(project_file), - f"{project_name}.uhtmanifest") - - command3 = [u_header_tool, f'"{project_file}"', f'"{uhtmanifest}"', - "-Unattended", "-WarningsAsErrors", "-installed"] - - subprocess.run(command3) - """ From 87d8c912cab99ac4fc9cae7a354de4c24aa5a456 Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 25 Jan 2023 13:34:48 +0100 Subject: [PATCH 469/483] Shortening the lines of code --- openpype/hosts/unreal/lib.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 8c4299be53..3b842a112e 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -303,15 +303,15 @@ def get_path_to_uat(engine_path: Path) -> Path: def get_path_to_cmdlet_project(ue_version: str) -> Path: - cmdlet_project: Path = Path(os.path.dirname(os.path.abspath(openpype.__file__))) + cmd_project: Path = Path(os.path.dirname(os.path.abspath(openpype.__file__))) # For now, only tested on Windows (For Linux and Mac it has to be implemented) if ue_version.split(".")[0] == "4": - cmdlet_project /= "hosts/unreal/integration/UE_4.7" + cmd_project /= "hosts/unreal/integration/UE_4.7" elif ue_version.split(".")[0] == "5": - cmdlet_project /= "hosts/unreal/integration/UE_5.0" + cmd_project /= "hosts/unreal/integration/UE_5.0" - return cmdlet_project / "CommandletProject/CommandletProject.uproject" + return cmd_project / "CommandletProject/CommandletProject.uproject" def get_path_to_ubt(engine_path: Path, ue_version: str) -> Path: @@ -363,21 +363,21 @@ def try_installing_plugin(engine_path: Path, raise RuntimeError("Path to the integration plugin is null!") # Create a path to the plugin in the engine - openpype_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" + op_plugin_path: Path = engine_path / "Engine/Plugins/Marketplace/OpenPype" - if not openpype_plugin_path.is_dir(): - print("--- OpenPype Plugin is not present. Creating a new plugin directory ...") - openpype_plugin_path.mkdir(parents=True, exist_ok=True) + if not op_plugin_path.is_dir(): + print("--- OpenPype Plugin is not present. Installing ...") + op_plugin_path.mkdir(parents=True, exist_ok=True) - engine_plugin_config_path: Path = openpype_plugin_path / "Config" + engine_plugin_config_path: Path = op_plugin_path / "Config" engine_plugin_config_path.mkdir(exist_ok=True) dir_util._path_created = {} - if not (openpype_plugin_path / "Binaries").is_dir() \ - or not (openpype_plugin_path / "Intermediate").is_dir(): + if not (op_plugin_path / "Binaries").is_dir() \ + or not (op_plugin_path / "Intermediate").is_dir(): print("--- Binaries are not present. Building the plugin ...") - _build_and_move_plugin(engine_path, openpype_plugin_path, env) + _build_and_move_plugin(engine_path, op_plugin_path, env) def _build_and_move_plugin(engine_path: Path, From ae3248b6d9ad95c9c816ba832545bb87c4f04809 Mon Sep 17 00:00:00 2001 From: Joseff Date: Wed, 25 Jan 2023 11:31:27 +0100 Subject: [PATCH 470/483] Refactored the generation of UE projects, plugin is now being installed in the engine. --- openpype/hosts/unreal/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 3b842a112e..b502737771 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -303,7 +303,7 @@ def get_path_to_uat(engine_path: Path) -> Path: def get_path_to_cmdlet_project(ue_version: str) -> Path: - cmd_project: Path = Path(os.path.dirname(os.path.abspath(openpype.__file__))) + cmd_project = Path(os.path.dirname(os.path.abspath(openpype.__file__))) # For now, only tested on Windows (For Linux and Mac it has to be implemented) if ue_version.split(".")[0] == "4": From 141214d86c8e1055210e517fd7cd467d2dd93496 Mon Sep 17 00:00:00 2001 From: Joseff Date: Fri, 3 Feb 2023 11:03:10 +0100 Subject: [PATCH 471/483] Added a stdout printing for the project generation command --- openpype/hosts/unreal/lib.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index b502737771..04171f3ac0 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -235,15 +235,27 @@ def create_unreal_project(project_name: str, if dev_mode or preset["dev_mode"]: commandlet_cmd.append('-GenerateCode') - subprocess.run(commandlet_cmd) + gen_process = subprocess.Popen(commandlet_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) - with open(project_file, mode="r+") as pf: + for line in gen_process.stdout: + print(line.decode(), end='') + gen_process.stdout.close() + return_code = gen_process.wait() + + if return_code and return_code != 0: + raise RuntimeError(f'Failed to generate \'{project_name}\' project! Exited with return code {return_code}') + + print("--- Project has been generated successfully.") + + with open(project_file.as_posix(), mode="r+") as pf: pf_json = json.load(pf) pf_json["EngineAssociation"] = _get_build_id(engine_path, ue_version) pf.seek(0) json.dump(pf_json, pf, indent=4) pf.truncate() - print(f'--- Engine ID has been writen into the project file') + print(f'--- Engine ID has been written into the project file') if dev_mode or preset["dev_mode"]: u_build_tool = get_path_to_ubt(engine_path, ue_version) From 283a5fb8e4cafc33eb6ad4bd36a82a02e5893222 Mon Sep 17 00:00:00 2001 From: Joseff Date: Fri, 3 Feb 2023 11:07:47 +0100 Subject: [PATCH 472/483] Shortened lines for the error message --- openpype/hosts/unreal/lib.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 04171f3ac0..2e1f59d439 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -229,7 +229,7 @@ def create_unreal_project(project_name: str, print("--- Generating a new project ...") commandlet_cmd = [f'{ue_editor_exe.as_posix()}', f'{cmdlet_project.as_posix()}', - f'-run=OPGenerateProject', + f'-run=OPGenerateProjec', f'{project_file.resolve().as_posix()}'] if dev_mode or preset["dev_mode"]: @@ -245,7 +245,8 @@ def create_unreal_project(project_name: str, return_code = gen_process.wait() if return_code and return_code != 0: - raise RuntimeError(f'Failed to generate \'{project_name}\' project! Exited with return code {return_code}') + raise RuntimeError(f'Failed to generate \'{project_name}\' project! ' + f'Exited with return code {return_code}') print("--- Project has been generated successfully.") From 5ad3bfbaf2cd732a08fc0f9e00600017b4ef0e95 Mon Sep 17 00:00:00 2001 From: Joseff Date: Mon, 20 Feb 2023 15:57:45 +0100 Subject: [PATCH 473/483] Fixed the generation of the project, added copyrights notices, removed .ini files. --- openpype/hosts/unreal/addon.py | 3 +- .../unreal/hooks/pre_workfile_preparation.py | 3 +- .../UE_4.7/CommandletProject/.gitignore | 2 + .../Config/DefaultEditor.ini | 0 .../Config/DefaultEngine.ini | 16 ------- .../CommandletProject/Config/DefaultGame.ini | 4 -- .../integration/UE_4.7/OpenPype/.gitignore | 6 +++ .../UE_4.7/OpenPype/OpenPype.uplugin | 1 - .../Source/OpenPype/OpenPype.Build.cs | 2 +- .../OPGenerateProjectCommandlet.cpp | 1 + .../Private/Commandlets/OPActionResult.cpp | 2 +- .../Source/OpenPype/Private/OpenPype.cpp | 1 + .../Source/OpenPype/Private/OpenPypeLib.cpp | 1 + .../Private/OpenPypePublishInstance.cpp | 1 + .../OpenPypePublishInstanceFactory.cpp | 1 + .../OpenPype/Private/OpenPypePythonBridge.cpp | 1 + .../OpenPype/Private/OpenPypeSettings.cpp | 2 +- .../Source/OpenPype/Private/OpenPypeStyle.cpp | 1 + .../OPGenerateProjectCommandlet.h | 2 +- .../Public/Commandlets/OPActionResult.h | 2 +- .../Source/OpenPype/Public/Logging/OP_Log.h | 1 + .../Source/OpenPype/Public/OPConstants.h | 1 + .../Source/OpenPype/Public/OpenPype.h | 2 +- .../Source/OpenPype/Public/OpenPypeLib.h | 1 + .../OpenPype/Public/OpenPypePublishInstance.h | 1 + .../Public/OpenPypePublishInstanceFactory.h | 1 + .../OpenPype/Public/OpenPypePythonBridge.h | 1 + .../Source/OpenPype/Public/OpenPypeSettings.h | 2 +- .../Source/OpenPype/Public/OpenPypeStyle.h | 1 + .../UE_5.0/CommandletProject/.gitignore | 35 ++++++++++++++++ .../Config/DefaultEditor.ini | 0 .../Config/DefaultEngine.ini | 42 ------------------- .../CommandletProject/Config/DefaultGame.ini | 4 -- .../UE_5.0/OpenPype/OpenPype.uplugin | 3 +- .../Source/OpenPype/OpenPype.Build.cs | 2 +- .../OPGenerateProjectCommandlet.cpp | 1 + .../Private/Commandlets/OPActionResult.cpp | 3 +- .../OpenPype/Private/Logging/OP_Log.cpp | 2 + .../Source/OpenPype/Private/OpenPype.cpp | 1 + .../OpenPype/Private/OpenPypeCommands.cpp | 2 +- .../Source/OpenPype/Private/OpenPypeLib.cpp | 1 + .../Private/OpenPypePublishInstance.cpp | 1 + .../OpenPypePublishInstanceFactory.cpp | 1 + .../OpenPype/Private/OpenPypePythonBridge.cpp | 1 + .../OpenPype/Private/OpenPypeSettings.cpp | 2 +- .../Source/OpenPype/Private/OpenPypeStyle.cpp | 2 + .../OPGenerateProjectCommandlet.h | 1 + .../Public/Commandlets/OPActionResult.h | 2 +- .../Source/OpenPype/Public/Logging/OP_Log.h | 1 + .../Source/OpenPype/Public/OPConstants.h | 1 + .../Source/OpenPype/Public/OpenPype.h | 2 +- .../Source/OpenPype/Public/OpenPypeCommands.h | 2 +- .../Source/OpenPype/Public/OpenPypeLib.h | 1 + .../OpenPype/Public/OpenPypePublishInstance.h | 1 + .../Public/OpenPypePublishInstanceFactory.h | 1 + .../OpenPype/Public/OpenPypePythonBridge.h | 1 + .../Source/OpenPype/Public/OpenPypeSettings.h | 2 +- .../Source/OpenPype/Public/OpenPypeStyle.h | 1 + openpype/hosts/unreal/lib.py | 6 +-- 59 files changed, 97 insertions(+), 91 deletions(-) delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEditor.ini delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEngine.ini delete mode 100644 openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultGame.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEditor.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEngine.ini delete mode 100644 openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultGame.ini diff --git a/openpype/hosts/unreal/addon.py b/openpype/hosts/unreal/addon.py index c92a44870f..24e2db975d 100644 --- a/openpype/hosts/unreal/addon.py +++ b/openpype/hosts/unreal/addon.py @@ -19,7 +19,8 @@ class UnrealAddon(OpenPypeModule, IHostAddon): unreal_plugin_path = os.path.join( UNREAL_ROOT_DIR, "integration", ue_plugin, "OpenPype" ) - if not env.get("OPENPYPE_UNREAL_PLUGIN"): + if not env.get("OPENPYPE_UNREAL_PLUGIN") or \ + env.get("OPENPYPE_UNREAL_PLUGIN") != unreal_plugin_path: env["OPENPYPE_UNREAL_PLUGIN"] = unreal_plugin_path # Set default environments if are not set via settings diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 821018ba9d..14285cb78c 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -127,6 +127,7 @@ class UnrealPrelaunchHook(PreLaunchHook): # Set "OPENPYPE_UNREAL_PLUGIN" to current process environment for # execution of `create_unreal_project` + if self.launch_context.env.get("OPENPYPE_UNREAL_PLUGIN"): self.log.info(( f"{self.signature} using OpenPype plugin from " @@ -138,7 +139,7 @@ class UnrealPrelaunchHook(PreLaunchHook): engine_path = detected[engine_version] - unreal_lib.try_installing_plugin(Path(engine_path), engine_version) + unreal_lib.try_installing_plugin(Path(engine_path), os.environ) project_file = project_path / unreal_project_filename if not project_file.is_file(): diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore index 1004610e4f..e74e6886b7 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore +++ b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/.gitignore @@ -1,6 +1,8 @@ /Saved /DerivedDataCache /Intermediate +/Content +/Config /Binaries /.idea /.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEditor.ini b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEditor.ini deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEngine.ini b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEngine.ini deleted file mode 100644 index 2845baccca..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultEngine.ini +++ /dev/null @@ -1,16 +0,0 @@ - - -[/Script/EngineSettings.GameMapsSettings] -GameDefaultMap=/Engine/Maps/Templates/Template_Default.Template_Default - - -[/Script/HardwareTargeting.HardwareTargetingSettings] -TargetedHardwareClass=Desktop -AppliedTargetedHardwareClass=Desktop -DefaultGraphicsPerformance=Maximum -AppliedDefaultGraphicsPerformance=Maximum - -[/Script/Engine.Engine] -+ActiveGameNameRedirects=(OldGameName="TP_BlankBP",NewGameName="/Script/CommandletProject") -+ActiveGameNameRedirects=(OldGameName="/Script/TP_BlankBP",NewGameName="/Script/CommandletProject") - diff --git a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultGame.ini b/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultGame.ini deleted file mode 100644 index 40956de961..0000000000 --- a/openpype/hosts/unreal/integration/UE_4.7/CommandletProject/Config/DefaultGame.ini +++ /dev/null @@ -1,4 +0,0 @@ - - -[/Script/EngineSettings.GeneralProjectSettings] -ProjectID=95AED0BF45A918DF73ABB3BB27D25356 diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore index b32a6f55e5..5add07aef8 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore @@ -33,3 +33,9 @@ /Binaries /Intermediate +/Saved +/DerivedDataCache +/Content +/Config +/.idea +/.vs diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin index 23155cb74d..b2cbe3cff3 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/OpenPype.uplugin @@ -12,7 +12,6 @@ "SupportURL": "https://pype.club/", "EngineVersion": "4.27", "CanContainContent": true, - "IsBetaVersion": true, "Installed": true, "Modules": [ { diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs index 13afb11003..f77c1383eb 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/OpenPype.Build.cs @@ -1,4 +1,4 @@ -// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. +// Copyright 2023, Ayon, All rights reserved. using UnrealBuildTool; diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp index 024a6097b3..abb1975027 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "Commandlets/Implementations/OPGenerateProjectCommandlet.h" #include "Editor.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp index 9236fbb057..6e50ef2221 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright 2023, Ayon, All rights reserved. #include "Commandlets/OPActionResult.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp index a510a5e3bf..9bf7b341c5 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPype.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "OpenPype.h" #include "ISettingsContainer.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp index a58e921288..008025e816 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "OpenPypeLib.h" #include "AssetViewUtils.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp index 424c4ed491..05638fbd0b 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp index 9b26da7fa4..a32ebe32cb 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "OpenPypePublishInstanceFactory.h" #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp index 8113231503..6ebfc528f0 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "OpenPypePythonBridge.h" UOpenPypePythonBridge* UOpenPypePythonBridge::Get() diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp index 951b522308..dd4228dfd0 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright 2023, Ayon, All rights reserved. #include "OpenPypeSettings.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp index b7abc38156..0cc854c5ef 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "OpenPypeStyle.h" #include "Framework/Application/SlateApplication.h" #include "Styling/SlateStyle.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h index 8738de6d4a..d1129aa070 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h @@ -1,6 +1,6 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once - #include "GameProjectUtils.h" #include "Commandlets/OPActionResult.h" #include "ProjectDescriptor.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h index f46ba9c62a..c960bbf190 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright 2023, Ayon, All rights reserved. #pragma once diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h index 4f8af3e2e6..3740c5285a 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h index 21a033e426..f4587f7a50 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OPConstants.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once namespace OPConstants diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h index 9cfa60176c..2454344128 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPype.h @@ -1,4 +1,4 @@ -// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. +// Copyright 2023, Ayon, All rights reserved. #pragma once diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h index 06425c7c7d..ef4d1027ea 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeLib.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "Engine.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h index 16b3194b96..8cfcd067c0 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "Engine.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h index 7d2c77fe6e..3fdb984411 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "CoreMinimal.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h index 692aab2e5e..827f76f56b 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "Engine.h" #include "OpenPypePythonBridge.generated.h" diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h index 9bdcfb2399..88defaa773 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright 2023, Ayon, All rights reserved. #pragma once diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h index fbc8bcdd5b..0e4af129d0 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "CoreMinimal.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore index 1004610e4f..80814ef0a6 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore +++ b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/.gitignore @@ -1,6 +1,41 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + /Saved /DerivedDataCache /Intermediate /Binaries +/Content +/Config /.idea /.vs \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEditor.ini b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEditor.ini deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEngine.ini b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEngine.ini deleted file mode 100644 index 3f5357dac4..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultEngine.ini +++ /dev/null @@ -1,42 +0,0 @@ - - -[/Script/EngineSettings.GameMapsSettings] -GameDefaultMap=/Engine/Maps/Templates/OpenWorld - - -[/Script/HardwareTargeting.HardwareTargetingSettings] -TargetedHardwareClass=Desktop -AppliedTargetedHardwareClass=Desktop -DefaultGraphicsPerformance=Maximum -AppliedDefaultGraphicsPerformance=Maximum - -[/Script/WindowsTargetPlatform.WindowsTargetSettings] -DefaultGraphicsRHI=DefaultGraphicsRHI_DX12 - -[/Script/Engine.RendererSettings] -r.GenerateMeshDistanceFields=True -r.DynamicGlobalIlluminationMethod=1 -r.ReflectionMethod=1 -r.Shadow.Virtual.Enable=1 - -[/Script/WorldPartitionEditor.WorldPartitionEditorSettings] -CommandletClass=Class'/Script/UnrealEd.WorldPartitionConvertCommandlet' - -[/Script/Engine.Engine] -+ActiveGameNameRedirects=(OldGameName="TP_BlankBP",NewGameName="/Script/CommandletProject") -+ActiveGameNameRedirects=(OldGameName="/Script/TP_BlankBP",NewGameName="/Script/CommandletProject") - -[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings] -bEnablePlugin=True -bAllowNetworkConnection=True -SecurityToken=684C16AF4BD96F1D6828A6B067693175 -bIncludeInShipping=False -bAllowExternalStartInShipping=False -bCompileAFSProject=False -bUseCompression=False -bLogFiles=False -bReportStats=False -ConnectionType=USBOnly -bUseManualIPAddress=False -ManualIPAddress= - diff --git a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultGame.ini b/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultGame.ini deleted file mode 100644 index c661b739ab..0000000000 --- a/openpype/hosts/unreal/integration/UE_5.0/CommandletProject/Config/DefaultGame.ini +++ /dev/null @@ -1,4 +0,0 @@ - - -[/Script/EngineSettings.GeneralProjectSettings] -ProjectID=D528076140C577E5807BA5BA135366BB diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin index b89eb43949..ff08edc13e 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/OpenPype.uplugin @@ -12,9 +12,8 @@ "SupportURL": "https://pype.club/", "CanContainContent": true, "EngineVersion": "5.0", - "IsBetaVersion": true, "IsExperimentalVersion": false, - "Installed": false, + "Installed": true, "Modules": [ { "Name": "OpenPype", diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs index 99c1c7b306..e1087fd720 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/OpenPype.Build.cs @@ -1,4 +1,4 @@ -// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. +// Copyright 2023, Ayon, All rights reserved. using UnrealBuildTool; diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp index 024a6097b3..abb1975027 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/Implementations/OPGenerateProjectCommandlet.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "Commandlets/Implementations/OPGenerateProjectCommandlet.h" #include "Editor.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp index 9236fbb057..23ae2dd329 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Commandlets/OPActionResult.cpp @@ -1,5 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. - +// Copyright 2023, Ayon, All rights reserved. #include "Commandlets/OPActionResult.h" #include "Logging/OP_Log.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp index 29b1068c21..198fb9df0c 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/Logging/OP_Log.cpp @@ -1 +1,3 @@ +// Copyright 2023, Ayon, All rights reserved. + #include "Logging/OP_Log.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPype.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPype.cpp index d23de61102..65da29da35 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPype.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPype.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "OpenPype.h" #include "ISettingsContainer.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp index 6187bd7c7e..881814e278 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeCommands.cpp @@ -1,4 +1,4 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright 2023, Ayon, All rights reserved. #include "OpenPypeCommands.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp index a58e921288..008025e816 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeLib.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "OpenPypeLib.h" #include "AssetViewUtils.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp index e6a85002c7..05d5c8a87d 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstance.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp index 9b26da7fa4..a32ebe32cb 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePublishInstanceFactory.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "OpenPypePublishInstanceFactory.h" #include "OpenPypePublishInstance.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp index 8113231503..6ebfc528f0 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypePythonBridge.cpp @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #include "OpenPypePythonBridge.h" UOpenPypePythonBridge* UOpenPypePythonBridge::Get() diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp index a6b9eba749..6562a81138 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeSettings.cpp @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright 2023, Ayon, All rights reserved. #include "OpenPypeSettings.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp index 49e805da4d..a4d75e048e 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Private/OpenPypeStyle.cpp @@ -1,3 +1,5 @@ +// Copyright 2023, Ayon, All rights reserved. + #include "OpenPypeStyle.h" #include "OpenPype.h" #include "Framework/Application/SlateApplication.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h index 8738de6d4a..6a6c6406e7 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/Implementations/OPGenerateProjectCommandlet.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h index f46ba9c62a..c960bbf190 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Commandlets/OPActionResult.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright 2023, Ayon, All rights reserved. #pragma once diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h index 4f8af3e2e6..3740c5285a 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/Logging/OP_Log.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once DEFINE_LOG_CATEGORY_STATIC(LogCommandletOPGenerateProject, Log, All); \ No newline at end of file diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h index 21a033e426..f4587f7a50 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OPConstants.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once namespace OPConstants diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h index 4261476da8..b89760099b 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPype.h @@ -1,4 +1,4 @@ -// Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. +// Copyright 2023, Ayon, All rights reserved. #pragma once diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h index 62ffb8de33..99b0be26f0 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeCommands.h @@ -1,4 +1,4 @@ -// Copyright Epic Games, Inc. All Rights Reserved. +// Copyright 2023, Ayon, All rights reserved. #pragma once diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h index 06425c7c7d..ef4d1027ea 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeLib.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "Engine.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h index c221f64135..bce41ef1b1 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstance.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "Engine.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h index 7d2c77fe6e..3fdb984411 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePublishInstanceFactory.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "CoreMinimal.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h index 692aab2e5e..827f76f56b 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypePythonBridge.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "Engine.h" #include "OpenPypePythonBridge.generated.h" diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h index aca80946bb..b818fe0e95 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeSettings.h @@ -1,4 +1,4 @@ -// Fill out your copyright notice in the Description page of Project Settings. +// Copyright 2023, Ayon, All rights reserved. #pragma once diff --git a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h index ae704251e1..039abe96ef 100644 --- a/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h +++ b/openpype/hosts/unreal/integration/UE_5.0/OpenPype/Source/OpenPype/Public/OpenPypeStyle.h @@ -1,3 +1,4 @@ +// Copyright 2023, Ayon, All rights reserved. #pragma once #include "CoreMinimal.h" #include "Styling/SlateStyle.h" diff --git a/openpype/hosts/unreal/lib.py b/openpype/hosts/unreal/lib.py index 2e1f59d439..28a5106042 100644 --- a/openpype/hosts/unreal/lib.py +++ b/openpype/hosts/unreal/lib.py @@ -229,7 +229,7 @@ def create_unreal_project(project_name: str, print("--- Generating a new project ...") commandlet_cmd = [f'{ue_editor_exe.as_posix()}', f'{cmdlet_project.as_posix()}', - f'-run=OPGenerateProjec', + f'-run=OPGenerateProject', f'{project_file.resolve().as_posix()}'] if dev_mode or preset["dev_mode"]: @@ -365,9 +365,7 @@ def _get_build_id(engine_path: Path, ue_version: str) -> str: return "{" + loaded_modules.get("BuildId") + "}" -def try_installing_plugin(engine_path: Path, - ue_version: str, - env: dict = None) -> None: +def try_installing_plugin(engine_path: Path, env: dict = None) -> None: env = env or os.environ integration_plugin_path: Path = Path(env.get("OPENPYPE_UNREAL_PLUGIN", "")) From f91cefa2fd1089ac564848254e16b7a0ba95ea37 Mon Sep 17 00:00:00 2001 From: Joseff Date: Mon, 20 Feb 2023 16:03:03 +0100 Subject: [PATCH 474/483] Updated .gitignore for the OpenPype plugin --- .../hosts/unreal/integration/UE_4.7/OpenPype/.gitignore | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore index 5add07aef8..b32a6f55e5 100644 --- a/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore +++ b/openpype/hosts/unreal/integration/UE_4.7/OpenPype/.gitignore @@ -33,9 +33,3 @@ /Binaries /Intermediate -/Saved -/DerivedDataCache -/Content -/Config -/.idea -/.vs From b1fa39439603a19b8884d1613f4a67ca55d33ff6 Mon Sep 17 00:00:00 2001 From: Joseff Date: Tue, 21 Feb 2023 20:23:32 +0100 Subject: [PATCH 475/483] Replaced the warning for exceeding the project name length with an exception. --- openpype/hosts/unreal/hooks/pre_workfile_preparation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py index 14285cb78c..4c9f8258f5 100644 --- a/openpype/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/openpype/hosts/unreal/hooks/pre_workfile_preparation.py @@ -79,9 +79,9 @@ class UnrealPrelaunchHook(PreLaunchHook): unreal_project_name = os.path.splitext(unreal_project_filename)[0] # Unreal is sensitive about project names longer then 20 chars if len(unreal_project_name) > 20: - self.log.warning(( - f"Project name exceed 20 characters ({unreal_project_name})!" - )) + raise ApplicationLaunchFailed( + f"Project name exceeds 20 characters ({unreal_project_name})!" + ) # Unreal doesn't accept non alphabet characters at the start # of the project name. This is because project name is then used From 9bd3ff7184c14394057e96568fff574a5e0a22cb Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 24 Feb 2023 17:51:24 +0000 Subject: [PATCH 476/483] Fix broken lib. --- openpype/hosts/maya/api/lib.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index ce80396326..4324d321dc 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -403,9 +403,9 @@ def lsattrs(attrs): """ - dep_fn = om.MFnDependencyNode() - dag_fn = om.MFnDagNode() - selection_list = om.MSelectionList() + dep_fn = OpenMaya.MFnDependencyNode() + dag_fn = OpenMaya.MFnDagNode() + selection_list = OpenMaya.MSelectionList() first_attr = next(iter(attrs)) @@ -419,7 +419,7 @@ def lsattrs(attrs): matches = set() for i in range(selection_list.length()): node = selection_list.getDependNode(i) - if node.hasFn(om.MFn.kDagNode): + if node.hasFn(OpenMaya.MFn.kDagNode): fn_node = dag_fn.setObject(node) full_path_names = [path.fullPathName() for path in fn_node.getAllPaths()] @@ -868,11 +868,11 @@ def maintained_selection_api(): Warning: This is *not* added to the undo stack. """ - original = om.MGlobal.getActiveSelectionList() + original = OpenMaya.MGlobal.getActiveSelectionList() try: yield finally: - om.MGlobal.setActiveSelectionList(original) + OpenMaya.MGlobal.setActiveSelectionList(original) @contextlib.contextmanager @@ -1282,11 +1282,11 @@ def get_id(node): if node is None: return - sel = om.MSelectionList() + sel = OpenMaya.MSelectionList() sel.add(node) api_node = sel.getDependNode(0) - fn = om.MFnDependencyNode(api_node) + fn = OpenMaya.MFnDependencyNode(api_node) if not fn.hasAttribute("cbId"): return @@ -3341,15 +3341,15 @@ def iter_visible_nodes_in_range(nodes, start, end): @memodict def get_visibility_mplug(node): """Return api 2.0 MPlug with cached memoize decorator""" - sel = om.MSelectionList() + sel = OpenMaya.MSelectionList() sel.add(node) dag = sel.getDagPath(0) - return om.MFnDagNode(dag).findPlug("visibility", True) + return OpenMaya.MFnDagNode(dag).findPlug("visibility", True) @contextlib.contextmanager def dgcontext(mtime): """MDGContext context manager""" - context = om.MDGContext(mtime) + context = OpenMaya.MDGContext(mtime) try: previous = context.makeCurrent() yield context @@ -3358,9 +3358,9 @@ def iter_visible_nodes_in_range(nodes, start, end): # We skip the first frame as we already used that frame to check for # overall visibilities. And end+1 to include the end frame. - scene_units = om.MTime.uiUnit() + scene_units = OpenMaya.MTime.uiUnit() for frame in range(start + 1, end + 1): - mtime = om.MTime(frame, unit=scene_units) + mtime = OpenMaya.MTime(frame, unit=scene_units) # Build little cache so we don't query the same MPlug's value # again if it was checked on this frame and also is a dependency From 3ae3d01a200ee0b007d38964ffa22573f441ee62 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Feb 2023 22:02:03 +0100 Subject: [PATCH 477/483] Don't use ObjectId in scene inventory view --- openpype/tools/sceneinventory/view.py | 42 ++++++++++++--------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index 3c4e03a195..d7c80df692 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -4,7 +4,6 @@ from functools import partial from qtpy import QtWidgets, QtCore import qtawesome -from bson.objectid import ObjectId from openpype.client import ( get_version_by_id, @@ -84,22 +83,20 @@ class SceneInventoryView(QtWidgets.QTreeView): if not items: return - repre_ids = [] - for item in items: - item_id = ObjectId(item["representation"]) - if item_id not in repre_ids: - repre_ids.append(item_id) + repre_ids = { + item["representation"] + for item in items + } project_name = legacy_io.active_project() repre_docs = get_representations( project_name, representation_ids=repre_ids, fields=["parent"] ) - version_ids = [] - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - if version_id not in version_ids: - version_ids.append(version_id) + version_ids = { + repre_doc["parent"] + for repre_doc in repre_docs + } loaded_versions = get_versions( project_name, version_ids=version_ids, hero=True @@ -107,18 +104,17 @@ class SceneInventoryView(QtWidgets.QTreeView): loaded_hero_versions = [] versions_by_parent_id = collections.defaultdict(list) - version_parents = [] + subset_ids = set() for version in loaded_versions: if version["type"] == "hero_version": loaded_hero_versions.append(version) else: parent_id = version["parent"] versions_by_parent_id[parent_id].append(version) - if parent_id not in version_parents: - version_parents.append(parent_id) + subset_ids.add(parent_id) all_versions = get_versions( - project_name, subset_ids=version_parents, hero=True + project_name, subset_ids=subset_ids, hero=True ) hero_versions = [] versions = [] @@ -146,11 +142,11 @@ class SceneInventoryView(QtWidgets.QTreeView): switch_to_versioned = None if has_loaded_hero_versions: def _on_switch_to_versioned(items): - repre_ids = [] + repre_ids = set() for item in items: - item_id = ObjectId(item["representation"]) + item_id = item["representation"] if item_id not in repre_ids: - repre_ids.append(item_id) + repre_ids.add(item_id) repre_docs = get_representations( project_name, @@ -158,13 +154,13 @@ class SceneInventoryView(QtWidgets.QTreeView): fields=["parent"] ) - version_ids = [] + version_ids = set() version_id_by_repre_id = {} for repre_doc in repre_docs: version_id = repre_doc["parent"] - version_id_by_repre_id[repre_doc["_id"]] = version_id - if version_id not in version_ids: - version_ids.append(version_id) + repre_id = str(repre_doc["_id"]) + version_id_by_repre_id[repre_id] = version_id + version_ids.add(version_id) hero_versions = get_hero_versions( project_name, @@ -194,7 +190,7 @@ class SceneInventoryView(QtWidgets.QTreeView): version_doc["name"] for item in items: - repre_id = ObjectId(item["representation"]) + repre_id = item["representation"] version_id = version_id_by_repre_id.get(repre_id) version_name = version_name_by_id.get(version_id) if version_name is not None: From cba1b46765ca2d03b3881b8fdb19fa73b205be30 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 23 Feb 2023 22:02:28 +0100 Subject: [PATCH 478/483] don't use ObjectId in switch dialog --- openpype/tools/sceneinventory/switch_dialog.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index 47baeaebea..e9575f254b 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -2,7 +2,6 @@ import collections import logging from qtpy import QtWidgets, QtCore import qtawesome -from bson.objectid import ObjectId from openpype.client import ( get_asset_by_name, @@ -161,7 +160,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): repre_ids = set() content_loaders = set() for item in self._items: - repre_ids.add(ObjectId(item["representation"])) + repre_ids.add(str(item["representation"])) content_loaders.add(item["loader"]) project_name = self.active_project() @@ -170,7 +169,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): representation_ids=repre_ids, archived=True )) - repres_by_id = {repre["_id"]: repre for repre in repres} + repres_by_id = {str(repre["_id"]): repre for repre in repres} # stash context values, works only for single representation if len(repres) == 1: @@ -181,18 +180,18 @@ class SwitchAssetDialog(QtWidgets.QDialog): content_repres = {} archived_repres = [] missing_repres = [] - version_ids = [] + version_ids = set() for repre_id in repre_ids: if repre_id not in repres_by_id: missing_repres.append(repre_id) elif repres_by_id[repre_id]["type"] == "archived_representation": repre = repres_by_id[repre_id] archived_repres.append(repre) - version_ids.append(repre["parent"]) + version_ids.add(repre["parent"]) else: repre = repres_by_id[repre_id] content_repres[repre_id] = repres_by_id[repre_id] - version_ids.append(repre["parent"]) + version_ids.add(repre["parent"]) versions = get_versions( project_name, @@ -1249,7 +1248,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc for container in self._items: - container_repre_id = ObjectId(container["representation"]) + container_repre_id = container["representation"] container_repre = self.content_repres[container_repre_id] container_repre_name = container_repre["name"] From 81ed872905bb1d43ef1c2340e1890e329cf31867 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 24 Feb 2023 10:31:49 +0100 Subject: [PATCH 479/483] apply suggested changes --- openpype/tools/sceneinventory/switch_dialog.py | 2 +- openpype/tools/sceneinventory/view.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/openpype/tools/sceneinventory/switch_dialog.py b/openpype/tools/sceneinventory/switch_dialog.py index e9575f254b..4aaad38bbc 100644 --- a/openpype/tools/sceneinventory/switch_dialog.py +++ b/openpype/tools/sceneinventory/switch_dialog.py @@ -195,7 +195,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): versions = get_versions( project_name, - version_ids=set(version_ids), + version_ids=version_ids, hero=True ) content_versions = {} diff --git a/openpype/tools/sceneinventory/view.py b/openpype/tools/sceneinventory/view.py index d7c80df692..a04171e429 100644 --- a/openpype/tools/sceneinventory/view.py +++ b/openpype/tools/sceneinventory/view.py @@ -142,11 +142,10 @@ class SceneInventoryView(QtWidgets.QTreeView): switch_to_versioned = None if has_loaded_hero_versions: def _on_switch_to_versioned(items): - repre_ids = set() - for item in items: - item_id = item["representation"] - if item_id not in repre_ids: - repre_ids.add(item_id) + repre_ids = { + item["representation"] + for item in items + } repre_docs = get_representations( project_name, @@ -168,10 +167,10 @@ class SceneInventoryView(QtWidgets.QTreeView): fields=["version_id"] ) - version_ids = set() + hero_src_version_ids = set() for hero_version in hero_versions: version_id = hero_version["version_id"] - version_ids.add(version_id) + hero_src_version_ids.add(version_id) hero_version_id = hero_version["_id"] for _repre_id, current_version_id in ( version_id_by_repre_id.items() @@ -181,7 +180,7 @@ class SceneInventoryView(QtWidgets.QTreeView): version_docs = get_versions( project_name, - version_ids=version_ids, + version_ids=hero_src_version_ids, fields=["name"] ) version_name_by_id = {} From eb72d10f934e816f8df966cb200e1b686d366e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Je=C5=BEek?= Date: Fri, 24 Feb 2023 21:43:21 +0100 Subject: [PATCH 480/483] global: source template fixed frame duplication (#4503) --- openpype/settings/defaults/project_anatomy/templates.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_anatomy/templates.json b/openpype/settings/defaults/project_anatomy/templates.json index 99a869963b..02c0e35377 100644 --- a/openpype/settings/defaults/project_anatomy/templates.json +++ b/openpype/settings/defaults/project_anatomy/templates.json @@ -55,7 +55,7 @@ }, "source": { "folder": "{root[work]}/{originalDirname}", - "file": "{originalBasename}<.{@frame}><_{udim}>.{ext}", + "file": "{originalBasename}.{ext}", "path": "{@folder}/{@file}" }, "__dynamic_keys_labels__": { From 0e896d981e87068659deda3f4daabcd79ca21490 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Tue, 14 Feb 2023 15:48:32 +0000 Subject: [PATCH 481/483] Simplify pointcache proxy integration --- .../plugins/publish/extract_pointcache.py | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index e551858d48..153c177043 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -1,5 +1,4 @@ import os -import copy from maya import cmds @@ -10,7 +9,6 @@ from openpype.hosts.maya.api.lib import ( maintained_selection, iter_visible_nodes_in_range ) -from openpype.lib import StringTemplate class ExtractAlembic(publish.Extractor): @@ -135,26 +133,14 @@ class ExtractAlembic(publish.Extractor): **options ) - template_data = copy.deepcopy(instance.data["anatomyData"]) - template_data.update({"ext": "abc"}) - templates = instance.context.data["anatomy"].templates["publish"] - published_filename_without_extension = StringTemplate( - templates["file"] - ).format(template_data).replace(".abc", "_proxy") - transfers = [] - destination = os.path.join( - instance.data["resourcesDir"], - filename.replace( - filename.split(".")[0], - published_filename_without_extension - ) - ) - transfers.append((path, destination)) - - for source, destination in transfers: - self.log.debug("Transfer: {} > {}".format(source, destination)) - - instance.data["transfers"] = transfers + representation = { + "name": "proxy", + "ext": "abc", + "files": path, + "stagingDir": dirname, + "outputName": "proxy" + } + instance.data["representations"].append(representation) def get_members_and_roots(self, instance): return instance[:], instance.data.get("setMembers") From 458eaa8d80889a9d2f1e812b8bd56e2aac500dad Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Thu, 16 Feb 2023 16:09:15 +0000 Subject: [PATCH 482/483] Fix pointcache proxy publishing --- openpype/hosts/maya/plugins/publish/extract_pointcache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 153c177043..892603535c 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -136,7 +136,7 @@ class ExtractAlembic(publish.Extractor): representation = { "name": "proxy", "ext": "abc", - "files": path, + "files": os.path.basename(path), "stagingDir": dirname, "outputName": "proxy" } From 2a213ec5618923336cb9abe38192c0aa5f606252 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Fri, 17 Feb 2023 15:18:01 +0000 Subject: [PATCH 483/483] Inform about skipping proxy extraction. --- openpype/hosts/maya/plugins/publish/extract_pointcache.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/extract_pointcache.py b/openpype/hosts/maya/plugins/publish/extract_pointcache.py index 892603535c..a3b0560099 100644 --- a/openpype/hosts/maya/plugins/publish/extract_pointcache.py +++ b/openpype/hosts/maya/plugins/publish/extract_pointcache.py @@ -114,6 +114,7 @@ class ExtractAlembic(publish.Extractor): # Extract proxy. if not instance.data.get("proxy"): + self.log.info("No proxy nodes found. Skipping proxy extraction.") return path = path.replace(".abc", "_proxy.abc")