mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
removed max addon
This commit is contained in:
parent
142d875748
commit
b69085f65d
76 changed files with 0 additions and 5854 deletions
|
|
@ -1,13 +0,0 @@
|
|||
from .version import __version__
|
||||
from .addon import (
|
||||
MaxAddon,
|
||||
MAX_HOST_DIR,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
|
||||
"MaxAddon",
|
||||
"MAX_HOST_DIR",
|
||||
)
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
from ayon_core.addon import AYONAddon, IHostAddon
|
||||
|
||||
from .version import __version__
|
||||
|
||||
MAX_HOST_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
class MaxAddon(AYONAddon, IHostAddon):
|
||||
name = "max"
|
||||
version = __version__
|
||||
host_name = "max"
|
||||
|
||||
def add_implementation_envs(self, env, _app):
|
||||
# Remove auto screen scale factor for Qt
|
||||
# - let 3dsmax decide it's value
|
||||
env.pop("QT_AUTO_SCREEN_SCALE_FACTOR", None)
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return [".max"]
|
||||
|
||||
def get_launch_hook_paths(self, app):
|
||||
if app.host_name != self.host_name:
|
||||
return []
|
||||
return [
|
||||
os.path.join(MAX_HOST_DIR, "hooks")
|
||||
]
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Public API for 3dsmax"""
|
||||
|
||||
from .pipeline import (
|
||||
MaxHost,
|
||||
)
|
||||
|
||||
|
||||
from .lib import (
|
||||
maintained_selection,
|
||||
lsattr,
|
||||
get_all_children
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"MaxHost",
|
||||
"maintained_selection",
|
||||
"lsattr",
|
||||
"get_all_children"
|
||||
]
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
from pymxs import runtime as rt
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline.publish import get_errored_instances_from_context
|
||||
|
||||
|
||||
class SelectInvalidAction(pyblish.api.Action):
|
||||
"""Select invalid objects in Blender when a publish plug-in failed."""
|
||||
label = "Select Invalid"
|
||||
on = "failed"
|
||||
icon = "search"
|
||||
|
||||
def process(self, context, plugin):
|
||||
errored_instances = get_errored_instances_from_context(context,
|
||||
plugin=plugin)
|
||||
|
||||
# Get the invalid nodes for the plug-ins
|
||||
self.log.info("Finding invalid nodes...")
|
||||
invalid = list()
|
||||
for instance in errored_instances:
|
||||
invalid_nodes = plugin.get_invalid(instance)
|
||||
if invalid_nodes:
|
||||
if isinstance(invalid_nodes, (list, tuple)):
|
||||
invalid.extend(invalid_nodes)
|
||||
else:
|
||||
self.log.warning(
|
||||
"Failed plug-in doesn't have any selectable objects."
|
||||
)
|
||||
|
||||
if not invalid:
|
||||
self.log.info("No invalid nodes found.")
|
||||
return
|
||||
invalid_names = [obj.name for obj in invalid if not isinstance(obj, tuple)]
|
||||
if not invalid_names:
|
||||
invalid_names = [obj.name for obj, _ in invalid]
|
||||
invalid = [obj for obj, _ in invalid]
|
||||
self.log.info(
|
||||
"Selecting invalid objects: %s", ", ".join(invalid_names)
|
||||
)
|
||||
|
||||
rt.Select(invalid)
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import attr
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
@attr.s
|
||||
class LayerMetadata(object):
|
||||
"""Data class for Render Layer metadata."""
|
||||
frameStart = attr.ib()
|
||||
frameEnd = attr.ib()
|
||||
|
||||
|
||||
@attr.s
|
||||
class RenderProduct(object):
|
||||
"""Getting Colorspace as
|
||||
Specific Render Product Parameter for submitting
|
||||
publish job.
|
||||
"""
|
||||
colorspace = attr.ib() # colorspace
|
||||
view = attr.ib()
|
||||
productName = attr.ib(default=None)
|
||||
|
||||
|
||||
class ARenderProduct(object):
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor."""
|
||||
# Initialize
|
||||
self.layer_data = self._get_layer_data()
|
||||
self.layer_data.products = self.get_colorspace_data()
|
||||
|
||||
def _get_layer_data(self):
|
||||
return LayerMetadata(
|
||||
frameStart=int(rt.rendStart),
|
||||
frameEnd=int(rt.rendEnd),
|
||||
)
|
||||
|
||||
def get_colorspace_data(self):
|
||||
"""To be implemented by renderer class.
|
||||
This should return a list of RenderProducts.
|
||||
Returns:
|
||||
list: List of RenderProduct
|
||||
"""
|
||||
colorspace_data = [
|
||||
RenderProduct(
|
||||
colorspace="sRGB",
|
||||
view="ACES 1.0",
|
||||
productName=""
|
||||
)
|
||||
]
|
||||
return colorspace_data
|
||||
|
|
@ -1,275 +0,0 @@
|
|||
# Render Element Example : For scanline render, VRay
|
||||
# https://help.autodesk.com/view/MAXDEV/2022/ENU/?guid=GUID-E8F75D47-B998-4800-A3A5-610E22913CFC
|
||||
# arnold
|
||||
# https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html
|
||||
import os
|
||||
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from ayon_max.api.lib import get_current_renderer
|
||||
from ayon_core.pipeline import get_current_project_name
|
||||
from ayon_core.settings import get_project_settings
|
||||
|
||||
|
||||
class RenderProducts(object):
|
||||
|
||||
def __init__(self, project_settings=None):
|
||||
self._project_settings = project_settings
|
||||
if not self._project_settings:
|
||||
self._project_settings = get_project_settings(
|
||||
get_current_project_name()
|
||||
)
|
||||
|
||||
def get_beauty(self, container):
|
||||
render_dir = os.path.dirname(rt.rendOutputFilename)
|
||||
|
||||
output_file = os.path.join(render_dir, container)
|
||||
|
||||
setting = self._project_settings
|
||||
img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa
|
||||
|
||||
start_frame = int(rt.rendStart)
|
||||
end_frame = int(rt.rendEnd) + 1
|
||||
|
||||
return {
|
||||
"beauty": self.get_expected_beauty(
|
||||
output_file, start_frame, end_frame, img_fmt
|
||||
)
|
||||
}
|
||||
|
||||
def get_multiple_beauty(self, outputs, cameras):
|
||||
beauty_output_frames = dict()
|
||||
for output, camera in zip(outputs, cameras):
|
||||
filename, ext = os.path.splitext(output)
|
||||
filename = filename.replace(".", "")
|
||||
ext = ext.replace(".", "")
|
||||
start_frame = int(rt.rendStart)
|
||||
end_frame = int(rt.rendEnd) + 1
|
||||
new_beauty = self.get_expected_beauty(
|
||||
filename, start_frame, end_frame, ext
|
||||
)
|
||||
beauty_output = ({
|
||||
f"{camera}_beauty": new_beauty
|
||||
})
|
||||
beauty_output_frames.update(beauty_output)
|
||||
return beauty_output_frames
|
||||
|
||||
def get_multiple_aovs(self, outputs, cameras):
|
||||
renderer_class = get_current_renderer()
|
||||
renderer = str(renderer_class).split(":")[0]
|
||||
aovs_frames = {}
|
||||
for output, camera in zip(outputs, cameras):
|
||||
filename, ext = os.path.splitext(output)
|
||||
filename = filename.replace(".", "")
|
||||
ext = ext.replace(".", "")
|
||||
start_frame = int(rt.rendStart)
|
||||
end_frame = int(rt.rendEnd) + 1
|
||||
|
||||
if renderer in [
|
||||
"ART_Renderer",
|
||||
"V_Ray_6_Hotfix_3",
|
||||
"V_Ray_GPU_6_Hotfix_3",
|
||||
"Default_Scanline_Renderer",
|
||||
"Quicksilver_Hardware_Renderer",
|
||||
]:
|
||||
render_name = self.get_render_elements_name()
|
||||
if render_name:
|
||||
for name in render_name:
|
||||
aovs_frames.update({
|
||||
f"{camera}_{name}": self.get_expected_aovs(
|
||||
filename, name, start_frame,
|
||||
end_frame, ext)
|
||||
})
|
||||
elif renderer == "Redshift_Renderer":
|
||||
render_name = self.get_render_elements_name()
|
||||
if render_name:
|
||||
rs_aov_files = rt.Execute("renderers.current.separateAovFiles") # noqa
|
||||
# this doesn't work, always returns False
|
||||
# rs_AovFiles = rt.RedShift_Renderer().separateAovFiles
|
||||
if ext == "exr" and not rs_aov_files:
|
||||
for name in render_name:
|
||||
if name == "RsCryptomatte":
|
||||
aovs_frames.update({
|
||||
f"{camera}_{name}": self.get_expected_aovs(
|
||||
filename, name, start_frame,
|
||||
end_frame, ext)
|
||||
})
|
||||
else:
|
||||
for name in render_name:
|
||||
aovs_frames.update({
|
||||
f"{camera}_{name}": self.get_expected_aovs(
|
||||
filename, name, start_frame,
|
||||
end_frame, ext)
|
||||
})
|
||||
elif renderer == "Arnold":
|
||||
render_name = self.get_arnold_product_name()
|
||||
if render_name:
|
||||
for name in render_name:
|
||||
aovs_frames.update({
|
||||
f"{camera}_{name}": self.get_expected_arnold_product( # noqa
|
||||
filename, name, start_frame,
|
||||
end_frame, ext)
|
||||
})
|
||||
elif renderer in [
|
||||
"V_Ray_6_Hotfix_3",
|
||||
"V_Ray_GPU_6_Hotfix_3"
|
||||
]:
|
||||
if ext != "exr":
|
||||
render_name = self.get_render_elements_name()
|
||||
if render_name:
|
||||
for name in render_name:
|
||||
aovs_frames.update({
|
||||
f"{camera}_{name}": self.get_expected_aovs(
|
||||
filename, name, start_frame,
|
||||
end_frame, ext)
|
||||
})
|
||||
|
||||
return aovs_frames
|
||||
|
||||
def get_aovs(self, container):
|
||||
render_dir = os.path.dirname(rt.rendOutputFilename)
|
||||
|
||||
output_file = os.path.join(render_dir,
|
||||
container)
|
||||
|
||||
setting = self._project_settings
|
||||
img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa
|
||||
|
||||
start_frame = int(rt.rendStart)
|
||||
end_frame = int(rt.rendEnd) + 1
|
||||
renderer_class = get_current_renderer()
|
||||
renderer = str(renderer_class).split(":")[0]
|
||||
render_dict = {}
|
||||
|
||||
if renderer in [
|
||||
"ART_Renderer",
|
||||
"V_Ray_6_Hotfix_3",
|
||||
"V_Ray_GPU_6_Hotfix_3",
|
||||
"Default_Scanline_Renderer",
|
||||
"Quicksilver_Hardware_Renderer",
|
||||
]:
|
||||
render_name = self.get_render_elements_name()
|
||||
if render_name:
|
||||
for name in render_name:
|
||||
render_dict.update({
|
||||
name: self.get_expected_aovs(
|
||||
output_file, name, start_frame,
|
||||
end_frame, img_fmt)
|
||||
})
|
||||
elif renderer == "Redshift_Renderer":
|
||||
render_name = self.get_render_elements_name()
|
||||
if render_name:
|
||||
rs_aov_files = rt.Execute("renderers.current.separateAovFiles")
|
||||
# this doesn't work, always returns False
|
||||
# rs_AovFiles = rt.RedShift_Renderer().separateAovFiles
|
||||
if img_fmt == "exr" and not rs_aov_files:
|
||||
for name in render_name:
|
||||
if name == "RsCryptomatte":
|
||||
render_dict.update({
|
||||
name: self.get_expected_aovs(
|
||||
output_file, name, start_frame,
|
||||
end_frame, img_fmt)
|
||||
})
|
||||
else:
|
||||
for name in render_name:
|
||||
render_dict.update({
|
||||
name: self.get_expected_aovs(
|
||||
output_file, name, start_frame,
|
||||
end_frame, img_fmt)
|
||||
})
|
||||
|
||||
elif renderer == "Arnold":
|
||||
render_name = self.get_arnold_product_name()
|
||||
if render_name:
|
||||
for name in render_name:
|
||||
render_dict.update({
|
||||
name: self.get_expected_arnold_product(
|
||||
output_file, name, start_frame,
|
||||
end_frame, img_fmt)
|
||||
})
|
||||
elif renderer in [
|
||||
"V_Ray_6_Hotfix_3",
|
||||
"V_Ray_GPU_6_Hotfix_3"
|
||||
]:
|
||||
if img_fmt != "exr":
|
||||
render_name = self.get_render_elements_name()
|
||||
if render_name:
|
||||
for name in render_name:
|
||||
render_dict.update({
|
||||
name: self.get_expected_aovs(
|
||||
output_file, name, start_frame,
|
||||
end_frame, img_fmt) # noqa
|
||||
})
|
||||
|
||||
return render_dict
|
||||
|
||||
def get_expected_beauty(self, folder, start_frame, end_frame, fmt):
|
||||
beauty_frame_range = []
|
||||
for f in range(start_frame, end_frame):
|
||||
frame = "%04d" % f
|
||||
beauty_output = f"{folder}.{frame}.{fmt}"
|
||||
beauty_output = beauty_output.replace("\\", "/")
|
||||
beauty_frame_range.append(beauty_output)
|
||||
|
||||
return beauty_frame_range
|
||||
|
||||
def get_arnold_product_name(self):
|
||||
"""Get all the Arnold AOVs name"""
|
||||
aov_name = []
|
||||
|
||||
amw = rt.MaxToAOps.AOVsManagerWindow()
|
||||
aov_mgr = rt.renderers.current.AOVManager
|
||||
# Check if there is any aov group set in AOV manager
|
||||
aov_group_num = len(aov_mgr.drivers)
|
||||
if aov_group_num < 1:
|
||||
return
|
||||
for i in range(aov_group_num):
|
||||
# get the specific AOV group
|
||||
aov_name.extend(aov.name for aov in aov_mgr.drivers[i].aov_list)
|
||||
# close the AOVs manager window
|
||||
amw.close()
|
||||
|
||||
return aov_name
|
||||
|
||||
def get_expected_arnold_product(self, folder, name,
|
||||
start_frame, end_frame, fmt):
|
||||
"""Get all the expected Arnold AOVs"""
|
||||
aov_list = []
|
||||
for f in range(start_frame, end_frame):
|
||||
frame = "%04d" % f
|
||||
render_element = f"{folder}_{name}.{frame}.{fmt}"
|
||||
render_element = render_element.replace("\\", "/")
|
||||
aov_list.append(render_element)
|
||||
|
||||
return aov_list
|
||||
|
||||
def get_render_elements_name(self):
|
||||
"""Get all the render element names for general """
|
||||
render_name = []
|
||||
render_elem = rt.maxOps.GetCurRenderElementMgr()
|
||||
render_elem_num = render_elem.NumRenderElements()
|
||||
if render_elem_num < 1:
|
||||
return
|
||||
# get render elements from the renders
|
||||
for i in range(render_elem_num):
|
||||
renderlayer_name = render_elem.GetRenderElement(i)
|
||||
if renderlayer_name.enabled:
|
||||
target, renderpass = str(renderlayer_name).split(":")
|
||||
render_name.append(renderpass)
|
||||
|
||||
return render_name
|
||||
|
||||
def get_expected_aovs(self, folder, name,
|
||||
start_frame, end_frame, fmt):
|
||||
"""Get all the expected render element output files. """
|
||||
render_elements = []
|
||||
for f in range(start_frame, end_frame):
|
||||
frame = "%04d" % f
|
||||
render_element = f"{folder}_{name}.{frame}.{fmt}"
|
||||
render_element = render_element.replace("\\", "/")
|
||||
render_elements.append(render_element)
|
||||
|
||||
return render_elements
|
||||
|
||||
def image_format(self):
|
||||
return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
import os
|
||||
from pymxs import runtime as rt
|
||||
from ayon_core.lib import Logger
|
||||
from ayon_core.settings import get_project_settings
|
||||
from ayon_core.pipeline import get_current_project_name
|
||||
from ayon_core.pipeline.context_tools import get_current_folder_entity
|
||||
|
||||
from ayon_max.api.lib import (
|
||||
set_render_frame_range,
|
||||
get_current_renderer,
|
||||
get_default_render_folder
|
||||
)
|
||||
|
||||
|
||||
class RenderSettings(object):
|
||||
|
||||
log = Logger.get_logger("RenderSettings")
|
||||
|
||||
_aov_chars = {
|
||||
"dot": ".",
|
||||
"dash": "-",
|
||||
"underscore": "_"
|
||||
}
|
||||
|
||||
def __init__(self, project_settings=None):
|
||||
"""
|
||||
Set up the naming convention for the render
|
||||
elements for the deadline submission
|
||||
"""
|
||||
|
||||
self._project_settings = project_settings
|
||||
if not self._project_settings:
|
||||
self._project_settings = get_project_settings(
|
||||
get_current_project_name()
|
||||
)
|
||||
|
||||
def set_render_camera(self, selection):
|
||||
for sel in selection:
|
||||
# to avoid Attribute Error from pymxs wrapper
|
||||
if rt.classOf(sel) in rt.Camera.classes:
|
||||
rt.viewport.setCamera(sel)
|
||||
return
|
||||
raise RuntimeError("Active Camera not found")
|
||||
|
||||
def render_output(self, container):
|
||||
folder = rt.maxFilePath
|
||||
# hard-coded, should be customized in the setting
|
||||
file = rt.maxFileName
|
||||
folder = folder.replace("\\", "/")
|
||||
# hard-coded, set the renderoutput path
|
||||
setting = self._project_settings
|
||||
render_folder = get_default_render_folder(setting)
|
||||
filename, ext = os.path.splitext(file)
|
||||
output_dir = os.path.join(folder,
|
||||
render_folder,
|
||||
filename)
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
# hard-coded, should be customized in the setting
|
||||
folder_attributes = get_current_folder_entity()["attrib"]
|
||||
|
||||
# get project resolution
|
||||
width = folder_attributes.get("resolutionWidth")
|
||||
height = folder_attributes.get("resolutionHeight")
|
||||
# Set Frame Range
|
||||
frame_start = folder_attributes.get("frame_start")
|
||||
frame_end = folder_attributes.get("frame_end")
|
||||
set_render_frame_range(frame_start, frame_end)
|
||||
# get the production render
|
||||
renderer_class = get_current_renderer()
|
||||
renderer = str(renderer_class).split(":")[0]
|
||||
|
||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
||||
output = os.path.join(output_dir, container)
|
||||
try:
|
||||
aov_separator = self._aov_chars[(
|
||||
self._project_settings["max"]
|
||||
["RenderSettings"]
|
||||
["aov_separator"]
|
||||
)]
|
||||
except KeyError:
|
||||
aov_separator = "."
|
||||
output_filename = f"{output}..{img_fmt}"
|
||||
output_filename = output_filename.replace("{aov_separator}",
|
||||
aov_separator)
|
||||
rt.rendOutputFilename = output_filename
|
||||
if renderer == "VUE_File_Renderer":
|
||||
return
|
||||
# TODO: Finish the arnold render setup
|
||||
if renderer == "Arnold":
|
||||
self.arnold_setup()
|
||||
|
||||
if renderer in [
|
||||
"ART_Renderer",
|
||||
"Redshift_Renderer",
|
||||
"V_Ray_6_Hotfix_3",
|
||||
"V_Ray_GPU_6_Hotfix_3",
|
||||
"Default_Scanline_Renderer",
|
||||
"Quicksilver_Hardware_Renderer",
|
||||
]:
|
||||
self.render_element_layer(output, width, height, img_fmt)
|
||||
|
||||
rt.rendSaveFile = True
|
||||
|
||||
if rt.renderSceneDialog.isOpen():
|
||||
rt.renderSceneDialog.close()
|
||||
|
||||
def arnold_setup(self):
|
||||
# get Arnold RenderView run in the background
|
||||
# for setting up renderable camera
|
||||
arv = rt.MAXToAOps.ArnoldRenderView()
|
||||
render_camera = rt.viewport.GetCamera()
|
||||
if render_camera:
|
||||
arv.setOption("Camera", str(render_camera))
|
||||
|
||||
# TODO: add AOVs and extension
|
||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
||||
setup_cmd = (
|
||||
f"""
|
||||
amw = MaxtoAOps.AOVsManagerWindow()
|
||||
amw.close()
|
||||
aovmgr = renderers.current.AOVManager
|
||||
aovmgr.drivers = #()
|
||||
img_fmt = "{img_fmt}"
|
||||
if img_fmt == "png" then driver = ArnoldPNGDriver()
|
||||
if img_fmt == "jpg" then driver = ArnoldJPEGDriver()
|
||||
if img_fmt == "exr" then driver = ArnoldEXRDriver()
|
||||
if img_fmt == "tif" then driver = ArnoldTIFFDriver()
|
||||
if img_fmt == "tiff" then driver = ArnoldTIFFDriver()
|
||||
append aovmgr.drivers driver
|
||||
aovmgr.drivers[1].aov_list = #()
|
||||
""")
|
||||
|
||||
rt.execute(setup_cmd)
|
||||
arv.close()
|
||||
|
||||
def render_element_layer(self, dir, width, height, ext):
|
||||
"""For Renderers with render elements"""
|
||||
rt.renderWidth = width
|
||||
rt.renderHeight = height
|
||||
render_elem = rt.maxOps.GetCurRenderElementMgr()
|
||||
render_elem_num = render_elem.NumRenderElements()
|
||||
if render_elem_num < 0:
|
||||
return
|
||||
|
||||
for i in range(render_elem_num):
|
||||
renderlayer_name = render_elem.GetRenderElement(i)
|
||||
target, renderpass = str(renderlayer_name).split(":")
|
||||
aov_name = f"{dir}_{renderpass}..{ext}"
|
||||
render_elem.SetRenderElementFileName(i, aov_name)
|
||||
|
||||
def get_render_output(self, container, output_dir):
|
||||
output = os.path.join(output_dir, container)
|
||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
||||
output_filename = f"{output}..{img_fmt}"
|
||||
return output_filename
|
||||
|
||||
def get_render_element(self):
|
||||
orig_render_elem = []
|
||||
render_elem = rt.maxOps.GetCurRenderElementMgr()
|
||||
render_elem_num = render_elem.NumRenderElements()
|
||||
if render_elem_num < 0:
|
||||
return
|
||||
|
||||
for i in range(render_elem_num):
|
||||
render_element = render_elem.GetRenderElementFilename(i)
|
||||
orig_render_elem.append(render_element)
|
||||
|
||||
return orig_render_elem
|
||||
|
||||
def get_batch_render_elements(self, container,
|
||||
output_dir, camera):
|
||||
render_element_list = list()
|
||||
output = os.path.join(output_dir, container)
|
||||
render_elem = rt.maxOps.GetCurRenderElementMgr()
|
||||
render_elem_num = render_elem.NumRenderElements()
|
||||
if render_elem_num < 0:
|
||||
return
|
||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
||||
|
||||
for i in range(render_elem_num):
|
||||
renderlayer_name = render_elem.GetRenderElement(i)
|
||||
target, renderpass = str(renderlayer_name).split(":")
|
||||
aov_name = f"{output}_{camera}_{renderpass}..{img_fmt}"
|
||||
render_element_list.append(aov_name)
|
||||
return render_element_list
|
||||
|
||||
def get_batch_render_output(self, camera):
|
||||
target_layer_no = rt.batchRenderMgr.FindView(camera)
|
||||
target_layer = rt.batchRenderMgr.GetView(target_layer_no)
|
||||
return target_layer.outputFilename
|
||||
|
||||
def batch_render_elements(self, camera):
|
||||
target_layer_no = rt.batchRenderMgr.FindView(camera)
|
||||
target_layer = rt.batchRenderMgr.GetView(target_layer_no)
|
||||
outputfilename = target_layer.outputFilename
|
||||
directory = os.path.dirname(outputfilename)
|
||||
render_elem = rt.maxOps.GetCurRenderElementMgr()
|
||||
render_elem_num = render_elem.NumRenderElements()
|
||||
if render_elem_num < 0:
|
||||
return
|
||||
ext = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
||||
|
||||
for i in range(render_elem_num):
|
||||
renderlayer_name = render_elem.GetRenderElement(i)
|
||||
target, renderpass = str(renderlayer_name).split(":")
|
||||
aov_name = f"{directory}_{camera}_{renderpass}..{ext}"
|
||||
render_elem.SetRenderElementFileName(i, aov_name)
|
||||
|
||||
def batch_render_layer(self, container,
|
||||
output_dir, cameras):
|
||||
outputs = list()
|
||||
output = os.path.join(output_dir, container)
|
||||
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa
|
||||
for cam in cameras:
|
||||
camera = rt.getNodeByName(cam)
|
||||
layer_no = rt.batchRenderMgr.FindView(cam)
|
||||
renderlayer = None
|
||||
if layer_no == 0:
|
||||
renderlayer = rt.batchRenderMgr.CreateView(camera)
|
||||
else:
|
||||
renderlayer = rt.batchRenderMgr.GetView(layer_no)
|
||||
# use camera name as renderlayer name
|
||||
renderlayer.name = cam
|
||||
renderlayer.outputFilename = f"{output}_{cam}..{img_fmt}"
|
||||
outputs.append(renderlayer.outputFilename)
|
||||
return outputs
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""3dsmax menu definition of AYON."""
|
||||
import os
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from ayon_core.tools.utils import host_tools
|
||||
from ayon_max.api import lib
|
||||
|
||||
|
||||
class AYONMenu(object):
|
||||
"""Object representing AYON menu.
|
||||
|
||||
This is using "hack" to inject itself before "Help" menu of 3dsmax.
|
||||
For some reason `postLoadingMenus` event doesn't fire, and main menu
|
||||
if probably re-initialized by menu templates, se we wait for at least
|
||||
1 event Qt event loop before trying to insert.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.main_widget = self.get_main_widget()
|
||||
self.menu = None
|
||||
|
||||
timer = QtCore.QTimer()
|
||||
# set number of event loops to wait.
|
||||
timer.setInterval(1)
|
||||
timer.timeout.connect(self._on_timer)
|
||||
timer.start()
|
||||
|
||||
self._timer = timer
|
||||
self._counter = 0
|
||||
|
||||
def _on_timer(self):
|
||||
if self._counter < 1:
|
||||
self._counter += 1
|
||||
return
|
||||
|
||||
self._counter = 0
|
||||
self._timer.stop()
|
||||
self._build_ayon_menu()
|
||||
|
||||
@staticmethod
|
||||
def get_main_widget():
|
||||
"""Get 3dsmax main window."""
|
||||
return QtWidgets.QWidget.find(rt.windows.getMAXHWND())
|
||||
|
||||
def get_main_menubar(self) -> QtWidgets.QMenuBar:
|
||||
"""Get main Menubar by 3dsmax main window."""
|
||||
return list(self.main_widget.findChildren(QtWidgets.QMenuBar))[0]
|
||||
|
||||
def _get_or_create_ayon_menu(
|
||||
self, name: str = "&AYON",
|
||||
before: str = "&Help") -> QtWidgets.QAction:
|
||||
"""Create AYON menu.
|
||||
|
||||
Args:
|
||||
name (str, Optional): AYON menu name.
|
||||
before (str, Optional): Name of the 3dsmax main menu item to
|
||||
add AYON menu before.
|
||||
|
||||
Returns:
|
||||
QtWidgets.QAction: AYON menu action.
|
||||
|
||||
"""
|
||||
if self.menu is not None:
|
||||
return self.menu
|
||||
|
||||
menu_bar = self.get_main_menubar()
|
||||
menu_items = menu_bar.findChildren(
|
||||
QtWidgets.QMenu, options=QtCore.Qt.FindDirectChildrenOnly)
|
||||
help_action = None
|
||||
for item in menu_items:
|
||||
if name in item.title():
|
||||
# we already have AYON menu
|
||||
return item
|
||||
|
||||
if before in item.title():
|
||||
help_action = item.menuAction()
|
||||
tab_menu_label = os.environ.get("AYON_MENU_LABEL") or "AYON"
|
||||
op_menu = QtWidgets.QMenu("&{}".format(tab_menu_label))
|
||||
menu_bar.insertMenu(help_action, op_menu)
|
||||
|
||||
self.menu = op_menu
|
||||
return op_menu
|
||||
|
||||
def _build_ayon_menu(self) -> QtWidgets.QAction:
|
||||
"""Build items in AYON menu."""
|
||||
ayon_menu = self._get_or_create_ayon_menu()
|
||||
load_action = QtWidgets.QAction("Load...", ayon_menu)
|
||||
load_action.triggered.connect(self.load_callback)
|
||||
ayon_menu.addAction(load_action)
|
||||
|
||||
publish_action = QtWidgets.QAction("Publish...", ayon_menu)
|
||||
publish_action.triggered.connect(self.publish_callback)
|
||||
ayon_menu.addAction(publish_action)
|
||||
|
||||
manage_action = QtWidgets.QAction("Manage...", ayon_menu)
|
||||
manage_action.triggered.connect(self.manage_callback)
|
||||
ayon_menu.addAction(manage_action)
|
||||
|
||||
library_action = QtWidgets.QAction("Library...", ayon_menu)
|
||||
library_action.triggered.connect(self.library_callback)
|
||||
ayon_menu.addAction(library_action)
|
||||
|
||||
ayon_menu.addSeparator()
|
||||
|
||||
workfiles_action = QtWidgets.QAction("Work Files...", ayon_menu)
|
||||
workfiles_action.triggered.connect(self.workfiles_callback)
|
||||
ayon_menu.addAction(workfiles_action)
|
||||
|
||||
ayon_menu.addSeparator()
|
||||
|
||||
res_action = QtWidgets.QAction("Set Resolution", ayon_menu)
|
||||
res_action.triggered.connect(self.resolution_callback)
|
||||
ayon_menu.addAction(res_action)
|
||||
|
||||
frame_action = QtWidgets.QAction("Set Frame Range", ayon_menu)
|
||||
frame_action.triggered.connect(self.frame_range_callback)
|
||||
ayon_menu.addAction(frame_action)
|
||||
|
||||
colorspace_action = QtWidgets.QAction("Set Colorspace", ayon_menu)
|
||||
colorspace_action.triggered.connect(self.colorspace_callback)
|
||||
ayon_menu.addAction(colorspace_action)
|
||||
|
||||
unit_scale_action = QtWidgets.QAction("Set Unit Scale", ayon_menu)
|
||||
unit_scale_action.triggered.connect(self.unit_scale_callback)
|
||||
ayon_menu.addAction(unit_scale_action)
|
||||
|
||||
return ayon_menu
|
||||
|
||||
def load_callback(self):
|
||||
"""Callback to show Loader tool."""
|
||||
host_tools.show_loader(parent=self.main_widget)
|
||||
|
||||
def publish_callback(self):
|
||||
"""Callback to show Publisher tool."""
|
||||
host_tools.show_publisher(parent=self.main_widget)
|
||||
|
||||
def manage_callback(self):
|
||||
"""Callback to show Scene Manager/Inventory tool."""
|
||||
host_tools.show_scene_inventory(parent=self.main_widget)
|
||||
|
||||
def library_callback(self):
|
||||
"""Callback to show Library Loader tool."""
|
||||
host_tools.show_library_loader(parent=self.main_widget)
|
||||
|
||||
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()
|
||||
|
||||
def colorspace_callback(self):
|
||||
"""Callback to reset colorspace"""
|
||||
return lib.reset_colorspace()
|
||||
|
||||
def unit_scale_callback(self):
|
||||
"""Callback to reset unit scale"""
|
||||
return lib.reset_unit_scale()
|
||||
|
|
@ -1,297 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pipeline tools for AYON 3ds max integration."""
|
||||
import os
|
||||
import logging
|
||||
from operator import attrgetter
|
||||
|
||||
import json
|
||||
|
||||
from ayon_core.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import (
|
||||
register_creator_plugin_path,
|
||||
register_loader_plugin_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
AYON_CONTAINER_ID,
|
||||
)
|
||||
from ayon_max.api.menu import AYONMenu
|
||||
from ayon_max.api import lib
|
||||
from ayon_max.api.plugin import MS_CUSTOM_ATTRIB
|
||||
from ayon_max import MAX_HOST_DIR
|
||||
|
||||
from pymxs import runtime as rt # noqa
|
||||
|
||||
log = logging.getLogger("ayon_max")
|
||||
|
||||
PLUGINS_DIR = os.path.join(MAX_HOST_DIR, "plugins")
|
||||
PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish")
|
||||
LOAD_PATH = os.path.join(PLUGINS_DIR, "load")
|
||||
CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
|
||||
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
|
||||
|
||||
|
||||
class MaxHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
|
||||
|
||||
name = "max"
|
||||
menu = None
|
||||
|
||||
def __init__(self):
|
||||
super(MaxHost, self).__init__()
|
||||
self._op_events = {}
|
||||
self._has_been_setup = False
|
||||
|
||||
def install(self):
|
||||
pyblish.api.register_host("max")
|
||||
|
||||
pyblish.api.register_plugin_path(PUBLISH_PATH)
|
||||
register_loader_plugin_path(LOAD_PATH)
|
||||
register_creator_plugin_path(CREATE_PATH)
|
||||
|
||||
# self._register_callbacks()
|
||||
self.menu = AYONMenu()
|
||||
|
||||
self._has_been_setup = True
|
||||
|
||||
rt.callbacks.addScript(rt.Name('systemPostNew'), on_new)
|
||||
|
||||
rt.callbacks.addScript(rt.Name('filePostOpen'),
|
||||
lib.check_colorspace)
|
||||
|
||||
rt.callbacks.addScript(rt.Name('postWorkspaceChange'),
|
||||
self._deferred_menu_creation)
|
||||
rt.NodeEventCallback(
|
||||
nameChanged=lib.update_modifier_node_names)
|
||||
|
||||
def workfile_has_unsaved_changes(self):
|
||||
return rt.getSaveRequired()
|
||||
|
||||
def get_workfile_extensions(self):
|
||||
return [".max"]
|
||||
|
||||
def save_workfile(self, dst_path=None):
|
||||
rt.saveMaxFile(dst_path)
|
||||
return dst_path
|
||||
|
||||
def open_workfile(self, filepath):
|
||||
rt.checkForSave()
|
||||
rt.loadMaxFile(filepath)
|
||||
return filepath
|
||||
|
||||
def get_current_workfile(self):
|
||||
return os.path.join(rt.maxFilePath, rt.maxFileName)
|
||||
|
||||
def get_containers(self):
|
||||
return ls()
|
||||
|
||||
def _register_callbacks(self):
|
||||
rt.callbacks.removeScripts(id=rt.name("OpenPypeCallbacks"))
|
||||
|
||||
rt.callbacks.addScript(
|
||||
rt.Name("postLoadingMenus"),
|
||||
self._deferred_menu_creation, id=rt.Name('OpenPypeCallbacks'))
|
||||
|
||||
def _deferred_menu_creation(self):
|
||||
self.log.info("Building menu ...")
|
||||
self.menu = AYONMenu()
|
||||
|
||||
@staticmethod
|
||||
def create_context_node():
|
||||
"""Helper for creating context holding node."""
|
||||
|
||||
root_scene = rt.rootScene
|
||||
|
||||
create_attr_script = ("""
|
||||
attributes "OpenPypeContext"
|
||||
(
|
||||
parameters main rollout:params
|
||||
(
|
||||
context type: #string
|
||||
)
|
||||
|
||||
rollout params "OpenPype Parameters"
|
||||
(
|
||||
editText editTextContext "Context" type: #string
|
||||
)
|
||||
)
|
||||
""")
|
||||
|
||||
attr = rt.execute(create_attr_script)
|
||||
rt.custAttributes.add(root_scene, attr)
|
||||
|
||||
return root_scene.OpenPypeContext.context
|
||||
|
||||
def update_context_data(self, data, changes):
|
||||
try:
|
||||
_ = rt.rootScene.OpenPypeContext.context
|
||||
except AttributeError:
|
||||
# context node doesn't exists
|
||||
self.create_context_node()
|
||||
|
||||
rt.rootScene.OpenPypeContext.context = json.dumps(data)
|
||||
|
||||
def get_context_data(self):
|
||||
try:
|
||||
context = rt.rootScene.OpenPypeContext.context
|
||||
except AttributeError:
|
||||
# context node doesn't exists
|
||||
context = self.create_context_node()
|
||||
if not context:
|
||||
context = "{}"
|
||||
return json.loads(context)
|
||||
|
||||
def save_file(self, dst_path=None):
|
||||
# Force forwards slashes to avoid segfault
|
||||
dst_path = dst_path.replace("\\", "/")
|
||||
rt.saveMaxFile(dst_path)
|
||||
|
||||
|
||||
def parse_container(container):
|
||||
"""Return the container node's full container data.
|
||||
|
||||
Args:
|
||||
container (str): A container node name.
|
||||
|
||||
Returns:
|
||||
dict: The container schema data for this container node.
|
||||
|
||||
"""
|
||||
data = lib.read(container)
|
||||
|
||||
# Backwards compatibility pre-schemas for containers
|
||||
data["schema"] = data.get("schema", "openpype:container-3.0")
|
||||
|
||||
# Append transient data
|
||||
data["objectName"] = container.Name
|
||||
return data
|
||||
|
||||
|
||||
def ls():
|
||||
"""Get all AYON containers."""
|
||||
objs = rt.objects
|
||||
containers = [
|
||||
obj for obj in objs
|
||||
if rt.getUserProp(obj, "id") in {
|
||||
AYON_CONTAINER_ID, AVALON_CONTAINER_ID
|
||||
}
|
||||
]
|
||||
|
||||
for container in sorted(containers, key=attrgetter("name")):
|
||||
yield parse_container(container)
|
||||
|
||||
|
||||
def on_new():
|
||||
lib.set_context_setting()
|
||||
if rt.checkForSave():
|
||||
rt.resetMaxFile(rt.Name("noPrompt"))
|
||||
rt.clearUndoBuffer()
|
||||
rt.redrawViews()
|
||||
|
||||
|
||||
def containerise(name: str, nodes: list, context,
|
||||
namespace=None, loader=None, suffix="_CON"):
|
||||
data = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"name": name,
|
||||
"namespace": namespace or "",
|
||||
"loader": loader,
|
||||
"representation": context["representation"]["id"],
|
||||
}
|
||||
container_name = f"{namespace}:{name}{suffix}"
|
||||
container = rt.container(name=container_name)
|
||||
import_custom_attribute_data(container, nodes)
|
||||
if not lib.imprint(container_name, data):
|
||||
print(f"imprinting of {container_name} failed.")
|
||||
return container
|
||||
|
||||
|
||||
def load_custom_attribute_data():
|
||||
"""Re-loading the AYON custom parameter built by the creator
|
||||
|
||||
Returns:
|
||||
attribute: re-loading the custom OP attributes set in Maxscript
|
||||
"""
|
||||
return rt.Execute(MS_CUSTOM_ATTRIB)
|
||||
|
||||
|
||||
def import_custom_attribute_data(container: str, selections: list):
|
||||
"""Importing the Openpype/AYON custom parameter built by the creator
|
||||
|
||||
Args:
|
||||
container (str): target container which adds custom attributes
|
||||
selections (list): nodes to be added into
|
||||
group in custom attributes
|
||||
"""
|
||||
attrs = load_custom_attribute_data()
|
||||
modifier = rt.EmptyModifier()
|
||||
rt.addModifier(container, modifier)
|
||||
container.modifiers[0].name = "OP Data"
|
||||
rt.custAttributes.add(container.modifiers[0], attrs)
|
||||
node_list = []
|
||||
sel_list = []
|
||||
for i in selections:
|
||||
node_ref = rt.NodeTransformMonitor(node=i)
|
||||
node_list.append(node_ref)
|
||||
sel_list.append(str(i))
|
||||
|
||||
# Setting the property
|
||||
rt.setProperty(
|
||||
container.modifiers[0].openPypeData,
|
||||
"all_handles", node_list)
|
||||
rt.setProperty(
|
||||
container.modifiers[0].openPypeData,
|
||||
"sel_list", sel_list)
|
||||
|
||||
|
||||
def update_custom_attribute_data(container: str, selections: list):
|
||||
"""Updating the AYON custom parameter built by the creator
|
||||
|
||||
Args:
|
||||
container (str): target container which adds custom attributes
|
||||
selections (list): nodes to be added into
|
||||
group in custom attributes
|
||||
"""
|
||||
if container.modifiers[0].name == "OP Data":
|
||||
rt.deleteModifier(container, container.modifiers[0])
|
||||
import_custom_attribute_data(container, selections)
|
||||
|
||||
|
||||
def get_previous_loaded_object(container: str):
|
||||
"""Get previous loaded_object through the OP data
|
||||
|
||||
Args:
|
||||
container (str): the container which stores the OP data
|
||||
|
||||
Returns:
|
||||
node_list(list): list of nodes which are previously loaded
|
||||
"""
|
||||
node_list = []
|
||||
node_transform_monitor_list = rt.getProperty(
|
||||
container.modifiers[0].openPypeData, "all_handles")
|
||||
for node_transform_monitor in node_transform_monitor_list:
|
||||
node_list.append(node_transform_monitor.node)
|
||||
return node_list
|
||||
|
||||
|
||||
def remove_container_data(container_node: str):
|
||||
"""Function to remove container data after updating, switching or deleting it.
|
||||
|
||||
Args:
|
||||
container_node (str): container node
|
||||
"""
|
||||
if container_node.modifiers[0].name == "OP Data":
|
||||
all_set_members_names = [
|
||||
member.node for member
|
||||
in container_node.modifiers[0].openPypeData.all_handles]
|
||||
# clean up the children of alembic dummy objects
|
||||
for current_set_member in all_set_members_names:
|
||||
shape_list = [members for members in current_set_member.Children
|
||||
if rt.ClassOf(members) == rt.AlembicObject
|
||||
or rt.isValidNode(members)]
|
||||
if shape_list: # noqa
|
||||
rt.Delete(shape_list)
|
||||
rt.Delete(current_set_member)
|
||||
rt.deleteModifier(container_node, container_node.modifiers[0])
|
||||
|
||||
rt.Delete(container_node)
|
||||
rt.redrawViews()
|
||||
|
|
@ -1,344 +0,0 @@
|
|||
import logging
|
||||
import contextlib
|
||||
from pymxs import runtime as rt
|
||||
from .lib import get_max_version, render_resolution
|
||||
|
||||
log = logging.getLogger("ayon_max")
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def play_preview_when_done(has_autoplay):
|
||||
"""Set preview playback option during context
|
||||
|
||||
Args:
|
||||
has_autoplay (bool): autoplay during creating
|
||||
preview animation
|
||||
"""
|
||||
current_playback = rt.preferences.playPreviewWhenDone
|
||||
try:
|
||||
rt.preferences.playPreviewWhenDone = has_autoplay
|
||||
yield
|
||||
finally:
|
||||
rt.preferences.playPreviewWhenDone = current_playback
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def viewport_layout_and_camera(camera, layout="layout_1"):
|
||||
"""Set viewport layout and camera during context
|
||||
***For 3dsMax 2024+
|
||||
Args:
|
||||
camera (str): viewport camera
|
||||
layout (str): layout to use in viewport, defaults to `layout_1`
|
||||
Use None to not change viewport layout during context.
|
||||
"""
|
||||
needs_maximise = 0
|
||||
# Set to first active non extended viewport
|
||||
rt.viewport.activeViewportEx(1)
|
||||
original_camera = rt.viewport.getCamera()
|
||||
original_type = rt.viewport.getType()
|
||||
review_camera = rt.getNodeByName(camera)
|
||||
|
||||
try:
|
||||
if rt.viewport.getLayout() != rt.name(layout):
|
||||
rt.execute("max tool maximize")
|
||||
needs_maximise = 1
|
||||
rt.viewport.setCamera(review_camera)
|
||||
yield
|
||||
finally:
|
||||
if needs_maximise == 1:
|
||||
rt.execute("max tool maximize")
|
||||
if original_type == rt.Name("view_camera"):
|
||||
rt.viewport.setCamera(original_camera)
|
||||
else:
|
||||
rt.viewport.setType(original_type)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def viewport_preference_setting(general_viewport,
|
||||
nitrous_manager,
|
||||
nitrous_viewport,
|
||||
vp_button_mgr):
|
||||
"""Function to set viewport setting during context
|
||||
***For Max Version < 2024
|
||||
Args:
|
||||
camera (str): Viewport camera for review render
|
||||
general_viewport (dict): General viewport setting
|
||||
nitrous_manager (dict): Nitrous graphic manager
|
||||
nitrous_viewport (dict): Nitrous setting for
|
||||
preview animation
|
||||
vp_button_mgr (dict): Viewport button manager Setting
|
||||
preview_preferences (dict): Preview Preferences Setting
|
||||
"""
|
||||
orig_vp_grid = rt.viewport.getGridVisibility(1)
|
||||
orig_vp_bkg = rt.viewport.IsSolidBackgroundColorMode()
|
||||
|
||||
nitrousGraphicMgr = rt.NitrousGraphicsManager
|
||||
viewport_setting = nitrousGraphicMgr.GetActiveViewportSetting()
|
||||
vp_button_mgr_original = {
|
||||
key: getattr(rt.ViewportButtonMgr, key) for key in vp_button_mgr
|
||||
}
|
||||
nitrous_manager_original = {
|
||||
key: getattr(nitrousGraphicMgr, key) for key in nitrous_manager
|
||||
}
|
||||
nitrous_viewport_original = {
|
||||
key: getattr(viewport_setting, key) for key in nitrous_viewport
|
||||
}
|
||||
|
||||
try:
|
||||
rt.viewport.setGridVisibility(1, general_viewport["dspGrid"])
|
||||
rt.viewport.EnableSolidBackgroundColorMode(general_viewport["dspBkg"])
|
||||
for key, value in vp_button_mgr.items():
|
||||
setattr(rt.ViewportButtonMgr, key, value)
|
||||
for key, value in nitrous_manager.items():
|
||||
setattr(nitrousGraphicMgr, key, value)
|
||||
for key, value in nitrous_viewport.items():
|
||||
if nitrous_viewport[key] != nitrous_viewport_original[key]:
|
||||
setattr(viewport_setting, key, value)
|
||||
yield
|
||||
|
||||
finally:
|
||||
rt.viewport.setGridVisibility(1, orig_vp_grid)
|
||||
rt.viewport.EnableSolidBackgroundColorMode(orig_vp_bkg)
|
||||
for key, value in vp_button_mgr_original.items():
|
||||
setattr(rt.ViewportButtonMgr, key, value)
|
||||
for key, value in nitrous_manager_original.items():
|
||||
setattr(nitrousGraphicMgr, key, value)
|
||||
for key, value in nitrous_viewport_original.items():
|
||||
setattr(viewport_setting, key, value)
|
||||
|
||||
|
||||
def _render_preview_animation_max_2024(
|
||||
filepath, start, end, percentSize, ext, viewport_options):
|
||||
"""Render viewport preview with MaxScript using `CreateAnimation`.
|
||||
****For 3dsMax 2024+
|
||||
Args:
|
||||
filepath (str): filepath for render output without frame number and
|
||||
extension, for example: /path/to/file
|
||||
start (int): startFrame
|
||||
end (int): endFrame
|
||||
percentSize (float): render resolution multiplier by 100
|
||||
e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x
|
||||
viewport_options (dict): viewport setting options, e.g.
|
||||
{"vpStyle": "defaultshading", "vpPreset": "highquality"}
|
||||
Returns:
|
||||
list: Created files
|
||||
"""
|
||||
# the percentSize argument must be integer
|
||||
percent = int(percentSize)
|
||||
filepath = filepath.replace("\\", "/")
|
||||
preview_output = f"{filepath}..{ext}"
|
||||
frame_template = f"{filepath}.{{:04d}}.{ext}"
|
||||
job_args = []
|
||||
for key, value in viewport_options.items():
|
||||
if isinstance(value, bool):
|
||||
if value:
|
||||
job_args.append(f"{key}:{value}")
|
||||
elif isinstance(value, str):
|
||||
if key == "vpStyle":
|
||||
if value == "Realistic":
|
||||
value = "defaultshading"
|
||||
elif value == "Shaded":
|
||||
log.warning(
|
||||
"'Shaded' Mode not supported in "
|
||||
"preview animation in Max 2024.\n"
|
||||
"Using 'defaultshading' instead.")
|
||||
value = "defaultshading"
|
||||
elif value == "ConsistentColors":
|
||||
value = "flatcolor"
|
||||
else:
|
||||
value = value.lower()
|
||||
elif key == "vpPreset":
|
||||
if value == "Quality":
|
||||
value = "highquality"
|
||||
elif value == "Customize":
|
||||
value = "userdefined"
|
||||
else:
|
||||
value = value.lower()
|
||||
job_args.append(f"{key}: #{value}")
|
||||
|
||||
job_str = (
|
||||
f'CreatePreview filename:"{preview_output}" outputAVI:false '
|
||||
f"percentSize:{percent} start:{start} end:{end} "
|
||||
f"{' '.join(job_args)} "
|
||||
"autoPlay:false"
|
||||
)
|
||||
rt.completeRedraw()
|
||||
rt.execute(job_str)
|
||||
# Return the created files
|
||||
return [frame_template.format(frame) for frame in range(start, end + 1)]
|
||||
|
||||
|
||||
def _render_preview_animation_max_pre_2024(
|
||||
filepath, startFrame, endFrame,
|
||||
width, height, percentSize, ext):
|
||||
"""Render viewport animation by creating bitmaps
|
||||
***For 3dsMax Version <2024
|
||||
Args:
|
||||
filepath (str): filepath without frame numbers and extension
|
||||
startFrame (int): start frame
|
||||
endFrame (int): end frame
|
||||
width (int): render resolution width
|
||||
height (int): render resolution height
|
||||
percentSize (float): render resolution multiplier by 100
|
||||
e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x
|
||||
ext (str): image extension
|
||||
Returns:
|
||||
list: Created filepaths
|
||||
"""
|
||||
|
||||
# get the screenshot
|
||||
percent = percentSize / 100.0
|
||||
res_width = width * percent
|
||||
res_height = height * percent
|
||||
frame_template = "{}.{{:04}}.{}".format(filepath, ext)
|
||||
frame_template.replace("\\", "/")
|
||||
files = []
|
||||
user_cancelled = False
|
||||
for frame in range(startFrame, endFrame + 1):
|
||||
rt.sliderTime = frame
|
||||
filepath = frame_template.format(frame)
|
||||
preview_res = rt.bitmap(
|
||||
res_width, res_height, filename=filepath
|
||||
)
|
||||
dib = rt.gw.getViewportDib()
|
||||
dib_width = float(dib.width)
|
||||
dib_height = float(dib.height)
|
||||
# aspect ratio
|
||||
viewportRatio = dib_width / dib_height
|
||||
renderRatio = float(res_width / res_height)
|
||||
if viewportRatio < renderRatio:
|
||||
heightCrop = (dib_width / renderRatio)
|
||||
topEdge = int((dib_height - heightCrop) / 2.0)
|
||||
tempImage_bmp = rt.bitmap(dib_width, heightCrop)
|
||||
src_box_value = rt.Box2(0, topEdge, dib_width, heightCrop)
|
||||
rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0))
|
||||
rt.copy(tempImage_bmp, preview_res)
|
||||
rt.close(tempImage_bmp)
|
||||
elif viewportRatio > renderRatio:
|
||||
widthCrop = dib_height * renderRatio
|
||||
leftEdge = int((dib_width - widthCrop) / 2.0)
|
||||
tempImage_bmp = rt.bitmap(widthCrop, dib_height)
|
||||
src_box_value = rt.Box2(leftEdge, 0, widthCrop, dib_height)
|
||||
rt.pasteBitmap(dib, tempImage_bmp, src_box_value, rt.Point2(0, 0))
|
||||
rt.copy(tempImage_bmp, preview_res)
|
||||
rt.close(tempImage_bmp)
|
||||
else:
|
||||
rt.copy(dib, preview_res)
|
||||
rt.save(preview_res)
|
||||
rt.close(preview_res)
|
||||
rt.close(dib)
|
||||
files.append(filepath)
|
||||
if rt.keyboard.escPressed:
|
||||
user_cancelled = True
|
||||
break
|
||||
# clean up the cache
|
||||
rt.gc(delayed=True)
|
||||
if user_cancelled:
|
||||
raise RuntimeError("User cancelled rendering of viewport animation.")
|
||||
return files
|
||||
|
||||
|
||||
def render_preview_animation(
|
||||
filepath,
|
||||
ext,
|
||||
camera,
|
||||
start_frame=None,
|
||||
end_frame=None,
|
||||
percentSize=100.0,
|
||||
width=1920,
|
||||
height=1080,
|
||||
viewport_options=None):
|
||||
"""Render camera review animation
|
||||
Args:
|
||||
filepath (str): filepath to render to, without frame number and
|
||||
extension
|
||||
ext (str): output file extension
|
||||
camera (str): viewport camera for preview render
|
||||
start_frame (int): start frame
|
||||
end_frame (int): end frame
|
||||
percentSize (float): render resolution multiplier by 100
|
||||
e.g. 100.0 is 1x, 50.0 is 0.5x, 150.0 is 1.5x
|
||||
width (int): render resolution width
|
||||
height (int): render resolution height
|
||||
viewport_options (dict): viewport setting options
|
||||
Returns:
|
||||
list: Rendered output files
|
||||
"""
|
||||
if start_frame is None:
|
||||
start_frame = int(rt.animationRange.start)
|
||||
if end_frame is None:
|
||||
end_frame = int(rt.animationRange.end)
|
||||
|
||||
if viewport_options is None:
|
||||
viewport_options = viewport_options_for_preview_animation()
|
||||
with play_preview_when_done(False):
|
||||
with viewport_layout_and_camera(camera):
|
||||
if int(get_max_version()) < 2024:
|
||||
with viewport_preference_setting(
|
||||
viewport_options["general_viewport"],
|
||||
viewport_options["nitrous_manager"],
|
||||
viewport_options["nitrous_viewport"],
|
||||
viewport_options["vp_btn_mgr"]
|
||||
):
|
||||
return _render_preview_animation_max_pre_2024(
|
||||
filepath,
|
||||
start_frame,
|
||||
end_frame,
|
||||
width,
|
||||
height,
|
||||
percentSize,
|
||||
ext
|
||||
)
|
||||
else:
|
||||
with render_resolution(width, height):
|
||||
return _render_preview_animation_max_2024(
|
||||
filepath,
|
||||
start_frame,
|
||||
end_frame,
|
||||
percentSize,
|
||||
ext,
|
||||
viewport_options
|
||||
)
|
||||
|
||||
|
||||
def viewport_options_for_preview_animation():
|
||||
"""Get default viewport options for `render_preview_animation`.
|
||||
|
||||
Returns:
|
||||
dict: viewport setting options
|
||||
"""
|
||||
# viewport_options should be the dictionary
|
||||
if int(get_max_version()) < 2024:
|
||||
return {
|
||||
"visualStyleMode": "defaultshading",
|
||||
"viewportPreset": "highquality",
|
||||
"vpTexture": False,
|
||||
"dspGeometry": True,
|
||||
"dspShapes": False,
|
||||
"dspLights": False,
|
||||
"dspCameras": False,
|
||||
"dspHelpers": False,
|
||||
"dspParticles": True,
|
||||
"dspBones": False,
|
||||
"dspBkg": True,
|
||||
"dspGrid": False,
|
||||
"dspSafeFrame": False,
|
||||
"dspFrameNums": False
|
||||
}
|
||||
else:
|
||||
viewport_options = {}
|
||||
viewport_options["general_viewport"] = {
|
||||
"dspBkg": True,
|
||||
"dspGrid": False
|
||||
}
|
||||
viewport_options["nitrous_manager"] = {
|
||||
"AntialiasingQuality": "None"
|
||||
}
|
||||
viewport_options["nitrous_viewport"] = {
|
||||
"VisualStyleMode": "defaultshading",
|
||||
"ViewportPreset": "highquality",
|
||||
"UseTextureEnabled": False
|
||||
}
|
||||
viewport_options["vp_btn_mgr"] = {
|
||||
"EnableButtons": False}
|
||||
return viewport_options
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pre-launch to force 3ds max startup script."""
|
||||
import os
|
||||
from ayon_max import MAX_HOST_DIR
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class ForceStartupScript(PreLaunchHook):
|
||||
"""Inject AYON environment to 3ds max.
|
||||
|
||||
Note that this works in combination whit 3dsmax startup script that
|
||||
is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH
|
||||
environment.
|
||||
|
||||
Hook `GlobalHostDataHook` must be executed before this hook.
|
||||
"""
|
||||
app_groups = {"3dsmax", "adsk_3dsmax"}
|
||||
order = 11
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
startup_args = [
|
||||
"-U",
|
||||
"MAXScript",
|
||||
os.path.join(MAX_HOST_DIR, "startup", "startup.ms"),
|
||||
]
|
||||
self.launch_context.launch_args.append(startup_args)
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Pre-launch hook to inject python environment."""
|
||||
import os
|
||||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class InjectPythonPath(PreLaunchHook):
|
||||
"""Inject AYON environment to 3dsmax.
|
||||
|
||||
Note that this works in combination whit 3dsmax startup script that
|
||||
is translating it back to PYTHONPATH for cases when 3dsmax drops PYTHONPATH
|
||||
environment.
|
||||
|
||||
Hook `GlobalHostDataHook` must be executed before this hook.
|
||||
"""
|
||||
app_groups = {"3dsmax", "adsk_3dsmax"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
self.launch_context.env["MAX_PYTHONPATH"] = os.environ["PYTHONPATH"]
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
from ayon_applications import PreLaunchHook, LaunchTypes
|
||||
|
||||
|
||||
class SetPath(PreLaunchHook):
|
||||
"""Set current dir to workdir.
|
||||
|
||||
Hook `GlobalHostDataHook` must be executed before this hook.
|
||||
"""
|
||||
app_groups = {"max"}
|
||||
launch_types = {LaunchTypes.local}
|
||||
|
||||
def execute(self):
|
||||
workdir = self.launch_context.env.get("AYON_WORKDIR", "")
|
||||
if not workdir:
|
||||
self.log.warning("BUG: Workdir is not filled.")
|
||||
return
|
||||
|
||||
self.launch_context.kwargs["cwd"] = workdir
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating camera."""
|
||||
from ayon_max.api import plugin
|
||||
|
||||
|
||||
class CreateCamera(plugin.MaxCreator):
|
||||
"""Creator plugin for Camera."""
|
||||
identifier = "io.openpype.creators.max.camera"
|
||||
label = "Camera"
|
||||
product_type = "camera"
|
||||
icon = "gear"
|
||||
|
||||
settings_category = "max"
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating raw max scene."""
|
||||
from ayon_max.api import plugin
|
||||
|
||||
|
||||
class CreateMaxScene(plugin.MaxCreator):
|
||||
"""Creator plugin for 3ds max scenes."""
|
||||
identifier = "io.openpype.creators.max.maxScene"
|
||||
label = "Max Scene"
|
||||
product_type = "maxScene"
|
||||
icon = "gear"
|
||||
|
||||
settings_category = "max"
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for model."""
|
||||
from ayon_max.api import plugin
|
||||
|
||||
|
||||
class CreateModel(plugin.MaxCreator):
|
||||
"""Creator plugin for Model."""
|
||||
identifier = "io.openpype.creators.max.model"
|
||||
label = "Model"
|
||||
product_type = "model"
|
||||
icon = "gear"
|
||||
|
||||
settings_category = "max"
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating pointcache alembics."""
|
||||
from ayon_max.api import plugin
|
||||
|
||||
|
||||
class CreatePointCache(plugin.MaxCreator):
|
||||
"""Creator plugin for Point caches."""
|
||||
identifier = "io.openpype.creators.max.pointcache"
|
||||
label = "Point Cache"
|
||||
product_type = "pointcache"
|
||||
icon = "gear"
|
||||
|
||||
settings_category = "max"
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating point cloud."""
|
||||
from ayon_max.api import plugin
|
||||
|
||||
|
||||
class CreatePointCloud(plugin.MaxCreator):
|
||||
"""Creator plugin for Point Clouds."""
|
||||
identifier = "io.openpype.creators.max.pointcloud"
|
||||
label = "Point Cloud"
|
||||
product_type = "pointcloud"
|
||||
icon = "gear"
|
||||
|
||||
settings_category = "max"
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating camera."""
|
||||
from ayon_max.api import plugin
|
||||
|
||||
|
||||
class CreateRedshiftProxy(plugin.MaxCreator):
|
||||
identifier = "io.openpype.creators.max.redshiftproxy"
|
||||
label = "Redshift Proxy"
|
||||
product_type = "redshiftproxy"
|
||||
icon = "gear"
|
||||
|
||||
settings_category = "max"
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating camera."""
|
||||
import os
|
||||
from ayon_max.api import plugin
|
||||
from ayon_core.lib import BoolDef
|
||||
from ayon_max.api.lib_rendersettings import RenderSettings
|
||||
|
||||
|
||||
class CreateRender(plugin.MaxCreator):
|
||||
"""Creator plugin for Renders."""
|
||||
identifier = "io.openpype.creators.max.render"
|
||||
label = "Render"
|
||||
product_type = "maxrender"
|
||||
icon = "gear"
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
from pymxs import runtime as rt
|
||||
file = rt.maxFileName
|
||||
filename, _ = os.path.splitext(file)
|
||||
instance_data["AssetName"] = filename
|
||||
instance_data["multiCamera"] = pre_create_data.get("multi_cam")
|
||||
num_of_renderlayer = rt.batchRenderMgr.numViews
|
||||
if num_of_renderlayer > 0:
|
||||
rt.batchRenderMgr.DeleteView(num_of_renderlayer)
|
||||
|
||||
instance = super(CreateRender, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
container_name = instance.data.get("instance_node")
|
||||
# set output paths for rendering(mandatory for deadline)
|
||||
RenderSettings().render_output(container_name)
|
||||
# TODO: create multiple camera options
|
||||
if self.selected_nodes:
|
||||
selected_nodes_name = []
|
||||
for sel in self.selected_nodes:
|
||||
name = sel.name
|
||||
selected_nodes_name.append(name)
|
||||
RenderSettings().batch_render_layer(
|
||||
container_name, filename,
|
||||
selected_nodes_name)
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
attrs = super(CreateRender, self).get_pre_create_attr_defs()
|
||||
return attrs + [
|
||||
BoolDef("multi_cam",
|
||||
label="Multiple Cameras Submission",
|
||||
default=False),
|
||||
]
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating review in Max."""
|
||||
from ayon_max.api import plugin
|
||||
from ayon_core.lib import BoolDef, EnumDef, NumberDef
|
||||
|
||||
|
||||
class CreateReview(plugin.MaxCreator):
|
||||
"""Review in 3dsMax"""
|
||||
|
||||
identifier = "io.openpype.creators.max.review"
|
||||
label = "Review"
|
||||
product_type = "review"
|
||||
icon = "video-camera"
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
review_width = 1920
|
||||
review_height = 1080
|
||||
percentSize = 100
|
||||
keep_images = False
|
||||
image_format = "png"
|
||||
visual_style = "Realistic"
|
||||
viewport_preset = "Quality"
|
||||
vp_texture = True
|
||||
anti_aliasing = "None"
|
||||
|
||||
def apply_settings(self, project_settings):
|
||||
settings = project_settings["max"]["CreateReview"] # noqa
|
||||
|
||||
# Take some defaults from settings
|
||||
self.review_width = settings.get("review_width", self.review_width)
|
||||
self.review_height = settings.get("review_height", self.review_height)
|
||||
self.percentSize = settings.get("percentSize", self.percentSize)
|
||||
self.keep_images = settings.get("keep_images", self.keep_images)
|
||||
self.image_format = settings.get("image_format", self.image_format)
|
||||
self.visual_style = settings.get("visual_style", self.visual_style)
|
||||
self.viewport_preset = settings.get(
|
||||
"viewport_preset", self.viewport_preset)
|
||||
self.anti_aliasing = settings.get(
|
||||
"anti_aliasing", self.anti_aliasing)
|
||||
self.vp_texture = settings.get("vp_texture", self.vp_texture)
|
||||
|
||||
def create(self, product_name, instance_data, pre_create_data):
|
||||
# Transfer settings from pre create to instance
|
||||
creator_attributes = instance_data.setdefault(
|
||||
"creator_attributes", dict())
|
||||
for key in ["imageFormat",
|
||||
"keepImages",
|
||||
"review_width",
|
||||
"review_height",
|
||||
"percentSize",
|
||||
"visualStyleMode",
|
||||
"viewportPreset",
|
||||
"antialiasingQuality",
|
||||
"vpTexture"]:
|
||||
if key in pre_create_data:
|
||||
creator_attributes[key] = pre_create_data[key]
|
||||
|
||||
super(CreateReview, self).create(
|
||||
product_name,
|
||||
instance_data,
|
||||
pre_create_data)
|
||||
|
||||
def get_instance_attr_defs(self):
|
||||
image_format_enum = ["exr", "jpg", "png", "tga"]
|
||||
|
||||
visual_style_preset_enum = [
|
||||
"Realistic", "Shaded", "Facets",
|
||||
"ConsistentColors", "HiddenLine",
|
||||
"Wireframe", "BoundingBox", "Ink",
|
||||
"ColorInk", "Acrylic", "Tech", "Graphite",
|
||||
"ColorPencil", "Pastel", "Clay", "ModelAssist"
|
||||
]
|
||||
preview_preset_enum = [
|
||||
"Quality", "Standard", "Performance",
|
||||
"DXMode", "Customize"]
|
||||
anti_aliasing_enum = ["None", "2X", "4X", "8X"]
|
||||
|
||||
return [
|
||||
NumberDef("review_width",
|
||||
label="Review width",
|
||||
decimals=0,
|
||||
minimum=0,
|
||||
default=self.review_width),
|
||||
NumberDef("review_height",
|
||||
label="Review height",
|
||||
decimals=0,
|
||||
minimum=0,
|
||||
default=self.review_height),
|
||||
NumberDef("percentSize",
|
||||
label="Percent of Output",
|
||||
default=self.percentSize,
|
||||
minimum=1,
|
||||
decimals=0),
|
||||
BoolDef("keepImages",
|
||||
label="Keep Image Sequences",
|
||||
default=self.keep_images),
|
||||
EnumDef("imageFormat",
|
||||
image_format_enum,
|
||||
default=self.image_format,
|
||||
label="Image Format Options"),
|
||||
EnumDef("visualStyleMode",
|
||||
visual_style_preset_enum,
|
||||
default=self.visual_style,
|
||||
label="Preference"),
|
||||
EnumDef("viewportPreset",
|
||||
preview_preset_enum,
|
||||
default=self.viewport_preset,
|
||||
label="Preview Preset"),
|
||||
EnumDef("antialiasingQuality",
|
||||
anti_aliasing_enum,
|
||||
default=self.anti_aliasing,
|
||||
label="Anti-aliasing Quality"),
|
||||
BoolDef("vpTexture",
|
||||
label="Viewport Texture",
|
||||
default=self.vp_texture)
|
||||
]
|
||||
|
||||
def get_pre_create_attr_defs(self):
|
||||
# Use same attributes as for instance attributes
|
||||
attrs = super().get_pre_create_attr_defs()
|
||||
return attrs + self.get_instance_attr_defs()
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Creator plugin for creating workfiles."""
|
||||
import ayon_api
|
||||
|
||||
from ayon_core.pipeline import CreatedInstance, AutoCreator
|
||||
from ayon_max.api import plugin
|
||||
from ayon_max.api.lib import read, imprint
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class CreateWorkfile(plugin.MaxCreatorBase, AutoCreator):
|
||||
"""Workfile auto-creator."""
|
||||
identifier = "io.ayon.creators.max.workfile"
|
||||
label = "Workfile"
|
||||
product_type = "workfile"
|
||||
icon = "fa5.file"
|
||||
|
||||
default_variant = "Main"
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
def create(self):
|
||||
variant = self.default_variant
|
||||
current_instance = next(
|
||||
(
|
||||
instance for instance in self.create_context.instances
|
||||
if instance.creator_identifier == self.identifier
|
||||
), None)
|
||||
project_name = self.project_name
|
||||
folder_path = self.create_context.get_current_folder_path()
|
||||
task_name = self.create_context.get_current_task_name()
|
||||
host_name = self.create_context.host_name
|
||||
|
||||
if current_instance is None:
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name, folder_path
|
||||
)
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name, folder_entity["id"], task_name
|
||||
)
|
||||
product_name = self.get_product_name(
|
||||
project_name,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
variant,
|
||||
host_name,
|
||||
)
|
||||
data = {
|
||||
"folderPath": folder_path,
|
||||
"task": task_name,
|
||||
"variant": variant
|
||||
}
|
||||
|
||||
data.update(
|
||||
self.get_dynamic_data(
|
||||
project_name,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
variant,
|
||||
host_name,
|
||||
current_instance)
|
||||
)
|
||||
self.log.info("Auto-creating workfile instance...")
|
||||
instance_node = self.create_node(product_name)
|
||||
data["instance_node"] = instance_node.name
|
||||
current_instance = CreatedInstance(
|
||||
self.product_type, product_name, data, self
|
||||
)
|
||||
self._add_instance_to_context(current_instance)
|
||||
imprint(instance_node.name, current_instance.data)
|
||||
elif (
|
||||
current_instance["folderPath"] != folder_path
|
||||
or current_instance["task"] != task_name
|
||||
):
|
||||
# Update instance context if is not the same
|
||||
folder_entity = ayon_api.get_folder_by_path(
|
||||
project_name, folder_path
|
||||
)
|
||||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name, folder_entity["id"], task_name
|
||||
)
|
||||
product_name = self.get_product_name(
|
||||
project_name,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
variant,
|
||||
host_name,
|
||||
)
|
||||
|
||||
current_instance["folderPath"] = folder_entity["path"]
|
||||
current_instance["task"] = task_name
|
||||
current_instance["productName"] = product_name
|
||||
|
||||
def collect_instances(self):
|
||||
self.cache_instance_data(self.collection_shared_data)
|
||||
cached_instances = self.collection_shared_data["max_cached_instances"]
|
||||
for instance in cached_instances.get(self.identifier, []):
|
||||
if not rt.getNodeByName(instance):
|
||||
continue
|
||||
created_instance = CreatedInstance.from_existing(
|
||||
read(rt.GetNodeByName(instance)), self
|
||||
)
|
||||
self._add_instance_to_context(created_instance)
|
||||
|
||||
def update_instances(self, update_list):
|
||||
for created_inst, _ in update_list:
|
||||
instance_node = created_inst.get("instance_node")
|
||||
imprint(
|
||||
instance_node,
|
||||
created_inst.data_to_store()
|
||||
)
|
||||
|
||||
def create_node(self, product_name):
|
||||
if rt.getNodeByName(product_name):
|
||||
node = rt.getNodeByName(product_name)
|
||||
return node
|
||||
node = rt.Container(name=product_name)
|
||||
node.isHidden = True
|
||||
return node
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import os
|
||||
|
||||
from ayon_max.api import lib
|
||||
from ayon_max.api.lib import (
|
||||
unique_namespace,
|
||||
get_namespace,
|
||||
object_transform_set
|
||||
)
|
||||
from ayon_max.api.pipeline import (
|
||||
containerise,
|
||||
get_previous_loaded_object,
|
||||
update_custom_attribute_data,
|
||||
remove_container_data
|
||||
)
|
||||
from ayon_core.pipeline import get_representation_path, load
|
||||
|
||||
|
||||
class FbxLoader(load.LoaderPlugin):
|
||||
"""Fbx Loader."""
|
||||
|
||||
product_types = {"camera"}
|
||||
representations = {"fbx"}
|
||||
order = -9
|
||||
icon = "code-fork"
|
||||
color = "white"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
from pymxs import runtime as rt
|
||||
filepath = self.filepath_from_context(context)
|
||||
filepath = os.path.normpath(filepath)
|
||||
rt.FBXImporterSetParam("Animation", True)
|
||||
rt.FBXImporterSetParam("Camera", True)
|
||||
rt.FBXImporterSetParam("AxisConversionMethod", True)
|
||||
rt.FBXImporterSetParam("Mode", rt.Name("create"))
|
||||
rt.FBXImporterSetParam("Preserveinstances", True)
|
||||
rt.ImportFile(
|
||||
filepath,
|
||||
rt.name("noPrompt"),
|
||||
using=rt.FBXIMP)
|
||||
|
||||
namespace = unique_namespace(
|
||||
name + "_",
|
||||
suffix="_",
|
||||
)
|
||||
selections = rt.GetCurrentSelection()
|
||||
|
||||
for selection in selections:
|
||||
selection.name = f"{namespace}:{selection.name}"
|
||||
|
||||
return containerise(
|
||||
name, selections, context,
|
||||
namespace, loader=self.__class__.__name__)
|
||||
|
||||
def update(self, container, context):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
repre_entity = context["representation"]
|
||||
path = get_representation_path(repre_entity)
|
||||
node_name = container["instance_node"]
|
||||
node = rt.getNodeByName(node_name)
|
||||
namespace, _ = get_namespace(node_name)
|
||||
|
||||
node_list = get_previous_loaded_object(node)
|
||||
rt.Select(node_list)
|
||||
prev_fbx_objects = rt.GetCurrentSelection()
|
||||
transform_data = object_transform_set(prev_fbx_objects)
|
||||
for prev_fbx_obj in prev_fbx_objects:
|
||||
if rt.isValidNode(prev_fbx_obj):
|
||||
rt.Delete(prev_fbx_obj)
|
||||
|
||||
rt.FBXImporterSetParam("Animation", True)
|
||||
rt.FBXImporterSetParam("Camera", True)
|
||||
rt.FBXImporterSetParam("Mode", rt.Name("merge"))
|
||||
rt.FBXImporterSetParam("AxisConversionMethod", True)
|
||||
rt.FBXImporterSetParam("Preserveinstances", True)
|
||||
rt.ImportFile(
|
||||
path, rt.name("noPrompt"), using=rt.FBXIMP)
|
||||
current_fbx_objects = rt.GetCurrentSelection()
|
||||
fbx_objects = []
|
||||
for fbx_object in current_fbx_objects:
|
||||
fbx_object.name = f"{namespace}:{fbx_object.name}"
|
||||
fbx_objects.append(fbx_object)
|
||||
fbx_transform = f"{fbx_object.name}.transform"
|
||||
if fbx_transform in transform_data.keys():
|
||||
fbx_object.pos = transform_data[fbx_transform] or 0
|
||||
fbx_object.scale = transform_data[
|
||||
f"{fbx_object.name}.scale"] or 0
|
||||
|
||||
update_custom_attribute_data(node, fbx_objects)
|
||||
lib.imprint(container["instance_node"], {
|
||||
"representation": repre_entity["id"]
|
||||
})
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
||||
def remove(self, container):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
remove_container_data(node)
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
import os
|
||||
from qtpy import QtWidgets, QtCore
|
||||
from ayon_core.lib.attribute_definitions import EnumDef
|
||||
from ayon_max.api import lib
|
||||
from ayon_max.api.lib import (
|
||||
unique_namespace,
|
||||
get_namespace,
|
||||
object_transform_set,
|
||||
is_headless
|
||||
)
|
||||
from ayon_max.api.pipeline import (
|
||||
containerise, get_previous_loaded_object,
|
||||
update_custom_attribute_data,
|
||||
remove_container_data
|
||||
)
|
||||
from ayon_core.pipeline import get_representation_path, load
|
||||
|
||||
|
||||
class MaterialDupOptionsWindow(QtWidgets.QDialog):
|
||||
"""The pop-up dialog allows users to choose material
|
||||
duplicate options for importing Max objects when updating
|
||||
or switching assets.
|
||||
"""
|
||||
def __init__(self, material_options):
|
||||
super(MaterialDupOptionsWindow, self).__init__()
|
||||
self.setWindowFlags(self.windowFlags() | QtCore.Qt.FramelessWindowHint)
|
||||
|
||||
self.material_option = None
|
||||
self.material_options = material_options
|
||||
|
||||
self.widgets = {
|
||||
"label": QtWidgets.QLabel(
|
||||
"Select material duplicate options before loading the max scene."),
|
||||
"material_options_list": QtWidgets.QListWidget(),
|
||||
"warning": QtWidgets.QLabel("No material options selected!"),
|
||||
"buttons": QtWidgets.QWidget(),
|
||||
"okButton": QtWidgets.QPushButton("Ok"),
|
||||
"cancelButton": QtWidgets.QPushButton("Cancel")
|
||||
}
|
||||
for key, value in material_options.items():
|
||||
item = QtWidgets.QListWidgetItem(value)
|
||||
self.widgets["material_options_list"].addItem(item)
|
||||
item.setData(QtCore.Qt.UserRole, key)
|
||||
# Build buttons.
|
||||
layout = QtWidgets.QHBoxLayout(self.widgets["buttons"])
|
||||
layout.addWidget(self.widgets["okButton"])
|
||||
layout.addWidget(self.widgets["cancelButton"])
|
||||
# Build layout.
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(self.widgets["label"])
|
||||
layout.addWidget(self.widgets["material_options_list"])
|
||||
layout.addWidget(self.widgets["buttons"])
|
||||
|
||||
self.widgets["okButton"].pressed.connect(self.on_ok_pressed)
|
||||
self.widgets["cancelButton"].pressed.connect(self.on_cancel_pressed)
|
||||
self.widgets["material_options_list"].itemPressed.connect(
|
||||
self.on_material_options_pressed)
|
||||
|
||||
def on_material_options_pressed(self, item):
|
||||
self.material_option = item.data(QtCore.Qt.UserRole)
|
||||
|
||||
def on_ok_pressed(self):
|
||||
if self.material_option is None:
|
||||
self.widgets["warning"].setVisible(True)
|
||||
return
|
||||
self.close()
|
||||
|
||||
def on_cancel_pressed(self):
|
||||
self.material_option = "promptMtlDups"
|
||||
self.close()
|
||||
|
||||
class MaxSceneLoader(load.LoaderPlugin):
|
||||
"""Max Scene Loader."""
|
||||
|
||||
product_types = {
|
||||
"camera",
|
||||
"maxScene",
|
||||
"model",
|
||||
}
|
||||
|
||||
representations = {"max"}
|
||||
order = -8
|
||||
icon = "code-fork"
|
||||
color = "green"
|
||||
mtl_dup_default = "promptMtlDups"
|
||||
mtl_dup_enum_dict = {
|
||||
"promptMtlDups": "Prompt on Duplicate Materials",
|
||||
"useMergedMtlDups": "Use Incoming Material",
|
||||
"useSceneMtlDups": "Use Scene Material",
|
||||
"renameMtlDups": "Merge and Rename Incoming Material"
|
||||
}
|
||||
@classmethod
|
||||
def get_options(cls, contexts):
|
||||
return [
|
||||
EnumDef("mtldup",
|
||||
items=cls.mtl_dup_enum_dict,
|
||||
default=cls.mtl_dup_default,
|
||||
label="Material Duplicate Options")
|
||||
]
|
||||
|
||||
def load(self, context, name=None, namespace=None, options=None):
|
||||
from pymxs import runtime as rt
|
||||
mat_dup_options = options.get("mtldup", self.mtl_dup_default)
|
||||
path = self.filepath_from_context(context)
|
||||
path = os.path.normpath(path)
|
||||
# import the max scene by using "merge file"
|
||||
path = path.replace('\\', '/')
|
||||
rt.MergeMaxFile(path, rt.Name(mat_dup_options),
|
||||
quiet=True, includeFullGroup=True)
|
||||
max_objects = rt.getLastMergedNodes()
|
||||
max_object_names = [obj.name for obj in max_objects]
|
||||
# implement the OP/AYON custom attributes before load
|
||||
max_container = []
|
||||
namespace = unique_namespace(
|
||||
name + "_",
|
||||
suffix="_",
|
||||
)
|
||||
for max_obj, obj_name in zip(max_objects, max_object_names):
|
||||
max_obj.name = f"{namespace}:{obj_name}"
|
||||
max_container.append(max_obj)
|
||||
return containerise(
|
||||
name, max_container, context,
|
||||
namespace, loader=self.__class__.__name__)
|
||||
|
||||
def update(self, container, context):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
repre_entity = context["representation"]
|
||||
path = get_representation_path(repre_entity)
|
||||
node_name = container["instance_node"]
|
||||
node = rt.getNodeByName(node_name)
|
||||
namespace, _ = get_namespace(node_name)
|
||||
# delete the old container with attribute
|
||||
# delete old duplicate
|
||||
# use the modifier OP data to delete the data
|
||||
node_list = get_previous_loaded_object(node)
|
||||
rt.select(node_list)
|
||||
prev_max_objects = rt.GetCurrentSelection()
|
||||
transform_data = object_transform_set(prev_max_objects)
|
||||
|
||||
for prev_max_obj in prev_max_objects:
|
||||
if rt.isValidNode(prev_max_obj): # noqa
|
||||
rt.Delete(prev_max_obj)
|
||||
material_option = self.mtl_dup_default
|
||||
if not is_headless():
|
||||
window = MaterialDupOptionsWindow(self.mtl_dup_enum_dict)
|
||||
window.exec_()
|
||||
material_option = window.material_option
|
||||
rt.MergeMaxFile(path, rt.Name(material_option), quiet=True)
|
||||
|
||||
current_max_objects = rt.getLastMergedNodes()
|
||||
|
||||
current_max_object_names = [obj.name for obj
|
||||
in current_max_objects]
|
||||
|
||||
max_objects = []
|
||||
for max_obj, obj_name in zip(current_max_objects,
|
||||
current_max_object_names):
|
||||
max_obj.name = f"{namespace}:{obj_name}"
|
||||
max_objects.append(max_obj)
|
||||
max_transform = f"{max_obj}.transform"
|
||||
if max_transform in transform_data.keys():
|
||||
max_obj.pos = transform_data[max_transform] or 0
|
||||
max_obj.scale = transform_data[
|
||||
f"{max_obj}.scale"] or 0
|
||||
|
||||
update_custom_attribute_data(node, max_objects)
|
||||
lib.imprint(container["instance_node"], {
|
||||
"representation": repre_entity["id"]
|
||||
})
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
||||
def remove(self, container):
|
||||
from pymxs import runtime as rt
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
remove_container_data(node)
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import os
|
||||
from ayon_core.pipeline import load, get_representation_path
|
||||
from ayon_max.api.pipeline import (
|
||||
containerise,
|
||||
get_previous_loaded_object,
|
||||
remove_container_data
|
||||
)
|
||||
from ayon_max.api import lib
|
||||
from ayon_max.api.lib import (
|
||||
maintained_selection, unique_namespace
|
||||
)
|
||||
|
||||
|
||||
class ModelAbcLoader(load.LoaderPlugin):
|
||||
"""Loading model with the Alembic loader."""
|
||||
|
||||
product_types = {"model"}
|
||||
label = "Load Model with Alembic"
|
||||
representations = {"abc"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
file_path = os.path.normpath(self.filepath_from_context(context))
|
||||
|
||||
abc_before = {
|
||||
c
|
||||
for c in rt.rootNode.Children
|
||||
if rt.classOf(c) == rt.AlembicContainer
|
||||
}
|
||||
|
||||
rt.AlembicImport.ImportToRoot = False
|
||||
rt.AlembicImport.CustomAttributes = True
|
||||
rt.AlembicImport.UVs = True
|
||||
rt.AlembicImport.VertexColors = True
|
||||
rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport)
|
||||
|
||||
abc_after = {
|
||||
c
|
||||
for c in rt.rootNode.Children
|
||||
if rt.classOf(c) == rt.AlembicContainer
|
||||
}
|
||||
|
||||
# This should yield new AlembicContainer node
|
||||
abc_containers = abc_after.difference(abc_before)
|
||||
|
||||
if len(abc_containers) != 1:
|
||||
self.log.error("Something failed when loading.")
|
||||
|
||||
abc_container = abc_containers.pop()
|
||||
|
||||
namespace = unique_namespace(
|
||||
name + "_",
|
||||
suffix="_",
|
||||
)
|
||||
abc_objects = []
|
||||
for abc_object in abc_container.Children:
|
||||
abc_object.name = f"{namespace}:{abc_object.name}"
|
||||
abc_objects.append(abc_object)
|
||||
# rename the abc container with namespace
|
||||
abc_container_name = f"{namespace}:{name}"
|
||||
abc_container.name = abc_container_name
|
||||
abc_objects.append(abc_container)
|
||||
|
||||
return containerise(
|
||||
name, abc_objects, context,
|
||||
namespace, loader=self.__class__.__name__
|
||||
)
|
||||
|
||||
def update(self, container, context):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
repre_entity = context["representation"]
|
||||
path = get_representation_path(repre_entity)
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
node_list = [n for n in get_previous_loaded_object(node)
|
||||
if rt.ClassOf(n) == rt.AlembicContainer]
|
||||
with maintained_selection():
|
||||
rt.Select(node_list)
|
||||
|
||||
for alembic in rt.Selection:
|
||||
abc = rt.GetNodeByName(alembic.name)
|
||||
rt.Select(abc.Children)
|
||||
for abc_con in abc.Children:
|
||||
abc_con.source = path
|
||||
rt.Select(abc_con.Children)
|
||||
for abc_obj in abc_con.Children:
|
||||
abc_obj.source = path
|
||||
lib.imprint(
|
||||
container["instance_node"],
|
||||
{"representation": repre_entity["id"]},
|
||||
)
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
||||
def remove(self, container):
|
||||
from pymxs import runtime as rt
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
remove_container_data(node)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_container_children(parent, type_name):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
def list_children(node):
|
||||
children = []
|
||||
for c in node.Children:
|
||||
children.append(c)
|
||||
children += list_children(c)
|
||||
return children
|
||||
|
||||
filtered = []
|
||||
for child in list_children(parent):
|
||||
class_type = str(rt.ClassOf(child.baseObject))
|
||||
if class_type == type_name:
|
||||
filtered.append(child)
|
||||
|
||||
return filtered
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import os
|
||||
from ayon_core.pipeline import load, get_representation_path
|
||||
from ayon_max.api.pipeline import (
|
||||
containerise, get_previous_loaded_object,
|
||||
update_custom_attribute_data,
|
||||
remove_container_data
|
||||
)
|
||||
from ayon_max.api import lib
|
||||
from ayon_max.api.lib import (
|
||||
unique_namespace,
|
||||
get_namespace,
|
||||
object_transform_set
|
||||
)
|
||||
from ayon_max.api.lib import maintained_selection
|
||||
|
||||
|
||||
class FbxModelLoader(load.LoaderPlugin):
|
||||
"""Fbx Model Loader."""
|
||||
|
||||
product_types = {"model"}
|
||||
representations = {"fbx"}
|
||||
order = -9
|
||||
icon = "code-fork"
|
||||
color = "white"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
from pymxs import runtime as rt
|
||||
filepath = self.filepath_from_context(context)
|
||||
filepath = os.path.normpath(filepath)
|
||||
rt.FBXImporterSetParam("Animation", False)
|
||||
rt.FBXImporterSetParam("Cameras", False)
|
||||
rt.FBXImporterSetParam("Mode", rt.Name("create"))
|
||||
rt.FBXImporterSetParam("Preserveinstances", True)
|
||||
rt.importFile(
|
||||
filepath, rt.name("noPrompt"), using=rt.FBXIMP)
|
||||
|
||||
namespace = unique_namespace(
|
||||
name + "_",
|
||||
suffix="_",
|
||||
)
|
||||
selections = rt.GetCurrentSelection()
|
||||
|
||||
for selection in selections:
|
||||
selection.name = f"{namespace}:{selection.name}"
|
||||
|
||||
return containerise(
|
||||
name, selections, context,
|
||||
namespace, loader=self.__class__.__name__)
|
||||
|
||||
def update(self, container, context):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
repre_entity = context["representation"]
|
||||
path = get_representation_path(repre_entity)
|
||||
node_name = container["instance_node"]
|
||||
node = rt.getNodeByName(node_name)
|
||||
if not node:
|
||||
rt.Container(name=node_name)
|
||||
namespace, _ = get_namespace(node_name)
|
||||
|
||||
node_list = get_previous_loaded_object(node)
|
||||
rt.Select(node_list)
|
||||
prev_fbx_objects = rt.GetCurrentSelection()
|
||||
transform_data = object_transform_set(prev_fbx_objects)
|
||||
for prev_fbx_obj in prev_fbx_objects:
|
||||
if rt.isValidNode(prev_fbx_obj):
|
||||
rt.Delete(prev_fbx_obj)
|
||||
|
||||
rt.FBXImporterSetParam("Animation", False)
|
||||
rt.FBXImporterSetParam("Cameras", False)
|
||||
rt.FBXImporterSetParam("Mode", rt.Name("create"))
|
||||
rt.FBXImporterSetParam("Preserveinstances", True)
|
||||
rt.importFile(path, rt.name("noPrompt"), using=rt.FBXIMP)
|
||||
current_fbx_objects = rt.GetCurrentSelection()
|
||||
fbx_objects = []
|
||||
for fbx_object in current_fbx_objects:
|
||||
fbx_object.name = f"{namespace}:{fbx_object.name}"
|
||||
fbx_objects.append(fbx_object)
|
||||
fbx_transform = f"{fbx_object}.transform"
|
||||
if fbx_transform in transform_data.keys():
|
||||
fbx_object.pos = transform_data[fbx_transform] or 0
|
||||
fbx_object.scale = transform_data[
|
||||
f"{fbx_object}.scale"] or 0
|
||||
|
||||
with maintained_selection():
|
||||
rt.Select(node)
|
||||
update_custom_attribute_data(node, fbx_objects)
|
||||
lib.imprint(container["instance_node"], {
|
||||
"representation": repre_entity["id"]
|
||||
})
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
||||
def remove(self, container):
|
||||
from pymxs import runtime as rt
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
remove_container_data(node)
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
import os
|
||||
|
||||
from ayon_max.api import lib
|
||||
from ayon_max.api.lib import (
|
||||
unique_namespace,
|
||||
get_namespace,
|
||||
maintained_selection,
|
||||
object_transform_set
|
||||
)
|
||||
from ayon_max.api.pipeline import (
|
||||
containerise,
|
||||
get_previous_loaded_object,
|
||||
update_custom_attribute_data,
|
||||
remove_container_data
|
||||
)
|
||||
from ayon_core.pipeline import get_representation_path, load
|
||||
|
||||
|
||||
class ObjLoader(load.LoaderPlugin):
|
||||
"""Obj Loader."""
|
||||
|
||||
product_types = {"model"}
|
||||
representations = {"obj"}
|
||||
order = -9
|
||||
icon = "code-fork"
|
||||
color = "white"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
filepath = os.path.normpath(self.filepath_from_context(context))
|
||||
self.log.debug("Executing command to import..")
|
||||
|
||||
rt.Execute(f'importFile @"{filepath}" #noPrompt using:ObjImp')
|
||||
|
||||
namespace = unique_namespace(
|
||||
name + "_",
|
||||
suffix="_",
|
||||
)
|
||||
# create "missing" container for obj import
|
||||
selections = rt.GetCurrentSelection()
|
||||
# get current selection
|
||||
for selection in selections:
|
||||
selection.name = f"{namespace}:{selection.name}"
|
||||
return containerise(
|
||||
name, selections, context,
|
||||
namespace, loader=self.__class__.__name__)
|
||||
|
||||
def update(self, container, context):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
repre_entity = context["representation"]
|
||||
path = get_representation_path(repre_entity)
|
||||
node_name = container["instance_node"]
|
||||
node = rt.getNodeByName(node_name)
|
||||
namespace, _ = get_namespace(node_name)
|
||||
node_list = get_previous_loaded_object(node)
|
||||
rt.Select(node_list)
|
||||
previous_objects = rt.GetCurrentSelection()
|
||||
transform_data = object_transform_set(previous_objects)
|
||||
for prev_obj in previous_objects:
|
||||
if rt.isValidNode(prev_obj):
|
||||
rt.Delete(prev_obj)
|
||||
|
||||
rt.Execute(f'importFile @"{path}" #noPrompt using:ObjImp')
|
||||
# get current selection
|
||||
selections = rt.GetCurrentSelection()
|
||||
for selection in selections:
|
||||
selection.name = f"{namespace}:{selection.name}"
|
||||
selection_transform = f"{selection}.transform"
|
||||
if selection_transform in transform_data.keys():
|
||||
selection.pos = transform_data[selection_transform] or 0
|
||||
selection.scale = transform_data[
|
||||
f"{selection}.scale"] or 0
|
||||
update_custom_attribute_data(node, selections)
|
||||
with maintained_selection():
|
||||
rt.Select(node)
|
||||
|
||||
lib.imprint(node_name, {
|
||||
"representation": repre_entity["id"]
|
||||
})
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
||||
def remove(self, container):
|
||||
from pymxs import runtime as rt
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
remove_container_data(node)
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
import os
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from ayon_core.pipeline.load import LoadError
|
||||
from ayon_max.api import lib
|
||||
from ayon_max.api.lib import (
|
||||
unique_namespace,
|
||||
get_namespace,
|
||||
object_transform_set,
|
||||
get_plugins
|
||||
)
|
||||
from ayon_max.api.lib import maintained_selection
|
||||
from ayon_max.api.pipeline import (
|
||||
containerise,
|
||||
get_previous_loaded_object,
|
||||
update_custom_attribute_data,
|
||||
remove_container_data
|
||||
)
|
||||
from ayon_core.pipeline import get_representation_path, load
|
||||
|
||||
|
||||
class ModelUSDLoader(load.LoaderPlugin):
|
||||
"""Loading model with the USD loader."""
|
||||
|
||||
product_types = {"model"}
|
||||
label = "Load Model(USD)"
|
||||
representations = {"usda"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
# asset_filepath
|
||||
plugin_info = get_plugins()
|
||||
if "usdimport.dli" not in plugin_info:
|
||||
raise LoadError("No USDImporter loaded/installed in Max..")
|
||||
filepath = os.path.normpath(self.filepath_from_context(context))
|
||||
import_options = rt.USDImporter.CreateOptions()
|
||||
base_filename = os.path.basename(filepath)
|
||||
_, ext = os.path.splitext(base_filename)
|
||||
log_filepath = filepath.replace(ext, "txt")
|
||||
|
||||
rt.LogPath = log_filepath
|
||||
rt.LogLevel = rt.Name("info")
|
||||
rt.USDImporter.importFile(filepath,
|
||||
importOptions=import_options)
|
||||
namespace = unique_namespace(
|
||||
name + "_",
|
||||
suffix="_",
|
||||
)
|
||||
asset = rt.GetNodeByName(name)
|
||||
usd_objects = []
|
||||
|
||||
for usd_asset in asset.Children:
|
||||
usd_asset.name = f"{namespace}:{usd_asset.name}"
|
||||
usd_objects.append(usd_asset)
|
||||
|
||||
asset_name = f"{namespace}:{name}"
|
||||
asset.name = asset_name
|
||||
# need to get the correct container after renamed
|
||||
asset = rt.GetNodeByName(asset_name)
|
||||
usd_objects.append(asset)
|
||||
|
||||
return containerise(
|
||||
name, usd_objects, context,
|
||||
namespace, loader=self.__class__.__name__)
|
||||
|
||||
def update(self, container, context):
|
||||
repre_entity = context["representation"]
|
||||
path = get_representation_path(repre_entity)
|
||||
node_name = container["instance_node"]
|
||||
node = rt.GetNodeByName(node_name)
|
||||
namespace, name = get_namespace(node_name)
|
||||
node_list = get_previous_loaded_object(node)
|
||||
rt.Select(node_list)
|
||||
prev_objects = [sel for sel in rt.GetCurrentSelection()
|
||||
if sel != rt.Container
|
||||
and sel.name != node_name]
|
||||
transform_data = object_transform_set(prev_objects)
|
||||
for n in prev_objects:
|
||||
rt.Delete(n)
|
||||
|
||||
import_options = rt.USDImporter.CreateOptions()
|
||||
base_filename = os.path.basename(path)
|
||||
_, ext = os.path.splitext(base_filename)
|
||||
log_filepath = path.replace(ext, "txt")
|
||||
|
||||
rt.LogPath = log_filepath
|
||||
rt.LogLevel = rt.Name("info")
|
||||
rt.USDImporter.importFile(
|
||||
path, importOptions=import_options)
|
||||
|
||||
asset = rt.GetNodeByName(name)
|
||||
usd_objects = []
|
||||
for children in asset.Children:
|
||||
children.name = f"{namespace}:{children.name}"
|
||||
usd_objects.append(children)
|
||||
children_transform = f"{children}.transform"
|
||||
if children_transform in transform_data.keys():
|
||||
children.pos = transform_data[children_transform] or 0
|
||||
children.scale = transform_data[
|
||||
f"{children}.scale"] or 0
|
||||
|
||||
asset.name = f"{namespace}:{asset.name}"
|
||||
usd_objects.append(asset)
|
||||
update_custom_attribute_data(node, usd_objects)
|
||||
with maintained_selection():
|
||||
rt.Select(node)
|
||||
|
||||
lib.imprint(node_name, {
|
||||
"representation": repre_entity["id"]
|
||||
})
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
||||
def remove(self, container):
|
||||
from pymxs import runtime as rt
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
remove_container_data(node)
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Simple alembic loader for 3dsmax.
|
||||
|
||||
Because of limited api, alembics can be only loaded, but not easily updated.
|
||||
|
||||
"""
|
||||
import os
|
||||
from ayon_core.pipeline import load, get_representation_path
|
||||
from ayon_max.api import lib, maintained_selection
|
||||
from ayon_max.api.lib import unique_namespace, reset_frame_range
|
||||
from ayon_max.api.pipeline import (
|
||||
containerise,
|
||||
get_previous_loaded_object,
|
||||
remove_container_data
|
||||
)
|
||||
|
||||
|
||||
class AbcLoader(load.LoaderPlugin):
|
||||
"""Alembic loader."""
|
||||
|
||||
product_types = {"camera", "animation", "pointcache"}
|
||||
label = "Load Alembic"
|
||||
representations = {"abc"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
file_path = self.filepath_from_context(context)
|
||||
file_path = os.path.normpath(file_path)
|
||||
|
||||
abc_before = {
|
||||
c
|
||||
for c in rt.rootNode.Children
|
||||
if rt.classOf(c) == rt.AlembicContainer
|
||||
}
|
||||
|
||||
rt.AlembicImport.ImportToRoot = False
|
||||
# TODO: it will be removed after the improvement
|
||||
# on the post-system setup
|
||||
reset_frame_range()
|
||||
rt.importFile(file_path, rt.name("noPrompt"), using=rt.AlembicImport)
|
||||
|
||||
abc_after = {
|
||||
c
|
||||
for c in rt.rootNode.Children
|
||||
if rt.classOf(c) == rt.AlembicContainer
|
||||
}
|
||||
|
||||
# This should yield new AlembicContainer node
|
||||
abc_containers = abc_after.difference(abc_before)
|
||||
|
||||
if len(abc_containers) != 1:
|
||||
self.log.error("Something failed when loading.")
|
||||
|
||||
abc_container = abc_containers.pop()
|
||||
selections = rt.GetCurrentSelection()
|
||||
for abc in selections:
|
||||
for cam_shape in abc.Children:
|
||||
cam_shape.playbackType = 0
|
||||
|
||||
namespace = unique_namespace(
|
||||
name + "_",
|
||||
suffix="_",
|
||||
)
|
||||
abc_objects = []
|
||||
for abc_object in abc_container.Children:
|
||||
abc_object.name = f"{namespace}:{abc_object.name}"
|
||||
abc_objects.append(abc_object)
|
||||
# rename the abc container with namespace
|
||||
abc_container_name = f"{namespace}:{name}"
|
||||
abc_container.name = abc_container_name
|
||||
abc_objects.append(abc_container)
|
||||
|
||||
return containerise(
|
||||
name, abc_objects, context,
|
||||
namespace, loader=self.__class__.__name__
|
||||
)
|
||||
|
||||
def update(self, container, context):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
repre_entity = context["representation"]
|
||||
path = get_representation_path(repre_entity)
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
abc_container = [n for n in get_previous_loaded_object(node)
|
||||
if rt.ClassOf(n) == rt.AlembicContainer]
|
||||
with maintained_selection():
|
||||
rt.Select(abc_container)
|
||||
|
||||
for alembic in rt.Selection:
|
||||
abc = rt.GetNodeByName(alembic.name)
|
||||
rt.Select(abc.Children)
|
||||
for abc_con in abc.Children:
|
||||
abc_con.source = path
|
||||
rt.Select(abc_con.Children)
|
||||
for abc_obj in abc_con.Children:
|
||||
abc_obj.source = path
|
||||
lib.imprint(
|
||||
container["instance_node"],
|
||||
{"representation": repre_entity["id"]},
|
||||
)
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
||||
def remove(self, container):
|
||||
from pymxs import runtime as rt
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
remove_container_data(node)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def get_container_children(parent, type_name):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
def list_children(node):
|
||||
children = []
|
||||
for c in node.Children:
|
||||
children.append(c)
|
||||
children += list_children(c)
|
||||
return children
|
||||
|
||||
filtered = []
|
||||
for child in list_children(parent):
|
||||
class_type = str(rt.classOf(child.baseObject))
|
||||
if class_type == type_name:
|
||||
filtered.append(child)
|
||||
|
||||
return filtered
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
import os
|
||||
from ayon_core.pipeline import load, get_representation_path
|
||||
from ayon_core.pipeline.load import LoadError
|
||||
from ayon_max.api.pipeline import (
|
||||
containerise,
|
||||
get_previous_loaded_object,
|
||||
update_custom_attribute_data,
|
||||
remove_container_data
|
||||
)
|
||||
|
||||
from ayon_max.api.lib import (
|
||||
unique_namespace,
|
||||
get_namespace,
|
||||
object_transform_set,
|
||||
get_plugins
|
||||
)
|
||||
from ayon_max.api import lib
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class OxAbcLoader(load.LoaderPlugin):
|
||||
"""Ornatrix Alembic loader."""
|
||||
|
||||
product_types = {"camera", "animation", "pointcache"}
|
||||
label = "Load Alembic with Ornatrix"
|
||||
representations = {"abc"}
|
||||
order = -10
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
postfix = "param"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
plugin_list = get_plugins()
|
||||
if "ephere.plugins.autodesk.max.ornatrix.dlo" not in plugin_list:
|
||||
raise LoadError("Ornatrix plugin not "
|
||||
"found/installed in Max yet..")
|
||||
|
||||
file_path = os.path.normpath(self.filepath_from_context(context))
|
||||
rt.AlembicImport.ImportToRoot = True
|
||||
rt.AlembicImport.CustomAttributes = True
|
||||
rt.importFile(
|
||||
file_path, rt.name("noPrompt"),
|
||||
using=rt.Ornatrix_Alembic_Importer)
|
||||
|
||||
scene_object = []
|
||||
for obj in rt.rootNode.Children:
|
||||
obj_type = rt.ClassOf(obj)
|
||||
if str(obj_type).startswith("Ox_"):
|
||||
scene_object.append(obj)
|
||||
|
||||
namespace = unique_namespace(
|
||||
name + "_",
|
||||
suffix="_",
|
||||
)
|
||||
abc_container = []
|
||||
for abc in scene_object:
|
||||
abc.name = f"{namespace}:{abc.name}"
|
||||
abc_container.append(abc)
|
||||
|
||||
return containerise(
|
||||
name, abc_container, context,
|
||||
namespace, loader=self.__class__.__name__
|
||||
)
|
||||
|
||||
def update(self, container, context):
|
||||
repre_entity = context["representation"]
|
||||
path = get_representation_path(repre_entity)
|
||||
node_name = container["instance_node"]
|
||||
namespace, name = get_namespace(node_name)
|
||||
node = rt.getNodeByName(node_name)
|
||||
node_list = get_previous_loaded_object(node)
|
||||
rt.Select(node_list)
|
||||
selections = rt.getCurrentSelection()
|
||||
transform_data = object_transform_set(selections)
|
||||
for prev_obj in selections:
|
||||
if rt.isValidNode(prev_obj):
|
||||
rt.Delete(prev_obj)
|
||||
|
||||
rt.AlembicImport.ImportToRoot = False
|
||||
rt.AlembicImport.CustomAttributes = True
|
||||
rt.importFile(
|
||||
path, rt.name("noPrompt"),
|
||||
using=rt.Ornatrix_Alembic_Importer)
|
||||
|
||||
scene_object = []
|
||||
for obj in rt.rootNode.Children:
|
||||
obj_type = rt.ClassOf(obj)
|
||||
if str(obj_type).startswith("Ox_"):
|
||||
scene_object.append(obj)
|
||||
ox_abc_objects = []
|
||||
for abc in scene_object:
|
||||
abc.Parent = container
|
||||
abc.name = f"{namespace}:{abc.name}"
|
||||
ox_abc_objects.append(abc)
|
||||
ox_transform = f"{abc}.transform"
|
||||
if ox_transform in transform_data.keys():
|
||||
abc.pos = transform_data[ox_transform] or 0
|
||||
abc.scale = transform_data[f"{abc}.scale"] or 0
|
||||
update_custom_attribute_data(node, ox_abc_objects)
|
||||
lib.imprint(
|
||||
container["instance_node"],
|
||||
{"representation": repre_entity["id"]},
|
||||
)
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
||||
def remove(self, container):
|
||||
from pymxs import runtime as rt
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
remove_container_data(node)
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
import os
|
||||
|
||||
from ayon_max.api import lib, maintained_selection
|
||||
from ayon_max.api.lib import (
|
||||
unique_namespace,
|
||||
|
||||
)
|
||||
from ayon_max.api.pipeline import (
|
||||
containerise,
|
||||
get_previous_loaded_object,
|
||||
update_custom_attribute_data,
|
||||
remove_container_data
|
||||
)
|
||||
from ayon_core.pipeline import get_representation_path, load
|
||||
|
||||
|
||||
class PointCloudLoader(load.LoaderPlugin):
|
||||
"""Point Cloud Loader."""
|
||||
|
||||
product_types = {"pointcloud"}
|
||||
representations = {"prt"}
|
||||
order = -8
|
||||
icon = "code-fork"
|
||||
color = "green"
|
||||
postfix = "param"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
"""load point cloud by tyCache"""
|
||||
from pymxs import runtime as rt
|
||||
filepath = os.path.normpath(self.filepath_from_context(context))
|
||||
obj = rt.tyCache()
|
||||
obj.filename = filepath
|
||||
|
||||
namespace = unique_namespace(
|
||||
name + "_",
|
||||
suffix="_",
|
||||
)
|
||||
obj.name = f"{namespace}:{obj.name}"
|
||||
|
||||
return containerise(
|
||||
name, [obj], context,
|
||||
namespace, loader=self.__class__.__name__)
|
||||
|
||||
def update(self, container, context):
|
||||
"""update the container"""
|
||||
from pymxs import runtime as rt
|
||||
|
||||
repre_entity = context["representation"]
|
||||
path = get_representation_path(repre_entity)
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
node_list = get_previous_loaded_object(node)
|
||||
update_custom_attribute_data(
|
||||
node, node_list)
|
||||
with maintained_selection():
|
||||
rt.Select(node_list)
|
||||
for prt in rt.Selection:
|
||||
prt.filename = path
|
||||
lib.imprint(container["instance_node"], {
|
||||
"representation": repre_entity["id"]
|
||||
})
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
||||
def remove(self, container):
|
||||
"""remove the container"""
|
||||
from pymxs import runtime as rt
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
remove_container_data(node)
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
import os
|
||||
import clique
|
||||
|
||||
from ayon_core.pipeline import (
|
||||
load,
|
||||
get_representation_path
|
||||
)
|
||||
from ayon_core.pipeline.load import LoadError
|
||||
from ayon_max.api.pipeline import (
|
||||
containerise,
|
||||
update_custom_attribute_data,
|
||||
get_previous_loaded_object,
|
||||
remove_container_data
|
||||
)
|
||||
from ayon_max.api import lib
|
||||
from ayon_max.api.lib import (
|
||||
unique_namespace,
|
||||
get_plugins
|
||||
)
|
||||
|
||||
|
||||
class RedshiftProxyLoader(load.LoaderPlugin):
|
||||
"""Load rs files with Redshift Proxy"""
|
||||
|
||||
label = "Load Redshift Proxy"
|
||||
product_types = {"redshiftproxy"}
|
||||
representations = {"rs"}
|
||||
order = -9
|
||||
icon = "code-fork"
|
||||
color = "white"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
from pymxs import runtime as rt
|
||||
plugin_info = get_plugins()
|
||||
if "redshift4max.dlr" not in plugin_info:
|
||||
raise LoadError("Redshift not loaded/installed in Max..")
|
||||
filepath = self.filepath_from_context(context)
|
||||
rs_proxy = rt.RedshiftProxy()
|
||||
rs_proxy.file = filepath
|
||||
files_in_folder = os.listdir(os.path.dirname(filepath))
|
||||
collections, remainder = clique.assemble(files_in_folder)
|
||||
if collections:
|
||||
rs_proxy.is_sequence = True
|
||||
|
||||
namespace = unique_namespace(
|
||||
name + "_",
|
||||
suffix="_",
|
||||
)
|
||||
rs_proxy.name = f"{namespace}:{rs_proxy.name}"
|
||||
|
||||
return containerise(
|
||||
name, [rs_proxy], context,
|
||||
namespace, loader=self.__class__.__name__)
|
||||
|
||||
def update(self, container, context):
|
||||
from pymxs import runtime as rt
|
||||
|
||||
repre_entity = context["representation"]
|
||||
path = get_representation_path(repre_entity)
|
||||
node = rt.getNodeByName(container["instance_node"])
|
||||
node_list = get_previous_loaded_object(node)
|
||||
rt.Select(node_list)
|
||||
update_custom_attribute_data(
|
||||
node, rt.Selection)
|
||||
for proxy in rt.Selection:
|
||||
proxy.file = path
|
||||
|
||||
lib.imprint(container["instance_node"], {
|
||||
"representation": repre_entity["id"]
|
||||
})
|
||||
|
||||
def switch(self, container, context):
|
||||
self.update(container, context)
|
||||
|
||||
def remove(self, container):
|
||||
from pymxs import runtime as rt
|
||||
node = rt.GetNodeByName(container["instance_node"])
|
||||
remove_container_data(node)
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class CollectCurrentFile(pyblish.api.ContextPlugin):
|
||||
"""Inject the current working file."""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.5
|
||||
label = "Max Current File"
|
||||
hosts = ['max']
|
||||
|
||||
def process(self, context):
|
||||
"""Inject the current working file"""
|
||||
folder = rt.maxFilePath
|
||||
file = rt.maxFileName
|
||||
if not folder or not file:
|
||||
self.log.error("Scene is not saved.")
|
||||
current_file = os.path.join(folder, file)
|
||||
|
||||
context.data["currentFile"] = current_file
|
||||
self.log.debug("Scene path: {}".format(current_file))
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect Render"""
|
||||
import os
|
||||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from ayon_core.pipeline.publish import KnownPublishError
|
||||
from ayon_max.api import colorspace
|
||||
from ayon_max.api.lib import get_max_version, get_current_renderer
|
||||
from ayon_max.api.lib_rendersettings import RenderSettings
|
||||
from ayon_max.api.lib_renderproducts import RenderProducts
|
||||
|
||||
|
||||
class CollectRender(pyblish.api.InstancePlugin):
|
||||
"""Collect Render for Deadline"""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.02
|
||||
label = "Collect 3dsmax Render Layers"
|
||||
hosts = ['max']
|
||||
families = ["maxrender"]
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
folder = rt.maxFilePath
|
||||
file = rt.maxFileName
|
||||
current_file = os.path.join(folder, file)
|
||||
filepath = current_file.replace("\\", "/")
|
||||
context.data['currentFile'] = current_file
|
||||
|
||||
files_by_aov = RenderProducts().get_beauty(instance.name)
|
||||
aovs = RenderProducts().get_aovs(instance.name)
|
||||
files_by_aov.update(aovs)
|
||||
|
||||
camera = rt.viewport.GetCamera()
|
||||
if instance.data.get("members"):
|
||||
camera_list = [member for member in instance.data["members"]
|
||||
if rt.ClassOf(member) == rt.Camera.Classes]
|
||||
if camera_list:
|
||||
camera = camera_list[-1]
|
||||
|
||||
instance.data["cameras"] = [camera.name] if camera else None # noqa
|
||||
|
||||
if instance.data.get("multiCamera"):
|
||||
cameras = instance.data.get("members")
|
||||
if not cameras:
|
||||
raise KnownPublishError("There should be at least"
|
||||
" one renderable camera in container")
|
||||
sel_cam = [
|
||||
c.name for c in cameras
|
||||
if rt.classOf(c) in rt.Camera.classes]
|
||||
container_name = instance.data.get("instance_node")
|
||||
render_dir = os.path.dirname(rt.rendOutputFilename)
|
||||
outputs = RenderSettings().batch_render_layer(
|
||||
container_name, render_dir, sel_cam
|
||||
)
|
||||
|
||||
instance.data["cameras"] = sel_cam
|
||||
|
||||
files_by_aov = RenderProducts().get_multiple_beauty(
|
||||
outputs, sel_cam)
|
||||
aovs = RenderProducts().get_multiple_aovs(
|
||||
outputs, sel_cam)
|
||||
files_by_aov.update(aovs)
|
||||
|
||||
if "expectedFiles" not in instance.data:
|
||||
instance.data["expectedFiles"] = list()
|
||||
instance.data["files"] = list()
|
||||
instance.data["expectedFiles"].append(files_by_aov)
|
||||
instance.data["files"].append(files_by_aov)
|
||||
|
||||
img_format = RenderProducts().image_format()
|
||||
# OCIO config not support in
|
||||
# most of the 3dsmax renderers
|
||||
# so this is currently hard coded
|
||||
# TODO: add options for redshift/vray ocio config
|
||||
instance.data["colorspaceConfig"] = ""
|
||||
instance.data["colorspaceDisplay"] = "sRGB"
|
||||
instance.data["colorspaceView"] = "ACES 1.0 SDR-video"
|
||||
|
||||
if int(get_max_version()) >= 2024:
|
||||
colorspace_mgr = rt.ColorPipelineMgr # noqa
|
||||
display = next(
|
||||
(display for display in colorspace_mgr.GetDisplayList()))
|
||||
view_transform = next(
|
||||
(view for view in colorspace_mgr.GetViewList(display)))
|
||||
instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath
|
||||
instance.data["colorspaceDisplay"] = display
|
||||
instance.data["colorspaceView"] = view_transform
|
||||
|
||||
instance.data["renderProducts"] = colorspace.ARenderProduct()
|
||||
instance.data["publishJobState"] = "Suspended"
|
||||
instance.data["attachTo"] = []
|
||||
renderer_class = get_current_renderer()
|
||||
renderer = str(renderer_class).split(":")[0]
|
||||
product_type = "maxrender"
|
||||
# also need to get the render dir for conversion
|
||||
data = {
|
||||
"folderPath": instance.data["folderPath"],
|
||||
"productName": str(instance.name),
|
||||
"publish": True,
|
||||
"maxversion": str(get_max_version()),
|
||||
"imageFormat": img_format,
|
||||
"productType": product_type,
|
||||
"family": product_type,
|
||||
"families": [product_type],
|
||||
"renderer": renderer,
|
||||
"source": filepath,
|
||||
"plugin": "3dsmax",
|
||||
"frameStart": instance.data["frameStartHandle"],
|
||||
"frameEnd": instance.data["frameEndHandle"],
|
||||
"farm": True
|
||||
}
|
||||
instance.data.update(data)
|
||||
|
||||
# TODO: this should be unified with maya and its "multipart" flag
|
||||
# on instance.
|
||||
if renderer == "Redshift_Renderer":
|
||||
instance.data.update(
|
||||
{"separateAovFiles": rt.Execute(
|
||||
"renderers.current.separateAovFiles")})
|
||||
|
||||
self.log.info("data: {0}".format(data))
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
# dont forget getting the focal length for burnin
|
||||
"""Collect Review"""
|
||||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from ayon_core.lib import BoolDef
|
||||
from ayon_max.api.lib import get_max_version
|
||||
from ayon_core.pipeline.publish import (
|
||||
AYONPyblishPluginMixin,
|
||||
KnownPublishError
|
||||
)
|
||||
|
||||
|
||||
class CollectReview(pyblish.api.InstancePlugin,
|
||||
AYONPyblishPluginMixin):
|
||||
"""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"]
|
||||
|
||||
def is_camera(node):
|
||||
is_camera_class = rt.classOf(node) in rt.Camera.classes
|
||||
return is_camera_class and rt.isProperty(node, "fov")
|
||||
|
||||
# Use first camera in instance
|
||||
cameras = [node for node in nodes if is_camera(node)]
|
||||
if cameras:
|
||||
if len(cameras) > 1:
|
||||
self.log.warning(
|
||||
"Found more than one camera in instance, using first "
|
||||
f"one found: {cameras[0]}"
|
||||
)
|
||||
camera = cameras[0]
|
||||
camera_name = camera.name
|
||||
focal_length = camera.fov
|
||||
else:
|
||||
raise KnownPublishError(
|
||||
"Unable to find a valid camera in 'Review' container."
|
||||
" Only native max Camera supported. "
|
||||
f"Found objects: {nodes}"
|
||||
)
|
||||
creator_attrs = instance.data["creator_attributes"]
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
|
||||
general_preview_data = {
|
||||
"review_camera": camera_name,
|
||||
"frameStart": instance.data["frameStartHandle"],
|
||||
"frameEnd": instance.data["frameEndHandle"],
|
||||
"percentSize": creator_attrs["percentSize"],
|
||||
"imageFormat": creator_attrs["imageFormat"],
|
||||
"keepImages": creator_attrs["keepImages"],
|
||||
"fps": instance.context.data["fps"],
|
||||
"review_width": creator_attrs["review_width"],
|
||||
"review_height": creator_attrs["review_height"],
|
||||
}
|
||||
|
||||
if int(get_max_version()) >= 2024:
|
||||
colorspace_mgr = rt.ColorPipelineMgr # noqa
|
||||
display = next(
|
||||
(display for display in colorspace_mgr.GetDisplayList()))
|
||||
view_transform = next(
|
||||
(view for view in colorspace_mgr.GetViewList(display)))
|
||||
instance.data["colorspaceConfig"] = colorspace_mgr.OCIOConfigPath
|
||||
instance.data["colorspaceDisplay"] = display
|
||||
instance.data["colorspaceView"] = view_transform
|
||||
|
||||
preview_data = {
|
||||
"vpStyle": creator_attrs["visualStyleMode"],
|
||||
"vpPreset": creator_attrs["viewportPreset"],
|
||||
"vpTextures": creator_attrs["vpTexture"],
|
||||
"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")
|
||||
}
|
||||
else:
|
||||
general_viewport = {
|
||||
"dspBkg": attr_values.get("dspBkg"),
|
||||
"dspGrid": attr_values.get("dspGrid")
|
||||
}
|
||||
nitrous_manager = {
|
||||
"AntialiasingQuality": creator_attrs["antialiasingQuality"],
|
||||
}
|
||||
nitrous_viewport = {
|
||||
"VisualStyleMode": creator_attrs["visualStyleMode"],
|
||||
"ViewportPreset": creator_attrs["viewportPreset"],
|
||||
"UseTextureEnabled": creator_attrs["vpTexture"]
|
||||
}
|
||||
preview_data = {
|
||||
"general_viewport": general_viewport,
|
||||
"nitrous_manager": nitrous_manager,
|
||||
"nitrous_viewport": nitrous_viewport,
|
||||
"vp_btn_mgr": {"EnableButtons": False}
|
||||
}
|
||||
|
||||
# Enable ftrack functionality
|
||||
instance.data.setdefault("families", []).append('ftrack')
|
||||
|
||||
burnin_members = instance.data.setdefault("burninDataMembers", {})
|
||||
burnin_members["focalLength"] = focal_length
|
||||
|
||||
instance.data.update(general_preview_data)
|
||||
instance.data["viewport_options"] = preview_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)
|
||||
]
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect current work file."""
|
||||
import os
|
||||
import pyblish.api
|
||||
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class CollectWorkfile(pyblish.api.InstancePlugin):
|
||||
"""Inject the current working file into context"""
|
||||
|
||||
order = pyblish.api.CollectorOrder - 0.01
|
||||
label = "Collect 3dsmax Workfile"
|
||||
hosts = ['max']
|
||||
families = ["workfile"]
|
||||
|
||||
def process(self, instance):
|
||||
"""Inject the current working file."""
|
||||
context = instance.context
|
||||
folder = rt.maxFilePath
|
||||
file = rt.maxFileName
|
||||
if not folder or not file:
|
||||
self.log.error("Scene is not saved.")
|
||||
ext = os.path.splitext(file)[-1].lstrip(".")
|
||||
|
||||
data = {}
|
||||
|
||||
data.update({
|
||||
"setMembers": context.data["currentFile"],
|
||||
"frameStart": context.data["frameStart"],
|
||||
"frameEnd": context.data["frameEnd"],
|
||||
"handleStart": context.data["handleStart"],
|
||||
"handleEnd": context.data["handleEnd"]
|
||||
})
|
||||
|
||||
data["representations"] = [{
|
||||
"name": ext,
|
||||
"ext": ext,
|
||||
"files": file,
|
||||
"stagingDir": folder,
|
||||
}]
|
||||
|
||||
instance.data.update(data)
|
||||
self.log.debug("Collected data: {}".format(data))
|
||||
self.log.debug("Collected instance: {}".format(file))
|
||||
self.log.debug("staging Dir: {}".format(folder))
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Export alembic file.
|
||||
|
||||
Note:
|
||||
Parameters on AlembicExport (AlembicExport.Parameter):
|
||||
|
||||
ParticleAsMesh (bool): Sets whether particle shapes are exported
|
||||
as meshes.
|
||||
AnimTimeRange (enum): How animation is saved:
|
||||
#CurrentFrame: saves current frame
|
||||
#TimeSlider: saves the active time segments on time slider (default)
|
||||
#StartEnd: saves a range specified by the Step
|
||||
StartFrame (int)
|
||||
EnFrame (int)
|
||||
ShapeSuffix (bool): When set to true, appends the string "Shape" to the
|
||||
name of each exported mesh. This property is set to false by default.
|
||||
SamplesPerFrame (int): Sets the number of animation samples per frame.
|
||||
Hidden (bool): When true, export hidden geometry.
|
||||
UVs (bool): When true, export the mesh UV map channel.
|
||||
Normals (bool): When true, export the mesh normals.
|
||||
VertexColors (bool): When true, export the mesh vertex color map 0 and the
|
||||
current vertex color display data when it differs
|
||||
ExtraChannels (bool): When true, export the mesh extra map channels
|
||||
(map channels greater than channel 1)
|
||||
Velocity (bool): When true, export the meh vertex and particle velocity
|
||||
data.
|
||||
MaterialIDs (bool): When true, export the mesh material ID as
|
||||
Alembic face sets.
|
||||
Visibility (bool): When true, export the node visibility data.
|
||||
LayerName (bool): When true, export the node layer name as an Alembic
|
||||
object property.
|
||||
MaterialName (bool): When true, export the geometry node material name as
|
||||
an Alembic object property
|
||||
ObjectID (bool): When true, export the geometry node g-buffer object ID as
|
||||
an Alembic object property.
|
||||
CustomAttributes (bool): When true, export the node and its modifiers
|
||||
custom attributes into an Alembic object compound property.
|
||||
"""
|
||||
import os
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import publish, OptionalPyblishPluginMixin
|
||||
from pymxs import runtime as rt
|
||||
from ayon_max.api import maintained_selection
|
||||
from ayon_max.api.lib import suspended_refresh
|
||||
from ayon_core.lib import BoolDef
|
||||
|
||||
|
||||
class ExtractAlembic(publish.Extractor,
|
||||
OptionalPyblishPluginMixin):
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract Pointcache"
|
||||
hosts = ["max"]
|
||||
families = ["pointcache"]
|
||||
optional = True
|
||||
active = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
parent_dir = self.staging_dir(instance)
|
||||
file_name = "{name}.abc".format(**instance.data)
|
||||
path = os.path.join(parent_dir, file_name)
|
||||
|
||||
with suspended_refresh():
|
||||
self._set_abc_attributes(instance)
|
||||
with maintained_selection():
|
||||
# select and export
|
||||
node_list = instance.data["members"]
|
||||
rt.Select(node_list)
|
||||
rt.exportFile(
|
||||
path,
|
||||
rt.name("noPrompt"),
|
||||
selectedOnly=True,
|
||||
using=rt.AlembicExport,
|
||||
)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
"name": "abc",
|
||||
"ext": "abc",
|
||||
"files": file_name,
|
||||
"stagingDir": parent_dir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
def _set_abc_attributes(self, instance):
|
||||
start = instance.data["frameStartHandle"]
|
||||
end = instance.data["frameEndHandle"]
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
custom_attrs = attr_values.get("custom_attrs", False)
|
||||
if not custom_attrs:
|
||||
self.log.debug(
|
||||
"No Custom Attributes included in this abc export...")
|
||||
rt.AlembicExport.ArchiveType = rt.Name("ogawa")
|
||||
rt.AlembicExport.CoordinateSystem = rt.Name("maya")
|
||||
rt.AlembicExport.StartFrame = start
|
||||
rt.AlembicExport.EndFrame = end
|
||||
rt.AlembicExport.CustomAttributes = custom_attrs
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
defs = super(ExtractAlembic, cls).get_attribute_defs()
|
||||
defs.extend([
|
||||
BoolDef("custom_attrs",
|
||||
label="Custom Attributes",
|
||||
default=False),
|
||||
])
|
||||
return defs
|
||||
|
||||
|
||||
class ExtractCameraAlembic(ExtractAlembic):
|
||||
"""Extract Camera with AlembicExport."""
|
||||
label = "Extract Alembic Camera"
|
||||
families = ["camera"]
|
||||
optional = True
|
||||
|
||||
|
||||
class ExtractModelAlembic(ExtractAlembic):
|
||||
"""Extract Geometry in Alembic Format"""
|
||||
label = "Extract Geometry (Alembic)"
|
||||
families = ["model"]
|
||||
optional = True
|
||||
|
||||
def _set_abc_attributes(self, instance):
|
||||
attr_values = self.get_attr_values_from_data(instance.data)
|
||||
custom_attrs = attr_values.get("custom_attrs", False)
|
||||
if not custom_attrs:
|
||||
self.log.debug(
|
||||
"No Custom Attributes included in this abc export...")
|
||||
rt.AlembicExport.ArchiveType = rt.name("ogawa")
|
||||
rt.AlembicExport.CoordinateSystem = rt.name("maya")
|
||||
rt.AlembicExport.CustomAttributes = custom_attrs
|
||||
rt.AlembicExport.UVs = True
|
||||
rt.AlembicExport.VertexColors = True
|
||||
rt.AlembicExport.PreserveInstances = True
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import publish, OptionalPyblishPluginMixin
|
||||
from pymxs import runtime as rt
|
||||
from ayon_max.api import maintained_selection
|
||||
from ayon_max.api.lib import convert_unit_scale
|
||||
|
||||
|
||||
class ExtractModelFbx(publish.Extractor, OptionalPyblishPluginMixin):
|
||||
"""
|
||||
Extract Geometry in FBX Format
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.05
|
||||
label = "Extract FBX"
|
||||
hosts = ["max"]
|
||||
families = ["model"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.fbx".format(**instance.data)
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
self._set_fbx_attributes()
|
||||
|
||||
with maintained_selection():
|
||||
# select and export
|
||||
node_list = instance.data["members"]
|
||||
rt.Select(node_list)
|
||||
rt.exportFile(
|
||||
filepath,
|
||||
rt.name("noPrompt"),
|
||||
selectedOnly=True,
|
||||
using=rt.FBXEXP,
|
||||
)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
"name": "fbx",
|
||||
"ext": "fbx",
|
||||
"files": filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info(
|
||||
"Extracted instance '%s' to: %s" % (instance.name, filepath)
|
||||
)
|
||||
|
||||
def _set_fbx_attributes(self):
|
||||
unit_scale = convert_unit_scale()
|
||||
rt.FBXExporterSetParam("Animation", False)
|
||||
rt.FBXExporterSetParam("Cameras", False)
|
||||
rt.FBXExporterSetParam("Lights", False)
|
||||
rt.FBXExporterSetParam("PointCache", False)
|
||||
rt.FBXExporterSetParam("AxisConversionMethod", "Animation")
|
||||
rt.FBXExporterSetParam("UpAxis", "Y")
|
||||
rt.FBXExporterSetParam("Preserveinstances", True)
|
||||
if unit_scale:
|
||||
rt.FBXExporterSetParam("ConvertUnit", unit_scale)
|
||||
|
||||
|
||||
class ExtractCameraFbx(ExtractModelFbx):
|
||||
"""Extract Camera with FbxExporter."""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.2
|
||||
label = "Extract Fbx Camera"
|
||||
families = ["camera"]
|
||||
optional = True
|
||||
|
||||
def _set_fbx_attributes(self):
|
||||
unit_scale = convert_unit_scale()
|
||||
rt.FBXExporterSetParam("Animation", True)
|
||||
rt.FBXExporterSetParam("Cameras", True)
|
||||
rt.FBXExporterSetParam("AxisConversionMethod", "Animation")
|
||||
rt.FBXExporterSetParam("UpAxis", "Y")
|
||||
rt.FBXExporterSetParam("Preserveinstances", True)
|
||||
if unit_scale:
|
||||
rt.FBXExporterSetParam("ConvertUnit", unit_scale)
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import publish, OptionalPyblishPluginMixin
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class ExtractMaxSceneRaw(publish.Extractor, OptionalPyblishPluginMixin):
|
||||
"""
|
||||
Extract Raw Max Scene with SaveSelected
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.2
|
||||
label = "Extract Max Scene (Raw)"
|
||||
hosts = ["max"]
|
||||
families = ["camera", "maxScene", "model"]
|
||||
optional = True
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
# publish the raw scene for camera
|
||||
self.log.debug("Extracting Raw Max Scene ...")
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.max".format(**instance.data)
|
||||
|
||||
max_path = os.path.join(stagingdir, filename)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
nodes = instance.data["members"]
|
||||
rt.saveNodes(nodes, max_path, quiet=True)
|
||||
|
||||
self.log.info("Performing Extraction ...")
|
||||
|
||||
representation = {
|
||||
"name": "max",
|
||||
"ext": "max",
|
||||
"files": filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info(
|
||||
"Extracted instance '%s' to: %s" % (instance.name, max_path)
|
||||
)
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import publish, OptionalPyblishPluginMixin
|
||||
from pymxs import runtime as rt
|
||||
from ayon_max.api import maintained_selection
|
||||
from ayon_max.api.lib import suspended_refresh
|
||||
from ayon_core.pipeline.publish import KnownPublishError
|
||||
|
||||
|
||||
class ExtractModelObj(publish.Extractor, OptionalPyblishPluginMixin):
|
||||
"""
|
||||
Extract Geometry in OBJ Format
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.05
|
||||
label = "Extract OBJ"
|
||||
hosts = ["max"]
|
||||
families = ["model"]
|
||||
optional = True
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
filename = "{name}.obj".format(**instance.data)
|
||||
filepath = os.path.join(stagingdir, filename)
|
||||
|
||||
with suspended_refresh():
|
||||
with maintained_selection():
|
||||
# select and export
|
||||
node_list = instance.data["members"]
|
||||
rt.Select(node_list)
|
||||
rt.exportFile(
|
||||
filepath,
|
||||
rt.name("noPrompt"),
|
||||
selectedOnly=True,
|
||||
using=rt.ObjExp,
|
||||
)
|
||||
if not os.path.exists(filepath):
|
||||
raise KnownPublishError(
|
||||
"File {} wasn't produced by 3ds max, please check the logs.")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
"name": "obj",
|
||||
"ext": "obj",
|
||||
"files": filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info(
|
||||
"Extracted instance '%s' to: %s" % (instance.name, filepath)
|
||||
)
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import os
|
||||
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from ayon_max.api import maintained_selection
|
||||
from ayon_core.pipeline import OptionalPyblishPluginMixin, publish
|
||||
|
||||
|
||||
class ExtractModelUSD(publish.Extractor,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Extract Geometry in USDA Format."""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.05
|
||||
label = "Extract Geometry (USD)"
|
||||
hosts = ["max"]
|
||||
families = ["model"]
|
||||
optional = True
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
self.log.info("Extracting Geometry ...")
|
||||
|
||||
stagingdir = self.staging_dir(instance)
|
||||
asset_filename = "{name}.usda".format(**instance.data)
|
||||
asset_filepath = os.path.join(stagingdir,
|
||||
asset_filename)
|
||||
self.log.info(f"Writing USD '{asset_filepath}' to '{stagingdir}'")
|
||||
|
||||
log_filename = "{name}.txt".format(**instance.data)
|
||||
log_filepath = os.path.join(stagingdir,
|
||||
log_filename)
|
||||
self.log.info(f"Writing log '{log_filepath}' to '{stagingdir}'")
|
||||
|
||||
# get the nodes which need to be exported
|
||||
export_options = self.get_export_options(log_filepath)
|
||||
with maintained_selection():
|
||||
# select and export
|
||||
node_list = instance.data["members"]
|
||||
rt.Select(node_list)
|
||||
rt.USDExporter.ExportFile(asset_filepath,
|
||||
exportOptions=export_options,
|
||||
contentSource=rt.Name("selected"),
|
||||
nodeList=node_list)
|
||||
|
||||
self.log.info("Performing Extraction ...")
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'usda',
|
||||
'ext': 'usda',
|
||||
'files': asset_filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
log_representation = {
|
||||
'name': 'txt',
|
||||
'ext': 'txt',
|
||||
'files': log_filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(log_representation)
|
||||
|
||||
self.log.info(
|
||||
f"Extracted instance '{instance.name}' to: {asset_filepath}")
|
||||
|
||||
@staticmethod
|
||||
def get_export_options(log_path):
|
||||
"""Set Export Options for USD Exporter"""
|
||||
|
||||
export_options = rt.USDExporter.createOptions()
|
||||
|
||||
export_options.Meshes = True
|
||||
export_options.Shapes = False
|
||||
export_options.Lights = False
|
||||
export_options.Cameras = False
|
||||
export_options.Materials = False
|
||||
export_options.MeshFormat = rt.Name('fromScene')
|
||||
export_options.FileFormat = rt.Name('ascii')
|
||||
export_options.UpAxis = rt.Name('y')
|
||||
export_options.LogLevel = rt.Name('info')
|
||||
export_options.LogPath = log_path
|
||||
export_options.PreserveEdgeOrientation = True
|
||||
export_options.TimeMode = rt.Name('current')
|
||||
|
||||
rt.USDexporter.UIOptions = export_options
|
||||
|
||||
return export_options
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import publish
|
||||
from pymxs import runtime as rt
|
||||
from ayon_max.api import maintained_selection
|
||||
|
||||
|
||||
class ExtractRedshiftProxy(publish.Extractor):
|
||||
"""
|
||||
Extract Redshift Proxy with rsProxy
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder - 0.1
|
||||
label = "Extract RedShift Proxy"
|
||||
hosts = ["max"]
|
||||
families = ["redshiftproxy"]
|
||||
|
||||
def process(self, instance):
|
||||
start = instance.data["frameStartHandle"]
|
||||
end = instance.data["frameEndHandle"]
|
||||
|
||||
self.log.debug("Extracting Redshift Proxy...")
|
||||
stagingdir = self.staging_dir(instance)
|
||||
rs_filename = "{name}.rs".format(**instance.data)
|
||||
rs_filepath = os.path.join(stagingdir, rs_filename)
|
||||
rs_filepath = rs_filepath.replace("\\", "/")
|
||||
|
||||
rs_filenames = self.get_rsfiles(instance, start, end)
|
||||
|
||||
with maintained_selection():
|
||||
# select and export
|
||||
node_list = instance.data["members"]
|
||||
rt.Select(node_list)
|
||||
# Redshift rsProxy command
|
||||
# rsProxy fp selected compress connectivity startFrame endFrame
|
||||
# camera warnExisting transformPivotToOrigin
|
||||
rt.rsProxy(rs_filepath, 1, 0, 0, start, end, 0, 1, 1)
|
||||
|
||||
self.log.info("Performing Extraction ...")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': 'rs',
|
||||
'ext': 'rs',
|
||||
'files': rs_filenames if len(rs_filenames) > 1 else rs_filenames[0], # noqa
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
self.log.info("Extracted instance '%s' to: %s" % (instance.name,
|
||||
stagingdir))
|
||||
|
||||
def get_rsfiles(self, instance, startFrame, endFrame):
|
||||
rs_filenames = []
|
||||
rs_name = instance.data["name"]
|
||||
for frame in range(startFrame, endFrame + 1):
|
||||
rs_filename = "%s.%04d.rs" % (rs_name, frame)
|
||||
rs_filenames.append(rs_filename)
|
||||
|
||||
return rs_filenames
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_max.api.preview_animation import (
|
||||
render_preview_animation
|
||||
)
|
||||
|
||||
|
||||
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")
|
||||
start = int(instance.data["frameStart"])
|
||||
end = int(instance.data["frameEnd"])
|
||||
filepath = os.path.join(staging_dir, instance.name)
|
||||
self.log.debug(
|
||||
"Writing Review Animation to '{}'".format(filepath))
|
||||
|
||||
review_camera = instance.data["review_camera"]
|
||||
viewport_options = instance.data.get("viewport_options", {})
|
||||
files = render_preview_animation(
|
||||
filepath,
|
||||
ext,
|
||||
review_camera,
|
||||
start,
|
||||
end,
|
||||
percentSize=instance.data["percentSize"],
|
||||
width=instance.data["review_width"],
|
||||
height=instance.data["review_height"],
|
||||
viewport_options=viewport_options)
|
||||
|
||||
filenames = [os.path.basename(path) for path in files]
|
||||
|
||||
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["frameStartHandle"],
|
||||
"frameEnd": instance.data["frameEndHandle"],
|
||||
"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)
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import publish
|
||||
from ayon_max.api.preview_animation import render_preview_animation
|
||||
|
||||
|
||||
class ExtractThumbnail(publish.Extractor):
|
||||
"""Extract Thumbnail for Review
|
||||
"""
|
||||
|
||||
order = pyblish.api.ExtractorOrder
|
||||
label = "Extract Thumbnail"
|
||||
hosts = ["max"]
|
||||
families = ["review"]
|
||||
|
||||
def process(self, instance):
|
||||
ext = instance.data.get("imageFormat")
|
||||
frame = int(instance.data["frameStart"])
|
||||
staging_dir = self.staging_dir(instance)
|
||||
filepath = os.path.join(
|
||||
staging_dir, f"{instance.name}_thumbnail")
|
||||
self.log.debug("Writing Thumbnail to '{}'".format(filepath))
|
||||
|
||||
review_camera = instance.data["review_camera"]
|
||||
viewport_options = instance.data.get("viewport_options", {})
|
||||
files = render_preview_animation(
|
||||
filepath,
|
||||
ext,
|
||||
review_camera,
|
||||
start_frame=frame,
|
||||
end_frame=frame,
|
||||
percentSize=instance.data["percentSize"],
|
||||
width=instance.data["review_width"],
|
||||
height=instance.data["review_height"],
|
||||
viewport_options=viewport_options)
|
||||
|
||||
thumbnail = next(os.path.basename(path) for path in files)
|
||||
|
||||
representation = {
|
||||
"name": "thumbnail",
|
||||
"ext": ext,
|
||||
"files": thumbnail,
|
||||
"stagingDir": staging_dir,
|
||||
"thumbnail": True
|
||||
}
|
||||
|
||||
self.log.debug(f"{representation}")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
instance.data["representations"].append(representation)
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<error id="main">
|
||||
<title>Invalid Model Name</title>
|
||||
<description>## Nodes found with Invalid Model Name
|
||||
|
||||
Nodes were detected in your scene which have invalid model name which does not
|
||||
match the regex you preset in AYON setting.
|
||||
### How to repair?
|
||||
Make sure the model name aligns with validation regex in your AYON setting.
|
||||
|
||||
</description>
|
||||
<detail>
|
||||
### Invalid nodes
|
||||
|
||||
{nodes}
|
||||
|
||||
|
||||
### How could this happen?
|
||||
|
||||
This often happens if you have mesh with the model naming does not match
|
||||
with regex in the setting.
|
||||
|
||||
</detail>
|
||||
</error>
|
||||
</root>
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import pyblish.api
|
||||
from ayon_core.lib import version_up
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
||||
"""Increment current workfile version."""
|
||||
|
||||
order = pyblish.api.IntegratorOrder + 0.9
|
||||
label = "Increment Workfile Version"
|
||||
hosts = ["max"]
|
||||
families = ["maxrender", "workfile"]
|
||||
|
||||
def process(self, context):
|
||||
path = context.data["currentFile"]
|
||||
filepath = version_up(path)
|
||||
|
||||
rt.saveMaxFile(filepath)
|
||||
self.log.info("Incrementing file version")
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import pyblish.api
|
||||
from ayon_core.pipeline import registered_host
|
||||
|
||||
|
||||
class SaveCurrentScene(pyblish.api.InstancePlugin):
|
||||
"""Save current scene"""
|
||||
|
||||
label = "Save current file"
|
||||
order = pyblish.api.ExtractorOrder - 0.49
|
||||
hosts = ["max"]
|
||||
families = ["maxrender", "workfile"]
|
||||
|
||||
def process(self, instance):
|
||||
host = registered_host()
|
||||
current_file = host.get_current_workfile()
|
||||
|
||||
assert instance.context.data["currentFile"] == current_file
|
||||
if instance.data["productType"] == "maxrender":
|
||||
host.save_workfile(current_file)
|
||||
|
||||
elif host.workfile_has_unsaved_changes():
|
||||
self.log.info(f"Saving current file: {current_file}")
|
||||
host.save_workfile(current_file)
|
||||
else:
|
||||
self.log.debug("No unsaved changes, skipping file save..")
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
import pyblish.api
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from pymxs import runtime as rt
|
||||
from ayon_core.lib import run_subprocess
|
||||
from ayon_max.api.lib_rendersettings import RenderSettings
|
||||
from ayon_max.api.lib_renderproducts import RenderProducts
|
||||
|
||||
|
||||
class SaveScenesForCamera(pyblish.api.InstancePlugin):
|
||||
"""Save scene files for multiple cameras without
|
||||
editing the original scene before deadline submission
|
||||
|
||||
"""
|
||||
|
||||
label = "Save Scene files for cameras"
|
||||
order = pyblish.api.ExtractorOrder - 0.48
|
||||
hosts = ["max"]
|
||||
families = ["maxrender"]
|
||||
|
||||
def process(self, instance):
|
||||
if not instance.data.get("multiCamera"):
|
||||
self.log.debug(
|
||||
"Multi Camera disabled. "
|
||||
"Skipping to save scene files for cameras")
|
||||
return
|
||||
current_folder = rt.maxFilePath
|
||||
current_filename = rt.maxFileName
|
||||
current_filepath = os.path.join(current_folder, current_filename)
|
||||
camera_scene_files = []
|
||||
scripts = []
|
||||
filename, ext = os.path.splitext(current_filename)
|
||||
fmt = RenderProducts().image_format()
|
||||
cameras = instance.data.get("cameras")
|
||||
if not cameras:
|
||||
return
|
||||
new_folder = f"{current_folder}_{filename}"
|
||||
os.makedirs(new_folder, exist_ok=True)
|
||||
for camera in cameras:
|
||||
new_output = RenderSettings().get_batch_render_output(camera) # noqa
|
||||
new_output = new_output.replace("\\", "/")
|
||||
new_filename = f"{filename}_{camera}{ext}"
|
||||
new_filepath = os.path.join(new_folder, new_filename)
|
||||
new_filepath = new_filepath.replace("\\", "/")
|
||||
camera_scene_files.append(new_filepath)
|
||||
RenderSettings().batch_render_elements(camera)
|
||||
rt.rendOutputFilename = new_output
|
||||
rt.saveMaxFile(current_filepath)
|
||||
script = ("""
|
||||
from pymxs import runtime as rt
|
||||
import os
|
||||
filename = "{filename}"
|
||||
new_filepath = "{new_filepath}"
|
||||
new_output = "{new_output}"
|
||||
camera = "{camera}"
|
||||
rt.rendOutputFilename = new_output
|
||||
directory = os.path.dirname(rt.rendOutputFilename)
|
||||
directory = os.path.join(directory, filename)
|
||||
render_elem = rt.maxOps.GetCurRenderElementMgr()
|
||||
render_elem_num = render_elem.NumRenderElements()
|
||||
if render_elem_num > 0:
|
||||
ext = "{ext}"
|
||||
for i in range(render_elem_num):
|
||||
renderlayer_name = render_elem.GetRenderElement(i)
|
||||
target, renderpass = str(renderlayer_name).split(":")
|
||||
aov_name = f"{{directory}}_{camera}_{{renderpass}}..{ext}"
|
||||
render_elem.SetRenderElementFileName(i, aov_name)
|
||||
rt.saveMaxFile(new_filepath)
|
||||
""").format(filename=instance.name,
|
||||
new_filepath=new_filepath,
|
||||
new_output=new_output,
|
||||
camera=camera,
|
||||
ext=fmt)
|
||||
scripts.append(script)
|
||||
|
||||
maxbatch_exe = os.path.join(
|
||||
os.path.dirname(sys.executable), "3dsmaxbatch")
|
||||
maxbatch_exe = maxbatch_exe.replace("\\", "/")
|
||||
if sys.platform == "windows":
|
||||
maxbatch_exe += ".exe"
|
||||
maxbatch_exe = os.path.normpath(maxbatch_exe)
|
||||
with tempfile.TemporaryDirectory() as tmp_dir_name:
|
||||
tmp_script_path = os.path.join(
|
||||
tmp_dir_name, "extract_scene_files.py")
|
||||
self.log.info("Using script file: {}".format(tmp_script_path))
|
||||
|
||||
with open(tmp_script_path, "wt") as tmp:
|
||||
for script in scripts:
|
||||
tmp.write(script + "\n")
|
||||
|
||||
try:
|
||||
current_filepath = current_filepath.replace("\\", "/")
|
||||
tmp_script_path = tmp_script_path.replace("\\", "/")
|
||||
run_subprocess([maxbatch_exe, tmp_script_path,
|
||||
"-sceneFile", current_filepath])
|
||||
except RuntimeError:
|
||||
self.log.debug("Checking the scene files existing")
|
||||
|
||||
for camera_scene in camera_scene_files:
|
||||
if not os.path.exists(camera_scene):
|
||||
self.log.error("Camera scene files not existed yet!")
|
||||
raise RuntimeError("MaxBatch.exe doesn't run as expected")
|
||||
self.log.debug(f"Found Camera scene:{camera_scene}")
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validator for Attributes."""
|
||||
import json
|
||||
|
||||
from pyblish.api import ContextPlugin, ValidatorOrder
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from ayon_core.pipeline.publish import (
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError,
|
||||
RepairContextAction
|
||||
)
|
||||
|
||||
|
||||
def has_property(object_name, property_name):
|
||||
"""Return whether an object has a property with given name"""
|
||||
return rt.Execute(f'isProperty {object_name} "{property_name}"')
|
||||
|
||||
|
||||
def is_matching_value(object_name, property_name, value):
|
||||
"""Return whether an existing property matches value `value"""
|
||||
property_value = rt.Execute(f"{object_name}.{property_name}")
|
||||
|
||||
# Wrap property value if value is a string valued attributes
|
||||
# starting with a `#`
|
||||
if (
|
||||
isinstance(value, str) and
|
||||
value.startswith("#") and
|
||||
not value.endswith(")")
|
||||
):
|
||||
# prefix value with `#`
|
||||
# not applicable for #() array value type
|
||||
# and only applicable for enum i.e. #bob, #sally
|
||||
property_value = f"#{property_value}"
|
||||
|
||||
return property_value == value
|
||||
|
||||
|
||||
class ValidateAttributes(OptionalPyblishPluginMixin,
|
||||
ContextPlugin):
|
||||
"""Validates attributes in the project setting are consistent
|
||||
with the nodes from MaxWrapper Class in 3ds max.
|
||||
E.g. "renderers.current.separateAovFiles",
|
||||
"renderers.production.PrimaryGIEngine"
|
||||
Admin(s) need to put the dict below and enable this validator for a check:
|
||||
{
|
||||
"renderers.current":{
|
||||
"separateAovFiles" : True
|
||||
},
|
||||
"renderers.production":{
|
||||
"PrimaryGIEngine": "#RS_GIENGINE_BRUTE_FORCE"
|
||||
}
|
||||
....
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
order = ValidatorOrder
|
||||
hosts = ["max"]
|
||||
label = "Attributes"
|
||||
actions = [RepairContextAction]
|
||||
optional = True
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, context):
|
||||
attributes = json.loads(
|
||||
context.data
|
||||
["project_settings"]
|
||||
["max"]
|
||||
["publish"]
|
||||
["ValidateAttributes"]
|
||||
["attributes"]
|
||||
)
|
||||
if not attributes:
|
||||
return
|
||||
invalid = []
|
||||
for object_name, required_properties in attributes.items():
|
||||
if not rt.Execute(f"isValidValue {object_name}"):
|
||||
# Skip checking if the node does not
|
||||
# exist in MaxWrapper Class
|
||||
cls.log.debug(f"Unable to find '{object_name}'."
|
||||
" Skipping validation of attributes.")
|
||||
continue
|
||||
|
||||
for property_name, value in required_properties.items():
|
||||
if not has_property(object_name, property_name):
|
||||
cls.log.error(
|
||||
"Non-existing property: "
|
||||
f"{object_name}.{property_name}")
|
||||
invalid.append((object_name, property_name))
|
||||
|
||||
if not is_matching_value(object_name, property_name, value):
|
||||
cls.log.error(
|
||||
f"Invalid value for: {object_name}.{property_name}"
|
||||
f" should be: {value}")
|
||||
invalid.append((object_name, property_name))
|
||||
|
||||
return invalid
|
||||
|
||||
def process(self, context):
|
||||
if not self.is_active(context.data):
|
||||
self.log.debug("Skipping Validate Attributes...")
|
||||
return
|
||||
invalid_attributes = self.get_invalid(context)
|
||||
if invalid_attributes:
|
||||
bullet_point_invalid_statement = "\n".join(
|
||||
"- {}".format(invalid) for invalid
|
||||
in invalid_attributes
|
||||
)
|
||||
report = (
|
||||
"Required Attribute(s) have invalid value(s).\n\n"
|
||||
f"{bullet_point_invalid_statement}\n\n"
|
||||
"You can use repair action to fix them if they are not\n"
|
||||
"unknown property value(s)."
|
||||
)
|
||||
raise PublishValidationError(
|
||||
report, title="Invalid Value(s) for Required Attribute(s)")
|
||||
|
||||
@classmethod
|
||||
def repair(cls, context):
|
||||
attributes = json.loads(
|
||||
context.data
|
||||
["project_settings"]
|
||||
["max"]
|
||||
["publish"]
|
||||
["ValidateAttributes"]
|
||||
["attributes"]
|
||||
)
|
||||
invalid_attributes = cls.get_invalid(context)
|
||||
for attrs in invalid_attributes:
|
||||
prop, attr = attrs
|
||||
value = attributes[prop][attr]
|
||||
if isinstance(value, str) and not value.startswith("#"):
|
||||
attribute_fix = '{}.{}="{}"'.format(
|
||||
prop, attr, value
|
||||
)
|
||||
else:
|
||||
attribute_fix = "{}.{}={}".format(
|
||||
prop, attr, value
|
||||
)
|
||||
rt.Execute(attribute_fix)
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from ayon_core.pipeline.publish import (
|
||||
RepairAction,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
from ayon_max.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
class ValidateCameraAttributes(OptionalPyblishPluginMixin,
|
||||
pyblish.api.InstancePlugin):
|
||||
"""Validates Camera has no invalid attribute properties
|
||||
or values.(For 3dsMax Cameras only)
|
||||
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ['camera']
|
||||
hosts = ['max']
|
||||
label = 'Validate Camera Attributes'
|
||||
actions = [SelectInvalidAction, RepairAction]
|
||||
optional = True
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
DEFAULTS = ["fov", "nearrange", "farrange",
|
||||
"nearclip", "farclip"]
|
||||
CAM_TYPE = ["Freecamera", "Targetcamera",
|
||||
"Physical"]
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
invalid = []
|
||||
if rt.units.DisplayType != rt.Name("Generic"):
|
||||
cls.log.warning(
|
||||
"Generic Type is not used as a scene unit\n\n"
|
||||
"sure you tweak the settings with your own values\n\n"
|
||||
"before validation.")
|
||||
cameras = instance.data["members"]
|
||||
project_settings = instance.context.data["project_settings"].get("max")
|
||||
cam_attr_settings = (
|
||||
project_settings["publish"]["ValidateCameraAttributes"]
|
||||
)
|
||||
for camera in cameras:
|
||||
if str(rt.ClassOf(camera)) not in cls.CAM_TYPE:
|
||||
cls.log.debug(
|
||||
"Skipping camera created from external plugin..")
|
||||
continue
|
||||
for attr in cls.DEFAULTS:
|
||||
default_value = cam_attr_settings.get(attr)
|
||||
if default_value == float(0):
|
||||
cls.log.debug(
|
||||
f"the value of {attr} in setting set to"
|
||||
" zero. Skipping the check.")
|
||||
continue
|
||||
if round(rt.getProperty(camera, attr), 1) != default_value:
|
||||
cls.log.error(
|
||||
f"Invalid attribute value for {camera.name}:{attr} "
|
||||
f"(should be: {default_value}))")
|
||||
invalid.append(camera)
|
||||
|
||||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
self.log.debug("Skipping Validate Camera Attributes.")
|
||||
return
|
||||
invalid = self.get_invalid(instance)
|
||||
|
||||
if invalid:
|
||||
raise PublishValidationError(
|
||||
"Invalid camera attributes found. See log.")
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
invalid_cameras = cls.get_invalid(instance)
|
||||
project_settings = instance.context.data["project_settings"].get("max")
|
||||
cam_attr_settings = (
|
||||
project_settings["publish"]["ValidateCameraAttributes"]
|
||||
)
|
||||
for camera in invalid_cameras:
|
||||
for attr in cls.DEFAULTS:
|
||||
expected_value = cam_attr_settings.get(attr)
|
||||
if expected_value == float(0):
|
||||
cls.log.debug(
|
||||
f"the value of {attr} in setting set to zero.")
|
||||
continue
|
||||
rt.setProperty(camera, attr, expected_value)
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.pipeline import PublishValidationError
|
||||
|
||||
|
||||
class ValidateCameraContent(pyblish.api.InstancePlugin):
|
||||
"""Validates Camera instance contents.
|
||||
|
||||
A Camera instance may only hold a SINGLE camera's transform
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["camera", "review"]
|
||||
hosts = ["max"]
|
||||
label = "Camera Contents"
|
||||
camera_type = ["$Free_Camera", "$Target_Camera",
|
||||
"$Physical_Camera", "$Target"]
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise PublishValidationError(("Camera instance must only include"
|
||||
"camera (and camera target). "
|
||||
f"Invalid content {invalid}"))
|
||||
|
||||
def get_invalid(self, instance):
|
||||
"""
|
||||
Get invalid nodes if the instance is not camera
|
||||
"""
|
||||
invalid = []
|
||||
container = instance.data["instance_node"]
|
||||
self.log.info(f"Validating camera content for {container}")
|
||||
|
||||
selection_list = instance.data["members"]
|
||||
for sel in selection_list:
|
||||
# to avoid Attribute Error from pymxs wrapper
|
||||
sel_tmp = str(sel)
|
||||
found = any(sel_tmp.startswith(cam) for cam in self.camera_type)
|
||||
if not found:
|
||||
self.log.error("Camera not found")
|
||||
invalid.append(sel)
|
||||
return invalid
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import PublishValidationError
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class ValidateExtendedViewport(pyblish.api.ContextPlugin):
|
||||
"""Validate if the first viewport is an extended viewport."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["review"]
|
||||
hosts = ["max"]
|
||||
label = "Validate Extended Viewport"
|
||||
|
||||
def process(self, context):
|
||||
try:
|
||||
rt.viewport.activeViewportEx(1)
|
||||
except RuntimeError:
|
||||
raise PublishValidationError(
|
||||
"Please make sure one viewport is not an extended viewport",
|
||||
description = (
|
||||
"Please make sure at least one viewport is not an "
|
||||
"extended viewport but a 3dsmax supported viewport "
|
||||
"i.e camera/persp/orthographic view.\n\n"
|
||||
"To rectify it, please go to view in the top menubar, "
|
||||
"go to Views -> Viewports Configuration -> Layout and "
|
||||
"right click on one of the panels to change it."
|
||||
))
|
||||
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import PublishValidationError
|
||||
|
||||
|
||||
class ValidateInstanceHasMembers(pyblish.api.InstancePlugin):
|
||||
"""Validates Instance has members.
|
||||
|
||||
Check if MaxScene containers includes any contents underneath.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["camera",
|
||||
"model",
|
||||
"maxScene",
|
||||
"review",
|
||||
"pointcache",
|
||||
"pointcloud",
|
||||
"redshiftproxy"]
|
||||
hosts = ["max"]
|
||||
label = "Container Contents"
|
||||
|
||||
def process(self, instance):
|
||||
if not instance.data["members"]:
|
||||
raise PublishValidationError("No content found in the container")
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate if instance context is the same as current context."""
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from ayon_max.api.action import SelectInvalidAction
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class ValidateInstanceInContext(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validator to check if instance context match current context.
|
||||
|
||||
When working in per-shot style you always publish data in context of
|
||||
current context (shot). This validator checks if this is so. It is optional
|
||||
so it can be disabled when needed.
|
||||
|
||||
Action on this validator will select invalid instances.
|
||||
"""
|
||||
order = ValidateContentsOrder
|
||||
label = "Instance in same Context"
|
||||
optional = True
|
||||
hosts = ["max"]
|
||||
actions = [SelectInvalidAction, RepairAction]
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
folderPath = instance.data.get("folderPath")
|
||||
task = instance.data.get("task")
|
||||
context = self.get_context(instance)
|
||||
if (folderPath, task) != context:
|
||||
context_label = "{} > {}".format(*context)
|
||||
instance_label = "{} > {}".format(folderPath, task)
|
||||
message = (
|
||||
"Instance '{}' publishes to different context(folder or task) "
|
||||
"than current context: {}. Current context: {}".format(
|
||||
instance.name, instance_label, context_label
|
||||
)
|
||||
)
|
||||
raise PublishValidationError(
|
||||
message=message,
|
||||
description=(
|
||||
"## Publishing to a different context data(folder or task)\n"
|
||||
"There are publish instances present which are publishing "
|
||||
"into a different folder path or task than your current context.\n\n"
|
||||
"Usually this is not what you want but there can be cases "
|
||||
"where you might want to publish into another context or "
|
||||
"shot. If that's the case you can disable the validation "
|
||||
"on the instance to ignore it."
|
||||
)
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
invalid = []
|
||||
folderPath = instance.data.get("folderPath")
|
||||
task = instance.data.get("task")
|
||||
context = cls.get_context(instance)
|
||||
if (folderPath, task) != context:
|
||||
invalid.append(rt.getNodeByName(instance.name))
|
||||
return invalid
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
context_asset = instance.context.data["folderPath"]
|
||||
context_task = instance.context.data["task"]
|
||||
instance_node = rt.getNodeByName(instance.data.get(
|
||||
"instance_node", ""))
|
||||
if not instance_node:
|
||||
return
|
||||
rt.SetUserProp(instance_node, "folderPath", context_asset)
|
||||
rt.SetUserProp(instance_node, "task", context_task)
|
||||
|
||||
@staticmethod
|
||||
def get_context(instance):
|
||||
"""Return asset, task from publishing context data"""
|
||||
context = instance.context
|
||||
return context.data["folderPath"], context.data["task"]
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validator for Loaded Plugin."""
|
||||
import os
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from ayon_core.pipeline.publish import (
|
||||
RepairAction,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
from ayon_max.api.lib import get_plugins
|
||||
|
||||
|
||||
class ValidateLoadedPlugin(OptionalPyblishPluginMixin,
|
||||
pyblish.api.InstancePlugin):
|
||||
"""Validates if the specific plugin is loaded in 3ds max.
|
||||
Studio Admin(s) can add the plugins they want to check in validation
|
||||
via studio defined project settings
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
hosts = ["max"]
|
||||
label = "Validate Loaded Plugins"
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
family_plugins_mapping = []
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
"""Plugin entry point."""
|
||||
family_plugins_mapping = cls.family_plugins_mapping
|
||||
if not family_plugins_mapping:
|
||||
return
|
||||
|
||||
# Backward compatibility - settings did have 'product_types'
|
||||
if "product_types" in family_plugins_mapping:
|
||||
family_plugins_mapping["families"] = family_plugins_mapping.pop(
|
||||
"product_types"
|
||||
)
|
||||
|
||||
invalid = []
|
||||
# Find all plug-in requirements for current instance
|
||||
instance_families = {instance.data["productType"]}
|
||||
instance_families.update(instance.data.get("families", []))
|
||||
cls.log.debug("Checking plug-in validation "
|
||||
f"for instance families: {instance_families}")
|
||||
all_required_plugins = set()
|
||||
|
||||
for mapping in family_plugins_mapping:
|
||||
# Check for matching families
|
||||
if not mapping:
|
||||
return
|
||||
|
||||
match_families = {
|
||||
fam.strip() for fam in mapping["families"]
|
||||
}
|
||||
has_match = "*" in match_families or match_families.intersection(
|
||||
instance_families)
|
||||
|
||||
if not has_match:
|
||||
continue
|
||||
|
||||
cls.log.debug(
|
||||
f"Found plug-in family requirements: {match_families}")
|
||||
required_plugins = [
|
||||
# match lowercase and format with os.environ to allow
|
||||
# plugin names defined by max version, e.g. {3DSMAX_VERSION}
|
||||
plugin.format(**os.environ).lower()
|
||||
for plugin in mapping["plugins"]
|
||||
# ignore empty fields in settings
|
||||
if plugin.strip()
|
||||
]
|
||||
|
||||
all_required_plugins.update(required_plugins)
|
||||
|
||||
if not all_required_plugins:
|
||||
# Instance has no plug-in requirements
|
||||
return
|
||||
|
||||
# get all DLL loaded plugins in Max and their plugin index
|
||||
available_plugins = {
|
||||
plugin_name.lower(): index for index, plugin_name in enumerate(
|
||||
get_plugins())
|
||||
}
|
||||
# validate the required plug-ins
|
||||
for plugin in sorted(all_required_plugins):
|
||||
plugin_index = available_plugins.get(plugin)
|
||||
if plugin_index is None:
|
||||
debug_msg = (
|
||||
f"Plugin {plugin} does not exist"
|
||||
" in 3dsMax Plugin List."
|
||||
)
|
||||
invalid.append((plugin, debug_msg))
|
||||
continue
|
||||
if not rt.pluginManager.isPluginDllLoaded(plugin_index):
|
||||
debug_msg = f"Plugin {plugin} not loaded."
|
||||
invalid.append((plugin, debug_msg))
|
||||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
self.log.debug("Skipping Validate Loaded Plugin...")
|
||||
return
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
bullet_point_invalid_statement = "\n".join(
|
||||
"- {}".format(message) for _, message in invalid
|
||||
)
|
||||
report = (
|
||||
"Required plugins are not loaded.\n\n"
|
||||
f"{bullet_point_invalid_statement}\n\n"
|
||||
"You can use repair action to load the plugin."
|
||||
)
|
||||
raise PublishValidationError(
|
||||
report, title="Missing Required Plugins")
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
# get all DLL loaded plugins in Max and their plugin index
|
||||
invalid = cls.get_invalid(instance)
|
||||
if not invalid:
|
||||
return
|
||||
|
||||
# get all DLL loaded plugins in Max and their plugin index
|
||||
available_plugins = {
|
||||
plugin_name.lower(): index for index, plugin_name in enumerate(
|
||||
get_plugins())
|
||||
}
|
||||
|
||||
for invalid_plugin, _ in invalid:
|
||||
plugin_index = available_plugins.get(invalid_plugin)
|
||||
|
||||
if plugin_index is None:
|
||||
cls.log.warning(
|
||||
f"Can't enable missing plugin: {invalid_plugin}")
|
||||
continue
|
||||
|
||||
if not rt.pluginManager.isPluginDllLoaded(plugin_index):
|
||||
rt.pluginManager.loadPluginDll(plugin_index)
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
|
||||
import pyblish.api
|
||||
from ayon_max.api.action import SelectInvalidAction
|
||||
from ayon_core.pipeline.publish import (
|
||||
ValidateMeshOrder,
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishValidationError
|
||||
)
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class ValidateMeshHasUVs(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
|
||||
"""Validate the current mesh has UVs.
|
||||
|
||||
This validator only checks if the mesh has UVs but not
|
||||
whether all the individual faces of the mesh have UVs.
|
||||
|
||||
It validates whether the current mesh has texture vertices.
|
||||
If the mesh does not have texture vertices, it does not
|
||||
have UVs in Max.
|
||||
|
||||
"""
|
||||
|
||||
order = ValidateMeshOrder
|
||||
hosts = ['max']
|
||||
families = ['model']
|
||||
label = 'Validate Mesh Has UVs'
|
||||
actions = [SelectInvalidAction]
|
||||
optional = True
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
meshes = [member for member in instance.data["members"]
|
||||
if rt.isProperty(member, "mesh")]
|
||||
invalid = [member for member in meshes
|
||||
if member.mesh.numTVerts == 0]
|
||||
return invalid
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
bullet_point_invalid_statement = "\n".join(
|
||||
"- {}".format(invalid.name) for invalid
|
||||
in invalid
|
||||
)
|
||||
report = (
|
||||
"Model meshes are required to have UVs.\n\n"
|
||||
"Meshes detected with invalid or missing UVs:\n"
|
||||
f"{bullet_point_invalid_statement}\n"
|
||||
)
|
||||
raise PublishValidationError(
|
||||
report,
|
||||
description=(
|
||||
"Model meshes are required to have UVs.\n\n"
|
||||
"Meshes detected with no texture vertice or missing UVs"),
|
||||
title="Non-mesh objects found or mesh has missing UVs")
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
|
||||
from ayon_core.pipeline import PublishValidationError
|
||||
|
||||
|
||||
class ValidateModelContent(pyblish.api.InstancePlugin):
|
||||
"""Validates Model instance contents.
|
||||
|
||||
A model instance may only hold either geometry-related
|
||||
object(excluding Shapes) or editable meshes.
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["model"]
|
||||
hosts = ["max"]
|
||||
label = "Model Contents"
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise PublishValidationError(("Model instance must only include"
|
||||
"Geometry and Editable Mesh. "
|
||||
f"Invalid types on: {invalid}"))
|
||||
|
||||
def get_invalid(self, instance):
|
||||
"""
|
||||
Get invalid nodes if the instance is not camera
|
||||
"""
|
||||
invalid = []
|
||||
container = instance.data["instance_node"]
|
||||
self.log.info(f"Validating model content for {container}")
|
||||
|
||||
selection_list = instance.data["members"]
|
||||
for sel in selection_list:
|
||||
if rt.ClassOf(sel) in rt.Camera.classes:
|
||||
invalid.append(sel)
|
||||
if rt.ClassOf(sel) in rt.Light.classes:
|
||||
invalid.append(sel)
|
||||
if rt.ClassOf(sel) in rt.Shape.classes:
|
||||
invalid.append(sel)
|
||||
|
||||
return invalid
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Validate model nodes names."""
|
||||
import re
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_max.api.action import SelectInvalidAction
|
||||
|
||||
from ayon_core.pipeline.publish import (
|
||||
OptionalPyblishPluginMixin,
|
||||
PublishXmlValidationError,
|
||||
ValidateContentsOrder
|
||||
)
|
||||
|
||||
class ValidateModelName(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validate Model Name.
|
||||
|
||||
Validation regex is `(.*)_(?P<subset>.*)_(GEO)` by default.
|
||||
The setting supports the following regex group name:
|
||||
- project
|
||||
- asset
|
||||
- subset
|
||||
|
||||
Examples:
|
||||
`{SOME_RANDOM_NAME}_{YOUR_SUBSET_NAME}_GEO` should be your
|
||||
default model name.
|
||||
The regex of `(?P<subset>.*)` can be replaced by `(?P<asset>.*)`
|
||||
and `(?P<project>.*)`.
|
||||
`(.*)_(?P<asset>.*)_(GEO)` check if your model name is
|
||||
`{SOME_RANDOM_NAME}_{CURRENT_ASSET_NAME}_GEO`
|
||||
`(.*)_(?P<project>.*)_(GEO)` check if your model name is
|
||||
`{SOME_RANDOM_NAME}_{CURRENT_PROJECT_NAME}_GEO`
|
||||
|
||||
"""
|
||||
optional = True
|
||||
order = ValidateContentsOrder
|
||||
hosts = ["max"]
|
||||
families = ["model"]
|
||||
label = "Validate Model Name"
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
# defined by settings
|
||||
regex = r"(.*)_(?P<subset>.*)_(GEO)"
|
||||
# cache
|
||||
regex_compiled = None
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
names = "\n".join(
|
||||
"- {}".format(node.name) for node in invalid
|
||||
)
|
||||
raise PublishXmlValidationError(
|
||||
plugin=self,
|
||||
message="Nodes found with invalid model names: {}".format(invalid),
|
||||
formatting_data={"nodes": names}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
if not cls.regex:
|
||||
cls.log.warning("No regex pattern set. Nothing to validate.")
|
||||
return
|
||||
|
||||
members = instance.data.get("members")
|
||||
if not members:
|
||||
cls.log.error("No members found in the instance.")
|
||||
return
|
||||
|
||||
cls.regex_compiled = re.compile(cls.regex)
|
||||
|
||||
invalid = []
|
||||
for obj in members:
|
||||
if cls.invalid_name(instance, obj):
|
||||
invalid.append(obj)
|
||||
return invalid
|
||||
|
||||
@classmethod
|
||||
def invalid_name(cls, instance, obj):
|
||||
"""Function to check the object has invalid name
|
||||
regarding to the validation regex in the AYON setttings
|
||||
|
||||
Args:
|
||||
instance (pyblish.api.instance): Instance
|
||||
obj (str): object name
|
||||
|
||||
Returns:
|
||||
str: invalid object
|
||||
"""
|
||||
regex = cls.regex_compiled
|
||||
name = obj.name
|
||||
match = regex.match(name)
|
||||
|
||||
if match is None:
|
||||
cls.log.error("Invalid model name on: %s", name)
|
||||
cls.log.error("Name doesn't match regex {}".format(regex.pattern))
|
||||
return obj
|
||||
|
||||
# Validate regex groups
|
||||
invalid = False
|
||||
compare = {
|
||||
"project": instance.context.data["projectName"],
|
||||
"asset": instance.data["folderPath"],
|
||||
"subset": instance.data["productName"]
|
||||
}
|
||||
for key, required_value in compare.items():
|
||||
if key in regex.groupindex:
|
||||
if match.group(key) != required_value:
|
||||
cls.log.error(
|
||||
"Invalid %s name for the model %s, "
|
||||
"required name is %s",
|
||||
key, name, required_value
|
||||
)
|
||||
invalid = True
|
||||
|
||||
if invalid:
|
||||
return obj
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
from ayon_core.pipeline import (
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from ayon_max.api.action import SelectInvalidAction
|
||||
|
||||
|
||||
def get_invalid_keys(obj):
|
||||
"""function to check on whether there is keyframe in
|
||||
|
||||
Args:
|
||||
obj (str): object needed to check if there is a keyframe
|
||||
|
||||
Returns:
|
||||
bool: whether invalid object(s) exist
|
||||
"""
|
||||
for transform in ["Position", "Rotation", "Scale"]:
|
||||
num_of_key = rt.NumKeys(rt.getPropertyController(
|
||||
obj.controller, transform))
|
||||
if num_of_key > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ValidateNoAnimation(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validates No Animation
|
||||
|
||||
Ensure no keyframes on nodes in the Instance
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["model"]
|
||||
hosts = ["max"]
|
||||
optional = True
|
||||
label = "Validate No Animation"
|
||||
actions = [SelectInvalidAction]
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
raise PublishValidationError(
|
||||
"Keyframes found on:\n\n{0}".format(invalid)
|
||||
,
|
||||
title="Keyframes on model"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_invalid(instance):
|
||||
"""Get invalid object(s) which have keyframe(s)
|
||||
|
||||
|
||||
Args:
|
||||
instance (pyblish.api.instance): Instance
|
||||
|
||||
Returns:
|
||||
list: list of invalid objects
|
||||
"""
|
||||
invalid = [invalid for invalid in instance.data["members"]
|
||||
if invalid.isAnimated or get_invalid_keys(invalid)]
|
||||
|
||||
return invalid
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
import pyblish.api
|
||||
from ayon_core.pipeline import PublishValidationError
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class ValidatePointCloud(pyblish.api.InstancePlugin):
|
||||
"""Validate that work file was saved."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["pointcloud"]
|
||||
hosts = ["max"]
|
||||
label = "Validate Point Cloud"
|
||||
|
||||
def process(self, instance):
|
||||
"""
|
||||
Notes:
|
||||
1. Validate if the export mode of Export Particle is at PRT format
|
||||
2. Validate the partition count and range set as default value
|
||||
Partition Count : 100
|
||||
Partition Range : 1 to 1
|
||||
3. Validate if the custom attribute(s) exist as parameter(s)
|
||||
of export_particle operator
|
||||
|
||||
"""
|
||||
report = []
|
||||
|
||||
if self.validate_export_mode(instance):
|
||||
report.append("The export mode is not at PRT")
|
||||
|
||||
if self.validate_partition_value(instance):
|
||||
report.append(("tyFlow Partition setting is "
|
||||
"not at the default value"))
|
||||
|
||||
invalid_attribute = self.validate_custom_attribute(instance)
|
||||
if invalid_attribute:
|
||||
report.append(("Custom Attribute not found "
|
||||
f":{invalid_attribute}"))
|
||||
|
||||
if report:
|
||||
raise PublishValidationError(f"{report}")
|
||||
|
||||
def validate_custom_attribute(self, instance):
|
||||
invalid = []
|
||||
container = instance.data["instance_node"]
|
||||
self.log.info(
|
||||
f"Validating tyFlow custom attributes for {container}")
|
||||
|
||||
selection_list = instance.data["members"]
|
||||
|
||||
project_settings = instance.context.data["project_settings"]
|
||||
attr_settings = project_settings["max"]["PointCloud"]["attribute"]
|
||||
for sel in selection_list:
|
||||
obj = sel.baseobject
|
||||
anim_names = rt.GetSubAnimNames(obj)
|
||||
for anim_name in anim_names:
|
||||
# get all the names of the related tyFlow nodes
|
||||
sub_anim = rt.GetSubAnim(obj, anim_name)
|
||||
if rt.IsProperty(sub_anim, "Export_Particles"):
|
||||
event_name = sub_anim.name
|
||||
opt = "${0}.{1}.export_particles".format(sel.name,
|
||||
event_name)
|
||||
for attr in attr_settings:
|
||||
key = attr["name"]
|
||||
value = attr["value"]
|
||||
custom_attr = "{0}.PRTChannels_{1}".format(opt,
|
||||
value)
|
||||
try:
|
||||
rt.Execute(custom_attr)
|
||||
except RuntimeError:
|
||||
invalid.append(key)
|
||||
|
||||
return invalid
|
||||
|
||||
def validate_partition_value(self, instance):
|
||||
invalid = []
|
||||
container = instance.data["instance_node"]
|
||||
self.log.info(
|
||||
f"Validating tyFlow partition value for {container}")
|
||||
|
||||
selection_list = instance.data["members"]
|
||||
for sel in selection_list:
|
||||
obj = sel.baseobject
|
||||
anim_names = rt.GetSubAnimNames(obj)
|
||||
for anim_name in anim_names:
|
||||
# get all the names of the related tyFlow nodes
|
||||
sub_anim = rt.GetSubAnim(obj, anim_name)
|
||||
if rt.IsProperty(sub_anim, "Export_Particles"):
|
||||
event_name = sub_anim.name
|
||||
opt = "${0}.{1}.export_particles".format(sel.name,
|
||||
event_name)
|
||||
count = rt.Execute(f'{opt}.PRTPartitionsCount')
|
||||
if count != 100:
|
||||
invalid.append(count)
|
||||
start = rt.Execute(f'{opt}.PRTPartitionsFrom')
|
||||
if start != 1:
|
||||
invalid.append(start)
|
||||
end = rt.Execute(f'{opt}.PRTPartitionsTo')
|
||||
if end != 1:
|
||||
invalid.append(end)
|
||||
|
||||
return invalid
|
||||
|
||||
def validate_export_mode(self, instance):
|
||||
invalid = []
|
||||
container = instance.data["instance_node"]
|
||||
self.log.info(
|
||||
f"Validating tyFlow export mode for {container}")
|
||||
|
||||
con = rt.GetNodeByName(container)
|
||||
selection_list = list(con.Children)
|
||||
for sel in selection_list:
|
||||
obj = sel.baseobject
|
||||
anim_names = rt.GetSubAnimNames(obj)
|
||||
for anim_name in anim_names:
|
||||
# get all the names of the related tyFlow nodes
|
||||
sub_anim = rt.GetSubAnim(obj, anim_name)
|
||||
# check if there is export particle operator
|
||||
boolean = rt.IsProperty(sub_anim, "Export_Particles")
|
||||
event_name = sub_anim.name
|
||||
if boolean:
|
||||
opt = f"${sel.name}.{event_name}.export_particles"
|
||||
export_mode = rt.Execute(f'{opt}.exportMode')
|
||||
if export_mode != 1:
|
||||
invalid.append(export_mode)
|
||||
|
||||
return invalid
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import (
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin)
|
||||
from ayon_core.pipeline.publish import RepairAction
|
||||
from ayon_max.api.lib import get_current_renderer
|
||||
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class ValidateRenderableCamera(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validates Renderable Camera
|
||||
|
||||
Check if the renderable camera used for rendering
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["maxrender"]
|
||||
hosts = ["max"]
|
||||
label = "Renderable Camera"
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
if not instance.data["cameras"]:
|
||||
raise PublishValidationError(
|
||||
"No renderable Camera found in scene."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
|
||||
rt.viewport.setType(rt.Name("view_camera"))
|
||||
camera = rt.viewport.GetCamera()
|
||||
cls.log.info(f"Camera {camera} set as renderable camera")
|
||||
renderer_class = get_current_renderer()
|
||||
renderer = str(renderer_class).split(":")[0]
|
||||
if renderer == "Arnold":
|
||||
arv = rt.MAXToAOps.ArnoldRenderView()
|
||||
arv.setOption("Camera", str(camera))
|
||||
arv.close()
|
||||
instance.data["cameras"] = [camera.name]
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import PublishValidationError
|
||||
from pymxs import runtime as rt
|
||||
from ayon_core.pipeline.publish import RepairAction
|
||||
from ayon_max.api.lib import get_current_renderer
|
||||
|
||||
|
||||
class ValidateRendererRedshiftProxy(pyblish.api.InstancePlugin):
|
||||
"""
|
||||
Validates Redshift as the current renderer for creating
|
||||
Redshift Proxy
|
||||
"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["redshiftproxy"]
|
||||
hosts = ["max"]
|
||||
label = "Redshift Renderer"
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_redshift_renderer(instance)
|
||||
if invalid:
|
||||
raise PublishValidationError("Please install Redshift for 3dsMax"
|
||||
" before using the Redshift proxy instance") # noqa
|
||||
invalid = self.get_current_renderer(instance)
|
||||
if invalid:
|
||||
raise PublishValidationError("The Redshift proxy extraction"
|
||||
"discontinued since the current renderer is not Redshift") # noqa
|
||||
|
||||
def get_redshift_renderer(self, instance):
|
||||
invalid = list()
|
||||
max_renderers_list = str(rt.RendererClass.classes)
|
||||
if "Redshift_Renderer" not in max_renderers_list:
|
||||
invalid.append(max_renderers_list)
|
||||
|
||||
return invalid
|
||||
|
||||
def get_current_renderer(self, instance):
|
||||
invalid = list()
|
||||
renderer_class = get_current_renderer()
|
||||
current_renderer = str(renderer_class).split(":")[0]
|
||||
if current_renderer != "Redshift_Renderer":
|
||||
invalid.append(current_renderer)
|
||||
|
||||
return invalid
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
for Renderer in rt.RendererClass.classes:
|
||||
renderer = Renderer()
|
||||
if "Redshift_Renderer" in str(renderer):
|
||||
rt.renderers.production = renderer
|
||||
break
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
import os
|
||||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
from ayon_core.pipeline.publish import (
|
||||
RepairAction,
|
||||
ValidateContentsOrder,
|
||||
PublishValidationError,
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from ayon_max.api.lib_rendersettings import RenderSettings
|
||||
|
||||
|
||||
class ValidateRenderPasses(OptionalPyblishPluginMixin,
|
||||
pyblish.api.InstancePlugin):
|
||||
"""Validates Render Passes before farm submission
|
||||
"""
|
||||
|
||||
order = ValidateContentsOrder
|
||||
families = ["maxrender"]
|
||||
hosts = ["max"]
|
||||
label = "Validate Render Passes"
|
||||
actions = [RepairAction]
|
||||
|
||||
settings_category = "max"
|
||||
|
||||
def process(self, instance):
|
||||
invalid = self.get_invalid(instance)
|
||||
if invalid:
|
||||
bullet_point_invalid_statement = "\n".join(
|
||||
f"- {err_type}: {filepath}" for err_type, filepath
|
||||
in invalid
|
||||
)
|
||||
report = (
|
||||
"Invalid render passes found.\n\n"
|
||||
f"{bullet_point_invalid_statement}\n\n"
|
||||
"You can use repair action to fix the invalid filepath."
|
||||
)
|
||||
raise PublishValidationError(
|
||||
report, title="Invalid Render Passes")
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance):
|
||||
"""Function to get invalid beauty render outputs and
|
||||
render elements.
|
||||
|
||||
1. Check Render Output Folder matches the name of
|
||||
the current Max Scene, e.g.
|
||||
The name of the current Max scene:
|
||||
John_Doe.max
|
||||
The expected render output directory:
|
||||
{root[work]}/{project[name]}/{hierarchy}/{asset}/
|
||||
work/{task[name]}/render/3dsmax/John_Doe/
|
||||
|
||||
2. Check image extension(s) of the render output(s)
|
||||
matches the image format in OP/AYON setting, e.g.
|
||||
The current image format in settings: png
|
||||
The expected render outputs: John_Doe.png
|
||||
|
||||
3. Check filename of render element ends with the name of
|
||||
render element from the 3dsMax Render Element Manager.
|
||||
e.g. The name of render element: RsCryptomatte
|
||||
The expected filename: {InstanceName}_RsCryptomatte.png
|
||||
|
||||
Args:
|
||||
instance (pyblish.api.Instance): instance
|
||||
workfile_name (str): filename of the Max scene
|
||||
|
||||
Returns:
|
||||
list: list of invalid filename which doesn't match
|
||||
with the project name
|
||||
"""
|
||||
invalid = []
|
||||
file = rt.maxFileName
|
||||
workfile_name, ext = os.path.splitext(file)
|
||||
if workfile_name not in rt.rendOutputFilename:
|
||||
cls.log.error(
|
||||
"Render output folder must include"
|
||||
f" the max scene name {workfile_name} "
|
||||
)
|
||||
invalid_folder_name = os.path.dirname(
|
||||
rt.rendOutputFilename).replace(
|
||||
"\\", "/").split("/")[-1]
|
||||
invalid.append(("Invalid Render Output Folder",
|
||||
invalid_folder_name))
|
||||
beauty_fname = os.path.basename(rt.rendOutputFilename)
|
||||
beauty_name, ext = os.path.splitext(beauty_fname)
|
||||
invalid_filenames = cls.get_invalid_filenames(
|
||||
instance, beauty_name)
|
||||
invalid.extend(invalid_filenames)
|
||||
invalid_image_format = cls.get_invalid_image_format(
|
||||
instance, ext.lstrip("."))
|
||||
invalid.extend(invalid_image_format)
|
||||
renderer = instance.data["renderer"]
|
||||
if renderer in [
|
||||
"ART_Renderer",
|
||||
"Redshift_Renderer",
|
||||
"V_Ray_6_Hotfix_3",
|
||||
"V_Ray_GPU_6_Hotfix_3",
|
||||
"Default_Scanline_Renderer",
|
||||
"Quicksilver_Hardware_Renderer",
|
||||
]:
|
||||
render_elem = rt.maxOps.GetCurRenderElementMgr()
|
||||
render_elem_num = render_elem.NumRenderElements()
|
||||
for i in range(render_elem_num):
|
||||
renderlayer_name = render_elem.GetRenderElement(i)
|
||||
renderpass = str(renderlayer_name).rsplit(":", 1)[-1]
|
||||
rend_file = render_elem.GetRenderElementFilename(i)
|
||||
if not rend_file:
|
||||
continue
|
||||
|
||||
rend_fname, ext = os.path.splitext(
|
||||
os.path.basename(rend_file))
|
||||
invalid_filenames = cls.get_invalid_filenames(
|
||||
instance, rend_fname, renderpass=renderpass)
|
||||
invalid.extend(invalid_filenames)
|
||||
invalid_image_format = cls.get_invalid_image_format(
|
||||
instance, ext)
|
||||
invalid.extend(invalid_image_format)
|
||||
elif renderer == "Arnold":
|
||||
cls.log.debug(
|
||||
"Renderpass validation does not support Arnold yet,"
|
||||
" validation skipped...")
|
||||
else:
|
||||
cls.log.debug(
|
||||
"Skipping render element validation "
|
||||
f"for renderer: {renderer}")
|
||||
return invalid
|
||||
|
||||
@classmethod
|
||||
def get_invalid_filenames(cls, instance, file_name, renderpass=None):
|
||||
"""Function to get invalid filenames from render outputs.
|
||||
|
||||
Args:
|
||||
instance (pyblish.api.Instance): instance
|
||||
file_name (str): name of the file
|
||||
renderpass (str, optional): name of the renderpass.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
list: invalid filenames
|
||||
"""
|
||||
invalid = []
|
||||
if instance.name not in file_name:
|
||||
cls.log.error("The renderpass filename should contain the instance name.")
|
||||
invalid.append(("Invalid instance name",
|
||||
file_name))
|
||||
if renderpass is not None:
|
||||
if not file_name.rstrip(".").endswith(renderpass):
|
||||
cls.log.error(
|
||||
f"Filename for {renderpass} should "
|
||||
f"end with {renderpass}: {file_name}"
|
||||
)
|
||||
invalid.append((f"Invalid {renderpass}",
|
||||
os.path.basename(file_name)))
|
||||
return invalid
|
||||
|
||||
@classmethod
|
||||
def get_invalid_image_format(cls, instance, ext):
|
||||
"""Function to check if the image format of the render outputs
|
||||
aligns with that in the setting.
|
||||
|
||||
Args:
|
||||
instance (pyblish.api.Instance): instance
|
||||
ext (str): image extension
|
||||
|
||||
Returns:
|
||||
list: list of files with invalid image format
|
||||
"""
|
||||
invalid = []
|
||||
settings = instance.context.data["project_settings"].get("max")
|
||||
image_format = settings["RenderSettings"]["image_format"]
|
||||
ext = ext.lstrip(".")
|
||||
if ext != image_format:
|
||||
msg = (
|
||||
f"Invalid image format {ext} for render outputs.\n"
|
||||
f"Should be: {image_format}")
|
||||
cls.log.error(msg)
|
||||
invalid.append((msg, ext))
|
||||
return invalid
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
container = instance.data.get("instance_node")
|
||||
# TODO: need to rename the function of render_output
|
||||
RenderSettings().render_output(container)
|
||||
cls.log.debug("Finished repairing the render output "
|
||||
"folder and filenames.")
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
import pyblish.api
|
||||
from pymxs import runtime as rt
|
||||
from ayon_core.pipeline import (
|
||||
OptionalPyblishPluginMixin
|
||||
)
|
||||
from ayon_core.pipeline.publish import (
|
||||
RepairAction,
|
||||
PublishValidationError
|
||||
)
|
||||
from ayon_max.api.lib import (
|
||||
reset_scene_resolution,
|
||||
imprint
|
||||
)
|
||||
|
||||
|
||||
class ValidateResolutionSetting(pyblish.api.InstancePlugin,
|
||||
OptionalPyblishPluginMixin):
|
||||
"""Validate the resolution setting aligned with DB"""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
families = ["maxrender"]
|
||||
hosts = ["max"]
|
||||
label = "Validate Resolution Setting"
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
||||
def process(self, instance):
|
||||
if not self.is_active(instance.data):
|
||||
return
|
||||
width, height = self.get_folder_resolution(instance)
|
||||
current_width, current_height = (
|
||||
self.get_current_resolution(instance)
|
||||
)
|
||||
|
||||
if current_width != width and current_height != height:
|
||||
raise PublishValidationError("Resolution Setting "
|
||||
"not matching resolution "
|
||||
"set on asset or shot.")
|
||||
if current_width != width:
|
||||
raise PublishValidationError("Width in Resolution Setting "
|
||||
"not matching resolution set "
|
||||
"on asset or shot.")
|
||||
|
||||
if current_height != height:
|
||||
raise PublishValidationError("Height in Resolution Setting "
|
||||
"not matching resolution set "
|
||||
"on asset or shot.")
|
||||
|
||||
def get_current_resolution(self, instance):
|
||||
return rt.renderWidth, rt.renderHeight
|
||||
|
||||
@classmethod
|
||||
def get_folder_resolution(cls, instance):
|
||||
task_entity = instance.data.get("taskEntity")
|
||||
if task_entity:
|
||||
task_attributes = task_entity["attrib"]
|
||||
width = task_attributes["resolutionWidth"]
|
||||
height = task_attributes["resolutionHeight"]
|
||||
return int(width), int(height)
|
||||
|
||||
# Defaults if not found in folder entity
|
||||
return 1920, 1080
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
reset_scene_resolution()
|
||||
|
||||
|
||||
class ValidateReviewResolutionSetting(ValidateResolutionSetting):
|
||||
families = ["review"]
|
||||
optional = True
|
||||
actions = [RepairAction]
|
||||
|
||||
def get_current_resolution(self, instance):
|
||||
current_width = instance.data["review_width"]
|
||||
current_height = instance.data["review_height"]
|
||||
return current_width, current_height
|
||||
|
||||
@classmethod
|
||||
def repair(cls, instance):
|
||||
context_width, context_height = (
|
||||
cls.get_folder_resolution(instance)
|
||||
)
|
||||
creator_attrs = instance.data["creator_attributes"]
|
||||
creator_attrs["review_width"] = context_width
|
||||
creator_attrs["review_height"] = context_height
|
||||
creator_attrs_data = {
|
||||
"creator_attributes": creator_attrs
|
||||
}
|
||||
# update the width and height of review
|
||||
# data in creator_attributes
|
||||
imprint(instance.data["instance_node"], creator_attrs_data)
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
from ayon_core.pipeline import PublishValidationError
|
||||
from pymxs import runtime as rt
|
||||
|
||||
|
||||
class ValidateSceneSaved(pyblish.api.InstancePlugin):
|
||||
"""Validate that workfile was saved."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder
|
||||
families = ["workfile"]
|
||||
hosts = ["max"]
|
||||
label = "Validate Workfile is saved"
|
||||
|
||||
def process(self, instance):
|
||||
if not rt.maxFilePath or not rt.maxFileName:
|
||||
raise PublishValidationError(
|
||||
"Workfile is not saved", title=self.label)
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
-- AYON Init Script
|
||||
(
|
||||
local sysPath = dotNetClass "System.IO.Path"
|
||||
local sysDir = dotNetClass "System.IO.Directory"
|
||||
local localScript = getThisScriptFilename()
|
||||
local startup = sysPath.Combine (sysPath.GetDirectoryName localScript) "startup.py"
|
||||
|
||||
local pythonpath = systemTools.getEnvVariable "MAX_PYTHONPATH"
|
||||
systemTools.setEnvVariable "PYTHONPATH" pythonpath
|
||||
|
||||
/*opens the create menu on startup to ensure users are presented with a useful default view.*/
|
||||
max create mode
|
||||
|
||||
python.ExecuteFile startup
|
||||
)
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
from ayon_max.api import MaxHost
|
||||
from ayon_core.pipeline import install_host
|
||||
# this might happen in some 3dsmax version where PYTHONPATH isn't added
|
||||
# to sys.path automatically
|
||||
for path in os.environ["PYTHONPATH"].split(os.pathsep):
|
||||
if path and path not in sys.path:
|
||||
sys.path.append(path)
|
||||
|
||||
host = MaxHost()
|
||||
install_host(host)
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'max' version."""
|
||||
__version__ = "0.2.1"
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
name = "max"
|
||||
title = "Max"
|
||||
version = "0.2.1"
|
||||
client_dir = "ayon_max"
|
||||
|
||||
ayon_required_addons = {
|
||||
"core": ">0.3.2",
|
||||
}
|
||||
ayon_compatible_addons = {}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
from typing import Type
|
||||
|
||||
from ayon_server.addons import BaseServerAddon
|
||||
|
||||
from .settings import MaxSettings, DEFAULT_VALUES
|
||||
|
||||
|
||||
class MaxAddon(BaseServerAddon):
|
||||
settings_model: Type[MaxSettings] = MaxSettings
|
||||
|
||||
async def get_default_settings(self):
|
||||
settings_model_cls = self.get_settings_model()
|
||||
return settings_model_cls(**DEFAULT_VALUES)
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
from .main import (
|
||||
MaxSettings,
|
||||
DEFAULT_VALUES,
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
"MaxSettings",
|
||||
"DEFAULT_VALUES",
|
||||
)
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
from ayon_server.settings import BaseSettingsModel, SettingsField
|
||||
|
||||
|
||||
def image_format_enum():
|
||||
"""Return enumerator for image output formats."""
|
||||
return [
|
||||
{"label": "exr", "value": "exr"},
|
||||
{"label": "jpg", "value": "jpg"},
|
||||
{"label": "png", "value": "png"},
|
||||
{"label": "tga", "value": "tga"}
|
||||
]
|
||||
|
||||
|
||||
def visual_style_enum():
|
||||
"""Return enumerator for viewport visual style."""
|
||||
return [
|
||||
{"label": "Realistic", "value": "Realistic"},
|
||||
{"label": "Shaded", "value": "Shaded"},
|
||||
{"label": "Facets", "value": "Facets"},
|
||||
{"label": "ConsistentColors",
|
||||
"value": "ConsistentColors"},
|
||||
{"label": "Wireframe", "value": "Wireframe"},
|
||||
{"label": "BoundingBox", "value": "BoundingBox"},
|
||||
{"label": "Ink", "value": "Ink"},
|
||||
{"label": "ColorInk", "value": "ColorInk"},
|
||||
{"label": "Acrylic", "value": "Acrylic"},
|
||||
{"label": "Tech", "value": "Tech"},
|
||||
{"label": "Graphite", "value": "Graphite"},
|
||||
{"label": "ColorPencil", "value": "ColorPencil"},
|
||||
{"label": "Pastel", "value": "Pastel"},
|
||||
{"label": "Clay", "value": "Clay"},
|
||||
{"label": "ModelAssist", "value": "ModelAssist"}
|
||||
]
|
||||
|
||||
|
||||
def preview_preset_enum():
|
||||
"""Return enumerator for viewport visual preset."""
|
||||
return [
|
||||
{"label": "Quality", "value": "Quality"},
|
||||
{"label": "Standard", "value": "Standard"},
|
||||
{"label": "Performance", "value": "Performance"},
|
||||
{"label": "DXMode", "value": "DXMode"},
|
||||
{"label": "Customize", "value": "Customize"},
|
||||
]
|
||||
|
||||
|
||||
def anti_aliasing_enum():
|
||||
"""Return enumerator for viewport anti-aliasing."""
|
||||
return [
|
||||
{"label": "None", "value": "None"},
|
||||
{"label": "2X", "value": "2X"},
|
||||
{"label": "4X", "value": "4X"},
|
||||
{"label": "8X", "value": "8X"}
|
||||
]
|
||||
|
||||
|
||||
class CreateReviewModel(BaseSettingsModel):
|
||||
review_width: int = SettingsField(1920, title="Review Width")
|
||||
review_height: int = SettingsField(1080, title="Review Height")
|
||||
percentSize: float = SettingsField(100.0, title="Percent of Output")
|
||||
keep_images: bool = SettingsField(False, title="Keep Image Sequences")
|
||||
image_format: str = SettingsField(
|
||||
enum_resolver=image_format_enum,
|
||||
title="Image Format Options"
|
||||
)
|
||||
visual_style: str = SettingsField(
|
||||
enum_resolver=visual_style_enum,
|
||||
title="Preference"
|
||||
)
|
||||
viewport_preset: str = SettingsField(
|
||||
enum_resolver=preview_preset_enum,
|
||||
title="Preview Preset"
|
||||
)
|
||||
anti_aliasing: str = SettingsField(
|
||||
enum_resolver=anti_aliasing_enum,
|
||||
title="Anti-aliasing Quality"
|
||||
)
|
||||
vp_texture: bool = SettingsField(True, title="Viewport Texture")
|
||||
|
||||
|
||||
DEFAULT_CREATE_REVIEW_SETTINGS = {
|
||||
"review_width": 1920,
|
||||
"review_height": 1080,
|
||||
"percentSize": 100.0,
|
||||
"keep_images": False,
|
||||
"image_format": "png",
|
||||
"visual_style": "Realistic",
|
||||
"viewport_preset": "Quality",
|
||||
"anti_aliasing": "None",
|
||||
"vp_texture": True
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
from pydantic import validator
|
||||
from ayon_server.settings import BaseSettingsModel, SettingsField
|
||||
from ayon_server.settings.validators import ensure_unique_names
|
||||
|
||||
|
||||
class ImageIOConfigModel(BaseSettingsModel):
|
||||
"""[DEPRECATED] Addon OCIO config settings. Please set the OCIO config
|
||||
path in the Core addon profiles here
|
||||
(ayon+settings://core/imageio/ocio_config_profiles).
|
||||
"""
|
||||
|
||||
override_global_config: bool = SettingsField(
|
||||
False,
|
||||
title="Override global OCIO config",
|
||||
description=(
|
||||
"DEPRECATED functionality. Please set the OCIO config path in the "
|
||||
"Core addon profiles here (ayon+settings://core/imageio/"
|
||||
"ocio_config_profiles)."
|
||||
),
|
||||
)
|
||||
filepath: list[str] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Config path",
|
||||
description=(
|
||||
"DEPRECATED functionality. Please set the OCIO config path in the "
|
||||
"Core addon profiles here (ayon+settings://core/imageio/"
|
||||
"ocio_config_profiles)."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ImageIOFileRuleModel(BaseSettingsModel):
|
||||
name: str = SettingsField("", title="Rule name")
|
||||
pattern: str = SettingsField("", title="Regex pattern")
|
||||
colorspace: str = SettingsField("", title="Colorspace name")
|
||||
ext: str = SettingsField("", title="File extension")
|
||||
|
||||
|
||||
class ImageIOFileRulesModel(BaseSettingsModel):
|
||||
activate_host_rules: bool = SettingsField(False)
|
||||
rules: list[ImageIOFileRuleModel] = SettingsField(
|
||||
default_factory=list,
|
||||
title="Rules"
|
||||
)
|
||||
|
||||
@validator("rules")
|
||||
def validate_unique_outputs(cls, value):
|
||||
ensure_unique_names(value)
|
||||
return value
|
||||
|
||||
|
||||
class ImageIOSettings(BaseSettingsModel):
|
||||
activate_host_color_management: bool = SettingsField(
|
||||
True, title="Enable Color Management"
|
||||
)
|
||||
ocio_config: ImageIOConfigModel = SettingsField(
|
||||
default_factory=ImageIOConfigModel,
|
||||
title="OCIO config"
|
||||
)
|
||||
file_rules: ImageIOFileRulesModel = SettingsField(
|
||||
default_factory=ImageIOFileRulesModel,
|
||||
title="File Rules"
|
||||
)
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
from ayon_server.settings import BaseSettingsModel, SettingsField
|
||||
from .imageio import ImageIOSettings
|
||||
from .render_settings import (
|
||||
RenderSettingsModel, DEFAULT_RENDER_SETTINGS
|
||||
)
|
||||
from .create_review_settings import (
|
||||
CreateReviewModel, DEFAULT_CREATE_REVIEW_SETTINGS
|
||||
)
|
||||
from .publishers import (
|
||||
PublishersModel, DEFAULT_PUBLISH_SETTINGS
|
||||
)
|
||||
|
||||
|
||||
def unit_scale_enum():
|
||||
"""Return enumerator for scene unit scale."""
|
||||
return [
|
||||
{"label": "mm", "value": "Millimeters"},
|
||||
{"label": "cm", "value": "Centimeters"},
|
||||
{"label": "m", "value": "Meters"},
|
||||
{"label": "km", "value": "Kilometers"}
|
||||
]
|
||||
|
||||
|
||||
class UnitScaleSettings(BaseSettingsModel):
|
||||
enabled: bool = SettingsField(True, title="Enabled")
|
||||
scene_unit_scale: str = SettingsField(
|
||||
"Centimeters",
|
||||
title="Scene Unit Scale",
|
||||
enum_resolver=unit_scale_enum
|
||||
)
|
||||
|
||||
|
||||
class PRTAttributesModel(BaseSettingsModel):
|
||||
_layout = "compact"
|
||||
name: str = SettingsField(title="Name")
|
||||
value: str = SettingsField(title="Attribute")
|
||||
|
||||
|
||||
class PointCloudSettings(BaseSettingsModel):
|
||||
attribute: list[PRTAttributesModel] = SettingsField(
|
||||
default_factory=list, title="Channel Attribute")
|
||||
|
||||
|
||||
class MaxSettings(BaseSettingsModel):
|
||||
unit_scale_settings: UnitScaleSettings = SettingsField(
|
||||
default_factory=UnitScaleSettings,
|
||||
title="Set Unit Scale"
|
||||
)
|
||||
imageio: ImageIOSettings = SettingsField(
|
||||
default_factory=ImageIOSettings,
|
||||
title="Color Management (ImageIO)"
|
||||
)
|
||||
RenderSettings: RenderSettingsModel = SettingsField(
|
||||
default_factory=RenderSettingsModel,
|
||||
title="Render Settings"
|
||||
)
|
||||
CreateReview: CreateReviewModel = SettingsField(
|
||||
default_factory=CreateReviewModel,
|
||||
title="Create Review"
|
||||
)
|
||||
PointCloud: PointCloudSettings = SettingsField(
|
||||
default_factory=PointCloudSettings,
|
||||
title="Point Cloud"
|
||||
)
|
||||
publish: PublishersModel = SettingsField(
|
||||
default_factory=PublishersModel,
|
||||
title="Publish Plugins")
|
||||
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
"unit_scale_settings": {
|
||||
"enabled": True,
|
||||
"scene_unit_scale": "Centimeters"
|
||||
},
|
||||
"RenderSettings": DEFAULT_RENDER_SETTINGS,
|
||||
"CreateReview": DEFAULT_CREATE_REVIEW_SETTINGS,
|
||||
"PointCloud": {
|
||||
"attribute": [
|
||||
{"name": "Age", "value": "age"},
|
||||
{"name": "Radius", "value": "radius"},
|
||||
{"name": "Position", "value": "position"},
|
||||
{"name": "Rotation", "value": "rotation"},
|
||||
{"name": "Scale", "value": "scale"},
|
||||
{"name": "Velocity", "value": "velocity"},
|
||||
{"name": "Color", "value": "color"},
|
||||
{"name": "TextureCoordinate", "value": "texcoord"},
|
||||
{"name": "MaterialID", "value": "matid"},
|
||||
{"name": "custFloats", "value": "custFloats"},
|
||||
{"name": "custVecs", "value": "custVecs"},
|
||||
]
|
||||
},
|
||||
"publish": DEFAULT_PUBLISH_SETTINGS
|
||||
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
from ayon_server.settings import BaseSettingsModel, SettingsField
|
||||
|
||||
|
||||
def aov_separators_enum():
|
||||
return [
|
||||
{"value": "dash", "label": "- (dash)"},
|
||||
{"value": "underscore", "label": "_ (underscore)"},
|
||||
{"value": "dot", "label": ". (dot)"}
|
||||
]
|
||||
|
||||
|
||||
def image_format_enum():
|
||||
"""Return enumerator for image output formats."""
|
||||
return [
|
||||
{"label": "bmp", "value": "bmp"},
|
||||
{"label": "exr", "value": "exr"},
|
||||
{"label": "tif", "value": "tif"},
|
||||
{"label": "tiff", "value": "tiff"},
|
||||
{"label": "jpg", "value": "jpg"},
|
||||
{"label": "png", "value": "png"},
|
||||
{"label": "tga", "value": "tga"},
|
||||
{"label": "dds", "value": "dds"}
|
||||
]
|
||||
|
||||
|
||||
class RenderSettingsModel(BaseSettingsModel):
|
||||
default_render_image_folder: str = SettingsField(
|
||||
title="Default render image folder"
|
||||
)
|
||||
aov_separator: str = SettingsField(
|
||||
"underscore",
|
||||
title="AOV Separator character",
|
||||
enum_resolver=aov_separators_enum
|
||||
)
|
||||
image_format: str = SettingsField(
|
||||
enum_resolver=image_format_enum,
|
||||
title="Output Image Format"
|
||||
)
|
||||
multipass: bool = SettingsField(title="multipass")
|
||||
|
||||
|
||||
DEFAULT_RENDER_SETTINGS = {
|
||||
"default_render_image_folder": "renders/3dsmax",
|
||||
"aov_separator": "underscore",
|
||||
"image_format": "exr",
|
||||
"multipass": True
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue