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.
+
+
+
+- **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.
+
+
+
+- **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.
+
+
+
## Custom Menu
You can add your custom tools menu into Maya by extending definitions in **Maya -> Scripts Menu Definition**.

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