mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-26 05:42:15 +01:00
Merge pull request #2061 from pypeclub/feature/royalrender-integration
Basic Royal Render Integration ✨
This commit is contained in:
commit
dcece2630c
17 changed files with 1759 additions and 514 deletions
|
|
@ -158,7 +158,9 @@ def extractenvironments(output_json_path, project, asset, task, app):
|
|||
@click.option("-d", "--debug", is_flag=True, help="Print debug messages")
|
||||
@click.option("-t", "--targets", help="Targets module", default=None,
|
||||
multiple=True)
|
||||
def publish(debug, paths, targets):
|
||||
@click.option("-g", "--gui", is_flag=True,
|
||||
help="Show Publish UI", default=False)
|
||||
def publish(debug, paths, targets, gui):
|
||||
"""Start CLI publishing.
|
||||
|
||||
Publish collects json from paths provided as an argument.
|
||||
|
|
@ -166,7 +168,7 @@ def publish(debug, paths, targets):
|
|||
"""
|
||||
if debug:
|
||||
os.environ['OPENPYPE_DEBUG'] = '3'
|
||||
PypeCommands.publish(list(paths), targets)
|
||||
PypeCommands.publish(list(paths), targets, gui)
|
||||
|
||||
|
||||
@main.command()
|
||||
|
|
|
|||
|
|
@ -412,6 +412,14 @@ class FtrackModule(
|
|||
hours_logged = (task_entity["time_logged"] / 60) / 60
|
||||
return hours_logged
|
||||
|
||||
def get_credentials(self):
|
||||
# type: () -> tuple
|
||||
"""Get local Ftrack credentials."""
|
||||
from .lib import credentials
|
||||
|
||||
cred = credentials.get_credentials(self.ftrack_url)
|
||||
return cred.get("username"), cred.get("api_key")
|
||||
|
||||
def cli(self, click_group):
|
||||
click_group.add_command(cli_main)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect default Deadline server."""
|
||||
import pyblish.api
|
||||
import os
|
||||
|
||||
|
||||
class CollectLocalFtrackCreds(pyblish.api.ContextPlugin):
|
||||
"""Collect default Royal Render path."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.01
|
||||
label = "Collect local ftrack credentials"
|
||||
targets = ["rr_control"]
|
||||
|
||||
def process(self, context):
|
||||
if os.getenv("FTRACK_API_USER") and os.getenv("FTRACK_API_KEY") and \
|
||||
os.getenv("FTRACK_SERVER"):
|
||||
return
|
||||
ftrack_module = context.data["openPypeModules"]["ftrack"]
|
||||
if ftrack_module.enabled:
|
||||
creds = ftrack_module.get_credentials()
|
||||
os.environ["FTRACK_API_USER"] = creds[0]
|
||||
os.environ["FTRACK_API_KEY"] = creds[1]
|
||||
os.environ["FTRACK_SERVER"] = ftrack_module.ftrack_url
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
from .royal_render_module import RoyalRenderModule
|
||||
|
||||
|
||||
__all__ = (
|
||||
"RoyalRenderModule",
|
||||
)
|
||||
199
openpype/modules/default_modules/royal_render/api.py
Normal file
199
openpype/modules/default_modules/royal_render/api.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Wrapper around Royal Render API."""
|
||||
import sys
|
||||
import os
|
||||
|
||||
from openpype.settings import get_project_settings
|
||||
from openpype.lib.local_settings import OpenPypeSettingsRegistry
|
||||
from openpype.lib import PypeLogger, run_subprocess
|
||||
from .rr_job import RRJob, SubmitFile, SubmitterParameter
|
||||
|
||||
|
||||
log = PypeLogger.get_logger("RoyalRender")
|
||||
|
||||
|
||||
class Api:
|
||||
|
||||
_settings = None
|
||||
RR_SUBMIT_CONSOLE = 1
|
||||
RR_SUBMIT_API = 2
|
||||
|
||||
def __init__(self, settings, project=None):
|
||||
self._settings = settings
|
||||
self._initialize_rr(project)
|
||||
|
||||
def _initialize_rr(self, project=None):
|
||||
# type: (str) -> None
|
||||
"""Initialize RR Path.
|
||||
|
||||
Args:
|
||||
project (str, Optional): Project name to set RR api in
|
||||
context.
|
||||
|
||||
"""
|
||||
if project:
|
||||
project_settings = get_project_settings(project)
|
||||
rr_path = (
|
||||
project_settings
|
||||
["royalrender"]
|
||||
["rr_paths"]
|
||||
)
|
||||
else:
|
||||
rr_path = (
|
||||
self._settings
|
||||
["modules"]
|
||||
["royalrender"]
|
||||
["rr_path"]
|
||||
["default"]
|
||||
)
|
||||
os.environ["RR_ROOT"] = rr_path
|
||||
self._rr_path = rr_path
|
||||
|
||||
def _get_rr_bin_path(self, rr_root=None):
|
||||
# type: (str) -> str
|
||||
"""Get path to RR bin folder."""
|
||||
rr_root = rr_root or self._rr_path
|
||||
is_64bit_python = sys.maxsize > 2 ** 32
|
||||
|
||||
rr_bin_path = ""
|
||||
if sys.platform.lower() == "win32":
|
||||
rr_bin_path = "/bin/win64"
|
||||
if not is_64bit_python:
|
||||
# we are using 64bit python
|
||||
rr_bin_path = "/bin/win"
|
||||
rr_bin_path = rr_bin_path.replace(
|
||||
"/", os.path.sep
|
||||
)
|
||||
|
||||
if sys.platform.lower() == "darwin":
|
||||
rr_bin_path = "/bin/mac64"
|
||||
if not is_64bit_python:
|
||||
rr_bin_path = "/bin/mac"
|
||||
|
||||
if sys.platform.lower() == "linux":
|
||||
rr_bin_path = "/bin/lx64"
|
||||
|
||||
return os.path.join(rr_root, rr_bin_path)
|
||||
|
||||
def _initialize_module_path(self):
|
||||
# type: () -> None
|
||||
"""Set RR modules for Python."""
|
||||
# default for linux
|
||||
rr_bin = self._get_rr_bin_path()
|
||||
rr_module_path = os.path.join(rr_bin, "lx64/lib")
|
||||
|
||||
if sys.platform.lower() == "win32":
|
||||
rr_module_path = rr_bin
|
||||
rr_module_path = rr_module_path.replace(
|
||||
"/", os.path.sep
|
||||
)
|
||||
|
||||
if sys.platform.lower() == "darwin":
|
||||
rr_module_path = os.path.join(rr_bin, "lib/python/27")
|
||||
|
||||
sys.path.append(os.path.join(self._rr_path, rr_module_path))
|
||||
|
||||
def create_submission(self, jobs, submitter_attributes, file_name=None):
|
||||
# type: (list[RRJob], list[SubmitterParameter], str) -> SubmitFile
|
||||
"""Create jobs submission file.
|
||||
|
||||
Args:
|
||||
jobs (list): List of :class:`RRJob`
|
||||
submitter_attributes (list): List of submitter attributes
|
||||
:class:`SubmitterParameter` for whole submission batch.
|
||||
file_name (str), optional): File path to write data to.
|
||||
|
||||
Returns:
|
||||
str: XML data of job submission files.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def submit_file(self, file, mode=RR_SUBMIT_CONSOLE):
|
||||
# type: (SubmitFile, int) -> None
|
||||
if mode == self.RR_SUBMIT_CONSOLE:
|
||||
self._submit_using_console(file)
|
||||
|
||||
# RR v7 supports only Python 2.7 so we bail out in fear
|
||||
# until there is support for Python 3 😰
|
||||
raise NotImplementedError(
|
||||
"Submission via RoyalRender API is not supported yet")
|
||||
# self._submit_using_api(file)
|
||||
|
||||
def _submit_using_console(self, file):
|
||||
# type: (SubmitFile) -> bool
|
||||
rr_console = os.path.join(
|
||||
self._get_rr_bin_path(),
|
||||
"rrSubmitterconsole"
|
||||
)
|
||||
|
||||
if sys.platform.lower() == "darwin":
|
||||
if "/bin/mac64" in rr_console:
|
||||
rr_console = rr_console.replace("/bin/mac64", "/bin/mac")
|
||||
|
||||
if sys.platform.lower() == "win32":
|
||||
if "/bin/win64" in rr_console:
|
||||
rr_console = rr_console.replace("/bin/win64", "/bin/win")
|
||||
rr_console += ".exe"
|
||||
|
||||
args = [rr_console, file]
|
||||
run_subprocess(" ".join(args), logger=log)
|
||||
|
||||
def _submit_using_api(self, file):
|
||||
# type: (SubmitFile) -> None
|
||||
"""Use RR API to submit jobs.
|
||||
|
||||
Args:
|
||||
file (SubmitFile): Submit jobs definition.
|
||||
|
||||
Throws:
|
||||
RoyalRenderException: When something fails.
|
||||
|
||||
"""
|
||||
self._initialize_module_path()
|
||||
import libpyRR2 as rrLib # noqa
|
||||
from rrJob import getClass_JobBasics # noqa
|
||||
import libpyRR2 as _RenderAppBasic # noqa
|
||||
|
||||
tcp = rrLib._rrTCP("") # noqa
|
||||
rr_server = tcp.getRRServer()
|
||||
|
||||
if len(rr_server) == 0:
|
||||
log.info("Got RR IP address {}".format(rr_server))
|
||||
|
||||
# TODO: Port is hardcoded in RR? If not, move it to Settings
|
||||
if not tcp.setServer(rr_server, 7773):
|
||||
log.error(
|
||||
"Can not set RR server: {}".format(tcp.errorMessage()))
|
||||
raise RoyalRenderException(tcp.errorMessage())
|
||||
|
||||
# TODO: This need UI and better handling of username/password.
|
||||
# We can't store password in keychain as it is pulled multiple
|
||||
# times and users on linux must enter keychain password every time.
|
||||
# Probably best way until we setup our own user management would be
|
||||
# to encrypt password and save it to json locally. Not bulletproof
|
||||
# but at least it is not stored in plaintext.
|
||||
reg = OpenPypeSettingsRegistry()
|
||||
try:
|
||||
rr_user = reg.get_item("rr_username")
|
||||
rr_password = reg.get_item("rr_password")
|
||||
except ValueError:
|
||||
# user has no rr credentials set
|
||||
pass
|
||||
else:
|
||||
# login to RR
|
||||
tcp.setLogin(rr_user, rr_password)
|
||||
|
||||
job = getClass_JobBasics()
|
||||
renderer = _RenderAppBasic()
|
||||
|
||||
# iterate over SubmitFile, set _JobBasic (job) and renderer
|
||||
# and feed it to jobSubmitNew()
|
||||
# not implemented yet
|
||||
job.renderer = renderer
|
||||
tcp.jobSubmitNew(job)
|
||||
|
||||
|
||||
class RoyalRenderException(Exception):
|
||||
"""Exception used in various error states coming from RR."""
|
||||
pass
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect default Deadline server."""
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectDefaultRRPath(pyblish.api.ContextPlugin):
|
||||
"""Collect default Royal Render path."""
|
||||
|
||||
order = pyblish.api.CollectorOrder + 0.01
|
||||
label = "Default Royal Render Path"
|
||||
|
||||
def process(self, context):
|
||||
try:
|
||||
rr_module = context.data.get(
|
||||
"openPypeModules")["royalrender"]
|
||||
except AttributeError:
|
||||
msg = "Cannot get OpenPype Royal Render module."
|
||||
self.log.error(msg)
|
||||
raise AssertionError(msg)
|
||||
|
||||
# get default deadline webservice url from deadline module
|
||||
self.log.debug(rr_module.rr_paths)
|
||||
context.data["defaultRRPath"] = rr_module.rr_paths["default"] # noqa: E501
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class CollectRRPathFromInstance(pyblish.api.InstancePlugin):
|
||||
"""Collect RR Path from instance."""
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
label = "Royal Render Path from the Instance"
|
||||
families = ["rendering"]
|
||||
|
||||
def process(self, instance):
|
||||
instance.data["rrPath"] = self._collect_rr_path(instance)
|
||||
self.log.info(
|
||||
"Using {} for submission.".format(instance.data["rrPath"]))
|
||||
|
||||
@staticmethod
|
||||
def _collect_rr_path(render_instance):
|
||||
# type: (pyblish.api.Instance) -> str
|
||||
"""Get Royal Render path from render instance."""
|
||||
rr_settings = (
|
||||
render_instance.context.data
|
||||
["system_settings"]
|
||||
["modules"]
|
||||
["royalrender"]
|
||||
)
|
||||
try:
|
||||
default_servers = rr_settings["rr_paths"]
|
||||
project_servers = (
|
||||
render_instance.context.data
|
||||
["project_settings"]
|
||||
["royalrender"]
|
||||
["rr_paths"]
|
||||
)
|
||||
rr_servers = {
|
||||
k: default_servers[k]
|
||||
for k in project_servers
|
||||
if k in default_servers
|
||||
}
|
||||
|
||||
except AttributeError:
|
||||
# Handle situation were we had only one url for deadline.
|
||||
return render_instance.context.data["defaultRRPath"]
|
||||
|
||||
return rr_servers[
|
||||
list(rr_servers.keys())[
|
||||
int(render_instance.data.get("rrPaths"))
|
||||
]
|
||||
]
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect sequences from Royal Render Job."""
|
||||
import os
|
||||
import re
|
||||
import copy
|
||||
import json
|
||||
from pprint import pformat
|
||||
|
||||
import pyblish.api
|
||||
from avalon import api
|
||||
|
||||
|
||||
def collect(root,
|
||||
regex=None,
|
||||
exclude_regex=None,
|
||||
frame_start=None,
|
||||
frame_end=None):
|
||||
"""Collect sequence collections in root"""
|
||||
|
||||
from avalon.vendor import clique
|
||||
|
||||
files = []
|
||||
for filename in os.listdir(root):
|
||||
|
||||
# Must have extension
|
||||
ext = os.path.splitext(filename)[1]
|
||||
if not ext:
|
||||
continue
|
||||
|
||||
# Only files
|
||||
if not os.path.isfile(os.path.join(root, filename)):
|
||||
continue
|
||||
|
||||
# Include and exclude regex
|
||||
if regex and not re.search(regex, filename):
|
||||
continue
|
||||
if exclude_regex and re.search(exclude_regex, filename):
|
||||
continue
|
||||
|
||||
files.append(filename)
|
||||
|
||||
# Match collections
|
||||
# Support filenames like: projectX_shot01_0010.tiff with this regex
|
||||
pattern = r"(?P<index>(?P<padding>0*)\d+)\.\D+\d?$"
|
||||
collections, remainder = clique.assemble(files,
|
||||
patterns=[pattern],
|
||||
minimum_items=1)
|
||||
|
||||
# Ignore any remainders
|
||||
if remainder:
|
||||
print("Skipping remainder {}".format(remainder))
|
||||
|
||||
# Exclude any frames outside start and end frame.
|
||||
for collection in collections:
|
||||
for index in list(collection.indexes):
|
||||
if frame_start is not None and index < frame_start:
|
||||
collection.indexes.discard(index)
|
||||
continue
|
||||
if frame_end is not None and index > frame_end:
|
||||
collection.indexes.discard(index)
|
||||
continue
|
||||
|
||||
# Keep only collections that have at least a single frame
|
||||
collections = [c for c in collections if c.indexes]
|
||||
|
||||
return collections
|
||||
|
||||
|
||||
class CollectSequencesFromJob(pyblish.api.ContextPlugin):
|
||||
"""Gather file sequences from job directory.
|
||||
|
||||
When "OPENPYPE_PUBLISH_DATA" environment variable is set these paths
|
||||
(folders or .json files) are parsed for image sequences. Otherwise the
|
||||
current working directory is searched for file sequences.
|
||||
|
||||
"""
|
||||
order = pyblish.api.CollectorOrder
|
||||
targets = ["rr_control"]
|
||||
label = "Collect Rendered Frames"
|
||||
|
||||
def process(self, context):
|
||||
if os.environ.get("OPENPYPE_PUBLISH_DATA"):
|
||||
self.log.debug(os.environ.get("OPENPYPE_PUBLISH_DATA"))
|
||||
paths = os.environ["OPENPYPE_PUBLISH_DATA"].split(os.pathsep)
|
||||
self.log.info("Collecting paths: {}".format(paths))
|
||||
else:
|
||||
cwd = context.get("workspaceDir", os.getcwd())
|
||||
paths = [cwd]
|
||||
|
||||
for path in paths:
|
||||
|
||||
self.log.info("Loading: {}".format(path))
|
||||
|
||||
if path.endswith(".json"):
|
||||
# Search using .json configuration
|
||||
with open(path, "r") as f:
|
||||
try:
|
||||
data = json.load(f)
|
||||
except Exception as exc:
|
||||
self.log.error("Error loading json: "
|
||||
"{} - Exception: {}".format(path, exc))
|
||||
raise
|
||||
|
||||
cwd = os.path.dirname(path)
|
||||
root_override = data.get("root")
|
||||
if root_override:
|
||||
if os.path.isabs(root_override):
|
||||
root = root_override
|
||||
else:
|
||||
root = os.path.join(cwd, root_override)
|
||||
else:
|
||||
root = cwd
|
||||
|
||||
metadata = data.get("metadata")
|
||||
if metadata:
|
||||
session = metadata.get("session")
|
||||
if session:
|
||||
self.log.info("setting session using metadata")
|
||||
api.Session.update(session)
|
||||
os.environ.update(session)
|
||||
|
||||
else:
|
||||
# Search in directory
|
||||
data = {}
|
||||
root = path
|
||||
|
||||
self.log.info("Collecting: {}".format(root))
|
||||
regex = data.get("regex")
|
||||
if regex:
|
||||
self.log.info("Using regex: {}".format(regex))
|
||||
|
||||
collections = collect(root=root,
|
||||
regex=regex,
|
||||
exclude_regex=data.get("exclude_regex"),
|
||||
frame_start=data.get("frameStart"),
|
||||
frame_end=data.get("frameEnd"))
|
||||
|
||||
self.log.info("Found collections: {}".format(collections))
|
||||
|
||||
if data.get("subset") and len(collections) > 1:
|
||||
self.log.error("Forced subset can only work with a single "
|
||||
"found sequence")
|
||||
raise RuntimeError("Invalid sequence")
|
||||
|
||||
fps = data.get("fps", 25)
|
||||
|
||||
# Get family from the data
|
||||
families = data.get("families", ["render"])
|
||||
if "render" not in families:
|
||||
families.append("render")
|
||||
if "ftrack" not in families:
|
||||
families.append("ftrack")
|
||||
if "review" not in families:
|
||||
families.append("review")
|
||||
|
||||
for collection in collections:
|
||||
instance = context.create_instance(str(collection))
|
||||
self.log.info("Collection: %s" % list(collection))
|
||||
|
||||
# Ensure each instance gets a unique reference to the data
|
||||
data = copy.deepcopy(data)
|
||||
|
||||
# If no subset provided, get it from collection's head
|
||||
subset = data.get("subset", collection.head.rstrip("_. "))
|
||||
|
||||
# If no start or end frame provided, get it from collection
|
||||
indices = list(collection.indexes)
|
||||
start = data.get("frameStart", indices[0])
|
||||
end = data.get("frameEnd", indices[-1])
|
||||
|
||||
# root = os.path.normpath(root)
|
||||
# self.log.info("Source: {}}".format(data.get("source", "")))
|
||||
|
||||
ext = list(collection)[0].split('.')[-1]
|
||||
|
||||
instance.data.update({
|
||||
"name": str(collection),
|
||||
"family": families[0], # backwards compatibility / pyblish
|
||||
"families": list(families),
|
||||
"subset": subset,
|
||||
"asset": data.get("asset", api.Session["AVALON_ASSET"]),
|
||||
"stagingDir": root,
|
||||
"frameStart": start,
|
||||
"frameEnd": end,
|
||||
"fps": fps,
|
||||
"source": data.get('source', '')
|
||||
})
|
||||
instance.append(collection)
|
||||
instance.context.data['fps'] = fps
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
'name': ext,
|
||||
'ext': '{}'.format(ext),
|
||||
'files': list(collection),
|
||||
"stagingDir": root,
|
||||
"anatomy_template": "render",
|
||||
"fps": fps,
|
||||
"tags": ['review']
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
|
||||
if data.get('user'):
|
||||
context.data["user"] = data['user']
|
||||
|
||||
self.log.debug("Collected instance:\n"
|
||||
"{}".format(pformat(instance.data)))
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Module providing support for Royal Render."""
|
||||
import os
|
||||
import openpype.modules
|
||||
from openpype.modules import OpenPypeModule
|
||||
from openpype_interfaces import IPluginPaths
|
||||
|
||||
|
||||
class RoyalRenderModule(OpenPypeModule, IPluginPaths):
|
||||
"""Class providing basic Royal Render implementation logic."""
|
||||
name = "royalrender"
|
||||
|
||||
@property
|
||||
def api(self):
|
||||
if not self._api:
|
||||
# import royal render modules
|
||||
from . import api as rr_api
|
||||
self._api = rr_api.Api(self.settings)
|
||||
|
||||
return self._api
|
||||
|
||||
def __init__(self, manager, settings):
|
||||
# type: (openpype.modules.base.ModulesManager, dict) -> None
|
||||
self.rr_paths = {}
|
||||
self._api = None
|
||||
self.settings = settings
|
||||
super(RoyalRenderModule, self).__init__(manager, settings)
|
||||
|
||||
def initialize(self, module_settings):
|
||||
# type: (dict) -> None
|
||||
rr_settings = module_settings[self.name]
|
||||
self.enabled = rr_settings["enabled"]
|
||||
self.rr_paths = rr_settings.get("rr_paths")
|
||||
|
||||
@staticmethod
|
||||
def get_plugin_paths():
|
||||
# type: () -> dict
|
||||
"""Royal Render plugin paths.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary of plugin paths for RR.
|
||||
"""
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
return {
|
||||
"publish": [os.path.join(current_dir, "plugins", "publish")]
|
||||
}
|
||||
256
openpype/modules/default_modules/royal_render/rr_job.py
Normal file
256
openpype/modules/default_modules/royal_render/rr_job.py
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Python wrapper for RoyalRender XML job file."""
|
||||
from xml.dom import minidom as md
|
||||
import attr
|
||||
from collections import namedtuple, OrderedDict
|
||||
|
||||
|
||||
CustomAttribute = namedtuple("CustomAttribute", ["name", "value"])
|
||||
|
||||
|
||||
@attr.s
|
||||
class RRJob:
|
||||
"""Mapping of Royal Render job file to a data class."""
|
||||
|
||||
# Required
|
||||
# --------
|
||||
|
||||
# Name of your render application. Same as in the render config file.
|
||||
# (Maya, Softimage)
|
||||
Software = attr.ib() # type: str
|
||||
|
||||
# The OS the scene was created on, all texture paths are set on
|
||||
# that OS. Possible values are windows, linux, osx
|
||||
SceneOS = attr.ib() # type: str
|
||||
|
||||
# Renderer you use. Same as in the render config file
|
||||
# (VRay, Mental Ray, Arnold)
|
||||
Renderer = attr.ib() # type: str
|
||||
|
||||
# Version you want to render with. (5.11, 2010, 12)
|
||||
Version = attr.ib() # type: str
|
||||
|
||||
# Name of the scene file with full path.
|
||||
SceneName = attr.ib() # type: str
|
||||
|
||||
# Is the job enabled for submission?
|
||||
# enabled by default
|
||||
IsActive = attr.ib() # type: str
|
||||
|
||||
# Sequence settings of this job
|
||||
SeqStart = attr.ib() # type: int
|
||||
SeqEnd = attr.ib() # type: int
|
||||
SeqStep = attr.ib() # type: int
|
||||
SeqFileOffset = attr.ib() # type: int
|
||||
|
||||
# If you specify ImageDir, then ImageFilename has no path. If you do
|
||||
# NOT specify ImageDir, then ImageFilename has to include the path.
|
||||
# Same for ImageExtension.
|
||||
# Important: Do not forget any _ or . in front or after the frame
|
||||
# numbering. Usually ImageExtension always starts with a . (.tga, .exr)
|
||||
ImageDir = attr.ib() # type: str
|
||||
ImageFilename = attr.ib() # type: str
|
||||
ImageExtension = attr.ib() # type: str
|
||||
|
||||
# Some applications always add a . or _ in front of the frame number.
|
||||
# Set this variable to that character. The user can then change
|
||||
# the filename at the rrSubmitter and the submitter keeps
|
||||
# track of this character.
|
||||
ImagePreNumberLetter = attr.ib() # type: str
|
||||
|
||||
# If you render a single file, e.g. Quicktime or Avi, then you have to
|
||||
# set this value. Videos have to be rendered at once on one client.
|
||||
ImageSingleOutputFile = attr.ib(default="false") # type: str
|
||||
|
||||
# Semi-Required (required for some render applications)
|
||||
# -----------------------------------------------------
|
||||
|
||||
# The database of your scene file. In Maya and XSI called "project",
|
||||
# in Lightwave "content dir"
|
||||
SceneDatabaseDir = attr.ib(default=None) # type: str
|
||||
|
||||
# Required if you want to split frames on multiple clients
|
||||
ImageWidth = attr.ib(default=None) # type: int
|
||||
ImageHeight = attr.ib(default=None) # type: int
|
||||
Camera = attr.ib(default=None) # type: str
|
||||
Layer = attr.ib(default=None) # type: str
|
||||
Channel = attr.ib(default=None) # type: str
|
||||
|
||||
# Optional
|
||||
# --------
|
||||
|
||||
# Used for the RR render license function.
|
||||
# E.g. If you render with mentalRay, then add mentalRay. If you render
|
||||
# with Nuke and you use Furnace plugins in your comp, add Furnace.
|
||||
# TODO: determine how this work for multiple plugins
|
||||
RequiredPlugins = attr.ib(default=None) # type: str
|
||||
|
||||
# Frame Padding of the frame number in the rendered filename.
|
||||
# Some render config files are setting the padding at render time.
|
||||
ImageFramePadding = attr.ib(default=None) # type: str
|
||||
|
||||
# Some render applications support overriding the image format at
|
||||
# the render commandline.
|
||||
OverrideImageFormat = attr.ib(default=None) # type: str
|
||||
|
||||
# rrControl can display the name of additonal channels that are
|
||||
# rendered. Each channel requires these two values. ChannelFilename
|
||||
# contains the full path.
|
||||
ChannelFilename = attr.ib(default=None) # type: str
|
||||
ChannelExtension = attr.ib(default=None) # type: str
|
||||
|
||||
# A value between 0 and 255. Each job gets the Pre ID attached as small
|
||||
# letter to the main ID. A new main ID is generated for every machine
|
||||
# for every 5/1000s.
|
||||
PreID = attr.ib(default=None) # type: int
|
||||
|
||||
# When the job is received by the server, the server checks for other
|
||||
# jobs send from this machine. If a job with the PreID was found, then
|
||||
# this jobs waits for the other job. Note: This flag can be used multiple
|
||||
# times to wait for multiple jobs.
|
||||
WaitForPreID = attr.ib(default=None) # type: int
|
||||
|
||||
# List of submitter options per job
|
||||
# list item must be of `SubmitterParameter` type
|
||||
SubmitterParameters = attr.ib(factory=list) # type: list
|
||||
|
||||
# List of Custom job attributes
|
||||
# Royal Render support custom attributes in format <CustomFoo> or
|
||||
# <CustomSomeOtherAttr>
|
||||
# list item must be of `CustomAttribute` named tuple
|
||||
CustomAttributes = attr.ib(factory=list) # type: list
|
||||
|
||||
# Additional information for subsequent publish script and
|
||||
# for better display in rrControl
|
||||
UserName = attr.ib(default=None) # type: str
|
||||
CustomSeQName = attr.ib(default=None) # type: str
|
||||
CustomSHotName = attr.ib(default=None) # type: str
|
||||
CustomVersionName = attr.ib(default=None) # type: str
|
||||
CustomUserInfo = attr.ib(default=None) # type: str
|
||||
SubmitMachine = attr.ib(default=None) # type: str
|
||||
Color_ID = attr.ib(default=2) # type: int
|
||||
|
||||
RequiredLicenses = attr.ib(default=None) # type: str
|
||||
|
||||
# Additional frame info
|
||||
Priority = attr.ib(default=50) # type: int
|
||||
TotalFrames = attr.ib(default=None) # type: int
|
||||
Tiled = attr.ib(default=None) # type: str
|
||||
|
||||
|
||||
class SubmitterParameter:
|
||||
"""Wrapper for Submitter Parameters."""
|
||||
def __init__(self, parameter, *args):
|
||||
# type: (str, list) -> None
|
||||
self._parameter = parameter
|
||||
self._values = args
|
||||
|
||||
def serialize(self):
|
||||
# type: () -> str
|
||||
"""Serialize submitter parameter as a string value.
|
||||
|
||||
This can be later on used as text node in job xml file.
|
||||
|
||||
Returns:
|
||||
str: concatenated string of parameter values.
|
||||
|
||||
"""
|
||||
return '"{param}={val}"'.format(
|
||||
param=self._parameter, val="~".join(self._values))
|
||||
|
||||
|
||||
@attr.s
|
||||
class SubmitFile:
|
||||
"""Class wrapping Royal Render submission XML file."""
|
||||
|
||||
# Syntax version of the submission file.
|
||||
syntax_version = attr.ib(default="6.0") # type: str
|
||||
|
||||
# Delete submission file after processing
|
||||
DeleteXML = attr.ib(default=1) # type: int
|
||||
|
||||
# List of submitter options per job
|
||||
# list item must be of `SubmitterParameter` type
|
||||
SubmitterParameters = attr.ib(factory=list) # type: list
|
||||
|
||||
# List of job is submission batch.
|
||||
# list item must be of type `RRJob`
|
||||
Jobs = attr.ib(factory=list) # type: list
|
||||
|
||||
@staticmethod
|
||||
def _process_submitter_parameters(parameters, dom, append_to):
|
||||
# type: (list[SubmitterParameter], md.Document, md.Element) -> None
|
||||
"""Take list of :class:`SubmitterParameter` and process it as XML.
|
||||
|
||||
This will take :class:`SubmitterParameter`, create XML element
|
||||
for them and convert value to Royal Render compatible string
|
||||
(options and values separated by ~)
|
||||
|
||||
Args:
|
||||
parameters (list of SubmitterParameter): List of parameters.
|
||||
dom (xml.dom.minidom.Document): XML Document
|
||||
append_to (xml.dom.minidom.Element): Element to append to.
|
||||
|
||||
"""
|
||||
for param in parameters:
|
||||
if not isinstance(param, SubmitterParameter):
|
||||
raise AttributeError(
|
||||
"{} is not of type `SubmitterParameter`".format(param))
|
||||
xml_parameter = dom.createElement("SubmitterParameter")
|
||||
xml_parameter.appendChild(dom.createTextNode(param.serialize()))
|
||||
append_to.appendChild(xml_parameter)
|
||||
|
||||
def serialize(self):
|
||||
# type: () -> str
|
||||
"""Return all data serialized as XML.
|
||||
|
||||
Returns:
|
||||
str: XML data as string.
|
||||
|
||||
"""
|
||||
def filter_data(a, v):
|
||||
"""Skip private attributes."""
|
||||
if a.name.startswith("_"):
|
||||
return False
|
||||
if v is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
root = md.Document()
|
||||
# root element: <RR_Job_File syntax_version="6.0">
|
||||
job_file = root.createElement('RR_Job_File')
|
||||
job_file.setAttribute("syntax_version", self.syntax_version)
|
||||
|
||||
# handle Submitter Parameters for batch
|
||||
# <SubmitterParameter>foo=bar~baz~goo</SubmitterParameter>
|
||||
self._process_submitter_parameters(
|
||||
self.SubmitterParameters, root, job_file)
|
||||
|
||||
for job in self.Jobs: # type: RRJob
|
||||
if not isinstance(job, RRJob):
|
||||
raise AttributeError(
|
||||
"{} is not of type `SubmitterParameter`".format(job))
|
||||
xml_job = root.createElement("Job")
|
||||
# handle Submitter Parameters for job
|
||||
self._process_submitter_parameters(
|
||||
job.SubmitterParameters, root, xml_job
|
||||
)
|
||||
job_custom_attributes = job.CustomAttributes
|
||||
|
||||
serialized_job = attr.asdict(
|
||||
job, dict_factory=OrderedDict, filter=filter_data)
|
||||
serialized_job.pop("CustomAttributes")
|
||||
serialized_job.pop("SubmitterParameters")
|
||||
|
||||
for custom_attr in job_custom_attributes: # type: CustomAttribute
|
||||
serialized_job["Custom{}".format(
|
||||
custom_attr.name)] = custom_attr.value
|
||||
|
||||
for item, value in serialized_job.items():
|
||||
xml_attr = root.create(item)
|
||||
xml_attr.appendChild(
|
||||
root.createTextNode(value)
|
||||
)
|
||||
xml_job.appendChild(xml_attr)
|
||||
|
||||
return root.toprettyxml(indent="\t")
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
## OpenPype RoyalRender integration plugins
|
||||
|
||||
### Installation
|
||||
|
||||
Copy content of this folder to your `RR_ROOT` (place where RoyalRender studio wide installation is).
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""This is RR control plugin that runs on the job by user interaction.
|
||||
|
||||
It asks user for context to publish, getting it from OpenPype. In order to
|
||||
run it needs `OPENPYPE_ROOT` to be set to know where to execute OpenPype.
|
||||
|
||||
"""
|
||||
import rr # noqa
|
||||
import rrGlobal # noqa
|
||||
import subprocess
|
||||
import os
|
||||
import glob
|
||||
import platform
|
||||
import tempfile
|
||||
import json
|
||||
|
||||
|
||||
class OpenPypeContextSelector:
|
||||
"""Class to handle publishing context determination in RR."""
|
||||
|
||||
def __init__(self):
|
||||
self.job = rr.getJob()
|
||||
self.context = None
|
||||
|
||||
self.openpype_executable = "openpype_gui"
|
||||
if platform.system().lower() == "windows":
|
||||
self.openpype_executable = "{}.exe".format(
|
||||
self.openpype_executable)
|
||||
|
||||
op_path = os.environ.get("OPENPYPE_ROOT")
|
||||
print("initializing ... {}".format(op_path))
|
||||
if not op_path:
|
||||
print("Warning: OpenPype root is not found.")
|
||||
|
||||
if platform.system().lower() == "windows":
|
||||
print(" * trying to find OpenPype on local computer.")
|
||||
op_path = os.path.join(
|
||||
os.environ.get("PROGRAMFILES"),
|
||||
"OpenPype", "openpype_console.exe"
|
||||
)
|
||||
if os.path.exists(op_path):
|
||||
print(" - found OpenPype installation {}".format(op_path))
|
||||
else:
|
||||
# try to find in user local context
|
||||
op_path = os.path.join(
|
||||
os.environ.get("LOCALAPPDATA"),
|
||||
"Programs",
|
||||
"OpenPype", "openpype_console.exe"
|
||||
)
|
||||
if os.path.exists(op_path):
|
||||
print(
|
||||
" - found OpenPype installation {}".format(
|
||||
op_path))
|
||||
else:
|
||||
raise Exception("Error: OpenPype was not found.")
|
||||
|
||||
self.openpype_root = op_path
|
||||
|
||||
# TODO: this should try to find metadata file. Either using
|
||||
# jobs custom attributes or using environment variable
|
||||
# or just using plain existence of file.
|
||||
# self.context = self._process_metadata_file()
|
||||
|
||||
def _process_metadata_file(self):
|
||||
"""Find and process metadata file.
|
||||
|
||||
Try to find metadata json file in job folder to get context from.
|
||||
|
||||
Returns:
|
||||
dict: Context from metadata json file.
|
||||
|
||||
"""
|
||||
image_dir = self.job.imageDir
|
||||
metadata_files = glob.glob(
|
||||
"{}{}*_metadata.json".format(image_dir, os.path.sep))
|
||||
if not metadata_files:
|
||||
return {}
|
||||
|
||||
raise NotImplementedError(
|
||||
"Processing existing metadata not implemented yet.")
|
||||
|
||||
def process_job(self):
|
||||
"""Process selected job.
|
||||
|
||||
This should process selected job. If context can be determined
|
||||
automatically, no UI will be show and publishing will directly
|
||||
proceed.
|
||||
"""
|
||||
if not self.context:
|
||||
self.show()
|
||||
|
||||
self.context["user"] = self.job.userName
|
||||
self.run_publish()
|
||||
|
||||
def show(self):
|
||||
"""Show UI for context selection.
|
||||
|
||||
Because of RR UI limitations, this must be done using OpenPype
|
||||
itself.
|
||||
|
||||
"""
|
||||
tf = tempfile.TemporaryFile(delete=False)
|
||||
context_file = tf.name
|
||||
op_args = [os.path.join(self.openpype_root, self.openpype_executable),
|
||||
"contextselection", tf.name]
|
||||
|
||||
tf.close()
|
||||
print(">>> running {}".format(" ".join(op_args)))
|
||||
|
||||
subprocess.call(op_args)
|
||||
|
||||
with open(context_file, "r") as cf:
|
||||
self.context = json.load(cf)
|
||||
|
||||
os.unlink(context_file)
|
||||
print(">>> context: {}".format(self.context))
|
||||
|
||||
if not self.context or \
|
||||
not self.context.get("project") or \
|
||||
not self.context.get("asset") or \
|
||||
not self.context.get("task"):
|
||||
self._show_rr_warning("Context selection failed.")
|
||||
return
|
||||
|
||||
# self.context["app_name"] = self.job.renderer.name
|
||||
self.context["app_name"] = "maya/2020"
|
||||
|
||||
@staticmethod
|
||||
def _show_rr_warning(text):
|
||||
warning_dialog = rrGlobal.getGenericUI()
|
||||
warning_dialog.addItem(rrGlobal.genUIType.label, "infoLabel", "")
|
||||
warning_dialog.setText("infoLabel", text)
|
||||
warning_dialog.addItem(
|
||||
rrGlobal.genUIType.layoutH, "btnLayout", "")
|
||||
warning_dialog.addItem(
|
||||
rrGlobal.genUIType.closeButton, "Ok", "btnLayout")
|
||||
warning_dialog.execute()
|
||||
del warning_dialog
|
||||
|
||||
def run_publish(self):
|
||||
"""Run publish process."""
|
||||
env = {'AVALON_PROJECT': str(self.context.get("project")),
|
||||
"AVALON_ASSET": str(self.context.get("asset")),
|
||||
"AVALON_TASK": str(self.context.get("task")),
|
||||
"AVALON_APP_NAME": str(self.context.get("app_name"))}
|
||||
|
||||
print(">>> setting environment:")
|
||||
for k, v in env.items():
|
||||
print(" {}: {}".format(k, v))
|
||||
|
||||
args = [os.path.join(self.openpype_root, self.openpype_executable),
|
||||
'publish', '-t', "rr_control", "--gui",
|
||||
os.path.join(self.job.imageDir,
|
||||
os.path.dirname(self.job.imageFileName))
|
||||
]
|
||||
|
||||
print(">>> running {}".format(" ".join(args)))
|
||||
orig = os.environ.copy()
|
||||
orig.update(env)
|
||||
try:
|
||||
subprocess.call(args, env=orig)
|
||||
except subprocess.CalledProcessError as e:
|
||||
self._show_rr_warning(" Publish failed [ {} ]".format(
|
||||
e.returncode
|
||||
))
|
||||
|
||||
|
||||
print("running selector")
|
||||
selector = OpenPypeContextSelector()
|
||||
selector.process_job()
|
||||
|
|
@ -79,7 +79,7 @@ class PypeCommands:
|
|||
standalonepublish.main()
|
||||
|
||||
@staticmethod
|
||||
def publish(paths, targets=None):
|
||||
def publish(paths, targets=None, gui=False):
|
||||
"""Start headless publishing.
|
||||
|
||||
Publish use json from passed paths argument.
|
||||
|
|
@ -88,20 +88,35 @@ class PypeCommands:
|
|||
paths (list): Paths to jsons.
|
||||
targets (string): What module should be targeted
|
||||
(to choose validator for example)
|
||||
gui (bool): Show publish UI.
|
||||
|
||||
Raises:
|
||||
RuntimeError: When there is no path to process.
|
||||
"""
|
||||
if not any(paths):
|
||||
raise RuntimeError("No publish paths specified")
|
||||
|
||||
from openpype.modules import ModulesManager
|
||||
from openpype import install, uninstall
|
||||
from openpype.api import Logger
|
||||
from openpype.tools.utils.host_tools import show_publish
|
||||
from openpype.tools.utils.lib import qt_app_context
|
||||
|
||||
# Register target and host
|
||||
import pyblish.api
|
||||
import pyblish.util
|
||||
|
||||
log = Logger.get_logger()
|
||||
|
||||
install()
|
||||
|
||||
manager = ModulesManager()
|
||||
|
||||
publish_paths = manager.collect_plugin_paths()["publish"]
|
||||
|
||||
for path in publish_paths:
|
||||
pyblish.api.register_plugin_path(path)
|
||||
|
||||
if not any(paths):
|
||||
raise RuntimeError("No publish paths specified")
|
||||
|
||||
env = get_app_environments_for_context(
|
||||
os.environ["AVALON_PROJECT"],
|
||||
os.environ["AVALON_ASSET"],
|
||||
|
|
@ -110,32 +125,39 @@ class PypeCommands:
|
|||
)
|
||||
os.environ.update(env)
|
||||
|
||||
log = Logger.get_logger()
|
||||
|
||||
install()
|
||||
|
||||
pyblish.api.register_target("filesequence")
|
||||
pyblish.api.register_host("shell")
|
||||
|
||||
if targets:
|
||||
for target in targets:
|
||||
print(f"setting target: {target}")
|
||||
pyblish.api.register_target(target)
|
||||
else:
|
||||
pyblish.api.register_target("filesequence")
|
||||
|
||||
os.environ["OPENPYPE_PUBLISH_DATA"] = os.pathsep.join(paths)
|
||||
|
||||
log.info("Running publish ...")
|
||||
|
||||
# Error exit as soon as any error occurs.
|
||||
error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}"
|
||||
plugins = pyblish.api.discover()
|
||||
print("Using plugins:")
|
||||
for plugin in plugins:
|
||||
print(plugin)
|
||||
|
||||
for result in pyblish.util.publish_iter():
|
||||
if result["error"]:
|
||||
log.error(error_format.format(**result))
|
||||
uninstall()
|
||||
sys.exit(1)
|
||||
if gui:
|
||||
with qt_app_context():
|
||||
show_publish()
|
||||
else:
|
||||
# Error exit as soon as any error occurs.
|
||||
error_format = ("Failed {plugin.__name__}: "
|
||||
"{error} -- {error.traceback}")
|
||||
|
||||
for result in pyblish.util.publish_iter():
|
||||
if result["error"]:
|
||||
log.error(error_format.format(**result))
|
||||
# uninstall()
|
||||
sys.exit(1)
|
||||
|
||||
log.info("Publish finished.")
|
||||
uninstall()
|
||||
|
||||
@staticmethod
|
||||
def remotepublishfromapp(project, batch_dir, host, user, targets=None):
|
||||
|
|
|
|||
|
|
@ -167,6 +167,16 @@
|
|||
"ffmpeg": 48
|
||||
}
|
||||
},
|
||||
"royalrender": {
|
||||
"enabled": false,
|
||||
"rr_paths": {
|
||||
"default": {
|
||||
"windows": "",
|
||||
"darwin": "",
|
||||
"linux": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"log_viewer": {
|
||||
"enabled": true
|
||||
},
|
||||
|
|
|
|||
|
|
@ -180,6 +180,31 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "royalrender",
|
||||
"label": "Royal Render",
|
||||
"require_restart": true,
|
||||
"collapsible": true,
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "dict-modifiable",
|
||||
"object_type": {
|
||||
"type": "path",
|
||||
"multiplatform": true
|
||||
},
|
||||
"key": "rr_paths",
|
||||
"required_keys": ["default"],
|
||||
"label": "Royal Render Root Paths"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "log_viewer",
|
||||
|
|
|
|||
1172
poetry.lock
generated
1172
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Test suite for User Settings."""
|
||||
# import pytest
|
||||
# from openpype.modules import ModulesManager
|
||||
|
||||
|
||||
def test_rr_job():
|
||||
# manager = ModulesManager()
|
||||
# rr_module = manager.modules_by_name["royalrender"]
|
||||
...
|
||||
Loading…
Add table
Add a link
Reference in a new issue