setting up deadline for 3dsmax

This commit is contained in:
Kayla Man 2023-02-08 22:03:05 +08:00
parent 9965e6b991
commit 95aff1808f
10 changed files with 565 additions and 2 deletions

View file

@ -120,3 +120,36 @@ def get_all_children(parent, node_type=None):
return ([x for x in child_list if rt.superClassOf(x) == node_type]
if node_type else child_list)
def get_current_renderer():
"""get current renderer"""
return rt.renderers.production
def get_default_render_folder(project_setting=None):
return (project_setting["max"]
["RenderSettings"]
["default_render_image_folder"]
)
def set_framerange(startFrame, endFrame):
"""Get/set the type of time range to be rendered.
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").
"""
# hard-code, there should be a custom setting for this
rt.rendTimeType = 4
if startFrame is not None and endFrame is not None:
frameRange = "{0}-{1}".format(startFrame, endFrame)
rt.rendPickupFrames = frameRange

View file

@ -0,0 +1,102 @@
# 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 openpype.hosts.max.api.lib import (
get_current_renderer,
get_default_render_folder
)
from openpype.pipeline.context_tools import get_current_project_asset
from openpype.settings import get_project_settings
from openpype.pipeline import legacy_io
class RenderProducts(object):
@classmethod
def __init__(self, project_settings=None):
self._project_settings = project_settings
if not self._project_settings:
self._project_settings = get_project_settings(
legacy_io.Session["AVALON_PROJECT"]
)
def render_product(self, container):
folder = rt.maxFilePath
folder = folder.replace("\\", "/")
setting = self._project_settings
render_folder = get_default_render_folder(setting)
output_file = os.path.join(folder, render_folder, container)
context = get_current_project_asset()
startFrame = context["data"].get("frameStart")
endFrame = context["data"].get("frameEnd") + 1
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"]
full_render_list = self.beauty_render_product(output_file,
startFrame,
endFrame,
img_fmt)
renderer_class = get_current_renderer()
renderer = str(renderer_class).split(":")[0]
if renderer == "VUE_File_Renderer":
return full_render_list
if (
renderer == "ART_Renderer" or
renderer == "Redshift Renderer" or
renderer == "V_Ray_6_Hotfix_3" or
renderer == "V_Ray_GPU_6_Hotfix_3" or
renderer == "Default_Scanline_Renderer" or
renderer == "Quicksilver_Hardware_Renderer"
):
render_elem_list = self.render_elements_product(output_file,
startFrame,
endFrame,
img_fmt)
for render_elem in render_elem_list:
full_render_list.append(render_elem)
return full_render_list
if renderer == "Arnold":
return full_render_list
def beauty_render_product(self, folder, startFrame, endFrame, fmt):
# get the beauty
beauty_frame_range = list()
for f in range(startFrame, endFrame):
beauty = "{0}.{1}.{2}".format(folder, str(f), fmt)
beauty = beauty.replace("\\", "/")
beauty_frame_range.append(beauty)
return beauty_frame_range
# TODO: Get the arnold render product
def render_elements_product(self, folder, startFrame, endFrame, fmt):
"""Get all the render element output files. """
render_dirname = list()
render_elem = rt.maxOps.GetCurRenderElementMgr()
render_elem_num = render_elem.NumRenderElements()
# get render elements from the renders
for i in range(render_elem_num):
renderlayer_name = render_elem.GetRenderElement(i)
target, renderpass = str(renderlayer_name).split(":")
render_dir = os.path.join(folder, renderpass)
if renderlayer_name.enabled:
for f in range(startFrame, endFrame):
render_element = "{0}.{1}.{2}".format(render_dir, str(f), fmt)
render_element = render_element.replace("\\", "/")
render_dirname.append(render_element)
return render_dirname
def image_format(self):
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"]
return img_fmt

View file

@ -0,0 +1,125 @@
import os
from pymxs import runtime as rt
from openpype.lib import Logger
from openpype.settings import get_project_settings
from openpype.pipeline import legacy_io
from openpype.pipeline.context_tools import get_current_project_asset
from openpype.hosts.max.api.lib import (
set_framerange,
get_current_renderer,
get_default_render_folder
)
class RenderSettings(object):
log = Logger.get_logger("RenderSettings")
_aov_chars = {
"dot": ".",
"dash": "-",
"underscore": "_"
}
@classmethod
def __init__(self, project_settings=None):
self._project_settings = project_settings
if not self._project_settings:
self._project_settings = get_project_settings(
legacy_io.Session["AVALON_PROJECT"]
)
def set_render_camera(self, selection):
for sel in selection:
# to avoid Attribute Error from pymxs wrapper
found = False
if rt.classOf(sel) in rt.Camera.classes:
found = True
rt.viewport.setCamera(sel)
break
if not found:
raise RuntimeError("Camera not found")
def set_renderoutput(self, container):
folder = rt.maxFilePath
# hard-coded, should be customized in the setting
folder = folder.replace("\\", "/")
# hard-coded, set the renderoutput path
setting = self._project_settings
render_folder = get_default_render_folder(setting)
output_dir = os.path.join(folder, render_folder)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# hard-coded, should be customized in the setting
context = get_current_project_asset()
# get project reoslution
width = context["data"].get("resolutionWidth")
height = context["data"].get("resolutionHeight")
# Set Frame Range
startFrame = context["data"].get("frameStart")
endFrame = context["data"].get("frameEnd")
set_framerange(startFrame, endFrame)
# get the production render
renderer_class = get_current_renderer()
renderer = str(renderer_class).split(":")[0]
img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"]
output = os.path.join(output_dir, container)
try:
aov_separator = self._aov_chars[(
self._project_settings["maya"]
["RenderSettings"]
["aov_separator"]
)]
except KeyError:
aov_separator = "."
outputFilename = "{0}.{1}".format(output, img_fmt)
outputFilename = outputFilename.replace("{aov_separator}", aov_separator)
rt.rendOutputFilename = outputFilename
if renderer == "VUE_File_Renderer":
return
# TODO: Finish the arnold render setup
if renderer == "Arnold":
return
if (
renderer == "ART_Renderer" or
renderer == "Redshift Renderer" or
renderer == "V_Ray_6_Hotfix_3" or
renderer == "V_Ray_GPU_6_Hotfix_3" or
renderer == "Default_Scanline_Renderer" or
renderer == "Quicksilver_Hardware_Renderer"
):
self.render_element_layer(output, width, height, img_fmt)
rt.rendSaveFile= True
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(":")
render_element = os.path.join(dir, renderpass)
aov_name = "{0}.{1}".format(render_element, ext)
try:
aov_separator = self._aov_chars[(
self._project_settings["maya"]
["RenderSettings"]
["aov_separator"]
)]
except KeyError:
aov_separator = "."
aov_name = aov_name.replace("{aov_separator}", aov_separator)
render_elem.SetRenderElementFileName(i, aov_name)

View file

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
"""Creator plugin for creating camera."""
from openpype.hosts.max.api import plugin
from openpype.pipeline import CreatedInstance
from openpype.hosts.max.api.lib_rendersettings import RenderSettings
class CreateRender(plugin.MaxCreator):
identifier = "io.openpype.creators.max.render"
label = "Render"
family = "maxrender"
icon = "gear"
def create(self, subset_name, instance_data, pre_create_data):
from pymxs import runtime as rt
sel_obj = list(rt.selection)
instance = super(CreateRender, self).create(
subset_name,
instance_data,
pre_create_data) # type: CreatedInstance
container_name = instance.data.get("instance_node")
container = rt.getNodeByName(container_name)
# TODO: Disable "Add to Containers?" Panel
# parent the selected cameras into the container
for obj in sel_obj:
obj.parent = container
# for additional work on the node:
# instance_node = rt.getNodeByName(instance.get("instance_node"))
# set viewport camera for rendering(mandatory for deadline)
RenderSettings().set_render_camera(sel_obj)
# set output paths for rendering(mandatory for deadline)
RenderSettings().set_renderoutput(container_name)

View file

@ -0,0 +1,72 @@
# -*- coding: utf-8 -*-
"""Collect Render"""
import os
import pyblish.api
from pymxs import runtime as rt
from openpype.pipeline import legacy_io
from openpype.hosts.max.api.lib import get_current_renderer
from openpype.hosts.max.api.lib_renderproducts import RenderProducts
class CollectRender(pyblish.api.InstancePlugin):
"""Collect Render for Deadline"""
order = pyblish.api.CollectorOrder + 0.01
label = "Collect 3dmax 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
asset = legacy_io.Session["AVALON_ASSET"]
render_layer_files = RenderProducts().render_product(instance.name)
folder = folder.replace("\\", "/")
imgFormat = RenderProducts().image_format()
renderer_class = get_current_renderer()
renderer_name = str(renderer_class).split(":")[0]
# setup the plugin as 3dsmax for the internal renderer
if (
renderer_name == "ART_Renderer" or
renderer_name == "Default_Scanline_Renderer" or
renderer_name == "Quicksilver_Hardware_Renderer"
):
plugin = "3dsmax"
if (
renderer_name == "V_Ray_6_Hotfix_3" or
renderer_name == "V_Ray_GPU_6_Hotfix_3"
):
plugin = "Vray"
if renderer_name == "Redshift Renderer":
plugin = "redshift"
if renderer_name == "Arnold":
plugin = "arnold"
# https://forums.autodesk.com/t5/3ds-max-programming/pymxs-quickrender-animation-range/td-p/11216183
data = {
"subset": instance.name,
"asset": asset,
"publish": True,
"imageFormat": imgFormat,
"family": 'maxrender',
"families": ['maxrender'],
"source": filepath,
"files": render_layer_files,
"plugin": plugin,
"frameStart": context.data['frameStart'],
"frameEnd": context.data['frameEnd']
}
self.log.info("data: {0}".format(data))
instance.data.update(data)

View file

@ -184,7 +184,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
self.log.info("multipart: {}".format(
multipart))
assert exp_files, "no file names were generated, this is bug"
self.log.info(exp_files)
# if we want to attach render to subset, check if we have AOV's
# in expectedFiles. If so, raise error as we cannot attach AOV
@ -320,7 +319,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin):
"renderSetupIncludeLights"
)
}
# Collect Deadline url if Deadline module is enabled
deadline_settings = (
context.data["system_settings"]["modules"]["deadline"]

View file

@ -0,0 +1,137 @@
import os
import json
import getpass
import requests
import pyblish.api
from openpype.pipeline import legacy_io
class MaxSubmitRenderDeadline(pyblish.api.InstancePlugin):
"""
3DMax File Submit Render Deadline
"""
label = "Submit 3DsMax Render to Deadline"
order = pyblish.api.IntegratorOrder
hosts = ["max"]
families = ["maxrender"]
targets = ["local"]
def process(self, instance):
context = instance.context
filepath = context.data["currentFile"]
filename = os.path.basename(filepath)
comment = context.data.get("comment", "")
deadline_user = context.data.get("deadlineUser", getpass.getuser())
jobname ="{0} - {1}".format(filename, instance.name)
# StartFrame to EndFrame
frames = "{start}-{end}".format(
start=int(instance.data["frameStart"]),
end=int(instance.data["frameEnd"])
)
payload = {
"JobInfo": {
# Top-level group name
"BatchName": filename,
# Job name, as seen in Monitor
"Name": jobname,
# Arbitrary username, for visualisation in Monitor
"UserName": deadline_user,
"Plugin": instance.data["plugin"],
"Pool": instance.data.get("primaryPool"),
"secondaryPool": instance.data.get("secondaryPool"),
"Frames": frames,
"ChunkSize" : instance.data.get("chunkSize", 10),
"Comment": comment
},
"PluginInfo": {
# Input
"SceneFile": instance.data["source"],
"Version": "2023",
"SaveFile" : True,
# Mandatory for Deadline
# Houdini version without patch number
"IgnoreInputs": True
},
# Mandatory for Deadline, may be empty
"AuxFiles": []
}
# Include critical environment variables with submission + api.Session
keys = [
# Submit along the current Avalon tool setup that we launched
# this application with so the Render Slave can build its own
# similar environment using it, e.g. "maya2018;vray4.x;yeti3.1.9"
"AVALON_TOOLS",
"OPENPYPE_VERSION"
]
# Add mongo url if it's enabled
if context.data.get("deadlinePassMongoUrl"):
keys.append("OPENPYPE_MONGO")
environment = dict({key: os.environ[key] for key in keys
if key in os.environ}, **legacy_io.Session)
payload["JobInfo"].update({
"EnvironmentKeyValue%d" % index: "{key}={value}".format(
key=key,
value=environment[key]
) for index, key in enumerate(environment)
})
# Include OutputFilename entries
# The first entry also enables double-click to preview rendered
# frames from Deadline Monitor
output_data = {}
# need to be fixed
for i, filepath in enumerate(instance.data["files"]):
dirname = os.path.dirname(filepath)
fname = os.path.basename(filepath)
output_data["OutputDirectory%d" % i] = dirname.replace("\\", "/")
output_data["OutputFilename%d" % i] = fname
if not os.path.exists(dirname):
self.log.info("Ensuring output directory exists: %s" %
dirname)
os.makedirs(dirname)
payload["JobInfo"].update(output_data)
self.submit(instance, payload)
def submit(self, instance, payload):
context = instance.context
deadline_url = context.data.get("defaultDeadline")
deadline_url = instance.data.get(
"deadlineUrl", deadline_url)
assert deadline_url, "Requires Deadline Webservice URL"
plugin = payload["JobInfo"]["Plugin"]
self.log.info("Using Render Plugin : {}".format(plugin))
self.log.info("Submitting..")
self.log.debug(json.dumps(payload, indent=4, sort_keys=True))
# E.g. http://192.168.0.1:8082/api/jobs
url = "{}/api/jobs".format(deadline_url)
response = requests.post(url, json=payload, verify=False)
if not response.ok:
raise Exception(response.text)
# Store output dir for unified publisher (filesequence)
expected_files = instance.data["files"]
self.log.info("exp:{}".format(expected_files))
output_dir = os.path.dirname(expected_files[0])
instance.data["outputDir"] = output_dir
instance.data["deadlineSubmissionJob"] = response.json()

View file

@ -0,0 +1,7 @@
{
"RenderSettings": {
"default_render_image_folder": "renders/max",
"aov_separator": "underscore",
"image_format": "exr"
}
}

View file

@ -82,6 +82,10 @@
"type": "schema",
"name": "schema_project_slack"
},
{
"type": "schema",
"name": "schema_project_max"
},
{
"type": "schema",
"name": "schema_project_maya"

View file

@ -0,0 +1,52 @@
{
"type": "dict",
"collapsible": true,
"key": "max",
"label": "Max",
"is_file": true,
"children": [
{
"type": "dict",
"collapsible": true,
"key": "RenderSettings",
"label": "Render Settings",
"children": [
{
"type": "text",
"key": "default_render_image_folder",
"label": "Default render image folder"
},
{
"key": "aov_separator",
"label": "AOV Separator character",
"type": "enum",
"multiselection": false,
"default": "underscore",
"enum_items": [
{"dash": "- (dash)"},
{"underscore": "_ (underscore)"},
{"dot": ". (dot)"}
]
},
{
"key": "image_format",
"label": "Output Image Format",
"type": "enum",
"multiselection": false,
"defaults": "exr",
"enum_items": [
{"avi": "avi"},
{"bmp": "bmp"},
{"exr": "exr"},
{"tif": "tif"},
{"tiff": "tiff"},
{"jpg": "jpg"},
{"png": "png"},
{"tga": "tga"},
{"dds": "dds"}
]
}
]
}
]
}