diff --git a/openpype/hosts/hiero/api/lib.py b/openpype/hosts/hiero/api/lib.py index bbd1edc14a..0d4368529f 100644 --- a/openpype/hosts/hiero/api/lib.py +++ b/openpype/hosts/hiero/api/lib.py @@ -1221,7 +1221,7 @@ def set_track_color(track_item, color): def check_inventory_versions(track_items=None): """ - Actual version color idetifier of Loaded containers + Actual version color identifier of Loaded containers Check all track items and filter only Loader nodes for its version. It will get all versions from database @@ -1249,10 +1249,10 @@ def check_inventory_versions(track_items=None): project_name = legacy_io.active_project() filter_result = filter_containers(containers, project_name) for container in filter_result.latest: - set_track_color(container["_item"], clip_color) + set_track_color(container["_item"], clip_color_last) for container in filter_result.outdated: - set_track_color(container["_item"], clip_color_last) + set_track_color(container["_item"], clip_color) def selection_changed_timeline(event): diff --git a/openpype/hosts/max/api/lib.py b/openpype/hosts/max/api/lib.py index 4fb750d91b..ac7d75db08 100644 --- a/openpype/hosts/max/api/lib.py +++ b/openpype/hosts/max/api/lib.py @@ -6,6 +6,11 @@ from pymxs import runtime as rt from typing import Union import contextlib +from openpype.pipeline.context_tools import ( + get_current_project_asset, + get_current_project +) + JSON_PREFIX = "JSON::" @@ -157,6 +162,112 @@ def get_multipass_setting(project_setting=None): ["multipass"]) +def set_scene_resolution(width: int, height: int): + """Set the render resolution + + Args: + width(int): value of the width + height(int): value of the height + + Returns: + None + + """ + rt.renderWidth = width + rt.renderHeight = height + + +def reset_scene_resolution(): + """Apply the scene resolution from the project definition + + scene resolution can be overwritten by an asset if the asset.data contains + any information regarding scene resolution . + Returns: + None + """ + data = ["data.resolutionWidth", "data.resolutionHeight"] + project_resolution = get_current_project(fields=data) + project_resolution_data = project_resolution["data"] + asset_resolution = get_current_project_asset(fields=data) + asset_resolution_data = asset_resolution["data"] + # Set project resolution + project_width = int(project_resolution_data.get("resolutionWidth", 1920)) + project_height = int(project_resolution_data.get("resolutionHeight", 1080)) + width = int(asset_resolution_data.get("resolutionWidth", project_width)) + height = int(asset_resolution_data.get("resolutionHeight", project_height)) + + set_scene_resolution(width, height) + + +def get_frame_range() -> dict: + """Get the current assets frame range and handles. + + Returns: + dict: with frame start, frame end, handle start, handle end. + """ + # Set frame start/end + asset = get_current_project_asset() + frame_start = asset["data"].get("frameStart") + frame_end = asset["data"].get("frameEnd") + # Backwards compatibility + if frame_start is None or frame_end is None: + frame_start = asset["data"].get("edit_in") + frame_end = asset["data"].get("edit_out") + if frame_start is None or frame_end is None: + return + handles = asset["data"].get("handles") or 0 + handle_start = asset["data"].get("handleStart") + if handle_start is None: + handle_start = handles + handle_end = asset["data"].get("handleEnd") + if handle_end is None: + handle_end = handles + return { + "frameStart": frame_start, + "frameEnd": frame_end, + "handleStart": handle_start, + "handleEnd": handle_end + } + + +def reset_frame_range(fps: bool = True): + """Set frame range to current asset. + This is part of 3dsmax documentation: + + animationRange: A System Global variable which lets you get and + set an Interval value that defines the start and end frames + of the Active Time Segment. + frameRate: A System Global variable which lets you get + and set an Integer value that defines the current + scene frame rate in frames-per-second. + """ + if fps: + data_fps = get_current_project(fields=["data.fps"]) + fps_number = float(data_fps["data"]["fps"]) + rt.frameRate = fps_number + frame_range = get_frame_range() + frame_start = frame_range["frameStart"] - int(frame_range["handleStart"]) + frame_end = frame_range["frameEnd"] + int(frame_range["handleEnd"]) + frange_cmd = f"animationRange = interval {frame_start} {frame_end}" + rt.execute(frange_cmd) + + +def set_context_setting(): + """Apply the project settings from the project definition + + Settings can be overwritten by an asset if the asset.data contains + any information regarding those settings. + + Examples of settings: + frame range + resolution + + Returns: + None + """ + reset_scene_resolution() + + def get_max_version(): """ Args: diff --git a/openpype/hosts/max/api/menu.py b/openpype/hosts/max/api/menu.py index 5c273b49b4..066cc90039 100644 --- a/openpype/hosts/max/api/menu.py +++ b/openpype/hosts/max/api/menu.py @@ -4,6 +4,7 @@ from qtpy import QtWidgets, QtCore from pymxs import runtime as rt from openpype.tools.utils import host_tools +from openpype.hosts.max.api import lib class OpenPypeMenu(object): @@ -107,6 +108,17 @@ class OpenPypeMenu(object): workfiles_action = QtWidgets.QAction("Work Files...", openpype_menu) workfiles_action.triggered.connect(self.workfiles_callback) openpype_menu.addAction(workfiles_action) + + openpype_menu.addSeparator() + + res_action = QtWidgets.QAction("Set Resolution", openpype_menu) + res_action.triggered.connect(self.resolution_callback) + openpype_menu.addAction(res_action) + + frame_action = QtWidgets.QAction("Set Frame Range", openpype_menu) + frame_action.triggered.connect(self.frame_range_callback) + openpype_menu.addAction(frame_action) + return openpype_menu def load_callback(self): @@ -128,3 +140,11 @@ class OpenPypeMenu(object): def workfiles_callback(self): """Callback to show Workfiles tool.""" host_tools.show_workfiles(parent=self.main_widget) + + def resolution_callback(self): + """Callback to reset scene resolution""" + return lib.reset_scene_resolution() + + def frame_range_callback(self): + """Callback to reset frame range""" + return lib.reset_frame_range() diff --git a/openpype/hosts/max/api/pipeline.py b/openpype/hosts/max/api/pipeline.py index f8a7b8ea5c..dacc402318 100644 --- a/openpype/hosts/max/api/pipeline.py +++ b/openpype/hosts/max/api/pipeline.py @@ -50,6 +50,11 @@ class MaxHost(HostBase, IWorkfileHost, ILoadHost, INewPublisher): self._has_been_setup = True + def context_setting(): + return lib.set_context_setting() + rt.callbacks.addScript(rt.Name('systemPostNew'), + context_setting) + def has_unsaved_changes(self): # TODO: how to get it from 3dsmax? return True diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 7c9e311c2f..63e4108c84 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -61,7 +61,7 @@ class CollectRender(pyblish.api.InstancePlugin): "plugin": "3dsmax", "frameStart": context.data['frameStart'], "frameEnd": context.data['frameEnd'], - "version": version_int + "version": version_int, } self.log.info("data: {0}".format(data)) instance.data.update(data) diff --git a/openpype/hosts/maya/plugins/create/create_review.py b/openpype/hosts/maya/plugins/create/create_review.py index f1b626c06b..e709239ae7 100644 --- a/openpype/hosts/maya/plugins/create/create_review.py +++ b/openpype/hosts/maya/plugins/create/create_review.py @@ -26,6 +26,7 @@ class CreateReview(plugin.Creator): "alpha cut" ] useMayaTimeline = True + panZoom = False def __init__(self, *args, **kwargs): super(CreateReview, self).__init__(*args, **kwargs) @@ -45,5 +46,6 @@ class CreateReview(plugin.Creator): data["keepImages"] = self.keepImages data["imagePlane"] = self.imagePlane data["transparency"] = self.transparency + data["panZoom"] = self.panZoom self.data = data diff --git a/openpype/hosts/maya/plugins/publish/collect_review.py b/openpype/hosts/maya/plugins/publish/collect_review.py index 548b1c996a..36affe852b 100644 --- a/openpype/hosts/maya/plugins/publish/collect_review.py +++ b/openpype/hosts/maya/plugins/publish/collect_review.py @@ -79,6 +79,8 @@ class CollectReview(pyblish.api.InstancePlugin): data['review_width'] = instance.data['review_width'] data['review_height'] = instance.data['review_height'] data["isolate"] = instance.data["isolate"] + data["panZoom"] = instance.data.get("panZoom", False) + data["panel"] = instance.data["panel"] cmds.setAttr(str(instance) + '.active', 1) self.log.debug('data {}'.format(instance.context[i].data)) instance.context[i].data.update(data) diff --git a/openpype/hosts/maya/plugins/publish/extract_playblast.py b/openpype/hosts/maya/plugins/publish/extract_playblast.py index 94571ff731..72b1489522 100644 --- a/openpype/hosts/maya/plugins/publish/extract_playblast.py +++ b/openpype/hosts/maya/plugins/publish/extract_playblast.py @@ -1,5 +1,6 @@ import os import json +import contextlib import clique import capture @@ -11,6 +12,16 @@ from maya import cmds import pymel.core as pm +@contextlib.contextmanager +def panel_camera(panel, camera): + original_camera = cmds.modelPanel(panel, query=True, camera=True) + try: + cmds.modelPanel(panel, edit=True, camera=camera) + yield + finally: + cmds.modelPanel(panel, edit=True, camera=original_camera) + + class ExtractPlayblast(publish.Extractor): """Extract viewport playblast. @@ -25,6 +36,16 @@ class ExtractPlayblast(publish.Extractor): optional = True capture_preset = {} + def _capture(self, preset): + self.log.info( + "Using preset:\n{}".format( + json.dumps(preset, sort_keys=True, indent=4) + ) + ) + + path = capture.capture(log=self.log, **preset) + self.log.debug("playblast path {}".format(path)) + def process(self, instance): self.log.info("Extracting capture..") @@ -43,7 +64,7 @@ class ExtractPlayblast(publish.Extractor): self.log.info("start: {}, end: {}".format(start, end)) # get cameras - camera = instance.data['review_camera'] + camera = instance.data["review_camera"] preset = lib.load_capture_preset(data=self.capture_preset) # Grab capture presets from the project settings @@ -57,23 +78,23 @@ class ExtractPlayblast(publish.Extractor): asset_height = asset_data.get("resolutionHeight") review_instance_width = instance.data.get("review_width") review_instance_height = instance.data.get("review_height") - preset['camera'] = camera + preset["camera"] = camera # Tests if project resolution is set, # if it is a value other than zero, that value is # used, if not then the asset resolution is # used if review_instance_width and review_instance_height: - preset['width'] = review_instance_width - preset['height'] = review_instance_height + preset["width"] = review_instance_width + preset["height"] = review_instance_height elif width_preset and height_preset: - preset['width'] = width_preset - preset['height'] = height_preset + preset["width"] = width_preset + preset["height"] = height_preset elif asset_width and asset_height: - preset['width'] = asset_width - preset['height'] = asset_height - preset['start_frame'] = start - preset['end_frame'] = end + preset["width"] = asset_width + preset["height"] = asset_height + preset["start_frame"] = start + preset["end_frame"] = end # Enforce persisting camera depth of field camera_options = preset.setdefault("camera_options", {}) @@ -86,8 +107,8 @@ class ExtractPlayblast(publish.Extractor): self.log.info("Outputting images to %s" % path) - preset['filename'] = path - preset['overwrite'] = True + preset["filename"] = path + preset["overwrite"] = True pm.refresh(f=True) @@ -114,7 +135,8 @@ class ExtractPlayblast(publish.Extractor): # Disable Pan/Zoom. pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) - cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), False) + preset.pop("pan_zoom", None) + preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] # Need to explicitly enable some viewport changes so the viewport is # refreshed ahead of playblasting. @@ -136,30 +158,39 @@ class ExtractPlayblast(publish.Extractor): ) override_viewport_options = ( - capture_presets['Viewport Options']['override_viewport_options'] + capture_presets["Viewport Options"]["override_viewport_options"] ) - with lib.maintained_time(): - filename = preset.get("filename", "%TEMP%") - # Force viewer to False in call to capture because we have our own - # viewer opening call to allow a signal to trigger between - # playblast and viewer - preset['viewer'] = False + # Force viewer to False in call to capture because we have our own + # viewer opening call to allow a signal to trigger between + # playblast and viewer + preset["viewer"] = False - # Update preset with current panel setting - # if override_viewport_options is turned off - if not override_viewport_options: - panel_preset = capture.parse_view(instance.data["panel"]) - panel_preset.pop("camera") - preset.update(panel_preset) + # Update preset with current panel setting + # if override_viewport_options is turned off + if not override_viewport_options: + panel_preset = capture.parse_view(instance.data["panel"]) + panel_preset.pop("camera") + preset.update(panel_preset) - self.log.info( - "Using preset:\n{}".format( - json.dumps(preset, sort_keys=True, indent=4) + # Need to ensure Python 2 compatibility. + # TODO: Remove once dropping Python 2. + if getattr(contextlib, "nested", None): + # Python 3 compatibility. + with contextlib.nested( + lib.maintained_time(), + panel_camera(instance.data["panel"], preset["camera"]) + ): + self._capture(preset) + else: + # Python 2 compatibility. + with contextlib.ExitStack() as stack: + stack.enter_context(lib.maintained_time()) + stack.enter_context( + panel_camera(instance.data["panel"], preset["camera"]) ) - ) - path = capture.capture(log=self.log, **preset) + self._capture(preset) # Restoring viewport options. if viewport_defaults: @@ -169,18 +200,17 @@ class ExtractPlayblast(publish.Extractor): cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom) - self.log.debug("playblast path {}".format(path)) - collected_files = os.listdir(stagingdir) patterns = [clique.PATTERNS["frames"]] collections, remainder = clique.assemble(collected_files, minimum_items=1, patterns=patterns) + filename = preset.get("filename", "%TEMP%") self.log.debug("filename {}".format(filename)) frame_collection = None for collection in collections: - filebase = collection.format('{head}').rstrip(".") + filebase = collection.format("{head}").rstrip(".") self.log.debug("collection head {}".format(filebase)) if filebase in filename: frame_collection = collection @@ -204,15 +234,15 @@ class ExtractPlayblast(publish.Extractor): collected_files = collected_files[0] representation = { - 'name': 'png', - 'ext': 'png', - 'files': collected_files, + "name": self.capture_preset["Codec"]["compression"], + "ext": self.capture_preset["Codec"]["compression"], + "files": collected_files, "stagingDir": stagingdir, "frameStart": start, "frameEnd": end, - 'fps': fps, - 'preview': True, - 'tags': tags, - 'camera_name': camera_node_name + "fps": fps, + "preview": True, + "tags": tags, + "camera_name": camera_node_name } instance.data["representations"].append(representation) diff --git a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py index 1d94bd58c5..f2d084b828 100644 --- a/openpype/hosts/maya/plugins/publish/extract_thumbnail.py +++ b/openpype/hosts/maya/plugins/publish/extract_thumbnail.py @@ -26,28 +26,28 @@ class ExtractThumbnail(publish.Extractor): def process(self, instance): self.log.info("Extracting capture..") - camera = instance.data['review_camera'] + camera = instance.data["review_camera"] - capture_preset = ( - instance.context.data["project_settings"]['maya']['publish']['ExtractPlayblast']['capture_preset'] - ) + maya_setting = instance.context.data["project_settings"]["maya"] + plugin_setting = maya_setting["publish"]["ExtractPlayblast"] + capture_preset = plugin_setting["capture_preset"] override_viewport_options = ( - capture_preset['Viewport Options']['override_viewport_options'] + capture_preset["Viewport Options"]["override_viewport_options"] ) try: preset = lib.load_capture_preset(data=capture_preset) except KeyError as ke: - self.log.error('Error loading capture presets: {}'.format(str(ke))) + self.log.error("Error loading capture presets: {}".format(str(ke))) preset = {} - self.log.info('Using viewport preset: {}'.format(preset)) + self.log.info("Using viewport preset: {}".format(preset)) # preset["off_screen"] = False - preset['camera'] = camera - preset['start_frame'] = instance.data["frameStart"] - preset['end_frame'] = instance.data["frameStart"] - preset['camera_options'] = { + preset["camera"] = camera + preset["start_frame"] = instance.data["frameStart"] + preset["end_frame"] = instance.data["frameStart"] + preset["camera_options"] = { "displayGateMask": False, "displayResolution": False, "displayFilmGate": False, @@ -74,14 +74,14 @@ class ExtractThumbnail(publish.Extractor): # used, if not then the asset resolution is # used if review_instance_width and review_instance_height: - preset['width'] = review_instance_width - preset['height'] = review_instance_height + preset["width"] = review_instance_width + preset["height"] = review_instance_height elif width_preset and height_preset: - preset['width'] = width_preset - preset['height'] = height_preset + preset["width"] = width_preset + preset["height"] = height_preset elif asset_width and asset_height: - preset['width'] = asset_width - preset['height'] = asset_height + preset["width"] = asset_width + preset["height"] = asset_height # Create temp directory for thumbnail # - this is to avoid "override" of source file @@ -96,8 +96,8 @@ class ExtractThumbnail(publish.Extractor): self.log.info("Outputting images to %s" % path) - preset['filename'] = path - preset['overwrite'] = True + preset["filename"] = path + preset["overwrite"] = True pm.refresh(f=True) @@ -123,14 +123,14 @@ class ExtractThumbnail(publish.Extractor): preset["viewport_options"] = {"imagePlane": image_plane} # Disable Pan/Zoom. - pan_zoom = cmds.getAttr("{}.panZoomEnabled".format(preset["camera"])) - cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), False) + preset.pop("pan_zoom", None) + preset["camera_options"]["panZoomEnabled"] = instance.data["panZoom"] with lib.maintained_time(): # Force viewer to False in call to capture because we have our own # viewer opening call to allow a signal to trigger between # playblast and viewer - preset['viewer'] = False + preset["viewer"] = False # Update preset with current panel setting # if override_viewport_options is turned off @@ -145,17 +145,15 @@ class ExtractThumbnail(publish.Extractor): _, thumbnail = os.path.split(playblast) - cmds.setAttr("{}.panZoomEnabled".format(preset["camera"]), pan_zoom) - self.log.info("file list {}".format(thumbnail)) if "representations" not in instance.data: instance.data["representations"] = [] representation = { - 'name': 'thumbnail', - 'ext': 'jpg', - 'files': thumbnail, + "name": "thumbnail", + "ext": "jpg", + "files": thumbnail, "stagingDir": dst_staging, "thumbnail": True } diff --git a/openpype/hosts/nuke/plugins/load/load_backdrop.py b/openpype/hosts/nuke/plugins/load/load_backdrop.py index f227aa161a..67c7877e60 100644 --- a/openpype/hosts/nuke/plugins/load/load_backdrop.py +++ b/openpype/hosts/nuke/plugins/load/load_backdrop.py @@ -54,22 +54,19 @@ class LoadBackdropNodes(load.LoaderPlugin): version = context['version'] version_data = version.get("data", {}) vname = version.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) namespace = namespace or context['asset']['name'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) # prepare data for imprinting # add additional metadata from the version to imprint to Avalon knob - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] + add_keys = ["source", "author", "fps"] - data_imprint = {"frameStart": first, - "frameEnd": last, - "version": vname, - "colorspaceInput": colorspace, - "objectName": object_name} + data_imprint = { + "version": vname, + "colorspaceInput": colorspace, + "objectName": object_name + } for k in add_keys: data_imprint.update({k: version_data[k]}) @@ -204,18 +201,13 @@ class LoadBackdropNodes(load.LoaderPlugin): name = container['name'] version_data = version_doc.get("data", {}) vname = version_doc.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) namespace = container['namespace'] colorspace = version_data.get("colorspace", None) object_name = "{}_{}".format(name, namespace) - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] + add_keys = ["source", "author", "fps"] data_imprint = {"representation": str(representation["_id"]), - "frameStart": first, - "frameEnd": last, "version": vname, "colorspaceInput": colorspace, "objectName": object_name} diff --git a/openpype/hosts/nuke/plugins/publish/collect_backdrop.py b/openpype/hosts/nuke/plugins/publish/collect_backdrop.py index 8eaefa6854..7d51af7e9e 100644 --- a/openpype/hosts/nuke/plugins/publish/collect_backdrop.py +++ b/openpype/hosts/nuke/plugins/publish/collect_backdrop.py @@ -51,38 +51,10 @@ class CollectBackdrops(pyblish.api.InstancePlugin): instance.data["label"] = "{0} ({1} nodes)".format( bckn.name(), len(instance.data["transientData"]["childNodes"])) - instance.data["families"].append(instance.data["family"]) - - # Get frame range - handle_start = instance.context.data["handleStart"] - handle_end = instance.context.data["handleEnd"] - first_frame = int(nuke.root()["first_frame"].getValue()) - last_frame = int(nuke.root()["last_frame"].getValue()) - # get version version = instance.context.data.get('version') - if not version: - raise RuntimeError("Script name has no version in the name.") + if version: + instance.data['version'] = version - instance.data['version'] = version - - # Add version data to instance - version_data = { - "handles": handle_start, - "handleStart": handle_start, - "handleEnd": handle_end, - "frameStart": first_frame + handle_start, - "frameEnd": last_frame - handle_end, - "version": int(version), - "families": [instance.data["family"]] + instance.data["families"], - "subset": instance.data["subset"], - "fps": instance.context.data["fps"] - } - - instance.data.update({ - "versionData": version_data, - "frameStart": first_frame, - "frameEnd": last_frame - }) self.log.info("Backdrop instance collected: `{}`".format(instance)) diff --git a/openpype/hosts/tvpaint/plugins/create/convert_legacy.py b/openpype/hosts/tvpaint/plugins/create/convert_legacy.py index 538c6e4c5e..5cfa1faa50 100644 --- a/openpype/hosts/tvpaint/plugins/create/convert_legacy.py +++ b/openpype/hosts/tvpaint/plugins/create/convert_legacy.py @@ -55,7 +55,7 @@ class TVPaintLegacyConverted(SubsetConvertorPlugin): self._convert_render_layers( to_convert["renderLayer"], current_instances) self._convert_render_passes( - to_convert["renderpass"], current_instances) + to_convert["renderPass"], current_instances) self._convert_render_scenes( to_convert["renderScene"], current_instances) self._convert_workfiles( @@ -116,7 +116,7 @@ class TVPaintLegacyConverted(SubsetConvertorPlugin): render_layers_by_group_id = {} for instance in current_instances: if instance.get("creator_identifier") == "render.layer": - group_id = instance["creator_identifier"]["group_id"] + group_id = instance["creator_attributes"]["group_id"] render_layers_by_group_id[group_id] = instance for render_pass in render_passes: diff --git a/openpype/hosts/tvpaint/plugins/create/create_render.py b/openpype/hosts/tvpaint/plugins/create/create_render.py index 9711024c79..2369c7329f 100644 --- a/openpype/hosts/tvpaint/plugins/create/create_render.py +++ b/openpype/hosts/tvpaint/plugins/create/create_render.py @@ -415,11 +415,11 @@ class CreateRenderPass(TVPaintCreator): .get("creator_attributes", {}) .get("render_layer_instance_id") ) - render_layer_info = render_layers.get(render_layer_instance_id) + render_layer_info = render_layers.get(render_layer_instance_id, {}) self.update_instance_labels( instance_data, - render_layer_info["variant"], - render_layer_info["template_data"] + render_layer_info.get("variant"), + render_layer_info.get("template_data") ) instance = CreatedInstance.from_existing(instance_data, self) self._add_instance_to_context(instance) @@ -607,11 +607,11 @@ class CreateRenderPass(TVPaintCreator): current_instances = self.host.list_instances() render_layers = [ { - "value": instance["instance_id"], - "label": instance["subset"] + "value": inst["instance_id"], + "label": inst["subset"] } - for instance in current_instances - if instance["creator_identifier"] == CreateRenderlayer.identifier + for inst in current_instances + if inst.get("creator_identifier") == CreateRenderlayer.identifier ] if not render_layers: render_layers.append({"value": None, "label": "N/A"}) @@ -697,6 +697,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): ["create"] ["auto_detect_render"] ) + self.enabled = plugin_settings.get("enabled", False) self.allow_group_rename = plugin_settings["allow_group_rename"] self.group_name_template = plugin_settings["group_name_template"] self.group_idx_offset = plugin_settings["group_idx_offset"] diff --git a/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py b/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py index 5eb702a1da..63f04cf3ce 100644 --- a/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py +++ b/openpype/hosts/tvpaint/plugins/publish/collect_instance_frames.py @@ -22,9 +22,11 @@ class CollectOutputFrameRange(pyblish.api.InstancePlugin): context = instance.context frame_start = asset_doc["data"]["frameStart"] + fps = asset_doc["data"]["fps"] frame_end = frame_start + ( context.data["sceneMarkOut"] - context.data["sceneMarkIn"] ) + instance.data["fps"] = fps instance.data["frameStart"] = frame_start instance.data["frameEnd"] = frame_end self.log.info( diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py index 7e35726030..d7984ce971 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_asset_name.py @@ -1,5 +1,8 @@ import pyblish.api -from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin, +) from openpype.hosts.tvpaint.api.pipeline import ( list_instances, write_instances, @@ -31,7 +34,10 @@ class FixAssetNames(pyblish.api.Action): write_instances(new_instance_items) -class ValidateAssetNames(pyblish.api.ContextPlugin): +class ValidateAssetName( + OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin +): """Validate assset name present on instance. Asset name on instance should be the same as context's. @@ -43,6 +49,8 @@ class ValidateAssetNames(pyblish.api.ContextPlugin): actions = [FixAssetNames] def process(self, context): + if not self.is_active(context.data): + return context_asset_name = context.data["asset"] for instance in context: asset_name = instance.data.get("asset") diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py index 6a496a2e49..8e52a636f4 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_layers_visibility.py @@ -11,7 +11,7 @@ class ValidateLayersVisiblity(pyblish.api.InstancePlugin): families = ["review", "render"] def process(self, instance): - layers = instance.data["layers"] + layers = instance.data.get("layers") # Instance have empty layers # - it is not job of this validator to check that if not layers: diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py index 0030b0fd1c..7b2cc62bb5 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_marks.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_marks.py @@ -1,7 +1,10 @@ import json import pyblish.api -from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin, +) from openpype.hosts.tvpaint.api.lib import execute_george @@ -23,7 +26,10 @@ class ValidateMarksRepair(pyblish.api.Action): ) -class ValidateMarks(pyblish.api.ContextPlugin): +class ValidateMarks( + OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin +): """Validate mark in and out are enabled and it's duration. Mark In/Out does not have to match frameStart and frameEnd but duration is @@ -59,6 +65,9 @@ class ValidateMarks(pyblish.api.ContextPlugin): } def process(self, context): + if not self.is_active(context.data): + return + current_data = { "markIn": context.data["sceneMarkIn"], "markInState": context.data["sceneMarkInState"], diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py b/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py index 4473e4b1b7..0ab8e811f5 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_scene_settings.py @@ -1,11 +1,17 @@ import json import pyblish.api -from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin, +) # TODO @iLliCiTiT add fix action for fps -class ValidateProjectSettings(pyblish.api.ContextPlugin): +class ValidateProjectSettings( + OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin +): """Validate scene settings against database.""" label = "Validate Scene Settings" @@ -13,6 +19,9 @@ class ValidateProjectSettings(pyblish.api.ContextPlugin): optional = True def process(self, context): + if not self.is_active(context.data): + return + expected_data = context.data["assetEntity"]["data"] scene_data = { "fps": context.data.get("sceneFps"), diff --git a/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py index 066e54c670..229ccfcd18 100644 --- a/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py +++ b/openpype/hosts/tvpaint/plugins/publish/validate_start_frame.py @@ -1,5 +1,8 @@ import pyblish.api -from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin, +) from openpype.hosts.tvpaint.api.lib import execute_george @@ -14,7 +17,10 @@ class RepairStartFrame(pyblish.api.Action): execute_george("tv_startframe 0") -class ValidateStartFrame(pyblish.api.ContextPlugin): +class ValidateStartFrame( + OptionalPyblishPluginMixin, + pyblish.api.ContextPlugin +): """Validate start frame being at frame 0.""" label = "Validate Start Frame" @@ -24,6 +30,9 @@ class ValidateStartFrame(pyblish.api.ContextPlugin): optional = True def process(self, context): + if not self.is_active(context.data): + return + start_frame = execute_george("tv_startframe") if start_frame == 0: return diff --git a/openpype/modules/deadline/plugins/publish/collect_pools.py b/openpype/modules/deadline/plugins/publish/collect_pools.py index 48130848d5..e221eb00ea 100644 --- a/openpype/modules/deadline/plugins/publish/collect_pools.py +++ b/openpype/modules/deadline/plugins/publish/collect_pools.py @@ -3,21 +3,60 @@ """ import pyblish.api +from openpype.lib import TextDef +from openpype.pipeline.publish import OpenPypePyblishPluginMixin -class CollectDeadlinePools(pyblish.api.InstancePlugin): +class CollectDeadlinePools(pyblish.api.InstancePlugin, + OpenPypePyblishPluginMixin): """Collect pools from instance if present, from Setting otherwise.""" order = pyblish.api.CollectorOrder + 0.420 label = "Collect Deadline Pools" - families = ["rendering", "render.farm", "renderFarm", "renderlayer"] + families = ["rendering", + "render.farm", + "renderFarm", + "renderlayer", + "maxrender"] primary_pool = None secondary_pool = None + @classmethod + def apply_settings(cls, project_settings, system_settings): + # deadline.publish.CollectDeadlinePools + settings = project_settings["deadline"]["publish"]["CollectDeadlinePools"] # noqa + cls.primary_pool = settings.get("primary_pool", None) + cls.secondary_pool = settings.get("secondary_pool", None) + def process(self, instance): + + attr_values = self.get_attr_values_from_data(instance.data) if not instance.data.get("primaryPool"): - instance.data["primaryPool"] = self.primary_pool or "none" + instance.data["primaryPool"] = ( + attr_values.get("primaryPool") or self.primary_pool or "none" + ) if not instance.data.get("secondaryPool"): - instance.data["secondaryPool"] = self.secondary_pool or "none" + instance.data["secondaryPool"] = ( + attr_values.get("secondaryPool") or self.secondary_pool or "none" # noqa + ) + + @classmethod + def get_attribute_defs(cls): + # TODO: Preferably this would be an enum for the user + # but the Deadline server URL can be dynamic and + # can be set per render instance. Since get_attribute_defs + # can't be dynamic unfortunately EnumDef isn't possible (yet?) + # pool_names = self.deadline_module.get_deadline_pools(deadline_url, + # self.log) + # secondary_pool_names = ["-"] + pool_names + + return [ + TextDef("primaryPool", + label="Primary Pool", + default=cls.primary_pool), + TextDef("secondaryPool", + label="Secondary Pool", + default=cls.secondary_pool) + ] diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index 417a03de74..c728b6b9c7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -3,7 +3,15 @@ import getpass import copy import attr -from openpype.pipeline import legacy_io +from openpype.lib import ( + TextDef, + BoolDef, + NumberDef, +) +from openpype.pipeline import ( + legacy_io, + OpenPypePyblishPluginMixin +) from openpype.settings import get_project_settings from openpype.hosts.max.api.lib import ( get_current_renderer, @@ -22,7 +30,8 @@ class MaxPluginInfo(object): IgnoreInputs = attr.ib(default=True) -class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): +class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, + OpenPypePyblishPluginMixin): label = "Submit Render to Deadline" hosts = ["max"] @@ -31,14 +40,22 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): use_published = True priority = 50 - tile_priority = 50 chunk_size = 1 jobInfo = {} pluginInfo = {} group = None - deadline_pool = None - deadline_pool_secondary = None - framePerTask = 1 + + @classmethod + def apply_settings(cls, project_settings, system_settings): + settings = project_settings["deadline"]["publish"]["MaxSubmitDeadline"] # noqa + + # Take some defaults from settings + cls.use_published = settings.get("use_published", + cls.use_published) + cls.priority = settings.get("priority", + cls.priority) + cls.chuck_size = settings.get("chunk_size", cls.chunk_size) + cls.group = settings.get("group", cls.group) def get_job_info(self): job_info = DeadlineJobInfo(Plugin="3dsmax") @@ -49,11 +66,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): 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) @@ -71,13 +88,13 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): job_info.Pool = instance.data.get("primaryPool") job_info.SecondaryPool = instance.data.get("secondaryPool") - job_info.ChunkSize = instance.data.get("chunkSize", 1) - job_info.Comment = context.data.get("comment") - job_info.Priority = instance.data.get("priority", self.priority) - job_info.FramesPerTask = instance.data.get("framesPerTask", 1) - if self.group: - job_info.Group = self.group + attr_values = self.get_attr_values_from_data(instance.data) + + job_info.ChunkSize = attr_values.get("chunkSize", 1) + job_info.Comment = context.data.get("comment") + job_info.Priority = attr_values.get("priority", self.priority) + job_info.Group = attr_values.get("group", self.group) # Add options from RenderGlobals render_globals = instance.data.get("renderGlobals", {}) @@ -216,3 +233,32 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline): plugin_info.update(plugin_data) return job_info, plugin_info + + @classmethod + def get_attribute_defs(cls): + defs = super(MaxSubmitDeadline, cls).get_attribute_defs() + defs.extend([ + BoolDef("use_published", + default=cls.use_published, + label="Use Published Scene"), + + NumberDef("priority", + minimum=1, + maximum=250, + decimals=0, + default=cls.priority, + label="Priority"), + + NumberDef("chunkSize", + minimum=1, + maximum=50, + decimals=0, + default=cls.chunk_size, + label="Frame Per Task"), + + TextDef("group", + default=cls.group, + label="Group Name"), + ]) + + return defs diff --git a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py index 05afa5080d..7c8ab62d4d 100644 --- a/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py +++ b/openpype/modules/deadline/plugins/publish/validate_deadline_pools.py @@ -17,7 +17,11 @@ class ValidateDeadlinePools(OptionalPyblishPluginMixin, label = "Validate Deadline Pools" order = pyblish.api.ValidatorOrder - families = ["rendering", "render.farm", "renderFarm", "renderlayer"] + families = ["rendering", + "render.farm", + "renderFarm", + "renderlayer", + "maxrender"] optional = True def process(self, instance): diff --git a/openpype/settings/defaults/project_settings/deadline.json b/openpype/settings/defaults/project_settings/deadline.json index 0cbd323299..fdd70f1a44 100644 --- a/openpype/settings/defaults/project_settings/deadline.json +++ b/openpype/settings/defaults/project_settings/deadline.json @@ -43,10 +43,7 @@ "use_published": true, "priority": 50, "chunk_size": 10, - "group": "none", - "deadline_pool": "", - "deadline_pool_secondary": "", - "framePerTask": 1 + "group": "none" }, "NukeSubmitDeadline": { "enabled": true, diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 2aa95fd1be..e914eb29f9 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -790,7 +790,7 @@ "ExtractPlayblast": { "capture_preset": { "Codec": { - "compression": "jpg", + "compression": "png", "format": "image", "quality": 95 }, @@ -817,7 +817,8 @@ }, "Generic": { "isolate_view": true, - "off_screen": true + "off_screen": true, + "pan_zoom": false }, "Renderer": { "rendererName": "vp2Renderer" diff --git a/openpype/settings/defaults/project_settings/tvpaint.json b/openpype/settings/defaults/project_settings/tvpaint.json index 9173a8c3d5..1671748e97 100644 --- a/openpype/settings/defaults/project_settings/tvpaint.json +++ b/openpype/settings/defaults/project_settings/tvpaint.json @@ -42,6 +42,7 @@ "default_variants": [] }, "auto_detect_render": { + "enabled": false, "allow_group_rename": true, "group_name_template": "L{group_index}", "group_idx_offset": 10, diff --git a/openpype/settings/defaults/system_settings/applications.json b/openpype/settings/defaults/system_settings/applications.json index 5fd9b926fb..eb3a88ce66 100644 --- a/openpype/settings/defaults/system_settings/applications.json +++ b/openpype/settings/defaults/system_settings/applications.json @@ -133,7 +133,7 @@ "linux": [] }, "arguments": { - "windows": [], + "windows": ["-U MAXScript {OPENPYPE_ROOT}\\openpype\\hosts\\max\\startup\\startup.ms"], "darwin": [], "linux": [] }, 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 9906939cc7..d8b5e4dc1f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_deadline.json @@ -239,27 +239,12 @@ { "type": "number", "key": "chunk_size", - "label": "Chunk Size" + "label": "Frame per Task" }, { "type": "text", "key": "group", "label": "Group Name" - }, - { - "type": "text", - "key": "deadline_pool", - "label": "Deadline pool" - }, - { - "type": "text", - "key": "deadline_pool_secondary", - "label": "Deadline pool (secondary)" - }, - { - "type": "number", - "key": "framePerTask", - "label": "Frame Per Task" } ] }, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json index 708b688ba5..1094595851 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_tvpaint.json @@ -202,7 +202,13 @@ "key": "auto_detect_render", "label": "Auto-Detect Create Render", "is_group": true, + "checkbox_key": "enabled", "children": [ + { + "type": "boolean", + "key": "enabled", + "label": "Enabled" + }, { "type": "label", "label": "The creator tries to auto-detect Render Layers and Render Passes in scene. For Render Layers is used group name as a variant and for Render Passes is used TVPaint layer name.

Group names can be renamed by their used order in scene. The renaming template where can be used {group_index} formatting key which is filled by \"used position index of group\".
- Template: L{group_index}
- Group offset: 10
- Group padding: 3
Would create group names \"L010\", \"L020\", ..." diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json index 1f0e4eeffb..dec5a5cdc2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_maya_capture.json @@ -91,6 +91,11 @@ "type": "boolean", "key": "off_screen", "label": " Off Screen" + }, + { + "type": "boolean", + "key": "pan_zoom", + "label": " 2D Pan/Zoom" } ] }, @@ -156,7 +161,7 @@ { "type": "boolean", "key": "override_viewport_options", - "label": "override_viewport_options" + "label": "Override Viewport Options" }, { "type": "enum", diff --git a/openpype/tools/project_manager/project_manager/view.py b/openpype/tools/project_manager/project_manager/view.py index b35491c5b2..2cf11b702d 100644 --- a/openpype/tools/project_manager/project_manager/view.py +++ b/openpype/tools/project_manager/project_manager/view.py @@ -72,8 +72,8 @@ class HierarchyView(QtWidgets.QTreeView): column_delegate_defs = { "name": NameDef(), "type": TypeDef(), - "frameStart": NumberDef(1), - "frameEnd": NumberDef(1), + "frameStart": NumberDef(0), + "frameEnd": NumberDef(0), "fps": NumberDef(1, decimals=3, step=1), "resolutionWidth": NumberDef(0), "resolutionHeight": NumberDef(0), diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 23cacb4193..68b402c5e0 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -110,6 +110,41 @@ or Deadlines **Draft Tile Assembler**. This is useful to fix some specific renderer glitches and advanced hacking of Maya Scene files. `Patch name` is label for patch for easier orientation. `Patch regex` is regex used to find line in file, after `Patch line` string is inserted. Note that you need to add line ending. +### Extract Playblast Settings (review) +These settings provide granular control over how the playblasts or reviews are produced in Maya. + +Some of these settings are also available on the instance itself, in which case these settings will become the default value when creating the review instance. + +![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings.png) + +- **Compression type** which file encoding to use. +- **Data format** what format is the file encoding. +- **Quality** lets you control the compression value for the output. Results can vary depending on the compression you selected. Quality values can range from 0 to 100, with a default value of 95. +- **Background Color** the viewports background color. +- **Background Bottom** the viewports background bottom color. +- **Background Top** the viewports background top color. +- **Override display options** override the viewports display options to use what is set in the settings. +- **Isolate view** isolates the view to what is in the review instance. If only a camera is present in the review instance, all nodes are displayed in view. +- **Off Screen** records the playblast hidden from the user. +- **2D Pan/Zoom** enables the 2D Pan/Zoom functionality of the camera. +- **Renderer name** which renderer to use for playblasting. +- **Width** width of the output resolution. If this value is `0`, the asset's width is used. +- **Height** height of the output resolution. If this value is `0`, the asset's height is used. + +#### Viewport Options + +Most settings to override in the viewport are self explanatory and can be found in Maya. + +![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings_viewport_options.png) + +- **Override Viewport Options** enable to use the settings below for the viewport when publishing the review. + +#### Camera Options + +These options are set on the camera shape when publishing the review. They correspond to attributes on the Maya camera shape node. + +![Extract Playblast Settings](assets/maya-admin_extract_playblast_settings_camera_options.png) + ## Custom Menu You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**. ![Custom menu definition](assets/maya-admin_scriptsmenu.png) diff --git a/website/docs/assets/maya-admin_extract_playblast_settings.png b/website/docs/assets/maya-admin_extract_playblast_settings.png new file mode 100644 index 0000000000..9312e30321 Binary files /dev/null and b/website/docs/assets/maya-admin_extract_playblast_settings.png differ diff --git a/website/docs/assets/maya-admin_extract_playblast_settings_camera_options.png b/website/docs/assets/maya-admin_extract_playblast_settings_camera_options.png new file mode 100644 index 0000000000..1a5561328e Binary files /dev/null and b/website/docs/assets/maya-admin_extract_playblast_settings_camera_options.png differ diff --git a/website/docs/assets/maya-admin_extract_playblast_settings_viewport_options.png b/website/docs/assets/maya-admin_extract_playblast_settings_viewport_options.png new file mode 100644 index 0000000000..5b801775d3 Binary files /dev/null and b/website/docs/assets/maya-admin_extract_playblast_settings_viewport_options.png differ