├── .gitignore ├── icons └── zync.png ├── scripts ├── config_maya.py.example ├── userSetup.py ├── maya_common.py ├── run_maya_tests.py ├── renderman_maya.py ├── zync_maya_test.py ├── resources │ └── submit_dialog.ui └── zync_maya.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | config_maya.py 2 | *.pyc 3 | -------------------------------------------------------------------------------- /icons/zync.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zync/zync-maya/HEAD/icons/zync.png -------------------------------------------------------------------------------- /scripts/config_maya.py.example: -------------------------------------------------------------------------------- 1 | # For single OS users. 2 | API_DIR = 'Z:/plugins/zync-python' 3 | 4 | # For multiple OS users. Detects user's operating system and 5 | # sets path to zync-python folder accordingly. 6 | #import platform 7 | #if platform.system() in ('Windows', 'Microsoft'): 8 | # API_DIR = 'Z:/plugins/zync-python' 9 | #else: 10 | # API_DIR = '/Volumes/server/plugins/zync-python' 11 | -------------------------------------------------------------------------------- /scripts/userSetup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import zync_maya 5 | 6 | import maya.cmds as cmds 7 | import maya.mel 8 | import maya.utils 9 | 10 | def create_zync_shelf(): 11 | maya.mel.eval('if (`shelfLayout -exists Zync `) deleteUI Zync;') 12 | shelfTab = maya.mel.eval('global string $gShelfTopLevel;') 13 | maya.mel.eval('global string $scriptsShelf;') 14 | maya.mel.eval('$scriptsShelf = `shelfLayout -p $gShelfTopLevel Zync`;') 15 | maya.mel.eval('shelfButton -parent $scriptsShelf -annotation "Render on Zync" ' + 16 | '-label "Render on Zync" -image "zync.png" -sourceType "python" ' + 17 | '-command ("zync_maya.submit_dialog()") -width 34 -height 34 -style "iconOnly";') 18 | 19 | maya.utils.executeDeferred( create_zync_shelf ) 20 | -------------------------------------------------------------------------------- /scripts/maya_common.py: -------------------------------------------------------------------------------- 1 | """ 2 | Zync Maya Plugin - Common 3 | """ 4 | import re 5 | 6 | # Regex string for checking if string contains a layer token. 7 | _HAS_LAYER_TOKEN_RE = re.compile(r'.*%l.*|.*.*|.*.*', re.IGNORECASE) 8 | _SUBSTITUTE_LAYER_TOKEN_RE = re.compile(r'%l||', re.IGNORECASE) 9 | _SUBSTITUTE_CAMERA_TOKEN_RE = re.compile(r'%c|', re.IGNORECASE) 10 | _SUBSTITUTE_SCENE_TOKEN_RE = re.compile(r'%s|', re.IGNORECASE) 11 | 12 | class MayaZyncException(Exception): 13 | pass 14 | 15 | 16 | class ZyncAbortedByUser(Exception): 17 | """ 18 | Exception to handle user's decision about canceling a process. 19 | """ 20 | pass 21 | 22 | 23 | class ZyncSubmissionCheckError(Exception): 24 | """ 25 | Exception to handle errors when trying to run a SubmissionCheck. 26 | """ 27 | pass 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zync Plugin for Autodesk's Maya 2 | 3 | **Notice:** this project has been archived and is no longer being maintained. 4 | 5 | For a list of Maya versions supported on Zync please see [our main website](https://www.zyncrender.com/#about). You can find additional info in [our documentation](https://docs.zyncrender.com/faq#q-what-applicationrendererplugin-versions-do-you-support). 6 | 7 | ## zync-python 8 | 9 | This plugin depends on zync-python, the Zync Python API. 10 | 11 | Before trying to install zync-maya, make sure to [download zync-python](https://github.com/zync/zync-python) and follow the setup instructions there. 12 | 13 | # Warning 14 | 15 | Note that the simplest and recommended way to install Zync plugins is through the Zync Client Application (see [instructions](https://docs.zyncrender.com/install-and-setup#option-1-the-plugins-tab-in-the-zync-client-app-simple-recommended-for-most-users)). The steps described below are for advanced users and we recommend to proceed with them only if you need to modify the plugin code for your custom needs. 16 | 17 | ## Clone the Repository 18 | 19 | Clone this repository to the desired location on your local system. If you're doing a site-wide plugin install, this will have to be a location accessible by everyone using the plugins. 20 | 21 | ## Config File 22 | 23 | Contained in `scripts/` you'll find a file called ```config_maya.py.example```. Make a copy of this file in the same directory, and rename it ```config_maya.py```. 24 | 25 | Edit ```config_maya.py``` in a Text Editor. It defines one config variable - `API_DIR` - the full path to your zync-python directory. 26 | 27 | Set `API_DIR` to point to the zync-python you installed earlier, save the file, and close it. 28 | 29 | ## zync.mod 30 | 31 | Now you'll need to point Maya to this folder to load it on startup. 32 | 33 | Create a file named `zync.mod` with the following contents: 34 | 35 | ``` 36 | + zync 1.0 Z:/path/to/plugins/zync-maya 37 | ``` 38 | 39 | This file can be placed anywhere within Maya's module search path. The module search path is defined by the `MAYA_MODULE_PATH` environment settings, as described in [the Maya docs](https://knowledge.autodesk.com/support/maya/learn-explore/caas/CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-228CCA33-4AFE-4380-8C3D-18D23F7EAC72-htm.html). 40 | 41 | You can view your `MAYA_MODULE_PATH` setting by running the following in the Maya Script Editor: 42 | 43 | ``` 44 | getenv MAYA_MODULE_PATH 45 | ``` 46 | 47 | Once `zync.mod` is in place, restart Maya. You should now see a "Zync" shelf with the Zync icon present. 48 | 49 | -------------------------------------------------------------------------------- /scripts/run_maya_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Runs a suite of Zync Maya unit tests. 3 | 4 | The Zync Maya unit tests must be run from within a Maya Python environment, 5 | so rather than running tests directly this script wraps mayapy. 6 | 7 | This script can run either the full set of sceneless tests, which do not 8 | require a Maya scene to be loaded, or it can run the scene-based tests 9 | on an individual Maya scene that is passed to it. 10 | 11 | If you are running a scene test, you must also provide --info-file, a 12 | JSON file containing the results you expect to get from get_scene_info in 13 | the main plugin. 14 | """ 15 | 16 | import argparse 17 | import ast 18 | import logging 19 | import os 20 | import platform 21 | import subprocess 22 | import tempfile 23 | 24 | 25 | def _get_maya_install_dir(maya_version): 26 | """Gets Maya install location.""" 27 | # No Windows support here at the moment. 28 | if platform.system() == 'Darwin': 29 | return '/Applications/Autodesk/maya%s/Maya.app/Contents/bin' % maya_version 30 | else: 31 | # Prefer Maya I/O if it's installed. 32 | if os.path.isdir('/usr/autodesk/mayaIO%s' % maya_version): 33 | return '/usr/autodesk/mayaIO%s/bin' % maya_version 34 | return '/usr/autodesk/maya%s/bin' % maya_version 35 | 36 | 37 | def _get_mayapy_path(maya_version): 38 | """Gets location of mayapy.""" 39 | return os.path.join(_get_maya_install_dir(maya_version), 'mayapy') 40 | 41 | 42 | def _get_maya_bin_path(maya_version): 43 | """Gets location of main Maya executable.""" 44 | return os.path.join(_get_maya_install_dir(maya_version), 'maya') 45 | 46 | 47 | def _get_scene_info_mel_script(renderer, layers, output_file): 48 | script_text = 'python("import zync_maya"); ' 49 | script_text += 'string $scene_info = python("zync_maya.get_scene_info(' 50 | script_text += '\'%s\', ' % renderer 51 | # List of layers being rendered comes in a comma-separated string, no need to join. 52 | script_text += '[\'%s\'], ' % layers 53 | script_text += 'False, [], [])"); ' 54 | script_text += 'string $output_file = "%s"; ' % output_file 55 | script_text += '$fp = `fopen $output_file "w"`; ' 56 | script_text += 'fprint $fp $scene_info; ' 57 | script_text += 'fclose $fp; ' 58 | return script_text 59 | 60 | 61 | def _clean_unicode_from_object(input_obj): 62 | """Returns a version of an object with all unicode replaced by standard strings. 63 | 64 | json.loads returns an object containing unicode values, this method helps us clean that 65 | for easier comparison. 66 | 67 | Args: 68 | input_obj: Python object to be cleaned - dict, list, str, etc. This function will 69 | recurse into that object to convert all nested unicode values. 70 | """ 71 | if isinstance(input_obj, dict): 72 | return {_clean_unicode_from_object(key): _clean_unicode_from_object(value) 73 | for key, value in input_obj.iteritems()} 74 | elif isinstance(input_obj, list): 75 | return [_clean_unicode_from_object(element) for element in input_obj] 76 | elif isinstance(input_obj, unicode): 77 | return input_obj.encode('utf-8') 78 | else: 79 | return input_obj 80 | 81 | 82 | def run_maya_and_get_scene_info(scene, renderer, layers, maya_version): 83 | # Write out a temporary MEL script which wraps the call to zync-maya. 84 | # We could use mayapy instead but mayapy has proven unreliable in initializing 85 | # its environment in the same way as standard maya. 86 | with tempfile.NamedTemporaryFile() as mel_script: 87 | # Maya produces a lot of output on startup that we don't have control over. 88 | # This output goes to both stdout & stderr and can differ based on what 89 | # plugins are installed and various other factors. In order to reliably 90 | # capture only the scene_info, we write it out to another temp file. 91 | scene_info_fd, scene_info_file = tempfile.mkstemp() 92 | mel_script.write(_get_scene_info_mel_script(renderer, layers, scene_info_file)) 93 | mel_script.flush() 94 | 95 | # Run Maya. This launches Maya, loads the scene file, runs our MEL wrapper 96 | # script, and exits. 97 | cmd = '%s -batch -script %s -file "%s"' % (_get_maya_bin_path(maya_version), 98 | mel_script.name, 99 | scene) 100 | p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, 101 | shell=True) 102 | out, err = p.communicate() 103 | if p.returncode: 104 | raise Exception( 105 | 'Maya failed to run. rc: [%d] stdout: [%s] stderr: [%s]' % (p.returncode, out, err)) 106 | 107 | # Read in the scene info from file and clean up. 108 | with os.fdopen(scene_info_fd) as fp: 109 | scene_info_raw = fp.read() 110 | os.remove(scene_info_file) 111 | 112 | try: 113 | scene_info_from_scene = _clean_unicode_from_object(ast.literal_eval(scene_info_raw)) 114 | except SyntaxError: 115 | print 'SyntaxError parsing scene_info.' 116 | print 'maya stdout: %s' % out 117 | print 'maya stderr: %s' % err 118 | raise 119 | 120 | return scene_info_from_scene 121 | 122 | 123 | def main(): 124 | logging.basicConfig( 125 | level=logging.INFO, 126 | format='%(asctime)s %(threadName)s %(module)s:%(lineno)d %(levelname)s %(message)s') 127 | 128 | parser = argparse.ArgumentParser(description=__doc__, 129 | formatter_class=argparse.RawTextHelpFormatter) 130 | parser.add_argument('--maya-version', required=True, help='Maya version number to test.') 131 | parser.add_argument('--scene', help='Path to the Maya scene to test.') 132 | parser.add_argument('--info-file', help='Path to JSON file containing expected scene info.') 133 | args = parser.parse_args() 134 | 135 | cmd = [_get_mayapy_path(args.maya_version), 136 | os.path.join(os.path.dirname(__file__), 'zync_maya_test.py')] 137 | if args.scene: 138 | cmd.extend(['--scene', args.scene, '--info-file', args.info_file]) 139 | 140 | if not os.path.exists(cmd[0]): 141 | raise RuntimeError('Maya %s is not installed on this system.' % args.maya_version) 142 | 143 | subprocess.check_call(cmd) 144 | 145 | 146 | if __name__ == '__main__': 147 | main() 148 | -------------------------------------------------------------------------------- /scripts/renderman_maya.py: -------------------------------------------------------------------------------- 1 | """ 2 | Zync Maya Plugin - Renderman 3 | """ 4 | 5 | import os 6 | import re 7 | from Queue import Queue 8 | 9 | import maya.cmds as cmds 10 | import maya.mel 11 | 12 | import maya_common 13 | 14 | 15 | class RendermanPre22Api(object): 16 | def get_version(self): 17 | return str(maya.mel.eval('rman getversion prman').split()[1]) 18 | 19 | def get_extension(self): 20 | return self._translate_format_to_extension( 21 | cmds.getAttr('rmanFinalOutputGlobals0.rman__riopt__Display_type')) 22 | 23 | def _translate_format_to_extension(self, image_format): 24 | """Translate an image format to the extension of files it 25 | generates. For example, "openexr" becomes "exr". 26 | 27 | Args: 28 | image_format: str, the image format 29 | 30 | Returns: 31 | str, the output extension. If the format is unrecognized, the 32 | original image format will be returned. 33 | """ 34 | # rman getPref returns a flat string where even items are format 35 | # names and odd indexes are file extensions. like: 36 | # "openexr exr softimage pic shader slo" 37 | formats_list = maya.mel.eval("rman getPref AssetnameExtTable;").split() 38 | # look for the format, then return the next item in the string 39 | # if the format isn't found, return it as is. 40 | try: 41 | format_index = formats_list.index(image_format) 42 | except ValueError: 43 | return image_format 44 | return formats_list[format_index+1] 45 | 46 | def get_output_dir(self): 47 | return maya.mel.eval('rmanGetDir rfmImages') 48 | 49 | def expand_string(self, str): 50 | return maya.mel.eval('rman subst "%s"' % str) 51 | 52 | 53 | class RendermanApi(object): 54 | def get_version(self): 55 | import rfm2 56 | return str(rfm2.config.cfg().build_info.version()) 57 | 58 | def get_extension(self): 59 | import rfm2 60 | display_dict = rfm2.api.displays.get_displays() 61 | main_display_path = rfm2.api.strings.expand_string(display_dict['displays']['beauty']['filePath']) 62 | return os.path.splitext(main_display_path)[1][1:] 63 | 64 | def get_output_dir(self): 65 | import rfm2 66 | return rfm2.api.scene.get_image_dir() 67 | 68 | def expand_string(self, str): 69 | import rfm2 70 | return rfm2.api.strings.expand_string(str) 71 | 72 | 73 | class Renderman(object): 74 | def __init__(self): 75 | self.camera = None 76 | self.layers_to_render = None 77 | self.renderman_api = None 78 | 79 | def init(self, layers_to_render, camera): 80 | self.layers_to_render = layers_to_render 81 | self.camera = camera 82 | 83 | def get_version(self): 84 | return self._get_api().get_version() 85 | 86 | def get_extension(self): 87 | return self._get_api().get_extension() 88 | 89 | def get_output_dir(self): 90 | return self._get_api().get_output_dir() 91 | 92 | def expand_string(self, str): 93 | return self._get_api().expand_string(str) 94 | 95 | def generate_files_from_tokenized_path(self, tokenizedPath): 96 | """Resolve all placeholders using Renderman function, but replace frame tags 97 | with wildcard, and resolve layers and camera.""" 98 | expandedPath = tokenizedPath 99 | expandedPath = expandedPath.replace('', '*') 100 | expandedPath = expandedPath.replace('', '*') 101 | expandedPath = expandedPath.replace('', '*') 102 | expandedPath = expandedPath.replace('', '*') 103 | expandedPath = expandedPath.replace('', '*') 104 | 105 | expandedPath = re.sub(maya_common._SUBSTITUTE_CAMERA_TOKEN_RE, self.camera, expandedPath) 106 | 107 | allPaths = [] 108 | if re.match(maya_common._HAS_LAYER_TOKEN_RE, expandedPath): 109 | for layer in self.layers_to_render: 110 | allPaths.append(re.sub(maya_common._SUBSTITUTE_LAYER_TOKEN_RE, layer, expandedPath)) 111 | else: 112 | allPaths.append(expandedPath) 113 | 114 | for path in allPaths: 115 | path = self.expand_string(path) 116 | import fnmatch 117 | for dirPath, dirs, files in os.walk(os.path.dirname(path)): 118 | for filename in fnmatch.filter(files, os.path.basename(path)): 119 | yield os.path.join(dirPath, filename) 120 | 121 | # Dependency detection 122 | def parse_rib_archives(self): 123 | queue = Queue() 124 | self._enqueue_rib_files(queue) 125 | if queue.empty(): 126 | return 127 | 128 | for file in self._process_rib_queue(queue): 129 | yield file 130 | 131 | if cmds.progressWindow(query=1, isCancelled=1): 132 | cmds.progressWindow(endProgress=1) 133 | raise maya_common.MayaZyncException("Submission cancelled") 134 | 135 | cmds.progressWindow(endProgress=1) 136 | 137 | def _enqueue_rib_files(self, queue): 138 | nodes = cmds.ls(type='RenderManArchive') 139 | for node in nodes: 140 | for file in self.ribArchive_handler(node): 141 | queue.put((file, node, 'rib')) 142 | 143 | def _process_rib_queue(self, queue): 144 | files_parsed = 0 145 | cmds.progressWindow(title='Parsing rib files for dependencies...', 146 | progress=files_parsed, maxValue=files_parsed + queue.qsize(), 147 | status='Parsing: %d of %d' % (files_parsed, files_parsed + queue.qsize()), isInterruptable=True) 148 | 149 | while not queue.empty() and not cmds.progressWindow(query=1, isCancelled=1): 150 | (file, node, file_type) = queue.get() 151 | files_parsed += 1 152 | cmds.progressWindow(edit=True, progress=files_parsed, maxValue=files_parsed + queue.qsize(), 153 | status='Parsing: %d of %d' % (files_parsed, files_parsed + queue.qsize())) 154 | 155 | scene_file = file.replace('\\', '/') 156 | print 'found file dependency from %s node %s: %s' % ('RenderManArchive', node, scene_file) 157 | yield scene_file 158 | 159 | if file_type == 'rib': 160 | for (f, t) in self._parse_rib_archive(file): 161 | queue.put((f, node, t)) 162 | 163 | def _parse_rib_archive(self, ribArchivePath): 164 | """Parses RIB archive file and tries to extract texture file names and other .rib files to parse. 165 | It read the file line by line with buffer limit, because the files can be very big. 166 | RIB files can be binary, in which case parsing them would be possible, but hacky, 167 | so we won't do that. 168 | We also check if the user has cancelled.""" 169 | 170 | fileSet = set() 171 | 172 | # Please see the link to easily see what those regex match: https://regex101.com/r/X1hBUJ/1 173 | patterns = [(r'\"((?:(?!\").)*?\.rib)\"', 'rib'), 174 | (r'\"string fileTextureName\" \[\"((?:(?!\").)*?)\"', 'tex'), 175 | (r'\"string lightColorMap\" \[\"((?:(?!\").)*?)\"', 'tex'), 176 | (r'\"string filename\" \[\"((?:(?!\").)*?)\"', 'tex')] 177 | with open(ribArchivePath, 'r') as content_file: 178 | line = content_file.readline(10000) 179 | while line != '' and not cmds.progressWindow(query=1, isCancelled=1): 180 | for (pattern, t) in patterns: 181 | for file in re.findall(pattern, line): 182 | if os.path.exists(file): 183 | fileSet.add((file, t)) 184 | line = content_file.readline(10000) 185 | 186 | for (f, t) in fileSet: 187 | yield (f, t) 188 | 189 | def _get_api(self): 190 | if self.renderman_api is not None: 191 | return self.renderman_api 192 | try: 193 | import rfm2 194 | self.renderman_api = RendermanApi() 195 | except ImportError as e: 196 | self.renderman_api = RendermanPre22Api() 197 | return self.renderman_api 198 | 199 | # Node handlers 200 | def ribArchive_handler(self, node): 201 | """Handles RIB archive nodes""" 202 | archive_path = cmds.getAttr('%s.filename' % node) 203 | for ribArchivePath in self.generate_files_from_tokenized_path(archive_path): 204 | yield ribArchivePath 205 | 206 | def pxrStdEnvMap_handler(self, node): 207 | """Handles PxrStdEnvMapLight nodes, up to Renderman 20""" 208 | filename = cmds.getAttr('%s.rman__EnvMap' % node) 209 | for expandedPath in self.generate_files_from_tokenized_path(filename): 210 | yield expandedPath 211 | 212 | def pxrTexture_handler(self, node): 213 | """Handles PxrTexture nodes""" 214 | filename = cmds.getAttr('%s.filename' % node) 215 | if cmds.getAttr('%s.atlasStyle' % node) != 0: 216 | filename = re.sub('_MAPID_', '*', filename) 217 | for expandedPath in self.generate_files_from_tokenized_path(filename): 218 | yield expandedPath 219 | 220 | def pxrMultiTexture_handler(self, node): 221 | """Handles PxrMultiTexture nodes""" 222 | for texture_id in range(0,10): 223 | filename = cmds.getAttr('%s.filename%d' % (node, texture_id)) 224 | if filename: 225 | for expandedPath in self.generate_files_from_tokenized_path(filename): 226 | yield expandedPath 227 | 228 | def pxrDomeLight_handler(self, node): 229 | """Handles PxrDomeLight nodes since Renderman 21""" 230 | filename = cmds.getAttr('%s.lightColorMap' % node) 231 | if filename: 232 | for expandedPath in self.generate_files_from_tokenized_path(filename): 233 | yield expandedPath 234 | 235 | def rmsEnvLight_handler(self, node): 236 | """Handles RMSEnvLight nodes""" 237 | filename = cmds.getAttr('%s.rman__EnvMap' % node) 238 | for expandedPath in self.generate_files_from_tokenized_path(filename): 239 | yield expandedPath 240 | 241 | def pxrPtexture_handler(self, node): 242 | filename = cmds.getAttr('%s.filename' % node) 243 | for expandedPath in self.generate_files_from_tokenized_path(filename): 244 | yield expandedPath 245 | 246 | def pxrNormalMap_handler(self, node): 247 | filename = cmds.getAttr('%s.filename' % node) 248 | for expandedPath in self.generate_files_from_tokenized_path(filename): 249 | yield expandedPath 250 | 251 | def generate_second_order_dependency_paths(self): 252 | for file in self.parse_rib_archives(): 253 | yield file 254 | -------------------------------------------------------------------------------- /scripts/zync_maya_test.py: -------------------------------------------------------------------------------- 1 | """Zync Maya Unit Tests. 2 | 3 | This script is not meant to be executed directly; it must be run via mayapy 4 | so the Maya Python environment is available to it. 5 | """ 6 | 7 | import argparse 8 | import json 9 | import os 10 | import sys 11 | import unittest 12 | 13 | import zync_maya 14 | import maya_common 15 | 16 | 17 | class TestMayaScene(unittest.TestCase): 18 | """Scene-based tests, acting on an individual scene which must be provided.""" 19 | scene_file = None 20 | info_file = None 21 | maya_cmds = None 22 | maya_mel = None 23 | 24 | def setUp(self): 25 | """ 26 | Import the maya api modules and initialize standalone if not already done. Ensure each test starts with a new file. 27 | """ 28 | if self.maya_cmds is None: 29 | import maya.standalone 30 | maya.standalone.initialize() 31 | import maya.cmds 32 | import maya.mel 33 | self.maya_cmds = maya.cmds 34 | self.maya_mel = maya.mel 35 | self.maya_cmds.file(f=True, new=True) 36 | 37 | def test_scene_info(self): 38 | if self.scene_file is None: 39 | raise unittest.SkipTest('scene_file is required to run this test.') 40 | with open(self.info_file) as fp: 41 | params = json.loads(fp.read())['params'] 42 | scene_info_master = _unicode_to_str(params['scene_info']) 43 | zync_maya.renderman.init(params['layers'].split(','), params['camera']) 44 | 45 | # Assume the structure is /scenes/. 46 | self.maya_cmds.workspace(directory=os.path.dirname(os.path.dirname(self.scene_file))) 47 | self.maya_cmds.file(self.scene_file, force=True, open=True, ignoreVersion=True, prompt=False) 48 | scene_info_from_scene = _unicode_to_str(zync_maya.get_scene_info( 49 | params['renderer'], params['layers'].split(','), False, [], 50 | zync_maya.parse_frame_range(params['frange']))) 51 | 52 | # Sort the file list from each set of scene info so we don't raise errors 53 | # caused only by file lists being in different orders. 54 | scene_info_from_scene['files'].sort() 55 | scene_info_master['files'].sort() 56 | 57 | # Be a bit less specific when checking renderer version. 58 | if 'arnold_version' in scene_info_from_scene: 59 | scene_info_from_scene['arnold_version'] = '.'.join( 60 | scene_info_from_scene['arnold_version'].split('.')[:2]) 61 | if 'arnold_version' in scene_info_master: 62 | scene_info_master['arnold_version'] = '.'.join( 63 | scene_info_master['arnold_version'].split('.')[:2]) 64 | 65 | if 'renderman_version' in scene_info_from_scene: 66 | scene_info_from_scene['renderman_version'] = ( 67 | scene_info_from_scene['renderman_version'].split('.')[0]) 68 | if 'renderman_version' in scene_info_master: 69 | scene_info_master['renderman_version'] = ( 70 | scene_info_master['renderman_version'].split('.')[0]) 71 | 72 | self.assertEqual(scene_info_from_scene, scene_info_master) 73 | 74 | def test_output_has_layer_problems(self): 75 | self.maya_cmds.loadPlugin('vrayformaya') 76 | renderer = 'vray' 77 | self.maya_cmds.setAttr("defaultRenderGlobals.currentRenderer", renderer, type="string") 78 | self.maya_mel.eval('vrayCreateVRaySettingsNode') 79 | prefix_attr = 'vraySettings.fileNamePrefix' 80 | 81 | # Test single layer and None fileNamePrefix 82 | layer_list = ['single_layer'] 83 | self.assertFalse(zync_maya.output_has_layer_problems(renderer, layer_list)) 84 | 85 | # Test multi-layer and None fileNamePrefix 86 | layer_list = ['foo', 'bar'] 87 | self.assertFalse(zync_maya.output_has_layer_problems(renderer, layer_list)) 88 | 89 | # Test multi-layer and non-layered fileNamePrefix 90 | self.maya_cmds.setAttr(prefix_attr, 'output_prefix_layer', type="string") 91 | self.assertTrue(zync_maya.output_has_layer_problems(renderer, layer_list)) 92 | 93 | # Test multi-layer and layered fileNamePrefix 94 | self.maya_cmds.setAttr(prefix_attr, 'output_prefix_path_', type="string") 95 | self.assertFalse(zync_maya.output_has_layer_problems(renderer, layer_list)) 96 | 97 | # Test multi-layer and layered fileNamePrefix 98 | self.maya_cmds.setAttr(prefix_attr, 'output_prefix_path_', type="string") 99 | self.assertFalse(zync_maya.output_has_layer_problems(renderer, layer_list)) 100 | 101 | # Test multi-layer and layered fileNamePrefix 102 | self.maya_cmds.setAttr(prefix_attr, 'output_prefix_path_%l', type="string") 103 | self.assertFalse(zync_maya.output_has_layer_problems(renderer, layer_list)) 104 | 105 | 106 | class TestMaya(unittest.TestCase): 107 | """Scene-less tests.""" 108 | 109 | def test_replace_attr_tokens(self): 110 | self.assertEqual( 111 | zync_maya._replace_attr_tokens('/path/to/textures//'), 112 | '/path/to/textures/*/*') 113 | self.assertEqual( 114 | zync_maya._replace_attr_tokens('/path/to/textures/texture01.jpg'), 115 | '/path/to/textures/texture01.jpg') 116 | with self.assertRaises(maya_common.MayaZyncException) as _: 117 | zync_maya._replace_attr_tokens('/') 118 | 119 | def test_maya_attr_is_true(self): 120 | self.assertEqual(zync_maya._maya_attr_is_true(True), True) 121 | self.assertEqual(zync_maya._maya_attr_is_true(False), False) 122 | self.assertEqual(zync_maya._maya_attr_is_true([True, True]), True) 123 | self.assertEqual(zync_maya._maya_attr_is_true([True, False]), True) 124 | self.assertEqual(zync_maya._maya_attr_is_true([False, False]), False) 125 | self.assertEqual(zync_maya._maya_attr_is_true(self._generate_true_vals()), True) 126 | self.assertEqual(zync_maya._maya_attr_is_true(self._generate_false_vals()), False) 127 | 128 | def _generate_true_vals(self): 129 | for i in range(3): 130 | yield True 131 | 132 | def _generate_false_vals(self): 133 | for i in range(3): 134 | yield False 135 | 136 | def test_parse_frame_range(self): 137 | self.assertEqual(zync_maya.parse_frame_range('92'), [92]) 138 | self.assertEqual(zync_maya.parse_frame_range('-5'), [-5]) 139 | self.assertEqual(zync_maya.parse_frame_range('23-26'), [23, 24, 25, 26]) 140 | self.assertEqual(zync_maya.parse_frame_range('-5--3'), [-5, -4, -3]) 141 | self.assertEqual(zync_maya.parse_frame_range('-1-2'), [-1, 0, 1, 2]) 142 | self.assertEqual(zync_maya.parse_frame_range('45-42'), [45, 44, 43, 42]) 143 | self.assertEqual(zync_maya.parse_frame_range('-97--99'), [-97, -98, -99]) 144 | self.assertEqual(zync_maya.parse_frame_range('1--2'), [1, 0, -1, -2]) 145 | self.assertEqual(zync_maya.parse_frame_range('1,57'), [1, 57]) 146 | self.assertEqual(zync_maya.parse_frame_range('5,23-25'), [5, 23, 24, 25]) 147 | with self.assertRaises(ValueError) as _: 148 | zync_maya.parse_frame_range('notAFrameRange') 149 | 150 | def test_extract_frame_num(self): 151 | self.assertEqual( 152 | zync_maya.extract_frame_number_from_file_path('/path/to/file.2763.exr'), 2763) 153 | self.assertEqual( 154 | zync_maya.extract_frame_number_from_file_path('/path/to/file.0001.exr'), 1) 155 | self.assertEqual( 156 | zync_maya.extract_frame_number_from_file_path('/path/to/singleFile.txt'), None) 157 | self.assertEqual( 158 | zync_maya.extract_frame_number_from_file_path('/path/to.2734.dir/file.png'), None) 159 | self.assertEqual( 160 | zync_maya.extract_frame_number_from_file_path('/path/to.2734.dir/file.9673.png'), 9673) 161 | self.assertEqual( 162 | zync_maya.extract_frame_number_from_file_path('/path/to/file_07.0283.exr'), 283) 163 | 164 | def test_submission_check(self): 165 | check = lambda: True 166 | true_check = zync_maya.SubmissionCheck(check=check, title='True check') 167 | self.assertTrue(true_check.run_check(show_confirmation=False)) 168 | 169 | check = lambda: False 170 | false_check = zync_maya.SubmissionCheck(check=check, title='False check') 171 | self.assertFalse(false_check.run_check(show_confirmation=False)) 172 | 173 | check = lambda: 'invalid type' 174 | exception_check = zync_maya.SubmissionCheck(check=check, title='Exception check') 175 | with self.assertRaises(maya_common.ZyncSubmissionCheckError): 176 | exception_check.run_check(show_confirmation=False) 177 | 178 | def test_replace_tokens_in_file_prefix(self): 179 | input = '%s__' 180 | scene_name = 'scene' 181 | layer = 'layer' 182 | camera = 'camera' 183 | expected = 'scene_layer_camera' 184 | self.assertEqual(zync_maya.replace_tokens_in_file_prefix(input, scene_name, layer, camera), expected) 185 | 186 | input = '_layer_camera' 187 | scene_name = 'scene' 188 | layer = 'null' 189 | camera = 'null' 190 | expected = 'scene_layer_camera' 191 | self.assertEqual(zync_maya.replace_tokens_in_file_prefix(input, scene_name, layer, camera), expected) 192 | 193 | input = 'camera_scene_layer' 194 | scene_name = 'null' 195 | layer = 'null' 196 | camera = 'null' 197 | expected = 'camera_scene_layer' 198 | self.assertEqual(zync_maya.replace_tokens_in_file_prefix(input, scene_name, layer, camera), expected) 199 | 200 | def test_replace_frame_number(self): 201 | test_input = "text_without_tokens.png" 202 | expected = "text_without_tokens.png" 203 | self.assertEqual(zync_maya.replace_frame_number(test_input, 123), expected) 204 | 205 | test_input = "test_#_path_##_with_###_multiple_####_tokens" 206 | expected = "test_123_path_123_with_123_multiple_0123_tokens" 207 | self.assertEqual(zync_maya.replace_frame_number(test_input, 123), expected) 208 | 209 | test_input = "test_#_with_##_all_###_zeroes_########" 210 | expected = "test_0_with_00_all_000_zeroes_00000000" 211 | self.assertEqual(zync_maya.replace_frame_number(test_input, 0), expected) 212 | 213 | test_input = "test" 214 | with self.assertRaises(ValueError) as _: 215 | zync_maya.replace_frame_number(test_input, -1) 216 | 217 | 218 | def _unicode_to_str(input_obj): 219 | """Returns a version of the input with all unicode replaced by standard 220 | strings. 221 | 222 | json.loads gives us unicode values, this method helps us clean that for 223 | easier comparison. 224 | 225 | Args: 226 | input_obj: whatever input you want to convert - dict, list, str, etc. will 227 | recurse into that object to convert all unicode values 228 | """ 229 | if isinstance(input_obj, dict): 230 | return {_unicode_to_str(key): _unicode_to_str(value) 231 | for key, value in input_obj.iteritems()} 232 | elif isinstance(input_obj, list): 233 | return [_unicode_to_str(element) for element in input_obj] 234 | elif isinstance(input_obj, unicode): 235 | return input_obj.encode('utf-8') 236 | else: 237 | return input_obj 238 | 239 | 240 | if __name__ == '__main__': 241 | parser = argparse.ArgumentParser(description=__doc__, 242 | formatter_class=argparse.RawTextHelpFormatter) 243 | parser.add_argument('--scene', help='Path to the Maya scene to test.') 244 | parser.add_argument('--info-file', help=('Path to JSON file containing ' 245 | 'expected scene information.')) 246 | args = parser.parse_args() 247 | 248 | if args.scene: 249 | if not args.info_file: 250 | print 'If you use --scene you must also use --info-file.' 251 | sys.exit(1) 252 | TestMayaScene.scene_file = args.scene 253 | TestMayaScene.info_file = args.info_file 254 | suite = unittest.TestSuite() 255 | suite.addTest(TestMayaScene('test_scene_info')) 256 | else: 257 | loader = unittest.TestLoader() 258 | suite = unittest.TestSuite() 259 | suite.addTests(loader.loadTestsFromTestCase(TestMaya)) 260 | suite.addTests(loader.loadTestsFromTestCase(TestMayaScene)) 261 | test_result = unittest.TextTestRunner().run(suite) 262 | 263 | # Since we're not using unittest.main, we need to manually provide an exit 264 | # code or the script will report 0 even if the test failed. mayapy is buggy 265 | # and its shutdown procedure will often cause stack traces and bad exit 266 | # codes even when tests were successful. os._exit circumvents the normal 267 | # shutdown process so we can focus on the actual test result. 268 | os._exit(not test_result.wasSuccessful()) 269 | -------------------------------------------------------------------------------- /scripts/resources/submit_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ZyncSubmitDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 550 10 | 890 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 550 22 | 1 23 | 24 | 25 | 26 | 27 | 700 28 | 890 29 | 30 | 31 | 32 | Dialog 33 | 34 | 35 | false 36 | 37 | 38 | 39 | 40 | 41 | 42 | 0 43 | 0 44 | 45 | 46 | 47 | Qt::ScrollBarAlwaysOff 48 | 49 | 50 | QAbstractScrollArea::AdjustToContents 51 | 52 | 53 | true 54 | 55 | 56 | 57 | 58 | 0 59 | 0 60 | 516 61 | 1127 62 | 63 | 64 | 65 | 66 | 0 67 | 0 68 | 69 | 70 | 71 | 72 | 460 73 | 0 74 | 75 | 76 | 77 | 78 | QLayout::SetDefaultConstraint 79 | 80 | 81 | 20 82 | 83 | 84 | 20 85 | 86 | 87 | 88 | 89 | QLayout::SetMinAndMaxSize 90 | 91 | 92 | 10 93 | 94 | 95 | 10 96 | 97 | 98 | 99 | 100 | 101 | 0 102 | 0 103 | 104 | 105 | 106 | 107 | 10 108 | 109 | 110 | 111 | 1 112 | 113 | 114 | `python "cmds.submit_callb('frame_step')"` 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | If selected, will perform a local scene export and render using the standalone version of the selected renderer. 124 | 125 | 126 | Use Standalone 127 | 128 | 129 | `python "cmds.submit_callb('use_standalone')"` 130 | 131 | 132 | 133 | 134 | 135 | 136 | Qt::Horizontal 137 | 138 | 139 | QSizePolicy::Fixed 140 | 141 | 142 | 143 | 80 144 | 20 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | If selected, will not parse .rib files for dependencies. 157 | 158 | 159 | Ignore second level dependencies 160 | 161 | 162 | `python "cmds.submit_callb('ignore_second_deps')"` 163 | 164 | 165 | 166 | 167 | 168 | 169 | Qt::Horizontal 170 | 171 | 172 | QSizePolicy::Fixed 173 | 174 | 175 | 176 | 80 177 | 20 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | Qt::Horizontal 190 | 191 | 192 | QSizePolicy::Fixed 193 | 194 | 195 | 196 | 80 197 | 20 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 10 209 | 210 | 211 | 212 | Camera: 213 | 214 | 215 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 216 | 217 | 218 | 219 | 220 | 221 | 222 | 6 223 | 224 | 225 | QLayout::SetDefaultConstraint 226 | 227 | 228 | 229 | 230 | Qt::Horizontal 231 | 232 | 233 | 234 | 80 235 | 20 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 10 245 | 246 | 247 | 248 | X 249 | 250 | 251 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 0 260 | 0 261 | 262 | 263 | 264 | 265 | 10 266 | 267 | 268 | 269 | 0 270 | 271 | 272 | `python "cmds.submit_callb('x_res')"` 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 10 281 | 282 | 283 | 284 | Y 285 | 286 | 287 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 0 296 | 0 297 | 298 | 299 | 300 | 301 | 10 302 | 303 | 304 | 305 | 0 306 | 307 | 308 | `python "cmds.submit_callb('y_res')"` 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 10 317 | 318 | 319 | 320 | Qt::Horizontal 321 | 322 | 323 | 324 | 240 325 | 20 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 10 337 | 338 | 339 | 340 | `python "cmds.submit_callb('camera')"` 341 | 342 | 343 | 344 | 345 | 346 | 347 | Existing Project: 348 | 349 | 350 | false 351 | 352 | 353 | `python "cmds.submit_callb('existing_project')"` 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | Ignore Missing Plugin Errors 363 | 364 | 365 | `python "cmds.submit_callb('ignore_plugin_errors')"` 366 | 367 | 368 | 369 | 370 | 371 | 372 | Qt::Horizontal 373 | 374 | 375 | QSizePolicy::Fixed 376 | 377 | 378 | 379 | 80 380 | 20 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | Zync will send you an email when this job completes, or if it fails. 393 | 394 | 395 | Notify on Job Completion 396 | 397 | 398 | `python "cmds.submit_callb('notify_complete')"` 399 | 400 | 401 | 402 | 403 | 404 | 405 | Qt::Horizontal 406 | 407 | 408 | QSizePolicy::Fixed 409 | 410 | 411 | 412 | 80 413 | 20 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 10 425 | 426 | 427 | 428 | Frame Step: 429 | 430 | 431 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 10 440 | 441 | 442 | 443 | Number of Tiles per Frame: 444 | 445 | 446 | Qt::AlignLeft|Qt::AlignTrailing|Qt::AlignVCenter 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 0 455 | 0 456 | 457 | 458 | 459 | 460 | 10 461 | 462 | 463 | 464 | 1 465 | 466 | 467 | `python "cmds.submit_callb('num_tiles')"` 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 16777215 476 | 16777215 477 | 478 | 479 | 480 | 481 | 10 482 | 483 | 484 | 485 | Num. Machines: 486 | 487 | 488 | Qt::AutoText 489 | 490 | 491 | false 492 | 493 | 494 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 495 | 496 | 497 | false 498 | 499 | 500 | false 501 | 502 | 503 | Qt::LinksAccessibleByMouse 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 16777215 512 | 16777215 513 | 514 | 515 | 516 | 517 | 10 518 | 519 | 520 | 521 | Job Priority: 522 | 523 | 524 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 525 | 526 | 527 | 528 | 529 | 530 | 531 | `python "cmds.submit_callb('renderer')"` 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 0 540 | 0 541 | 542 | 543 | 544 | 545 | 10 546 | 547 | 548 | 549 | `python "cmds.submit_callb('instance_type')"` 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 0 558 | 0 559 | 560 | 561 | 562 | 563 | 10 564 | 565 | 566 | 567 | 10 568 | 569 | 570 | `python "cmds.submit_callb('chunk_size')"` 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 10 579 | 580 | 581 | 582 | Chunk Size: 583 | 584 | 585 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 586 | 587 | 588 | 589 | 590 | 591 | 592 | Path to the output directory... 593 | 594 | 595 | `python "cmds.submit_callb('output_dir')"` 596 | 597 | 598 | 599 | 600 | 601 | 602 | `python "cmds.submit_callb('existing_project_name')"` 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 10 611 | 612 | 613 | 614 | Frame Range: 615 | 616 | 617 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 0 626 | 0 627 | 628 | 629 | 630 | 631 | 10 632 | 633 | 634 | 635 | Render Layers: 636 | 637 | 638 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | Lucida Grande 647 | 10 648 | 649 | 650 | 651 | Job Type: 652 | 653 | 654 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | Upload Only 664 | 665 | 666 | `python "cmds.submit_callb('upload_only')"` 667 | 668 | 669 | 670 | 671 | 672 | 673 | Qt::Horizontal 674 | 675 | 676 | QSizePolicy::Fixed 677 | 678 | 679 | 680 | 80 681 | 20 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 0 693 | 0 694 | 695 | 696 | 697 | 698 | 10 699 | 700 | 701 | 702 | 1001-1101 703 | 704 | 705 | `python "cmds.submit_callb('frange')"` 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 10 714 | 715 | 716 | 717 | Resolution: 718 | 719 | 720 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 0 729 | 0 730 | 731 | 732 | 733 | 734 | 16777215 735 | 16777215 736 | 737 | 738 | 739 | 740 | 10 741 | 742 | 743 | 744 | Parent ID: 745 | 746 | 747 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 0 758 | 0 759 | 760 | 761 | 762 | If selected Zync sill skip file syncing phase. Beware, it may result in outdated scene and/or missing dependencies during the rendering. 763 | 764 | 765 | Skip File Sync 766 | 767 | 768 | `python "cmds.submit_callb('skip_check')"` 769 | 770 | 771 | 772 | 773 | 774 | 775 | Qt::Horizontal 776 | 777 | 778 | QSizePolicy::Fixed 779 | 780 | 781 | 782 | 80 783 | 20 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 0 795 | 0 796 | 797 | 798 | 799 | Qt::Horizontal 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 40 810 | 16777215 811 | 812 | 813 | 814 | 50 815 | 816 | 817 | `python "cmds.submit_callb('priority')"` 818 | 819 | 820 | 821 | 822 | 823 | 824 | Qt::Horizontal 825 | 826 | 827 | 828 | 40 829 | 20 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 0 843 | 0 844 | 845 | 846 | 847 | 848 | 40 849 | 16777215 850 | 851 | 852 | 853 | `python "cmds.submit_callb('parent_id')"` 854 | 855 | 856 | 857 | 858 | 859 | 860 | Qt::Horizontal 861 | 862 | 863 | QSizePolicy::Expanding 864 | 865 | 866 | 867 | 120 868 | 20 869 | 870 | 871 | 872 | 873 | 874 | 875 | 876 | 877 | 878 | 879 | 10 880 | 881 | 882 | 883 | Output Directory: 884 | 885 | 886 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 887 | 888 | 889 | 890 | 891 | 892 | 893 | 894 | 0 895 | 0 896 | 897 | 898 | 899 | <a style="color:#ff8a00;" href="http://zync.cloudpricingcalculator.appspot.com">Cost Calculator</a> 900 | 901 | 902 | Qt::RichText 903 | 904 | 905 | true 906 | 907 | 908 | Qt::TextBrowserInteraction 909 | 910 | 911 | 912 | 913 | 914 | 915 | 916 | 10 917 | 918 | 919 | 920 | Renderer: 921 | 922 | 923 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 924 | 925 | 926 | 927 | 928 | 929 | 930 | 931 | 10 932 | 933 | 934 | 935 | Machine Type: 936 | 937 | 938 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 939 | 940 | 941 | 942 | 943 | 944 | 945 | 946 | 0 947 | 0 948 | 949 | 950 | 951 | 952 | 40 953 | 0 954 | 955 | 956 | 957 | 958 | 40 959 | 0 960 | 961 | 962 | 963 | 964 | 10 965 | 966 | 967 | 968 | 10 969 | 970 | 971 | `python "cmds.submit_callb('num_instances')"` 972 | 973 | 974 | 975 | 976 | 977 | 978 | 979 | 0 980 | 0 981 | 982 | 983 | 984 | 985 | 0 986 | 200 987 | 988 | 989 | 990 | QFrame::Plain 991 | 992 | 993 | Qt::ScrollBarAlwaysOn 994 | 995 | 996 | QAbstractScrollArea::AdjustIgnored 997 | 998 | 999 | true 1000 | 1001 | 1002 | 7 1003 | 1004 | 1005 | false 1006 | 1007 | 1008 | true 1009 | 1010 | 1011 | QAbstractItemView::MultiSelection 1012 | 1013 | 1014 | QListView::Adjust 1015 | 1016 | 1017 | QListView::Batched 1018 | 1019 | 1020 | false 1021 | 1022 | 1023 | true 1024 | 1025 | 1026 | 1027 | 1028 | 1029 | 1030 | 1031 | 10 1032 | 1033 | 1034 | 1035 | Project Folder: 1036 | 1037 | 1038 | Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter 1039 | 1040 | 1041 | 1042 | 1043 | 1044 | 1045 | New Project: 1046 | 1047 | 1048 | true 1049 | 1050 | 1051 | `python "cmds.submit_callb('new_project')"` 1052 | 1053 | 1054 | 1055 | 1056 | 1057 | 1058 | Path to the project... 1059 | 1060 | 1061 | `python "cmds.submit_callb('project')"` 1062 | 1063 | 1064 | 1065 | 1066 | 1067 | 1068 | 1069 | 0 1070 | 0 1071 | 1072 | 1073 | 1074 | Est. Cost per Hour: Not Available 1075 | 1076 | 1077 | 1078 | 1079 | 1080 | 1081 | 1082 | 1083 | Use Vray Nightly Build 1084 | 1085 | 1086 | `python "cmds.submit_callb('vray_nightly')"` 1087 | 1088 | 1089 | 1090 | 1091 | 1092 | 1093 | Qt::Horizontal 1094 | 1095 | 1096 | QSizePolicy::Fixed 1097 | 1098 | 1099 | 1100 | 80 1101 | 20 1102 | 1103 | 1104 | 1105 | 1106 | 1107 | 1108 | 1109 | 1110 | 1111 | `python "cmds.submit_callb('job_type')"` 1112 | 1113 | 1114 | 1115 | 1116 | 1117 | 1118 | New project name... 1119 | 1120 | 1121 | `python "cmds.submit_callb('new_project_name')"` 1122 | 1123 | 1124 | 1125 | 1126 | 1127 | 1128 | 1129 | 0 1130 | 0 1131 | 1132 | 1133 | 1134 | 1135 | 0 1136 | 0 1137 | 1138 | 1139 | 1140 | QFrame::NoFrame 1141 | 1142 | 1143 | QFrame::Plain 1144 | 1145 | 1146 | 0 1147 | 1148 | 1149 | 1150 | 0 1151 | 1152 | 1153 | 0 1154 | 1155 | 1156 | 0 1157 | 1158 | 1159 | 0 1160 | 1161 | 1162 | 1163 | 1164 | Select Extra Files 1165 | 1166 | 1167 | 1168 | 1169 | 1170 | 1171 | Qt::Horizontal 1172 | 1173 | 1174 | 1175 | 40 1176 | 20 1177 | 1178 | 1179 | 1180 | 1181 | 1182 | 1183 | 1184 | true 1185 | 1186 | 1187 | 1188 | 0 1189 | 0 1190 | 1191 | 1192 | 1193 | Select files 1194 | 1195 | 1196 | 1197 | 16 1198 | 16 1199 | 1200 | 1201 | 1202 | "cmds.select_files_callb()" 1203 | 1204 | 1205 | 1206 | 1207 | 1208 | 1209 | Qt::Horizontal 1210 | 1211 | 1212 | QSizePolicy::Expanding 1213 | 1214 | 1215 | 1216 | 153 1217 | 20 1218 | 1219 | 1220 | 1221 | 1222 | 1223 | sync_extra_assets 1224 | horizontalSpacer_7 1225 | select_files 1226 | horizontalSpacer_8 1227 | 1228 | 1229 | 1230 | 1231 | 1232 | 1233 | 1234 | Qt::Horizontal 1235 | 1236 | 1237 | 1238 | 1239 | 1240 | 1241 | 1242 | 1243 | 1244 | 0 1245 | 0 1246 | 1247 | 1248 | 1249 | 1250 | Lucida Grande 1251 | 12 1252 | 1253 | 1254 | 1255 | Login With Google 1256 | 1257 | 1258 | "cmds.login_with_google_callb()" 1259 | 1260 | 1261 | 1262 | 1263 | 1264 | 1265 | 1266 | 0 1267 | 0 1268 | 1269 | 1270 | 1271 | 1272 | 1273 | 1274 | Qt::AlignCenter 1275 | 1276 | 1277 | 1278 | 1279 | 1280 | 1281 | 1282 | Lucida Grande 1283 | 12 1284 | 1285 | 1286 | 1287 | Logout 1288 | 1289 | 1290 | "cmds.logout_callb()" 1291 | 1292 | 1293 | 1294 | 1295 | 1296 | 1297 | 1298 | 1299 | 0 1300 | 1301 | 1302 | 0 1303 | 1304 | 1305 | 1306 | 1307 | 1308 | 0 1309 | 0 1310 | 1311 | 1312 | 1313 | 1314 | Lucida Grande 1315 | 24 1316 | 1317 | 1318 | 1319 | Launch Job 1320 | 1321 | 1322 | "cmds.do_submit_callb()" 1323 | 1324 | 1325 | 1326 | 1327 | 1328 | 1329 | 1330 | 1331 | 1332 | 1333 | 1334 | 1335 | scrollArea 1336 | existing_project 1337 | existing_project_name 1338 | new_project 1339 | new_project_name 1340 | parent_id 1341 | priority 1342 | upload_only 1343 | skip_check 1344 | ignore_plugin_errors 1345 | project 1346 | output_dir 1347 | renderer 1348 | job_type 1349 | vray_nightly 1350 | use_standalone 1351 | num_tiles 1352 | ignore_second_deps 1353 | frange 1354 | frame_step 1355 | chunk_size 1356 | camera 1357 | layers 1358 | x_res 1359 | y_res 1360 | submit_button 1361 | 1362 | 1363 | 1364 | 1365 | -------------------------------------------------------------------------------- /scripts/zync_maya.py: -------------------------------------------------------------------------------- 1 | """ 2 | Zync Maya Plugin 3 | 4 | This Maya plugin implements the Zync Python API to provide an interface 5 | for launching Maya jobs on Zync. 6 | 7 | Depends on the zync-python Python API: 8 | 9 | https://github.com/zync/zync-python 10 | 11 | Usage: 12 | import zync_maya 13 | zync_maya.submit_dialog() 14 | 15 | """ 16 | 17 | __version__ = '1.5.13' 18 | 19 | 20 | import base64 21 | import copy 22 | import functools 23 | import glob 24 | import itertools 25 | import math 26 | import os 27 | import re 28 | import string 29 | import sys 30 | import traceback 31 | import types 32 | import webbrowser 33 | 34 | import maya_common 35 | import renderman_maya 36 | 37 | zync = None 38 | renderman = renderman_maya.Renderman() 39 | 40 | VRAY_ENGINE_NAME_CPU = 'cpu' # 0 41 | VRAY_ENGINE_NAME_OPENCL = 'opencl' # 1 42 | VRAY_ENGINE_NAME_CUDA = 'cuda' # 2 43 | VRAY_ENGINE_NAME_UNKNOWN = 'unknown' 44 | RENDER_LABEL_VRAY_CUDA = 'V-Ray (CUDA)' 45 | 46 | RENDERER_NAMES = { 47 | 'vray': 'V-Ray', 48 | 'arnold': 'Arnold', 49 | 'renderman': 'Renderman', 50 | 'redshift': 'Redshift', 51 | } 52 | 53 | TOKEN_TO_PATTERN_MAP = { 54 | '': '', 55 | '': '', 56 | '': '', 57 | '': '', 58 | '': '', 59 | 'u_v': '|', 60 | '_': '|', 61 | '', 62 | '': '', 63 | '': '', 65 | } 66 | 67 | REDSHIFT_CACHE_ATTRIBUTES = ['irradianceCacheFilename', 'irradiancePointCloudFilename', 'photonFilename', 'subsurfaceScatteringFilename'] 68 | REDSHIFT_OCIO_ATTRIBUTES = ['clrMgmtOcioFilename', 'lutFilename'] 69 | 70 | class NamePrefixAttributes(object): 71 | arnold = 'defaultRenderGlobals.imageFilePrefix' 72 | sw = 'defaultRenderGlobals.imageFilePrefix' 73 | mr = 'defaultRenderGlobals.imageFilePrefix' 74 | vray = 'vraySettings.fileNamePrefix' 75 | redshift = 'defaultRenderGlobals.imageFilePrefix' 76 | 77 | @classmethod 78 | def get_prefix(cls, renderer): 79 | return getattr(cls, renderer) 80 | 81 | 82 | def show_exceptions(func): 83 | """Error-showing decorator for all entry points 84 | 85 | Catches all exceptions and shows them on the screen and in console before 86 | re-raising. Uses `exception_already_shown` attribute to prevent showing 87 | the same exception twice. 88 | """ 89 | @functools.wraps(func) 90 | def wrapped(*args, **kwargs): 91 | try: 92 | return func(*args, **kwargs) 93 | except Exception as e: 94 | if not getattr(e, 'exception_already_shown', False): 95 | traceback.print_exc() 96 | cmds.confirmDialog(title='Error', message=unicode(e.message), button='OK', 97 | defaultButton='OK', icon='critical') 98 | e.exception_already_shown = True 99 | raise 100 | return wrapped 101 | 102 | # Importing zync-python is deferred until user's action (i.e. attempt 103 | # to open plugin window), because we are not able to reliably show message 104 | # windows any time earlier. Zync-python is not needed for plugin to load. 105 | @show_exceptions 106 | def import_zync_python(): 107 | """Imports zync-python""" 108 | global zync 109 | if zync: 110 | return 111 | 112 | if os.environ.get('ZYNC_API_DIR'): 113 | API_DIR = os.environ.get('ZYNC_API_DIR') 114 | else: 115 | config_path = os.path.join(os.path.dirname(__file__), 'config_maya.py') 116 | if not os.path.exists(config_path): 117 | raise maya_common.MayaZyncException( 118 | "Plugin configuration incomplete: zync-python path not provided.\n\n" 119 | "Re-installing the plugin may solve the problem.") 120 | import imp 121 | config_maya = imp.load_source('config_maya', config_path) 122 | API_DIR = config_maya.API_DIR 123 | if not isinstance(API_DIR, basestring): 124 | raise maya_common.MayaZyncException("API_DIR defined in config_maya.py is not a string") 125 | 126 | sys.path.append(API_DIR) 127 | import zync 128 | 129 | 130 | UI_FILE = '%s/resources/submit_dialog.ui' % (os.path.dirname(__file__),) 131 | 132 | _VERSION_CHECK_RESULT = None 133 | 134 | # a list of Xgen attributes which contain filenames we should include for upload 135 | _XGEN_FILE_ATTRS = [ 136 | 'files', 137 | 'wiresFile', 138 | 'cacheFileName', 139 | ] 140 | 141 | # Valid frame range regexes. 142 | # A single frame, e.g. 47, -4 143 | _SINGLE_FRAME_RE = re.compile(r'^(-?)\d+$') 144 | # A contiguous range of frames, e.g. 1-5, -3-2 145 | _FRAME_RANGE_RE = re.compile(r'^(?P(-?)\d+)-(?P(-?)\d+)$') 146 | 147 | # Regex for finding a frame number in a file path. 148 | _FRAME_NUMBER_RE = re.compile(r'.+\.(?P[0-9]+)\..+') 149 | 150 | # Regex string for checking if string contains a layer token. 151 | _HAS_LAYER_TOKEN_RE = re.compile(r'.*%l.*|.*.*|.*.*', re.IGNORECASE) 152 | _SUBSTITUTE_LAYER_TOKEN_RE = re.compile(r'%l||', re.IGNORECASE) 153 | _SUBSTITUTE_CAMERA_TOKEN_RE = re.compile(r'%c|', re.IGNORECASE) 154 | _SUBSTITUTE_SCENE_TOKEN_RE = re.compile(r'%s|', re.IGNORECASE) 155 | 156 | # Pairs of attributes which define possible Bifrost cache locations. 157 | _BIFROST_CACHE_PATH_ATTRS = ( 158 | ('guideCachePath', 'guideCacheFileName'), 159 | ('liquidCachePath', 'liquidCacheFileName'), 160 | ('liquidmeshCachePath', 'liquidmeshCacheFileName'), 161 | ('solidCachePath', 'solidCacheFileName'), 162 | ) 163 | 164 | 165 | _XGEN_IMPORT_ERROR = None 166 | _RENDERSETUP_IMPORT_ERROR = None 167 | 168 | import maya.cmds as cmds 169 | import maya.mel 170 | import maya.utils 171 | # Attempt to import Xgen API. Log error on failure but continue, in 172 | # case of older Maya version or if Xgen is simply unavailable for 173 | # some reason. 174 | try: 175 | import xgenm 176 | except ImportError as e: 177 | _XGEN_IMPORT_ERROR = str(e) 178 | print 'Error loading Xgen API: %s' % _XGEN_IMPORT_ERROR 179 | # Only newer versions of Maya have the Render Setup API. 180 | try: 181 | import maya.app.renderSetup.model.renderSetup as renderSetup 182 | except (ImportError, RuntimeError) as e: 183 | _RENDERSETUP_IMPORT_ERROR = str(e) 184 | print 'Error loading Render Setup API: %s' % _RENDERSETUP_IMPORT_ERROR 185 | 186 | 187 | def eval_ui(path, ui_type='textField', **kwargs): 188 | """ 189 | Returns the value from the given ui element. 190 | """ 191 | return getattr(cmds, ui_type)(path, query=True, **kwargs) 192 | 193 | 194 | def proj_dir(): 195 | """ 196 | Returns the Maya project directory of the current scene. 197 | """ 198 | return cmds.workspace(q=True, rd=True) 199 | 200 | 201 | def frame_range(): 202 | """ 203 | Returns the frame-range of the maya scene as a string, like: 204 | 1001-1350 205 | """ 206 | start = str(int(cmds.getAttr('defaultRenderGlobals.startFrame'))) 207 | end = str(int(cmds.getAttr('defaultRenderGlobals.endFrame'))) 208 | return '%s-%s' % (start, end) 209 | 210 | 211 | def get_render_layers(): 212 | """Get a list of all render layers in the scene.""" 213 | layers = [] 214 | try: 215 | all_layers = cmds.ls(type='renderLayer', showNamespace=True) 216 | for i in range(0, len(all_layers), 2): 217 | if all_layers[i+1] == ':': 218 | layers.append(all_layers[i]) 219 | except Exception: 220 | layers = cmds.ls(type='renderLayer') 221 | return layers 222 | 223 | 224 | def udim_range(): 225 | bake_sets = list(bake_set for bake_set in cmds.ls(type='VRayBakeOptions') \ 226 | if bake_set != 'vrayDefaultBakeOptions') 227 | u_max = 0 228 | v_max = 0 229 | for bake_set in bake_sets: 230 | conn_list = cmds.listConnections(bake_set) 231 | if conn_list == None or len(conn_list) == 0: 232 | continue 233 | uv_info = cmds.polyEvaluate(conn_list[0], b2=True) 234 | if uv_info[0][1] > u_max: 235 | u_max = int(math.ceil(uv_info[0][1])) 236 | if uv_info[1][1] > v_max: 237 | v_max = int(math.ceil(uv_info[1][1])) 238 | return '1001-%d' % (1001+u_max+(10*v_max)) 239 | 240 | 241 | def seq_to_glob(in_path): 242 | """Takes an image sequence path and returns it in glob format. 243 | 244 | Any frame numbers or other tokens will be replaced by a '*'. 245 | Image sequences may be numerical sequences, e.g. /path/to/file.1001.exr 246 | will return as /path/to/file.*.exr. Image sequences may also use tokens to 247 | denote sequences, e.g. /path/to/texture..tif will return as 248 | /path/to/texture.*.tif. 249 | 250 | Args: 251 | in_path: str, the image sequence path 252 | 253 | Returns: 254 | String, the new path, subbed with any needed wildcards. 255 | """ 256 | if in_path is None: 257 | return in_path 258 | in_path = _replace_attr_tokens(in_path) 259 | in_path = re.sub('', '*', in_path, flags=re.IGNORECASE) 260 | 261 | found_token = False 262 | if '#' in in_path: 263 | in_path = re.sub('#+', '*', in_path, flags=re.IGNORECASE) 264 | found_token = True 265 | for token in TOKEN_TO_PATTERN_MAP: 266 | if token in in_path.lower() and TOKEN_TO_PATTERN_MAP[token] is not None: 267 | in_path = re.sub(TOKEN_TO_PATTERN_MAP[token], '*', in_path, flags=re.IGNORECASE) 268 | found_token = True 269 | if found_token: 270 | return in_path 271 | 272 | head = os.path.dirname(in_path) 273 | base = os.path.basename(in_path) 274 | matches = list(re.finditer(r'\d+', base)) 275 | if matches: 276 | match = matches[-1] 277 | new_base = '%s*%s' % (base[:match.start()], base[match.end():]) 278 | return '%s/%s' % (head, new_base) 279 | else: 280 | return in_path 281 | 282 | 283 | def _replace_attr_tokens(path): 284 | if not path: 285 | return path 286 | glob_path = re.sub(r'', '*', path, flags=re.IGNORECASE) 287 | if not re.search(r'[^/*]', glob_path): 288 | raise maya_common.MayaZyncException( 289 | 'A file path using attr: tags resolved to %s, which is too wide. ' 290 | 'Please use attr: tags only for portions of the file path to limit the ' 291 | 'potential matches for these paths; this will help both Arnold and ' 292 | 'Zync locate these files.' % glob_path) 293 | return glob_path 294 | 295 | 296 | def get_file_node_path(node): 297 | """Get the file path used by a Maya file node. 298 | Args: 299 | node: str, name of the Maya file node 300 | Returns: 301 | str, the file path in use 302 | """ 303 | # if the path appears to be sequence, use computedFileTextureNamePattern, 304 | # this preserves the <> tag 305 | if cmds.attributeQuery('computedFileTextureNamePattern', node=node, exists=True): 306 | textureNamePattern = cmds.getAttr('%s.computedFileTextureNamePattern' % node) 307 | if any(token in textureNamePattern.lower() for token in TOKEN_TO_PATTERN_MAP): 308 | return cmds.getAttr('%s.computedFileTextureNamePattern' % node) 309 | # otherwise use fileTextureName 310 | return cmds.getAttr('%s.fileTextureName' % node) 311 | 312 | 313 | def node_uses_image_sequence(node): 314 | """Determine if a node uses an image sequence or just a single image, 315 | not always obvious from its file path alone. 316 | Args: 317 | node: str, name of the Maya node 318 | Returns: 319 | bool, True if node uses an image sequence 320 | """ 321 | # useFrameExtension indicates an explicit image sequence 322 | # a token implies a sequence 323 | node_path = get_file_node_path(node).lower() 324 | return (cmds.getAttr('%s.useFrameExtension' % node) == True or 325 | any(token in node_path for token in TOKEN_TO_PATTERN_MAP)) 326 | 327 | 328 | def _get_layer_overrides(attr): 329 | """Gets any files set in layer overrides linked to the given attribute. 330 | 331 | Args: 332 | attr: str, Maya attribute name, like file1.fileTextureName 333 | 334 | Yields: 335 | the value of any render layer overrides. this can be a str, 336 | int, float - it depends on what type the attr itself is. 337 | """ 338 | connections = cmds.listConnections(attr, plugs=True) 339 | # listConnections can return None if there are no connections 340 | if connections: 341 | for connection in connections: 342 | # listConnections gives us any "plugs" which are connected to 343 | # the attribute. a plug represents the connection, not the actual 344 | # value of the override. a plug is a str, like: 345 | # layer1.adjustments[1].plug 346 | if connection: 347 | # we only care when the plug refers to a render layer, as it 348 | # will represent a render layer override. 349 | node_name = connection.split('.')[0] 350 | if cmds.nodeType(node_name) == 'renderLayer': 351 | # turn the plug name into a value name, which looks 352 | # like: layer1.adjustments[1].value 353 | attr_name = '%s.value' % '.'.join(connection.split('.')[:-1]) 354 | yield cmds.getAttr(attr_name) 355 | 356 | 357 | def _file_handler(node): 358 | """Returns the file referenced by a Maya file node. Returned files may 359 | contain wildcards when they reference image sequences, for example an 360 | animated texture node, or a path containing token.""" 361 | texture_path = get_file_node_path(node) 362 | # if the node is an image sequence, transform the path into a 363 | # glob-style path, i.e. using * in place of any sequence number 364 | # or token. this will match what's provided via the file list 365 | # in the job's scene_info, so we can properly path swap 366 | if node_uses_image_sequence(node): 367 | texture_path = seq_to_glob(texture_path) 368 | yield texture_path 369 | # if the Arnold "Use .tx" flag is on, look for a .tx version 370 | # of the texture as well 371 | try: 372 | if cmds.getAttr('defaultArnoldRenderOptions.use_existing_tiled_textures'): 373 | head, _ = os.path.splitext(texture_path) 374 | yield '%s.tx' % head 375 | except: 376 | pass 377 | # look for layer overrides set on the path 378 | for override_path in _get_layer_overrides('%s.fileTextureName' % node): 379 | yield override_path 380 | 381 | 382 | def _cache_file_handler(node): 383 | """Returns the files references by the given cacheFile node""" 384 | path = cmds.getAttr('%s.cachePath' % node) 385 | cache_name = cmds.getAttr('%s.cacheName' % node) 386 | 387 | yield '%s/%s*.mc' % (path, cache_name) 388 | yield '%s/%s*.mcc' % (path, cache_name) 389 | yield '%s/%s*.mcx' % (path, cache_name) 390 | yield '%s/%s.xml' % (path, cache_name) 391 | 392 | 393 | def _diskCache_handler(node): 394 | """Given a diskCache node, returns path of cache file it 395 | references. 396 | 397 | Args: 398 | node: str, name of diskCache node 399 | 400 | Yields: 401 | tuple of str, paths referenced 402 | """ 403 | cache_name = cmds.getAttr('%s.cacheName' % node) 404 | # if its an absolute path we're done, otherwise we need to resolve it 405 | # via project settings 406 | if os.path.isabs(cache_name): 407 | yield cache_name 408 | else: 409 | disk_cache_dir = cmds.workspace(fileRuleEntry='diskCache') 410 | if not disk_cache_dir: 411 | print 'WARNING: disk cache path not found. assuming data/' 412 | disk_cache_dir = 'data' 413 | # resolve relative paths with the main project path 414 | if not os.path.isabs(disk_cache_dir): 415 | disk_cache_dir = os.path.join(cmds.workspace(q=True, rd=True), 416 | disk_cache_dir) 417 | yield os.path.join(disk_cache_dir, cache_name) 418 | 419 | 420 | def _vrmesh_handler(node): 421 | """Handles vray meshes""" 422 | yield cmds.getAttr('%s.fileName' % node) 423 | 424 | 425 | def _mrtex_handler(node): 426 | """Handles mentalrayTexutre nodes""" 427 | yield cmds.getAttr('%s.fileTextureName' % node) 428 | 429 | 430 | def _gpu_handler(node): 431 | """Handles gpuCache nodes""" 432 | yield cmds.getAttr('%s.cacheFileName' % node) 433 | 434 | 435 | def _mrOptions_handler(node): 436 | """Handles mentalrayOptions nodes, for Final Gather""" 437 | mapName = cmds.getAttr('%s.finalGatherFilename' % node).strip() 438 | if mapName != "": 439 | path = cmds.workspace(q=True, rd=True) 440 | if path[-1] != "/": 441 | path += "/" 442 | path += "renderData/mentalray/finalgMap/" 443 | path += mapName 444 | #if not mapName.endswith(".fgmap"): 445 | # path += ".fgmap" 446 | path += "*" 447 | yield path 448 | 449 | 450 | def _mrIbl_handler(node): 451 | """Handles mentalrayIblShape nodes""" 452 | yield cmds.getAttr('%s.texture' % node) 453 | 454 | 455 | def _abc_handler(node): 456 | """Handles AlembicNode nodes""" 457 | yield cmds.getAttr('%s.abc_File' % node) 458 | 459 | 460 | def _vrSettings_handler(node): 461 | """Handles VRaySettingsNode nodes, for irradiance map""" 462 | irmap = cmds.getAttr('%s.ifile' % node) 463 | if cmds.getAttr('%s.imode' % node) == 7: 464 | if irmap.find('.') == -1: 465 | irmap += '*' 466 | else: 467 | last_dot = irmap.rfind('.') 468 | irmap = '%s*%s' % (irmap[:last_dot], irmap[last_dot:]) 469 | yield irmap 470 | yield cmds.getAttr('%s.fnm' % node) 471 | 472 | 473 | def _particle_handler(node): 474 | project_dir = cmds.workspace(q=True, rd=True) 475 | if project_dir[-1] == '/': 476 | project_dir = project_dir[:-1] 477 | if node.find('|') == -1: 478 | node_base = node 479 | else: 480 | node_base = node.split('|')[-1] 481 | path = None 482 | try: 483 | startup_cache = cmds.getAttr('%s.scp' % (node,)).strip() 484 | if startup_cache in (None, ''): 485 | path = None 486 | else: 487 | path = '%s/particles/%s/%s*' % (project_dir, startup_cache, node_base) 488 | except: 489 | path = None 490 | if path == None: 491 | scene_base, ext = os.path.splitext(os.path.basename(cmds.file(q=True, loc=True))) 492 | path = '%s/particles/%s/%s*' % (project_dir, scene_base, node_base) 493 | yield path 494 | 495 | 496 | def _ies_handler(node): 497 | """Handles VRayLightIESShape nodes, for IES lighting files""" 498 | yield cmds.getAttr('%s.iesFile' % node) 499 | 500 | 501 | def _fur_handler(node): 502 | """Handles FurDescription nodes""" 503 | # 504 | # Find all "Map" attributes and see if they have stored file paths. 505 | # 506 | for attr in cmds.listAttr(node): 507 | if attr.find('Map') != -1 and cmds.attributeQuery(attr, node=node, at=True) == 'typed': 508 | index_list = ['0', '1'] 509 | for index in index_list: 510 | try: 511 | map_path = cmds.getAttr('%s.%s[%s]' % (node, attr, index)) 512 | if map_path != None and map_path != '': 513 | yield map_path 514 | except: 515 | pass 516 | 517 | 518 | def _ptex_handler(node): 519 | """Handles Mental Ray ptex nodes""" 520 | yield cmds.getAttr('%s.S00' % node) 521 | 522 | 523 | def _substance_handler(node): 524 | """Handles Vray Substance nodes""" 525 | yield cmds.getAttr('%s.p' % node) 526 | 527 | 528 | def _imagePlane_handler(node): 529 | """Handles Image Planes""" 530 | # only return the path if the display mode is NOT set to "None" 531 | if cmds.getAttr('%s.displayMode' % (node,)) != 0: 532 | texture_path = cmds.getAttr('%s.imageName' % (node,)) 533 | try: 534 | if cmds.getAttr('%s.useFrameExtension' % (node,)) == True: 535 | yield seq_to_glob(texture_path) 536 | else: 537 | yield texture_path 538 | except: 539 | yield texture_path 540 | 541 | 542 | def _mesh_handler(node): 543 | """Handles Mesh nodes, in case they are using MR Proxies""" 544 | for attr in ['%s.miProxyFile', '%s.rman__param___draFile']: 545 | try: 546 | proxy_path = cmds.getAttr(attr % node) 547 | if proxy_path != None: 548 | yield proxy_path 549 | except: 550 | pass 551 | 552 | 553 | def _dynGlobals_handler(node): 554 | """Handles dynGlobals nodes""" 555 | project_dir = cmds.workspace(q=True, rd=True) 556 | if project_dir[-1] == '/': 557 | project_dir = project_dir[:-1] 558 | cache_dir = cmds.getAttr('%s.cd' % (node,)) 559 | if cache_dir not in (None, ''): 560 | path = '%s/particles/%s/*' % (project_dir, cache_dir.strip()) 561 | yield path 562 | 563 | 564 | def _aiStandIn_handler(node): 565 | """Handles aiStandIn nodes""" 566 | path = cmds.getAttr('%s.dso' % (node,)) 567 | # change frame reference to wildcard pattern 568 | yield seq_to_glob(path) 569 | 570 | 571 | def _aiImage_handler(node): 572 | """Handles aiImage nodes""" 573 | filename = cmds.getAttr('%s.filename' % node) 574 | yield seq_to_glob(filename) 575 | 576 | 577 | def _aiPhotometricLight_handler(node): 578 | """Handles aiPhotometricLight nodes""" 579 | yield cmds.getAttr('%s.aiFilename' % node) 580 | 581 | 582 | def _exocortex_handler(node): 583 | """Handles Exocortex Alembic nodes""" 584 | yield cmds.getAttr('%s.fileName' % node) 585 | 586 | 587 | def _vrayPtex_handler(node): 588 | yield cmds.getAttr('%s.ptexFile' % node) 589 | 590 | 591 | def _vrayVolumeGrid_handler(node): 592 | path = cmds.getAttr('%s.if' % node) 593 | yield seq_to_glob(path) 594 | 595 | 596 | def _vrayScene_handler(node): 597 | vrscene_path = cmds.getAttr('%s.fPath' % node) 598 | yield vrscene_path 599 | # Scan the .vrscene file for dependencies buried within. 600 | # If the file does not exist we skip the scan but still report the main 601 | # .vrscene file dependency to Zync, this is to allow Zync's default 602 | # missing file detection to kick in when the job runs. 603 | if os.path.exists(vrscene_path): 604 | with open(vrscene_path) as fp: 605 | for vrscene_line in fp: 606 | # Files in the .vrscene are attached to nodes like this: 607 | # BitmapBuffer bitmapBuffer_1 { 608 | # file="/path/to/file.exr"; 609 | vrscene_line = vrscene_line.strip() 610 | if vrscene_line.startswith('file='): 611 | file_path = '='.join(vrscene_line.split('=')[1:]) 612 | file_path = file_path.strip(';') 613 | file_path = file_path.strip('\'"') 614 | yield file_path 615 | 616 | 617 | def _openVDBRead_handler(node): 618 | """Handles OpenVDBRead nodes""" 619 | yield cmds.getAttr('%s.file' % node) 620 | 621 | 622 | def _aiVolume_handler(node): 623 | """Handles aiVolume nodes, Arnold volume grid files.""" 624 | yield seq_to_glob(cmds.getAttr('%s.filename' % node)) 625 | 626 | 627 | def _mash_handler(node): 628 | archive_paths = cmds.getAttr('%s.ribArchives' % node) 629 | if archive_paths: 630 | for archive_path in archive_paths.split(','): 631 | yield archive_path 632 | 633 | 634 | def _mashAudio_handler(node): 635 | yield cmds.getAttr('%s.filename' % node) 636 | 637 | 638 | def _bifrost_handler(frames_to_render, bifrost_container): 639 | cache_paths = set() 640 | for cache_path_attr, cache_name_attr in _BIFROST_CACHE_PATH_ATTRS: 641 | container_attrs = cmds.listAttr(bifrost_container) 642 | if cache_path_attr in container_attrs and cache_name_attr in container_attrs: 643 | cache_path = cmds.getAttr('%s.%s' % (bifrost_container, cache_path_attr)) 644 | cache_name = cmds.getAttr('%s.%s' % (bifrost_container, cache_name_attr)) 645 | if cache_path and cache_name: 646 | cache_paths.add(os.path.join(cache_path, cache_name)) 647 | 648 | for cache_directory in cache_paths: 649 | for cache_file in glob.glob('%s/*/*' % cache_directory): 650 | frame_num = extract_frame_number_from_file_path(cache_file) 651 | if frame_num is None: 652 | yield cache_file 653 | else: 654 | if frame_num in frames_to_render: 655 | yield cache_file 656 | 657 | 658 | def get_redshift_version(): 659 | return str(cmds.pluginInfo('redshift4maya', query=True, version=True)) 660 | 661 | 662 | def generate_redshift_asset_paths(): 663 | for path in cmds.file(q=True, list=True): 664 | yield seq_to_glob(path) 665 | 666 | for path in generate_redshift_layer_overriden_paths('RedshiftOptions', REDSHIFT_CACHE_ATTRIBUTES): 667 | yield path 668 | 669 | for path in generate_redshift_layer_overriden_paths('RedshiftPostEffects', REDSHIFT_OCIO_ATTRIBUTES): 670 | if path: 671 | # OCIO files 672 | if path.lower().endswith('.ocio'): 673 | for ocio_file in zync.get_ocio_files(path): 674 | yield ocio_file 675 | else: 676 | yield path 677 | 678 | 679 | def generate_redshift_layer_overriden_paths(node_type, attributes): 680 | for node in cmds.ls(type=node_type): 681 | for attr in attributes: 682 | for layer in get_render_layers(): 683 | path = get_layer_override(layer, 'redshift', '%s.%s' % (node, attr)) 684 | yield seq_to_glob(path) 685 | 686 | 687 | def generate_redshift_second_order_dependency_paths(frame_numbers): 688 | for proxy_node in cmds.ls(type='RedshiftProxyMesh'): 689 | proxy_path = cmds.getAttr('%s.computedFileNamePattern' % proxy_node) 690 | if cmds.getAttr('%s.useFrameExtension' % proxy_node): 691 | proxy_paths = [replace_frame_number(proxy_path, frame) for frame in frame_numbers] 692 | else: 693 | proxy_paths = [proxy_path] 694 | for path in proxy_paths: 695 | for proxy_asset in maya.mel.eval('rsProxy -q -dependencies "%s"' % path): 696 | yield _clean_path(seq_to_glob(proxy_asset)) 697 | 698 | 699 | def replace_frame_number(path, frame_number): 700 | frame_number = int(frame_number) 701 | if frame_number < 0: 702 | raise ValueError("Frame number must be non-negative") 703 | 704 | def split_by_token(_path): 705 | return re.split('(#+)', _path) 706 | 707 | def is_token(_part): 708 | return '#' in _part 709 | 710 | def replace_token(_path, _frame_number): 711 | _frame_number = str(_frame_number) 712 | if len(_frame_number) >= len(_path): 713 | return _frame_number 714 | num_of_zeroes = len(_path) - len(_frame_number) 715 | return (num_of_zeroes*'0') + _frame_number 716 | 717 | parts = split_by_token(path) 718 | final_path = "" 719 | for part in parts: 720 | final_part = part 721 | if is_token(part): 722 | final_part = replace_token(part, frame_number) 723 | final_path += final_part 724 | return final_path 725 | 726 | 727 | def get_scene_files(frames_to_render, renderer): 728 | generator = itertools.chain() 729 | # Generate asset paths 730 | if renderer == 'redshift': 731 | generator = itertools.chain(generator, generate_redshift_asset_paths()) 732 | else: 733 | generator = itertools.chain(generator, generate_asset_paths(frames_to_render)) 734 | 735 | # Generate recursive dependency paths 736 | if not eval_ui('ignore_second_deps', 'checkBox', v=True): 737 | if renderer == 'renderman': 738 | generator = itertools.chain(generator, renderman.generate_second_order_dependency_paths()) 739 | elif renderer == 'redshift': 740 | generator = itertools.chain(generator, generate_redshift_second_order_dependency_paths(frames_to_render)) 741 | 742 | # Generate xgen paths 743 | generator = itertools.chain(generator, generate_xgen_paths()) 744 | 745 | # Handle OCIO dependencies 746 | generator = itertools.chain(generator, generate_ocio_files()) 747 | return list(generator) 748 | 749 | 750 | def generate_asset_paths(frames_to_render): 751 | """Returns all of the files being used by the scene""" 752 | file_types = { 753 | 'file': _file_handler, 754 | 'cacheFile': _cache_file_handler, 755 | 'diskCache': _diskCache_handler, 756 | 'VRayMesh': _vrmesh_handler, 757 | 'mentalrayTexture': _mrtex_handler, 758 | 'gpuCache': _gpu_handler, 759 | 'mentalrayOptions': _mrOptions_handler, 760 | 'mentalrayIblShape': _mrIbl_handler, 761 | 'AlembicNode': _abc_handler, 762 | 'VRaySettingsNode': _vrSettings_handler, 763 | 'particle': _particle_handler, 764 | 'VRayLightIESShape': _ies_handler, 765 | 'FurDescription': _fur_handler, 766 | 'mib_ptex_lookup': _ptex_handler, 767 | 'substance': _substance_handler, 768 | 'imagePlane': _imagePlane_handler, 769 | 'mesh': _mesh_handler, 770 | 'dynGlobals': _dynGlobals_handler, 771 | 'aiStandIn': _aiStandIn_handler, 772 | 'aiImage': _aiImage_handler, 773 | 'aiPhotometricLight': _aiPhotometricLight_handler, 774 | 'ExocortexAlembicFile': _exocortex_handler, 775 | 'VRayPtex': _vrayPtex_handler, 776 | 'VRayVolumeGrid': _vrayVolumeGrid_handler, 777 | 'VRayScene': _vrayScene_handler, 778 | 'RenderManArchive': renderman.ribArchive_handler, 779 | 'PxrStdEnvMapLight': renderman.pxrStdEnvMap_handler, 780 | 'PxrDomeLight': renderman.pxrDomeLight_handler, 781 | 'PxrTexture': renderman.pxrTexture_handler, 782 | 'PxrBump': renderman.pxrTexture_handler, # PxrBump and PxrTexture are identical. 783 | 'PxrMultiTexture': renderman.pxrMultiTexture_handler, 784 | 'PxrDomeLight': renderman.pxrDomeLight_handler, 785 | 'RMSEnvLight': renderman.rmsEnvLight_handler, 786 | 'PxrPtexture': renderman.pxrPtexture_handler, 787 | 'PxrNormalMap': renderman.pxrNormalMap_handler, 788 | 'OpenVDBRead': _openVDBRead_handler, 789 | 'aiVolume': _aiVolume_handler, 790 | 'MASH_Waiter': _mash_handler, 791 | 'MASH_Audio': _mashAudio_handler, 792 | 'bifrostContainer': functools.partial(_bifrost_handler, frames_to_render), 793 | } 794 | 795 | for file_type in file_types: 796 | handler = file_types.get(file_type) 797 | nodes = cmds.ls(type=file_type) 798 | for node in nodes: 799 | for scene_file in handler(node): 800 | if scene_file: 801 | scene_file = scene_file.replace('\\', '/') 802 | print 'found file dependency from %s node %s: %s' % (file_type, node, scene_file) 803 | yield scene_file 804 | 805 | 806 | def generate_xgen_paths(): 807 | try: 808 | for xgen_file in get_xgen_files(): 809 | yield xgen_file 810 | except NameError as e: 811 | print 'error retrieving Xgen file list: %s' % str(e) 812 | 813 | 814 | def get_xgen_files(): 815 | """Yield all Xgen file dependencies in the scene.""" 816 | # Get collection list, if the call fails due to Xgen not being 817 | # loaded, stop. 818 | if _XGEN_IMPORT_ERROR: 819 | raise NameError('Xgen is not loaded due to error: %s' % _XGEN_IMPORT_ERROR) 820 | # try to get collection list using uiPalettes() instead of the standard 821 | # xgenm.palettes() because the latter can pick up temp collections 822 | # which aren't needed and sometimes don't actually exist. 823 | try: 824 | collection_list = xgenm.ui.util.xgUtil.uiPalettes() 825 | # sometimes xgenm.ui doesn't exist, if the user does not have the Xgen 826 | # plugin loaded. in this case, fall back to the old way of getting 827 | # collections. it's unlikely this will return any of the abovementioned 828 | # temporary collections, or anything at all, because the user will 829 | # have the Xgen plugin loaded if they are using Xgen. 830 | except AttributeError: 831 | collection_list = xgenm.palettes() 832 | for collection in collection_list: 833 | for def_file in _get_xgen_collection_definition(collection): 834 | print 'found Xgen collection definition: %s' % def_file 835 | yield def_file 836 | for xgen_file in _get_xgen_collection_files(collection): 837 | print 'found Xgen collection file: %s' % xgen_file 838 | yield xgen_file 839 | 840 | 841 | def _get_xgen_collection_definition(collection_name): 842 | """Yield Xgen collection direct dependencies. 843 | 844 | Args: 845 | collection_name: str, name of Xgen collection in the current scene 846 | 847 | Returns: 848 | Yields str for each definition files associated with that collection. 849 | """ 850 | if _XGEN_IMPORT_ERROR: 851 | raise NameError('Xgen is not loaded due to error: %s' % _XGEN_IMPORT_ERROR) 852 | scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True)) 853 | scene_name, _ = os.path.splitext(scene_basename) 854 | # Xgen definition files must meet very specific conventions - they 855 | # must live in the same directory as the scene file and be named 856 | # according to a strict __ format. 857 | # These are Xgen conventions, not specific to Zync. 858 | # Maya avoids using the namespace character ':' in filenames, so 859 | # we must do the same replacement. 860 | filenames = [ 861 | '%s__%s.xgen' % (scene_name, collection_name.replace(':', '__')), 862 | '%s__%s.abc' % (scene_name, collection_name.replace(':', '__ns__')), 863 | ] 864 | for filename in filenames: 865 | yield os.path.join(scene_dir, filename).replace('\\', '/') 866 | 867 | 868 | def _get_xgen_collection_files(collection_name): 869 | """Get Xgen indirect dependencies, specifically files stored 870 | in related objects.""" 871 | if _XGEN_IMPORT_ERROR: 872 | raise NameError('Xgen is not loaded due to error: %s' % _XGEN_IMPORT_ERROR) 873 | xg_proj_path = xgenm.getAttr('xgProjectPath', collection_name) 874 | xg_data_path = xgenm.getAttr('xgDataPath', collection_name) 875 | xg_data_path = xg_data_path.replace('${PROJECT}', xg_proj_path) 876 | # upload all files under collection root 877 | for dir_name, subdir_list, file_list in os.walk(xg_data_path): 878 | for xg_file in file_list: 879 | if not xg_file.startswith('.'): 880 | yield os.path.join(dir_name, xg_file).replace('\\', '/') 881 | # search objects for files too 882 | for xg_desc in xgenm.descriptions(collection_name): 883 | obj_list = (xgenm.objects(collection_name, xg_desc) + 884 | xgenm.fxModules(collection_name, xg_desc)) 885 | for xg_obj in obj_list: 886 | for xg_file in _get_xgen_object_files(collection_name, xg_desc, xg_obj): 887 | yield xg_file 888 | 889 | 890 | def _get_xgen_object_files(collection_name, desc_name, object_name): 891 | """Get all files linked to an Xgen object.""" 892 | if _XGEN_IMPORT_ERROR: 893 | raise NameError('Xgen is not loaded due to error: %s' % _XGEN_IMPORT_ERROR) 894 | # the "files" attr requires some special parsing, handle this first 895 | if xgenm.attrExists('files', collection_name, desc_name, object_name): 896 | for file_path in _get_files_from_files_attr(collection_name, desc_name, 897 | object_name): 898 | yield file_path 899 | # look for other attributes which are expected to contain file paths 900 | for file_attr in _XGEN_FILE_ATTRS: 901 | # "files" attr is already handled above 902 | if file_attr == 'files': 903 | continue 904 | if xgenm.attrExists(file_attr, collection_name, desc_name, object_name): 905 | yield xgenm.getAttr(file_attr, collection_name, desc_name, object_name) 906 | # search other attributes for file paths 907 | for other_attr_file in _get_files_from_other_attrs(collection_name, desc_name, 908 | object_name): 909 | yield other_attr_file 910 | 911 | 912 | def _get_files_from_files_attr(collection_name, desc_name, object_name): 913 | """Get all files stored in the "files" attribute of an Xgen object.""" 914 | xg_proj_path = xgenm.getAttr('xgProjectPath', collection_name) 915 | # files attr has a rather strange format, which we must parse and attempt 916 | # to infer file paths from. For example: 917 | # #ArchiveGroup 0 name="stalagmite" thumbnail="stalagmite.png" description="No description." \ 918 | # materials="${PROJECT}/xgen/archives/materials/stalagmite.ma" color=[1.0,0.0,0.0]\n0 \ 919 | # "${PROJECT}/xgen/archives/abc/stalagmite.abc" 920 | for attr in re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', 921 | xgenm.getAttr('files', collection_name, desc_name, object_name)): 922 | attr_split = attr.split('=') 923 | current_file = None 924 | if not attr_split: 925 | pass 926 | # Look for something that looks like a file path 927 | elif len(attr_split) < 2 and ('/' in attr or '\\' in attr): 928 | current_file = attr.strip('"').replace('${PROJECT}', xg_proj_path) 929 | # Also catch materials= tags. 930 | elif attr_split[0] == 'materials': 931 | current_file = attr_split[1].strip('"').replace('${PROJECT}', xg_proj_path) 932 | if current_file: 933 | yield current_file 934 | # If the file is a .gz archive, look for a toc file as well. Arnold archives 935 | # in particular often require this. 936 | if current_file.endswith('.gz'): 937 | head, _ = os.path.splitext(current_file) 938 | toc_path = head + 'toc' 939 | if os.path.exists(toc_path): 940 | yield toc_path 941 | 942 | 943 | def _get_files_from_other_attrs(collection_name, desc_name, object_name): 944 | """Searches attributes of an Xgen object to try to detect file paths.""" 945 | for attr in xgenm.attrs(collection_name, desc_name, object_name): 946 | # skip any attrs which probably contain plain file paths, which we've 947 | # already collected above 948 | if attr in _XGEN_FILE_ATTRS: 949 | continue 950 | attr_val = xgenm.getAttr(attr, collection_name, desc_name, object_name) 951 | # the map() directive indicates an Xgen expression which reads in an image 952 | # map and applies derives the attribute value from that image data, much 953 | # like a texture map. 954 | if 'map(' in attr_val: 955 | opening_paren = attr_val.find('map(') + 4 956 | closing_paren = _find_matching_paren(attr_val, opening_paren) 957 | # if no matching paren was found just skip this attr - its probably a 958 | # broken expression or a bit of code left in a comment 959 | if closing_paren is None: 960 | continue 961 | file_path = attr_val[opening_paren:closing_paren].strip('"').strip("'") 962 | # if the path starts with $, that means its path is based on an Xgen 963 | # variable, usually ${DESC}. this means it is stored within the 964 | # collection directory structure, and would have already been collected 965 | # above 966 | if file_path.startswith('$'): 967 | continue 968 | # if its a file, yield it. if its a directory, recurse into the directory 969 | # and yield all files contained within 970 | if os.path.isfile(file_path): 971 | yield file_path.replace('\\', '/') 972 | elif os.path.isdir(file_path): 973 | for child_dir, subdir_list, file_list in os.walk(file_path): 974 | for child_file in file_list: 975 | yield os.path.join(child_dir, child_file).replace('\\', '/') 976 | 977 | 978 | def _find_matching_paren(some_string, opening_paren): 979 | """Given a string and the position of an opening paren, returns the position 980 | of the corresponding closing paren. 981 | 982 | Args: 983 | some_string: str, the string which contains the parens 984 | opening_paren: int, index within some_string of the opening paren 985 | 986 | Returns: 987 | int, position of the corresponding closing paren, or None if none was found. 988 | """ 989 | current_open_parens = 0 990 | for i in range(opening_paren + 1, len(some_string)): 991 | if some_string[i] == '(': 992 | current_open_parens += 1 993 | elif some_string[i] == ')': 994 | if current_open_parens == 0: 995 | return i 996 | else: 997 | current_open_parens -= 1 998 | return None 999 | 1000 | 1001 | def generate_ocio_files(): 1002 | """ Yields external OCIO config file and its dependencies, if enabled. """ 1003 | if cmds.colorManagementPrefs(q=True, cmEnabled=True) and cmds.colorManagementPrefs(q=True, cmConfigFileEnabled=True): 1004 | for ocio_file in zync.get_ocio_files(str(cmds.colorManagementPrefs(q=True, configFilePath=True))): 1005 | yield ocio_file 1006 | 1007 | 1008 | def get_default_extension(renderer): 1009 | """ 1010 | Returns the filename prefix for the given renderer, either mental ray 1011 | or maya software. 1012 | """ 1013 | if renderer == 'sw': 1014 | menu_grp = 'imageMenuMayaSW' 1015 | elif renderer == 'mr': 1016 | menu_grp = 'imageMenuMentalRay' 1017 | else: 1018 | raise Exception('Invalid Renderer: %s' % renderer) 1019 | try: 1020 | val = cmds.optionMenuGrp(menu_grp, q=True, v=True) 1021 | except RuntimeError: 1022 | msg = 'Please open the Maya Render globals before submitting.' 1023 | raise Exception(msg) 1024 | else: 1025 | return val.split()[-1][1:-1] 1026 | 1027 | 1028 | LAYER_INFO = {} 1029 | def collect_layer_info(layer, renderer): 1030 | cur_layer = cmds.editRenderLayerGlobals(q=True, currentRenderLayer=True) 1031 | _switch_to_renderlayer(layer) 1032 | 1033 | layer_info = {} 1034 | 1035 | # get list of active render passes 1036 | layer_info['render_passes'] = [] 1037 | if (renderer == 'vray' and 1038 | cmds.getAttr('vraySettings.imageFormatStr') != 'exr (multichannel)' 1039 | and cmds.getAttr('vraySettings.relements_enableall') != False): 1040 | pass_list = cmds.ls(type='VRayRenderElement') 1041 | pass_list += cmds.ls(type='VRayRenderElementSet') 1042 | for r_pass in pass_list: 1043 | if cmds.getAttr('%s.enabled' % (r_pass,)) == True: 1044 | layer_info['render_passes'].append(r_pass) 1045 | elif renderer == 'redshift': 1046 | for options_node in cmds.ls(type='RedshiftOptions'): 1047 | for attr in REDSHIFT_CACHE_ATTRIBUTES: 1048 | node_attr = '{0}.{1}'.format(options_node, attr) 1049 | layer_info[node_attr] = cmds.getAttr(node_attr) 1050 | for options_node in cmds.ls(type='RedshiftPostEffects'): 1051 | for attr in REDSHIFT_OCIO_ATTRIBUTES: 1052 | node_attr = '{0}.{1}'.format(options_node, attr) 1053 | layer_info[node_attr] = cmds.getAttr(node_attr) 1054 | try: 1055 | layer_info['prefix'] = cmds.getAttr(NamePrefixAttributes.get_prefix(renderer)) 1056 | except Exception: 1057 | layer_info['prefix'] = '' 1058 | 1059 | _switch_to_renderlayer(cur_layer) 1060 | return layer_info 1061 | 1062 | 1063 | def clear_layer_info(): 1064 | global LAYER_INFO 1065 | LAYER_INFO = {} 1066 | 1067 | 1068 | def get_layer_override(layer, renderer, field): 1069 | global LAYER_INFO 1070 | if layer not in LAYER_INFO: 1071 | LAYER_INFO[layer] = collect_layer_info(layer, renderer) 1072 | return LAYER_INFO[layer][field] 1073 | 1074 | 1075 | def get_maya_version(): 1076 | """Returns the current major Maya version in use.""" 1077 | # `about -api` returns a value containing both major and minor 1078 | # maya versions in one integer, e.g. 201515. Divide by 100 to 1079 | # find the major version. 1080 | version_full = maya.mel.eval('about -api') / 100.0 1081 | # Maya 2018 added two additional digits to the API version. 1082 | if float(version_full) >= 201800: 1083 | version_full /= 100.0 1084 | # Maya 2016 rounds down to the nearest .5 1085 | if int(version_full) == 2016: 1086 | version_rounded = math.floor(version_full * 2) / 2 1087 | # Other versions round down to the nearest whole version. 1088 | else: 1089 | version_rounded = math.floor(version_full) 1090 | # if it's a whole number e.g. 2016.0, drop the decimal 1091 | if version_rounded.is_integer(): 1092 | version_rounded = int(version_rounded) 1093 | return str(version_rounded) 1094 | 1095 | 1096 | def get_scene_info(renderer, layers_to_render, is_bake, extra_assets, frames_to_render): 1097 | """Returns scene info for the current scene. 1098 | 1099 | Args: 1100 | renderer: str, the renderer that will be used - some info returned is 1101 | renderer-specific 1102 | layers_to_render: [str], list of render layers that will be rendered 1103 | is_bake: bool, whether job is a bake job 1104 | extra_assets: [str], list of any extra files to include 1105 | frames_to_render: [int], list of each frame to be rendered 1106 | 1107 | Returns: 1108 | dict of scene information 1109 | """ 1110 | print '--> initializing' 1111 | clear_layer_info() 1112 | 1113 | print '--> render layers' 1114 | scene_info = {'render_layers': get_render_layers()} 1115 | 1116 | print '--> checking selections' 1117 | if is_bake: 1118 | selected_bake_sets = layers_to_render 1119 | if selected_bake_sets == None: 1120 | selected_bake_sets = [] 1121 | selected_layers = [] 1122 | else: 1123 | selected_layers = layers_to_render 1124 | if selected_layers == None: 1125 | selected_layers = [] 1126 | selected_bake_sets = [] 1127 | 1128 | # Detect a list of referenced files. We must use ls() instead of file(q=True, r=True) 1129 | # because the latter will only detect references one level down, not nested references. 1130 | print '--> references' 1131 | scene_info['references'] = [] 1132 | scene_info['unresolved_references'] = [] 1133 | for ref_node in cmds.ls(type='reference'): 1134 | try: 1135 | scene_info['references'].append(cmds.referenceQuery(ref_node, filename=True)) 1136 | scene_info['unresolved_references'].append( 1137 | cmds.referenceQuery(ref_node, filename=True, unresolvedName=True)) 1138 | except: 1139 | pass 1140 | 1141 | print '--> render passes' 1142 | scene_info['render_passes'] = {} 1143 | if renderer == 'vray' and cmds.getAttr('vraySettings.imageFormatStr') != 'exr (multichannel)': 1144 | pass_list = cmds.ls(type='VRayRenderElement') 1145 | pass_list += cmds.ls(type='VRayRenderElementSet') 1146 | if len(pass_list) > 0: 1147 | for layer in selected_layers: 1148 | scene_info['render_passes'][layer] = [] 1149 | enabled_passes = get_layer_override(layer, renderer, 'render_passes') 1150 | for r_pass in pass_list: 1151 | if r_pass in enabled_passes: 1152 | vray_name = None 1153 | vray_explicit_name = None 1154 | vray_file_name = None 1155 | for attr_name in cmds.listAttr(r_pass): 1156 | if attr_name.startswith('vray_filename'): 1157 | vray_file_name = cmds.getAttr('%s.%s' % (r_pass, attr_name)) 1158 | elif attr_name.startswith('vray_name'): 1159 | vray_name = cmds.getAttr('%s.%s' % (r_pass, attr_name)) 1160 | elif attr_name.startswith('vray_explicit_name'): 1161 | vray_explicit_name = cmds.getAttr('%s.%s' % (r_pass, attr_name)) 1162 | if vray_file_name != None and vray_file_name != "": 1163 | final_name = vray_file_name 1164 | elif vray_explicit_name != None and vray_explicit_name != "": 1165 | final_name = vray_explicit_name 1166 | elif vray_name != None and vray_name != "": 1167 | final_name = vray_name 1168 | else: 1169 | continue 1170 | # special case for Material Select elements - these are named based on the material 1171 | # they are connected to. 1172 | if 'vray_mtl_mtlselect' in cmds.listAttr(r_pass): 1173 | connections = cmds.listConnections('%s.vray_mtl_mtlselect' % (r_pass,)) 1174 | if connections: 1175 | final_name += '_%s' % (str(connections[0]),) 1176 | scene_info['render_passes'][layer].append(final_name) 1177 | 1178 | print '--> bake sets' 1179 | scene_info['bake_sets'] = {} 1180 | for bake_set in selected_bake_sets: 1181 | scene_info['bake_sets'][bake_set] = { 1182 | 'uvs': _get_bake_set_uvs(bake_set), 1183 | 'map': _get_bake_set_map(bake_set), 1184 | 'shape': _get_bake_set_shape(bake_set), 1185 | 'output_path': _get_bake_set_output_path(bake_set), 1186 | } 1187 | 1188 | print '--> frame extension & padding' 1189 | if renderer == 'vray': 1190 | scene_info['extension'] = cmds.getAttr('vraySettings.imageFormatStr') 1191 | if scene_info['extension'] == None: 1192 | scene_info['extension'] = 'png' 1193 | scene_info['padding'] = int(cmds.getAttr('vraySettings.fileNamePadding')) 1194 | elif renderer == 'mr': 1195 | scene_info['extension'] = cmds.getAttr('defaultRenderGlobals.imfPluginKey') 1196 | if not scene_info['extension']: 1197 | scene_info['extension'] = get_default_extension(renderer) 1198 | scene_info['padding'] = int(cmds.getAttr('defaultRenderGlobals.extensionPadding')) 1199 | elif renderer == 'arnold': 1200 | scene_info['extension'] = cmds.getAttr('defaultRenderGlobals.imfPluginKey') 1201 | scene_info['padding'] = int(cmds.getAttr('defaultRenderGlobals.extensionPadding')) 1202 | elif renderer == 'renderman': 1203 | if cmds.getAttr('defaultRenderGlobals.outFormatControl'): 1204 | scene_info['extension'] = cmds.getAttr('defaultRenderGlobals.outFormatExt').lstrip('.') 1205 | else: 1206 | scene_info['extension'] = renderman.get_extension() 1207 | scene_info['padding'] = int(cmds.getAttr('defaultRenderGlobals.extensionPadding')) 1208 | elif renderer == 'redshift': 1209 | scene_info['extension'] = cmds.getAttr('defaultRenderGlobals.imfPluginKey') 1210 | scene_info['padding'] = int(cmds.getAttr('defaultRenderGlobals.extensionPadding')) 1211 | scene_info['extension'] = scene_info['extension'][:3] 1212 | 1213 | # collect a dict of attrs that define how output frames have frame numbers 1214 | # and extension added to their names. 1215 | if renderer == 'arnold': 1216 | print '--> output name format' 1217 | scene_info['output_name_format'] = {} 1218 | attr_list = { 1219 | 'outFormatControl', 1220 | 'animation', 1221 | 'putFrameBeforeExt', 1222 | 'periodInExt', 1223 | 'extensionPadding', 1224 | } 1225 | for name_attr in attr_list: 1226 | if cmds.attributeQuery(name_attr, n='defaultRenderGlobals', ex=True): 1227 | scene_info['output_name_format'][name_attr] = cmds.getAttr('defaultRenderGlobals.%s' % name_attr) 1228 | 1229 | print '--> output file prefixes' 1230 | prefix = get_layer_override('defaultRenderLayer', renderer, 'prefix') 1231 | scene_info['file_prefix'] = [prefix] 1232 | prefixes_to_verify = [prefix] 1233 | layer_prefixes = {} 1234 | for layer in selected_layers: 1235 | layer_prefix = get_layer_override(layer, renderer, 'prefix') 1236 | if layer_prefix != None: 1237 | layer_prefixes[layer] = layer_prefix 1238 | prefixes_to_verify.append(layer_prefix) 1239 | scene_info['file_prefix'].append(layer_prefixes) 1240 | 1241 | print '--> files' 1242 | assets = set() 1243 | for asset in get_scene_files(frames_to_render, renderer): 1244 | if asset: 1245 | assets.add(_clean_path(asset)) 1246 | for asset in extra_assets: 1247 | if asset: 1248 | assets.add(_clean_path(asset)) 1249 | scene_info['files'] = [_clean_path(_absolutize_path(path)) for path in assets] 1250 | # Xgen files are already included in the main files list, but we also 1251 | # include them separately so Zync can perform Xgen-related tasks on 1252 | # the much smaller subset 1253 | scene_info['xgen_files'] = list(set(get_xgen_files())) 1254 | 1255 | print '--> plugins' 1256 | scene_info['plugins'] = [] 1257 | plugin_list = cmds.pluginInfo(query=True, pluginsInUse=True) 1258 | for i in range(0, len(plugin_list), 2): 1259 | scene_info['plugins'].append(str(plugin_list[i])) 1260 | 1261 | # detect MentalCore 1262 | if renderer == 'mr': 1263 | mentalcore_used = False 1264 | try: 1265 | mc_nodes = cmds.ls(type='core_globals') 1266 | if len(mc_nodes) == 0: 1267 | mentalcore_used = False 1268 | else: 1269 | mc_node = mc_nodes[0] 1270 | if cmds.getAttr('%s.ec' % (mc_node,)) == True: 1271 | mentalcore_used = True 1272 | else: 1273 | mentalcore_used = False 1274 | except: 1275 | mentalcore_used = False 1276 | else: 1277 | mentalcore_used = False 1278 | if mentalcore_used: 1279 | scene_info['plugins'].append('mentalcore') 1280 | 1281 | # detect use of cache files 1282 | if len(cmds.ls(type='cacheFile')) > 0: 1283 | scene_info['plugins'].append('cache') 1284 | 1285 | print '--> maya version' 1286 | scene_info['version'] = get_maya_version() 1287 | 1288 | scene_info['vray_version'] = '' 1289 | if renderer == 'vray': 1290 | print '--> vray version' 1291 | try: 1292 | scene_info['vray_version'] = '.'.join(str(cmds.vray('version')).split('.')[0:3]) 1293 | scene_info['vray_production_engine_name'] = _get_vray_production_engine_name() 1294 | except Exception as e: 1295 | raise maya_common.MayaZyncException(_plugin_load_error_message('VRay')) 1296 | 1297 | scene_info['arnold_version'] = '' 1298 | if renderer == 'arnold': 1299 | print '--> arnold version' 1300 | try: 1301 | scene_info['arnold_version'] = str(cmds.pluginInfo('mtoa', query=True, version=True)) 1302 | except Exception as e: 1303 | raise maya_common.MayaZyncException(_plugin_load_error_message('Arnold')) 1304 | 1305 | if renderer == 'renderman': 1306 | print '--> renderman version' 1307 | try: 1308 | scene_info['renderman_version'] = renderman.get_version() 1309 | except Exception as e: 1310 | raise maya_common.MayaZyncException(_plugin_load_error_message('Renderman')) 1311 | 1312 | if renderer == 'redshift': 1313 | try: 1314 | scene_info['redshift_version'] = get_redshift_version() 1315 | except Exception as e: 1316 | raise maya_common.MayaZyncException(_plugin_load_error_message('Redshift')) 1317 | 1318 | if renderer == 'arnold': 1319 | _check_arnold_gpu() 1320 | 1321 | # If this is an Arnold job and AOVs are on, include a list of AOV 1322 | # names in scene_info. If "Merge AOVs" is on, i.e. multichannel EXRs, 1323 | # the AOVs will be rendered in a single image, so consider AOVs to be 1324 | # OFF for purposes of the Zync job. 1325 | try: 1326 | aov_on = (cmds.getAttr('defaultArnoldRenderOptions.aovMode') and 1327 | not cmds.getAttr('defaultArnoldDriver.mergeAOVs')) 1328 | override_prefix = cmds.getAttr('defaultArnoldDriver.prefix') 1329 | except: 1330 | aov_on = False 1331 | override_prefix = '' 1332 | should_override_prefix = bool(override_prefix) 1333 | if aov_on: 1334 | print '--> AOVs' 1335 | scene_info['aovs'] = [cmds.getAttr('%s.name' % (n,)) for n in cmds.ls(type='aiAOV')] 1336 | 1337 | if scene_info['aovs']: 1338 | # Here goes verification of the output prefixes. Once the AOVs are about 1339 | # to render into the separate files, output prefix is suppose to contain 1340 | # tag. The regular prefixes can be override by the one set 1341 | # up in the defaultArnoldDriver 1342 | output_prefix_aov_warning = False 1343 | for output in prefixes_to_verify: 1344 | if not output or '' not in output: 1345 | output_prefix_aov_warning = True 1346 | 1347 | SubmissionCheck( 1348 | check=lambda: (output_prefix_aov_warning and not should_override_prefix) or \ 1349 | (should_override_prefix and '' not in override_prefix), 1350 | title='RenderPass tag missing', 1351 | message='AOVs are selected to render into separate files, but the ' 1352 | 'output prefix of one of the layers does not contain ' 1353 | ' tag. Are you sure the configuration is correct?', 1354 | ).run_check() 1355 | 1356 | output_prefix_layer_warning = False 1357 | for output in prefixes_to_verify: 1358 | if not output or '' not in output: 1359 | output_prefix_layer_warning = True 1360 | 1361 | SubmissionCheck( 1362 | check=lambda: (output_prefix_layer_warning and not should_override_prefix) or \ 1363 | (should_override_prefix and '' not in override_prefix), 1364 | title='RenderLayer tag missing', 1365 | message='Tag is required in output if AOVs are present. ' 1366 | 'Are you sure the configuration is correct?', 1367 | ).run_check() 1368 | 1369 | else: 1370 | scene_info['aovs'] = [] 1371 | 1372 | # collect info on whether scene uses Legacy Render Layers or new Render 1373 | # Setup system (Maya 2016.5 and higher only) 1374 | if float(get_maya_version()) >= 2016.5: 1375 | print '--> renderSetupEnable' 1376 | # 0 or 1. 0 = legacy render layers, 1 = new render setup system 1377 | if cmds.optionVar(exists='renderSetupEnable'): 1378 | scene_info['renderSetupEnable'] = cmds.optionVar(query='renderSetupEnable') 1379 | else: 1380 | scene_info['renderSetupEnable'] = 1 1381 | 1382 | return scene_info 1383 | 1384 | 1385 | def _clean_path(path): 1386 | return path.replace('\\', '/') 1387 | 1388 | 1389 | def _plugin_load_error_message(plugin_name): 1390 | message = 'Could not detect {0} version. ' \ 1391 | 'This is required to render {0} jobs. ' \ 1392 | 'Do you have the {0} plugin loaded?' 1393 | return message.format(plugin_name) 1394 | 1395 | 1396 | def _check_arnold_gpu(): 1397 | try: 1398 | renderDevice = cmds.getAttr('defaultArnoldRenderOptions.renderDevice') 1399 | except: 1400 | renderDevice = 0 1401 | 1402 | SubmissionCheck( 1403 | check=lambda: (renderDevice != 0 and renderDevice != '0'), 1404 | title='GPU rendering not supported', 1405 | always_fail=True, 1406 | message='Zync does not currently support GPU rendering for Arnold. ' 1407 | 'Please change render device to CPU in render settings.', 1408 | ).run_check() 1409 | 1410 | 1411 | def _absolutize_path(path): 1412 | if path: 1413 | # Maya sometimes prefixes relative paths with //, but abspath considers 1414 | # such paths as absolute, so // needs to be removed 1415 | if path.startswith('//'): 1416 | path = path.replace('//', '', 1) 1417 | if not os.path.isabs(path): 1418 | return os.path.abspath(os.path.join(proj_dir(), path)) 1419 | return path 1420 | 1421 | 1422 | def _get_bake_set_uvs(bake_set): 1423 | conn_list = cmds.listConnections(bake_set) 1424 | if conn_list == None or len(conn_list) == 0: 1425 | return None 1426 | return cmds.polyEvaluate(conn_list[0], b2=True) 1427 | 1428 | 1429 | def _get_bake_set_map(bake_set): 1430 | return cmds.getAttr('%s.bakeChannel' % bake_set) 1431 | 1432 | 1433 | def _get_bake_set_shape(bake_set): 1434 | transforms = cmds.listConnections(bake_set) 1435 | if transforms == None or len(transforms) == 0: 1436 | return None 1437 | transform = transforms[0] 1438 | shape_nodes = cmds.listRelatives(transform) 1439 | if shape_nodes == None or len(shape_nodes) == 0: 1440 | return None 1441 | return shape_nodes[0] 1442 | 1443 | 1444 | def _get_bake_set_output_path(bake_set): 1445 | out_path = cmds.getAttr('%s.outputTexturePath' % bake_set) 1446 | out_path = out_path.replace('\\', '/') 1447 | if out_path[0] == '/' or out_path[1] == ':': 1448 | full_path = out_path 1449 | else: 1450 | full_path = proj_dir().replace('\\', '/') 1451 | if full_path[-1] != '/': 1452 | full_path += '/' 1453 | full_path += out_path 1454 | return full_path 1455 | 1456 | 1457 | def _get_vray_production_engine_name(): 1458 | """Get the vray production engine if the renderer is set to vray. 1459 | 1460 | Return: 1461 | str, see VRAY_ENGINE_NAME_xxx constants for possible values 1462 | """ 1463 | try: 1464 | engine_id = cmds.getAttr('vraySettings.productionEngine') 1465 | if engine_id == 0: 1466 | return VRAY_ENGINE_NAME_CPU 1467 | elif engine_id == 1: 1468 | return VRAY_ENGINE_NAME_OPENCL 1469 | elif engine_id == 2: 1470 | return VRAY_ENGINE_NAME_CUDA 1471 | return VRAY_ENGINE_NAME_UNKNOWN 1472 | except ValueError: 1473 | return VRAY_ENGINE_NAME_CPU 1474 | 1475 | 1476 | def _switch_to_renderlayer(layer_name): 1477 | # Use the newer Render Setup API if it exists and Render Setup is enabled. 1478 | if (_RENDERSETUP_IMPORT_ERROR is None and 1479 | cmds.optionVar(exists='renderSetupEnable') and 1480 | cmds.optionVar(query='renderSetupEnable') and 1481 | not os.getenv('MAYA_ENABLE_LEGACY_RENDER_LAYERS')): 1482 | rs = renderSetup.instance() 1483 | # defaultRenderLayer doesn't exist as a Render Setup layer, it must be 1484 | # treated as a legacy layer always. 1485 | if layer_name == 'defaultRenderLayer': 1486 | rs.switchToLayerUsingLegacyName('defaultRenderLayer') 1487 | else: 1488 | # The rest of the zync-maya code works with legacy render layer names, 1489 | # which prefix Render Setup layers with an rs_ prefix. 1490 | if layer_name.startswith('rs_'): 1491 | layer_name = layer_name[3:] 1492 | rs.switchToLayer(rs.getRenderLayer(layer_name)) 1493 | else: 1494 | cmds.editRenderLayerGlobals(currentRenderLayer=layer_name) 1495 | 1496 | 1497 | def _maya_attr_is_true(attr_val): 1498 | """Whether a Maya attr evaluates to True. 1499 | 1500 | When querying an attribute value from an ambiguous object the Maya API will return 1501 | a list of values, which need to be properly handled to evaluate properly. 1502 | """ 1503 | if isinstance(attr_val, types.BooleanType): 1504 | return attr_val 1505 | elif isinstance(attr_val, (types.ListType, types.GeneratorType)): 1506 | return any(attr_val) 1507 | else: 1508 | return bool(attr_val) 1509 | 1510 | 1511 | def _unused(*args): 1512 | """Method to mark a variable as unused. 1513 | 1514 | Args: 1515 | *args: does nothing 1516 | """ 1517 | _ = args 1518 | pass 1519 | 1520 | 1521 | # TODO(cipriano) Move this function into zync-python. (b/79435050) 1522 | def parse_frame_range(frame_range): 1523 | frame_list = list() 1524 | for frange_section in frame_range.split(','): 1525 | frame_list.extend(_parse_frame_range_section(frange_section)) 1526 | return frame_list 1527 | 1528 | 1529 | # TODO(cipriano) Support embedded step number. (b/70778535) 1530 | def _parse_frame_range_section(frange_section): 1531 | range_match = _FRAME_RANGE_RE.match(frange_section) 1532 | if range_match: 1533 | start_frame = int(range_match.group('sf')) 1534 | end_frame = int(range_match.group('ef')) 1535 | if end_frame >= start_frame: 1536 | return range(start_frame, end_frame+1) 1537 | else: 1538 | return range(start_frame, end_frame-1, -1) 1539 | 1540 | single_frame_match = _SINGLE_FRAME_RE.match(frange_section) 1541 | if single_frame_match: 1542 | return [int(single_frame_match.group(0))] 1543 | 1544 | raise ValueError('unable to parse frame range section %s' % frange_section) 1545 | 1546 | 1547 | # TODO(cipriano) Move this function into zync-python. (b/79435050) 1548 | def extract_frame_number_from_file_path(file_path): 1549 | frame_match = _FRAME_NUMBER_RE.match(os.path.basename(file_path)) 1550 | if frame_match: 1551 | return int(frame_match.group('frame')) 1552 | return None 1553 | 1554 | 1555 | class SubmissionCheck(object): 1556 | """ 1557 | Manages the running of submission checks and display of confirmation dialogs. 1558 | """ 1559 | def __init__(self, check, title, message='', check_args=None, check_kwargs=None, 1560 | confirm_msg='Yes, submit job.', cancel_msg='No, cancel job submission.', 1561 | always_fail=False): 1562 | """ 1563 | Initialize Check and set attributes 1564 | 1565 | Args: 1566 | check: callable, function that runs check, callable should return a bool. 1567 | title: str, title of confirm dialog window. 1568 | message: str, message to display on confirm dialog. 1569 | check_args: list, list of arguments to pass to check function. 1570 | check_kwargs: dict, keyword arguments to pass to check function. 1571 | confirm_msg: str, text to display on confirm button. 1572 | cancel_msg: str, text to display on cancel button. 1573 | """ 1574 | self.title = title 1575 | self.message = message 1576 | self.check = check 1577 | self.check_args = check_args if check_args is not None else [] 1578 | self.check_kwargs = check_kwargs if check_kwargs is not None else {} 1579 | self.confirm_msg = confirm_msg 1580 | self.cancel_msg = cancel_msg 1581 | self.always_fail = always_fail 1582 | 1583 | def confirm_or_abort(self): 1584 | """ 1585 | Display a confirmation dialog, allowing the user to continue or abort the job submission 1586 | 1587 | Raises: 1588 | ZyncAbortedByUser exception if user aborts. 1589 | """ 1590 | response = cmds.confirmDialog( 1591 | title=self.title, 1592 | message=self.message, 1593 | button=(self.confirm_msg, self.cancel_msg), 1594 | defaultButton=self.confirm_msg, 1595 | cancelButton=self.cancel_msg, 1596 | icon='warning') 1597 | if response != self.confirm_msg: 1598 | raise maya_common.ZyncAbortedByUser('Aborted by user') 1599 | 1600 | def run_check(self, show_confirmation=True): 1601 | """ 1602 | Run the check and show the confirmation dialog. 1603 | Args: 1604 | show_confirmation: bool, whether or not to show the confirmation dialog. 1605 | 1606 | Returns: 1607 | bool, the return value of the check. 1608 | 1609 | Raises: 1610 | ZyncSubmissionCheckError when there is an error running the submission check or if check does not return a bool. 1611 | """ 1612 | try: 1613 | check_return = self.check(*self.check_args, **self.check_kwargs) 1614 | except Exception, e: 1615 | raise maya_common.ZyncSubmissionCheckError('{}: {}'.format(self.title, e.message)) 1616 | if not isinstance(check_return, bool): 1617 | raise maya_common.ZyncSubmissionCheckError('{}: Invalid check. Did not return a boolean.'.format(self.title)) 1618 | if check_return: 1619 | if self.always_fail: 1620 | raise maya_common.ZyncSubmissionCheckError(self.message) 1621 | elif show_confirmation: 1622 | self.confirm_or_abort() 1623 | return check_return 1624 | 1625 | 1626 | class SubmitWindow(object): 1627 | """ 1628 | A Maya UI window for submitting to Zync 1629 | """ 1630 | @show_exceptions 1631 | def __init__(self, title='Zync Submit (version %s)' % __version__): 1632 | """ 1633 | Constructs the window. 1634 | You must call show() to display the window. 1635 | """ 1636 | import_zync_python() 1637 | self.title = title 1638 | 1639 | scene_name = cmds.file(q=True, loc=True) 1640 | if scene_name == 'unknown': 1641 | raise maya_common.MayaZyncException('Please save your scene before launching a job.') 1642 | 1643 | # this will perform the Google OAuth flow so future API requests 1644 | # will be authenticated 1645 | self.zync_conn = zync.Zync(application='maya') 1646 | 1647 | self.vray_production_engine_name = VRAY_ENGINE_NAME_UNKNOWN 1648 | 1649 | self.new_project_name = self.zync_conn.get_project_name(scene_name) 1650 | 1651 | self.num_instances = 1 1652 | self.priority = 50 1653 | self.parent_id = None 1654 | 1655 | self.project = proj_dir() 1656 | if self.project[-1] == '/': 1657 | self.project = self.project[:-1] 1658 | 1659 | self.frange = frame_range() 1660 | self.udim_range = udim_range() 1661 | self.frame_step = cmds.getAttr('defaultRenderGlobals.byFrameStep') 1662 | self.chunk_size = 10 1663 | self.upload_only = 0 1664 | self.start_new_slots = 1 1665 | self.skip_check = 0 1666 | self.notify_complete = 0 1667 | self.vray_nightly = 0 1668 | self.use_standalone = 0 1669 | self.ignore_second_deps = 0 1670 | self.num_tiles = 1 1671 | self.ignore_plugin_errors = 0 1672 | self.login_type = 'zync' 1673 | 1674 | mi_setting = self.zync_conn.CONFIG.get('USE_MI') 1675 | if mi_setting in (None, '', 1, '1'): 1676 | self.force_mi = True 1677 | else: 1678 | self.force_mi = False 1679 | 1680 | self.x_res = cmds.getAttr('defaultResolution.width') 1681 | self.y_res = cmds.getAttr('defaultResolution.height') 1682 | 1683 | self.parse_renderer_from_scene() 1684 | self.init_layers() 1685 | self.init_bake() 1686 | 1687 | self.name = self.loadUI(UI_FILE) 1688 | 1689 | self.check_references() 1690 | 1691 | def loadUI(self, ui_file): 1692 | """ 1693 | Loads the UI and does post-load commands. 1694 | """ 1695 | # Maya 2016 and up will use Maya IO by default. 1696 | self.is_maya_io = (float(get_maya_version()) >= 2016) 1697 | # Create some new functions. These functions are called by UI elements in 1698 | # resources/submit_dialog.ui. Each UI element in that file uses these 1699 | # functions to query this window Object for its initial value. 1700 | # 1701 | # For example, the "frange" textbox calls cmds.submit_callb('frange'), 1702 | # which causes its value to be set to whatever the value of self.frange 1703 | # is currently set to. 1704 | # 1705 | # Initial values can also be function based. For example, the "renderer" 1706 | # dropdown calls cmds.submit_callb('renderer'), which in turn triggers 1707 | # self.init_renderer(). 1708 | # 1709 | # The UI doesn't have a reference to this window Object, but it does have 1710 | # access to the Maya API. So we monkey patch these new functions into the 1711 | # API so the UI can in effect call class functions. 1712 | cmds.submit_callb = self.get_initial_value 1713 | cmds.do_submit_callb = self.submit 1714 | cmds.select_files_callb = self.select_files 1715 | cmds.login_with_google_callb = self.login_with_google 1716 | cmds.logout_callb = self.logout 1717 | 1718 | # 1719 | # Delete the "ZyncSubmitDialog" window if it exists. 1720 | # 1721 | if cmds.window('ZyncSubmitDialog', q=True, ex=True): 1722 | cmds.deleteUI('ZyncSubmitDialog') 1723 | 1724 | # 1725 | # Load the UI file. See the init_* functions below for more info on 1726 | # what each UI element does as it's loaded. 1727 | # 1728 | name = cmds.loadUI(f=ui_file) 1729 | 1730 | # If topLeftCorner is unspecified, Maya 2019 puts the dialog in the top-left corner 1731 | # of the screen in a way that makes title bar invisible 1732 | cmds.window(name, e=True, title=self.title, topLeftCorner=(50, 50)) 1733 | 1734 | # 1735 | # Callbacks - set up functions to be called as UI elements are modified. 1736 | # 1737 | cmds.textField('num_instances', e=True, changeCommand=self.change_num_instances) 1738 | cmds.optionMenu('instance_type', e=True, changeCommand=self.change_instance_type) 1739 | cmds.radioButton('existing_project', e=True, onCommand=self.select_existing_project) 1740 | cmds.radioButton('new_project', e=True, onCommand=self.select_new_project) 1741 | cmds.checkBox('upload_only', e=True, changeCommand=self.upload_only_toggle) 1742 | cmds.optionMenu('renderer', e=True, changeCommand=self.change_renderer) 1743 | cmds.optionMenu('job_type', e=True, changeCommand=self.change_job_type) 1744 | cmds.textField('num_tiles', e=True, changeCommand=self.change_num_tiles) 1745 | cmds.checkBox('sync_extra_assets', e=True, changeCommand=self.sync_extra_assets_toggle) 1746 | cmds.button('select_files', e=True, enable=False) 1747 | cmds.textScrollList('layers', e=True, selectCommand=self.change_layers) 1748 | # No point in even showing the standalone option to users of old Maya, where 1749 | # we force standalone use. 1750 | cmds.checkBox('use_standalone', e=True, changeCommand=self.change_standalone, 1751 | vis=self.is_maya_io) 1752 | 1753 | # 1754 | # Call a few of those callbacks now to set initial UI state. 1755 | # 1756 | self.change_renderer(self.renderer) 1757 | self.select_new_project(True) 1758 | self.set_user_label(self.zync_conn.email) 1759 | 1760 | return name 1761 | 1762 | @show_exceptions 1763 | def upload_only_toggle(self, checked): 1764 | if checked: 1765 | cmds.textField('num_instances', e=True, en=False) 1766 | cmds.optionMenu('instance_type', e=True, en=False) 1767 | cmds.checkBox('skip_check', e=True, en=False) 1768 | cmds.textField('output_dir', e=True, en=False) 1769 | cmds.optionMenu('renderer', e=True, en=False) 1770 | cmds.optionMenu('job_type', e=True, en=False) 1771 | cmds.checkBox('vray_nightly', e=True, en=False) 1772 | cmds.checkBox('use_standalone', e=True, en=False) 1773 | cmds.textField('frange', e=True, en=False) 1774 | cmds.textField('frame_step', e=True, en=False) 1775 | cmds.textField('chunk_size', e=True, en=False) 1776 | cmds.optionMenu('camera', e=True, en=False) 1777 | cmds.textScrollList('layers', e=True, en=False) 1778 | cmds.textField('x_res', e=True, en=False) 1779 | cmds.textField('y_res', e=True, en=False) 1780 | else: 1781 | cmds.textField('num_instances', e=True, en=True) 1782 | cmds.optionMenu('instance_type', e=True, en=True) 1783 | cmds.checkBox('skip_check', e=True, en=True) 1784 | cmds.textField('output_dir', e=True, en=True) 1785 | cmds.optionMenu('renderer', e=True, en=True) 1786 | cmds.optionMenu('job_type', e=True, en=True) 1787 | cmds.textField('frange', e=True, en=True) 1788 | cmds.textField('frame_step', e=True, en=True) 1789 | cmds.textField('chunk_size', e=True, en=True, changeCommand=self.change_chunk_size) 1790 | cmds.optionMenu('camera', e=True, en=True) 1791 | cmds.textScrollList('layers', e=True, en=True) 1792 | cmds.textField('x_res', e=True, en=True) 1793 | cmds.textField('y_res', e=True, en=True) 1794 | self.change_renderer(eval_ui('renderer', ui_type='optionMenu', v=True)) 1795 | 1796 | @show_exceptions 1797 | def sync_extra_assets_toggle(self, checked): 1798 | """Event triggered when the Sync Extra Assets control is toggled. 1799 | 1800 | Args: 1801 | checked: bool, whether the checkbox is checked 1802 | """ 1803 | cmds.button('select_files', e=True, enable=checked) 1804 | 1805 | @show_exceptions 1806 | def change_num_instances(self, *args, **kwargs): 1807 | _unused(args) 1808 | _unused(kwargs) 1809 | self.update_est_cost() 1810 | 1811 | @show_exceptions 1812 | def change_num_tiles(self, num_tiles): 1813 | if int(num_tiles) > 1: 1814 | cmds.textField('chunk_size', e=True, tx='1') 1815 | 1816 | @show_exceptions 1817 | def change_chunk_size(self, chunk_size): 1818 | if int(chunk_size) > 1: 1819 | cmds.textField('num_tiles', e=True, tx='1') 1820 | 1821 | @show_exceptions 1822 | def change_instance_type(self, *args, **kwargs): 1823 | _unused(args) 1824 | _unused(kwargs) 1825 | self.update_est_cost() 1826 | 1827 | @show_exceptions 1828 | def change_renderer(self, renderer): 1829 | cmds.checkBox('vray_nightly', e=True, en=False) 1830 | cmds.checkBox('vray_nightly', e=True, vis=False) 1831 | cmds.checkBox('vray_nightly', e=True, v=False) 1832 | if renderer in ('vray', 'V-Ray'): 1833 | renderer_key = 'vray' 1834 | cmds.checkBox('use_standalone', e=True, en=True) 1835 | cmds.checkBox('use_standalone', e=True, v=False) 1836 | cmds.checkBox('use_standalone', e=True, label='Use Vray Standalone') 1837 | cmds.checkBox('use_standalone', e=True, vis=True) 1838 | cmds.checkBox('ignore_second_deps', e=True, vis=False) 1839 | cmds.textField('num_tiles', e=True, en=False) 1840 | elif renderer.lower() == 'arnold': 1841 | renderer_key = 'arnold' 1842 | cmds.checkBox('use_standalone', e=True, en=True) 1843 | cmds.checkBox('use_standalone', e=True, v=False) 1844 | cmds.checkBox('use_standalone', e=True, label='Use Arnold Standalone') 1845 | cmds.checkBox('use_standalone', e=True, vis=True) 1846 | cmds.checkBox('ignore_second_deps', e=True, vis=False) 1847 | cmds.textField('num_tiles', e=True, en=False) 1848 | elif renderer.lower() == 'renderman': 1849 | renderer_key = 'renderman' 1850 | cmds.checkBox('use_standalone', e=True, v=False) 1851 | cmds.checkBox('use_standalone', e=True, en=False) 1852 | cmds.checkBox('use_standalone', e=True, label='Use Standalone') 1853 | cmds.checkBox('use_standalone', e=True, vis=True) 1854 | cmds.checkBox('ignore_second_deps', e=True, vis=True) 1855 | cmds.textField('num_tiles', e=True, en=False) 1856 | cmds.textField('num_tiles', e=True, tx='1') 1857 | elif renderer.lower() == 'redshift': 1858 | renderer_key = 'redshift' 1859 | cmds.checkBox('use_standalone', e=True, vis=False) 1860 | cmds.checkBox('use_standalone', e=True, en=False) 1861 | cmds.checkBox('use_standalone', e=True, v=False) 1862 | cmds.checkBox('ignore_second_deps', e=True, vis=True) 1863 | cmds.textField('num_tiles', e=True, en=False) 1864 | cmds.textField('num_tiles', e=True, tx='1') 1865 | else: 1866 | raise maya_common.MayaZyncException('Unrecognized renderer "%s".' % renderer) 1867 | cmds.textField('chunk_size', e=True, en=True, changeCommand=self.change_chunk_size) 1868 | 1869 | # job_types dropdown - remove all items for list, then allow in job types 1870 | # from self.zync_conn.JOB_SUBTYPES 1871 | old_types = cmds.optionMenu('job_type', q=True, ill=True) 1872 | if old_types != None: 1873 | cmds.deleteUI(old_types) 1874 | first_type = None 1875 | visible = False 1876 | if renderer_key != None and renderer_key in self.job_types: 1877 | for job_type in self.job_types[renderer_key]: 1878 | if first_type == None: 1879 | first_type = job_type 1880 | label = string.capwords(job_type) 1881 | if label != 'Render': 1882 | visible = True 1883 | print cmds.menuItem(parent='job_type', label=label) 1884 | else: 1885 | print cmds.menuItem(parent='job_type', label='Render') 1886 | first_type = 'Render' 1887 | cmds.optionMenu('job_type', e=True, vis=visible) 1888 | cmds.text('job_type_label', e=True, vis=visible) 1889 | self.change_job_type(first_type) 1890 | # force refresh of a few other UI elements 1891 | self.init_instance_type() 1892 | self.update_est_cost() 1893 | self.change_standalone(eval_ui('use_standalone', 'checkBox', v=True)) 1894 | self.init_output_dir() 1895 | 1896 | @show_exceptions 1897 | def change_job_type(self, job_type): 1898 | job_type = job_type.lower() 1899 | if job_type == 'render': 1900 | cmds.textField('output_dir', e=True, en=True) 1901 | cmds.text('frange_label', e=True, label='Frame Range:') 1902 | cmds.textField('frange', e=True, tx=self.frange) 1903 | cmds.optionMenu('camera', e=True, en=True) 1904 | cmds.text('layers_label', e=True, label='Render Layers:') 1905 | cmds.textScrollList('layers', e=True, removeAll=True) 1906 | cmds.textScrollList('layers', e=True, append=self.layers) 1907 | if len(self.layers) == 1: 1908 | cmds.textScrollList('layers', e=True, selectIndexedItem=1) 1909 | cmds.textField('x_res', e=True, tx=self.x_res) 1910 | cmds.textField('y_res', e=True, tx=self.y_res) 1911 | elif job_type == 'bake': 1912 | cmds.textField('output_dir', e=True, en=False) 1913 | cmds.text('frange_label', e=True, label='UDIM Range:') 1914 | cmds.textField('frange', e=True, tx=self.udim_range) 1915 | cmds.optionMenu('camera', e=True, en=False) 1916 | cmds.text('layers_label', e=True, label='Bake Sets:') 1917 | cmds.textScrollList('layers', e=True, removeAll=True) 1918 | cmds.textScrollList('layers', e=True, append=self.bake_sets) 1919 | try: 1920 | default_x_res = str(cmds.getAttr('vrayDefaultBakeOptions.resolutionX')) 1921 | except: 1922 | default_x_res = '' 1923 | cmds.textField('x_res', e=True, tx=default_x_res) 1924 | try: 1925 | default_y_res = str(cmds.getAttr('vrayDefaultBakeOptions.resolutionY')) 1926 | except: 1927 | default_y_res = '' 1928 | cmds.textField('y_res', e=True, tx=default_y_res) 1929 | else: 1930 | cmds.error('Unknown Job Type "%s".' % (job_type,)) 1931 | 1932 | @show_exceptions 1933 | def change_layers(self): 1934 | if cmds.optionMenu('job_type', q=True, v=True).lower() != 'bake': 1935 | return 1936 | if cmds.textScrollList('layers', q=True, nsi=True) > 1: 1937 | return 1938 | bake_sets = eval_ui('layers', 'textScrollList', ai=True, si=True) 1939 | bake_set = bake_sets[0] 1940 | cmds.textField('x_res', e=True, tx=cmds.getAttr('%s.resolutionX' % (bake_set,))) 1941 | cmds.textField('y_res', e=True, tx=cmds.getAttr('%s.resolutionY' % (bake_set,))) 1942 | 1943 | @show_exceptions 1944 | def change_standalone(self, checked): 1945 | """Event triggered when the Use Standalone control is toggled. 1946 | 1947 | Args: 1948 | checked: bool, whether the checkbox is checked 1949 | """ 1950 | current_renderer = self.get_renderer() 1951 | # if using arnold standalone, disable chunk size. arnold stores info 1952 | # one-frame-per-file so chunk size is not applicable. 1953 | if current_renderer == 'arnold' and checked: 1954 | cmds.textField('chunk_size', e=True, en=False) 1955 | else: 1956 | cmds.textField('chunk_size', e=True, en=True) 1957 | 1958 | if (current_renderer == 'vray' or current_renderer == 'arnold') and checked: 1959 | cmds.textField('num_tiles', e=True, en=True) 1960 | else: 1961 | cmds.textField('num_tiles', e=True, en=False) 1962 | 1963 | @show_exceptions 1964 | def select_new_project(self, selected): 1965 | if selected: 1966 | cmds.textField('new_project_name', e=True, en=True) 1967 | cmds.optionMenu('existing_project_name', e=True, en=False) 1968 | 1969 | @show_exceptions 1970 | def select_existing_project(self, selected): 1971 | if selected: 1972 | cmds.textField('new_project_name', e=True, en=False) 1973 | cmds.optionMenu('existing_project_name', e=True, en=True) 1974 | 1975 | def check_references(self): 1976 | """ 1977 | Run any checks to ensure all reference files are accurate. If not, 1978 | raise an Exception to halt the submit process. 1979 | 1980 | This function currently does nothing. Before Maya Binary was supported 1981 | it checked to ensure no .mb files were being used. 1982 | """ 1983 | #for ref in cmds.file(q=True, r=True): 1984 | # if check_failed: 1985 | # raise Exception(msg) 1986 | pass 1987 | 1988 | def get_render_params(self): 1989 | """ 1990 | Returns a dict of all the render parameters set on the UI 1991 | """ 1992 | params = dict() 1993 | 1994 | if cmds.radioButton('existing_project', q=True, sl=True) == True: 1995 | proj_name = eval_ui('existing_project_name', 'optionMenu', v=True) 1996 | if proj_name == None or proj_name.strip() == '': 1997 | raise maya_common.MayaZyncException('Your project name cannot be blank. Please ' 1998 | 'select New Project and enter a name.') 1999 | else: 2000 | proj_name = eval_ui('new_project_name', text=True) 2001 | params['proj_name'] = proj_name 2002 | 2003 | parent = eval_ui('parent_id', text=True).strip() 2004 | if parent != None and parent != '': 2005 | params['parent_id'] = parent 2006 | params['upload_only'] = int(eval_ui('upload_only', 'checkBox', v=True)) 2007 | params['start_new_slots'] = self.start_new_slots 2008 | params['skip_check'] = int(eval_ui('skip_check', 'checkBox', v=True)) 2009 | params['notify_complete'] = int(eval_ui('notify_complete', 'checkBox', v=True)) 2010 | params['project'] = eval_ui('project', text=True) 2011 | params['sync_extra_assets'] = int(eval_ui('sync_extra_assets', 'checkBox', v=True)) 2012 | 2013 | # 2014 | # Get the output path. If it is a relative path, convert it to an 2015 | # absolute path by joining it to the Maya project path. 2016 | # 2017 | params['out_path'] = eval_ui('output_dir', text=True) 2018 | if not os.path.isabs(params['out_path']): 2019 | params['out_path'] = os.path.abspath(os.path.join(params['project'], 2020 | params['out_path'])) 2021 | 2022 | params['ignore_plugin_errors'] = int(eval_ui('ignore_plugin_errors', 'checkBox', v=True)) 2023 | 2024 | params['renderer'] = self.get_renderer() 2025 | 2026 | params['job_subtype'] = eval_ui('job_type', ui_type='optionMenu', v=True).lower() 2027 | 2028 | params['priority'] = int(eval_ui('priority', text=True)) 2029 | params['num_instances'] = int(eval_ui('num_instances', text=True)) 2030 | params['num_tiles'] = int(eval_ui('num_tiles', text=True)) 2031 | 2032 | selected_type = self.zync_conn.machine_type_from_label( 2033 | eval_ui('instance_type', 'optionMenu', v=True), params['renderer'] + '-maya') 2034 | if not selected_type: 2035 | raise maya_common.MayaZyncException('Unknown instance type selected: %s' % selected_type) 2036 | params['instance_type'] = selected_type 2037 | 2038 | params['frange'] = eval_ui('frange', text=True) 2039 | params['step'] = self._get_frame_step_param() 2040 | params['chunk_size'] = int(eval_ui('chunk_size', text=True)) 2041 | params['xres'] = int(eval_ui('x_res', text=True)) 2042 | params['yres'] = int(eval_ui('y_res', text=True)) 2043 | params['use_standalone'] = 0 2044 | 2045 | params['camera'] = eval_ui('camera', 'optionMenu', v=True) 2046 | if not params['camera']: 2047 | raise maya_common.MayaZyncException('Please select a render camera. If the list is ' 2048 | 'empty, try adding a renderable camera in your ' 2049 | 'scene render settings.') 2050 | 2051 | if params['upload_only'] == 0 and params['renderer'] == 'vray': 2052 | params['vray_nightly'] = int(eval_ui('vray_nightly', 'checkBox', v=True)) 2053 | if params['use_standalone'] == 1 and params['job_subtype'] == 'bake': 2054 | cmds.error('Vray Standalone is not currently supported for Bake jobs.') 2055 | elif params['upload_only'] == 0 and params['renderer'] == 'mr': 2056 | params['vray_nightly'] = 0 2057 | elif params['upload_only'] == 0 and params['renderer'] == 'arnold': 2058 | params['vray_nightly'] = 0 2059 | else: 2060 | params['vray_nightly'] = 0 2061 | 2062 | if params['upload_only'] == 1: 2063 | params['layers'] = None 2064 | params['bake_sets'] = None 2065 | elif params['job_subtype'] == 'bake': 2066 | bake_sets = eval_ui('layers', 'textScrollList', ai=True, si=True) 2067 | if not bake_sets: 2068 | raise maya_common.MayaZyncException('Please select bake set(s).') 2069 | bake_sets = ','.join(bake_sets) 2070 | params['bake_sets'] = bake_sets 2071 | params['layers'] = None 2072 | else: 2073 | layers = eval_ui('layers', 'textScrollList', ai=True, si=True) 2074 | if not layers: 2075 | raise maya_common.MayaZyncException('Please select layer(s) to render.') 2076 | layers = ','.join(layers) 2077 | params['layers'] = layers 2078 | params['bake_sets'] = None 2079 | 2080 | return params 2081 | 2082 | def _get_frame_step_param(self): 2083 | try: 2084 | step = int(eval_ui('frame_step', text=True)) 2085 | if step < 1: 2086 | raise ValueError 2087 | return step 2088 | except ValueError: 2089 | raise maya_common.MayaZyncException('Zync only supports whole numbers >=1 for Frame Step.') 2090 | 2091 | @show_exceptions 2092 | def show(self): 2093 | """ 2094 | Displays the window. 2095 | """ 2096 | cmds.showWindow(self.name) 2097 | 2098 | def init_bake(self): 2099 | self.bake_sets = (bake_set for bake_set in cmds.ls(type='VRayBakeOptions') \ 2100 | if bake_set != 'vrayDefaultBakeOptions') 2101 | self.bake_sets = list(self.bake_sets) 2102 | self.bake_sets.sort() 2103 | 2104 | # 2105 | # These init_* functions get run automatcially when the UI file is loaded. 2106 | # The function names must match the name of the UI element e.g. init_camera() 2107 | # will be run when the "camera" UI element is initialized. 2108 | # 2109 | 2110 | def init_layers(self): 2111 | self.layers = get_render_layers() 2112 | 2113 | def init_existing_project_name(self): 2114 | self.projects = self.zync_conn.get_project_list() 2115 | project_found = False 2116 | for project in self.projects: 2117 | cmds.menuItem(parent='existing_project_name', label=project['name']) 2118 | if project['name'] == self.new_project_name: 2119 | project_found = True 2120 | if project_found: 2121 | cmds.optionMenu('existing_project_name', e=True, v=self.new_project_name) 2122 | if len(self.projects) == 0: 2123 | cmds.radioButton('existing_project', e=True, en=False) 2124 | else: 2125 | cmds.radioButton('existing_project', e=True, en=True) 2126 | 2127 | def init_instance_type(self): 2128 | old_selection = eval_ui('instance_type', ui_type='optionMenu', v=True) 2129 | old_types = cmds.optionMenu('instance_type', q=True, ill=True) 2130 | if old_types is not None: 2131 | cmds.deleteUI(old_types) 2132 | current_renderer = '%s-maya' % self.get_renderer() 2133 | set_to = None 2134 | 2135 | self.refresh_instance_types_cache() 2136 | for label in self.zync_conn.get_machine_type_labels(current_renderer): 2137 | if label == old_selection: 2138 | set_to = label 2139 | cmds.menuItem(parent='instance_type', label=label) 2140 | if set_to: 2141 | cmds.optionMenu('instance_type', e=True, v=set_to) 2142 | self.update_est_cost() 2143 | 2144 | def refresh_instance_types_cache(self): 2145 | usage_tag = None 2146 | if self.renderer == 'vray': 2147 | if self.vray_production_engine_name == VRAY_ENGINE_NAME_CUDA: 2148 | usage_tag = 'maya_vray_rt' 2149 | elif self.renderer == 'redshift': 2150 | usage_tag = 'maya_redshift' 2151 | self.zync_conn.refresh_instance_types_cache(renderer=self.renderer, usage_tag=usage_tag) 2152 | 2153 | def parse_renderer_from_scene(self): 2154 | # Try to detect the currently selected renderer, so it will be selected 2155 | # when the form appears. If we can't, fall back to the default set in zync.py. 2156 | current_renderer = cmds.getAttr('defaultRenderGlobals.currentRenderer') 2157 | if current_renderer == 'mentalRay': 2158 | key = 'mr' 2159 | elif current_renderer == 'vray': 2160 | key = 'vray' 2161 | elif current_renderer == 'arnold': 2162 | key = 'arnold' 2163 | # handle 'renderman', renderMan' and 'renderManRIS' 2164 | elif current_renderer.lower().startswith('renderman'): 2165 | key = 'renderman' 2166 | elif current_renderer == 'redshift': 2167 | key = 'redshift' 2168 | else: 2169 | key = 'vray' 2170 | # if that renderer is not supported, default to Vray 2171 | self.renderer = key 2172 | 2173 | # read vray production engine 2174 | if key == 'vray': 2175 | self.vray_production_engine_name = _get_vray_production_engine_name() 2176 | 2177 | def init_renderer(self): 2178 | # Add the list of renderers to UI element. 2179 | rend_found = False 2180 | default_renderer_name = RENDERER_NAMES.get(self.renderer, 'vray') 2181 | 2182 | if self.vray_production_engine_name == VRAY_ENGINE_NAME_CUDA: 2183 | cmds.menuItem(parent='renderer', label=RENDER_LABEL_VRAY_CUDA) 2184 | cmds.optionMenu('renderer', e=True, v=RENDER_LABEL_VRAY_CUDA, enable=False) 2185 | else: 2186 | for item in RENDERER_NAMES.values(): 2187 | cmds.menuItem(parent='renderer', label=item) 2188 | if item == default_renderer_name: 2189 | rend_found = True 2190 | if rend_found: 2191 | cmds.optionMenu('renderer', e=True, v=default_renderer_name) 2192 | 2193 | def init_job_type(self): 2194 | self.job_types = self.zync_conn.JOB_SUBTYPES['maya'] 2195 | 2196 | def init_camera(self): 2197 | cam_parents = [cmds.listRelatives(x, ap=True)[-1] for x in cmds.ls(cameras=True)] 2198 | for cam in cam_parents: 2199 | # Only show renderable cameras, but look at render layer overrides to see 2200 | # if cameras are set to renderable in other layers. 2201 | if (_maya_attr_is_true(cmds.getAttr(cam + '.renderable')) or 2202 | any([_maya_attr_is_true(override) 2203 | for override in _get_layer_overrides('%s.renderable' % cam)])): 2204 | cmds.menuItem(parent='camera', label=cam) 2205 | 2206 | def init_output_dir(self): 2207 | # renderman doesn't use standard project settings, it has its own 2208 | # preference. 2209 | if self.get_renderer() == 'renderman': 2210 | default_output_dir = renderman.get_output_dir() 2211 | else: 2212 | # the project settings define where that project's rendered images should 2213 | # go. get this project setting, defaulting to "images" if it's not found 2214 | # or blank. 2215 | images_rule = cmds.workspace(fileRuleEntry='images') 2216 | if not images_rule or not images_rule.strip(): 2217 | images_rule = 'images' 2218 | # this is usually a relative path, and if it is it's relative to the 2219 | # project directory. if image_rule is an absolute path os.path.join 2220 | # will throw out the project dir. 2221 | default_output_dir = os.path.join(cmds.workspace(q=True, rd=True), images_rule) 2222 | cmds.textField('output_dir', e=True, tx=default_output_dir) 2223 | 2224 | def update_est_cost(self): 2225 | renderer = '%s-maya' % self.get_renderer() 2226 | machine_type = self.zync_conn.machine_type_from_label( 2227 | eval_ui('instance_type', ui_type='optionMenu', v=True), renderer) 2228 | if machine_type and renderer: 2229 | machine_type_price = self.zync_conn.get_machine_type_price(machine_type, renderer) 2230 | if machine_type_price: 2231 | num_machines = int(eval_ui('num_instances', text=True)) 2232 | text = '$%.02f' % (num_machines * machine_type_price) 2233 | else: 2234 | text = 'Not Available' 2235 | else: 2236 | text = 'Not Available' 2237 | cmds.text('est_cost', e=True, label='Est. Cost per Hour: %s' % text) 2238 | 2239 | def get_renderer(self): 2240 | """Get the renderer which is currently selected in the Zync plugin. 2241 | The label shown in the menu (and returned be eval_ui) is slightly 2242 | different than what we want, so we need to translate it based on 2243 | the master list of renderers. 2244 | 2245 | Returns: 2246 | str, the currently selected renderer, or None if we weren't 2247 | able to identify the one selected. 2248 | """ 2249 | selected_renderer_label = eval_ui('renderer', ui_type='optionMenu', v=True) 2250 | for renderer, renderer_label in RENDERER_NAMES.iteritems(): 2251 | if renderer_label == selected_renderer_label: 2252 | return renderer 2253 | if selected_renderer_label == RENDER_LABEL_VRAY_CUDA: 2254 | return 'vray' 2255 | return None 2256 | 2257 | def set_user_label(self, username): 2258 | cmds.text('google_login_status', e=True, label='Logged in as %s' % username) 2259 | 2260 | def clear_user_label(self): 2261 | cmds.text('google_login_status', e=True, label='') 2262 | 2263 | @show_exceptions 2264 | def get_initial_value(self, name): 2265 | """Returns the initial value for a given attribute. 2266 | 2267 | Args: 2268 | name: str the attribute name 2269 | 2270 | Returns: 2271 | str, the initial attribute value, or "Undefined" if the attribute was 2272 | not found 2273 | """ 2274 | init_name = '_'.join(('init', name)) 2275 | if hasattr(self, init_name): 2276 | return getattr(self, init_name)() 2277 | elif hasattr(self, name): 2278 | return getattr(self, name) 2279 | else: 2280 | return 'Undefined' 2281 | 2282 | @show_exceptions 2283 | def login_with_google(self): 2284 | """Perform the Google OAuth flow.""" 2285 | self.login_type = 'google' 2286 | self.zync_conn.login_with_google() 2287 | self.set_user_label(self.zync_conn.email) 2288 | 2289 | @show_exceptions 2290 | def logout(self): 2291 | self.zync_conn.logout() 2292 | self.clear_user_label() 2293 | 2294 | @show_exceptions 2295 | def select_files(self): 2296 | import_zync_python() 2297 | import file_select_dialog 2298 | proj_name = eval_ui('new_project_name', text=True) 2299 | self.file_select_dialog = file_select_dialog.FileSelectDialog(proj_name) 2300 | self.file_select_dialog.show() 2301 | 2302 | @show_exceptions 2303 | def _submit_vray_job(self, layer_list, params, sf, ef): 2304 | """Collects info, exports vrscenes and sends jobs 2305 | 2306 | See also: export_vrscene 2307 | 2308 | Args: 2309 | layer_list: [str], List of layers names 2310 | params: dict, render job parameters 2311 | start_frame: int, the first frame to export 2312 | end_frame: int, the last frame to export 2313 | """ 2314 | print 'Vray job, collecting additional info...' 2315 | self.verify_vray_production_engine() 2316 | 2317 | print 'Exporting .vrscene files...' 2318 | for layer in layer_list: 2319 | print 'Exporting layer %s...' % layer 2320 | vrscene_path = self.get_standalone_scene_path('vrscene', layer=layer) 2321 | possible_scene_names, render_params = self.export_vrscene( 2322 | vrscene_path, layer, params, sf, ef) 2323 | 2324 | layer_file = None 2325 | for possible_scene_name in possible_scene_names: 2326 | if os.path.exists(possible_scene_name): 2327 | layer_file = possible_scene_name 2328 | break 2329 | if layer_file is None: 2330 | print 'Failed to find a .vrscene file. Looked for:' 2331 | for item in enumerate(possible_scene_names): 2332 | print "%s: %s" % item 2333 | raise zync.ZyncError( 2334 | 'the .vrscene file generated by the Zync Maya plugin ' 2335 | 'was not found. Unable to submit job.') 2336 | 2337 | print 'Submitting job for layer %s...' % layer 2338 | self.zync_conn.submit_job('vray', layer_file, params=render_params) 2339 | 2340 | @show_exceptions 2341 | def _submit_arnold_job(self, layer_list, params, sf, ef): 2342 | """Collects info, exports ass files and sends jobs 2343 | 2344 | See also: export_ass 2345 | 2346 | Args: 2347 | layer_list: [str], List of layers names 2348 | params: dict, render job parameters 2349 | start_frame: int, the first frame to export 2350 | end_frame: int, the last frame to export 2351 | """ 2352 | print 'Arnold job, collecting additional info...' 2353 | ass_path = self.get_standalone_scene_path('ass') 2354 | 2355 | print 'Exporting .ass files...' 2356 | for layer in layer_list: 2357 | print 'Exporting layer %s...' % layer 2358 | layer_file_wildcard, render_params = self.export_ass( 2359 | ass_path, layer, params, sf, ef) 2360 | print 'Submitting job for layer %s...' % layer 2361 | self.zync_conn.submit_job( 2362 | 'arnold', layer_file_wildcard, params=render_params) 2363 | 2364 | @show_exceptions 2365 | def submit(self): 2366 | """Submit a job to Zync.""" 2367 | if not self.zync_conn.has_user_login(): 2368 | raise maya_common.MayaZyncException('You must login before submitting a new job.') 2369 | 2370 | job_uses_standalone = (not self.is_maya_io or eval_ui('use_standalone', 'checkBox', v=True)) 2371 | 2372 | if not self.verify_eula_acceptance(not job_uses_standalone): 2373 | cmds.error('Job submission canceled.') 2374 | 2375 | print 'Collecting render parameters...' 2376 | scene_path = cmds.file(q=True, loc=True) 2377 | params = self.get_render_params() 2378 | 2379 | if 'PREEMPTIBLE' in params['instance_type']: 2380 | import pvm_consent_dialog 2381 | from settings import Settings 2382 | consent_dialog = pvm_consent_dialog.PvmConsentDialog() 2383 | if not Settings.get().get_pvm_ack() and not consent_dialog.prompt(): 2384 | return 2385 | 2386 | if params['sync_extra_assets']: 2387 | import_zync_python() 2388 | import file_select_dialog 2389 | proj_name = eval_ui('new_project_name', text=True) 2390 | extra_assets = file_select_dialog.FileSelectDialog.get_extra_assets(proj_name) 2391 | if not extra_assets: 2392 | raise maya_common.MayaZyncException('No extra assets selected') 2393 | 2394 | layers_to_render = (params['layers'].split(',') if params['layers'] else None) 2395 | if params['renderer'] == 'renderman': 2396 | renderman.init(layers_to_render, params['camera']) 2397 | 2398 | submission_checks = [ 2399 | SubmissionCheck( 2400 | check=lambda: '(ALPHA)' in params.get('instance_type', ''), 2401 | title='ALPHA instance type selected', 2402 | message='You\'ve selected an instance type for your job which is ' 2403 | 'still in alpha, and could be unstable for some workloads. ' 2404 | 'Are you sure you want to submit the job using this ' 2405 | 'instance type?'), 2406 | SubmissionCheck( 2407 | check=lambda: (cmds.attributeQuery('animation', node='defaultRenderGlobals', exists=True) and 2408 | not cmds.getAttr('defaultRenderGlobals.animation')), 2409 | title='Animation Off', 2410 | message='It looks like you have animation disabled in your scene. ' 2411 | 'If you render multiple frames they will probably overwrite ' 2412 | 'each other. Are you sure you want to submit the job using ' 2413 | 'these render settings?'), 2414 | SubmissionCheck( 2415 | check=lambda: (cmds.attributeQuery('modifyExtension', node='defaultRenderGlobals', exists=True) and 2416 | cmds.getAttr('defaultRenderGlobals.modifyExtension')), 2417 | always_fail=True, 2418 | title='Renumber Frames is On', 2419 | message='It looks like you have "Renumber Frames" enabled in your scene. This option is ' 2420 | 'not supported on Zync and will cause rendered frames to overwrite each other. Please ' 2421 | 'disable it before continuing.') 2422 | ] 2423 | 2424 | for submission_check in submission_checks: 2425 | submission_check.run_check() 2426 | 2427 | print 'Collecting scene info...' 2428 | try: 2429 | params['scene_info'] = get_scene_info(params['renderer'], 2430 | layers_to_render, 2431 | (eval_ui('job_type', ui_type='optionMenu', v=True).lower() == 'bake'), 2432 | extra_assets if params['sync_extra_assets'] else [], 2433 | parse_frame_range(params['frange'])) 2434 | except maya_common.ZyncAbortedByUser: 2435 | # If the job is aborted just finish the submit function 2436 | return 2437 | 2438 | params['plugin_version'] = __version__ 2439 | 2440 | try: 2441 | if job_uses_standalone: 2442 | frange_split = params['frange'].split(',') 2443 | sf = int(frange_split[0].split('-')[0]) 2444 | 2445 | if params['upload_only'] == 1: 2446 | layer_list = ['defaultRenderLayer'] 2447 | ef = sf 2448 | else: 2449 | layer_list = params['layers'].split(',') 2450 | ef = int(frange_split[-1].split('-')[-1]) 2451 | 2452 | SubmissionCheck( 2453 | check=output_has_layer_problems, 2454 | title='Layer not in output filename', 2455 | check_args=[params['renderer'], layer_list], 2456 | message='The specified File Name Prefix does not include a layer token (%l, , ). ' 2457 | 'The output rendered files may overwrite each other. Are you sure you want to submit?' 2458 | ).run_check() 2459 | 2460 | if params['renderer'] == 'vray': 2461 | self._submit_vray_job(layer_list, params, sf, ef) 2462 | elif params['renderer'] == 'arnold': 2463 | self._submit_arnold_job(layer_list, params, sf, ef) 2464 | else: 2465 | raise maya_common.MayaZyncException('Renderer %s unsupported for standalone rendering.' % params['renderer']) 2466 | 2467 | cmds.confirmDialog(title='Success', 2468 | message='{num_jobs} {label} submitted to Zync.'.format( 2469 | num_jobs=len(layer_list), 2470 | label='job' if len(layer_list) == 1 else 'jobs'), 2471 | button='OK', defaultButton='OK') 2472 | 2473 | else: 2474 | # Uncomment this section if you want to 2475 | # save a unique copy of the scene file each time your submit a job. 2476 | ''' 2477 | original_path = cmds.file(q=True, loc=True) 2478 | original_modified = cmds.file(q=True, modified=True) 2479 | scene_path = generate_scene_path() 2480 | cmds.file(rename=scene_path) 2481 | cmds.file(save=True, type='mayaAscii') 2482 | cmds.file(rename=original_path) 2483 | cmds.file(modified=original_modified) 2484 | ''' 2485 | 2486 | if (cmds.objExists('vraySettings') and 2487 | cmds.attributeQuery('vrscene_on', node='vraySettings', exists=True) and 2488 | cmds.getAttr('vraySettings.vrscene_on')): 2489 | raise maya_common.MayaZyncException('You have "Export to a .vrscene file" turned ' 2490 | 'on. This will cause Vray to attempt a scene ' 2491 | 'export rather than a render. Please disable ' 2492 | 'this option before submitting this scene to ' 2493 | 'Zync for rendering.') 2494 | 2495 | self.zync_conn.submit_job('maya', scene_path, params=params) 2496 | cmds.confirmDialog(title='Success', message='Job submitted to Zync.', 2497 | button='OK', defaultButton='OK') 2498 | 2499 | except zync.ZyncPreflightError as e: 2500 | cmds.confirmDialog(title='Preflight Check Failed', message=str(e), 2501 | button='OK', defaultButton='OK') 2502 | 2503 | except zync.ZyncError as e: 2504 | cmds.confirmDialog(title='Submission Error', 2505 | message='Error submitting job: %s' % (str(e),), 2506 | button='OK', defaultButton='OK', icon='critical') 2507 | 2508 | else: 2509 | print 'Done.' 2510 | 2511 | def verify_vray_production_engine(self): 2512 | if self.vray_production_engine_name not in [VRAY_ENGINE_NAME_CPU, VRAY_ENGINE_NAME_CUDA]: 2513 | raise maya_common.MayaZyncException('Current V-Ray production engine is not supported by Zync. ' 2514 | 'Please go to Render Settings -> VRay tab to change it to CPU or CUDA') 2515 | 2516 | @staticmethod 2517 | def export_vrscene(vrscene_path, layer, params, start_frame, end_frame): 2518 | """Export a .vrscene of the current scene. 2519 | 2520 | Args: 2521 | vrscene_path: str, path to which to export the .vrscene. A layer name will 2522 | be inserted into the filename. 2523 | layer: str, the name of the render layer to export 2524 | params: dict, render job parameters 2525 | start_frame: int, the first frame to export 2526 | end_frame: int, the last frame to export 2527 | 2528 | Returns: 2529 | tuple: 2530 | - list of possible locations where the .vrscene may be found (Vray adds 2531 | layer names automatically and is sometimes inconsistent) 2532 | - dict of render job parameters, with any modifications to make the 2533 | job run similarly with Vray standalone. 2534 | """ 2535 | cmds.undoInfo(openChunk=True) 2536 | 2537 | _switch_to_renderlayer(layer) 2538 | 2539 | scene_path = cmds.file(q=True, loc=True) 2540 | scene_head, extension = os.path.splitext(scene_path) 2541 | scene_name = os.path.basename(scene_head) 2542 | 2543 | render_params = copy.deepcopy(params) 2544 | 2545 | print '--> bake GI flag' 2546 | render_params['scene_info']['bake_gi'] = False 2547 | try: 2548 | if cmds.getAttr('vraySettings.gi'): 2549 | primary_engine = int(cmds.getAttr('vraySettings.pe')) 2550 | secondary_engine = int(cmds.getAttr('vraySettings.se')) 2551 | _NONE_RENDERER_ID = 0 2552 | _BRUTE_FORCE_RENDERER_ID = 2 2553 | render_params['scene_info']['bake_gi'] = primary_engine != _BRUTE_FORCE_RENDERER_ID or \ 2554 | (secondary_engine != _NONE_RENDERER_ID and secondary_engine != _BRUTE_FORCE_RENDERER_ID) 2555 | except: 2556 | pass 2557 | 2558 | render_params['scene_info']['render_layers'] = [layer] 2559 | render_params['project_dir'] = params['project'] 2560 | render_params['output_dir'] = params['out_path'] 2561 | render_params['use_nightly'] = params['vray_nightly'] 2562 | if ('extension' not in params['scene_info'] or 2563 | params['scene_info']['extension'] == None or 2564 | params['scene_info']['extension'].strip() == ''): 2565 | render_params['scene_info']['extension'] = 'png' 2566 | 2567 | tail = cmds.getAttr(NamePrefixAttributes.vray) 2568 | if not tail: 2569 | tail = scene_name 2570 | if len(params['layers'].split(',')) > 1: 2571 | tail += '_{}'.format(layer) 2572 | else: 2573 | clean_camera = render_params['camera'].replace(':', '_') 2574 | tail = replace_tokens_in_file_prefix(tail, scene_name, layer, clean_camera) 2575 | if tail[-1] != '.': 2576 | tail += '.' 2577 | 2578 | render_params['output_filename'] = '%s.%s' % (tail, render_params['scene_info']['extension']) 2579 | render_params['output_filename'] = render_params['output_filename'].replace('\\', '/') 2580 | 2581 | # Set up render globals for vray export. These changes will 2582 | # be reverted later when we run cmds.undo(). 2583 | # 2584 | # Turn "Don't save image" OFF - this will ensure Vray knows to translate 2585 | # all render output settings. 2586 | cmds.setAttr('vraySettings.dontSaveImage', 0) 2587 | # Turn rendering off. 2588 | cmds.setAttr('vraySettings.vrscene_render_on', 0) 2589 | # Turn Vrscene export on. 2590 | cmds.setAttr('vraySettings.vrscene_on', 1) 2591 | # Set the Vrscene export filename. 2592 | cmds.setAttr('vraySettings.vrscene_filename', vrscene_path, type='string') 2593 | # Ensure we export only a single file. 2594 | cmds.setAttr('vraySettings.misc_separateFiles', 0) 2595 | cmds.setAttr('vraySettings.misc_eachFrameInFile', 0) 2596 | 2597 | # Turn off Geom Cache. If you render a frame locally with this on, and then 2598 | # immediately export to zync, the cached geometry is written to the file. 2599 | # Any geo that has deformations are only rendered in the cached state and 2600 | # not updated per frame. This is an issue with Vray and using 'vrend' instead 2601 | # of BatchRender to export the vrscene. 2602 | try: 2603 | cmds.setAttr('vraySettings.globopt_cache_geom_plugins', 0) 2604 | cmds.setAttr('vraySettings.globopt_cache_bitmaps', 0) 2605 | # older versions of Vray do not have these settings. if they don't exist a 2606 | # RuntimeError will be raised, which we can ignore. 2607 | except RuntimeError: 2608 | pass 2609 | 2610 | # Set compression options. 2611 | cmds.setAttr('vraySettings.misc_meshAsHex', 1) 2612 | cmds.setAttr('vraySettings.misc_transformAsHex', 1) 2613 | cmds.setAttr('vraySettings.misc_compressedVrscene', 1) 2614 | # Turn the VFB off, make sure the viewer is hidden. 2615 | cmds.setAttr('vraySettings.vfbOn', 0) 2616 | cmds.setAttr('vraySettings.hideRVOn', 1) 2617 | # Ensure animation is fully enabled and configured with the correct 2618 | # frame range. This is usually the case already, but some users will 2619 | # have it disabled expecting their existing local farm to update 2620 | # with the correct settings. 2621 | cmds.setAttr('vraySettings.animBatchOnly', 0) 2622 | cmds.setAttr('defaultRenderGlobals.animation', 1) 2623 | cmds.setAttr('defaultRenderGlobals.startFrame', start_frame) 2624 | cmds.setAttr('defaultRenderGlobals.endFrame', end_frame) 2625 | # Set resolution of the scene to layer resolution to avoid problems with regions. 2626 | cmds.setAttr('vraySettings.width', render_params['xres']) 2627 | cmds.setAttr('vraySettings.height', render_params['yres']) 2628 | 2629 | # Run the export. 2630 | maya.mel.eval('vrend -camera "%s" -layer "%s"' % (render_params['camera'], layer)) 2631 | 2632 | queue_empty = cmds.undoInfo(query=True, undoQueueEmpty=True) 2633 | cmds.undoInfo(closeChunk=True) 2634 | if not queue_empty: 2635 | cmds.undo() 2636 | 2637 | vrscene_base, ext = os.path.splitext(vrscene_path) 2638 | if layer == 'defaultRenderLayer': 2639 | possible_scene_names = [ 2640 | '%s_masterLayer%s' % (vrscene_base, ext), 2641 | '%s%s' % (vrscene_base, ext), 2642 | '%s_defaultRenderLayer%s' % (vrscene_base, ext) 2643 | ] 2644 | else: 2645 | possible_scene_names = [ 2646 | '%s_%s%s' % (vrscene_base, layer, ext), 2647 | vrscene_path, 2648 | ] 2649 | # In older Vray versions rs_ prefix is used for rendersetup layers, in 2650 | # later versions it is ignored. 2651 | if layer.startswith('rs_'): 2652 | possible_scene_names.append('%s_%s%s' % (vrscene_base, layer[3:], ext)) 2653 | 2654 | return possible_scene_names, render_params 2655 | 2656 | @staticmethod 2657 | def export_ass(ass_path, layer, params, start_frame, end_frame): 2658 | """Export .ass files of the current scene. 2659 | 2660 | Args: 2661 | ass_path: str, path to which to export the .ass files 2662 | layer: str, the name of the render layer to export 2663 | params: dict, render job parameters 2664 | start_frame: int, the first frame to export 2665 | end_frame: int, the last frame to export 2666 | 2667 | Returns: 2668 | tuple: 2669 | - str path to the final export location. will contain a wildcard in 2670 | place of frame number, to indicate the set of files produced. 2671 | - dict of render job parameters, with any modifications to make the 2672 | job run similarly with Arnold standalone. 2673 | """ 2674 | cmds.undoInfo(openChunk=True) 2675 | 2676 | _switch_to_renderlayer(layer) 2677 | 2678 | # We need to remove 'rs_' prefix from layer name, 2679 | # as it is removed by Arnold during standalone rendering. 2680 | if layer.startswith('rs_'): 2681 | layer = layer[3:] 2682 | 2683 | scene_path = cmds.file(q=True, loc=True) 2684 | scene_head, extension = os.path.splitext(scene_path) 2685 | scene_name = os.path.basename(scene_head) 2686 | 2687 | render_params = copy.deepcopy(params) 2688 | 2689 | render_params['project_dir'] = params['project'] 2690 | render_params['output_dir'] = params['out_path'] 2691 | 2692 | tail = cmds.getAttr(NamePrefixAttributes.arnold) 2693 | if not tail: 2694 | tail = scene_name 2695 | if len(params['layers'].split(',')) > 1: 2696 | tail += '_{}'.format(layer) 2697 | else: 2698 | clean_camera = params['camera'].replace(':', '_') 2699 | tail = replace_tokens_in_file_prefix(tail, scene_name, layer, clean_camera) 2700 | try: 2701 | render_version = cmds.getAttr('defaultRenderGlobals.renderVersion') 2702 | if render_version != None: 2703 | tail = re.sub('%v|', 2704 | cmds.getAttr('defaultRenderGlobals.renderVersion'), 2705 | tail, flags=re.IGNORECASE) 2706 | except ValueError: 2707 | pass 2708 | if tail[-1] != '.': 2709 | tail += '.' 2710 | 2711 | render_params['output_filename'] = '%s.%s' % (tail, params['scene_info']['extension']) 2712 | render_params['output_filename'] = render_params['output_filename'].replace('\\', '/') 2713 | 2714 | ass_base, ext = os.path.splitext(ass_path) 2715 | layer_mangled = base64.b64encode(layer)[-4:] 2716 | layer_file = '%s_%s_%s%s' % (ass_base, layer, layer_mangled, ext) 2717 | layer_file_wildcard = '%s_%s*%s' % (ass_base, layer, ext) 2718 | 2719 | # Override renderSetup options to keep exported *.ass files names consistent 2720 | # with what backend expects (filename.framenumber.ext), see b/128825029 2721 | maya.cmds.setAttr('defaultRenderGlobals.putFrameBeforeExt', 1) 2722 | maya.cmds.setAttr('defaultRenderGlobals.periodInExt', 1) 2723 | ass_cmd = ('arnoldExportAss -f "%s" -endFrame %s -mask 255 ' % (layer_file, end_frame) + 2724 | '-lightLinks 1 -frameStep %d.0 -startFrame %s ' % (render_params['step'], start_frame) + 2725 | '-shadowLinks 1 -cam %s' % (params['camera'],)) 2726 | maya.mel.eval(ass_cmd) 2727 | 2728 | queue_empty = cmds.undoInfo(query=True, undoQueueEmpty=True) 2729 | cmds.undoInfo(closeChunk=True) 2730 | if not queue_empty: 2731 | cmds.undo() 2732 | 2733 | return layer_file_wildcard, render_params 2734 | 2735 | def get_standalone_scene_path(self, suffix, layer=None): 2736 | """Get a file path for exporting a standalone scene, based on current scene 2737 | and matching the Zync convention of where these files should be stored. 2738 | 2739 | This does NOT perform the actual export, only returns the path at which 2740 | it should be stored. 2741 | 2742 | Args: 2743 | suffix: str, the suffix of the filename e.g. "vrscene" or "ass" 2744 | layer: str, the layer name to append to the file path 2745 | 2746 | Returns: 2747 | str the standalone scene file path 2748 | """ 2749 | scene_path = cmds.file(q=True, loc=True) 2750 | scene_head, _ = os.path.splitext(scene_path) 2751 | if layer is not None: 2752 | scene_head += '_%s' % layer 2753 | return self.zync_conn.generate_file_path( 2754 | '%s.%s' % (scene_head, suffix)).replace('\\', '/') 2755 | 2756 | def verify_eula_acceptance(self, is_mayaio_job): 2757 | """Verify EULA/ToS acceptance and if needed perform acceptance flow. 2758 | 2759 | Args: 2760 | is_mayaio_job: bool, whether the intended job will make use of Maya I/O. 2761 | 2762 | Returns: 2763 | bool, True if all required agreements are accepted, False if user declined 2764 | """ 2765 | applicable_eula_types = ['zync', 'cloud', 'licensor'] 2766 | if is_mayaio_job: 2767 | applicable_eula_types.append('mayaio') 2768 | to_accept = [eula for eula in self.zync_conn.get_eulas() 2769 | if eula.get('eula_kind').lower() in applicable_eula_types] 2770 | # Blank accepted_by field indicates agreement is not yet accepted. 2771 | not_accepted = [eula for eula in to_accept if not eula.get('accepted_by')] 2772 | if not_accepted: 2773 | eula_url = '%s/account#legal' % self.zync_conn.url 2774 | cmds.confirmDialog( 2775 | title='Accept Agreement', 2776 | message=( 2777 | 'Please read and accept the required EULA(s) and Terms of Service(s). ' 2778 | 'A browser window will open where you can do this.\n\nURL: %s' % eula_url), 2779 | button=['OK'], 2780 | defaultButton='OK') 2781 | webbrowser.open(eula_url) 2782 | eula_response = cmds.confirmDialog( 2783 | title='Accept Agreement', 2784 | message='Have you accepted all agreements?', 2785 | button=['Yes', 'No'], 2786 | defaultButton='Yes', 2787 | cancelButton='No', 2788 | dismissString='No') 2789 | 2790 | if eula_response == 'No': 2791 | return False 2792 | 2793 | return True 2794 | 2795 | 2796 | @show_exceptions 2797 | def submit_dialog(): 2798 | submit_window = SubmitWindow() 2799 | submit_window.show() 2800 | # show update notification last so it gets focus 2801 | if not is_latest_version(): 2802 | show_update_notification() 2803 | 2804 | 2805 | def is_latest_version(): 2806 | global _VERSION_CHECK_RESULT 2807 | if _VERSION_CHECK_RESULT is None: 2808 | try: 2809 | import_zync_python() 2810 | _VERSION_CHECK_RESULT = zync.is_latest_version([('zync_maya', __version__)]) 2811 | # if there's an exception during version check, print the exception but 2812 | # assume user is up to date. we don't want to block them launching jobs. 2813 | except: 2814 | print 'Exception checking version number' 2815 | print traceback.format_exc() 2816 | return True 2817 | return _VERSION_CHECK_RESULT 2818 | 2819 | 2820 | def replace_tokens_in_file_prefix(file_prefix, scene_name, layer, camera): 2821 | """ 2822 | Replace various tokens in the file output prefix with values from the scene. 2823 | 2824 | Args: 2825 | file_prefix: str, string containing tokens to be replaced. 2826 | scene_name: str, name of scene file to replace _SUBSTITUTE_SCENE_TOKEN_RE. 2827 | layer: str, name of layer to replace _SUBSTITUTE_LAYER_TOKEN_RE. 2828 | camera: str, name of camera to replace _SUBSTITUTE_CAMERA_TOKEN_RE. 2829 | 2830 | Returns: 2831 | str, token replaced file prefix. 2832 | """ 2833 | mappings = ( 2834 | (maya_common._SUBSTITUTE_SCENE_TOKEN_RE, scene_name), 2835 | (maya_common._SUBSTITUTE_LAYER_TOKEN_RE, layer), 2836 | (maya_common._SUBSTITUTE_CAMERA_TOKEN_RE, camera), 2837 | ) 2838 | for regex, value in mappings: 2839 | file_prefix = re.sub(regex, value, file_prefix) 2840 | return file_prefix 2841 | 2842 | 2843 | def output_has_layer_problems(renderer, layer_list): 2844 | """ 2845 | Submission check to ensure a layer token (%l, , or ) exists in render file name output attribute 2846 | and that there are multiple render layer in the layer_list. If the prefix is empty, return False since we'll take 2847 | care of setting the output path. 2848 | 2849 | Args: 2850 | renderer: str, name of renderer. 2851 | layer_list: [str], list of string names of layers to be checked. 2852 | 2853 | Returns: 2854 | bool, True if outputs are problematic False if outputs are safe 2855 | """ 2856 | try: 2857 | output_prefix = cmds.getAttr(NamePrefixAttributes.get_prefix(renderer)) 2858 | except AttributeError: 2859 | raise maya_common.MayaZyncException('Renderer %s unsupported for rendering.' % renderer) 2860 | if output_prefix is None: 2861 | return False 2862 | return len(layer_list) > 1 and not re.match(maya_common._HAS_LAYER_TOKEN_RE, output_prefix) 2863 | 2864 | 2865 | def show_update_notification(): 2866 | def _link(url, text): 2867 | return ('%s') % (url, text) 2868 | 2869 | window_name = cmds.window(title='Zync Update Available', width=400, height=165) 2870 | 2871 | cmds.columnLayout('l', rowSpacing=8, columnAttach=('both', 100)) 2872 | cmds.text(label='
An update to the Zync plugin has
been released.', 2873 | align="center", width=200) 2874 | cmds.text(label=_link('https://download.zyncrender.com', 'Download the Update'), 2875 | hyperlink=True, align="center", width=200) 2876 | cmds.text(label=_link('https://docs.zyncrender.com/update-plugins', 2877 | 'Plugin Update HOWTO'), hyperlink=True, align="center", width=200) 2878 | cmds.text(label=' Once the update is installed, please
restart Maya to complete the process.', 2879 | align="center", width=200) 2880 | cmds.button(label='Close', width=200, align='center', 2881 | command='cmds.deleteUI("%s", window=True)' % window_name) 2882 | 2883 | cmds.showWindow() 2884 | --------------------------------------------------------------------------------