mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #5001 from ynput/feature/OP-5871_Max-Review-feature
This commit is contained in:
commit
6280dd1e16
11 changed files with 413 additions and 4 deletions
|
|
@ -284,6 +284,21 @@ def get_max_version():
|
|||
return max_info[7]
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def viewport_camera(camera):
|
||||
original = rt.viewport.getCamera()
|
||||
if not original:
|
||||
# if there is no original camera
|
||||
# use the current camera as original
|
||||
original = rt.getNodeByName(camera)
|
||||
review_camera = rt.getNodeByName(camera)
|
||||
try:
|
||||
rt.viewport.setCamera(review_camera)
|
||||
yield
|
||||
finally:
|
||||
rt.viewport.setCamera(original)
|
||||
|
||||
|
||||
def set_timeline(frameStart, frameEnd):
|
||||
"""Set frame range for timeline editor in Max
|
||||
"""
|
||||
|
|
|
|||
57
openpype/hosts/max/plugins/create/create_review.py
Normal file
57
openpype/hosts/max/plugins/create/create_review.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating review in Max."""
|
||||
from openpype.hosts.max.api import plugin
|
||||
from openpype.lib import BoolDef, EnumDef, NumberDef
|
||||
|
||||
|
||||
class CreateReview(plugin.MaxCreator):
|
||||
"""Review in 3dsMax"""
|
||||
|
||||
identifier = "io.openpype.creators.max.review"
|
||||
label = "Review"
|
||||
family = "review"
|
||||
icon = "video-camera"
|
||||
|
||||
def create(self, subset_name, instance_data, pre_create_data):
|
||||
|
||||
instance_data["imageFormat"] = pre_create_data.get("imageFormat")
|
||||
instance_data["keepImages"] = pre_create_data.get("keepImages")
|
||||
instance_data["percentSize"] = pre_create_data.get("percentSize")
|
||||
instance_data["rndLevel"] = pre_create_data.get("rndLevel")
|
||||
|
||||
super(CreateReview, self).create(
|
||||
subset_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
attrs = super(CreateReview, self).get_pre_create_attr_defs()
|
||||
|
||||
image_format_enum = [
|
||||
"bmp", "cin", "exr", "jpg", "hdr", "rgb", "png",
|
||||
"rla", "rpf", "dds", "sgi", "tga", "tif", "vrimg"
|
||||
]
|
||||
|
||||
rndLevel_enum = [
|
||||
"smoothhighlights", "smooth", "facethighlights",
|
||||
"facet", "flat", "litwireframe", "wireframe", "box"
|
||||
]
|
||||
|
||||
return attrs + [
|
||||
BoolDef("keepImages",
|
||||
label="Keep Image Sequences",
|
||||
default=False),
|
||||
EnumDef("imageFormat",
|
||||
image_format_enum,
|
||||
default="png",
|
||||
label="Image Format Options"),
|
||||
NumberDef("percentSize",
|
||||
label="Percent of Output",
|
||||
default=100,
|
||||
minimum=1,
|
||||
decimals=0),
|
||||
EnumDef("rndLevel",
|
||||
rndLevel_enum,
|
||||
default="smoothhighlights",
|
||||
label="Preference")
|
||||
]
|
||||
92
openpype/hosts/max/plugins/publish/collect_review.py
Normal file
92
openpype/hosts/max/plugins/publish/collect_review.py
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# dont forget getting the focal length for burnin
|
||||
"""Collect Review"""
|
||||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from openpype.lib import BoolDef
|
||||
from openpype.pipeline.publish import OpenPypePyblishPluginMixin
|
||||
|
||||
|
||||
class CollectReview(pyblish.api.InstancePlugin,
|
||||
OpenPypePyblishPluginMixin):
|
||||
"""Collect Review Data for Preview Animation"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.02
|
||||
label = "Collect Review Data"
|
||||
hosts = ['max']
|
||||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
nodes = instance.data["members"]
|
||||
focal_length = None
|
||||
camera_name = None
|
||||
for node in nodes:
|
||||
if rt.classOf(node) in rt.Camera.classes:
|
||||
camera_name = node.name
|
||||
focal_length = node.fov
|
||||
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
data = {
|
||||
"review_camera": camera_name,
|
||||
"frameStart": instance.context.data["frameStart"],
|
||||
"frameEnd": instance.context.data["frameEnd"],
|
||||
"fps": instance.context.data["fps"],
|
||||
"dspGeometry": attr_values.get("dspGeometry"),
|
||||
"dspShapes": attr_values.get("dspShapes"),
|
||||
"dspLights": attr_values.get("dspLights"),
|
||||
"dspCameras": attr_values.get("dspCameras"),
|
||||
"dspHelpers": attr_values.get("dspHelpers"),
|
||||
"dspParticles": attr_values.get("dspParticles"),
|
||||
"dspBones": attr_values.get("dspBones"),
|
||||
"dspBkg": attr_values.get("dspBkg"),
|
||||
"dspGrid": attr_values.get("dspGrid"),
|
||||
"dspSafeFrame": attr_values.get("dspSafeFrame"),
|
||||
"dspFrameNums": attr_values.get("dspFrameNums")
|
||||
}
|
||||
# Enable ftrack functionality
|
||||
instance.data.setdefault("families", []).append('ftrack')
|
||||
|
||||
burnin_members = instance.data.setdefault("burninDataMembers", {})
|
||||
burnin_members["focalLength"] = focal_length
|
||||
|
||||
self.log.debug(f"data:{data}")
|
||||
instance.data.update(data)
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
|
||||
return [
|
||||
BoolDef("dspGeometry",
|
||||
label="Geometry",
|
||||
default=True),
|
||||
BoolDef("dspShapes",
|
||||
label="Shapes",
|
||||
default=False),
|
||||
BoolDef("dspLights",
|
||||
label="Lights",
|
||||
default=False),
|
||||
BoolDef("dspCameras",
|
||||
label="Cameras",
|
||||
default=False),
|
||||
BoolDef("dspHelpers",
|
||||
label="Helpers",
|
||||
default=False),
|
||||
BoolDef("dspParticles",
|
||||
label="Particle Systems",
|
||||
default=True),
|
||||
BoolDef("dspBones",
|
||||
label="Bone Objects",
|
||||
default=False),
|
||||
BoolDef("dspBkg",
|
||||
label="Background",
|
||||
default=True),
|
||||
BoolDef("dspGrid",
|
||||
label="Active Grid",
|
||||
default=False),
|
||||
BoolDef("dspSafeFrame",
|
||||
label="Safe Frames",
|
||||
default=False),
|
||||
BoolDef("dspFrameNums",
|
||||
label="Frame Numbers",
|
||||
default=False)
|
||||
]
|
||||
102
openpype/hosts/max/plugins/publish/extract_review_animation.py
Normal file
102
openpype/hosts/max/plugins/publish/extract_review_animation.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.max.api.lib import viewport_camera, get_max_version
|
||||
|
||||
|
||||
class ExtractReviewAnimation(publish.Extractor):
|
||||
"""
|
||||
Extract Review by Review Animation
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder + 0.001
|
||||
label = "Extract Review Animation"
|
||||
hosts = ["max"]
|
||||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
staging_dir = self.staging_dir(instance)
|
||||
ext = instance.data.get("imageFormat")
|
||||
filename = "{0}..{1}".format(instance.name, ext)
|
||||
start = int(instance.data["frameStart"])
|
||||
end = int(instance.data["frameEnd"])
|
||||
fps = int(instance.data["fps"])
|
||||
filepath = os.path.join(staging_dir, filename)
|
||||
filepath = filepath.replace("\\", "/")
|
||||
filenames = self.get_files(
|
||||
instance.name, start, end, ext)
|
||||
|
||||
self.log.debug(
|
||||
"Writing Review Animation to"
|
||||
" '%s' to '%s'" % (filename, staging_dir))
|
||||
|
||||
review_camera = instance.data["review_camera"]
|
||||
with viewport_camera(review_camera):
|
||||
preview_arg = self.set_preview_arg(
|
||||
instance, filepath, start, end, fps)
|
||||
rt.execute(preview_arg)
|
||||
|
||||
tags = ["review"]
|
||||
if not instance.data.get("keepImages"):
|
||||
tags.append("delete")
|
||||
|
||||
self.log.debug("Performing Extraction ...")
|
||||
|
||||
representation = {
|
||||
"name": instance.data["imageFormat"],
|
||||
"ext": instance.data["imageFormat"],
|
||||
"files": filenames,
|
||||
"stagingDir": staging_dir,
|
||||
"frameStart": instance.data["frameStart"],
|
||||
"frameEnd": instance.data["frameEnd"],
|
||||
"tags": tags,
|
||||
"preview": True,
|
||||
"camera_name": review_camera
|
||||
}
|
||||
self.log.debug(f"{representation}")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
def get_files(self, filename, start, end, ext):
|
||||
file_list = []
|
||||
for frame in range(int(start), int(end) + 1):
|
||||
actual_name = "{}.{:04}.{}".format(
|
||||
filename, frame, ext)
|
||||
file_list.append(actual_name)
|
||||
|
||||
return file_list
|
||||
|
||||
def set_preview_arg(self, instance, filepath,
|
||||
start, end, fps):
|
||||
job_args = list()
|
||||
default_option = f'CreatePreview filename:"{filepath}"'
|
||||
job_args.append(default_option)
|
||||
frame_option = f"outputAVI:false start:{start} end:{end} fps:{fps}" # noqa
|
||||
job_args.append(frame_option)
|
||||
rndLevel = instance.data.get("rndLevel")
|
||||
if rndLevel:
|
||||
option = f"rndLevel:#{rndLevel}"
|
||||
job_args.append(option)
|
||||
options = [
|
||||
"percentSize", "dspGeometry", "dspShapes",
|
||||
"dspLights", "dspCameras", "dspHelpers", "dspParticles",
|
||||
"dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums"
|
||||
]
|
||||
|
||||
for key in options:
|
||||
enabled = instance.data.get(key)
|
||||
if enabled:
|
||||
job_args.append(f"{key}:{enabled}")
|
||||
|
||||
if get_max_version() == 2024:
|
||||
# hardcoded for current stage
|
||||
auto_play_option = "autoPlay:false"
|
||||
job_args.append(auto_play_option)
|
||||
|
||||
job_str = " ".join(job_args)
|
||||
self.log.debug(job_str)
|
||||
|
||||
return job_str
|
||||
91
openpype/hosts/max/plugins/publish/extract_thumbnail.py
Normal file
91
openpype/hosts/max/plugins/publish/extract_thumbnail.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import os
|
||||
import tempfile
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
from openpype.pipeline import publish
|
||||
from openpype.hosts.max.api.lib import viewport_camera, get_max_version
|
||||
|
||||
|
||||
class ExtractThumbnail(publish.Extractor):
|
||||
"""
|
||||
Extract Thumbnail for Review
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract Thumbnail"
|
||||
hosts = ["max"]
|
||||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
# TODO: Create temp directory for thumbnail
|
||||
# - this is to avoid "override" of source file
|
||||
tmp_staging = tempfile.mkdtemp(prefix="pyblish_tmp_")
|
||||
self.log.debug(
|
||||
f"Create temp directory {tmp_staging} for thumbnail"
|
||||
)
|
||||
fps = int(instance.data["fps"])
|
||||
frame = int(instance.data["frameStart"])
|
||||
instance.context.data["cleanupFullPaths"].append(tmp_staging)
|
||||
filename = "{name}_thumbnail..png".format(**instance.data)
|
||||
filepath = os.path.join(tmp_staging, filename)
|
||||
filepath = filepath.replace("\\", "/")
|
||||
thumbnail = self.get_filename(instance.name, frame)
|
||||
|
||||
self.log.debug(
|
||||
"Writing Thumbnail to"
|
||||
" '%s' to '%s'" % (filename, tmp_staging))
|
||||
review_camera = instance.data["review_camera"]
|
||||
with viewport_camera(review_camera):
|
||||
preview_arg = self.set_preview_arg(
|
||||
instance, filepath, fps, frame)
|
||||
rt.execute(preview_arg)
|
||||
|
||||
representation = {
|
||||
"name": "thumbnail",
|
||||
"ext": "png",
|
||||
"files": thumbnail,
|
||||
"stagingDir": tmp_staging,
|
||||
"thumbnail": True
|
||||
}
|
||||
|
||||
self.log.debug(f"{representation}")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
def get_filename(self, filename, target_frame):
|
||||
thumbnail_name = "{}_thumbnail.{:04}.png".format(
|
||||
filename, target_frame
|
||||
)
|
||||
return thumbnail_name
|
||||
|
||||
def set_preview_arg(self, instance, filepath, fps, frame):
|
||||
job_args = list()
|
||||
default_option = f'CreatePreview filename:"{filepath}"'
|
||||
job_args.append(default_option)
|
||||
frame_option = f"outputAVI:false start:{frame} end:{frame} fps:{fps}" # noqa
|
||||
job_args.append(frame_option)
|
||||
rndLevel = instance.data.get("rndLevel")
|
||||
if rndLevel:
|
||||
option = f"rndLevel:#{rndLevel}"
|
||||
job_args.append(option)
|
||||
options = [
|
||||
"percentSize", "dspGeometry", "dspShapes",
|
||||
"dspLights", "dspCameras", "dspHelpers", "dspParticles",
|
||||
"dspBones", "dspBkg", "dspGrid", "dspSafeFrame", "dspFrameNums"
|
||||
]
|
||||
|
||||
for key in options:
|
||||
enabled = instance.data.get(key)
|
||||
if enabled:
|
||||
job_args.append(f"{key}:{enabled}")
|
||||
if get_max_version() == 2024:
|
||||
# hardcoded for current stage
|
||||
auto_play_option = "autoPlay:false"
|
||||
job_args.append(auto_play_option)
|
||||
|
||||
job_str = " ".join(job_args)
|
||||
self.log.debug(job_str)
|
||||
|
||||
return job_str
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from openpype.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError
|
||||
)
|
||||
from openpype.hosts.max.api.lib import get_frame_range, set_timeline
|
||||
|
||||
|
||||
class ValidateAnimationTimeline(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Validates Animation Timeline for Preview Animation in Max
|
||||
"""
|
||||
|
||||
label = "Animation Timeline for Review"
|
||||
order = ValidateContentsOrder
|
||||
families = ["review"]
|
||||
hosts = ["max"]
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, instance):
|
||||
frame_range = get_frame_range()
|
||||
frame_start_handle = frame_range["frameStart"] - int(
|
||||
frame_range["handleStart"]
|
||||
)
|
||||
frame_end_handle = frame_range["frameEnd"] + int(
|
||||
frame_range["handleEnd"]
|
||||
)
|
||||
if rt.animationRange.start != frame_start_handle or (
|
||||
rt.animationRange.end != frame_end_handle
|
||||
):
|
||||
raise PublishValidationError("Incorrect animation timeline "
|
||||
"set for preview animation.. "
|
||||
"\nYou can use repair action to "
|
||||
"the correct animation timeline")
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
frame_range = get_frame_range()
|
||||
frame_start_handle = frame_range["frameStart"] - int(
|
||||
frame_range["handleStart"]
|
||||
)
|
||||
frame_end_handle = frame_range["frameEnd"] + int(
|
||||
frame_range["handleEnd"]
|
||||
)
|
||||
set_timeline(frame_start_handle, frame_end_handle)
|
||||
|
|
@ -11,7 +11,7 @@ class ValidateCameraContent(pyblish.api.InstancePlugin):
|
|||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["camera"]
|
||||
families = ["camera", "review"]
|
||||
hosts = ["max"]
|
||||
label = "Camera Contents"
|
||||
camera_type = ["$Free_Camera", "$Target_Camera",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ class ValidateMaxContents(pyblish.api.InstancePlugin):
|
|||
order = pyblish.api.ValidatorOrder
|
||||
families = ["camera",
|
||||
"maxScene",
|
||||
"maxrender"]
|
||||
"maxrender",
|
||||
"review"]
|
||||
hosts = ["max"]
|
||||
label = "Max Scene Contents"
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ class ExtractBurnin(publish.Extractor):
|
|||
"aftereffects",
|
||||
"photoshop",
|
||||
"flame",
|
||||
"houdini"
|
||||
"houdini",
|
||||
"max"
|
||||
# "resolve"
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ class ExtractReview(pyblish.api.InstancePlugin):
|
|||
"maya",
|
||||
"blender",
|
||||
"houdini",
|
||||
"max",
|
||||
"shell",
|
||||
"hiero",
|
||||
"premiere",
|
||||
|
|
|
|||
|
|
@ -262,7 +262,8 @@
|
|||
],
|
||||
"hosts": [
|
||||
"maya",
|
||||
"houdini"
|
||||
"houdini",
|
||||
"max"
|
||||
],
|
||||
"task_types": [],
|
||||
"task_names": [],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue