mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
425 lines
12 KiB
Python
425 lines
12 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Library of functions useful for 3dsmax pipeline."""
|
|
import contextlib
|
|
import json
|
|
from typing import Any, Dict, Union
|
|
|
|
import six
|
|
from openpype.pipeline.context_tools import (
|
|
get_current_project, get_current_project_asset)
|
|
from pymxs import runtime as rt
|
|
|
|
JSON_PREFIX = "JSON::"
|
|
|
|
|
|
def imprint(node_name: str, data: dict) -> bool:
|
|
node = rt.GetNodeByName(node_name)
|
|
if not node:
|
|
return False
|
|
|
|
for k, v in data.items():
|
|
if isinstance(v, (dict, list)):
|
|
rt.SetUserProp(node, k, f"{JSON_PREFIX}{json.dumps(v)}")
|
|
else:
|
|
rt.SetUserProp(node, k, v)
|
|
|
|
return True
|
|
|
|
|
|
def lsattr(
|
|
attr: str,
|
|
value: Union[str, None] = None,
|
|
root: Union[str, None] = None) -> list:
|
|
"""List nodes having attribute with specified value.
|
|
|
|
Args:
|
|
attr (str): Attribute name to match.
|
|
value (str, Optional): Value to match, of omitted, all nodes
|
|
with specified attribute are returned no matter of value.
|
|
root (str, Optional): Root node name. If omitted, scene root is used.
|
|
|
|
Returns:
|
|
list of nodes.
|
|
"""
|
|
root = rt.RootNode if root is None else rt.GetNodeByName(root)
|
|
|
|
def output_node(node, nodes):
|
|
nodes.append(node)
|
|
for child in node.Children:
|
|
output_node(child, nodes)
|
|
|
|
nodes = []
|
|
output_node(root, nodes)
|
|
return [
|
|
n for n in nodes
|
|
if rt.GetUserProp(n, attr) == value
|
|
] if value else [
|
|
n for n in nodes
|
|
if rt.GetUserProp(n, attr)
|
|
]
|
|
|
|
|
|
def read(container) -> dict:
|
|
data = {}
|
|
props = rt.GetUserPropBuffer(container)
|
|
# this shouldn't happen but let's guard against it anyway
|
|
if not props:
|
|
return data
|
|
|
|
for line in props.split("\r\n"):
|
|
try:
|
|
key, value = line.split("=")
|
|
except ValueError:
|
|
# if the line cannot be split we can't really parse it
|
|
continue
|
|
|
|
value = value.strip()
|
|
if isinstance(value.strip(), six.string_types) and \
|
|
value.startswith(JSON_PREFIX):
|
|
with contextlib.suppress(json.JSONDecodeError):
|
|
value = json.loads(value[len(JSON_PREFIX):])
|
|
|
|
# default value behavior
|
|
# convert maxscript boolean values
|
|
if value == "true":
|
|
value = True
|
|
elif value == "false":
|
|
value = False
|
|
|
|
data[key.strip()] = value
|
|
|
|
data["instance_node"] = container.Name
|
|
|
|
return data
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def maintained_selection():
|
|
previous_selection = rt.GetCurrentSelection()
|
|
try:
|
|
yield
|
|
finally:
|
|
if previous_selection:
|
|
rt.Select(previous_selection)
|
|
else:
|
|
rt.Select()
|
|
|
|
|
|
def get_all_children(parent, node_type=None):
|
|
"""Handy function to get all the children of a given node
|
|
|
|
Args:
|
|
parent (3dsmax Node1): Node to get all children of.
|
|
node_type (None, runtime.class): give class to check for
|
|
e.g. rt.FFDBox/rt.GeometryClass etc.
|
|
|
|
Returns:
|
|
list: list of all children of the parent node
|
|
"""
|
|
def list_children(node):
|
|
children = []
|
|
for c in node.Children:
|
|
children.append(c)
|
|
children = children + list_children(c)
|
|
return children
|
|
child_list = list_children(parent)
|
|
|
|
return ([x for x in child_list if rt.SuperClassOf(x) == node_type]
|
|
if node_type else child_list)
|
|
|
|
|
|
def get_current_renderer():
|
|
"""
|
|
Notes:
|
|
Get current renderer for Max
|
|
|
|
Returns:
|
|
"{Current Renderer}:{Current Renderer}"
|
|
e.g. "Redshift_Renderer:Redshift_Renderer"
|
|
"""
|
|
return rt.renderers.production
|
|
|
|
|
|
def get_default_render_folder(project_setting=None):
|
|
return (project_setting["max"]
|
|
["RenderSettings"]
|
|
["default_render_image_folder"])
|
|
|
|
|
|
def set_render_frame_range(start_frame, end_frame):
|
|
"""
|
|
Note:
|
|
Frame range can be specified in different types. Possible values are:
|
|
* `1` - Single frame.
|
|
* `2` - Active time segment ( animationRange ).
|
|
* `3` - User specified Range.
|
|
* `4` - User specified Frame pickup string (for example `1,3,5-12`).
|
|
|
|
Todo:
|
|
Current type is hard-coded, there should be a custom setting for this.
|
|
"""
|
|
rt.rendTimeType = 3
|
|
if start_frame is not None and end_frame is not None:
|
|
rt.rendStart = int(start_frame)
|
|
rt.rendEnd = int(end_frame)
|
|
|
|
|
|
def get_multipass_setting(project_setting=None):
|
|
return (project_setting["max"]
|
|
["RenderSettings"]
|
|
["multipass"])
|
|
|
|
|
|
def set_scene_resolution(width: int, height: int):
|
|
"""Set the render resolution
|
|
|
|
Args:
|
|
width(int): value of the width
|
|
height(int): value of the height
|
|
|
|
Returns:
|
|
None
|
|
|
|
"""
|
|
# make sure the render dialog is closed
|
|
# for the update of resolution
|
|
# Changing the Render Setup dialog settings should be done
|
|
# with the actual Render Setup dialog in a closed state.
|
|
if rt.renderSceneDialog.isOpen():
|
|
rt.renderSceneDialog.close()
|
|
|
|
rt.renderWidth = width
|
|
rt.renderHeight = height
|
|
|
|
|
|
def reset_scene_resolution():
|
|
"""Apply the scene resolution from the project definition
|
|
|
|
scene resolution can be overwritten by an asset if the asset.data contains
|
|
any information regarding scene resolution .
|
|
Returns:
|
|
None
|
|
"""
|
|
data = ["data.resolutionWidth", "data.resolutionHeight"]
|
|
project_resolution = get_current_project(fields=data)
|
|
project_resolution_data = project_resolution["data"]
|
|
asset_resolution = get_current_project_asset(fields=data)
|
|
asset_resolution_data = asset_resolution["data"]
|
|
# Set project resolution
|
|
project_width = int(project_resolution_data.get("resolutionWidth", 1920))
|
|
project_height = int(project_resolution_data.get("resolutionHeight", 1080))
|
|
width = int(asset_resolution_data.get("resolutionWidth", project_width))
|
|
height = int(asset_resolution_data.get("resolutionHeight", project_height))
|
|
|
|
set_scene_resolution(width, height)
|
|
|
|
|
|
def get_frame_range() -> Union[Dict[str, Any], None]:
|
|
"""Get the current assets frame range and handles.
|
|
|
|
Returns:
|
|
dict: with frame start, frame end, handle start, handle end.
|
|
"""
|
|
# Set frame start/end
|
|
asset = get_current_project_asset()
|
|
frame_start = asset["data"].get("frameStart")
|
|
frame_end = asset["data"].get("frameEnd")
|
|
|
|
if frame_start is None or frame_end is None:
|
|
return
|
|
|
|
handle_start = asset["data"].get("handleStart", 0)
|
|
handle_end = asset["data"].get("handleEnd", 0)
|
|
return {
|
|
"frameStart": frame_start,
|
|
"frameEnd": frame_end,
|
|
"handleStart": handle_start,
|
|
"handleEnd": handle_end
|
|
}
|
|
|
|
|
|
def reset_frame_range(fps: bool = True):
|
|
"""Set frame range to current asset.
|
|
This is part of 3dsmax documentation:
|
|
|
|
animationRange: A System Global variable which lets you get and
|
|
set an Interval value that defines the start and end frames
|
|
of the Active Time Segment.
|
|
frameRate: A System Global variable which lets you get
|
|
and set an Integer value that defines the current
|
|
scene frame rate in frames-per-second.
|
|
"""
|
|
if fps:
|
|
data_fps = get_current_project(fields=["data.fps"])
|
|
fps_number = float(data_fps["data"]["fps"])
|
|
rt.frameRate = fps_number
|
|
frame_range = get_frame_range()
|
|
frame_start_handle = frame_range["frameStart"] - int(
|
|
frame_range["handleStart"]
|
|
)
|
|
frame_end_handle = frame_range["frameEnd"] + int(frame_range["handleEnd"])
|
|
set_timeline(frame_start_handle, frame_end_handle)
|
|
set_render_frame_range(frame_start_handle, frame_end_handle)
|
|
|
|
|
|
def set_context_setting():
|
|
"""Apply the project settings from the project definition
|
|
|
|
Settings can be overwritten by an asset if the asset.data contains
|
|
any information regarding those settings.
|
|
|
|
Examples of settings:
|
|
frame range
|
|
resolution
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
reset_scene_resolution()
|
|
reset_frame_range()
|
|
|
|
|
|
def get_max_version():
|
|
"""
|
|
Args:
|
|
get max version date for deadline
|
|
|
|
Returns:
|
|
#(25000, 62, 0, 25, 0, 0, 997, 2023, "")
|
|
max_info[7] = max version date
|
|
"""
|
|
max_info = rt.MaxVersion()
|
|
return max_info[7]
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def viewport_camera(camera):
|
|
original = rt.viewport.getCamera()
|
|
if not original:
|
|
# if there is no original camera
|
|
# use the current camera as original
|
|
original = rt.getNodeByName(camera)
|
|
review_camera = rt.getNodeByName(camera)
|
|
try:
|
|
rt.viewport.setCamera(review_camera)
|
|
yield
|
|
finally:
|
|
rt.viewport.setCamera(original)
|
|
|
|
|
|
def set_timeline(frameStart, frameEnd):
|
|
"""Set frame range for timeline editor in Max
|
|
"""
|
|
rt.animationRange = rt.interval(frameStart, frameEnd)
|
|
return rt.animationRange
|
|
|
|
|
|
def unique_namespace(namespace, format="%02d",
|
|
prefix="", suffix="", con_suffix="CON"):
|
|
"""Return unique namespace
|
|
|
|
Arguments:
|
|
namespace (str): Name of namespace to consider
|
|
format (str, optional): Formatting of the given iteration number
|
|
suffix (str, optional): Only consider namespaces with this suffix.
|
|
con_suffix: max only, for finding the name of the master container
|
|
|
|
>>> unique_namespace("bar")
|
|
# bar01
|
|
>>> unique_namespace(":hello")
|
|
# :hello01
|
|
>>> unique_namespace("bar:", suffix="_NS")
|
|
# bar01_NS:
|
|
|
|
"""
|
|
|
|
def current_namespace():
|
|
current = namespace
|
|
# When inside a namespace Max adds no trailing :
|
|
if not current.endswith(":"):
|
|
current += ":"
|
|
return current
|
|
|
|
# Always check against the absolute namespace root
|
|
# There's no clash with :x if we're defining namespace :a:x
|
|
ROOT = ":" if namespace.startswith(":") else current_namespace()
|
|
|
|
# Strip trailing `:` tokens since we might want to add a suffix
|
|
start = ":" if namespace.startswith(":") else ""
|
|
end = ":" if namespace.endswith(":") else ""
|
|
namespace = namespace.strip(":")
|
|
if ":" in namespace:
|
|
# Split off any nesting that we don't uniqify anyway.
|
|
parents, namespace = namespace.rsplit(":", 1)
|
|
start += parents + ":"
|
|
ROOT += start
|
|
|
|
iteration = 1
|
|
increment_version = True
|
|
while increment_version:
|
|
nr_namespace = namespace + format % iteration
|
|
unique = prefix + nr_namespace + suffix
|
|
container_name = f"{unique}:{namespace}{con_suffix}"
|
|
if not rt.getNodeByName(container_name):
|
|
name_space = start + unique + end
|
|
increment_version = False
|
|
return name_space
|
|
else:
|
|
increment_version = True
|
|
iteration += 1
|
|
|
|
|
|
def get_namespace(container_name):
|
|
"""Get the namespace and name of the sub-container
|
|
|
|
Args:
|
|
container_name (str): the name of master container
|
|
|
|
Raises:
|
|
RuntimeError: when there is no master container found
|
|
|
|
Returns:
|
|
namespace (str): namespace of the sub-container
|
|
name (str): name of the sub-container
|
|
"""
|
|
node = rt.getNodeByName(container_name)
|
|
if not node:
|
|
raise RuntimeError("Master Container Not Found..")
|
|
name = rt.getUserProp(node, "name")
|
|
namespace = rt.getUserProp(node, "namespace")
|
|
return namespace, name
|
|
|
|
|
|
def object_transform_set(container_children):
|
|
"""A function which allows to store the transform of
|
|
previous loaded object(s)
|
|
Args:
|
|
container_children(list): A list of nodes
|
|
|
|
Returns:
|
|
transform_set (dict): A dict with all transform data of
|
|
the previous loaded object(s)
|
|
"""
|
|
transform_set = {}
|
|
for node in container_children:
|
|
name = f"{node.name}.transform"
|
|
transform_set[name] = node.pos
|
|
name = f"{node.name}.scale"
|
|
transform_set[name] = node.scale
|
|
return transform_set
|
|
|
|
|
|
def get_plugins() -> list:
|
|
"""Get all loaded plugins in 3dsMax
|
|
|
|
Returns:
|
|
plugin_info_list: a list of loaded plugins
|
|
"""
|
|
manager = rt.PluginManager
|
|
count = manager.pluginDllCount
|
|
plugin_info_list = []
|
|
for p in range(1, count + 1):
|
|
plugin_info = manager.pluginDllName(p)
|
|
plugin_info_list.append(plugin_info)
|
|
|
|
return plugin_info_list
|