mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Merge branch 'develop' of https://github.com/ynput/ayon-core into bugfix/AY-5647_Enabled-toggle-is-ignored-in-RoyalRender
This commit is contained in:
commit
db012ea12f
23 changed files with 19 additions and 9 deletions
|
|
@ -0,0 +1,9 @@
|
|||
from .version import __version__
|
||||
from .addon import RoyalRenderAddon
|
||||
|
||||
|
||||
__all__ = (
|
||||
"__version__",
|
||||
|
||||
"RoyalRenderAddon",
|
||||
)
|
||||
33
server_addon/royalrender/client/ayon_royalrender/addon.py
Normal file
33
server_addon/royalrender/client/ayon_royalrender/addon.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Module providing support for Royal Render."""
|
||||
import os
|
||||
|
||||
from ayon_core.addon import AYONAddon, IPluginPaths
|
||||
|
||||
from .version import __version__
|
||||
|
||||
|
||||
class RoyalRenderAddon(AYONAddon, IPluginPaths):
|
||||
"""Class providing basic Royal Render implementation logic."""
|
||||
name = "royalrender"
|
||||
version = __version__
|
||||
|
||||
def initialize(self, studio_settings):
|
||||
# type: (dict) -> None
|
||||
self.enabled = False
|
||||
addon_settings = studio_settings.get(self.name)
|
||||
if addon_settings:
|
||||
self.enabled = addon_settings["enabled"]
|
||||
|
||||
@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")]
|
||||
}
|
||||
181
server_addon/royalrender/client/ayon_royalrender/api.py
Normal file
181
server_addon/royalrender/client/ayon_royalrender/api.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Wrapper around Royal Render API."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
from ayon_core.lib import Logger, run_subprocess, AYONSettingsRegistry
|
||||
from ayon_core.lib.vendor_bin_utils import find_tool_in_custom_paths
|
||||
|
||||
from .rr_job import SubmitFile
|
||||
from .rr_job import RRJob, SubmitterParameter # noqa F401
|
||||
|
||||
|
||||
class Api:
|
||||
|
||||
_settings = None
|
||||
RR_SUBMIT_CONSOLE = 1
|
||||
RR_SUBMIT_API = 2
|
||||
|
||||
def __init__(self, rr_path=None):
|
||||
self.log = Logger.get_logger("RoyalRender")
|
||||
self._rr_path = rr_path
|
||||
os.environ["RR_ROOT"] = rr_path
|
||||
|
||||
@staticmethod
|
||||
def get_rr_bin_path(rr_root, tool_name=None):
|
||||
# type: (str, str) -> str
|
||||
"""Get path to RR bin folder.
|
||||
|
||||
Args:
|
||||
tool_name (str): Name of RR executable you want.
|
||||
rr_root (str): Custom RR root if needed.
|
||||
|
||||
Returns:
|
||||
str: Path to the tool based on current platform.
|
||||
|
||||
"""
|
||||
is_64bit_python = sys.maxsize > 2 ** 32
|
||||
|
||||
rr_bin_parts = [rr_root, "bin"]
|
||||
if sys.platform.lower() == "win32":
|
||||
rr_bin_parts.append("win")
|
||||
|
||||
if sys.platform.lower() == "darwin":
|
||||
rr_bin_parts.append("mac")
|
||||
|
||||
if sys.platform.lower().startswith("linux"):
|
||||
rr_bin_parts.append("lx")
|
||||
|
||||
rr_bin_path = os.sep.join(rr_bin_parts)
|
||||
|
||||
paths_to_check = []
|
||||
# if we use 64bit python, append 64bit specific path first
|
||||
if is_64bit_python:
|
||||
if not tool_name:
|
||||
return rr_bin_path + "64"
|
||||
paths_to_check.append(rr_bin_path + "64")
|
||||
|
||||
# otherwise use 32bit
|
||||
if not tool_name:
|
||||
return rr_bin_path
|
||||
paths_to_check.append(rr_bin_path)
|
||||
|
||||
return find_tool_in_custom_paths(paths_to_check, tool_name)
|
||||
|
||||
def _initialize_module_path(self):
|
||||
# type: () -> None
|
||||
"""Set RR modules for Python."""
|
||||
# default for linux
|
||||
rr_bin = self.get_rr_bin_path(self._rr_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))
|
||||
|
||||
@staticmethod
|
||||
def create_submission(jobs, submitter_attributes):
|
||||
# type: (list[RRJob], list[SubmitterParameter]) -> 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.
|
||||
|
||||
Returns:
|
||||
str: XML data of job submission files.
|
||||
|
||||
"""
|
||||
return SubmitFile(SubmitterParameters=submitter_attributes, Jobs=jobs)
|
||||
|
||||
def submit_file(self, file, mode=RR_SUBMIT_CONSOLE):
|
||||
# type: (SubmitFile, int) -> None
|
||||
if mode == self.RR_SUBMIT_CONSOLE:
|
||||
self._submit_using_console(file)
|
||||
return
|
||||
|
||||
# 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, job_file):
|
||||
# type: (SubmitFile) -> None
|
||||
rr_start_local = self.get_rr_bin_path(
|
||||
self._rr_path, "rrStartLocal")
|
||||
|
||||
self.log.info("rr_console: {}".format(rr_start_local))
|
||||
|
||||
args = [rr_start_local, "rrSubmitterconsole", job_file]
|
||||
self.log.info("Executing: {}".format(" ".join(args)))
|
||||
env = os.environ
|
||||
env["RR_ROOT"] = self._rr_path
|
||||
run_subprocess(args, logger=self.log, env=env)
|
||||
|
||||
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:
|
||||
self.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):
|
||||
self.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 = AYONSettingsRegistry("rr_settings")
|
||||
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
|
||||
360
server_addon/royalrender/client/ayon_royalrender/lib.py
Normal file
360
server_addon/royalrender/client/ayon_royalrender/lib.py
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Submitting render job to RoyalRender."""
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import tempfile
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_core.lib import (
|
||||
BoolDef,
|
||||
NumberDef,
|
||||
is_running_from_build,
|
||||
is_in_tests,
|
||||
)
|
||||
from ayon_core.lib.execute import run_ayon_launcher_process
|
||||
from ayon_royalrender.api import Api as rrApi
|
||||
from ayon_royalrender.rr_job import (
|
||||
CustomAttribute,
|
||||
RRJob,
|
||||
RREnvList,
|
||||
get_rr_platform,
|
||||
)
|
||||
from ayon_core.pipeline import AYONPyblishPluginMixin
|
||||
from ayon_core.pipeline.publish import KnownPublishError
|
||||
from ayon_core.pipeline.publish.lib import get_published_workfile_instance
|
||||
|
||||
|
||||
class BaseCreateRoyalRenderJob(pyblish.api.InstancePlugin,
|
||||
AYONPyblishPluginMixin):
|
||||
"""Creates separate rendering job for Royal Render"""
|
||||
label = "Create Nuke Render job in RR"
|
||||
order = pyblish.api.IntegratorOrder + 0.1
|
||||
hosts = ["nuke"]
|
||||
families = ["render", "prerender"]
|
||||
targets = ["local"]
|
||||
optional = True
|
||||
|
||||
priority = 50
|
||||
chunk_size = 1
|
||||
concurrent_tasks = 1
|
||||
use_gpu = True
|
||||
use_published = True
|
||||
|
||||
@classmethod
|
||||
def get_attribute_defs(cls):
|
||||
return [
|
||||
NumberDef(
|
||||
"priority",
|
||||
label="Priority",
|
||||
default=cls.priority,
|
||||
decimals=0
|
||||
),
|
||||
NumberDef(
|
||||
"chunk",
|
||||
label="Frames Per Task",
|
||||
default=cls.chunk_size,
|
||||
decimals=0,
|
||||
minimum=1,
|
||||
maximum=1000
|
||||
),
|
||||
NumberDef(
|
||||
"concurrency",
|
||||
label="Concurrency",
|
||||
default=cls.concurrent_tasks,
|
||||
decimals=0,
|
||||
minimum=1,
|
||||
maximum=10
|
||||
),
|
||||
BoolDef(
|
||||
"use_gpu",
|
||||
default=cls.use_gpu,
|
||||
label="Use GPU"
|
||||
),
|
||||
BoolDef(
|
||||
"suspend_publish",
|
||||
default=False,
|
||||
label="Suspend publish"
|
||||
),
|
||||
BoolDef(
|
||||
"use_published",
|
||||
default=cls.use_published,
|
||||
label="Use published workfile"
|
||||
)
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._rr_root = None
|
||||
self.scene_path = None
|
||||
self.job = None
|
||||
self.submission_parameters = None
|
||||
self.rr_api = None
|
||||
|
||||
def process(self, instance):
|
||||
if not instance.data.get("farm"):
|
||||
self.log.info("Skipping local instance.")
|
||||
return
|
||||
|
||||
instance.data["attributeValues"] = self.get_attr_values_from_data(
|
||||
instance.data)
|
||||
|
||||
# add suspend_publish attributeValue to instance data
|
||||
instance.data["suspend_publish"] = instance.data["attributeValues"][
|
||||
"suspend_publish"]
|
||||
|
||||
context = instance.context
|
||||
|
||||
self._rr_root = instance.data.get("rr_root")
|
||||
if not self._rr_root:
|
||||
raise KnownPublishError(
|
||||
("Missing RoyalRender root. "
|
||||
"You need to configure RoyalRender module."))
|
||||
|
||||
self.rr_api = rrApi(self._rr_root)
|
||||
|
||||
self.scene_path = context.data["currentFile"]
|
||||
if self.use_published:
|
||||
published_workfile = get_published_workfile_instance(context)
|
||||
|
||||
# fallback if nothing was set
|
||||
if published_workfile is None:
|
||||
self.log.warning("Falling back to workfile")
|
||||
file_path = context.data["currentFile"]
|
||||
else:
|
||||
workfile_repre = published_workfile.data["representations"][0]
|
||||
file_path = workfile_repre["published_path"]
|
||||
|
||||
self.scene_path = file_path
|
||||
self.log.info(
|
||||
"Using published scene for render {}".format(self.scene_path)
|
||||
)
|
||||
|
||||
if not instance.data.get("expectedFiles"):
|
||||
instance.data["expectedFiles"] = []
|
||||
|
||||
if not instance.data.get("rrJobs"):
|
||||
instance.data["rrJobs"] = []
|
||||
|
||||
def get_job(self, instance, script_path, render_path, node_name):
|
||||
"""Get RR job based on current instance.
|
||||
|
||||
Args:
|
||||
script_path (str): Path to Nuke script.
|
||||
render_path (str): Output path.
|
||||
node_name (str): Name of the render node.
|
||||
|
||||
Returns:
|
||||
RRJob: RoyalRender Job instance.
|
||||
|
||||
"""
|
||||
start_frame = int(instance.data["frameStartHandle"])
|
||||
end_frame = int(instance.data["frameEndHandle"])
|
||||
|
||||
batch_name = os.path.basename(script_path)
|
||||
jobname = "%s - %s" % (batch_name, instance.name)
|
||||
if is_in_tests():
|
||||
batch_name += datetime.now().strftime("%d%m%Y%H%M%S")
|
||||
|
||||
render_dir = os.path.normpath(os.path.dirname(render_path))
|
||||
output_filename_0 = self.pad_file_name(render_path, str(start_frame))
|
||||
file_name, file_ext = os.path.splitext(
|
||||
os.path.basename(output_filename_0))
|
||||
|
||||
custom_attributes = []
|
||||
if is_running_from_build():
|
||||
custom_attributes = [
|
||||
CustomAttribute(
|
||||
name="OpenPypeVersion",
|
||||
value=os.environ.get("OPENPYPE_VERSION"))
|
||||
]
|
||||
|
||||
# this will append expected files to instance as needed.
|
||||
expected_files = self.expected_files(
|
||||
instance, render_path, start_frame, end_frame)
|
||||
instance.data["expectedFiles"].extend(expected_files)
|
||||
|
||||
job = RRJob(
|
||||
Software="",
|
||||
Renderer="",
|
||||
SeqStart=int(start_frame),
|
||||
SeqEnd=int(end_frame),
|
||||
SeqStep=int(instance.data.get("byFrameStep", 1)),
|
||||
SeqFileOffset=0,
|
||||
Version=0,
|
||||
SceneName=script_path,
|
||||
IsActive=True,
|
||||
ImageDir=render_dir.replace("\\", "/"),
|
||||
ImageFilename=file_name,
|
||||
ImageExtension=file_ext,
|
||||
ImagePreNumberLetter="",
|
||||
ImageSingleOutputFile=False,
|
||||
SceneOS=get_rr_platform(),
|
||||
Layer=node_name,
|
||||
SceneDatabaseDir=script_path,
|
||||
CustomSHotName=jobname,
|
||||
CompanyProjectName=instance.context.data["projectName"],
|
||||
ImageWidth=instance.data["resolutionWidth"],
|
||||
ImageHeight=instance.data["resolutionHeight"],
|
||||
CustomAttributes=custom_attributes
|
||||
)
|
||||
|
||||
return job
|
||||
|
||||
def update_job_with_host_specific(self, instance, job):
|
||||
"""Host specific mapping for RRJob"""
|
||||
raise NotImplementedError
|
||||
|
||||
def expected_files(self, instance, path, start_frame, end_frame):
|
||||
"""Get expected files.
|
||||
|
||||
This function generate expected files from provided
|
||||
path and start/end frames.
|
||||
|
||||
It was taken from Deadline module, but this should be
|
||||
probably handled better in collector to support more
|
||||
flexible scenarios.
|
||||
|
||||
Args:
|
||||
instance (Instance)
|
||||
path (str): Output path.
|
||||
start_frame (int): Start frame.
|
||||
end_frame (int): End frame.
|
||||
|
||||
Returns:
|
||||
list: List of expected files.
|
||||
|
||||
"""
|
||||
dir_name = os.path.dirname(path)
|
||||
file = os.path.basename(path)
|
||||
|
||||
expected_files = []
|
||||
|
||||
if "#" in file:
|
||||
pparts = file.split("#")
|
||||
padding = "%0{}d".format(len(pparts) - 1)
|
||||
file = pparts[0] + padding + pparts[-1]
|
||||
|
||||
if "%" not in file:
|
||||
expected_files.append(path)
|
||||
return expected_files
|
||||
|
||||
if instance.data.get("slate"):
|
||||
start_frame -= 1
|
||||
|
||||
expected_files.extend(
|
||||
os.path.join(dir_name, (file % i)).replace("\\", "/")
|
||||
for i in range(start_frame, (end_frame + 1))
|
||||
)
|
||||
return expected_files
|
||||
|
||||
def pad_file_name(self, path, first_frame):
|
||||
"""Return output file path with #### for padding.
|
||||
|
||||
RR requires the path to be formatted with # in place of numbers.
|
||||
For example `/path/to/render.####.png`
|
||||
|
||||
Args:
|
||||
path (str): path to rendered image
|
||||
first_frame (str): from representation to cleany replace with #
|
||||
padding
|
||||
|
||||
Returns:
|
||||
str
|
||||
|
||||
"""
|
||||
self.log.debug("pad_file_name path: `{}`".format(path))
|
||||
if "%" in path:
|
||||
search_results = re.search(r"(%0)(\d)(d.)", path).groups()
|
||||
self.log.debug("_ search_results: `{}`".format(search_results))
|
||||
return int(search_results[1])
|
||||
if "#" in path:
|
||||
self.log.debug("already padded: `{}`".format(path))
|
||||
return path
|
||||
|
||||
if first_frame:
|
||||
padding = len(first_frame)
|
||||
path = path.replace(first_frame, "#" * padding)
|
||||
|
||||
return path
|
||||
|
||||
def inject_environment(self, instance, job):
|
||||
# type: (pyblish.api.Instance, RRJob) -> RRJob
|
||||
"""Inject environment variables for RR submission.
|
||||
|
||||
This function mimics the behaviour of the Deadline
|
||||
integration. It is just temporary solution until proper
|
||||
runtime environment injection is implemented in RR.
|
||||
|
||||
Args:
|
||||
instance (pyblish.api.Instance): Publishing instance
|
||||
job (RRJob): RRJob instance to be injected.
|
||||
|
||||
Returns:
|
||||
RRJob: Injected RRJob instance.
|
||||
|
||||
Throws:
|
||||
RuntimeError: If any of the required env vars is missing.
|
||||
|
||||
"""
|
||||
|
||||
temp_file_name = "{}_{}.json".format(
|
||||
datetime.utcnow().strftime('%Y%m%d%H%M%S%f'),
|
||||
str(uuid.uuid1())
|
||||
)
|
||||
|
||||
export_url = os.path.join(tempfile.gettempdir(), temp_file_name)
|
||||
print(">>> Temporary path: {}".format(export_url))
|
||||
|
||||
anatomy_data = instance.context.data["anatomyData"]
|
||||
addons_manager = instance.context.data["ayonAddonsManager"]
|
||||
applications_addon = addons_manager.get_enabled_addon("applications")
|
||||
|
||||
folder_key = "folder"
|
||||
if applications_addon is None:
|
||||
# Use 'asset' when applications addon command is not used
|
||||
folder_key = "asset"
|
||||
|
||||
add_kwargs = {
|
||||
"project": anatomy_data["project"]["name"],
|
||||
folder_key: instance.context.data["folderPath"],
|
||||
"task": anatomy_data["task"]["name"],
|
||||
"app": instance.context.data.get("appName"),
|
||||
"envgroup": "farm"
|
||||
}
|
||||
|
||||
if not all(add_kwargs.values()):
|
||||
raise RuntimeError((
|
||||
"Missing required env vars: AYON_PROJECT_NAME, AYON_FOLDER_PATH,"
|
||||
" AYON_TASK_NAME, AYON_APP_NAME"
|
||||
))
|
||||
|
||||
args = ["--headless"]
|
||||
# Use applications addon to extract environments
|
||||
# NOTE this is for backwards compatibility, the global command
|
||||
# will be removed in future and only applications addon command
|
||||
# should be used.
|
||||
if applications_addon is not None:
|
||||
args.extend(["addon", "applications"])
|
||||
|
||||
args.extend([
|
||||
"extractenvironments",
|
||||
export_url
|
||||
])
|
||||
|
||||
if os.getenv('IS_TEST'):
|
||||
args.append("--automatic-tests")
|
||||
|
||||
for key, value in add_kwargs.items():
|
||||
args.extend([f"--{key}", value])
|
||||
self.log.debug("Executing: {}".format(" ".join(args)))
|
||||
run_ayon_launcher_process(*args, logger=self.log)
|
||||
|
||||
self.log.debug("Loading file ...")
|
||||
with open(export_url) as fp:
|
||||
contents = json.load(fp)
|
||||
|
||||
job.rrEnvList = RREnvList(contents).serialize()
|
||||
return job
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Requires:
|
||||
instance.context.data["project_settings"]
|
||||
Provides:
|
||||
instance.data["rr_root"] (str) - root folder of RoyalRender server
|
||||
"""
|
||||
import os.path
|
||||
|
||||
import pyblish.api
|
||||
from ayon_royalrender.rr_job import get_rr_platform
|
||||
|
||||
|
||||
class CollectRRPathFromInstance(pyblish.api.InstancePlugin):
|
||||
"""Collect RR Path from instance.
|
||||
|
||||
All RoyalRender server roots are set in `Studio Settings`, each project
|
||||
uses only key pointing to that part to limit typos inside of Project
|
||||
settings.
|
||||
Eventually could be possible to add dropdown with these keys to the
|
||||
Creators to allow artists to select which RR server they would like to use.
|
||||
"""
|
||||
|
||||
order = pyblish.api.CollectorOrder
|
||||
label = "Collect Royal Render path name from the Instance"
|
||||
families = ["render", "prerender", "renderlayer"]
|
||||
|
||||
def process(self, instance):
|
||||
instance.data["rr_root"] = self._collect_root(instance)
|
||||
self.log.info(
|
||||
"Using '{}' for submission.".format(instance.data["rr_root"]))
|
||||
|
||||
def _collect_root(self, instance):
|
||||
# type: (pyblish.api.Instance) -> str
|
||||
"""Get Royal Render pat name from render instance.
|
||||
If artist should be able to select specific RR server it must be added
|
||||
to creator. It is not there yet.
|
||||
"""
|
||||
rr_settings = instance.context.data["project_settings"]["royalrender"]
|
||||
rr_paths = rr_settings["rr_paths"]
|
||||
selected_keys = rr_settings["selected_rr_paths"]
|
||||
|
||||
platform = get_rr_platform()
|
||||
key_to_path = {
|
||||
item["name"]: item["value"][platform]
|
||||
for item in rr_paths
|
||||
}
|
||||
|
||||
for selected_key in selected_keys:
|
||||
rr_root = key_to_path[selected_key]
|
||||
if os.path.exists(rr_root):
|
||||
return rr_root
|
||||
|
|
@ -0,0 +1,227 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Collect sequences from Royal Render Job."""
|
||||
import os
|
||||
import re
|
||||
import copy
|
||||
import json
|
||||
from pprint import pformat
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
def collect(root,
|
||||
regex=None,
|
||||
exclude_regex=None,
|
||||
frame_start=None,
|
||||
frame_end=None):
|
||||
"""Collect sequence collections in root"""
|
||||
|
||||
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 "AYON_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"
|
||||
review = True
|
||||
|
||||
def process(self, context):
|
||||
|
||||
self.review = (
|
||||
context.data
|
||||
["project_settings"]
|
||||
["royalrender"]
|
||||
["publish"]
|
||||
["CollectSequencesFromJob"]
|
||||
["review"]
|
||||
)
|
||||
|
||||
publish_data_paths = (
|
||||
os.environ.get("AYON_PUBLISH_DATA")
|
||||
or os.environ.get("OPENPYPE_PUBLISH_DATA")
|
||||
)
|
||||
if publish_data_paths:
|
||||
self.log.debug(publish_data_paths)
|
||||
paths = publish_data_paths.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")
|
||||
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("productName") and len(collections) > 1:
|
||||
self.log.error("Forced produce 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 and self.review:
|
||||
self.log.info("attaching review")
|
||||
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 product provided, get it from collection's head
|
||||
product_name = (
|
||||
data.get("productName", 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])
|
||||
|
||||
ext = list(collection)[0].split('.')[-1]
|
||||
|
||||
instance.data.update({
|
||||
"name": str(collection),
|
||||
"productType": families[0],
|
||||
"family": families[0],
|
||||
"families": list(families),
|
||||
"productName": product_name,
|
||||
"folderPath": data.get(
|
||||
"folderPath", context.data["folderPath"]
|
||||
),
|
||||
"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),
|
||||
"frameStart": start,
|
||||
"frameEnd": end,
|
||||
"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,43 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Submitting render job to RoyalRender."""
|
||||
import os
|
||||
|
||||
from maya.OpenMaya import MGlobal # noqa: F401
|
||||
|
||||
from ayon_royalrender import lib
|
||||
from ayon_core.pipeline.farm.tools import iter_expected_files
|
||||
|
||||
|
||||
class CreateMayaRoyalRenderJob(lib.BaseCreateRoyalRenderJob):
|
||||
label = "Create Maya Render job in RR"
|
||||
hosts = ["maya"]
|
||||
families = ["renderlayer"]
|
||||
|
||||
def update_job_with_host_specific(self, instance, job):
|
||||
job.Software = "Maya"
|
||||
job.Version = "{0:.2f}".format(MGlobal.apiVersion() / 10000)
|
||||
if instance.data.get("cameras"):
|
||||
job.Camera = instance.data["cameras"][0].replace("'", '"')
|
||||
workspace = instance.context.data["workspaceDir"]
|
||||
job.SceneDatabaseDir = workspace
|
||||
|
||||
return job
|
||||
|
||||
def process(self, instance):
|
||||
"""Plugin entry point."""
|
||||
super(CreateMayaRoyalRenderJob, self).process(instance)
|
||||
|
||||
expected_files = instance.data["expectedFiles"]
|
||||
first_file_path = next(iter_expected_files(expected_files))
|
||||
output_dir = os.path.dirname(first_file_path)
|
||||
instance.data["outputDir"] = output_dir
|
||||
|
||||
layer = instance.data["setMembers"] # type: str
|
||||
layer_name = layer.removeprefix("rs_")
|
||||
|
||||
job = self.get_job(instance, self.scene_path, first_file_path,
|
||||
layer_name)
|
||||
job = self.update_job_with_host_specific(instance, job)
|
||||
job = self.inject_environment(instance, job)
|
||||
|
||||
instance.data["rrJobs"].append(job)
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Submitting render job to RoyalRender."""
|
||||
import re
|
||||
|
||||
from ayon_royalrender import lib
|
||||
|
||||
|
||||
class CreateNukeRoyalRenderJob(lib.BaseCreateRoyalRenderJob):
|
||||
"""Creates separate rendering job for Royal Render"""
|
||||
label = "Create Nuke Render job in RR"
|
||||
hosts = ["nuke"]
|
||||
families = ["render", "prerender"]
|
||||
|
||||
def process(self, instance):
|
||||
super(CreateNukeRoyalRenderJob, self).process(instance)
|
||||
|
||||
# redefinition of families
|
||||
if "render" in instance.data["productType"]:
|
||||
instance.data["productType"] = "write"
|
||||
instance.data["family"] = "write"
|
||||
instance.data["families"].insert(0, "render2d")
|
||||
elif "prerender" in instance.data["productType"]:
|
||||
instance.data["productType"] = "write"
|
||||
instance.data["family"] = "write"
|
||||
instance.data["families"].insert(0, "prerender")
|
||||
|
||||
jobs = self.create_jobs(instance)
|
||||
for job in jobs:
|
||||
job = self.update_job_with_host_specific(instance, job)
|
||||
job = self.inject_environment(instance, job)
|
||||
|
||||
instance.data["rrJobs"].append(job)
|
||||
|
||||
def update_job_with_host_specific(self, instance, job):
|
||||
nuke_version = re.search(
|
||||
r"\d+\.\d+", instance.context.data.get("hostVersion"))
|
||||
|
||||
job.Software = "Nuke"
|
||||
job.Version = nuke_version.group()
|
||||
|
||||
return job
|
||||
|
||||
def create_jobs(self, instance):
|
||||
"""Nuke creates multiple RR jobs - for baking etc."""
|
||||
# get output path
|
||||
render_path = instance.data['path']
|
||||
script_path = self.scene_path
|
||||
node = instance.data["transientData"]["node"]
|
||||
|
||||
# main job
|
||||
jobs = [
|
||||
self.get_job(
|
||||
instance,
|
||||
script_path,
|
||||
render_path,
|
||||
node.name()
|
||||
)
|
||||
]
|
||||
|
||||
for baking_script in instance.data.get("bakingNukeScripts", []):
|
||||
render_path = baking_script["bakeRenderPath"]
|
||||
script_path = baking_script["bakeScriptPath"]
|
||||
exe_node_name = baking_script["bakeWriteNodeName"]
|
||||
|
||||
jobs.append(self.get_job(
|
||||
instance,
|
||||
script_path,
|
||||
render_path,
|
||||
exe_node_name
|
||||
))
|
||||
|
||||
return jobs
|
||||
|
|
@ -0,0 +1,244 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Create publishing job on RoyalRender."""
|
||||
import os
|
||||
import attr
|
||||
import json
|
||||
|
||||
import pyblish.api
|
||||
|
||||
from ayon_royalrender.rr_job import (
|
||||
RRJob,
|
||||
RREnvList,
|
||||
get_rr_platform
|
||||
)
|
||||
from ayon_core.pipeline.publish import KnownPublishError
|
||||
from ayon_core.pipeline.farm.pyblish_functions import (
|
||||
create_skeleton_instance,
|
||||
create_instances_for_aov,
|
||||
attach_instances_to_product,
|
||||
prepare_representations,
|
||||
create_metadata_path
|
||||
)
|
||||
from ayon_core.pipeline import publish
|
||||
|
||||
|
||||
class CreatePublishRoyalRenderJob(pyblish.api.InstancePlugin,
|
||||
publish.ColormanagedPyblishPluginMixin):
|
||||
"""Creates job which publishes rendered files to publish area.
|
||||
|
||||
Job waits until all rendering jobs are finished, triggers `publish` command
|
||||
where it reads from prepared .json file with metadata about what should
|
||||
be published, renames prepared images and publishes them.
|
||||
|
||||
When triggered it produces .log file next to .json file in work area.
|
||||
"""
|
||||
label = "Create publish job in RR"
|
||||
order = pyblish.api.IntegratorOrder + 0.2
|
||||
icon = "tractor"
|
||||
targets = ["local"]
|
||||
hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"]
|
||||
families = ["render.farm", "prerender.farm",
|
||||
"renderlayer", "imagesequence", "vrayscene"]
|
||||
aov_filter = {"maya": [r".*([Bb]eauty).*"],
|
||||
"aftereffects": [r".*"], # for everything from AE
|
||||
"harmony": [r".*"], # for everything from AE
|
||||
"celaction": [r".*"]}
|
||||
|
||||
skip_integration_repre_list = []
|
||||
|
||||
# mapping of instance properties to be transferred to new instance
|
||||
# for every specified family
|
||||
instance_transfer = {
|
||||
"slate": ["slateFrames", "slate"],
|
||||
"review": ["lutPath"],
|
||||
"render2d": ["bakingNukeScripts", "version"],
|
||||
"renderlayer": ["convertToScanline"]
|
||||
}
|
||||
|
||||
# list of family names to transfer to new family if present
|
||||
families_transfer = ["render3d", "render2d", "ftrack", "slate"]
|
||||
|
||||
environ_keys = [
|
||||
"FTRACK_API_USER",
|
||||
"FTRACK_API_KEY",
|
||||
"FTRACK_SERVER",
|
||||
"AYON_APP_NAME",
|
||||
"AYON_USERNAME",
|
||||
"AYON_SG_USERNAME",
|
||||
]
|
||||
priority = 50
|
||||
|
||||
def process(self, instance):
|
||||
context = instance.context
|
||||
self.context = context
|
||||
self.anatomy = instance.context.data["anatomy"]
|
||||
|
||||
if not instance.data.get("farm"):
|
||||
self.log.info("Skipping local instance.")
|
||||
return
|
||||
|
||||
instance_skeleton_data = create_skeleton_instance(
|
||||
instance,
|
||||
families_transfer=self.families_transfer,
|
||||
instance_transfer=self.instance_transfer)
|
||||
|
||||
do_not_add_review = False
|
||||
if instance.data.get("review") is False:
|
||||
self.log.debug("Instance has review explicitly disabled.")
|
||||
do_not_add_review = True
|
||||
|
||||
if isinstance(instance.data.get("expectedFiles")[0], dict):
|
||||
instances = create_instances_for_aov(
|
||||
instance, instance_skeleton_data,
|
||||
self.aov_filter, self.skip_integration_repre_list,
|
||||
do_not_add_review)
|
||||
|
||||
else:
|
||||
representations = prepare_representations(
|
||||
instance_skeleton_data,
|
||||
instance.data.get("expectedFiles"),
|
||||
self.anatomy,
|
||||
self.aov_filter,
|
||||
self.skip_integration_repre_list,
|
||||
do_not_add_review,
|
||||
instance.context,
|
||||
self
|
||||
)
|
||||
|
||||
if "representations" not in instance_skeleton_data.keys():
|
||||
instance_skeleton_data["representations"] = []
|
||||
|
||||
# add representation
|
||||
instance_skeleton_data["representations"] += representations
|
||||
instances = [instance_skeleton_data]
|
||||
|
||||
# attach instances to product
|
||||
if instance.data.get("attachTo"):
|
||||
instances = attach_instances_to_product(
|
||||
instance.data.get("attachTo"), instances
|
||||
)
|
||||
|
||||
self.log.info("Creating RoyalRender Publish job ...")
|
||||
|
||||
if not instance.data.get("rrJobs"):
|
||||
self.log.error(("There is no prior RoyalRender "
|
||||
"job on the instance."))
|
||||
raise KnownPublishError(
|
||||
"Can't create publish job without prior rendering jobs first")
|
||||
|
||||
rr_job = self.get_job(instance, instances)
|
||||
instance.data["rrJobs"].append(rr_job)
|
||||
|
||||
# publish job file
|
||||
publish_job = {
|
||||
"folderPath": instance_skeleton_data["folderPath"],
|
||||
"frameStart": instance_skeleton_data["frameStart"],
|
||||
"frameEnd": instance_skeleton_data["frameEnd"],
|
||||
"fps": instance_skeleton_data["fps"],
|
||||
"source": instance_skeleton_data["source"],
|
||||
"user": instance.context.data["user"],
|
||||
"version": instance.context.data["version"], # workfile version
|
||||
"intent": instance.context.data.get("intent"),
|
||||
"comment": instance.context.data.get("comment"),
|
||||
"job": attr.asdict(rr_job),
|
||||
"instances": instances
|
||||
}
|
||||
|
||||
metadata_path, rootless_metadata_path = \
|
||||
create_metadata_path(instance, self.anatomy)
|
||||
|
||||
self.log.info("Writing json file: {}".format(metadata_path))
|
||||
with open(metadata_path, "w") as f:
|
||||
json.dump(publish_job, f, indent=4, sort_keys=True)
|
||||
|
||||
def get_job(self, instance, instances):
|
||||
"""Create RR publishing job.
|
||||
|
||||
Based on provided original instance and additional instances,
|
||||
create publishing job and return it to be submitted to farm.
|
||||
|
||||
Args:
|
||||
instance (Instance): Original instance.
|
||||
instances (list of Instance): List of instances to
|
||||
be published on farm.
|
||||
|
||||
Returns:
|
||||
RRJob: RoyalRender publish job.
|
||||
|
||||
"""
|
||||
data = instance.data.copy()
|
||||
product_name = data["productName"]
|
||||
jobname = "Publish - {}".format(product_name)
|
||||
|
||||
# Transfer the environment from the original job to this dependent
|
||||
# job, so they use the same environment
|
||||
metadata_path, rootless_metadata_path = \
|
||||
create_metadata_path(instance, self.anatomy)
|
||||
|
||||
anatomy_data = instance.context.data["anatomyData"]
|
||||
|
||||
environment = RREnvList({
|
||||
"AYON_PROJECT_NAME": anatomy_data["project"]["name"],
|
||||
"AYON_FOLDER_PATH": instance.context.data["folderPath"],
|
||||
"AYON_TASK_NAME": anatomy_data["task"]["name"],
|
||||
"AYON_USERNAME": anatomy_data["user"]
|
||||
})
|
||||
|
||||
# add environments from self.environ_keys
|
||||
for env_key in self.environ_keys:
|
||||
if os.getenv(env_key):
|
||||
environment[env_key] = os.environ[env_key]
|
||||
|
||||
# pass environment keys from self.environ_job_filter
|
||||
# and collect all pre_ids to wait for
|
||||
jobs_pre_ids = []
|
||||
for job in instance.data["rrJobs"]: # type: RRJob
|
||||
jobs_pre_ids.append(job.PreID)
|
||||
|
||||
priority = self.priority or instance.data.get("priority", 50)
|
||||
|
||||
# rr requires absolute path or all jobs won't show up in rrControl
|
||||
abs_metadata_path = self.anatomy.fill_root(rootless_metadata_path)
|
||||
|
||||
# command line set in E01__OpenPype__PublishJob.cfg, here only
|
||||
# additional logging
|
||||
args = [
|
||||
">", os.path.join(os.path.dirname(abs_metadata_path),
|
||||
"rr_out.log"),
|
||||
"2>&1"
|
||||
]
|
||||
|
||||
job = RRJob(
|
||||
Software="OpenPype",
|
||||
Renderer="Once",
|
||||
SeqStart=1,
|
||||
SeqEnd=1,
|
||||
SeqStep=1,
|
||||
SeqFileOffset=0,
|
||||
Version=os.environ["AYON_BUNDLE_NAME"],
|
||||
SceneName=abs_metadata_path,
|
||||
# command line arguments
|
||||
CustomAddCmdFlags=" ".join(args),
|
||||
IsActive=True,
|
||||
ImageFilename="execOnce.file",
|
||||
ImageDir="<SceneFolder>",
|
||||
ImageExtension="",
|
||||
ImagePreNumberLetter="",
|
||||
SceneOS=get_rr_platform(),
|
||||
rrEnvList=environment.serialize(),
|
||||
Priority=priority,
|
||||
CustomSHotName=jobname,
|
||||
CompanyProjectName=instance.context.data["projectName"]
|
||||
)
|
||||
|
||||
# add assembly jobs as dependencies
|
||||
if instance.data.get("tileRendering"):
|
||||
self.log.info("Adding tile assembly jobs as dependencies...")
|
||||
job.WaitForPreIDs += instance.data.get("assemblySubmissionJobs")
|
||||
elif instance.data.get("bakingSubmissionJobs"):
|
||||
self.log.info("Adding baking submission jobs as dependencies...")
|
||||
job.WaitForPreIDs += instance.data["bakingSubmissionJobs"]
|
||||
else:
|
||||
job.WaitForPreIDs += jobs_pre_ids
|
||||
|
||||
return job
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Submit jobs to RoyalRender."""
|
||||
import tempfile
|
||||
|
||||
import pyblish.api
|
||||
from ayon_royalrender.api import (
|
||||
RRJob,
|
||||
Api as rrApi,
|
||||
SubmitterParameter
|
||||
)
|
||||
from ayon_core.pipeline.publish import KnownPublishError
|
||||
|
||||
|
||||
class SubmitJobsToRoyalRender(pyblish.api.ContextPlugin):
|
||||
"""Find all jobs, create submission XML and submit it to RoyalRender."""
|
||||
label = "Submit jobs to RoyalRender"
|
||||
order = pyblish.api.IntegratorOrder + 0.3
|
||||
targets = ["local"]
|
||||
|
||||
def __init__(self):
|
||||
super(SubmitJobsToRoyalRender, self).__init__()
|
||||
self._rr_root = None
|
||||
self._rr_api = None
|
||||
self._submission_parameters = []
|
||||
|
||||
def process(self, context):
|
||||
|
||||
# iterate over all instances and try to find RRJobs
|
||||
jobs = []
|
||||
instance_rr_path = None
|
||||
for instance in context:
|
||||
if isinstance(instance.data.get("rrJob"), RRJob):
|
||||
jobs.append(instance.data.get("rrJob"))
|
||||
if instance.data.get("rrJobs"):
|
||||
if all(
|
||||
isinstance(job, RRJob)
|
||||
for job in instance.data.get("rrJobs")):
|
||||
jobs += instance.data.get("rrJobs")
|
||||
if instance.data.get("rr_root"):
|
||||
instance_rr_path = instance.data["rr_root"]
|
||||
|
||||
if jobs:
|
||||
self._rr_root = instance_rr_path
|
||||
if not self._rr_root:
|
||||
raise KnownPublishError(
|
||||
("Missing RoyalRender root. "
|
||||
"You need to configure RoyalRender module."))
|
||||
self._rr_api = rrApi(self._rr_root)
|
||||
self._submission_parameters = self.get_submission_parameters()
|
||||
self.process_submission(jobs)
|
||||
return
|
||||
|
||||
self.log.info("No RoyalRender jobs found")
|
||||
|
||||
def process_submission(self, jobs):
|
||||
# type: ([RRJob]) -> None
|
||||
|
||||
idx_pre_id = 0
|
||||
for job in jobs:
|
||||
job.PreID = idx_pre_id
|
||||
if idx_pre_id > 0:
|
||||
job.WaitForPreIDs.append(idx_pre_id - 1)
|
||||
idx_pre_id += 1
|
||||
|
||||
submission = rrApi.create_submission(
|
||||
jobs,
|
||||
self._submission_parameters)
|
||||
|
||||
xml = tempfile.NamedTemporaryFile(suffix=".xml", delete=False)
|
||||
with open(xml.name, "w") as f:
|
||||
f.write(submission.serialize())
|
||||
|
||||
self.log.info("submitting job(s) file: {}".format(xml.name))
|
||||
self._rr_api.submit_file(file=xml.name)
|
||||
|
||||
def create_file(self, name, ext, contents=None):
|
||||
temp = tempfile.NamedTemporaryFile(
|
||||
dir=self.tempdir,
|
||||
suffix=ext,
|
||||
prefix=name + '.',
|
||||
delete=False,
|
||||
)
|
||||
|
||||
if contents:
|
||||
with open(temp.name, 'w') as f:
|
||||
f.write(contents)
|
||||
|
||||
return temp.name
|
||||
|
||||
def get_submission_parameters(self):
|
||||
return [SubmitterParameter("RequiredMemory", "0")]
|
||||
305
server_addon/royalrender/client/ayon_royalrender/rr_job.py
Normal file
305
server_addon/royalrender/client/ayon_royalrender/rr_job.py
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Python wrapper for RoyalRender XML job file."""
|
||||
import sys
|
||||
from xml.dom import minidom as md
|
||||
import attr
|
||||
from collections import namedtuple, OrderedDict
|
||||
|
||||
|
||||
CustomAttribute = namedtuple("CustomAttribute", ["name", "value"])
|
||||
|
||||
|
||||
def get_rr_platform():
|
||||
# type: () -> str
|
||||
"""Returns name of platform used in rr jobs."""
|
||||
if sys.platform.lower() in ["win32", "win64"]:
|
||||
return "windows"
|
||||
elif sys.platform.lower() == "darwin":
|
||||
return "mac"
|
||||
else:
|
||||
return "linux"
|
||||
|
||||
|
||||
class RREnvList(dict):
|
||||
def serialize(self):
|
||||
# <rrEnvList>VariableA=ValueA~~~VariableB=ValueB</rrEnvList>
|
||||
return "~~~".join(
|
||||
["{}={}".format(k, v) for k, v in sorted(self.items())])
|
||||
|
||||
@staticmethod
|
||||
def parse(data):
|
||||
# type: (str) -> RREnvList
|
||||
"""Parse rrEnvList string and return it as RREnvList object."""
|
||||
out = RREnvList()
|
||||
for var in data.split("~~~"):
|
||||
k, v = var.split("=", maxsplit=1)
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
|
||||
@attr.s
|
||||
class RRJob(object):
|
||||
"""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: bool
|
||||
|
||||
# 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: bool
|
||||
|
||||
# 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: int
|
||||
|
||||
# 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.
|
||||
WaitForPreIDs = attr.ib(factory=list) # type: list
|
||||
|
||||
# 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
|
||||
|
||||
# This is used to hold command line arguments for Execute job
|
||||
CustomAddCmdFlags = attr.ib(default=None) # type: str
|
||||
|
||||
# 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
|
||||
CompanyProjectName = attr.ib(default=None) # type: str
|
||||
|
||||
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
|
||||
|
||||
# Environment
|
||||
# only used in RR 8.3 and newer
|
||||
rrEnvList = attr.ib(default=None, type=str) # 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(object):
|
||||
"""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 the submitter options per job.
|
||||
# list item must be of `SubmitterParameter` type
|
||||
SubmitterParameters = attr.ib(factory=list) # type: list
|
||||
|
||||
# List of the jobs in 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)
|
||||
root.appendChild(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")
|
||||
# we are handling `WaitForPreIDs` separately.
|
||||
wait_pre_ids = serialized_job.pop("WaitForPreIDs", [])
|
||||
|
||||
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.createElement(item)
|
||||
xml_attr.appendChild(
|
||||
root.createTextNode(str(value))
|
||||
)
|
||||
xml_job.appendChild(xml_attr)
|
||||
|
||||
# WaitForPreID - can be used multiple times
|
||||
for pre_id in wait_pre_ids:
|
||||
xml_attr = root.createElement("WaitForPreID")
|
||||
xml_attr.appendChild(
|
||||
root.createTextNode(str(pre_id))
|
||||
)
|
||||
xml_job.appendChild(xml_attr)
|
||||
|
||||
job_file.appendChild(xml_job)
|
||||
|
||||
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,205 @@
|
|||
# -*- 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 ayon_core. 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 = {}
|
||||
|
||||
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 not os.path.exists(op_path):
|
||||
# try to find in user local context
|
||||
op_path = os.path.join(
|
||||
os.environ.get("LOCALAPPDATA"),
|
||||
"Programs",
|
||||
"OpenPype", "openpype_console.exe"
|
||||
)
|
||||
if not os.path.exists(op_path):
|
||||
raise Exception("Error: OpenPype was not found.")
|
||||
|
||||
op_path = os.path.dirname(op_path)
|
||||
print(" - found OpenPype installation {}".format(op_path))
|
||||
|
||||
self.openpype_root = op_path
|
||||
|
||||
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 and not self.show():
|
||||
return
|
||||
|
||||
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("folder") or \
|
||||
not self.context.get("task"):
|
||||
self._show_rr_warning("Context selection failed.")
|
||||
return False
|
||||
|
||||
# self.context["app_name"] = self.job.renderer.name
|
||||
# there should be mapping between OpenPype and Royal Render
|
||||
# app names and versions, but since app_name is not used
|
||||
# currently down the line (but it is required by OP publish command
|
||||
# right now).
|
||||
# self.context["app_name"] = "maya/2022"
|
||||
return True
|
||||
|
||||
@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 = {"AYON_PROJECT_NAME": str(self.context.get("project")),
|
||||
"AYON_FOLDER_PATH": str(self.context.get("folder")),
|
||||
"AYON_TASK_NAME": str(self.context.get("task")),
|
||||
# "AYON_APP_NAME": str(self.context.get("app_name"))
|
||||
}
|
||||
|
||||
print(">>> setting environment:")
|
||||
for k, v in env.items():
|
||||
print(" {}: {}".format(k, v))
|
||||
|
||||
publishing_paths = [os.path.join(self.job.imageDir,
|
||||
os.path.dirname(
|
||||
self.job.imageFileName))]
|
||||
|
||||
# add additional channels
|
||||
channel_idx = 0
|
||||
channel = self.job.channelFileName(channel_idx)
|
||||
while channel:
|
||||
channel_path = os.path.dirname(
|
||||
os.path.join(self.job.imageDir, channel))
|
||||
if channel_path not in publishing_paths:
|
||||
publishing_paths.append(channel_path)
|
||||
channel_idx += 1
|
||||
channel = self.job.channelFileName(channel_idx)
|
||||
|
||||
args = [os.path.join(self.openpype_root, self.openpype_executable),
|
||||
'publish', '-t', "rr_control", "--gui"
|
||||
]
|
||||
|
||||
args += publishing_paths
|
||||
|
||||
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()
|
||||
|
||||
# try to set context from environment
|
||||
for key, env_keys in (
|
||||
("project", ["AYON_PROJECT_NAME", "AVALON_PROJECT"]),
|
||||
("folder", ["AYON_FOLDER_PATH", "AVALON_ASSET"]),
|
||||
("task", ["AYON_TASK_NAME", "AVALON_TASK"]),
|
||||
# ("app_name", ["AYON_APP_NAME", "AVALON_APP_NAME"])
|
||||
):
|
||||
value = ""
|
||||
for env_key in env_keys:
|
||||
value = os.getenv(env_key)
|
||||
if value:
|
||||
break
|
||||
selector.context[key] = value
|
||||
|
||||
# if anything inside is None, scratch the whole thing and
|
||||
# ask user for context.
|
||||
for _, v in selector.context.items():
|
||||
if not v:
|
||||
selector.context = {}
|
||||
break
|
||||
|
||||
selector.process_job()
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2 KiB |
|
|
@ -0,0 +1,71 @@
|
|||
IconApp= E01__OpenPype.png
|
||||
Name= OpenPype
|
||||
rendererName= Once
|
||||
Version= 1
|
||||
Version_Minor= 0
|
||||
Type=Execute
|
||||
TYPEv9=Execute
|
||||
ExecuteJobType=Once
|
||||
|
||||
|
||||
################################# [Windows] [Linux] [Osx] ##################################
|
||||
|
||||
|
||||
CommandLine=<envFileExecute <rrEnvFile>>
|
||||
|
||||
CommandLine=<rrEnvLine>
|
||||
|
||||
|
||||
::win CommandLine= set "CUDA_VISIBLE_DEVICES=<GpuListC>"
|
||||
::lx CommandLine= setenv CUDA_VISIBLE_DEVICES <GpuListC>
|
||||
::osx CommandLine= setenv CUDA_VISIBLE_DEVICES <GpuListC>
|
||||
|
||||
|
||||
CommandLine=
|
||||
<SetEnvGlobal>
|
||||
|
||||
CommandLine=
|
||||
<SetEnvSoft>
|
||||
|
||||
CommandLine=
|
||||
<ResetExitCode>
|
||||
|
||||
CommandLine= "<Exe>" --headless publish <Scene>
|
||||
--targets royalrender
|
||||
--targets farm
|
||||
<AdditionalCommandlineParam>
|
||||
<CustomFlags>
|
||||
|
||||
CommandLine=
|
||||
<CheckExitCode>
|
||||
|
||||
|
||||
|
||||
################################## Render Settings ##################################
|
||||
|
||||
|
||||
|
||||
################################## Submitter Settings ##################################
|
||||
StartMultipleInstances= 0~0
|
||||
SceneFileExtension= *.json
|
||||
AllowImageNameChange= 0
|
||||
AllowImageDirChange= 0
|
||||
SequenceDivide= 0~1
|
||||
PPSequenceCheck=0~0
|
||||
PPCreateSmallVideo=0~0
|
||||
PPCreateFullVideo=0~0
|
||||
AllowLocalRenderOut= 0~0
|
||||
|
||||
|
||||
################################## Client Settings ##################################
|
||||
|
||||
IconApp=E01__OpenPype.png
|
||||
|
||||
licenseFailLine=
|
||||
|
||||
errorSearchLine=
|
||||
|
||||
permanentErrorSearchLine =
|
||||
|
||||
Frozen_MinCoreUsage=0.3
|
||||
Frozen_Minutes=30
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
IconApp= E01__OpenPype.png
|
||||
Name= OpenPype
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
[Windows]
|
||||
Executable= openpype_console.exe
|
||||
Path= OS; <ProgramFiles(x86)>\OpenPype\*\openpype_console.exe
|
||||
Path= 32; <ProgramFiles(x86)>\OpenPype\*\openpype_console.exe
|
||||
|
||||
[Linux]
|
||||
Executable= openpype_console
|
||||
Path= OS; /opt/openpype/*/openpype_console
|
||||
|
||||
[Mac]
|
||||
Executable= openpype_console
|
||||
Path= OS; /Applications/OpenPype*/Content/MacOS/openpype_console
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
PrePostType= pre
|
||||
CommandLine=
|
||||
<ResetExitCode>
|
||||
CommandLine= <OsxApp "<rrBin64>rrPythonconsole" > "<RR_DIR>render_apps/_prepost_scripts/PreOpenPypeInjectEnvironments.py"
|
||||
|
||||
CommandLine=
|
||||
<CheckExitCode> <FN>
|
||||
|
||||
CommandLine= "<RenderAppPath:OpenPype>"
|
||||
CommandLine=
|
||||
<CheckExitCode> <FN>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
os.environ["OPENYPYPE_TESTVAR"] = "OpenPype was here"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring AYON addon 'royalrender' version."""
|
||||
__version__ = "0.2.0"
|
||||
|
|
@ -1,3 +1,10 @@
|
|||
name = "royalrender"
|
||||
title = "Royal Render"
|
||||
version = "0.1.1"
|
||||
version = "0.2.0"
|
||||
|
||||
client_dir = "ayon_royalrender"
|
||||
|
||||
ayon_required_addons = {
|
||||
"core": ">0.3.2",
|
||||
}
|
||||
ayon_compatible_addons = {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue