From c5b05a95c6799284bf688ef8a0b45e67841dc6b4 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 10 Aug 2023 11:42:08 +0100 Subject: [PATCH 01/37] Basic implementation --- .../blender/plugins/create/create_render.py | 49 +++++ .../blender/plugins/publish/collect_render.py | 81 ++++++++ .../publish/submit_blender_deadline.py | 175 ++++++++++++++++++ .../plugins/publish/submit_publish_job.py | 2 +- 4 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/blender/plugins/create/create_render.py create mode 100644 openpype/hosts/blender/plugins/publish/collect_render.py create mode 100644 openpype/modules/deadline/plugins/publish/submit_blender_deadline.py diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py new file mode 100644 index 0000000000..8323b88cfe --- /dev/null +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -0,0 +1,49 @@ +"""Create render.""" + +import bpy + +from openpype.pipeline import get_current_task_name +from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES + + +class CreateRenderlayer(plugin.Creator): + """Single baked camera""" + + name = "renderingMain" + label = "Render" + family = "renderlayer" + icon = "eye" + + render_settings = {} + + def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process) + ops.execute_in_main_thread(mti) + + def _process(self): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + asset = self.data["asset"] + subset = self.data["subset"] + name = plugin.asset_name(asset, subset) + asset_group = bpy.data.collections.new(name=name) + instances.children.link(asset_group) + self.data['task'] = get_current_task_name() + lib.imprint(asset_group, self.data) + + if (self.options or {}).get("useSelection"): + selected = lib.get_selection() + for obj in selected: + asset_group.objects.link(obj) + elif (self.options or {}).get("asset_group"): + obj = (self.options or {}).get("asset_group") + asset_group.objects.link(obj) + + return asset_group diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py new file mode 100644 index 0000000000..c0d314e466 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +"""Collect render data.""" + +import os +import re + +import bpy + +import pyblish.api + + +class CollectBlenderRender(pyblish.api.InstancePlugin): + """Gather all publishable render layers from renderSetup.""" + + order = pyblish.api.CollectorOrder + 0.01 + hosts = ["blender"] + families = ["renderlayer"] + label = "Collect Render Layers" + sync_workfile_version = False + + def process(self, instance): + context = instance.context + + filepath = context.data["currentFile"].replace("\\", "/") + + frame_start = context.data["frameStart"] + frame_end = context.data["frameEnd"] + frame_handle_start = context.data["frameStartHandle"] + frame_handle_end = context.data["frameEndHandle"] + + instance.data.update({ + "frameStart": frame_start, + "frameEnd": frame_end, + "frameStartHandle": frame_handle_start, + "frameEndHandle": frame_handle_end, + "fps": context.data["fps"], + "byFrameStep": bpy.context.scene.frame_step, + "farm": True, + "toBeRenderedOn": "deadline", + }) + + # instance.data["expectedFiles"] = self.generate_expected_files( + # instance, filepath) + + expected_files = [] + + for frame in range( + int(frame_start), + int(frame_end) + 1, + int(bpy.context.scene.frame_step), + ): + frame_str = str(frame).rjust(4, "0") + expected_files.append(f"C:/tmp/{frame_str}.png") + + instance.data["expectedFiles"] = expected_files + + self.log.debug(instance.data["expectedFiles"]) + + def generate_expected_files(self, instance, path): + """Create expected files in instance data""" + + dir = os.path.dirname(path) + file = os.path.basename(path) + + if "#" in file: + def replace(match): + return "%0{}d".format(len(match.group())) + + file = re.sub("#+", replace, file) + + if "%" not in file: + return path + + expected_files = [] + start = instance.data["frameStart"] + end = instance.data["frameEnd"] + for i in range(int(start), (int(end) + 1)): + expected_files.append( + os.path.join(dir, (file % i)).replace("\\", "/")) + + return expected_files diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py new file mode 100644 index 0000000000..66be306a52 --- /dev/null +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +"""Submitting render job to Deadline.""" + +import os +import getpass +import attr +from datetime import datetime + +import bpy + +from openpype.lib import is_running_from_build +from openpype.pipeline import legacy_io +from openpype.pipeline.farm.tools import iter_expected_files +from openpype.tests.lib import is_in_tests + +from openpype_modules.deadline import abstract_submit_deadline +from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo + + +def _validate_deadline_bool_value(instance, attribute, value): + if not isinstance(value, (str, bool)): + raise TypeError(f"Attribute {attribute} must be str or bool.") + if value not in {"1", "0", True, False}: + raise ValueError( + f"Value of {attribute} must be one of '0', '1', True, False") + + +@attr.s +class BlenderPluginInfo(): + SceneFile = attr.ib(default=None) # Input + Version = attr.ib(default=None) # Mandatory for Deadline + + +class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): + label = "Submit Render to Deadline" + hosts = ["blender"] + families = ["renderlayer"] + + priority = 50 + + jobInfo = {} + pluginInfo = {} + group = None + + def get_job_info(self): + job_info = DeadlineJobInfo(Plugin="Blender") + + job_info.update(self.jobInfo) + + instance = self._instance + context = instance.context + + # Always use the original work file name for the Job name even when + # rendering is done from the published Work File. The original work + # file name is clearer because it can also have subversion strings, + # etc. which are stripped for the published file. + src_filepath = context.data["currentFile"] + src_filename = os.path.basename(src_filepath) + + if is_in_tests(): + src_filename += datetime.now().strftime("%d%m%Y%H%M%S") + + job_info.Name = f"{src_filename} - {instance.name}" + job_info.BatchName = src_filename + instance.data.get("blenderRenderPlugin", "Blender") + job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) + + # Deadline requires integers in frame range + frames = "{start}-{end}x{step}".format( + start=int(instance.data["frameStartHandle"]), + end=int(instance.data["frameEndHandle"]), + step=int(instance.data["byFrameStep"]), + ) + job_info.Frames = frames + + job_info.Pool = instance.data.get("primaryPool") + job_info.SecondaryPool = instance.data.get("secondaryPool") + job_info.Comment = context.data.get("comment") + job_info.Priority = instance.data.get("priority", self.priority) + + if self.group != "none" and self.group: + job_info.Group = self.group + + attr_values = self.get_attr_values_from_data(instance.data) + render_globals = instance.data.setdefault("renderGlobals", {}) + machine_list = attr_values.get("machineList", "") + if machine_list: + if attr_values.get("whitelist", True): + machine_list_key = "Whitelist" + else: + machine_list_key = "Blacklist" + render_globals[machine_list_key] = machine_list + + job_info.Priority = attr_values.get("priority") + job_info.ChunkSize = attr_values.get("chunkSize") + + # Add options from RenderGlobals + render_globals = instance.data.get("renderGlobals", {}) + job_info.update(render_globals) + + keys = [ + "FTRACK_API_KEY", + "FTRACK_API_USER", + "FTRACK_SERVER", + "OPENPYPE_SG_USER", + "AVALON_PROJECT", + "AVALON_ASSET", + "AVALON_TASK", + "AVALON_APP_NAME", + "OPENPYPE_DEV" + "IS_TEST" + ] + + # Add OpenPype version if we are running from build. + if is_running_from_build(): + keys.append("OPENPYPE_VERSION") + + # Add mongo url if it's enabled + if self._instance.context.data.get("deadlinePassMongoUrl"): + keys.append("OPENPYPE_MONGO") + + environment = dict({key: os.environ[key] for key in keys + if key in os.environ}, **legacy_io.Session) + + for key in keys: + value = environment.get(key) + if not value: + continue + job_info.EnvironmentKeyValue[key] = value + + # to recognize job from PYPE for turning Event On/Off + job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" + job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1" + + # Adding file dependencies. + if self.asset_dependencies: + dependencies = instance.context.data["fileDependencies"] + for dependency in dependencies: + job_info.AssetDependency += dependency + + # Add list of expected files to job + # --------------------------------- + exp = instance.data.get("expectedFiles") + for filepath in iter_expected_files(exp): + job_info.OutputDirectory += os.path.dirname(filepath) + job_info.OutputFilename += os.path.basename(filepath) + + return job_info + + def get_plugin_info(self): + instance = self._instance + context = instance.context + + plugin_info = BlenderPluginInfo( + SceneFile=self.scene_path, + Version=bpy.app.version_string, + ) + + plugin_payload = attr.asdict(plugin_info) + + # Patching with pluginInfo from settings + for key, value in self.pluginInfo.items(): + plugin_payload[key] = value + + return plugin_payload + + def process(self, instance): + output_dir = "C:/tmp" + instance.data["outputDir"] = output_dir + + super(BlenderSubmitDeadline, self).process(instance) + + # TODO: Avoid the need for this logic here, needed for submit publish + # Store output dir for unified publisher (filesequence) + # output_dir = os.path.dirname(instance.data["expectedFiles"][0]) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index ec182fcd66..47cb441143 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -95,7 +95,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, targets = ["local"] hosts = ["fusion", "max", "maya", "nuke", "houdini", - "celaction", "aftereffects", "harmony"] + "celaction", "aftereffects", "harmony", "blender"] families = ["render.farm", "prerender.farm", "renderlayer", "imagesequence", From fb76d6348155b6f0db6dee923d74a653921d3707 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 10 Aug 2023 17:41:02 +0100 Subject: [PATCH 02/37] Implemented settings and removed hardcoded paths --- .../blender/plugins/publish/collect_render.py | 150 +++++++++++++----- .../publish/submit_blender_deadline.py | 41 +++-- .../defaults/project_settings/blender.json | 4 + .../defaults/project_settings/deadline.json | 9 ++ .../schema_project_blender.json | 31 ++++ .../schema_project_deadline.json | 44 +++++ 6 files changed, 228 insertions(+), 51 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index c0d314e466..eb37d5b946 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -6,6 +6,12 @@ import re import bpy +from openpype.pipeline import ( + get_current_project_name, +) +from openpype.settings import ( + get_project_settings, +) import pyblish.api @@ -18,16 +24,117 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): label = "Collect Render Layers" sync_workfile_version = False + @staticmethod + def get_default_render_folder(settings): + """Get default render folder from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["default_render_image_folder"]) + + @staticmethod + def get_image_format(settings): + """Get image format from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["image_format"]) + + @staticmethod + def get_render_product(file_path, render_folder, file_name, instance, ext): + output_file = os.path.join( + file_path, render_folder, file_name, instance.name) + + render_product = f"{output_file}.####.{ext}" + render_product = render_product.replace("\\", "/") + + return render_product + + @staticmethod + def generate_expected_files( + render_product, frame_start, frame_end, frame_step + ): + path = os.path.dirname(render_product) + file = os.path.basename(render_product) + + expected_files = [] + + for frame in range(frame_start, frame_end + 1, frame_step): + frame_str = str(frame).rjust(4, "0") + expected_file = os.path.join(path, re.sub("#+", frame_str, file)) + expected_files.append(expected_file.replace("\\", "/")) + + return expected_files + + @staticmethod + def set_render_format(ext): + image_settings = bpy.context.scene.render.image_settings + + if ext == "exr": + image_settings.file_format = "OPEN_EXR" + elif ext == "bmp": + image_settings.file_format = "BMP" + elif ext == "iris": + image_settings.file_format = "IRIS" + elif ext == "png": + image_settings.file_format = "PNG" + elif ext == "jpeg": + image_settings.file_format = "JPEG" + elif ext == "jpeg2000": + image_settings.file_format = "JPEG2000" + elif ext == "tga": + image_settings.file_format = "TARGA" + elif ext == "tga_raw": + image_settings.file_format = "TARGA_RAW" + elif ext == "tiff": + image_settings.file_format = "TIFF" + + @staticmethod + def set_render_camera(instance): + # There should be only one camera in the instance + found = False + for obj in instance: + if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA": + bpy.context.scene.camera = obj + found = True + break + + assert found, "No camera found in the render instance" + def process(self, instance): context = instance.context filepath = context.data["currentFile"].replace("\\", "/") + file_path = os.path.dirname(filepath) + file_name = os.path.basename(filepath) + file_name, _ = os.path.splitext(file_name) + + project = get_current_project_name() + settings = get_project_settings(project) + + render_folder = self.get_default_render_folder(settings) + ext = self.get_image_format(settings) + + render_product = self.get_render_product( + file_path, render_folder, file_name, instance, ext) + + # We set the render path, the format and the camera + bpy.context.scene.render.filepath = render_product + self.set_render_format(ext) + self.set_render_camera(instance) + + # We save the file to save the render settings + bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) frame_start = context.data["frameStart"] frame_end = context.data["frameEnd"] frame_handle_start = context.data["frameStartHandle"] frame_handle_end = context.data["frameEndHandle"] + expected_files = self.generate_expected_files( + render_product, int(frame_start), int(frame_end), + int(bpy.context.scene.frame_step)) + instance.data.update({ "frameStart": frame_start, "frameEnd": frame_end, @@ -36,46 +143,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): "fps": context.data["fps"], "byFrameStep": bpy.context.scene.frame_step, "farm": True, - "toBeRenderedOn": "deadline", + "expectedFiles": expected_files, }) - # instance.data["expectedFiles"] = self.generate_expected_files( - # instance, filepath) - - expected_files = [] - - for frame in range( - int(frame_start), - int(frame_end) + 1, - int(bpy.context.scene.frame_step), - ): - frame_str = str(frame).rjust(4, "0") - expected_files.append(f"C:/tmp/{frame_str}.png") - - instance.data["expectedFiles"] = expected_files - - self.log.debug(instance.data["expectedFiles"]) - - def generate_expected_files(self, instance, path): - """Create expected files in instance data""" - - dir = os.path.dirname(path) - file = os.path.basename(path) - - if "#" in file: - def replace(match): - return "%0{}d".format(len(match.group())) - - file = re.sub("#+", replace, file) - - if "%" not in file: - return path - - expected_files = [] - start = instance.data["frameStart"] - end = instance.data["frameEnd"] - for i in range(int(start), (int(end) + 1)): - expected_files.append( - os.path.join(dir, (file % i)).replace("\\", "/")) - - return expected_files + self.log.info(f"data: {instance.data}") diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 66be306a52..761c5b7b06 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -29,6 +29,7 @@ def _validate_deadline_bool_value(instance, attribute, value): class BlenderPluginInfo(): SceneFile = attr.ib(default=None) # Input Version = attr.ib(default=None) # Mandatory for Deadline + SaveFile = attr.ib(default=True) class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): @@ -36,8 +37,9 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): hosts = ["blender"] families = ["renderlayer"] + use_published = True priority = 50 - + chunk_size = 1 jobInfo = {} pluginInfo = {} group = None @@ -148,12 +150,10 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): return job_info def get_plugin_info(self): - instance = self._instance - context = instance.context - plugin_info = BlenderPluginInfo( SceneFile=self.scene_path, Version=bpy.app.version_string, + SaveFile=True, ) plugin_payload = attr.asdict(plugin_info) @@ -164,12 +164,33 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): return plugin_payload - def process(self, instance): - output_dir = "C:/tmp" + def process_submission(self): + instance = self._instance + + expected_files = instance.data["expectedFiles"] + if not expected_files: + raise RuntimeError("No Render Elements found!") + + output_dir = os.path.dirname(expected_files[0]) instance.data["outputDir"] = output_dir + instance.data["toBeRenderedOn"] = "deadline" - super(BlenderSubmitDeadline, self).process(instance) + file = os.path.basename(bpy.context.scene.render.filepath) + bpy.context.scene.render.filepath = os.path.join(output_dir, file) + bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) - # TODO: Avoid the need for this logic here, needed for submit publish - # Store output dir for unified publisher (filesequence) - # output_dir = os.path.dirname(instance.data["expectedFiles"][0]) + self.log.debug(f"expected_files[0]: {expected_files[0]}") + self.log.debug(f"Output dir: {output_dir}") + + payload = self.assemble_payload() + return self.submit(payload) + + def from_published_scene(self): + """ Do not overwrite expected files. + + Use published is set to True, so rendering will be triggered + from published scene (in 'publish' folder). Default implementation + of abstract class renames expected (eg. rendered) files accordingly + which is not needed here. + """ + return super().from_published_scene(False) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index df865adeba..333a1fed56 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -17,6 +17,10 @@ "rules": {} } }, + "RenderSettings": { + "default_render_image_folder": "renders/blender", + "image_format": "exr" + }, "workfile_builder": { "create_first_version": false, "custom_templates": [] diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 1b8c8397d7..33ea533863 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -99,6 +99,15 @@ "deadline_chunk_size": 10, "deadline_job_delay": "00:00:00:00" }, + "BlenderSubmitDeadline": { + "enabled": true, + "optional": false, + "active": true, + "use_published": true, + "priority": 50, + "chunk_size": 10, + "group": "none" + }, "ProcessSubmittedJobOnFarm": { "enabled": true, "deadline_department": "", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index aeb70dfd8c..787e190de5 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -54,6 +54,37 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "RenderSettings", + "label": "Render Settings", + "children": [ + { + "type": "text", + "key": "default_render_image_folder", + "label": "Default render image folder" + }, + { + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"exr": "exr"}, + {"bmp": "bmp"}, + {"iris": "iris"}, + {"png": "png"}, + {"jpeg": "jpeg"}, + {"jpeg2000": "jpeg2000"}, + {"tga": "tga"}, + {"tga_raw": "tga_raw"}, + {"tiff": "tiff"} + ] + } + ] + }, { "type": "schema_template", "name": "template_workfile_options", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json index 6d59b5a92b..596bc30f91 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -531,6 +531,50 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "BlenderSubmitDeadline", + "label": "Blender Submit to Deadline", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "boolean", + "key": "use_published", + "label": "Use Published scene" + }, + { + "type": "number", + "key": "priority", + "label": "Priority" + }, + { + "type": "number", + "key": "chunk_size", + "label": "Frame per Task" + }, + { + "type": "text", + "key": "group", + "label": "Group Name" + } + ] + }, { "type": "dict", "collapsible": true, From a1e8a5eb4c49893291403c4283a60b8a7fad27cf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 10 Aug 2023 17:54:45 +0100 Subject: [PATCH 03/37] Removed some missed leftover code --- .../deadline/plugins/publish/submit_blender_deadline.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 761c5b7b06..b3deb39399 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -175,13 +175,6 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): instance.data["outputDir"] = output_dir instance.data["toBeRenderedOn"] = "deadline" - file = os.path.basename(bpy.context.scene.render.filepath) - bpy.context.scene.render.filepath = os.path.join(output_dir, file) - bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) - - self.log.debug(f"expected_files[0]: {expected_files[0]}") - self.log.debug(f"Output dir: {output_dir}") - payload = self.assemble_payload() return self.submit(payload) From 17b5a86c51c3d1ca3f1a16f8cb249f8c3e8d5ca4 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 11 Aug 2023 11:24:09 +0100 Subject: [PATCH 04/37] Fixed problem with image format --- .../blender/plugins/publish/collect_render.py | 8 +++----- .../projects_schema/schema_project_blender.json | 17 ++++++++--------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index eb37d5b946..11b98c76e6 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -74,19 +74,17 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): image_settings.file_format = "OPEN_EXR" elif ext == "bmp": image_settings.file_format = "BMP" - elif ext == "iris": + elif ext == "rgb": image_settings.file_format = "IRIS" elif ext == "png": image_settings.file_format = "PNG" elif ext == "jpeg": image_settings.file_format = "JPEG" - elif ext == "jpeg2000": + elif ext == "jp2": image_settings.file_format = "JPEG2000" elif ext == "tga": image_settings.file_format = "TARGA" - elif ext == "tga_raw": - image_settings.file_format = "TARGA_RAW" - elif ext == "tiff": + elif ext == "tif": image_settings.file_format = "TIFF" @staticmethod diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 787e190de5..84efec5c0a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -72,15 +72,14 @@ "multiselection": false, "defaults": "exr", "enum_items": [ - {"exr": "exr"}, - {"bmp": "bmp"}, - {"iris": "iris"}, - {"png": "png"}, - {"jpeg": "jpeg"}, - {"jpeg2000": "jpeg2000"}, - {"tga": "tga"}, - {"tga_raw": "tga_raw"}, - {"tiff": "tiff"} + {"exr": "OpenEXR"}, + {"bmp": "BMP"}, + {"rgb": "Iris"}, + {"png": "PNG"}, + {"jpg": "JPEG"}, + {"jp2": "JPEG 2000"}, + {"tga": "Targa"}, + {"tif": "TIFF"} ] } ] From 7d7a41792e96ab3eb93a48e8c641f8bba3ec0f58 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 11 Aug 2023 14:52:15 +0100 Subject: [PATCH 05/37] Added more comments --- .../blender/plugins/publish/collect_render.py | 15 +++++++++++++++ .../plugins/publish/submit_blender_deadline.py | 11 +++++------ 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 11b98c76e6..7f060c3b7c 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -42,6 +42,17 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): @staticmethod def get_render_product(file_path, render_folder, file_name, instance, ext): + """ + Generate the path to the render product. Blender interprets the `#` + as the frame number, when it renders. + + Args: + file_path (str): The path to the blender scene. + render_folder (str): The render folder set in settings. + file_name (str): The name of the blender scene. + instance (pyblish.api.Instance): The instance to publish. + ext (str): The image format to render. + """ output_file = os.path.join( file_path, render_folder, file_name, instance.name) @@ -54,6 +65,10 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): def generate_expected_files( render_product, frame_start, frame_end, frame_step ): + """Generate the expected files for the render product. + This returns a list of files that should be rendered. It replaces + the sequence of `#` with the frame number. + """ path = os.path.dirname(render_product) file = os.path.basename(render_product) diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index b3deb39399..7aee087ddc 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -179,11 +179,10 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): return self.submit(payload) def from_published_scene(self): - """ Do not overwrite expected files. - - Use published is set to True, so rendering will be triggered - from published scene (in 'publish' folder). Default implementation - of abstract class renames expected (eg. rendered) files accordingly - which is not needed here. + """ + This is needed to set the correct path for the json metadata. Because + the rendering path is set in the blend file during the collection, + and the path is adjusted to use the published scene, this ensures that + the metadata and the rendered files are in the same location. """ return super().from_published_scene(False) From 94c801ce84fb1bd99ffe57fee9bb0ea127dc03c9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 11 Aug 2023 14:52:28 +0100 Subject: [PATCH 06/37] Removed redundant code --- .../deadline/plugins/publish/submit_blender_deadline.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 7aee087ddc..9bfc4fbb07 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -17,14 +17,6 @@ from openpype_modules.deadline import abstract_submit_deadline from openpype_modules.deadline.abstract_submit_deadline import DeadlineJobInfo -def _validate_deadline_bool_value(instance, attribute, value): - if not isinstance(value, (str, bool)): - raise TypeError(f"Attribute {attribute} must be str or bool.") - if value not in {"1", "0", True, False}: - raise ValueError( - f"Value of {attribute} must be one of '0', '1', True, False") - - @attr.s class BlenderPluginInfo(): SceneFile = attr.ib(default=None) # Input From 85b49ec761deb3187ded2ee4262f23efa227c40f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 22 Aug 2023 10:52:02 +0100 Subject: [PATCH 07/37] Basic implementation for AOVs rendering --- .../blender/plugins/publish/collect_render.py | 101 ++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 7f060c3b7c..fafdd9cc2d 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -12,6 +12,10 @@ from openpype.pipeline import ( from openpype.settings import ( get_project_settings, ) +from openpype.hosts.blender.api.ops import ( + MainThreadItem, + execute_in_main_thread +) import pyblish.api @@ -41,7 +45,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): ["image_format"]) @staticmethod - def get_render_product(file_path, render_folder, file_name, instance, ext): + def get_render_product(output_path, instance): """ Generate the path to the render product. Blender interprets the `#` as the frame number, when it renders. @@ -53,10 +57,9 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): instance (pyblish.api.Instance): The instance to publish. ext (str): The image format to render. """ - output_file = os.path.join( - file_path, render_folder, file_name, instance.name) + output_file = os.path.join(output_path, instance.name) - render_product = f"{output_file}.####.{ext}" + render_product = f"{output_file}.####" render_product = render_product.replace("\\", "/") return render_product @@ -83,9 +86,13 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): @staticmethod def set_render_format(ext): + # Set Blender to save the file with the right extension + bpy.context.scene.render.use_file_extension = True + image_settings = bpy.context.scene.render.image_settings if ext == "exr": + # TODO: Check if multilayer option is selected image_settings.file_format = "OPEN_EXR" elif ext == "bmp": image_settings.file_format = "BMP" @@ -102,6 +109,86 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): elif ext == "tif": image_settings.file_format = "TIFF" + def _set_node_tree(self, output_path, instance): + # Set the scene to use the compositor node tree to render + bpy.context.scene.use_nodes = True + + tree = bpy.context.scene.node_tree + + # Get the Render Layers node + rl_node = None + for node in tree.nodes: + if node.bl_idname == "CompositorNodeRLayers": + rl_node = node + break + + # If there's not a Render Layers node, we create it + if not rl_node: + rl_node = tree.nodes.new("CompositorNodeRLayers") + + # Get the enabled output sockets, that are the active passes for the + # render. + # We also exclude some layers. + exclude_sockets = ["Image", "Alpha"] + passes = [ + socket for socket in rl_node.outputs + if socket.enabled and socket.name not in exclude_sockets + ] + + # Remove all output nodes + for node in tree.nodes: + if node.bl_idname == "CompositorNodeOutputFile": + tree.nodes.remove(node) + + # Create a new output node + output = tree.nodes.new("CompositorNodeOutputFile") + + + context = bpy.context.copy() + # context = create_blender_context() + context["node"] = output + + win = bpy.context.window_manager.windows[0] + screen = win.screen + area = screen.areas[0] + region = area.regions[0] + + context["window"] = win + context['screen'] = screen + context['area'] = area + context['region'] = region + + self.log.debug(f"context: {context}") + + # Change area type to node editor, to execute node operators + old_area_type = area.ui_type + area.ui_type = "CompositorNodeTree" + + # Remove the default input socket from the output node + bpy.ops.node.output_file_remove_active_socket(context) + + output.base_path = output_path + image_settings = bpy.context.scene.render.image_settings + output.format.file_format = image_settings.file_format + + # For each active render pass, we add a new socket to the output node + # and link it + for render_pass in passes: + bpy.ops.node.output_file_add_socket( + context, file_path=f"{instance.name}_{render_pass.name}.####") + + node_input = output.inputs[-1] + + tree.links.new(render_pass, node_input) + + # Restore the area type + area.ui_type = old_area_type + + def set_node_tree(self, output_path, instance): + """ Run the creator on Blender main thread""" + mti = MainThreadItem(self._set_node_tree, output_path, instance) + execute_in_main_thread(mti) + @staticmethod def set_render_camera(instance): # There should be only one camera in the instance @@ -128,8 +215,10 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): render_folder = self.get_default_render_folder(settings) ext = self.get_image_format(settings) - render_product = self.get_render_product( - file_path, render_folder, file_name, instance, ext) + output_path = os.path.join(file_path, render_folder, file_name) + + render_product = self.get_render_product(output_path, instance) + self.set_node_tree(output_path, instance) # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product From eb1b5425de4aa546eaaae682f2acf9209dadf359 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 22 Aug 2023 11:54:49 +0100 Subject: [PATCH 08/37] Added support for multilayer EXR --- .../blender/plugins/publish/collect_render.py | 48 ++++++++++++------- .../defaults/project_settings/blender.json | 3 +- .../schema_project_blender.json | 35 ++++++++------ 3 files changed, 52 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index fafdd9cc2d..cd3b922697 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -44,6 +44,14 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): ["RenderSettings"] ["image_format"]) + @staticmethod + def get_multilayer(settings): + """Get multilayer from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["multilayer_exr"]) + @staticmethod def get_render_product(output_path, instance): """ @@ -85,15 +93,15 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): return expected_files @staticmethod - def set_render_format(ext): + def set_render_format(ext, multilayer): # Set Blender to save the file with the right extension bpy.context.scene.render.use_file_extension = True image_settings = bpy.context.scene.render.image_settings if ext == "exr": - # TODO: Check if multilayer option is selected - image_settings.file_format = "OPEN_EXR" + image_settings.file_format = ( + "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR") elif ext == "bmp": image_settings.file_format = "BMP" elif ext == "rgb": @@ -109,6 +117,21 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): elif ext == "tif": image_settings.file_format = "TIFF" + def _create_context(): + context = bpy.context.copy() + + win = bpy.context.window_manager.windows[0] + screen = win.screen + area = screen.areas[0] + region = area.regions[0] + + context["window"] = win + context['screen'] = screen + context['area'] = area + context['region'] = region + + return context + def _set_node_tree(self, output_path, instance): # Set the scene to use the compositor node tree to render bpy.context.scene.use_nodes = True @@ -143,22 +166,10 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # Create a new output node output = tree.nodes.new("CompositorNodeOutputFile") - - context = bpy.context.copy() - # context = create_blender_context() + context = self._create_context() context["node"] = output - win = bpy.context.window_manager.windows[0] - screen = win.screen - area = screen.areas[0] - region = area.regions[0] - - context["window"] = win - context['screen'] = screen - context['area'] = area - context['region'] = region - - self.log.debug(f"context: {context}") + area = context["area"] # Change area type to node editor, to execute node operators old_area_type = area.ui_type @@ -214,6 +225,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): render_folder = self.get_default_render_folder(settings) ext = self.get_image_format(settings) + multilayer = self.get_multilayer(settings) output_path = os.path.join(file_path, render_folder, file_name) @@ -222,7 +234,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product - self.set_render_format(ext) + self.set_render_format(ext, multilayer) self.set_render_camera(instance) # We save the file to save the render settings diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 333a1fed56..c375e550c2 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -19,7 +19,8 @@ }, "RenderSettings": { "default_render_image_folder": "renders/blender", - "image_format": "exr" + "image_format": "exr", + "multilayer_exr": true }, "workfile_builder": { "create_first_version": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 84efec5c0a..d8ef1eee3e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -66,21 +66,26 @@ "label": "Default render image folder" }, { - "key": "image_format", - "label": "Output Image Format", - "type": "enum", - "multiselection": false, - "defaults": "exr", - "enum_items": [ - {"exr": "OpenEXR"}, - {"bmp": "BMP"}, - {"rgb": "Iris"}, - {"png": "PNG"}, - {"jpg": "JPEG"}, - {"jp2": "JPEG 2000"}, - {"tga": "Targa"}, - {"tif": "TIFF"} - ] + "key": "image_format", + "label": "Output Image Format", + "type": "enum", + "multiselection": false, + "defaults": "exr", + "enum_items": [ + {"exr": "OpenEXR"}, + {"bmp": "BMP"}, + {"rgb": "Iris"}, + {"png": "PNG"}, + {"jpg": "JPEG"}, + {"jp2": "JPEG 2000"}, + {"tga": "Targa"}, + {"tif": "TIFF"} + ] + }, + { + "key": "multilayer_exr", + "type": "boolean", + "label": "Multilayer (EXR)" } ] }, From 9f56721334c8a7d42aa88c34ee4ef01befeb153d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 22 Aug 2023 15:56:23 +0100 Subject: [PATCH 09/37] Implemented AOVs rendering in deadline and publishing --- openpype/hosts/blender/api/colorspace.py | 52 +++++++++++ .../blender/plugins/publish/collect_render.py | 92 ++++++++++++++----- .../publish/submit_blender_deadline.py | 3 +- 3 files changed, 122 insertions(+), 25 deletions(-) create mode 100644 openpype/hosts/blender/api/colorspace.py diff --git a/openpype/hosts/blender/api/colorspace.py b/openpype/hosts/blender/api/colorspace.py new file mode 100644 index 0000000000..59deb514f8 --- /dev/null +++ b/openpype/hosts/blender/api/colorspace.py @@ -0,0 +1,52 @@ +import attr + +import bpy + + +@attr.s +class LayerMetadata(object): + """Data class for Render Layer metadata.""" + frameStart = attr.ib() + frameEnd = attr.ib() + + +@attr.s +class RenderProduct(object): + """Getting Colorspace as + Specific Render Product Parameter for submitting + publish job. + """ + colorspace = attr.ib() # colorspace + view = attr.ib() + productName = attr.ib(default=None) + + +class ARenderProduct(object): + + def __init__(self): + """Constructor.""" + # Initialize + self.layer_data = self._get_layer_data() + self.layer_data.products = self.get_colorspace_data() + + def _get_layer_data(self): + scene = bpy.context.scene + + return LayerMetadata( + frameStart=int(scene.frame_start), + frameEnd=int(scene.frame_end), + ) + + def get_colorspace_data(self): + """To be implemented by renderer class. + This should return a list of RenderProducts. + Returns: + list: List of RenderProduct + """ + return [ + RenderProduct( + colorspace="sRGB", + view="ACES 1.0", + productName="" + ) + ] diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index cd3b922697..becb735c21 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -12,10 +12,7 @@ from openpype.pipeline import ( from openpype.settings import ( get_project_settings, ) -from openpype.hosts.blender.api.ops import ( - MainThreadItem, - execute_in_main_thread -) +from openpype.hosts.blender.api import colorspace import pyblish.api @@ -73,12 +70,13 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): return render_product @staticmethod - def generate_expected_files( - render_product, frame_start, frame_end, frame_step + def generate_expected_beauty( + render_product, frame_start, frame_end, frame_step, ext ): - """Generate the expected files for the render product. - This returns a list of files that should be rendered. It replaces - the sequence of `#` with the frame number. + """ + Generate the expected files for the render product for the beauty + render. This returns a list of files that should be rendered. It + replaces the sequence of `#` with the frame number. """ path = os.path.dirname(render_product) file = os.path.basename(render_product) @@ -87,9 +85,39 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): for frame in range(frame_start, frame_end + 1, frame_step): frame_str = str(frame).rjust(4, "0") - expected_file = os.path.join(path, re.sub("#+", frame_str, file)) + filename = re.sub("#+", frame_str, file) + expected_file = f"{os.path.join(path, filename)}.{ext}" expected_files.append(expected_file.replace("\\", "/")) + return { + "beauty": expected_files + } + + @staticmethod + def generate_expected_aovs( + aov_file_product, frame_start, frame_end, frame_step, ext + ): + """ + Generate the expected files for the render product for the beauty + render. This returns a list of files that should be rendered. It + replaces the sequence of `#` with the frame number. + """ + expected_files = {} + + for aov_name, aov_file in aov_file_product: + path = os.path.dirname(aov_file) + file = os.path.basename(aov_file) + + aov_files = [] + + for frame in range(frame_start, frame_end + 1, frame_step): + frame_str = str(frame).rjust(4, "0") + filename = re.sub("#+", frame_str, file) + expected_file = f"{os.path.join(path, filename)}.{ext}" + aov_files.append(expected_file.replace("\\", "/")) + + expected_files[aov_name] = aov_files + return expected_files @staticmethod @@ -117,7 +145,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): elif ext == "tif": image_settings.file_format = "TIFF" - def _create_context(): + def _create_context(self): context = bpy.context.copy() win = bpy.context.window_manager.windows[0] @@ -132,7 +160,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): return context - def _set_node_tree(self, output_path, instance): + def set_node_tree(self, output_path, instance): # Set the scene to use the compositor node tree to render bpy.context.scene.use_nodes = True @@ -153,8 +181,9 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # render. # We also exclude some layers. exclude_sockets = ["Image", "Alpha"] - passes = [ - socket for socket in rl_node.outputs + passes = [ + socket + for socket in rl_node.outputs if socket.enabled and socket.name not in exclude_sockets ] @@ -182,11 +211,16 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): image_settings = bpy.context.scene.render.image_settings output.format.file_format = image_settings.file_format + aov_file_products = [] + # For each active render pass, we add a new socket to the output node # and link it for render_pass in passes: - bpy.ops.node.output_file_add_socket( - context, file_path=f"{instance.name}_{render_pass.name}.####") + filepath = f"{instance.name}_{render_pass.name}.####" + bpy.ops.node.output_file_add_socket(context, file_path=filepath) + + aov_file_products.append( + (render_pass.name, os.path.join(output_path, filepath))) node_input = output.inputs[-1] @@ -195,10 +229,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # Restore the area type area.ui_type = old_area_type - def set_node_tree(self, output_path, instance): - """ Run the creator on Blender main thread""" - mti = MainThreadItem(self._set_node_tree, output_path, instance) - execute_in_main_thread(mti) + return aov_file_products @staticmethod def set_render_camera(instance): @@ -230,7 +261,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): output_path = os.path.join(file_path, render_folder, file_name) render_product = self.get_render_product(output_path, instance) - self.set_node_tree(output_path, instance) + aov_file_product = self.set_node_tree(output_path, instance) # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product @@ -245,9 +276,15 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): frame_handle_start = context.data["frameStartHandle"] frame_handle_end = context.data["frameEndHandle"] - expected_files = self.generate_expected_files( + expected_beauty = self.generate_expected_beauty( render_product, int(frame_start), int(frame_end), - int(bpy.context.scene.frame_step)) + int(bpy.context.scene.frame_step), ext) + + expected_aovs = self.generate_expected_aovs( + aov_file_product, int(frame_start), int(frame_end), + int(bpy.context.scene.frame_step), ext) + + expected_files = expected_beauty | expected_aovs instance.data.update({ "frameStart": frame_start, @@ -257,7 +294,14 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): "fps": context.data["fps"], "byFrameStep": bpy.context.scene.frame_step, "farm": True, - "expectedFiles": expected_files, + "expectedFiles": [expected_files], + # OCIO not currently implemented in Blender, but the following + # settings are required by the schema, so it is hardcoded. + # TODO: Implement OCIO in Blender + "colorspaceConfig": "", + "colorspaceDisplay": "sRGB", + "colorspaceView": "ACES 1.0 SDR-video", + "renderProducts": colorspace.ARenderProduct(), }) self.log.info(f"data: {instance.data}") diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 9bfc4fbb07..ad456c0d13 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -163,7 +163,8 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): if not expected_files: raise RuntimeError("No Render Elements found!") - output_dir = os.path.dirname(expected_files[0]) + first_file = next(iter_expected_files(expected_files)) + output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir instance.data["toBeRenderedOn"] = "deadline" From c9f5a9743257b8ce242660da473d69cb8dba8b52 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 22 Aug 2023 16:33:51 +0100 Subject: [PATCH 10/37] Added setting for aov separator --- .../blender/plugins/publish/collect_render.py | 26 ++++++++++++++++--- .../defaults/project_settings/blender.json | 1 + .../schema_project_blender.json | 12 +++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index becb735c21..b16354460a 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -33,6 +33,23 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): ["RenderSettings"] ["default_render_image_folder"]) + @staticmethod + def get_aov_separator(settings): + """Get aov separator from blender settings.""" + + aov_sep = (settings["blender"] + ["RenderSettings"] + ["aov_separator"]) + + if aov_sep == "dash": + return "-" + elif aov_sep == "underscore": + return "_" + elif aov_sep == "dot": + return "." + else: + raise ValueError(f"Invalid aov separator: {aov_sep}") + @staticmethod def get_image_format(settings): """Get image format from blender settings.""" @@ -160,7 +177,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): return context - def set_node_tree(self, output_path, instance): + def set_node_tree(self, output_path, instance, aov_sep): # Set the scene to use the compositor node tree to render bpy.context.scene.use_nodes = True @@ -181,7 +198,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # render. # We also exclude some layers. exclude_sockets = ["Image", "Alpha"] - passes = [ + passes = [ socket for socket in rl_node.outputs if socket.enabled and socket.name not in exclude_sockets @@ -216,7 +233,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # For each active render pass, we add a new socket to the output node # and link it for render_pass in passes: - filepath = f"{instance.name}_{render_pass.name}.####" + filepath = f"{instance.name}{aov_sep}{render_pass.name}.####" bpy.ops.node.output_file_add_socket(context, file_path=filepath) aov_file_products.append( @@ -255,13 +272,14 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): settings = get_project_settings(project) render_folder = self.get_default_render_folder(settings) + aov_sep = self.get_aov_separator(settings) ext = self.get_image_format(settings) multilayer = self.get_multilayer(settings) output_path = os.path.join(file_path, render_folder, file_name) render_product = self.get_render_product(output_path, instance) - aov_file_product = self.set_node_tree(output_path, instance) + aov_file_product = self.set_node_tree(output_path, instance, aov_sep) # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index c375e550c2..17387f4db6 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -19,6 +19,7 @@ }, "RenderSettings": { "default_render_image_folder": "renders/blender", + "aov_separator": "underscore", "image_format": "exr", "multilayer_exr": true }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index d8ef1eee3e..ecad74b621 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -65,6 +65,18 @@ "key": "default_render_image_folder", "label": "Default render image folder" }, + { + "key": "aov_separator", + "label": "AOV Separator Character", + "type": "enum", + "multiselection": false, + "defaults": "underscore", + "enum_items": [ + {"dash": "- (dash)"}, + {"underscore": "_ (underscore)"}, + {"dot": ". (dot)"} + ] + }, { "key": "image_format", "label": "Output Image Format", From 5f901f2a62d781d6f29dd145fe69bbc26da24651 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 22 Aug 2023 17:38:33 +0100 Subject: [PATCH 11/37] Added setting to set AOVs --- .../blender/plugins/publish/collect_render.py | 25 +++++++++++++++++++ .../defaults/project_settings/blender.json | 3 ++- .../schema_project_blender.json | 23 +++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index b16354460a..309d40d9fd 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -162,6 +162,29 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): elif ext == "tif": image_settings.file_format = "TIFF" + @staticmethod + def set_render_passes(settings): + aov_list = (settings["blender"] + ["RenderSettings"] + ["aov_list"]) + + scene = bpy.context.scene + vl = bpy.context.view_layer + + vl.use_pass_combined = "combined" in aov_list + vl.use_pass_z = "z" in aov_list + vl.use_pass_mist = "mist" in aov_list + vl.use_pass_normal = "normal" in aov_list + vl.use_pass_diffuse_direct = "diffuse_light" in aov_list + vl.use_pass_diffuse_color = "diffuse_color" in aov_list + vl.use_pass_glossy_direct = "specular_light" in aov_list + vl.use_pass_glossy_color = "specular_color" in aov_list + vl.eevee.use_pass_volume_direct = "volume_light" in aov_list + vl.use_pass_emit = "emission" in aov_list + vl.use_pass_environment = "environment" in aov_list + vl.use_pass_shadow = "shadow" in aov_list + vl.use_pass_ambient_occlusion = "ao" in aov_list + def _create_context(self): context = bpy.context.copy() @@ -276,6 +299,8 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): ext = self.get_image_format(settings) multilayer = self.get_multilayer(settings) + self.set_render_passes(settings) + output_path = os.path.join(file_path, render_folder, file_name) render_product = self.get_render_product(output_path, instance) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 17387f4db6..d36fc503dd 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -21,7 +21,8 @@ "default_render_image_folder": "renders/blender", "aov_separator": "underscore", "image_format": "exr", - "multilayer_exr": true + "multilayer_exr": true, + "aov_list": [] }, "workfile_builder": { "create_first_version": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index ecad74b621..b7d61d1d69 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -98,6 +98,29 @@ "key": "multilayer_exr", "type": "boolean", "label": "Multilayer (EXR)" + }, + { + "key": "aov_list", + "label": "AOVs to create", + "type": "enum", + "multiselection": true, + "defaults": "empty", + "enum_items": [ + {"empty": "< empty >"}, + {"combined": "Combined"}, + {"z": "Z"}, + {"mist": "Mist"}, + {"normal": "Normal"}, + {"diffuse_light": "Diffuse Light"}, + {"diffuse_color": "Diffuse Color"}, + {"specular_light": "Specular Light"}, + {"specular_color": "Specular Color"}, + {"volume_light": "Volume Light"}, + {"emission": "Emission"}, + {"environment": "Environment"}, + {"shadow": "Shadow"}, + {"ao": "Ambient Occlusion"} + ] } ] }, From fc1a98b47173d34843b0e2b59ca6e492763700d0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 09:32:32 +0100 Subject: [PATCH 12/37] Added support for custom render passes --- .../blender/plugins/publish/collect_render.py | 15 ++++++++++- .../defaults/project_settings/blender.json | 3 ++- .../schema_project_blender.json | 27 +++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 309d40d9fd..7c95bb14cf 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -168,7 +168,10 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): ["RenderSettings"] ["aov_list"]) - scene = bpy.context.scene + custom_passes = (settings["blender"] + ["RenderSettings"] + ["custom_passes"]) + vl = bpy.context.view_layer vl.use_pass_combined = "combined" in aov_list @@ -185,6 +188,16 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): vl.use_pass_shadow = "shadow" in aov_list vl.use_pass_ambient_occlusion = "ao" in aov_list + aovs_names = [aov.name for aov in vl.aovs] + for cp in custom_passes: + cp_name = cp[0] + if cp_name not in aovs_names: + aov = vl.aovs.add() + aov.name = cp_name + else: + aov = vl.aovs[cp_name] + aov.type = cp[1].get("type", "VALUE") + def _create_context(self): context = bpy.context.copy() diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index d36fc503dd..8b1d602df0 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -22,7 +22,8 @@ "aov_separator": "underscore", "image_format": "exr", "multilayer_exr": true, - "aov_list": [] + "aov_list": [], + "custom_passes": [] }, "workfile_builder": { "create_first_version": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index b7d61d1d69..8db57f49eb 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -121,6 +121,33 @@ {"shadow": "Shadow"}, {"ao": "Ambient Occlusion"} ] + }, + { + "type": "label", + "label": "Add custom AOVs. They are added to the view layer and in the Compositing Nodetree,\nbut they need to be added manually to the Shader Nodetree." + }, + { + "type": "dict-modifiable", + "store_as_list": true, + "key": "custom_passes", + "label": "Custom Passes", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "type", + "label": "Type", + "type": "enum", + "multiselection": false, + "defaults": "color", + "enum_items": [ + {"COLOR": "Color"}, + {"VALUE": "Value"} + ] + } + ] + } } ] }, From 8f78ebeabdb1e6e7fdf0ab461d8b609669266c29 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 10:51:01 +0100 Subject: [PATCH 13/37] Fixed problem with blender context and multilayer exr --- .../blender/plugins/publish/collect_render.py | 47 +++++-------------- 1 file changed, 13 insertions(+), 34 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 7c95bb14cf..2622c51432 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -198,22 +198,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): aov = vl.aovs[cp_name] aov.type = cp[1].get("type", "VALUE") - def _create_context(self): - context = bpy.context.copy() - - win = bpy.context.window_manager.windows[0] - screen = win.screen - area = screen.areas[0] - region = area.regions[0] - - context["window"] = win - context['screen'] = screen - context['area'] = area - context['region'] = region - - return context - - def set_node_tree(self, output_path, instance, aov_sep): + def set_node_tree(self, output_path, instance, aov_sep, ext, multilayer): # Set the scene to use the compositor node tree to render bpy.context.scene.use_nodes = True @@ -248,17 +233,10 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # Create a new output node output = tree.nodes.new("CompositorNodeOutputFile") - context = self._create_context() - context["node"] = output - - area = context["area"] - - # Change area type to node editor, to execute node operators - old_area_type = area.ui_type - area.ui_type = "CompositorNodeTree" - - # Remove the default input socket from the output node - bpy.ops.node.output_file_remove_active_socket(context) + if ext == "exr" and multilayer: + output.layer_slots.clear() + else: + output.file_slots.clear() output.base_path = output_path image_settings = bpy.context.scene.render.image_settings @@ -270,18 +248,18 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): # and link it for render_pass in passes: filepath = f"{instance.name}{aov_sep}{render_pass.name}.####" - bpy.ops.node.output_file_add_socket(context, file_path=filepath) + if ext == "exr" and multilayer: + output.layer_slots.new(render_pass.name) + else: + output.file_slots.new(filepath) - aov_file_products.append( - (render_pass.name, os.path.join(output_path, filepath))) + aov_file_products.append( + (render_pass.name, os.path.join(output_path, filepath))) node_input = output.inputs[-1] tree.links.new(render_pass, node_input) - # Restore the area type - area.ui_type = old_area_type - return aov_file_products @staticmethod @@ -317,7 +295,8 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): output_path = os.path.join(file_path, render_folder, file_name) render_product = self.get_render_product(output_path, instance) - aov_file_product = self.set_node_tree(output_path, instance, aov_sep) + aov_file_product = self.set_node_tree( + output_path, instance, aov_sep, ext, multilayer) # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product From c529abb8ab383ff50e92b8b0284ff2efa2fbff63 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 15:39:31 +0100 Subject: [PATCH 14/37] Moved most of the logic in the creator --- .../blender/plugins/create/create_render.py | 267 +++++++++++++++++- .../blender/plugins/publish/collect_render.py | 235 +-------------- 2 files changed, 266 insertions(+), 236 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 8323b88cfe..49d356ab67 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -1,9 +1,18 @@ """Create render.""" +import os +import re import bpy +from openpype.pipeline import ( + get_current_context, + get_current_project_name, +) +from openpype.settings import ( + get_project_settings, +) from openpype.pipeline import get_current_task_name -from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api import plugin, lib from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -15,14 +24,217 @@ class CreateRenderlayer(plugin.Creator): family = "renderlayer" icon = "eye" - render_settings = {} + @staticmethod + def get_default_render_folder(settings): + """Get default render folder from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["default_render_image_folder"]) + + @staticmethod + def get_aov_separator(settings): + """Get aov separator from blender settings.""" + + aov_sep = (settings["blender"] + ["RenderSettings"] + ["aov_separator"]) + + if aov_sep == "dash": + return "-" + elif aov_sep == "underscore": + return "_" + elif aov_sep == "dot": + return "." + else: + raise ValueError(f"Invalid aov separator: {aov_sep}") + + @staticmethod + def get_image_format(settings): + """Get image format from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["image_format"]) + + @staticmethod + def get_multilayer(settings): + """Get multilayer from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["multilayer_exr"]) + + @staticmethod + def get_render_product(output_path, name): + """ + Generate the path to the render product. Blender interprets the `#` + as the frame number, when it renders. + + Args: + file_path (str): The path to the blender scene. + render_folder (str): The render folder set in settings. + file_name (str): The name of the blender scene. + instance (pyblish.api.Instance): The instance to publish. + ext (str): The image format to render. + """ + output_file = os.path.join(output_path, name) + + render_product = f"{output_file}.####" + render_product = render_product.replace("\\", "/") + + return render_product + + @staticmethod + def set_render_format(ext, multilayer): + # Set Blender to save the file with the right extension + bpy.context.scene.render.use_file_extension = True + + image_settings = bpy.context.scene.render.image_settings + + if ext == "exr": + image_settings.file_format = ( + "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR") + elif ext == "bmp": + image_settings.file_format = "BMP" + elif ext == "rgb": + image_settings.file_format = "IRIS" + elif ext == "png": + image_settings.file_format = "PNG" + elif ext == "jpeg": + image_settings.file_format = "JPEG" + elif ext == "jp2": + image_settings.file_format = "JPEG2000" + elif ext == "tga": + image_settings.file_format = "TARGA" + elif ext == "tif": + image_settings.file_format = "TIFF" + + @staticmethod + def set_render_passes(settings): + aov_list = (settings["blender"] + ["RenderSettings"] + ["aov_list"]) + + custom_passes = (settings["blender"] + ["RenderSettings"] + ["custom_passes"]) + + vl = bpy.context.view_layer + + vl.use_pass_combined = "combined" in aov_list + vl.use_pass_z = "z" in aov_list + vl.use_pass_mist = "mist" in aov_list + vl.use_pass_normal = "normal" in aov_list + vl.use_pass_diffuse_direct = "diffuse_light" in aov_list + vl.use_pass_diffuse_color = "diffuse_color" in aov_list + vl.use_pass_glossy_direct = "specular_light" in aov_list + vl.use_pass_glossy_color = "specular_color" in aov_list + vl.eevee.use_pass_volume_direct = "volume_light" in aov_list + vl.use_pass_emit = "emission" in aov_list + vl.use_pass_environment = "environment" in aov_list + vl.use_pass_shadow = "shadow" in aov_list + vl.use_pass_ambient_occlusion = "ao" in aov_list + + aovs_names = [aov.name for aov in vl.aovs] + for cp in custom_passes: + cp_name = cp[0] + if cp_name not in aovs_names: + aov = vl.aovs.add() + aov.name = cp_name + else: + aov = vl.aovs[cp_name] + aov.type = cp[1].get("type", "VALUE") + + return aov_list, custom_passes + + def set_node_tree(self, output_path, name, aov_sep, ext, multilayer): + # Set the scene to use the compositor node tree to render + bpy.context.scene.use_nodes = True + + tree = bpy.context.scene.node_tree + + # Get the Render Layers node + rl_node = None + for node in tree.nodes: + if node.bl_idname == "CompositorNodeRLayers": + rl_node = node + break + + # If there's not a Render Layers node, we create it + if not rl_node: + rl_node = tree.nodes.new("CompositorNodeRLayers") + + # Get the enabled output sockets, that are the active passes for the + # render. + # We also exclude some layers. + exclude_sockets = ["Image", "Alpha"] + passes = [ + socket + for socket in rl_node.outputs + if socket.enabled and socket.name not in exclude_sockets + ] + + # Remove all output nodes + for node in tree.nodes: + if node.bl_idname == "CompositorNodeOutputFile": + tree.nodes.remove(node) + + # Create a new output node + output = tree.nodes.new("CompositorNodeOutputFile") + + if ext == "exr" and multilayer: + output.layer_slots.clear() + else: + output.file_slots.clear() + + output.base_path = output_path + image_settings = bpy.context.scene.render.image_settings + output.format.file_format = image_settings.file_format + + aov_file_products = [] + + # For each active render pass, we add a new socket to the output node + # and link it + for render_pass in passes: + filepath = f"{name}{aov_sep}{render_pass.name}.####" + if ext == "exr" and multilayer: + output.layer_slots.new(render_pass.name) + else: + output.file_slots.new(filepath) + + aov_file_products.append( + (render_pass.name, os.path.join(output_path, filepath))) + + node_input = output.inputs[-1] + + tree.links.new(render_pass, node_input) + + return aov_file_products + + @staticmethod + def set_render_camera(asset_group): + # There should be only one camera in the instance + found = False + for obj in asset_group.all_objects: + if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA": + bpy.context.scene.camera = obj + found = True + break + + assert found, "No camera found in the render instance" + + @staticmethod + def imprint_render_settings(node, data): + RENDER_DATA = "render_data" + if not node.get(RENDER_DATA): + node[RENDER_DATA] = {} + for key, value in data.items(): + if value is None: + continue + node[RENDER_DATA][key] = value def process(self): - """ Run the creator on Blender main thread""" - mti = ops.MainThreadItem(self._process) - ops.execute_in_main_thread(mti) - - def _process(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: @@ -46,4 +258,45 @@ class CreateRenderlayer(plugin.Creator): obj = (self.options or {}).get("asset_group") asset_group.objects.link(obj) + filepath = bpy.data.filepath + assert filepath, "Workfile not saved. Please save the file first." + + file_path = os.path.dirname(filepath) + file_name = os.path.basename(filepath) + file_name, _ = os.path.splitext(file_name) + + project = get_current_project_name() + settings = get_project_settings(project) + + render_folder = self.get_default_render_folder(settings) + aov_sep = self.get_aov_separator(settings) + ext = self.get_image_format(settings) + multilayer = self.get_multilayer(settings) + + aov_list, custom_passes = self.set_render_passes(settings) + + output_path = os.path.join(file_path, render_folder, file_name) + + render_product = self.get_render_product(output_path, name) + aov_file_product = self.set_node_tree( + output_path, name, aov_sep, ext, multilayer) + + # We set the render path, the format and the camera + bpy.context.scene.render.filepath = render_product + self.set_render_format(ext, multilayer) + self.set_render_camera(asset_group) + + render_settings = { + "render_folder": render_folder, + "aov_separator": aov_sep, + "image_format": ext, + "multilayer_exr": multilayer, + "aov_list": aov_list, + "custom_passes": custom_passes, + "render_product": render_product, + "aov_file_product": aov_file_product, + } + + self.imprint_render_settings(asset_group, render_settings) + return asset_group diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 2622c51432..557a4c9066 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -6,12 +6,6 @@ import re import bpy -from openpype.pipeline import ( - get_current_project_name, -) -from openpype.settings import ( - get_project_settings, -) from openpype.hosts.blender.api import colorspace import pyblish.api @@ -25,67 +19,6 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): label = "Collect Render Layers" sync_workfile_version = False - @staticmethod - def get_default_render_folder(settings): - """Get default render folder from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["default_render_image_folder"]) - - @staticmethod - def get_aov_separator(settings): - """Get aov separator from blender settings.""" - - aov_sep = (settings["blender"] - ["RenderSettings"] - ["aov_separator"]) - - if aov_sep == "dash": - return "-" - elif aov_sep == "underscore": - return "_" - elif aov_sep == "dot": - return "." - else: - raise ValueError(f"Invalid aov separator: {aov_sep}") - - @staticmethod - def get_image_format(settings): - """Get image format from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["image_format"]) - - @staticmethod - def get_multilayer(settings): - """Get multilayer from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["multilayer_exr"]) - - @staticmethod - def get_render_product(output_path, instance): - """ - Generate the path to the render product. Blender interprets the `#` - as the frame number, when it renders. - - Args: - file_path (str): The path to the blender scene. - render_folder (str): The render folder set in settings. - file_name (str): The name of the blender scene. - instance (pyblish.api.Instance): The instance to publish. - ext (str): The image format to render. - """ - output_file = os.path.join(output_path, instance.name) - - render_product = f"{output_file}.####" - render_product = render_product.replace("\\", "/") - - return render_product - @staticmethod def generate_expected_beauty( render_product, frame_start, frame_end, frame_step, ext @@ -137,174 +70,18 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): return expected_files - @staticmethod - def set_render_format(ext, multilayer): - # Set Blender to save the file with the right extension - bpy.context.scene.render.use_file_extension = True - - image_settings = bpy.context.scene.render.image_settings - - if ext == "exr": - image_settings.file_format = ( - "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR") - elif ext == "bmp": - image_settings.file_format = "BMP" - elif ext == "rgb": - image_settings.file_format = "IRIS" - elif ext == "png": - image_settings.file_format = "PNG" - elif ext == "jpeg": - image_settings.file_format = "JPEG" - elif ext == "jp2": - image_settings.file_format = "JPEG2000" - elif ext == "tga": - image_settings.file_format = "TARGA" - elif ext == "tif": - image_settings.file_format = "TIFF" - - @staticmethod - def set_render_passes(settings): - aov_list = (settings["blender"] - ["RenderSettings"] - ["aov_list"]) - - custom_passes = (settings["blender"] - ["RenderSettings"] - ["custom_passes"]) - - vl = bpy.context.view_layer - - vl.use_pass_combined = "combined" in aov_list - vl.use_pass_z = "z" in aov_list - vl.use_pass_mist = "mist" in aov_list - vl.use_pass_normal = "normal" in aov_list - vl.use_pass_diffuse_direct = "diffuse_light" in aov_list - vl.use_pass_diffuse_color = "diffuse_color" in aov_list - vl.use_pass_glossy_direct = "specular_light" in aov_list - vl.use_pass_glossy_color = "specular_color" in aov_list - vl.eevee.use_pass_volume_direct = "volume_light" in aov_list - vl.use_pass_emit = "emission" in aov_list - vl.use_pass_environment = "environment" in aov_list - vl.use_pass_shadow = "shadow" in aov_list - vl.use_pass_ambient_occlusion = "ao" in aov_list - - aovs_names = [aov.name for aov in vl.aovs] - for cp in custom_passes: - cp_name = cp[0] - if cp_name not in aovs_names: - aov = vl.aovs.add() - aov.name = cp_name - else: - aov = vl.aovs[cp_name] - aov.type = cp[1].get("type", "VALUE") - - def set_node_tree(self, output_path, instance, aov_sep, ext, multilayer): - # Set the scene to use the compositor node tree to render - bpy.context.scene.use_nodes = True - - tree = bpy.context.scene.node_tree - - # Get the Render Layers node - rl_node = None - for node in tree.nodes: - if node.bl_idname == "CompositorNodeRLayers": - rl_node = node - break - - # If there's not a Render Layers node, we create it - if not rl_node: - rl_node = tree.nodes.new("CompositorNodeRLayers") - - # Get the enabled output sockets, that are the active passes for the - # render. - # We also exclude some layers. - exclude_sockets = ["Image", "Alpha"] - passes = [ - socket - for socket in rl_node.outputs - if socket.enabled and socket.name not in exclude_sockets - ] - - # Remove all output nodes - for node in tree.nodes: - if node.bl_idname == "CompositorNodeOutputFile": - tree.nodes.remove(node) - - # Create a new output node - output = tree.nodes.new("CompositorNodeOutputFile") - - if ext == "exr" and multilayer: - output.layer_slots.clear() - else: - output.file_slots.clear() - - output.base_path = output_path - image_settings = bpy.context.scene.render.image_settings - output.format.file_format = image_settings.file_format - - aov_file_products = [] - - # For each active render pass, we add a new socket to the output node - # and link it - for render_pass in passes: - filepath = f"{instance.name}{aov_sep}{render_pass.name}.####" - if ext == "exr" and multilayer: - output.layer_slots.new(render_pass.name) - else: - output.file_slots.new(filepath) - - aov_file_products.append( - (render_pass.name, os.path.join(output_path, filepath))) - - node_input = output.inputs[-1] - - tree.links.new(render_pass, node_input) - - return aov_file_products - - @staticmethod - def set_render_camera(instance): - # There should be only one camera in the instance - found = False - for obj in instance: - if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA": - bpy.context.scene.camera = obj - found = True - break - - assert found, "No camera found in the render instance" - def process(self, instance): context = instance.context - filepath = context.data["currentFile"].replace("\\", "/") - file_path = os.path.dirname(filepath) - file_name = os.path.basename(filepath) - file_name, _ = os.path.splitext(file_name) + render_data = bpy.data.collections[str(instance)].get("render_data") - project = get_current_project_name() - settings = get_project_settings(project) + assert render_data, "No render data found." - render_folder = self.get_default_render_folder(settings) - aov_sep = self.get_aov_separator(settings) - ext = self.get_image_format(settings) - multilayer = self.get_multilayer(settings) + self.log.info(f"render_data: {dict(render_data)}") - self.set_render_passes(settings) - - output_path = os.path.join(file_path, render_folder, file_name) - - render_product = self.get_render_product(output_path, instance) - aov_file_product = self.set_node_tree( - output_path, instance, aov_sep, ext, multilayer) - - # We set the render path, the format and the camera - bpy.context.scene.render.filepath = render_product - self.set_render_format(ext, multilayer) - self.set_render_camera(instance) - - # We save the file to save the render settings - bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) + render_product = render_data.get("render_product") + aov_file_product = render_data.get("aov_file_product") + ext = render_data.get("image_format") frame_start = context.data["frameStart"] frame_end = context.data["frameEnd"] From 259255e7df5bfa6511c859d064aaea0d420b2f88 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 15:40:28 +0100 Subject: [PATCH 15/37] Hound fixes --- openpype/hosts/blender/plugins/create/create_render.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 49d356ab67..53a84ab0b8 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -1,11 +1,9 @@ """Create render.""" import os -import re import bpy from openpype.pipeline import ( - get_current_context, get_current_project_name, ) from openpype.settings import ( From 089909192026a82fde87ebe80e54871c712e32af Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 16:33:02 +0100 Subject: [PATCH 16/37] Fix EXR multilayer output --- openpype/hosts/blender/plugins/create/create_render.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 53a84ab0b8..468e4024e9 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -183,10 +183,12 @@ class CreateRenderlayer(plugin.Creator): if ext == "exr" and multilayer: output.layer_slots.clear() + filepath = f"{name}{aov_sep}AOVs.####" + output.base_path = os.path.join(output_path, filepath) else: output.file_slots.clear() + output.base_path = output_path - output.base_path = output_path image_settings = bpy.context.scene.render.image_settings output.format.file_format = image_settings.file_format @@ -195,10 +197,11 @@ class CreateRenderlayer(plugin.Creator): # For each active render pass, we add a new socket to the output node # and link it for render_pass in passes: - filepath = f"{name}{aov_sep}{render_pass.name}.####" if ext == "exr" and multilayer: output.layer_slots.new(render_pass.name) else: + filepath = f"{name}{aov_sep}{render_pass.name}.####" + output.file_slots.new(filepath) aov_file_products.append( From 028d15fc1efadd93fc963eae52177efa12d66d49 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 23 Aug 2023 16:55:31 +0100 Subject: [PATCH 17/37] Added validator to check if file is saved --- .../plugins/publish/validate_file_saved.py | 20 +++++++++++ .../defaults/project_settings/blender.json | 6 ++++ .../schemas/schema_blender_publish.json | 33 +++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 openpype/hosts/blender/plugins/publish/validate_file_saved.py diff --git a/openpype/hosts/blender/plugins/publish/validate_file_saved.py b/openpype/hosts/blender/plugins/publish/validate_file_saved.py new file mode 100644 index 0000000000..e191585c55 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/validate_file_saved.py @@ -0,0 +1,20 @@ +import bpy + +import pyblish.api + + +class ValidateFileSaved(pyblish.api.InstancePlugin): + """Validate that the workfile has been saved.""" + + order = pyblish.api.ValidatorOrder - 0.01 + hosts = ["blender"] + label = "Validate File Saved" + optional = False + exclude_families = [] + + def process(self, instance): + if [ef for ef in self.exclude_families + if instance.data["family"] in ef]: + return + if bpy.data.is_dirty: + raise RuntimeError("Workfile is not saved.") diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 8b1d602df0..09ed800ac8 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -35,6 +35,12 @@ "optional": true, "active": true }, + "ValidateFileSaved": { + "enabled": true, + "optional": false, + "active": true, + "exclude_families": [] + }, "ValidateMeshHasUvs": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index 2f0bf0a831..0b694e5b70 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -18,6 +18,39 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateFileSaved", + "label": "Validate File Saved", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "splitter" + }, + { + "key": "exclude_families", + "label": "Exclude Families", + "type": "list", + "object_type": "text" + } + ] + }, { "type": "collapsible-wrap", "label": "Model", From a0c25edab9e3f209486a53b8caa99b658366f9c7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 24 Aug 2023 11:06:37 +0100 Subject: [PATCH 18/37] Add check to remove instance if any error is triggered during creation --- .../blender/plugins/create/create_render.py | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 468e4024e9..63093b539c 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -235,30 +235,7 @@ class CreateRenderlayer(plugin.Creator): continue node[RENDER_DATA][key] = value - def process(self): - # Get Instance Container or create it if it does not exist - instances = bpy.data.collections.get(AVALON_INSTANCES) - if not instances: - instances = bpy.data.collections.new(name=AVALON_INSTANCES) - bpy.context.scene.collection.children.link(instances) - - # Create instance object - asset = self.data["asset"] - subset = self.data["subset"] - name = plugin.asset_name(asset, subset) - asset_group = bpy.data.collections.new(name=name) - instances.children.link(asset_group) - self.data['task'] = get_current_task_name() - lib.imprint(asset_group, self.data) - - if (self.options or {}).get("useSelection"): - selected = lib.get_selection() - for obj in selected: - asset_group.objects.link(obj) - elif (self.options or {}).get("asset_group"): - obj = (self.options or {}).get("asset_group") - asset_group.objects.link(obj) - + def prepare_rendering(self, asset_group, name): filepath = bpy.data.filepath assert filepath, "Workfile not saved. Please save the file first." @@ -300,4 +277,28 @@ class CreateRenderlayer(plugin.Creator): self.imprint_render_settings(asset_group, render_settings) + def process(self): + # Get Instance Container or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + + # Create instance object + asset = self.data["asset"] + subset = self.data["subset"] + name = plugin.asset_name(asset, subset) + asset_group = bpy.data.collections.new(name=name) + + try: + instances.children.link(asset_group) + self.data['task'] = get_current_task_name() + lib.imprint(asset_group, self.data) + + self.prepare_rendering(asset_group, name) + except Exception: + # Remove the instance if there was an error + bpy.data.collections.remove(asset_group) + raise + return asset_group From 153a2999011f27901dd05a8953cd91ea82b649c1 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 24 Aug 2023 11:37:52 +0100 Subject: [PATCH 19/37] Removed camera from render instance and added validator to check camera --- .../blender/plugins/create/create_render.py | 13 ---------- .../publish/validate_render_camera_is_set.py | 17 +++++++++++++ .../defaults/project_settings/blender.json | 5 ++++ .../schemas/schema_blender_publish.json | 24 +++++++++++++++++++ 4 files changed, 46 insertions(+), 13 deletions(-) create mode 100644 openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 63093b539c..2952baafd3 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -213,18 +213,6 @@ class CreateRenderlayer(plugin.Creator): return aov_file_products - @staticmethod - def set_render_camera(asset_group): - # There should be only one camera in the instance - found = False - for obj in asset_group.all_objects: - if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA": - bpy.context.scene.camera = obj - found = True - break - - assert found, "No camera found in the render instance" - @staticmethod def imprint_render_settings(node, data): RENDER_DATA = "render_data" @@ -262,7 +250,6 @@ class CreateRenderlayer(plugin.Creator): # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product self.set_render_format(ext, multilayer) - self.set_render_camera(asset_group) render_settings = { "render_folder": render_folder, diff --git a/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py new file mode 100644 index 0000000000..5a06c1ff0a --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py @@ -0,0 +1,17 @@ +import bpy + +import pyblish.api + + +class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin): + """Validate that there is a camera set as active for rendering.""" + + order = pyblish.api.ValidatorOrder + hosts = ["blender"] + families = ["renderlayer"] + label = "Validate Render Camera Is Set" + optional = False + + def process(self, instance): + if not bpy.context.scene.camera: + raise RuntimeError("No camera is active for rendering.") diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 09ed800ac8..9cbbb49593 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -41,6 +41,11 @@ "active": true, "exclude_families": [] }, + "ValidateRenderCameraIsSet": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateMeshHasUvs": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index 0b694e5b70..05e7f13e70 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -51,6 +51,30 @@ } ] }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateRenderCameraIsSet", + "label": "Validate Render Camera Is Set", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + }, { "type": "collapsible-wrap", "label": "Model", From 0f1bf31f69bfb0632eba5f8994e84c6f72d52126 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 24 Aug 2023 15:33:16 +0100 Subject: [PATCH 20/37] Updated settings for Ayon --- server_addon/blender/server/settings/main.py | 7 ++ .../server/settings/publish_plugins.py | 31 +++++ .../server/settings/render_settings.py | 106 ++++++++++++++++++ server_addon/blender/server/version.py | 2 +- .../server/settings/publish_plugins.py | 25 ++++- 5 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 server_addon/blender/server/settings/render_settings.py diff --git a/server_addon/blender/server/settings/main.py b/server_addon/blender/server/settings/main.py index f6118d39cd..4476ea709b 100644 --- a/server_addon/blender/server/settings/main.py +++ b/server_addon/blender/server/settings/main.py @@ -9,6 +9,10 @@ from .publish_plugins import ( PublishPuginsModel, DEFAULT_BLENDER_PUBLISH_SETTINGS ) +from .render_settings import ( + RenderSettingsModel, + DEFAULT_RENDER_SETTINGS +) class UnitScaleSettingsModel(BaseSettingsModel): @@ -37,6 +41,8 @@ class BlenderSettings(BaseSettingsModel): default_factory=BlenderImageIOModel, title="Color Management (ImageIO)" ) + render_settings: RenderSettingsModel = Field( + default_factory=RenderSettingsModel, title="Render Settings") workfile_builder: TemplateWorkfileBaseOptions = Field( default_factory=TemplateWorkfileBaseOptions, title="Workfile Builder" @@ -55,6 +61,7 @@ DEFAULT_VALUES = { }, "set_frames_startup": True, "set_resolution_startup": True, + "render_settings": DEFAULT_RENDER_SETTINGS, "publish": DEFAULT_BLENDER_PUBLISH_SETTINGS, "workfile_builder": { "create_first_version": False, diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 65dda78411..575bfe9f39 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -26,6 +26,16 @@ class ValidatePluginModel(BaseSettingsModel): active: bool = Field(title="Active") +class ValidateFileSavedModel(BaseSettingsModel): + enabled: bool = Field(title="ValidateFileSaved") + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + exclude_families: list[str] = Field( + default_factory=list, + title="Exclude product types" + ) + + class ExtractBlendModel(BaseSettingsModel): enabled: bool = Field(True) optional: bool = Field(title="Optional") @@ -53,6 +63,16 @@ class PublishPuginsModel(BaseSettingsModel): title="Validate Camera Zero Keyframe", section="Validators" ) + ValidateFileSaved: ValidateFileSavedModel = Field( + default_factory=ValidateFileSavedModel, + title="Validate File Saved", + section="Validators" + ) + ValidateRenderCameraIsSet: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Render Camera Is Set", + section="Validators" + ) ValidateMeshHasUvs: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Validate Mesh Has Uvs" @@ -118,6 +138,17 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": True, "active": True }, + "ValidateFileSaved": { + "enabled": True, + "optional": False, + "active": True, + "exclude_families": [] + }, + "ValidateRenderCameraIsSet": { + "enabled": True, + "optional": False, + "active": True + }, "ValidateMeshHasUvs": { "enabled": True, "optional": True, diff --git a/server_addon/blender/server/settings/render_settings.py b/server_addon/blender/server/settings/render_settings.py new file mode 100644 index 0000000000..bef16328d6 --- /dev/null +++ b/server_addon/blender/server/settings/render_settings.py @@ -0,0 +1,106 @@ +"""Providing models and values for Blender Render Settings.""" +from pydantic import Field + +from ayon_server.settings import BaseSettingsModel + + +def aov_separators_enum(): + return [ + {"value": "dash", "label": "- (dash)"}, + {"value": "underscore", "label": "_ (underscore)"}, + {"value": "dot", "label": ". (dot)"} + ] + + +def image_format_enum(): + return [ + {"value": "exr", "label": "OpenEXR"}, + {"value": "bmp", "label": "BMP"}, + {"value": "rgb", "label": "Iris"}, + {"value": "png", "label": "PNG"}, + {"value": "jpg", "label": "JPEG"}, + {"value": "jp2", "label": "JPEG 2000"}, + {"value": "tga", "label": "Targa"}, + {"value": "tif", "label": "TIFF"}, + ] + + +def aov_list_enum(): + return [ + {"value": "empty", "label": "< none >"}, + {"value": "combined", "label": "Combined"}, + {"value": "z", "label": "Z"}, + {"value": "mist", "label": "Mist"}, + {"value": "normal", "label": "Normal"}, + {"value": "diffuse_light", "label": "Diffuse Light"}, + {"value": "diffuse_color", "label": "Diffuse Color"}, + {"value": "specular_light", "label": "Specular Light"}, + {"value": "specular_color", "label": "Specular Color"}, + {"value": "volume_light", "label": "Volume Light"}, + {"value": "emission", "label": "Emission"}, + {"value": "environment", "label": "Environment"}, + {"value": "shadow", "label": "Shadow"}, + {"value": "ao", "label": "Ambient Occlusion"} + ] + + +def custom_passes_types_enum(): + return [ + {"value": "COLOR", "label": "Color"}, + {"value": "VALUE", "label": "Value"}, + ] + + +class CustomPassesModel(BaseSettingsModel): + """Custom Passes""" + _layout = "compact" + + attribute: str = Field("", title="Attribute name") + value: str = Field( + "Color", + title="Type", + enum_resolver=custom_passes_types_enum + ) + + +class RenderSettingsModel(BaseSettingsModel): + default_render_image_folder: str = Field( + title="Default Render Image Folder" + ) + aov_separator: str = Field( + "underscore", + title="AOV Separator Character", + enum_resolver=aov_separators_enum + ) + image_format: str = Field( + "exr", + title="Image Format", + enum_resolver=image_format_enum + ) + multilayer_exr: bool = Field( + title="Multilayer (EXR)" + ) + aov_list: list[str] = Field( + default_factory=list, + enum_resolver=aov_list_enum, + title="AOVs to create" + ) + custom_passes: list[CustomPassesModel] = Field( + default_factory=list, + title="Custom Passes", + description=( + "Add custom AOVs. They are added to the view layer and in the " + "Compositing Nodetree,\nbut they need to be added manually to " + "the Shader Nodetree." + ) + ) + + +DEFAULT_RENDER_SETTINGS = { + "default_render_image_folder": "renders/blender", + "aov_separator": "underscore", + "image_format": "exr", + "multilayer_exr": True, + "aov_list": [], + "custom_passes": [] +} diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index 485f44ac21..b3f4756216 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index 8d1b667345..a29caa7ba1 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -208,6 +208,16 @@ class CelactionSubmitDeadlineModel(BaseSettingsModel): ) +class BlenderSubmitDeadlineModel(BaseSettingsModel): + enabled: bool = Field(True) + optional: bool = Field(title="Optional") + active: bool = Field(title="Active") + use_published: bool = Field(title="Use Published scene") + priority: int = Field(title="Priority") + chunk_size: int = Field(title="Frame per Task") + group: str = Field("", title="Group Name") + + class AOVFilterSubmodel(BaseSettingsModel): _layout = "expanded" name: str = Field(title="Host") @@ -276,8 +286,10 @@ class PublishPluginsModel(BaseSettingsModel): title="After Effects to deadline") CelactionSubmitDeadline: CelactionSubmitDeadlineModel = Field( default_factory=CelactionSubmitDeadlineModel, - title="Celaction Submit Deadline" - ) + title="Celaction Submit Deadline") + BlenderSubmitDeadline: BlenderSubmitDeadlineModel = Field( + default_factory=BlenderSubmitDeadlineModel, + title="Blender Submit Deadline") ProcessSubmittedJobOnFarm: ProcessSubmittedJobOnFarmModel = Field( default_factory=ProcessSubmittedJobOnFarmModel, title="Process submitted job on farm.") @@ -384,6 +396,15 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = { "deadline_chunk_size": 10, "deadline_job_delay": "00:00:00:00" }, + "BlenderSubmitDeadline": { + "enabled": True, + "optional": False, + "active": True, + "use_published": True, + "priority": 50, + "chunk_size": 10, + "group": "none" + }, "ProcessSubmittedJobOnFarm": { "enabled": True, "deadline_department": "", From 1ee944d03c07172d5dd6919eab7b4a974ab52369 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 24 Aug 2023 15:33:53 +0100 Subject: [PATCH 21/37] Increase workfile version after render publish --- .../blender/plugins/publish/increment_workfile_version.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index 27fa4baf28..5f49ad7185 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -9,7 +9,8 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin): label = "Increment Workfile Version" optional = True hosts = ["blender"] - families = ["animation", "model", "rig", "action", "layout", "blendScene"] + families = ["animation", "model", "rig", "action", "layout", "blendScene", + "renderlayer"] def process(self, context): From 481e814858e9c90e4b8308707d341387cb151fd9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 24 Aug 2023 16:39:50 +0100 Subject: [PATCH 22/37] Save the workfile after creating the render instance Blender, by design, doesn't set the file as dirty if modifications happen by script. So, when creating the instance and setting the render settings, the file is not marked as dirty. This means that there is the risk of sending to deadline a file without the right settings. Even the validator to check that the file is saved will detect the file as saved, even if it isn't. The only solution for now it is to force the file to be saved. --- .../hosts/blender/plugins/create/create_render.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 2952baafd3..62700cb55c 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -288,4 +288,15 @@ class CreateRenderlayer(plugin.Creator): bpy.data.collections.remove(asset_group) raise + # TODO: this is undesiderable, but it's the only way to be sure that + # the file is saved before the render starts. + # Blender, by design, doesn't set the file as dirty if modifications + # happen by script. So, when creating the instance and setting the + # render settings, the file is not marked as dirty. This means that + # there is the risk of sending to deadline a file without the right + # settings. Even the validator to check that the file is saved will + # detect the file as saved, even if it isn't. The only solution for + # now it is to force the file to be saved. + bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) + return asset_group From 4b27abfae2320e3324cccea63b82a2690097fab5 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 11 Sep 2023 15:17:17 +0100 Subject: [PATCH 23/37] Implemented several suggestions from reviews Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Co-authored-by: Oscar Domingo Co-authored-by: Roy Nieterau --- openpype/hosts/blender/api/colorspace.py | 8 ++++---- openpype/hosts/blender/plugins/create/create_render.py | 10 +++------- .../projects_schema/schema_project_blender.json | 2 +- .../blender/server/settings/render_settings.py | 2 +- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/blender/api/colorspace.py b/openpype/hosts/blender/api/colorspace.py index 59deb514f8..0f504a3be0 100644 --- a/openpype/hosts/blender/api/colorspace.py +++ b/openpype/hosts/blender/api/colorspace.py @@ -12,12 +12,12 @@ class LayerMetadata(object): @attr.s class RenderProduct(object): - """Getting Colorspace as - Specific Render Product Parameter for submitting + """ + Getting Colorspace as Specific Render Product Parameter for submitting publish job. """ - colorspace = attr.ib() # colorspace - view = attr.ib() + colorspace = attr.ib() # colorspace + view = attr.ib() # OCIO view transform productName = attr.ib(default=None) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 62700cb55c..fa3cae6cc8 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -3,13 +3,11 @@ import os import bpy +from openpype.settings import get_project_settings from openpype.pipeline import ( get_current_project_name, + get_current_task_name, ) -from openpype.settings import ( - get_project_settings, -) -from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -76,9 +74,7 @@ class CreateRenderlayer(plugin.Creator): instance (pyblish.api.Instance): The instance to publish. ext (str): The image format to render. """ - output_file = os.path.join(output_path, name) - - render_product = f"{output_file}.####" + render_product = f"{os.path.join(output_path, name)}.####" render_product = render_product.replace("\\", "/") return render_product diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 8db57f49eb..a283a2ff5c 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -140,7 +140,7 @@ "label": "Type", "type": "enum", "multiselection": false, - "defaults": "color", + "default": "COLOR", "enum_items": [ {"COLOR": "Color"}, {"VALUE": "Value"} diff --git a/server_addon/blender/server/settings/render_settings.py b/server_addon/blender/server/settings/render_settings.py index bef16328d6..7a47095d3c 100644 --- a/server_addon/blender/server/settings/render_settings.py +++ b/server_addon/blender/server/settings/render_settings.py @@ -57,7 +57,7 @@ class CustomPassesModel(BaseSettingsModel): attribute: str = Field("", title="Attribute name") value: str = Field( - "Color", + "COLOR", title="Type", enum_resolver=custom_passes_types_enum ) From 01282f3af797d2c3f879bd91de692965eb25ebdb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 12 Sep 2023 11:47:48 +0100 Subject: [PATCH 24/37] Fix AOVs publish with multilayer EXR --- openpype/hosts/blender/plugins/create/create_render.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index fa3cae6cc8..84387ffb16 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -177,10 +177,15 @@ class CreateRenderlayer(plugin.Creator): # Create a new output node output = tree.nodes.new("CompositorNodeOutputFile") + aov_file_products = [] + if ext == "exr" and multilayer: output.layer_slots.clear() filepath = f"{name}{aov_sep}AOVs.####" output.base_path = os.path.join(output_path, filepath) + + aov_file_products.append( + ("AOVs", os.path.join(output_path, filepath))) else: output.file_slots.clear() output.base_path = output_path @@ -188,8 +193,6 @@ class CreateRenderlayer(plugin.Creator): image_settings = bpy.context.scene.render.image_settings output.format.file_format = image_settings.file_format - aov_file_products = [] - # For each active render pass, we add a new socket to the output node # and link it for render_pass in passes: From d949041ad9013e4aa4d02fb0f8e4cd6e540019ed Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 12 Sep 2023 15:10:04 +0100 Subject: [PATCH 25/37] Change behaviour for multilayer exr --- .../blender/plugins/create/create_render.py | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 84387ffb16..1c7f883836 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -177,34 +177,29 @@ class CreateRenderlayer(plugin.Creator): # Create a new output node output = tree.nodes.new("CompositorNodeOutputFile") - aov_file_products = [] - - if ext == "exr" and multilayer: - output.layer_slots.clear() - filepath = f"{name}{aov_sep}AOVs.####" - output.base_path = os.path.join(output_path, filepath) - - aov_file_products.append( - ("AOVs", os.path.join(output_path, filepath))) - else: - output.file_slots.clear() - output.base_path = output_path - image_settings = bpy.context.scene.render.image_settings output.format.file_format = image_settings.file_format + # In case of a multilayer exr, we don't need to use the output node, + # because the blender render already outputs a multilayer exr. + if ext == "exr" and multilayer: + output.layer_slots.clear() + return [] + + output.file_slots.clear() + output.base_path = output_path + + aov_file_products = [] + # For each active render pass, we add a new socket to the output node # and link it for render_pass in passes: - if ext == "exr" and multilayer: - output.layer_slots.new(render_pass.name) - else: - filepath = f"{name}{aov_sep}{render_pass.name}.####" + filepath = f"{name}{aov_sep}{render_pass.name}.####" - output.file_slots.new(filepath) + output.file_slots.new(filepath) - aov_file_products.append( - (render_pass.name, os.path.join(output_path, filepath))) + aov_file_products.append( + (render_pass.name, os.path.join(output_path, filepath))) node_input = output.inputs[-1] From 96638726a90896673457dccc91f5bec5fd069ae9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 13:09:32 +0100 Subject: [PATCH 26/37] Produce reviews for the beauty render when publishing --- .../hosts/blender/plugins/create/create_render.py | 11 ++++++----- .../hosts/blender/plugins/publish/collect_render.py | 3 +++ .../deadline/plugins/publish/submit_publish_job.py | 1 + .../settings/defaults/project_settings/deadline.json | 3 +++ .../deadline/server/settings/publish_plugins.py | 6 ++++++ 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 1c7f883836..abb04061af 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -62,7 +62,7 @@ class CreateRenderlayer(plugin.Creator): ["multilayer_exr"]) @staticmethod - def get_render_product(output_path, name): + def get_render_product(output_path, name, aov_sep): """ Generate the path to the render product. Blender interprets the `#` as the frame number, when it renders. @@ -74,7 +74,8 @@ class CreateRenderlayer(plugin.Creator): instance (pyblish.api.Instance): The instance to publish. ext (str): The image format to render. """ - render_product = f"{os.path.join(output_path, name)}.####" + filepath = os.path.join(output_path, name) + render_product = f"{filepath}{aov_sep}beauty.####" render_product = render_product.replace("\\", "/") return render_product @@ -233,17 +234,16 @@ class CreateRenderlayer(plugin.Creator): ext = self.get_image_format(settings) multilayer = self.get_multilayer(settings) + self.set_render_format(ext, multilayer) aov_list, custom_passes = self.set_render_passes(settings) output_path = os.path.join(file_path, render_folder, file_name) - render_product = self.get_render_product(output_path, name) + render_product = self.get_render_product(output_path, name, aov_sep) aov_file_product = self.set_node_tree( output_path, name, aov_sep, ext, multilayer) - # We set the render path, the format and the camera bpy.context.scene.render.filepath = render_product - self.set_render_format(ext, multilayer) render_settings = { "render_folder": render_folder, @@ -254,6 +254,7 @@ class CreateRenderlayer(plugin.Creator): "custom_passes": custom_passes, "render_product": render_product, "aov_file_product": aov_file_product, + "review": True, } self.imprint_render_settings(asset_group, render_settings) diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index 557a4c9066..e0fc933241 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -82,6 +82,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): render_product = render_data.get("render_product") aov_file_product = render_data.get("aov_file_product") ext = render_data.get("image_format") + multilayer = render_data.get("multilayer_exr") frame_start = context.data["frameStart"] frame_end = context.data["frameEnd"] @@ -105,6 +106,8 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): "frameEndHandle": frame_handle_end, "fps": context.data["fps"], "byFrameStep": bpy.context.scene.frame_step, + "review": render_data.get("review", False), + "multipartExr": ext == "exr" and multilayer, "farm": True, "expectedFiles": [expected_files], # OCIO not currently implemented in Blender, but the following diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index 609bfc3d3b..903b6e42e7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -107,6 +107,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, "redshift_rop"] aov_filter = {"maya": [r".*([Bb]eauty).*"], + "blender": [r".*([Bb]eauty).*"], "aftereffects": [r".*"], # for everything from AE "harmony": [r".*"], # for everything from AE "celaction": [r".*"], diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 33ea533863..9e88f3b6f2 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -121,6 +121,9 @@ "maya": [ ".*([Bb]eauty).*" ], + "blender": [ + ".*([Bb]eauty).*" + ], "aftereffects": [ ".*" ], diff --git a/server_addon/deadline/server/settings/publish_plugins.py b/server_addon/deadline/server/settings/publish_plugins.py index a29caa7ba1..32a5d0e353 100644 --- a/server_addon/deadline/server/settings/publish_plugins.py +++ b/server_addon/deadline/server/settings/publish_plugins.py @@ -421,6 +421,12 @@ DEFAULT_DEADLINE_PLUGINS_SETTINGS = { ".*([Bb]eauty).*" ] }, + { + "name": "blender", + "value": [ + ".*([Bb]eauty).*" + ] + }, { "name": "aftereffects", "value": [ From 3c2c33bcea84ada5c9292e12fc53c51e74a73e5a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 17:06:29 +0100 Subject: [PATCH 27/37] Reorganized code and added validator to check the output folder --- openpype/hosts/blender/api/__init__.py | 3 + openpype/hosts/blender/api/render_lib.py | 240 +++++++++++++++++ .../blender/plugins/create/create_render.py | 248 +----------------- .../publish/validate_deadline_publish.py | 47 ++++ 4 files changed, 293 insertions(+), 245 deletions(-) create mode 100644 openpype/hosts/blender/api/render_lib.py create mode 100644 openpype/hosts/blender/plugins/publish/validate_deadline_publish.py diff --git a/openpype/hosts/blender/api/__init__.py b/openpype/hosts/blender/api/__init__.py index 75a11affde..e15f1193a5 100644 --- a/openpype/hosts/blender/api/__init__.py +++ b/openpype/hosts/blender/api/__init__.py @@ -38,6 +38,8 @@ from .lib import ( from .capture import capture +from .render_lib import prepare_rendering + __all__ = [ "install", @@ -66,4 +68,5 @@ __all__ = [ "get_selection", "capture", # "unique_name", + "prepare_rendering", ] diff --git a/openpype/hosts/blender/api/render_lib.py b/openpype/hosts/blender/api/render_lib.py new file mode 100644 index 0000000000..994de43503 --- /dev/null +++ b/openpype/hosts/blender/api/render_lib.py @@ -0,0 +1,240 @@ +import os + +import bpy + +from openpype.settings import get_project_settings +from openpype.pipeline import get_current_project_name + + +def get_default_render_folder(settings): + """Get default render folder from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["default_render_image_folder"]) + +def get_aov_separator(settings): + """Get aov separator from blender settings.""" + + aov_sep = (settings["blender"] + ["RenderSettings"] + ["aov_separator"]) + + if aov_sep == "dash": + return "-" + elif aov_sep == "underscore": + return "_" + elif aov_sep == "dot": + return "." + else: + raise ValueError(f"Invalid aov separator: {aov_sep}") + +def get_image_format(settings): + """Get image format from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["image_format"]) + +def get_multilayer(settings): + """Get multilayer from blender settings.""" + + return (settings["blender"] + ["RenderSettings"] + ["multilayer_exr"]) + +def get_render_product(output_path, name, aov_sep): + """ + Generate the path to the render product. Blender interprets the `#` + as the frame number, when it renders. + + Args: + file_path (str): The path to the blender scene. + render_folder (str): The render folder set in settings. + file_name (str): The name of the blender scene. + instance (pyblish.api.Instance): The instance to publish. + ext (str): The image format to render. + """ + filepath = os.path.join(output_path, name) + render_product = f"{filepath}{aov_sep}beauty.####" + render_product = render_product.replace("\\", "/") + + return render_product + +def set_render_format(ext, multilayer): + # Set Blender to save the file with the right extension + bpy.context.scene.render.use_file_extension = True + + image_settings = bpy.context.scene.render.image_settings + + if ext == "exr": + image_settings.file_format = ( + "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR") + elif ext == "bmp": + image_settings.file_format = "BMP" + elif ext == "rgb": + image_settings.file_format = "IRIS" + elif ext == "png": + image_settings.file_format = "PNG" + elif ext == "jpeg": + image_settings.file_format = "JPEG" + elif ext == "jp2": + image_settings.file_format = "JPEG2000" + elif ext == "tga": + image_settings.file_format = "TARGA" + elif ext == "tif": + image_settings.file_format = "TIFF" + +def set_render_passes(settings): + aov_list = (settings["blender"] + ["RenderSettings"] + ["aov_list"]) + + custom_passes = (settings["blender"] + ["RenderSettings"] + ["custom_passes"]) + + vl = bpy.context.view_layer + + vl.use_pass_combined = "combined" in aov_list + vl.use_pass_z = "z" in aov_list + vl.use_pass_mist = "mist" in aov_list + vl.use_pass_normal = "normal" in aov_list + vl.use_pass_diffuse_direct = "diffuse_light" in aov_list + vl.use_pass_diffuse_color = "diffuse_color" in aov_list + vl.use_pass_glossy_direct = "specular_light" in aov_list + vl.use_pass_glossy_color = "specular_color" in aov_list + vl.eevee.use_pass_volume_direct = "volume_light" in aov_list + vl.use_pass_emit = "emission" in aov_list + vl.use_pass_environment = "environment" in aov_list + vl.use_pass_shadow = "shadow" in aov_list + vl.use_pass_ambient_occlusion = "ao" in aov_list + + aovs_names = [aov.name for aov in vl.aovs] + for cp in custom_passes: + cp_name = cp[0] + if cp_name not in aovs_names: + aov = vl.aovs.add() + aov.name = cp_name + else: + aov = vl.aovs[cp_name] + aov.type = cp[1].get("type", "VALUE") + + return aov_list, custom_passes + +def set_node_tree(output_path, name, aov_sep, ext, multilayer): + # Set the scene to use the compositor node tree to render + bpy.context.scene.use_nodes = True + + tree = bpy.context.scene.node_tree + + # Get the Render Layers node + rl_node = None + for node in tree.nodes: + if node.bl_idname == "CompositorNodeRLayers": + rl_node = node + break + + # If there's not a Render Layers node, we create it + if not rl_node: + rl_node = tree.nodes.new("CompositorNodeRLayers") + + # Get the enabled output sockets, that are the active passes for the + # render. + # We also exclude some layers. + exclude_sockets = ["Image", "Alpha"] + passes = [ + socket + for socket in rl_node.outputs + if socket.enabled and socket.name not in exclude_sockets + ] + + # Remove all output nodes + for node in tree.nodes: + if node.bl_idname == "CompositorNodeOutputFile": + tree.nodes.remove(node) + + # Create a new output node + output = tree.nodes.new("CompositorNodeOutputFile") + + image_settings = bpy.context.scene.render.image_settings + output.format.file_format = image_settings.file_format + + # In case of a multilayer exr, we don't need to use the output node, + # because the blender render already outputs a multilayer exr. + if ext == "exr" and multilayer: + output.layer_slots.clear() + return [] + + output.file_slots.clear() + output.base_path = output_path + + aov_file_products = [] + + # For each active render pass, we add a new socket to the output node + # and link it + for render_pass in passes: + filepath = f"{name}{aov_sep}{render_pass.name}.####" + + output.file_slots.new(filepath) + + aov_file_products.append( + (render_pass.name, os.path.join(output_path, filepath))) + + node_input = output.inputs[-1] + + tree.links.new(render_pass, node_input) + + return aov_file_products + +def imprint_render_settings(node, data): + RENDER_DATA = "render_data" + if not node.get(RENDER_DATA): + node[RENDER_DATA] = {} + for key, value in data.items(): + if value is None: + continue + node[RENDER_DATA][key] = value + +def prepare_rendering(asset_group): + name = asset_group.name + + filepath = bpy.data.filepath + assert filepath, "Workfile not saved. Please save the file first." + + file_path = os.path.dirname(filepath) + file_name = os.path.basename(filepath) + file_name, _ = os.path.splitext(file_name) + + project = get_current_project_name() + settings = get_project_settings(project) + + render_folder = get_default_render_folder(settings) + aov_sep = get_aov_separator(settings) + ext = get_image_format(settings) + multilayer = get_multilayer(settings) + + set_render_format(ext, multilayer) + aov_list, custom_passes = set_render_passes(settings) + + output_path = os.path.join(file_path, render_folder, file_name) + + render_product = get_render_product(output_path, name, aov_sep) + aov_file_product = set_node_tree( + output_path, name, aov_sep, ext, multilayer) + + bpy.context.scene.render.filepath = render_product + + render_settings = { + "render_folder": render_folder, + "aov_separator": aov_sep, + "image_format": ext, + "multilayer_exr": multilayer, + "aov_list": aov_list, + "custom_passes": custom_passes, + "render_product": render_product, + "aov_file_product": aov_file_product, + "review": True, + } + + imprint_render_settings(asset_group, render_settings) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index abb04061af..7a91726a5f 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -3,12 +3,9 @@ import os import bpy -from openpype.settings import get_project_settings -from openpype.pipeline import ( - get_current_project_name, - get_current_task_name, -) +from openpype.pipeline import get_current_task_name from openpype.hosts.blender.api import plugin, lib +from openpype.hosts.blender.api.render_lib import prepare_rendering from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES @@ -20,245 +17,6 @@ class CreateRenderlayer(plugin.Creator): family = "renderlayer" icon = "eye" - @staticmethod - def get_default_render_folder(settings): - """Get default render folder from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["default_render_image_folder"]) - - @staticmethod - def get_aov_separator(settings): - """Get aov separator from blender settings.""" - - aov_sep = (settings["blender"] - ["RenderSettings"] - ["aov_separator"]) - - if aov_sep == "dash": - return "-" - elif aov_sep == "underscore": - return "_" - elif aov_sep == "dot": - return "." - else: - raise ValueError(f"Invalid aov separator: {aov_sep}") - - @staticmethod - def get_image_format(settings): - """Get image format from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["image_format"]) - - @staticmethod - def get_multilayer(settings): - """Get multilayer from blender settings.""" - - return (settings["blender"] - ["RenderSettings"] - ["multilayer_exr"]) - - @staticmethod - def get_render_product(output_path, name, aov_sep): - """ - Generate the path to the render product. Blender interprets the `#` - as the frame number, when it renders. - - Args: - file_path (str): The path to the blender scene. - render_folder (str): The render folder set in settings. - file_name (str): The name of the blender scene. - instance (pyblish.api.Instance): The instance to publish. - ext (str): The image format to render. - """ - filepath = os.path.join(output_path, name) - render_product = f"{filepath}{aov_sep}beauty.####" - render_product = render_product.replace("\\", "/") - - return render_product - - @staticmethod - def set_render_format(ext, multilayer): - # Set Blender to save the file with the right extension - bpy.context.scene.render.use_file_extension = True - - image_settings = bpy.context.scene.render.image_settings - - if ext == "exr": - image_settings.file_format = ( - "OPEN_EXR_MULTILAYER" if multilayer else "OPEN_EXR") - elif ext == "bmp": - image_settings.file_format = "BMP" - elif ext == "rgb": - image_settings.file_format = "IRIS" - elif ext == "png": - image_settings.file_format = "PNG" - elif ext == "jpeg": - image_settings.file_format = "JPEG" - elif ext == "jp2": - image_settings.file_format = "JPEG2000" - elif ext == "tga": - image_settings.file_format = "TARGA" - elif ext == "tif": - image_settings.file_format = "TIFF" - - @staticmethod - def set_render_passes(settings): - aov_list = (settings["blender"] - ["RenderSettings"] - ["aov_list"]) - - custom_passes = (settings["blender"] - ["RenderSettings"] - ["custom_passes"]) - - vl = bpy.context.view_layer - - vl.use_pass_combined = "combined" in aov_list - vl.use_pass_z = "z" in aov_list - vl.use_pass_mist = "mist" in aov_list - vl.use_pass_normal = "normal" in aov_list - vl.use_pass_diffuse_direct = "diffuse_light" in aov_list - vl.use_pass_diffuse_color = "diffuse_color" in aov_list - vl.use_pass_glossy_direct = "specular_light" in aov_list - vl.use_pass_glossy_color = "specular_color" in aov_list - vl.eevee.use_pass_volume_direct = "volume_light" in aov_list - vl.use_pass_emit = "emission" in aov_list - vl.use_pass_environment = "environment" in aov_list - vl.use_pass_shadow = "shadow" in aov_list - vl.use_pass_ambient_occlusion = "ao" in aov_list - - aovs_names = [aov.name for aov in vl.aovs] - for cp in custom_passes: - cp_name = cp[0] - if cp_name not in aovs_names: - aov = vl.aovs.add() - aov.name = cp_name - else: - aov = vl.aovs[cp_name] - aov.type = cp[1].get("type", "VALUE") - - return aov_list, custom_passes - - def set_node_tree(self, output_path, name, aov_sep, ext, multilayer): - # Set the scene to use the compositor node tree to render - bpy.context.scene.use_nodes = True - - tree = bpy.context.scene.node_tree - - # Get the Render Layers node - rl_node = None - for node in tree.nodes: - if node.bl_idname == "CompositorNodeRLayers": - rl_node = node - break - - # If there's not a Render Layers node, we create it - if not rl_node: - rl_node = tree.nodes.new("CompositorNodeRLayers") - - # Get the enabled output sockets, that are the active passes for the - # render. - # We also exclude some layers. - exclude_sockets = ["Image", "Alpha"] - passes = [ - socket - for socket in rl_node.outputs - if socket.enabled and socket.name not in exclude_sockets - ] - - # Remove all output nodes - for node in tree.nodes: - if node.bl_idname == "CompositorNodeOutputFile": - tree.nodes.remove(node) - - # Create a new output node - output = tree.nodes.new("CompositorNodeOutputFile") - - image_settings = bpy.context.scene.render.image_settings - output.format.file_format = image_settings.file_format - - # In case of a multilayer exr, we don't need to use the output node, - # because the blender render already outputs a multilayer exr. - if ext == "exr" and multilayer: - output.layer_slots.clear() - return [] - - output.file_slots.clear() - output.base_path = output_path - - aov_file_products = [] - - # For each active render pass, we add a new socket to the output node - # and link it - for render_pass in passes: - filepath = f"{name}{aov_sep}{render_pass.name}.####" - - output.file_slots.new(filepath) - - aov_file_products.append( - (render_pass.name, os.path.join(output_path, filepath))) - - node_input = output.inputs[-1] - - tree.links.new(render_pass, node_input) - - return aov_file_products - - @staticmethod - def imprint_render_settings(node, data): - RENDER_DATA = "render_data" - if not node.get(RENDER_DATA): - node[RENDER_DATA] = {} - for key, value in data.items(): - if value is None: - continue - node[RENDER_DATA][key] = value - - def prepare_rendering(self, asset_group, name): - filepath = bpy.data.filepath - assert filepath, "Workfile not saved. Please save the file first." - - file_path = os.path.dirname(filepath) - file_name = os.path.basename(filepath) - file_name, _ = os.path.splitext(file_name) - - project = get_current_project_name() - settings = get_project_settings(project) - - render_folder = self.get_default_render_folder(settings) - aov_sep = self.get_aov_separator(settings) - ext = self.get_image_format(settings) - multilayer = self.get_multilayer(settings) - - self.set_render_format(ext, multilayer) - aov_list, custom_passes = self.set_render_passes(settings) - - output_path = os.path.join(file_path, render_folder, file_name) - - render_product = self.get_render_product(output_path, name, aov_sep) - aov_file_product = self.set_node_tree( - output_path, name, aov_sep, ext, multilayer) - - bpy.context.scene.render.filepath = render_product - - render_settings = { - "render_folder": render_folder, - "aov_separator": aov_sep, - "image_format": ext, - "multilayer_exr": multilayer, - "aov_list": aov_list, - "custom_passes": custom_passes, - "render_product": render_product, - "aov_file_product": aov_file_product, - "review": True, - } - - self.imprint_render_settings(asset_group, render_settings) - def process(self): # Get Instance Container or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) @@ -277,7 +35,7 @@ class CreateRenderlayer(plugin.Creator): self.data['task'] = get_current_task_name() lib.imprint(asset_group, self.data) - self.prepare_rendering(asset_group, name) + prepare_rendering(asset_group) except Exception: # Remove the instance if there was an error bpy.data.collections.remove(asset_group) diff --git a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py new file mode 100644 index 0000000000..54a4442bdb --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py @@ -0,0 +1,47 @@ +import os + +import bpy + +import pyblish.api +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.hosts.blender.api.render_lib import prepare_rendering + + +class ValidateDeadlinePublish(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates Render File Directory is + not the same in every submission + """ + + order = ValidateContentsOrder + families = ["renderlayer"] + hosts = ["blender"] + label = "Validate Render Output for Deadline" + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + filepath = bpy.data.filepath + file = os.path.basename(filepath) + filename, ext = os.path.splitext(file) + if filename not in bpy.context.scene.render.filepath: + raise PublishValidationError( + "Render output folder " + "doesn't match the max scene name! " + "Use Repair action to " + "fix the folder file path.." + ) + + @classmethod + def repair(cls, instance): + container = bpy.data.collections[str(instance)] + prepare_rendering(container) + bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) + cls.log.debug("Reset the render output folder...") From c315ee8f65db1a6a973d6480070c5c098c33327f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 17:20:26 +0100 Subject: [PATCH 28/37] Hound fixes --- openpype/hosts/blender/api/render_lib.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/api/render_lib.py b/openpype/hosts/blender/api/render_lib.py index 994de43503..43560ee6d5 100644 --- a/openpype/hosts/blender/api/render_lib.py +++ b/openpype/hosts/blender/api/render_lib.py @@ -13,12 +13,13 @@ def get_default_render_folder(settings): ["RenderSettings"] ["default_render_image_folder"]) + def get_aov_separator(settings): """Get aov separator from blender settings.""" aov_sep = (settings["blender"] - ["RenderSettings"] - ["aov_separator"]) + ["RenderSettings"] + ["aov_separator"]) if aov_sep == "dash": return "-" @@ -29,6 +30,7 @@ def get_aov_separator(settings): else: raise ValueError(f"Invalid aov separator: {aov_sep}") + def get_image_format(settings): """Get image format from blender settings.""" @@ -36,6 +38,7 @@ def get_image_format(settings): ["RenderSettings"] ["image_format"]) + def get_multilayer(settings): """Get multilayer from blender settings.""" @@ -43,6 +46,7 @@ def get_multilayer(settings): ["RenderSettings"] ["multilayer_exr"]) + def get_render_product(output_path, name, aov_sep): """ Generate the path to the render product. Blender interprets the `#` @@ -61,6 +65,7 @@ def get_render_product(output_path, name, aov_sep): return render_product + def set_render_format(ext, multilayer): # Set Blender to save the file with the right extension bpy.context.scene.render.use_file_extension = True @@ -85,14 +90,15 @@ def set_render_format(ext, multilayer): elif ext == "tif": image_settings.file_format = "TIFF" + def set_render_passes(settings): aov_list = (settings["blender"] ["RenderSettings"] ["aov_list"]) custom_passes = (settings["blender"] - ["RenderSettings"] - ["custom_passes"]) + ["RenderSettings"] + ["custom_passes"]) vl = bpy.context.view_layer @@ -122,6 +128,7 @@ def set_render_passes(settings): return aov_list, custom_passes + def set_node_tree(output_path, name, aov_sep, ext, multilayer): # Set the scene to use the compositor node tree to render bpy.context.scene.use_nodes = True @@ -187,6 +194,7 @@ def set_node_tree(output_path, name, aov_sep, ext, multilayer): return aov_file_products + def imprint_render_settings(node, data): RENDER_DATA = "render_data" if not node.get(RENDER_DATA): @@ -196,6 +204,7 @@ def imprint_render_settings(node, data): continue node[RENDER_DATA][key] = value + def prepare_rendering(asset_group): name = asset_group.name From 11921a9de995794cbc950ce183b5d3e837d78d17 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 17:20:43 +0100 Subject: [PATCH 29/37] Added settings for new validator --- .../defaults/project_settings/blender.json | 5 ++ .../schemas/schema_blender_publish.json | 84 +++++++++++++------ .../server/settings/publish_plugins.py | 10 +++ 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index 9cbbb49593..f3eb31174f 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -46,6 +46,11 @@ "optional": false, "active": true }, + "ValidateDeadlinePublish": { + "enabled": true, + "optional": false, + "active": true + }, "ValidateMeshHasUvs": { "enabled": true, "optional": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json index 05e7f13e70..7f1a8a915b 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -51,30 +51,6 @@ } ] }, - { - "type": "dict", - "collapsible": true, - "key": "ValidateRenderCameraIsSet", - "label": "Validate Render Camera Is Set", - "checkbox_key": "enabled", - "children": [ - { - "type": "boolean", - "key": "enabled", - "label": "Enabled" - }, - { - "type": "boolean", - "key": "optional", - "label": "Optional" - }, - { - "type": "boolean", - "key": "active", - "label": "Active" - } - ] - }, { "type": "collapsible-wrap", "label": "Model", @@ -103,6 +79,66 @@ } ] }, + { + "type": "collapsible-wrap", + "label": "Render", + "children": [ + { + "type": "schema_template", + "name": "template_publish_plugin", + "template_data": [ + { + "type": "dict", + "collapsible": true, + "key": "ValidateRenderCameraIsSet", + "label": "Validate Render Camera Is Set", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateDeadlinePublish", + "label": "Validate Render Output for Deadline", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + } + ] + } + ] + }, { "type": "splitter" }, diff --git a/server_addon/blender/server/settings/publish_plugins.py b/server_addon/blender/server/settings/publish_plugins.py index 575bfe9f39..5e047b7013 100644 --- a/server_addon/blender/server/settings/publish_plugins.py +++ b/server_addon/blender/server/settings/publish_plugins.py @@ -73,6 +73,11 @@ class PublishPuginsModel(BaseSettingsModel): title="Validate Render Camera Is Set", section="Validators" ) + ValidateDeadlinePublish: ValidatePluginModel = Field( + default_factory=ValidatePluginModel, + title="Validate Render Output for Deadline", + section="Validators" + ) ValidateMeshHasUvs: ValidatePluginModel = Field( default_factory=ValidatePluginModel, title="Validate Mesh Has Uvs" @@ -149,6 +154,11 @@ DEFAULT_BLENDER_PUBLISH_SETTINGS = { "optional": False, "active": True }, + "ValidateDeadlinePublish": { + "enabled": True, + "optional": False, + "active": True + }, "ValidateMeshHasUvs": { "enabled": True, "optional": True, From 2acbf241ad12b32affa6772e87728c31e0b3135c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 15 Sep 2023 18:05:23 +0100 Subject: [PATCH 30/37] Changed family to "render" --- openpype/hosts/blender/plugins/create/create_render.py | 4 +--- openpype/hosts/blender/plugins/publish/collect_render.py | 3 ++- .../blender/plugins/publish/increment_workfile_version.py | 2 +- .../blender/plugins/publish/validate_deadline_publish.py | 2 +- .../blender/plugins/publish/validate_render_camera_is_set.py | 2 +- .../deadline/plugins/publish/submit_blender_deadline.py | 2 +- 6 files changed, 7 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/plugins/create/create_render.py b/openpype/hosts/blender/plugins/create/create_render.py index 7a91726a5f..f938a21808 100644 --- a/openpype/hosts/blender/plugins/create/create_render.py +++ b/openpype/hosts/blender/plugins/create/create_render.py @@ -1,6 +1,4 @@ """Create render.""" -import os - import bpy from openpype.pipeline import get_current_task_name @@ -14,7 +12,7 @@ class CreateRenderlayer(plugin.Creator): name = "renderingMain" label = "Render" - family = "renderlayer" + family = "render" icon = "eye" def process(self): diff --git a/openpype/hosts/blender/plugins/publish/collect_render.py b/openpype/hosts/blender/plugins/publish/collect_render.py index e0fc933241..92e2473a95 100644 --- a/openpype/hosts/blender/plugins/publish/collect_render.py +++ b/openpype/hosts/blender/plugins/publish/collect_render.py @@ -15,7 +15,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): order = pyblish.api.CollectorOrder + 0.01 hosts = ["blender"] - families = ["renderlayer"] + families = ["render"] label = "Collect Render Layers" sync_workfile_version = False @@ -100,6 +100,7 @@ class CollectBlenderRender(pyblish.api.InstancePlugin): expected_files = expected_beauty | expected_aovs instance.data.update({ + "family": "render.farm", "frameStart": frame_start, "frameEnd": frame_end, "frameStartHandle": frame_handle_start, diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index 5f49ad7185..3d176f9c30 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -10,7 +10,7 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin): optional = True hosts = ["blender"] families = ["animation", "model", "rig", "action", "layout", "blendScene", - "renderlayer"] + "render"] def process(self, context): diff --git a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py index 54a4442bdb..f89a7d3d58 100644 --- a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py @@ -19,7 +19,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, """ order = ValidateContentsOrder - families = ["renderlayer"] + families = ["render.farm"] hosts = ["blender"] label = "Validate Render Output for Deadline" optional = True diff --git a/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py index 5a06c1ff0a..ba3a796f35 100644 --- a/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py +++ b/openpype/hosts/blender/plugins/publish/validate_render_camera_is_set.py @@ -8,7 +8,7 @@ class ValidateRenderCameraIsSet(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder hosts = ["blender"] - families = ["renderlayer"] + families = ["render"] label = "Validate Render Camera Is Set" optional = False diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index ad456c0d13..307fc8b5a2 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -27,7 +27,7 @@ class BlenderPluginInfo(): class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): label = "Submit Render to Deadline" hosts = ["blender"] - families = ["renderlayer"] + families = ["render.farm"] use_published = True priority = 50 From ccf4b7f54781d81d9d6d331f63e3a5a8ab0e0d6f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Sep 2023 10:52:38 +0100 Subject: [PATCH 31/37] Fix error description Co-authored-by: Roy Nieterau --- .../hosts/blender/plugins/publish/validate_deadline_publish.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py index f89a7d3d58..14220b5c9c 100644 --- a/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py +++ b/openpype/hosts/blender/plugins/publish/validate_deadline_publish.py @@ -34,7 +34,7 @@ class ValidateDeadlinePublish(pyblish.api.InstancePlugin, if filename not in bpy.context.scene.render.filepath: raise PublishValidationError( "Render output folder " - "doesn't match the max scene name! " + "doesn't match the blender scene name! " "Use Repair action to " "fix the folder file path.." ) From 4737ca8d5962b3d27d93f6799f58884e2c0a47ae Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Sep 2023 11:08:21 +0100 Subject: [PATCH 32/37] Added note on the Multilayer EXR setting --- .../schemas/projects_schema/schema_project_blender.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index a283a2ff5c..4c9405fcd3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -99,6 +99,10 @@ "type": "boolean", "label": "Multilayer (EXR)" }, + { + "type": "label", + "label": "Note: Multilayer EXR is only used when output format type set to EXR." + }, { "key": "aov_list", "label": "AOVs to create", From 6b3aa6a247c895507771d414c653757634628727 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 2 Oct 2023 12:45:18 +0100 Subject: [PATCH 33/37] Added Cycles render passes --- openpype/hosts/blender/api/render_lib.py | 8 +++++++- .../schemas/projects_schema/schema_project_blender.json | 5 ++++- server_addon/blender/server/settings/render_settings.py | 5 ++++- server_addon/blender/server/version.py | 2 +- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/api/render_lib.py b/openpype/hosts/blender/api/render_lib.py index 43560ee6d5..d564b5ebcb 100644 --- a/openpype/hosts/blender/api/render_lib.py +++ b/openpype/hosts/blender/api/render_lib.py @@ -116,6 +116,12 @@ def set_render_passes(settings): vl.use_pass_shadow = "shadow" in aov_list vl.use_pass_ambient_occlusion = "ao" in aov_list + cycles = vl.cycles + + cycles.denoising_store_passes = "denoising" in aov_list + cycles.use_pass_volume_direct = "volume_direct" in aov_list + cycles.use_pass_volume_indirect = "volume_indirect" in aov_list + aovs_names = [aov.name for aov in vl.aovs] for cp in custom_passes: cp_name = cp[0] @@ -149,7 +155,7 @@ def set_node_tree(output_path, name, aov_sep, ext, multilayer): # Get the enabled output sockets, that are the active passes for the # render. # We also exclude some layers. - exclude_sockets = ["Image", "Alpha"] + exclude_sockets = ["Image", "Alpha", "Noisy Image"] passes = [ socket for socket in rl_node.outputs diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json index 4c9405fcd3..535d9434a3 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -123,7 +123,10 @@ {"emission": "Emission"}, {"environment": "Environment"}, {"shadow": "Shadow"}, - {"ao": "Ambient Occlusion"} + {"ao": "Ambient Occlusion"}, + {"denoising": "Denoising"}, + {"volume_direct": "Direct Volumetric Scattering"}, + {"volume_indirect": "Indirect Volumetric Scattering"} ] }, { diff --git a/server_addon/blender/server/settings/render_settings.py b/server_addon/blender/server/settings/render_settings.py index 7a47095d3c..f62013982e 100644 --- a/server_addon/blender/server/settings/render_settings.py +++ b/server_addon/blender/server/settings/render_settings.py @@ -40,7 +40,10 @@ def aov_list_enum(): {"value": "emission", "label": "Emission"}, {"value": "environment", "label": "Environment"}, {"value": "shadow", "label": "Shadow"}, - {"value": "ao", "label": "Ambient Occlusion"} + {"value": "ao", "label": "Ambient Occlusion"}, + {"value": "denoising", "label": "Denoising"}, + {"value": "volume_direct", "label": "Direct Volumetric Scattering"}, + {"value": "volume_indirect", "label": "Indirect Volumetric Scattering"} ] diff --git a/server_addon/blender/server/version.py b/server_addon/blender/server/version.py index b3f4756216..ae7362549b 100644 --- a/server_addon/blender/server/version.py +++ b/server_addon/blender/server/version.py @@ -1 +1 @@ -__version__ = "0.1.2" +__version__ = "0.1.3" From 8e1f3beff6e68ec0111ec68754050dc9768754ab Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 2 Oct 2023 12:45:50 +0100 Subject: [PATCH 34/37] Fixed render job environment variables --- .../modules/deadline/plugins/publish/submit_blender_deadline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py index 307fc8b5a2..4a7497b075 100644 --- a/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_blender_deadline.py @@ -123,7 +123,7 @@ class BlenderSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.EnvironmentKeyValue[key] = value # to recognize job from PYPE for turning Event On/Off - job_info.EnvironmentKeyValue["OPENPYPE_RENDER_JOB"] = "1" + job_info.add_render_job_env_var() job_info.EnvironmentKeyValue["OPENPYPE_LOG_NO_COLORS"] = "1" # Adding file dependencies. From 35b3006f29f0c5c197abb2f010a6e51f6d214c95 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 2 Oct 2023 15:34:48 +0100 Subject: [PATCH 35/37] Improved naming for RenderProduct --- openpype/hosts/blender/api/colorspace.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/blender/api/colorspace.py b/openpype/hosts/blender/api/colorspace.py index 0f504a3be0..4521612b7d 100644 --- a/openpype/hosts/blender/api/colorspace.py +++ b/openpype/hosts/blender/api/colorspace.py @@ -22,12 +22,11 @@ class RenderProduct(object): class ARenderProduct(object): - def __init__(self): """Constructor.""" # Initialize self.layer_data = self._get_layer_data() - self.layer_data.products = self.get_colorspace_data() + self.layer_data.products = self.get_render_products() def _get_layer_data(self): scene = bpy.context.scene @@ -37,7 +36,7 @@ class ARenderProduct(object): frameEnd=int(scene.frame_end), ) - def get_colorspace_data(self): + def get_render_products(self): """To be implemented by renderer class. This should return a list of RenderProducts. Returns: From 3328ba321db903218b2c28d16676cf6dffd34573 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:01:54 +0200 Subject: [PATCH 36/37] AYON Workfiles Tool: Open workfile changes context (#5671) * change context when opening workfile * do not call 'set_context' in blender * removed unused import --- openpype/hosts/blender/api/ops.py | 10 +-- openpype/tools/ayon_workfiles/abstract.py | 6 +- openpype/tools/ayon_workfiles/control.py | 67 +++++++++++++++---- .../ayon_workfiles/widgets/files_widget.py | 31 ++++++--- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/openpype/hosts/blender/api/ops.py b/openpype/hosts/blender/api/ops.py index 62d7987b47..0eb90eeff9 100644 --- a/openpype/hosts/blender/api/ops.py +++ b/openpype/hosts/blender/api/ops.py @@ -16,6 +16,7 @@ import bpy import bpy.utils.previews from openpype import style +from openpype import AYON_SERVER_ENABLED from openpype.pipeline import get_current_asset_name, get_current_task_name from openpype.tools.utils import host_tools @@ -331,10 +332,11 @@ class LaunchWorkFiles(LaunchQtApp): def execute(self, context): result = super().execute(context) - self._window.set_context({ - "asset": get_current_asset_name(), - "task": get_current_task_name() - }) + if not AYON_SERVER_ENABLED: + self._window.set_context({ + "asset": get_current_asset_name(), + "task": get_current_task_name() + }) return result def before_window_show(self): diff --git a/openpype/tools/ayon_workfiles/abstract.py b/openpype/tools/ayon_workfiles/abstract.py index f511181837..ce399fd4c6 100644 --- a/openpype/tools/ayon_workfiles/abstract.py +++ b/openpype/tools/ayon_workfiles/abstract.py @@ -914,10 +914,12 @@ class AbstractWorkfilesFrontend(AbstractWorkfilesCommon): # Controller actions @abstractmethod - def open_workfile(self, filepath): - """Open a workfile. + def open_workfile(self, folder_id, task_id, filepath): + """Open a workfile for context. Args: + folder_id (str): Folder id. + task_id (str): Task id. filepath (str): Workfile path. """ diff --git a/openpype/tools/ayon_workfiles/control.py b/openpype/tools/ayon_workfiles/control.py index 1153a3c01f..3784959caf 100644 --- a/openpype/tools/ayon_workfiles/control.py +++ b/openpype/tools/ayon_workfiles/control.py @@ -452,12 +452,12 @@ class BaseWorkfileController( self._emit_event("controller.refresh.finished") # Controller actions - def open_workfile(self, filepath): + def open_workfile(self, folder_id, task_id, filepath): self._emit_event("open_workfile.started") failed = False try: - self._host_open_workfile(filepath) + self._open_workfile(folder_id, task_id, filepath) except Exception: failed = True @@ -575,6 +575,53 @@ class BaseWorkfileController( self._expected_selection.get_expected_selection_data(), ) + def _get_event_context_data( + self, project_name, folder_id, task_id, folder=None, task=None + ): + if folder is None: + folder = self.get_folder_entity(folder_id) + if task is None: + task = self.get_task_entity(task_id) + # NOTE keys should be OpenPype compatible + return { + "project_name": project_name, + "folder_id": folder_id, + "asset_id": folder_id, + "asset_name": folder["name"], + "task_id": task_id, + "task_name": task["name"], + "host_name": self.get_host_name(), + } + + def _open_workfile(self, folder_id, task_id, filepath): + project_name = self.get_current_project_name() + event_data = self._get_event_context_data( + project_name, folder_id, task_id + ) + event_data["filepath"] = filepath + + emit_event("workfile.open.before", event_data, source="workfiles.tool") + + # Change context + task_name = event_data["task_name"] + if ( + folder_id != self.get_current_folder_id() + or task_name != self.get_current_task_name() + ): + # Use OpenPype asset-like object + asset_doc = get_asset_by_id( + event_data["project_name"], + event_data["folder_id"], + ) + change_current_context( + asset_doc, + event_data["task_name"] + ) + + self._host_open_workfile(filepath) + + emit_event("workfile.open.after", event_data, source="workfiles.tool") + def _save_as_workfile( self, folder_id, @@ -591,18 +638,14 @@ class BaseWorkfileController( task_name = task["name"] # QUESTION should the data be different for 'before' and 'after'? - # NOTE keys should be OpenPype compatible - event_data = { - "project_name": project_name, - "folder_id": folder_id, - "asset_id": folder_id, - "asset_name": folder["name"], - "task_id": task_id, - "task_name": task_name, - "host_name": self.get_host_name(), + event_data = self._get_event_context_data( + project_name, folder_id, task_id, folder, task + ) + event_data.update({ "filename": filename, "workdir_path": workdir, - } + }) + emit_event("workfile.save.before", event_data, source="workfiles.tool") # Create workfiles root folder diff --git a/openpype/tools/ayon_workfiles/widgets/files_widget.py b/openpype/tools/ayon_workfiles/widgets/files_widget.py index fbf4dbc593..656ddf1dd8 100644 --- a/openpype/tools/ayon_workfiles/widgets/files_widget.py +++ b/openpype/tools/ayon_workfiles/widgets/files_widget.py @@ -106,7 +106,8 @@ class FilesWidget(QtWidgets.QWidget): self._on_published_cancel_clicked) self._selected_folder_id = None - self._selected_tak_name = None + self._selected_task_id = None + self._selected_task_name = None self._pre_select_folder_id = None self._pre_select_task_name = None @@ -178,7 +179,7 @@ class FilesWidget(QtWidgets.QWidget): # ------------------------------------------------------------- # Workarea workfiles # ------------------------------------------------------------- - def _open_workfile(self, filepath): + def _open_workfile(self, folder_id, task_name, filepath): if self._controller.has_unsaved_changes(): result = self._save_changes_prompt() if result is None: @@ -186,12 +187,15 @@ class FilesWidget(QtWidgets.QWidget): if result: self._controller.save_current_workfile() - self._controller.open_workfile(filepath) + self._controller.open_workfile(folder_id, task_name, filepath) def _on_workarea_open_clicked(self): path = self._workarea_widget.get_selected_path() - if path: - self._open_workfile(path) + if not path: + return + folder_id = self._selected_folder_id + task_id = self._selected_task_id + self._open_workfile(folder_id, task_id, path) def _on_current_open_requests(self): self._on_workarea_open_clicked() @@ -238,8 +242,12 @@ class FilesWidget(QtWidgets.QWidget): } filepath = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0] - if filepath: - self._open_workfile(filepath) + if not filepath: + return + + folder_id = self._selected_folder_id + task_id = self._selected_task_id + self._open_workfile(folder_id, task_id, filepath) def _on_workarea_save_clicked(self): result = self._exec_save_as_dialog() @@ -279,10 +287,11 @@ class FilesWidget(QtWidgets.QWidget): def _on_task_changed(self, event): self._selected_folder_id = event["folder_id"] - self._selected_tak_name = event["task_name"] + self._selected_task_id = event["task_id"] + self._selected_task_name = event["task_name"] self._valid_selected_context = ( self._selected_folder_id is not None - and self._selected_tak_name is not None + and self._selected_task_id is not None ) self._update_published_btns_state() @@ -311,7 +320,7 @@ class FilesWidget(QtWidgets.QWidget): if enabled: self._pre_select_folder_id = self._selected_folder_id - self._pre_select_task_name = self._selected_tak_name + self._pre_select_task_name = self._selected_task_name else: self._pre_select_folder_id = None self._pre_select_task_name = None @@ -334,7 +343,7 @@ class FilesWidget(QtWidgets.QWidget): return True if self._pre_select_task_name is None: return False - return self._pre_select_task_name != self._selected_tak_name + return self._pre_select_task_name != self._selected_task_name def _on_published_cancel_clicked(self): folder_id = self._pre_select_folder_id From 7206757c13e29d20f74d173b05327274077aa6c8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 2 Oct 2023 18:18:08 +0200 Subject: [PATCH 37/37] Publisher: Refactor Report Maker plugin data storage to be a dict by plugin.id (#5668) * Refactor plugin data storage to be a dict by plugin.id + Fix `_current_plugin_data` type on `__init__` * Avoid plural when the plugin is singular * Refactor `_plugin_data_by_plugin_id` to `_plugin_data_by_id` --- openpype/tools/publisher/control.py | 56 ++++++++++++++--------------- 1 file changed, 26 insertions(+), 30 deletions(-) diff --git a/openpype/tools/publisher/control.py b/openpype/tools/publisher/control.py index e6b68906fd..a6264303d5 100644 --- a/openpype/tools/publisher/control.py +++ b/openpype/tools/publisher/control.py @@ -176,11 +176,10 @@ class PublishReportMaker: self._create_discover_result = None self._convert_discover_result = None self._publish_discover_result = None - self._plugin_data = [] - self._plugin_data_with_plugin = [] - self._stored_plugins = set() - self._current_plugin_data = [] + self._plugin_data_by_id = {} + self._current_plugin = None + self._current_plugin_data = {} self._all_instances_by_id = {} self._current_context = None @@ -192,9 +191,9 @@ class PublishReportMaker: create_context.convertor_discover_result ) self._publish_discover_result = create_context.publish_discover_result - self._plugin_data = [] - self._plugin_data_with_plugin = [] - self._stored_plugins = set() + + self._plugin_data_by_id = {} + self._current_plugin = None self._current_plugin_data = {} self._all_instances_by_id = {} self._current_context = context @@ -211,18 +210,11 @@ class PublishReportMaker: if self._current_plugin_data: self._current_plugin_data["passed"] = True + self._current_plugin = plugin self._current_plugin_data = self._add_plugin_data_item(plugin) - def _get_plugin_data_item(self, plugin): - store_item = None - for item in self._plugin_data_with_plugin: - if item["plugin"] is plugin: - store_item = item["data"] - break - return store_item - def _add_plugin_data_item(self, plugin): - if plugin in self._stored_plugins: + if plugin.id in self._plugin_data_by_id: # A plugin would be processed more than once. What can cause it: # - there is a bug in controller # - plugin class is imported into multiple files @@ -230,15 +222,9 @@ class PublishReportMaker: raise ValueError( "Plugin '{}' is already stored".format(str(plugin))) - self._stored_plugins.add(plugin) - plugin_data_item = self._create_plugin_data_item(plugin) + self._plugin_data_by_id[plugin.id] = plugin_data_item - self._plugin_data_with_plugin.append({ - "plugin": plugin, - "data": plugin_data_item - }) - self._plugin_data.append(plugin_data_item) return plugin_data_item def _create_plugin_data_item(self, plugin): @@ -279,7 +265,7 @@ class PublishReportMaker: """Add result of single action.""" plugin = result["plugin"] - store_item = self._get_plugin_data_item(plugin) + store_item = self._plugin_data_by_id.get(plugin.id) if store_item is None: store_item = self._add_plugin_data_item(plugin) @@ -301,14 +287,24 @@ class PublishReportMaker: instance, instance in self._current_context ) - plugins_data = copy.deepcopy(self._plugin_data) - if plugins_data and not plugins_data[-1]["passed"]: - plugins_data[-1]["passed"] = True + plugins_data_by_id = copy.deepcopy( + self._plugin_data_by_id + ) + + # Ensure the current plug-in is marked as `passed` in the result + # so that it shows on reports for paused publishes + if self._current_plugin is not None: + current_plugin_data = plugins_data_by_id.get( + self._current_plugin.id + ) + if current_plugin_data and not current_plugin_data["passed"]: + current_plugin_data["passed"] = True if publish_plugins: for plugin in publish_plugins: - if plugin not in self._stored_plugins: - plugins_data.append(self._create_plugin_data_item(plugin)) + if plugin.id not in plugins_data_by_id: + plugins_data_by_id[plugin.id] = \ + self._create_plugin_data_item(plugin) reports = [] if self._create_discover_result is not None: @@ -329,7 +325,7 @@ class PublishReportMaker: ) return { - "plugins_data": plugins_data, + "plugins_data": list(plugins_data_by_id.values()), "instances": instances_details, "context": self._extract_context_data(self._current_context), "crashed_file_paths": crashed_file_paths,