From 46411c9723a80a781ec513d6a2cfd175cec44644 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 4 Aug 2022 11:10:45 +0100 Subject: [PATCH 1/7] Implemented review --- openpype/hosts/blender/api/__init__.py | 5 + openpype/hosts/blender/api/capture.py | 278 ++++++++++++++++ openpype/hosts/blender/api/lib.py | 9 + openpype/hosts/blender/api/plugin.py | 7 +- .../blender/plugins/create/create_review.py | 47 +++ .../blender/plugins/publish/collect_review.py | 64 ++++ .../plugins/publish/extract_playblast.py | 125 +++++++ .../plugins/publish/extract_thumbnail.py | 102 ++++++ .../plugins/publish/integrate_thumbnail.py | 10 + openpype/plugins/publish/extract_review.py | 1 + .../defaults/project_settings/blender.json | 137 ++++++++ .../schema_project_blender.json | 4 + .../schemas/schema_blender_publish.json | 308 ++++++++++++++++++ 13 files changed, 1095 insertions(+), 2 deletions(-) create mode 100644 openpype/hosts/blender/api/capture.py create mode 100644 openpype/hosts/blender/plugins/create/create_review.py create mode 100644 openpype/hosts/blender/plugins/publish/collect_review.py create mode 100644 openpype/hosts/blender/plugins/publish/extract_playblast.py create mode 100644 openpype/hosts/blender/plugins/publish/extract_thumbnail.py create mode 100644 openpype/hosts/blender/plugins/publish/integrate_thumbnail.py create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json diff --git a/openpype/hosts/blender/api/__init__.py b/openpype/hosts/blender/api/__init__.py index e017d74d912..75a11affde6 100644 --- a/openpype/hosts/blender/api/__init__.py +++ b/openpype/hosts/blender/api/__init__.py @@ -31,10 +31,13 @@ lsattrs, read, maintained_selection, + maintained_time, get_selection, # unique_name, ) +from .capture import capture + __all__ = [ "install", @@ -56,9 +59,11 @@ # Utility functions "maintained_selection", + "maintained_time", "lsattr", "lsattrs", "read", "get_selection", + "capture", # "unique_name", ] diff --git a/openpype/hosts/blender/api/capture.py b/openpype/hosts/blender/api/capture.py new file mode 100644 index 00000000000..7cf9e52cb61 --- /dev/null +++ b/openpype/hosts/blender/api/capture.py @@ -0,0 +1,278 @@ + +"""Blender Capture +Playblasting with independent viewport, camera and display options +""" +import contextlib +import bpy + +from .lib import maintained_time +from .plugin import deselect_all, create_blender_context + + +def capture( + camera=None, + width=None, + height=None, + filename=None, + start_frame=None, + end_frame=None, + step_frame=None, + sound=None, + isolate=None, + maintain_aspect_ratio=True, + overwrite=False, + image_settings=None, + display_options=None +): + """Playblast in an independent windows + Arguments: + camera (str, optional): Name of camera, defaults to "Camera" + width (int, optional): Width of output in pixels + height (int, optional): Height of output in pixels + filename (str, optional): Name of output file path. Defaults to current + render output path. + start_frame (int, optional): Defaults to current start frame. + end_frame (int, optional): Defaults to current end frame. + step_frame (int, optional): Defaults to 1. + sound (str, optional): Specify the sound node to be used during + playblast. When None (default) no sound will be used. + isolate (list): List of nodes to isolate upon capturing + maintain_aspect_ratio (bool, optional): Modify height in order to + maintain aspect ratio. + overwrite (bool, optional): Whether or not to overwrite if file + already exists. If disabled and file exists and error will be + raised. + image_settings (dict, optional): Supplied image settings for render, + using `ImageSettings` + display_options (dict, optional): Supplied display options for render + """ + + scene = bpy.context.scene + camera = camera or "Camera" + + # Ensure camera exists. + if camera not in scene.objects and camera != "AUTO": + raise RuntimeError("Camera does not exist: {0}".format(camera)) + + # Ensure resolution. + if width and height: + maintain_aspect_ratio = False + width = width or scene.render.resolution_x + height = height or scene.render.resolution_y + if maintain_aspect_ratio: + ratio = scene.render.resolution_x / scene.render.resolution_y + height = round(width / ratio) + + # Get frame range. + if start_frame is None: + start_frame = scene.frame_start + if end_frame is None: + end_frame = scene.frame_end + if step_frame is None: + step_frame = 1 + frame_range = (start_frame, end_frame, step_frame) + + if filename is None: + filename = scene.render.filepath + + render_options = { + "filepath": "{}.".format(filename.rstrip(".")), + "resolution_x": width, + "resolution_y": height, + "use_overwrite": overwrite, + } + + with _independent_window() as window: + + applied_view(window, camera, isolate, options=display_options) + + with contextlib.ExitStack() as stack: + stack.enter_context(maintain_camera(window, camera)) + stack.enter_context(applied_frame_range(window, *frame_range)) + stack.enter_context(applied_render_options(window, render_options)) + stack.enter_context(applied_image_settings(window, image_settings)) + stack.enter_context(maintained_time()) + + bpy.ops.render.opengl( + animation=True, + render_keyed_only=False, + sequencer=False, + write_still=False, + view_context=True + ) + + return filename + + +ImageSettings = { + "file_format": "FFMPEG", + "color_mode": "RGB", + "ffmpeg": { + "format": "QUICKTIME", + "use_autosplit": False, + "codec": "H264", + "constant_rate_factor": "MEDIUM", + "gopsize": 18, + "use_max_b_frames": False, + }, +} + + +def isolate_objects(window, objects): + """Isolate selection""" + deselect_all() + + for obj in objects: + obj.select_set(True) + + context = create_blender_context(selected=objects, window=window) + + bpy.ops.view3d.view_axis(context, type="FRONT") + bpy.ops.view3d.localview(context) + + deselect_all() + + +def _apply_options(entity, options): + for option, value in options.items(): + if isinstance(value, dict): + _apply_options(getattr(entity, option), value) + else: + setattr(entity, option, value) + + +def applied_view(window, camera, isolate=None, options=None): + """Apply view options to window.""" + area = window.screen.areas[0] + space = area.spaces[0] + + area.ui_type = "VIEW_3D" + + meshes = [obj for obj in window.scene.objects if obj.type == "MESH"] + + if camera == "AUTO": + space.region_3d.view_perspective = "ORTHO" + isolate_objects(window, isolate or meshes) + else: + isolate_objects(window, isolate or meshes) + space.camera = window.scene.objects.get(camera) + space.region_3d.view_perspective = "CAMERA" + + if isinstance(options, dict): + _apply_options(space, options) + else: + space.shading.type = "SOLID" + space.shading.color_type = "MATERIAL" + space.show_gizmo = False + space.overlay.show_overlays = False + + +@contextlib.contextmanager +def applied_frame_range(window, start, end, step): + """Context manager for setting frame range.""" + # Store current frame range + current_frame_start = window.scene.frame_start + current_frame_end = window.scene.frame_end + current_frame_step = window.scene.frame_step + # Apply frame range + window.scene.frame_start = start + window.scene.frame_end = end + window.scene.frame_step = step + try: + yield + finally: + # Restore frame range + window.scene.frame_start = current_frame_start + window.scene.frame_end = current_frame_end + window.scene.frame_step = current_frame_step + + +@contextlib.contextmanager +def applied_render_options(window, options): + """Context manager for setting render options.""" + render = window.scene.render + + # Store current settings + original = {} + for opt in options.copy(): + try: + original[opt] = getattr(render, opt) + except ValueError: + options.pop(opt) + + # Apply settings + _apply_options(render, options) + + try: + yield + finally: + # Restore previous settings + _apply_options(render, original) + + +@contextlib.contextmanager +def applied_image_settings(window, options): + """Context manager to override image settings.""" + + options = options or ImageSettings.copy() + ffmpeg = options.pop("ffmpeg", {}) + render = window.scene.render + + # Store current image settings + original = {} + for opt in options.copy(): + try: + original[opt] = getattr(render.image_settings, opt) + except ValueError: + options.pop(opt) + + # Store current ffmpeg settings + original_ffmpeg = {} + for opt in ffmpeg.copy(): + try: + original_ffmpeg[opt] = getattr(render.ffmpeg, opt) + except ValueError: + ffmpeg.pop(opt) + + # Apply image settings + for opt, value in options.items(): + setattr(render.image_settings, opt, value) + + # Apply ffmpeg settings + for opt, value in ffmpeg.items(): + setattr(render.ffmpeg, opt, value) + + try: + yield + finally: + # Restore previous settings + for opt, value in original.items(): + setattr(render.image_settings, opt, value) + for opt, value in original_ffmpeg.items(): + setattr(render.ffmpeg, opt, value) + + +@contextlib.contextmanager +def maintain_camera(window, camera): + """Context manager to override camera.""" + current_camera = window.scene.camera + if camera in window.scene.objects: + window.scene.camera = window.scene.objects.get(camera) + try: + yield + finally: + window.scene.camera = current_camera + + +@contextlib.contextmanager +def _independent_window(): + """Create capture-window context.""" + context = create_blender_context() + current_windows = set(bpy.context.window_manager.windows) + bpy.ops.wm.window_new(context) + window = list(set(bpy.context.window_manager.windows) - current_windows)[0] + context["window"] = window + try: + yield window + finally: + bpy.ops.wm.window_close(context) \ No newline at end of file diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index 20098c0fe81..ee127ab313b 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -284,3 +284,12 @@ def maintained_selection(): # This could happen if the active node was deleted during the # context. log.exception("Failed to set active object.") + +@contextlib.contextmanager +def maintained_time(): + """Maintain current frame during context.""" + current_time = bpy.context.scene.frame_current + try: + yield + finally: + bpy.context.scene.frame_current = current_time diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index c59be8d7ff9..1274795c6be 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -62,7 +62,8 @@ def prepare_data(data, container_name=None): def create_blender_context(active: Optional[bpy.types.Object] = None, - selected: Optional[bpy.types.Object] = None,): + selected: Optional[bpy.types.Object] = None, + window: Optional[bpy.types.Window] = None): """Create a new Blender context. If an object is passed as parameter, it is set as selected and active. """ @@ -72,7 +73,9 @@ def create_blender_context(active: Optional[bpy.types.Object] = None, override_context = bpy.context.copy() - for win in bpy.context.window_manager.windows: + windows = [window] if window else bpy.context.window_manager.windows + + for win in windows: for area in win.screen.areas: if area.type == 'VIEW_3D': for region in area.regions: diff --git a/openpype/hosts/blender/plugins/create/create_review.py b/openpype/hosts/blender/plugins/create/create_review.py new file mode 100644 index 00000000000..bf4ea6a7cd6 --- /dev/null +++ b/openpype/hosts/blender/plugins/create/create_review.py @@ -0,0 +1,47 @@ +"""Create review.""" + +import bpy + +from openpype.pipeline import legacy_io +from openpype.hosts.blender.api import plugin, lib, ops +from openpype.hosts.blender.api.pipeline import AVALON_INSTANCES + + +class CreateReview(plugin.Creator): + """Single baked camera""" + + name = "reviewDefault" + label = "Review" + family = "review" + icon = "video-camera" + + 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'] = legacy_io.Session.get('AVALON_TASK') + 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_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py new file mode 100644 index 00000000000..09b5558d317 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -0,0 +1,64 @@ +import bpy + +import pyblish.api +from openpype.pipeline import legacy_io + + +class CollectReview(pyblish.api.InstancePlugin): + """Collect Review data + + """ + + order = pyblish.api.CollectorOrder + 0.3 + label = "Collect Review Data" + families = ["review"] + + def process(self, instance): + + self.log.debug(f"instance: {instance}") + + # get cameras + cameras = [ + obj + for obj in instance + if isinstance(obj, bpy.types.Object) and obj.type == "CAMERA" + ] + + assert len(cameras) == 1, ( + f"Not a single camera found in extraction: {cameras}" + ) + camera = cameras[0].name + self.log.debug(f"camera: {camera}") + + # get isolate objects list from meshes instance members . + isolate_objects = [ + obj + for obj in instance + if isinstance(obj, bpy.types.Object) and obj.type == "MESH" + ] + + if not instance.data.get("remove"): + + task = legacy_io.Session.get("AVALON_TASK") + + instance.data.update({ + "subset": f"{task}Review", + "review_camera": camera, + "frameStart": instance.context.data["frameStart"], + "frameEnd": instance.context.data["frameEnd"], + "fps": instance.context.data["fps"], + "isolate": isolate_objects, + }) + + self.log.debug(f"instance data: {instance.data}") + + # TODO : Collect audio + audio_tracks = [] + instance.data["audio"] = [] + for track in audio_tracks: + instance.data["audio"].append( + { + "offset": track.offset.get(), + "filename": track.filename.get(), + } + ) \ No newline at end of file diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py new file mode 100644 index 00000000000..d07968d4692 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -0,0 +1,125 @@ +import os +import clique + +import bpy + +import pyblish.api +import openpype.api +from openpype.hosts.blender.api import capture +from openpype.hosts.blender.api.lib import maintained_time + + +class ExtractPlayblast(openpype.api.Extractor): + """ + Extract viewport playblast. + + Takes review camera and creates review Quicktime video based on viewport + capture. + """ + + label = "Extract Playblast" + hosts = ["blender"] + families = ["review"] + optional = True + order = pyblish.api.ExtractorOrder + 0.01 + + + def process(self, instance): + self.log.info("Extracting capture..") + + self.log.info(instance.data) + print(instance.context.data) + + # get scene fps + fps = instance.data.get("fps") + if fps is None: + fps = bpy.context.scene.render.fps + instance.data["fps"] = fps + + self.log.info(f"fps: {fps}") + + # If start and end frames cannot be determined, + # get them from Blender timeline. + start = instance.data.get("frameStart", bpy.context.scene.frame_start) + end = instance.data.get("frameEnd", bpy.context.scene.frame_end) + + self.log.info(f"start: {start}, end: {end}") + assert end > start, "Invalid time range !" + + # get cameras + camera = instance.data("review_camera", None) + + # get isolate objects list + isolate = instance.data("isolate", None) + + # get ouput path + stagingdir = self.staging_dir(instance) + filename = instance.name + path = os.path.join(stagingdir, filename) + + self.log.info(f"Outputting images to {path}") + + project_settings = instance.context.data["project_settings"]["blender"] + presets = project_settings["publish"]["ExtractPlayblast"]["presets"] + preset = presets.get("default") + preset.update({ + "camera": camera, + "start_frame": start, + "end_frame": end, + "filename": path, + "overwrite": True, + "isolate": isolate, + }) + preset.setdefault( + "image_settings", + { + "file_format": "PNG", + "color_mode": "RGB", + "color_depth": "8", + "compression": 15, + }, + ) + + with maintained_time(): + path = capture(**preset) + + self.log.debug(f"playblast path {path}") + + collected_files = os.listdir(stagingdir) + collections, remainder = clique.assemble( + collected_files, + patterns=[f"{filename}\\.{clique.DIGITS_PATTERN}\\.png$"], + ) + + if len(collections) > 1: + raise RuntimeError( + f"More than one collection found in stagingdir: {stagingdir}" + ) + elif len(collections) == 0: + raise RuntimeError( + f"No collection found in stagingdir: {stagingdir}" + ) + + frame_collection = collections[0] + + self.log.info(f"We found collection of interest {frame_collection}") + + instance.data.setdefault("representations", []) + + tags = ["review"] + if not instance.data.get("keepImages"): + tags.append("delete") + + representation = { + "name": "png", + "ext": "png", + "files": list(frame_collection), + "stagingDir": stagingdir, + "frameStart": start, + "frameEnd": end, + "fps": fps, + "preview": True, + "tags": tags, + "camera_name": camera + } + instance.data["representations"].append(representation) \ No newline at end of file diff --git a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py new file mode 100644 index 00000000000..3fabdf61e08 --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py @@ -0,0 +1,102 @@ +import os +import glob + +import pyblish.api +import openpype.api +from openpype.hosts.blender.api import capture +from openpype.hosts.blender.api.lib import maintained_time + +import bpy + + +class ExtractThumbnail(openpype.api.Extractor): + """Extract viewport thumbnail. + + Takes review camera and creates a thumbnail based on viewport + capture. + + """ + + label = "Extract Thumbnail" + hosts = ["blender"] + families = ["review"] + order = pyblish.api.ExtractorOrder + 0.01 + + def process(self, instance): + self.log.info("Extracting capture..") + + stagingdir = self.staging_dir(instance) + filename = instance.name + path = os.path.join(stagingdir, filename) + + self.log.info(f"Outputting images to {path}") + + camera = instance.data.get("review_camera", "AUTO") + start = instance.data.get("frameStart", bpy.context.scene.frame_start) + family = instance.data.get("family") + isolate = instance.data("isolate", None) + + project_settings = instance.context.data["project_settings"]["blender"] + extractor_settings = project_settings["publish"]["ExtractThumbnail"] + presets = extractor_settings.get("presets") + + preset = presets.get(family, {}) + + preset.update({ + "camera": camera, + "start_frame": start, + "end_frame": start, + "filename": path, + "overwrite": True, + "isolate": isolate, + }) + preset.setdefault( + "image_settings", + { + "file_format": "JPEG", + "color_mode": "RGB", + "quality": 100, + }, + ) + + with maintained_time(): + path = capture(**preset) + + thumbnail = os.path.basename(self._fix_output_path(path)) + + self.log.info(f"thumbnail: {thumbnail}") + + instance.data.setdefault("representations", []) + + representation = { + "name": "thumbnail", + "ext": "jpg", + "files": thumbnail, + "stagingDir": stagingdir, + "thumbnail": True + } + instance.data["representations"].append(representation) + + def _fix_output_path(self, filepath): + """"Workaround to return correct filepath. + + To workaround this we just glob.glob() for any file extensions and + assume the latest modified file is the correct file and return it. + + """ + # Catch cancelled playblast + if filepath is None: + self.log.warning( + "Playblast did not result in output path. " + "Playblast is probably interrupted." + ) + return None + + if not os.path.exists(filepath): + files = glob.glob(f"{filepath}.*.jpg") + + if not files: + raise RuntimeError(f"Couldn't find playblast from: {filepath}") + filepath = max(files, key=os.path.getmtime) + + return filepath \ No newline at end of file diff --git a/openpype/hosts/blender/plugins/publish/integrate_thumbnail.py b/openpype/hosts/blender/plugins/publish/integrate_thumbnail.py new file mode 100644 index 00000000000..489db673dea --- /dev/null +++ b/openpype/hosts/blender/plugins/publish/integrate_thumbnail.py @@ -0,0 +1,10 @@ +import pyblish.api +from openpype.plugins.publish import integrate_thumbnail + + +class IntegrateThumbnails(integrate_thumbnail.IntegrateThumbnails): + """Integrate Thumbnails.""" + + label = "Integrate Thumbnails" + order = pyblish.api.IntegratorOrder + 0.01 + families = ["review"] diff --git a/openpype/plugins/publish/extract_review.py b/openpype/plugins/publish/extract_review.py index 533a87acb40..5068b0261ed 100644 --- a/openpype/plugins/publish/extract_review.py +++ b/openpype/plugins/publish/extract_review.py @@ -41,6 +41,7 @@ class ExtractReview(pyblish.api.InstancePlugin): hosts = [ "nuke", "maya", + "blender", "shell", "hiero", "premiere", diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index a7262dcb5d7..e3be0b50ee0 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -2,5 +2,142 @@ "workfile_builder": { "create_first_version": false, "custom_templates": [] + }, + "publish": { + "ValidateLinkedVersion": { + "enabled": true, + "optional": true, + "active": true + }, + "ValidateLinkedData": { + "enabled": true, + "optional": true, + "active": true + }, + "ExtractBlend": { + "enabled": true, + "optional": true, + "active": true, + "pack_images": true + }, + "ExtractBlendAnimation": { + "enabled": true, + "optional": true, + "active": true + }, + "ExtractCamera": { + "enabled": true, + "optional": true, + "active": true + }, + "ExtractFBX": { + "enabled": true, + "optional": true, + "active": true, + "scale_length": 0 + }, + "ExtractAnimationFBX": { + "enabled": true, + "optional": true, + "active": true + }, + "ExtractABC": { + "enabled": true, + "optional": true, + "active": true + }, + "ExtractLayout": { + "enabled": true, + "optional": true, + "active": true + }, + "ExtractThumbnail": { + "enabled": true, + "optional": true, + "active": true, + "presets": { + "model": { + "image_settings": { + "file_format": "JPEG", + "color_mode": "RGB", + "quality": 100 + }, + "display_options": { + "shading": { + "light": "STUDIO", + "studio_light": "Default", + "type": "SOLID", + "color_type": "OBJECT", + "show_xray": false, + "show_shadows": false, + "show_cavity": true + }, + "overlay": { + "show_overlays": false + } + } + }, + "rig": { + "image_settings": { + "file_format": "JPEG", + "color_mode": "RGB", + "quality": 100 + }, + "display_options": { + "shading": { + "light": "STUDIO", + "studio_light": "Default", + "type": "SOLID", + "color_type": "OBJECT", + "show_xray": true, + "show_shadows": false, + "show_cavity": false + }, + "overlay": { + "show_overlays": true, + "show_ortho_grid": false, + "show_floor": false, + "show_axis_x": false, + "show_axis_y": false, + "show_axis_z": false, + "show_text": false, + "show_stats": false, + "show_cursor": false, + "show_annotation": false, + "show_extras": false, + "show_relationship_lines": false, + "show_outline_selected": false, + "show_motion_paths": false, + "show_object_origins": false, + "show_bones": true + } + } + } + } + }, + "ExtractPlayblast": { + "enabled": true, + "optional": true, + "active": true, + "presets": { + "default": { + "image_settings": { + "file_format": "PNG", + "color_mode": "RGB", + "color_depth": "8", + "compression": 15 + }, + "display_options": { + "shading": { + "type": "MATERIAL", + "render_pass": "COMBINED" + }, + "overlay": { + "show_overlays": false + } + } + } + } + } } } \ No newline at end of file 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 af09329a034..4c72ebda2fa 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_blender.json @@ -12,6 +12,10 @@ "workfile_builder/builder_on_start", "workfile_builder/profiles" ] + }, + { + "type": "schema", + "name": "schema_blender_publish" } ] } 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 new file mode 100644 index 00000000000..a634b6dabe2 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_blender_publish.json @@ -0,0 +1,308 @@ +{ + "type": "dict", + "collapsible": true, + "key": "publish", + "label": "Publish plugins", + "children": [ + { + "type": "label", + "label": "Validators" + }, + { + "type": "dict", + "collapsible": true, + "key": "ValidateLinkedVersion", + "label": "Validate Linked Version", + "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": "ValidateLinkedData", + "label": "Validate Linked Data", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + } + ] + }, + { + "type": "splitter" + }, + { + "type": "label", + "label": "Extractors" + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractBlend", + "label": "Extract Blend", + "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": "pack_images", + "label": "Pack Images" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractBlendAnimation", + "label": "Extract Blend Animation", + "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": "ExtractCamera", + "label": "Extract Camera as FBX", + "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": "ExtractFBX", + "label": "Extract FBX (model and rig)", + "checkbox_key": "enabled", + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "number", + "key": "scale_length", + "label": "Scale length", + "decimal": 3, + "minimum": 0, + "maximum": 1000 + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractAnimationFBX", + "label": "Extract Animation FBX", + "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": "ExtractABC", + "label": "Extract ABC (model and pointcache)", + "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": "ExtractLayout", + "label": "Extract Layout as JSON", + "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": "ExtractThumbnail", + "label": "ExtractThumbnail", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "raw-json", + "key": "presets", + "label": "Presets" + } + ] + }, + { + "type": "dict", + "collapsible": true, + "key": "ExtractPlayblast", + "label": "ExtractPlayblast", + "checkbox_key": "enabled", + "is_group": true, + "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, + { + "type": "boolean", + "key": "optional", + "label": "Optional" + }, + { + "type": "boolean", + "key": "active", + "label": "Active" + }, + { + "type": "raw-json", + "key": "presets", + "label": "Presets" + } + ] + } + ] +} \ No newline at end of file From 13997f4b4cc55c2168c360870593ea561daf1eeb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 4 Aug 2022 12:22:44 +0100 Subject: [PATCH 2/7] Removed thumbnail integrator --- .../blender/plugins/publish/integrate_thumbnail.py | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 openpype/hosts/blender/plugins/publish/integrate_thumbnail.py diff --git a/openpype/hosts/blender/plugins/publish/integrate_thumbnail.py b/openpype/hosts/blender/plugins/publish/integrate_thumbnail.py deleted file mode 100644 index 489db673dea..00000000000 --- a/openpype/hosts/blender/plugins/publish/integrate_thumbnail.py +++ /dev/null @@ -1,10 +0,0 @@ -import pyblish.api -from openpype.plugins.publish import integrate_thumbnail - - -class IntegrateThumbnails(integrate_thumbnail.IntegrateThumbnails): - """Integrate Thumbnails.""" - - label = "Integrate Thumbnails" - order = pyblish.api.IntegratorOrder + 0.01 - families = ["review"] From a380069aad8ff9445666637aac638ae37f04eac4 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 9 Jan 2023 16:23:46 +0000 Subject: [PATCH 3/7] Hound fixes --- openpype/hosts/blender/api/capture.py | 2 +- openpype/hosts/blender/api/lib.py | 1 + openpype/hosts/blender/plugins/publish/collect_review.py | 2 +- openpype/hosts/blender/plugins/publish/extract_playblast.py | 3 +-- openpype/hosts/blender/plugins/publish/extract_thumbnail.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/api/capture.py b/openpype/hosts/blender/api/capture.py index 7cf9e52cb61..849f8ee6298 100644 --- a/openpype/hosts/blender/api/capture.py +++ b/openpype/hosts/blender/api/capture.py @@ -275,4 +275,4 @@ def _independent_window(): try: yield window finally: - bpy.ops.wm.window_close(context) \ No newline at end of file + bpy.ops.wm.window_close(context) diff --git a/openpype/hosts/blender/api/lib.py b/openpype/hosts/blender/api/lib.py index dd3cc48a985..6526f1fb870 100644 --- a/openpype/hosts/blender/api/lib.py +++ b/openpype/hosts/blender/api/lib.py @@ -285,6 +285,7 @@ def maintained_selection(): # context. log.exception("Failed to set active object.") + @contextlib.contextmanager def maintained_time(): """Maintain current frame during context.""" diff --git a/openpype/hosts/blender/plugins/publish/collect_review.py b/openpype/hosts/blender/plugins/publish/collect_review.py index 09b5558d317..d6abd9d967a 100644 --- a/openpype/hosts/blender/plugins/publish/collect_review.py +++ b/openpype/hosts/blender/plugins/publish/collect_review.py @@ -61,4 +61,4 @@ def process(self, instance): "offset": track.offset.get(), "filename": track.filename.get(), } - ) \ No newline at end of file + ) diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py index d07968d4692..1114b633be0 100644 --- a/openpype/hosts/blender/plugins/publish/extract_playblast.py +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -23,7 +23,6 @@ class ExtractPlayblast(openpype.api.Extractor): optional = True order = pyblish.api.ExtractorOrder + 0.01 - def process(self, instance): self.log.info("Extracting capture..") @@ -122,4 +121,4 @@ def process(self, instance): "tags": tags, "camera_name": camera } - instance.data["representations"].append(representation) \ No newline at end of file + instance.data["representations"].append(representation) diff --git a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py index 3fabdf61e08..5e9d876989f 100644 --- a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py @@ -99,4 +99,4 @@ def _fix_output_path(self, filepath): raise RuntimeError(f"Couldn't find playblast from: {filepath}") filepath = max(files, key=os.path.getmtime) - return filepath \ No newline at end of file + return filepath From 1575f24539b134f4e19315529338060c4d6ab00f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Jan 2023 15:51:01 +0000 Subject: [PATCH 4/7] Remove OCIO submodule --- vendor/configs/OpenColorIO-Configs | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/configs/OpenColorIO-Configs diff --git a/vendor/configs/OpenColorIO-Configs b/vendor/configs/OpenColorIO-Configs deleted file mode 160000 index 0bb079c08be..00000000000 --- a/vendor/configs/OpenColorIO-Configs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0bb079c08be410030669cbf5f19ff869b88af953 From 255fd940a1bb2c9cfab2bca9e064a036abf86ab4 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Jan 2023 15:51:28 +0000 Subject: [PATCH 5/7] Fix default settings syntax error --- openpype/settings/defaults/project_settings/blender.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/blender.json b/openpype/settings/defaults/project_settings/blender.json index e0adeb96daf..7eabec61060 100644 --- a/openpype/settings/defaults/project_settings/blender.json +++ b/openpype/settings/defaults/project_settings/blender.json @@ -65,7 +65,7 @@ "enabled": true, "optional": true, "active": false - } + }, "ExtractThumbnail": { "enabled": true, "optional": true, From 1da3854c82e6145e42d2e986263e4dbe7f5d950e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 16 Jan 2023 11:04:22 +0000 Subject: [PATCH 6/7] Implemented suggestions --- .../hosts/blender/plugins/publish/extract_thumbnail.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py index 5e9d876989f..ba455cf450a 100644 --- a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py @@ -21,6 +21,7 @@ class ExtractThumbnail(openpype.api.Extractor): hosts = ["blender"] families = ["review"] order = pyblish.api.ExtractorOrder + 0.01 + presets = {} def process(self, instance): self.log.info("Extracting capture..") @@ -36,11 +37,7 @@ def process(self, instance): family = instance.data.get("family") isolate = instance.data("isolate", None) - project_settings = instance.context.data["project_settings"]["blender"] - extractor_settings = project_settings["publish"]["ExtractThumbnail"] - presets = extractor_settings.get("presets") - - preset = presets.get(family, {}) + preset = self.presets.get(family, {}) preset.update({ "camera": camera, From ea1f10b9db63f21161cb97c6ac984d07d361e7ed Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 20 Feb 2023 12:52:08 +0000 Subject: [PATCH 7/7] Removed references to openpype.api --- openpype/hosts/blender/plugins/publish/extract_playblast.py | 5 ++--- openpype/hosts/blender/plugins/publish/extract_thumbnail.py | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_playblast.py b/openpype/hosts/blender/plugins/publish/extract_playblast.py index 1114b633be0..8dc2f66c22a 100644 --- a/openpype/hosts/blender/plugins/publish/extract_playblast.py +++ b/openpype/hosts/blender/plugins/publish/extract_playblast.py @@ -4,12 +4,12 @@ import bpy import pyblish.api -import openpype.api +from openpype.pipeline import publish from openpype.hosts.blender.api import capture from openpype.hosts.blender.api.lib import maintained_time -class ExtractPlayblast(openpype.api.Extractor): +class ExtractPlayblast(publish.Extractor): """ Extract viewport playblast. @@ -27,7 +27,6 @@ def process(self, instance): self.log.info("Extracting capture..") self.log.info(instance.data) - print(instance.context.data) # get scene fps fps = instance.data.get("fps") diff --git a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py index ba455cf450a..65c3627375e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/blender/plugins/publish/extract_thumbnail.py @@ -2,14 +2,14 @@ import glob import pyblish.api -import openpype.api +from openpype.pipeline import publish from openpype.hosts.blender.api import capture from openpype.hosts.blender.api.lib import maintained_time import bpy -class ExtractThumbnail(openpype.api.Extractor): +class ExtractThumbnail(publish.Extractor): """Extract viewport thumbnail. Takes review camera and creates a thumbnail based on viewport