diff --git a/colorbleed/plugins/global/publish/submit_publish_job.py b/colorbleed/plugins/global/publish/submit_publish_job.py index 34a09c9b81..184ed0b43d 100644 --- a/colorbleed/plugins/global/publish/submit_publish_job.py +++ b/colorbleed/plugins/global/publish/submit_publish_job.py @@ -123,7 +123,9 @@ class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): label = "Submit image sequence jobs to Deadline" order = pyblish.api.IntegratorOrder + 0.1 hosts = ["fusion", "maya"] - families = ["colorbleed.saver.deadline", "colorbleed.renderlayer"] + families = ["colorbleed.saver.deadline", + "colorbleed.renderlayer", + "colorbleed.vrayscene"] def process(self, instance): @@ -134,8 +136,10 @@ class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): # Get a submission job job = instance.data.get("deadlineSubmissionJob") if not job: - raise RuntimeError("Can't continue without valid deadline " - "submission prior to this plug-in.") + self.log.warning("Can't continue without valid deadline " + "submission prior to this plug-in.") + self.log.info("Skipping Publish Job") + return data = instance.data.copy() subset = data["subset"] @@ -155,15 +159,17 @@ class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): # This assumes the output files start with subset name and ends with # a file extension. if "ext" in instance.data: - ext = re.escape(instance.data["ext"]) + ext = instance.data["ext"].strip(".") else: ext = "\.\D+" - regex = "^{subset}.*\d+{ext}$".format(subset=re.escape(subset), - ext=ext) + regex = "^{subset}.*\d+\.{ext}$".format(subset=re.escape(subset), + ext=re.escape(ext)) + + # Remove deadline submission job, not needed in metadata + data.pop("deadlineSubmissionJob") # Write metadata for publish job - render_job = data.pop("deadlineSubmissionJob") metadata = { "regex": regex, "startFrame": start, @@ -189,7 +195,7 @@ class SubmitDependentImageSequenceJobDeadline(pyblish.api.InstancePlugin): override = data["overrideExistingFrame"] # override = data.get("overrideExistingFrame", False) - out_file = render_job.get("OutFile") + out_file = job.get("OutFile") if not out_file: raise RuntimeError("OutFile not found in render job!") diff --git a/colorbleed/plugins/maya/create/colorbleed_vrayscene.py b/colorbleed/plugins/maya/create/colorbleed_vrayscene.py new file mode 100644 index 0000000000..e30f4ac3ce --- /dev/null +++ b/colorbleed/plugins/maya/create/colorbleed_vrayscene.py @@ -0,0 +1,34 @@ +from collections import OrderedDict + +import avalon.maya + + +class CreateVRayScene(avalon.maya.Creator): + + label = "VRay Scene" + family = "colorbleed.vrayscene" + # icon = "blocks" + + def __init__(self, *args, **kwargs): + super(CreateVRayScene, self).__init__(*args, **kwargs) + + # We won't be publishing this one + self.data["id"] = "avalon.vrayscene" + + # We don't need subset or asset attributes + self.data.pop("subset", None) + self.data.pop("asset", None) + self.data.pop("active", None) + + data = OrderedDict(**self.data) + + data["camera"] = "persp" + data["suspendRenderJob"] = False + data["suspendPublishJob"] = False + data["includeDefaultRenderLayer"] = False + data["extendFrames"] = False + data["pools"] = "" + + self.data = data + + self.options = {"useSelection": False} # Force no content diff --git a/colorbleed/plugins/maya/publish/collect_vray_scene.py b/colorbleed/plugins/maya/publish/collect_vray_scene.py new file mode 100644 index 0000000000..cedd527106 --- /dev/null +++ b/colorbleed/plugins/maya/publish/collect_vray_scene.py @@ -0,0 +1,111 @@ +import os + +import pyblish.api + +from avalon import api, maya + +from maya import cmds + + +class CollectVRayScene(pyblish.api.ContextPlugin): + """Collect all information prior for exporting vrscenes + """ + + order = pyblish.api.CollectorOrder + label = "Collect VRay Scene" + hosts = ["maya"] + + def process(self, context): + + # Sort by displayOrder + def sort_by_display_order(layer): + return cmds.getAttr("%s.displayOrder" % layer) + + asset = api.Session["AVALON_ASSET"] + work_dir = context.data["workspaceDir"] + + # Get VRay Scene instance + vray_scenes = maya.lsattr("family", "colorbleed.vrayscene") + if not vray_scenes: + self.log.info("No instance found of family: `colorbleed.vrayscene`") + return + + assert len(vray_scenes) == 1, "Multiple vrayscene instances found!" + vray_scene = vray_scenes[0] + + vrscene_data = {k: cmds.getAttr("%s.%s" % (vray_scene, k)) for + k in cmds.listAttr(vray_scene, userDefined=True)} + + # Output data + start_frame = int(cmds.getAttr("defaultRenderGlobals.startFrame")) + end_frame = int(cmds.getAttr("defaultRenderGlobals.endFrame")) + + # Create output file path with template + file_name = context.data["currentFile"].replace("\\", "/") + vrscene = ("vrayscene", "", "_", "") + vrscene_output = os.path.join(work_dir, *vrscene) + + vrscene_data["startFrame"] = start_frame + vrscene_data["endFrame"] = end_frame + vrscene_data["vrsceneOutput"] = vrscene_output + + context.data["startFrame"] = start_frame + context.data["endFrame"] = end_frame + + # Check and create render output template for render job + # outputDir is required for submit_publish_job + if not vrscene_data.get("suspendRenderJob", False): + renders = ("renders", "", "_", "") + output_renderpath = os.path.join(work_dir, *renders) + vrscene_data["outputDir"] = output_renderpath + + # Get resolution + resolution = (cmds.getAttr("defaultResolution.width"), + cmds.getAttr("defaultResolution.height")) + + # Get format extension + extension = cmds.getAttr("vraySettings.imageFormatStr") + + # Get render layers + render_layers = [i for i in cmds.ls(type="renderLayer") if + cmds.getAttr("{}.renderable".format(i)) and not + cmds.referenceQuery(i, isNodeReferenced=True)] + + # Check if we need to filter out the default render layer + if vrscene_data.get("includeDefaultRenderLayer", True): + render_layers = [r for r in render_layers + if r != "defaultRenderLayer"] + + render_layers = sorted(render_layers, key=sort_by_display_order) + for layer in render_layers: + + if layer.endswith("defaultRenderLayer"): + layer = "masterLayer" + + data = { + "subset": layer, + "setMembers": layer, + + "startFrame": start_frame, + "endFrame": end_frame, + "renderer": "vray", + "resolution": resolution, + "ext": extension, + + # instance subset + "family": "VRay Scene", + "families": ["colorbleed.vrayscene"], + "asset": asset, + "time": api.time(), + "author": context.data["user"], + + # Add source to allow tracing back to the scene from + # which was submitted originally + "source": file_name + } + + data.update(vrscene_data) + + instance = context.create_instance(layer) + self.log.info("Created: %s" % instance.name) + instance.data.update(data) diff --git a/colorbleed/plugins/maya/publish/submit_deadline.py b/colorbleed/plugins/maya/publish/submit_maya_deadline.py similarity index 100% rename from colorbleed/plugins/maya/publish/submit_deadline.py rename to colorbleed/plugins/maya/publish/submit_maya_deadline.py diff --git a/colorbleed/plugins/maya/publish/submit_vray_deadline.py b/colorbleed/plugins/maya/publish/submit_vray_deadline.py new file mode 100644 index 0000000000..97c26497f8 --- /dev/null +++ b/colorbleed/plugins/maya/publish/submit_vray_deadline.py @@ -0,0 +1,249 @@ +import getpass +import json +import os +from copy import deepcopy + +import pyblish.api + +from avalon import api +from avalon.vendor import requests + +from maya import cmds + + +class VraySubmitDeadline(pyblish.api.InstancePlugin): + """Export the scene to `.vrscene` files per frame per render layer + + vrscene files will be written out based on the following template: + /vrayscene//_/ + + A dependency job will be added for each layer to render the framer + through VRay Standalone + + """ + label = "Submit to Deadline ( vrscene )" + order = pyblish.api.IntegratorOrder + hosts = ["maya"] + families = ["colorbleed.vrayscene"] + + def process(self, instance): + + AVALON_DEADLINE = api.Session.get("AVALON_DEADLINE", + "http://localhost:8082") + assert AVALON_DEADLINE, "Requires AVALON_DEADLINE" + + context = instance.context + + deadline_url = "{}/api/jobs".format(AVALON_DEADLINE) + deadline_user = context.data.get("deadlineUser", getpass.getuser()) + + filepath = context.data["currentFile"] + filename = os.path.basename(filepath) + task_name = "{} - {}".format(filename, instance.name) + + batch_name = "VRay Scene Export - {}".format(filename) + + # Get the output template for vrscenes + vrscene_output = instance.data["vrsceneOutput"] + + # This is also the input file for the render job + first_file = self.format_output_filename(instance, + filename, + vrscene_output) + + # Primary job + self.log.info("Submitting export job ..") + + payload = { + "JobInfo": { + # Top-level group name + "BatchName": batch_name, + + # Job name, as seen in Monitor + "Name": task_name, + + # Arbitrary username, for visualisation in Monitor + "UserName": deadline_user, + + "Plugin": "MayaCmd", + "Frames": "1", + + "Comment": context.data.get("comment", ""), + }, + "PluginInfo": { + + # Mandatory for Deadline + "Version": cmds.about(version=True), + + # Input + "SceneFile": filepath, + # Output directory and filename + "OutputFilePath": vrscene_output.replace("\\", "/"), + + "CommandLineOptions": self.build_command(instance), + + "UseOnlyCommandLineOptions": True, + + "SkipExistingFrames": True, + }, + + # Mandatory for Deadline, may be empty + "AuxFiles": [] + } + + environment = dict(AVALON_TOOLS="global;python36;maya2018") + environment.update(api.Session.copy()) + + jobinfo_environment = self.build_jobinfo_environment(environment) + + payload["JobInfo"].update(jobinfo_environment) + + self.log.info("Job Data:\n{}".format(json.dumps(payload))) + + response = requests.post(url=deadline_url, json=payload) + if not response.ok: + raise RuntimeError(response.text) + + # Secondary job + # Store job to create dependency chain + dependency = response.json() + + if instance.data["suspendRenderJob"]: + self.log.info("Skipping render job and publish job") + return + + self.log.info("Submitting render job ..") + + start_frame = int(instance.data["startFrame"]) + end_frame = int(instance.data["endFrame"]) + ext = instance.data.get("ext", "exr") + + # Create output directory for renders + render_ouput = self.format_output_filename(instance, + filename, + instance.data["outputDir"], + dir=True) + + self.log.info("Render output: %s" % render_ouput) + + # Update output dir + instance.data["outputDir"] = render_ouput + + # Format output file name + sequence_filename = ".".join([instance.name, "%04d", ext]) + output_filename = os.path.join(render_ouput, sequence_filename) + + payload_b = { + "JobInfo": { + + "JobDependency0": dependency["_id"], + "BatchName": batch_name, + "Name": "Render {}".format(task_name), + "UserName": deadline_user, + + "Frames": "{}-{}".format(start_frame, end_frame), + + "Plugin": "Vray", + "OverrideTaskExtraInfoNames": False, + "Whitelist": "cb7" + }, + "PluginInfo": { + + "InputFilename": first_file, + "OutputFilename": output_filename, + "SeparateFilesPerFrame": True, + "VRayEngine": "V-Ray", + + "Width": instance.data["resolution"][0], + "Height": instance.data["resolution"][1], + + }, + "AuxFiles": [], + } + + # Add vray renderslave to environment + tools = environment["AVALON_TOOLS"] + ";vrayrenderslave" + environment_b = deepcopy(environment) + environment_b["AVALON_TOOLS"] = tools + + jobinfo_environment_b = self.build_jobinfo_environment(environment_b) + payload_b["JobInfo"].update(jobinfo_environment_b) + + self.log.info(json.dumps(payload_b)) + + # Post job to deadline + response_b = requests.post(url=deadline_url, json=payload_b) + if not response_b.ok: + raise RuntimeError(response_b.text) + + # Add job for publish job + if not instance.data.get("suspendPublishJob", False): + instance.data["deadlineSubmissionJob"] = response_b.json() + + def build_command(self, instance): + """Create command for Render.exe to export vray scene + + Returns: + str + + """ + + cmd = ('-r vray -proj {project} -cam {cam} -noRender -s {startFrame} ' + '-e {endFrame} -rl {layer} -exportFramesSeparate') + + return cmd.format(project=instance.context.data["workspaceDir"], + cam=instance.data.get("cam", "persp"), + startFrame=instance.data["startFrame"], + endFrame=instance.data["endFrame"], + layer=instance.name) + + def build_jobinfo_environment(self, env): + """Format environment keys and values to match Deadline rquirements + + Returns: + dict + + """ + return {"EnvironmentKeyValue%d" % index: "%s=%s" % (k, env[k]) + for index, k in enumerate(env)} + + def format_output_filename(self, instance, filename, template, dir=False): + """Format the expected output file of the Export job + + Example: + /_/ + "shot010_v006/shot010_v006_CHARS/CHARS" + + Args: + instance: + filename(str): + dir(bool): + + Returns: + str + + """ + + def smart_replace(string, key_values): + new_string = string + for key, value in key_values.items(): + new_string = new_string.replace(key, value) + return new_string + + # Ensure filename has no extension + file_name, _ = os.path.splitext(filename) + + # Reformat without tokens + output_path = smart_replace(template, + {"": file_name, + "": instance.name}) + + if dir: + return output_path.replace("\\", "/") + + start_frame = int(instance.data["startFrame"]) + filename_zero = "{}_{:04d}.vrscene".format(output_path, start_frame) + + result = filename_zero.replace("\\", "/") + + return result diff --git a/colorbleed/plugins/maya/publish/validate_translator_settings.py b/colorbleed/plugins/maya/publish/validate_translator_settings.py new file mode 100644 index 0000000000..7c7479792c --- /dev/null +++ b/colorbleed/plugins/maya/publish/validate_translator_settings.py @@ -0,0 +1,45 @@ +import pyblish.api +import colorbleed.api + +from maya import cmds + + +class ValidateTranslatorEnabled(pyblish.api.ContextPlugin): + + order = colorbleed.api.ValidateContentsOrder + label = "VRay Translator Settings" + families = ["colorbleed.vrayscene"] + actions = [colorbleed.api.RepairContextAction] + + def process(self, context): + + # Get vraySettings node + vray_settings = cmds.ls(type="VRaySettingsNode") + assert vray_settings, "Please ensure a VRay Settings Node is present" + + node = vray_settings[0] + + if not cmds.getAttr("{}.vrscene_on".format(node)): + self.info.error("Export vrscene not enabled") + + if not cmds.getAttr("{}.misc_eachFrameInFile".format(node)): + self.info.error("Each Frame in File not enabled") + + vrscene_filename = cmds.getAttr("{}.vrscene_filename".format(node)) + if vrscene_filename != "vrayscene//_/": + self.info.error("Template for file name is wrong") + + @classmethod + def repair(cls, context): + + vray_settings = cmds.ls(type="VRaySettingsNode") + if not vray_settings: + node = cmds.createNode("VRaySettingsNode") + else: + node = vray_settings[0] + + cmds.setAttr("{}.vrscene_on".format(node), True) + cmds.setAttr("{}.misc_eachFrameInFile".format(node), True) + cmds.setAttr("{}.vrscene_filename".format(node), + "vrayscene//_/", + type="string")