├── .gitignore ├── README.md ├── config_nuke.py.example └── zync_nuke.py /.gitignore: -------------------------------------------------------------------------------- 1 | config_nuke.py 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zync Plugin for Nuke 2 | 3 | **Notice:** this project has been archived and is no longer being maintained. 4 | 5 | Tested with all versions of Nuke 7 and 8. 6 | 7 | ## zync-python 8 | 9 | This plugin depends on zync-python, the Zync Python API. 10 | 11 | Before trying to install zync-nuke, 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 this folder you'll find a file called ```config_nuke.py.example```. Make a copy of this file in the same directory, and rename it ```config_nuke.py```. 24 | 25 | Edit ```config_nuke.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 | ## Set Up menu.py 30 | 31 | You'll need to locate your .nuke folder, usually stored within your HOME folder. In there you'll need a file called menu.py. This file may already exist. 32 | 33 | menu.py should contain the following text: 34 | 35 | ```python 36 | import nuke 37 | nuke.pluginAddPath('/path/to/zync-nuke') 38 | import zync_nuke 39 | menubar = nuke.menu('Nuke'); 40 | menu = menubar.addMenu('&Render') 41 | menu.addCommand('Render on Zync', 'zync_nuke.submit_dialog()') 42 | ``` 43 | 44 | This will add an item to the "Render" menu in Zync that will allow you to launch Zync jobs. 45 | 46 | ## Done 47 | 48 | That's it! Restart Nuke to pull in the changes you made. 49 | -------------------------------------------------------------------------------- /config_nuke.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 | -------------------------------------------------------------------------------- /zync_nuke.py: -------------------------------------------------------------------------------- 1 | """Zync Nuke Plugin 2 | 3 | This package provides a Python-based Nuke plugin 4 | for launching jobs to the Zync Render Platform. 5 | 6 | Usage as a menu item: 7 | nuke.pluginAddPath('/path/to/zync-nuke') 8 | import zync_nuke 9 | menu.addCommand('Render on Zync', 'zync_nuke.submit_dialog()') 10 | """ 11 | 12 | import nuke 13 | import nukescripts 14 | import platform 15 | import os 16 | import re 17 | 18 | __version__ = '1.3.3' 19 | 20 | READ_NODE_CLASSES = ['AudioRead', 'Axis', 'Axis2', 'Camera', 'Camera2', 'DeepRead', 'OCIOFileTransform', 21 | 'ParticleCache', 'Precomp', 'Read', 'ReadGeo', 'ReadGeo2', 'Vectorfield' ] 22 | WRITE_NODE_CLASSES = ['DeepWrite', 'GenerateLUT', 'Write', 'WriteGeo'] 23 | PATH_KNOB_NAMES = ['proxy', 'file', 'vfield_file'] 24 | 25 | 26 | if os.environ.get('ZYNC_API_DIR'): 27 | API_DIR = os.environ.get('ZYNC_API_DIR') 28 | else: 29 | config_path = os.path.join(os.path.dirname(__file__), 'config_nuke.py') 30 | if not os.path.exists(config_path): 31 | raise Exception('Could not locate config_nuke.py, please create.') 32 | from config_nuke import * 33 | 34 | required_config = ['API_DIR'] 35 | 36 | for key in required_config: 37 | if not key in globals(): 38 | raise Exception('config_nuke.py must define a value for %s.' % (key,)) 39 | 40 | nuke.pluginAddPath(API_DIR) 41 | import zync 42 | 43 | 44 | def get_dependent_nodes(root): 45 | """Returns a list of all of the root node's dependencies. 46 | 47 | Uses `nuke.dependencies()`. This will work with nested dependencies. 48 | """ 49 | all_deps = {root} 50 | all_deps.update(nuke.dependencies(list(all_deps))) 51 | 52 | seen = set() 53 | while True: 54 | diff = all_deps - seen 55 | to_add = nuke.dependencies(list(diff)) 56 | all_deps.update(to_add) 57 | seen.update(diff) 58 | if len(diff) == 0: 59 | break 60 | 61 | return list(all_deps) 62 | 63 | 64 | def select_deps(nodes): 65 | """Selects all of the dependent nodes for the given list of nodes.""" 66 | for node in nodes: 67 | for node in get_dependent_nodes(node): 68 | node.setSelected(True) 69 | 70 | 71 | def freeze_node(node, view=None): 72 | """If the node has an expression, evaluate it so that Zync receives a file path it can understand. 73 | 74 | Accounts for and retains frame number expressions. 75 | """ 76 | 77 | def is_knob_rewritable(knob): 78 | return knob is not None and isinstance(knob.value(), basestring) and knob.value() 79 | 80 | node_classes_to_absolutize = ['AudioRead', 'Axis', 'Axis2' 'Camera', 'Camera2' 'DeepRead', 'DeepWrite', 81 | 'GenerateLUT', 'OCIOFileTransform', 'ParticleCache', 82 | 'Precomp', 'Read', 'ReadGeo', 'ReadGeo2', 'Vectorfield', 83 | 'Write', 'WriteGeo'] 84 | 85 | for knob_name in PATH_KNOB_NAMES: 86 | knob = node.knob(knob_name) 87 | if is_knob_rewritable(knob): 88 | _evaluate_path_expression(node, knob) 89 | if node.Class() in node_classes_to_absolutize: 90 | # Nuke scene can have file paths relative to project directory 91 | _maybe_absolutize_path(knob) 92 | if view: 93 | _expand_view_tokens_in_path(knob, view) 94 | _clean_path(knob) 95 | 96 | 97 | def _evaluate_path_expression(node, knob): 98 | knob_value = knob.value() 99 | # If the knob value has an open bracket, assume it's an expression. 100 | if '[' in knob_value: 101 | if node.Class() in WRITE_NODE_CLASSES: 102 | knob.setValue(nuke.filename(node)) 103 | else: 104 | # Running knob.evaluate() will freeze not just expressions, but frame number as well. Use regex to search for 105 | # any frame number expressions, and replace them with a placeholder. 106 | to_eval = knob_value 107 | placeholders = {} 108 | regexs = [r'#+', r'%.*d'] 109 | for regex in regexs: 110 | match = 1 111 | while match: 112 | match = re.search(regex, to_eval) 113 | if match: 114 | placeholder = '__frame%d' % (len(placeholders) + 1,) 115 | original = match.group() 116 | placeholders[placeholder] = original 117 | to_eval = to_eval[0:match.start()] + '{%s}' % (placeholder,) + to_eval[match.end():] 118 | # Set the knob value to our string with placeholders. 119 | knob.setValue(to_eval) 120 | # Now evaluate the knob to freeze the path. 121 | frozen_path = knob.evaluate() 122 | # Use our dictionary of placeholders to place the original frame number expressions back in. 123 | frozen_path = frozen_path.format(**placeholders) 124 | # Finally, set the frozen path back to the knob. 125 | knob.setValue(frozen_path) 126 | 127 | 128 | def _maybe_absolutize_path(knob): 129 | if not os.path.isabs(knob.value()): 130 | project_dir = _get_project_directory() 131 | absolute_path = os.path.abspath(os.path.join(project_dir, knob.value())) 132 | knob.setValue(absolute_path) 133 | 134 | 135 | def _get_project_directory(): 136 | project_dir = nuke.root().knob('project_directory').evaluate() 137 | if not project_dir: 138 | # When no project dir is set, return the dir in which Nuke scene lives 139 | project_dir = os.path.dirname(nuke.root().knob('name').getValue()) 140 | return project_dir 141 | 142 | 143 | def _expand_view_tokens_in_path(knob, view): 144 | view_expanded_path = knob.value() 145 | # Token %v is replaced with the first letter of the view name 146 | view_expanded_path = view_expanded_path.replace('%v', view[0]) 147 | # Token %V is replaced with the full name of the view 148 | view_expanded_path = view_expanded_path.replace('%V', view) 149 | knob.setValue(view_expanded_path) 150 | 151 | 152 | def _clean_path(knob): 153 | path = knob.value() 154 | path = path.replace('\\', '/') 155 | knob.setValue(path) 156 | 157 | 158 | def gizmos_to_groups(nodes): 159 | """If the node is a Gizmo, use makeGroup() to turn it into a Group.""" 160 | # Deselect all nodes. catch errors for nuke versons that don't support the recurseGroups option. 161 | try: 162 | node_list = nuke.allNodes(recurseGroups=True) 163 | except: 164 | node_list = nuke.allNodes() 165 | for node in node_list: 166 | node.setSelected(False) 167 | for node in nodes: 168 | if hasattr(node, 'makeGroup') and callable(getattr(node, 'makeGroup')): 169 | node.setSelected(True) 170 | node.makeGroup() 171 | nuke.delete(node) 172 | 173 | 174 | class WriteChanges(object): 175 | """Given a script to save to, will save all of the changes made in the with block to the script, 176 | 177 | then undoes those changes in the current script. 178 | 179 | For example: 180 | with WriteChanges('/Volumes/af/show/omg/script.nk'): 181 | for node in nuke.allNodes(): 182 | node.setYpos(100) 183 | """ 184 | 185 | def __init__(self, script, save_func=None): 186 | """Initialize a WriteChanges context manager. 187 | 188 | Must provide a script to write to. 189 | If you provide a save_func, it will be called instead of the default 190 | `nuke.scriptSave`. The function must have the same interface as 191 | `nuke.scriptSave`. A possible alternative is `nuke.nodeCopy`. 192 | """ 193 | self.undo = nuke.Undo 194 | self.__disabled = self.undo.disabled() 195 | self.script = script 196 | if save_func: 197 | self.save_func = save_func 198 | else: 199 | self.save_func = nuke.scriptSave 200 | 201 | def __enter__(self): 202 | """Enters the with block. 203 | 204 | NOTE: does not return an object, so assigment using 'as' doesn't work: 205 | `with WriteChanges('foo') as wc:` 206 | """ 207 | if self.__disabled: 208 | self.undo.enable() 209 | 210 | self.undo.begin() 211 | 212 | def __exit__(self, type, value, traceback): 213 | """Exits the with block. 214 | 215 | First it calls the save_func, then undoes all actions in the with 216 | context, leaving the state of the current script untouched. 217 | """ 218 | self.save_func(self.script) 219 | self.undo.cancel() 220 | if self.__disabled: 221 | self.undo.disable() 222 | 223 | 224 | class ZyncRenderPanel(nukescripts.panels.PythonPanel): 225 | 226 | def __init__(self): 227 | if nuke.root().name() == 'Root' or nuke.modified(): 228 | msg = 'Please save your script before rendering on Zync.' 229 | raise Exception(msg) 230 | 231 | self.zync_conn = zync.Zync(application='nuke') 232 | 233 | nukescripts.panels.PythonPanel.__init__(self, 'Zync Render', 'com.google.zync') 234 | 235 | if platform.system() in ('Windows', 'Microsoft'): 236 | self.usernameDefault = os.environ['USERNAME'] 237 | else: 238 | self.usernameDefault = os.environ['USER'] 239 | 240 | # Get write nodes from scene 241 | self.writeListNames = [] 242 | self.writeDict = dict() 243 | self.update_write_dict() 244 | 245 | # Create UI knobs 246 | self.num_slots = nuke.Int_Knob('num_slots', 'Num. Machines:') 247 | self.num_slots.setDefaultValue((1,)) 248 | 249 | sorted_types = [t for t in self.zync_conn.INSTANCE_TYPES] 250 | sorted_types.sort(self.zync_conn.compare_instance_types) 251 | display_list = [] 252 | for inst_type in sorted_types: 253 | inst_desc = self.zync_conn.INSTANCE_TYPES[inst_type]['description'].replace(', preemptible', '') 254 | label = '%s (%s)' % (inst_type, inst_desc) 255 | inst_type_base = inst_type.split(' ')[-1] 256 | pricing_key = 'CP-ZYNC-%s-NUKE' % (inst_type_base.upper(),) 257 | if 'PREEMPTIBLE' in inst_type.upper(): 258 | pricing_key += '-PREEMPTIBLE' 259 | if (pricing_key in self.zync_conn.PRICING['gcp_price_list'] and 'us' in self.zync_conn.PRICING['gcp_price_list'][ 260 | pricing_key]): 261 | label += ' $%s/hr' % (self.zync_conn.PRICING['gcp_price_list'][pricing_key]['us'],) 262 | display_list.append(label) 263 | self.instance_type = nuke.Enumeration_Knob('instance_type', 'Type:', display_list) 264 | 265 | self.pricing_label = nuke.Text_Knob('pricing_label', '') 266 | self.pricing_label.setValue('Est. Cost per Hour: Not Available') 267 | 268 | calculator_link = nuke.Text_Knob('calculator_link', '') 269 | calculator_link.setValue('Cost Calculator') 271 | 272 | proj_response = self.zync_conn.get_project_list() 273 | existing_projects = [' '] + [p['name'] for p in proj_response] 274 | self.existing_project = nuke.Enumeration_Knob('existing_project', 'Existing Project:', existing_projects) 275 | 276 | self.new_project = nuke.String_Knob('project', ' New Project:') 277 | self.new_project.clearFlag(nuke.STARTLINE) 278 | 279 | self.upload_only = nuke.Boolean_Knob('upload_only', 'Upload Only') 280 | self.upload_only.setFlag(nuke.STARTLINE) 281 | 282 | self.parent_id = nuke.String_Knob('parent_id', 'Parent ID:') 283 | self.parent_id.setValue('') 284 | 285 | self.priority = nuke.Int_Knob('priority', 'Job Priority:') 286 | self.priority.setDefaultValue((50,)) 287 | 288 | self.skip_check = nuke.Boolean_Knob('skip_check', 'Skip File Sync') 289 | self.skip_check.setFlag(nuke.STARTLINE) 290 | 291 | first = nuke.root().knob('first_frame').value() 292 | last = nuke.root().knob('last_frame').value() 293 | frange = '%d-%d' % (first, last) 294 | self.frange = nuke.String_Knob('frange', 'Frame Range:', frange) 295 | 296 | self.fstep = nuke.Int_Knob('fstep', 'Frame Step:') 297 | self.fstep.setDefaultValue((1,)) 298 | 299 | selected_write_nodes = [] 300 | for node in nuke.selectedNodes(): 301 | if node.Class() in WRITE_NODE_CLASSES: 302 | selected_write_nodes.append(node.name()) 303 | self.writeNodes = [] 304 | col_num = 1 305 | for writeName in self.writeListNames: 306 | knob = nuke.Boolean_Knob(writeName, writeName) 307 | if len(selected_write_nodes) == 0: 308 | knob.setValue(True) 309 | elif writeName in selected_write_nodes: 310 | knob.setValue(True) 311 | else: 312 | knob.setValue(False) 313 | if col_num == 1: 314 | knob.setFlag(nuke.STARTLINE) 315 | if col_num > 3: 316 | col_num = 1 317 | else: 318 | col_num += 1 319 | knob.setTooltip(self.writeDict[writeName].knob('file').value()) 320 | self.writeNodes.append(knob) 321 | 322 | self.chunk_size = nuke.Int_Knob('chunk_size', 'Chunk Size:') 323 | self.chunk_size.setDefaultValue((10,)) 324 | 325 | # controls for logging in and out 326 | self.loginButton = nuke.Script_Knob('login', 'Login With Google') 327 | self.logoutButton = nuke.Script_Knob('logout', 'Logout') 328 | # keep everything on the same line 329 | self.logoutButton.clearFlag(nuke.STARTLINE) 330 | self.userLabel = nuke.Text_Knob('user_label', '') 331 | self.userLabel.setValue(' %s' % self.zync_conn.email) 332 | self.userLabel.clearFlag(nuke.STARTLINE) 333 | 334 | # these buttons must be named okButton and cancelButton for Nuke to add default OK/Cancel functionality. 335 | # if named something else, Nuke will add its own default buttons. 336 | self.okButton = nuke.Script_Knob('submit', 'Submit Job') 337 | self.cancelButton = nuke.Script_Knob('cancel', 'Cancel') 338 | 339 | self.addKnob(self.num_slots) 340 | self.addKnob(self.instance_type) 341 | self.addKnob(self.pricing_label) 342 | self.addKnob(calculator_link) 343 | self.addKnob(ZyncRenderPanel._get_divider()) 344 | self.addKnob(self.existing_project) 345 | self.addKnob(self.new_project) 346 | self.addKnob(self.parent_id) 347 | self.addKnob(self.upload_only) 348 | self.addKnob(self.priority) 349 | self.addKnob(self.skip_check) 350 | self.addKnob(self.frange) 351 | self.addKnob(self.fstep) 352 | for k in self.writeNodes: 353 | self.addKnob(k) 354 | self.addKnob(self.chunk_size) 355 | self.addKnob(ZyncRenderPanel._get_divider()) 356 | self.addKnob(self.loginButton) 357 | self.addKnob(self.logoutButton) 358 | self.addKnob(self.userLabel) 359 | self.addKnob(ZyncRenderPanel._get_divider()) 360 | self.addKnob(self.okButton) 361 | self.addKnob(self.cancelButton) 362 | 363 | # Collect render-specific knobs for iterating on later 364 | self.render_knobs = ( 365 | self.num_slots, self.instance_type, self.frange, self.fstep, self.chunk_size, self.skip_check, self.priority, 366 | self.parent_id) 367 | 368 | self.setMinimumSize(600, 410) 369 | self.update_pricing_label() 370 | 371 | @staticmethod 372 | def _get_divider(): 373 | """Get a divider, a horizontal line used for organizing UI elements.""" 374 | return nuke.Text_Knob('divider', '', '') 375 | 376 | def update_write_dict(self): 377 | wd = dict() 378 | for node in (x for x in nuke.allNodes() if x.Class() in WRITE_NODE_CLASSES): 379 | if not node.knob('disable').value(): 380 | wd[node.name()] = node 381 | 382 | self.writeDict.update(wd) 383 | self.writeListNames = self.writeDict.keys() 384 | self.writeListNames.sort() 385 | 386 | 387 | @staticmethod 388 | def _get_caravr_version(): 389 | """Returns CaraVR version, if present or empty string otherwise. 390 | 391 | To discover CaraVR we use plugin path list, which should contain a record of 392 | the form: 393 | '/Library/Application Support/Nuke/10.0/plugins/CaraVR/1.0/ToolSets/CaraVR' 394 | (OSX) 395 | 'C:\\Program Files\\Common 396 | Files/Nuke/11.0/plugins\\CaraVR\\1.0\\ToolSets/CaraVR' (Windows) 397 | We take path apart from the right until we encounter "ToolSet" component and 398 | the one preceding it is 399 | considered to be a version. This has been offered as a canonical way by The 400 | Foundry Support. 401 | """ 402 | cara_plugins = nuke.plugins(nuke.ALL, 'CaraVR') 403 | for cara_plugin in cara_plugins: 404 | if 'toolsets' in cara_plugin.lower(): 405 | remaining_path = cara_plugin.lower() 406 | last_folder_name = '' 407 | while remaining_path: 408 | remaining_path, folder_name = os.path.split(remaining_path) 409 | if last_folder_name == 'toolsets': 410 | return folder_name 411 | last_folder_name = folder_name 412 | return None 413 | 414 | def get_params(self): 415 | """Returns a dictionary of the job parameters from the submit render gui.""" 416 | params = dict() 417 | params['plugin_version'] = __version__ 418 | params['num_instances'] = self.num_slots.value() 419 | 420 | for inst_type in self.zync_conn.INSTANCE_TYPES: 421 | if self.instance_type.value().startswith(inst_type): 422 | params['instance_type'] = inst_type 423 | 424 | # these fields can't both be blank, we check in submit() before 425 | # reaching this point 426 | params['proj_name'] = self.existing_project.value().strip() 427 | if params['proj_name'] == '': 428 | params['proj_name'] = self.new_project.value().strip() 429 | 430 | params['frange'] = self.frange.value() 431 | params['step'] = self.fstep.value() 432 | params['chunk_size'] = self.chunk_size.value() 433 | params['upload_only'] = int(self.upload_only.value()) 434 | params['priority'] = int(self.priority.value()) 435 | parent = self.parent_id.value() 436 | if parent != None and parent != '': 437 | params['parent_id'] = int(self.parent_id.value()) 438 | 439 | params['start_new_instances'] = '1' 440 | params['skip_check'] = '1' if self.skip_check.value() else '0' 441 | params['notify_complete'] = '0' 442 | params['scene_info'] = {'nuke_version': nuke.NUKE_VERSION_STRING, 'views': nuke.views()} 443 | caravr_version = ZyncRenderPanel._get_caravr_version() 444 | if caravr_version: 445 | params['scene_info']['caravr_version'] = caravr_version 446 | 447 | return params 448 | 449 | def submit_checks(self): 450 | """Check current settings and raise errors for anything that 451 | 452 | could cause problems when submitting the job. 453 | 454 | Raises: 455 | zync.ZyncError for any issues found 456 | """ 457 | if not self.zync_conn.has_user_login(): 458 | raise zync.ZyncError('Please login before submitting a job.') 459 | 460 | if self.existing_project.value().strip() == '' and self.new_project.value().strip() == '': 461 | raise zync.ZyncError( 462 | 'Project name cannot be blank. Please either choose ' + 'an existing project from the dropdown or enter the desired ' + 'project name in the New Project field.') 463 | 464 | if self.skip_check.value(): 465 | skip_answer = nuke.ask( 466 | 'You\'ve asked Zync to skip the file check ' + 'for this job. If you\'ve added new files to your script this ' + 'job WILL error. Your nuke script will still be uploaded. Are ' + 'you sure you want to continue?') 467 | if not skip_answer: 468 | raise zync.ZyncError('Job submission canceled.') 469 | 470 | def submit(self): 471 | """Does the work to submit the current Nuke script to Zync, given that the parameters on the dialog are set.""" 472 | 473 | selected_write_names = [] 474 | selected_write_nodes = [] 475 | for k in self.writeNodes: 476 | if k.value(): 477 | selected_write_names.append(k.label()) 478 | selected_write_nodes.append(nuke.toNode(k.label())) 479 | 480 | active_viewer = nuke.activeViewer() 481 | if active_viewer: 482 | viewer_input = active_viewer.activeInput() 483 | if viewer_input is None: 484 | viewed_node = None 485 | else: 486 | viewed_node = active_viewer.node().input(viewer_input) 487 | else: 488 | viewer_input, viewed_node = None, None 489 | 490 | script_path = nuke.root().knob('name').getValue() 491 | new_script = self.maybe_correct_path_separators(script_path) 492 | write_node_to_user_path_map = dict() 493 | read_dependencies = [] 494 | 495 | with WriteChanges(new_script): 496 | # Nuke 7.0v1 through 7.0v8 broke its own undo() functionality, so this will only run on versions other than those. 497 | if nuke.NUKE_VERSION_MAJOR != 7 or nuke.NUKE_VERSION_MINOR > 0 or nuke.NUKE_VERSION_RELEASE > 8: 498 | # Remove all nodes that aren't connected to the Write nodes being rendered. 499 | select_deps(selected_write_nodes) 500 | for node in nuke.allNodes(): 501 | if node.isSelected(): 502 | node.setSelected(False) 503 | else: 504 | node.setSelected(True) 505 | nuke.nodeDelete() 506 | # Freeze expressions on all nodes. Catch errors for Nuke versions that don't support the recurseGroups option. 507 | try: 508 | node_list = nuke.allNodes(recurseGroups=True) 509 | except: 510 | node_list = nuke.allNodes() 511 | for node in node_list: 512 | freeze_node(node) 513 | 514 | _collect_write_node_paths(selected_write_names, write_node_to_user_path_map) 515 | read_nodes = [read_node for read_node in node_list if read_node.Class() in READ_NODE_CLASSES] 516 | _collect_read_node_paths(read_nodes, read_dependencies) 517 | 518 | # reconnect the viewer 519 | if viewer_input is not None and viewed_node is not None: 520 | nuke.connectViewer(viewer_input, viewed_node) 521 | 522 | # exec before render 523 | # nuke.callbacks.beforeRenders 524 | 525 | try: 526 | render_params = self.get_params() 527 | if render_params is None: 528 | return 529 | render_params['scene_info']['write_node_to_output_map'] = write_node_to_user_path_map 530 | render_params['scene_info']['files'] = read_dependencies 531 | self.zync_conn.submit_job('nuke', new_script, ','.join(selected_write_names), render_params) 532 | except zync.ZyncPreflightError as e: 533 | raise Exception('Preflight Check Failed:\n\n%s' % (str(e),)) 534 | 535 | nuke.message('Job submitted to ZYNC.') 536 | 537 | def knobChanged(self, knob): 538 | """Handles knob callbacks.""" 539 | if knob is self.okButton: 540 | # Run presubmit checks to make sure the job is ready to be launched with the currently selected parameters. we do 541 | # this here so we can display errors to the user before the dialog closes and destroys all of their settings. we 542 | # cannot do the full job submission here though, because trying to use nuke.Undo functionality while a modal 543 | # dialog is open crashes Nuke. 544 | try: 545 | self.submit_checks() 546 | # Raised exceptions will automatically cause Nuke to abort and leave the dialog open. we just capture that and 547 | # show a message to the user so they know what went wrong. the full exception will be printed to the Script 548 | # Editor for further debugging. 549 | except Exception as e: 550 | nuke.message(str(e)) 551 | raise 552 | elif knob is self.loginButton: 553 | # Run the auth flow, and display the user's email address, adding a little whitespace padding for visual clarity. 554 | self.userLabel.setValue(' %s' % self.zync_conn.login_with_google()) 555 | elif knob is self.logoutButton: 556 | self.zync_conn.logout() 557 | self.userLabel.setValue('') 558 | elif knob is self.upload_only: 559 | checked = self.upload_only.value() 560 | for rk in self.render_knobs: 561 | rk.setEnabled(not checked) 562 | for k in self.writeNodes: 563 | k.setEnabled(not checked) 564 | elif knob is self.num_slots or knob is self.instance_type: 565 | self.update_pricing_label() 566 | 567 | def showModalDialog(self): 568 | """Shows the Zync Submit dialog and does the work to submit it.""" 569 | if nukescripts.panels.PythonPanel.showModalDialog(self): 570 | self.submit() 571 | 572 | def update_pricing_label(self): 573 | machine_type = self.instance_type.value().split(' (')[0] 574 | num_machines = self.num_slots.value() 575 | machine_type_base = machine_type.split(' ')[-1] 576 | field_name = 'CP-ZYNC-%s-NUKE' % (machine_type_base.upper(),) 577 | if 'PREEMPTIBLE' in machine_type.upper(): 578 | field_name += '-PREEMPTIBLE' 579 | if (field_name in self.zync_conn.PRICING['gcp_price_list'] and 'us' in self.zync_conn.PRICING['gcp_price_list'][ 580 | field_name]): 581 | cost = '$%.02f' % ((float(num_machines) * self.zync_conn.PRICING['gcp_price_list'][field_name]['us']),) 582 | else: 583 | cost = 'Not Available' 584 | self.pricing_label.setValue('Est. Cost per Hour: %s' % (cost,)) 585 | 586 | def maybe_correct_path_separators(self, path): 587 | if os.sep != '/': 588 | path = path.replace('/', os.sep) 589 | path = self.zync_conn.generate_file_path(path) 590 | if os.sep != '/': 591 | path = path.replace(os.sep, '/') 592 | return path 593 | 594 | 595 | def submit_dialog(): 596 | ZyncRenderPanel().showModalDialog() 597 | 598 | 599 | def _collect_write_node_paths(selected_write_node_names, write_node_to_user_path_map): 600 | for write_name in selected_write_node_names: 601 | write_node = nuke.toNode(write_name) 602 | if write_node.proxy(): 603 | write_path = write_node.knob('proxy').value() 604 | else: 605 | write_path = write_node.knob('file').value() 606 | output_path, _ = os.path.split(write_path) 607 | write_node_to_user_path_map[write_name] = output_path 608 | 609 | 610 | def _collect_read_node_paths(read_nodes, read_node_path_list): 611 | for read_node in read_nodes: 612 | read_path = None 613 | if hasattr(read_node, 'proxy') and read_node.proxy(): 614 | read_path = read_node.knob('proxy').value() 615 | if not read_path: 616 | # If proxy is empty, Nuke uses original file path and rescales, so the original file is a dependency to upload 617 | for knob_name in PATH_KNOB_NAMES: 618 | if knob_name != 'proxy' and read_node.knob(knob_name): 619 | read_path = read_node.knob(knob_name).value() 620 | if read_path: 621 | break 622 | if read_path: 623 | read_node_path_list.append(read_path) 624 | --------------------------------------------------------------------------------