Package nukescripts :: Module udim
[hide private]
[frames] | no frames]

Source Code for Module nukescripts.udim

  1  # Copyright (c) 2012 The Foundry Visionmongers Ltd.  All Rights Reserved. 
  2  import nuke, os, re, sys, math, time 
  3  from nukescripts import execute_panel 
  4  from nukescripts import panels 
  5  from PySide2 import (QtCore, QtGui, QtWidgets) 
  6   
  7   
8 -def parseUdimFile(f):
9 10 """Parsing a filename string in search of the udim number. 11 The function returns the udim number, and None if it is not able to decode the udim value. 12 13 The udim value is a unique number that defines the tile coordinate. 14 If u,v are the real tile coordinates the equivalent udim number is calculated with the following formula: 15 udim = 1001 + u + 10 * v (Note: u >=0 && u < 10 && udim > 1000 && udim < 2000) 16 17 Redefine this function if the parsing function is not appropriate with your filename syntax.""" 18 19 sequences = re.split("[._]+", f) 20 21 udim = None 22 23 # find the udim number 24 # it gets the last valid udim number available in the filename 25 for s in sequences: 26 try: 27 udim_value = int(s) 28 except ValueError: 29 # not a number 30 udim_value = 0 31 32 if udim_value > 1000 and udim_value < 2000: 33 udim = udim_value 34 35 if udim == None: 36 return None 37 38 return udim
39
40 -def uv2udim(uv):
41 u,v = uv 42 return 1001 + u + 10 * v
43
44 -def checkUdimValue(udim):
45 if udim == None: 46 return True 47 48 if type(udim) == int: 49 return True 50 51 if type(udim) != tuple: 52 return False 53 54 u,v = udim 55 56 if type(u) == int and type(v) == int: 57 return True 58 59 return False
60
61 -def udimStr(s, label):
62 return s.format(label)
63
64 -class UDIMErrorDialog(QtWidgets.QDialog):
65 - def __init__(self, parent=None, error_msg="", udim_label="UDIM"):
66 super(UDIMErrorDialog, self).__init__(parent) 67 68 # Create widgets 69 self.Text = QtWidgets.QTextEdit() 70 self.OkButton = QtWidgets.QPushButton("Ok") 71 72 hlayout = QtWidgets.QHBoxLayout() 73 spacer = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 74 hlayout.insertSpacerItem(0, spacer) 75 hlayout.addWidget(self.OkButton) 76 77 # Create layout and add widgets 78 layout = QtWidgets.QVBoxLayout() 79 layout.addWidget(self.Text) 80 layout.addLayout(hlayout) 81 82 # Set dialog layout 83 self.setMinimumSize( 600, 400 ) 84 self.setLayout(layout) 85 self.setModal(True) 86 self.setWindowTitle( udimStr("{0} files import error", udim_label)) 87 88 # Set error message 89 self.Text.setReadOnly(True) 90 self.Text.setText(error_msg) 91 92 # Add buttons signal 93 self.OkButton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 94 self.OkButton.clicked.connect(self.accept)
95 96
97 -class UDIMFile:
98 - def __init__(self, udim, uv, filename):
99 self.udim = udim 100 self.uv = uv 101 self.filename = filename 102 self.enabled = True 103 self.conflict = False
104
105 -def compare_UDIMFile(a, b):
106 return cmp(a.udim, b.udim)
107
108 -class TableDelegate(QtWidgets.QStyledItemDelegate):
109
110 - def initStyleOption(self, option, index):
111 QtWidgets.QStyledItemDelegate.initStyleOption(self, option, index) 112 if index.column() == 1: 113 option.textElideMode = QtCore.Qt.ElideLeft
114
115 -class UDIMOptionsDialog(QtWidgets.QDialog):
116
117 - def __init__(self, parent=None, parsing_func=parseUdimFile, udim_label="UDIM"):
118 super(UDIMOptionsDialog, self).__init__(parent) 119 120 self.setWindowTitle(udim_label + " import") 121 122 self.UdimMap = [] 123 self.UdimConflict = False 124 self.UdimParsingFunc = parsing_func 125 self.cellChangedConnected = False 126 self.ForceToExit = False 127 self.ErrorMsg = None 128 self.UdimLabel = udim_label 129 130 # Create widgets 131 self.UdimList = QtWidgets.QTableWidget(0, 3, self) 132 self.AddFilesButton = QtWidgets.QPushButton("Add Files") 133 self.ConflictLabel = QtWidgets.QLabel("") 134 self.Separator = QtWidgets.QFrame() 135 self.ReadModeComboBox = QtWidgets.QComboBox() 136 self.PostageStampCheckBox = QtWidgets.QCheckBox("postage stamp") 137 self.GroupNodesCheckBox = QtWidgets.QCheckBox("group nodes") 138 139 self.OkButton = QtWidgets.QPushButton("Ok") 140 self.CancelButton = QtWidgets.QPushButton("Cancel") 141 142 # Setup tooltip 143 self.PostageStampCheckBox.setToolTip( udimStr("Enable the node postage stamp generation for all {0} files.", self.UdimLabel) ) 144 self.GroupNodesCheckBox.setToolTip( udimStr("Place all {0} files in a group.", self.UdimLabel) ) 145 146 # Create layout and add widgets 147 layout = QtWidgets.QVBoxLayout() 148 layout.addWidget(self.UdimList) 149 layout.addWidget(self.AddFilesButton) 150 layout.addWidget(self.ConflictLabel) 151 layout.addWidget(self.Separator) 152 layout.addWidget(self.ReadModeComboBox) 153 layout.addWidget(self.PostageStampCheckBox) 154 layout.addWidget(self.GroupNodesCheckBox) 155 156 hlayout = QtWidgets.QHBoxLayout() 157 hlayout.insertSpacerItem(0, QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)) 158 hlayout.addWidget(self.OkButton) 159 hlayout.addWidget(self.CancelButton) 160 161 layout.addLayout(hlayout) 162 163 # Set dialog layout 164 self.setMinimumSize(800, 400) 165 self.setLayout(layout) 166 self.setModal(True) 167 168 self.Separator.setFrameShape(QtWidgets.QFrame.HLine) 169 self.Separator.setFrameShadow(QtWidgets.QFrame.Sunken) 170 171 self.AddFilesButton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 172 self.OkButton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 173 self.CancelButton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 174 self.ReadModeComboBox.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) 175 176 headerView = self.UdimList.horizontalHeader() 177 headerView.setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) 178 headerView.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) 179 headerView.setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) 180 181 self.UdimList.setItemDelegate(TableDelegate()) 182 self.UdimList.setHorizontalHeaderLabels( [self.UdimLabel, "filename", ""] ) 183 self.UdimList.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) 184 185 self.ReadModeComboBox.addItems( ["single read node", "multiple read nodes"] ) 186 187 # Add buttons signal 188 self.AddFilesButton.clicked.connect(self.importUdimFiles) 189 self.OkButton.clicked.connect(self.accept) 190 self.CancelButton.clicked.connect(self.reject)
191
192 - def updateTableWidget(self):
193 194 # sort the udim list 195 self.UdimMap.sort(compare_UDIMFile) 196 197 # validate all udim entry 198 udimCountMap = {} 199 200 self.UdimConflict = False 201 202 # count the number of time a udim tile is been used 203 for u in self.UdimMap: 204 205 if u.enabled == False: 206 continue 207 208 if u.udim in udimCountMap: 209 udimCountMap[u.udim] = udimCountMap[u.udim] + 1 210 self.UdimConflict = True 211 else: 212 udimCountMap[u.udim] = 1 213 214 # Disable the OK button in case of conflict 215 self.OkButton.setEnabled(not self.UdimConflict) 216 217 if self.UdimConflict : 218 palette = QtGui.QPalette() 219 palette.setColor(QtGui.QPalette.WindowText, QtCore.Qt.yellow) 220 self.ConflictLabel.setText(udimStr("Conflict : multiple files with the same {0} number. Disable unnecessary files.", self.UdimLabel)) 221 self.ConflictLabel.setPalette(palette) 222 else: 223 self.ConflictLabel.setText("Conflict : none") 224 self.ConflictLabel.setPalette(QtGui.QPalette()) 225 226 # Table cell changed 227 if self.cellChangedConnected: 228 self.UdimList.cellChanged.disconnect(self.cellChanged) 229 self.cellChangedConnected = False 230 231 # recreat the QT table 232 self.UdimList.setRowCount( len(self.UdimMap) ) 233 234 row = 0 235 for u in self.UdimMap: 236 237 if u.uv == None: 238 udim_id = QtWidgets.QTableWidgetItem( str(u.udim) ) 239 else: 240 u_coord,v_coord = u.uv 241 udim_id = QtWidgets.QTableWidgetItem( "(%(u)02d,%(v)02d)" % {"u":u_coord, "v":v_coord} ) 242 243 udim_filename = QtWidgets.QTableWidgetItem( u.filename ) 244 245 if u.enabled and u.udim in udimCountMap and udimCountMap[u.udim] > 1: 246 udim_id.setForeground(QtGui.QBrush(QtCore.Qt.yellow)) 247 udim_filename.setForeground(QtGui.QBrush(QtCore.Qt.yellow)) 248 249 checked = QtWidgets.QTableWidgetItem("") 250 251 if u.enabled == True: 252 checked.setCheckState(QtCore.Qt.Checked) 253 else: 254 checked.setCheckState(QtCore.Qt.Unchecked) 255 udim_id.setForeground(QtGui.QBrush(QtCore.Qt.darkGray)) 256 udim_filename.setForeground(QtGui.QBrush(QtCore.Qt.darkGray)) 257 258 self.UdimList.setItem( row, 0, udim_id ) 259 self.UdimList.setItem( row, 1, udim_filename ) 260 self.UdimList.setItem( row, 2, checked ) 261 262 row = row + 1 263 264 # Table cell changed 265 self.UdimList.cellChanged.connect(self.cellChanged) 266 self.cellChangedConnected = True
267
268 - def cellChanged(self, row, column):
269 270 if column == 2: 271 checked = self.UdimList.item(row, column) 272 273 if checked.checkState() == QtCore.Qt.Checked: 274 self.UdimMap[row].enabled = True 275 else: 276 self.UdimMap[row].enabled = False 277 278 # regenerate table 279 self.updateTableWidget()
280
281 - def addUdimFile(self, udim_file):
282 283 # avoid to add the same file if present 284 for u in self.UdimMap: 285 if u.filename == udim_file.filename: 286 return 287 288 self.UdimMap.append(udim_file)
289
290 - def importUdimFiles(self):
291 default_dir = None 292 # get all files 293 files = nuke.getClipname( "Read File(s)", default=default_dir, multiple=True ) 294 295 if files == None: 296 if len(self.UdimMap) == 0: 297 self.ForceToExit = True 298 return 299 300 warning_msg = "" 301 302 # save the new files in the internal map 303 for f in files: 304 305 # the file could be a sequence split it 306 sequences = splitInSequence(f) 307 308 for s in sequences: 309 310 if os.path.isfile(s) == False: 311 continue 312 313 # parse the udim file 314 udim = self.UdimParsingFunc(s) 315 316 # check the parsing function result 317 if checkUdimValue(udim) == False: 318 self.reject() 319 self.ErrorMsg = udimStr("Error. Wrong type returned by {0} parsing function.",self.UdimLabel ) 320 raise ValueError(self.ErrorMsg) 321 322 if udim == None: 323 warning_msg = warning_msg + s + "\n" 324 warning = True 325 else: 326 uv = None 327 udim_value = 0 328 329 try: 330 udim_value = int(udim) 331 except TypeError: 332 udim_value = uv2udim(udim) 333 uv = udim 334 335 self.addUdimFile(UDIMFile(udim_value, uv, s)) 336 337 # show a warning message in case of problems 338 if len(warning_msg) > 0: 339 errorMsg = udimStr( "The following files do not contain a valid {0} number: \n\n" + warning_msg, self.UdimLabel ) 340 udimLabel = self.UdimLabel 341 e = UDIMErrorDialog(error_msg = errorMsg, udim_label = udimLabel) 342 e.exec_() 343 344 # regenerate table 345 self.updateTableWidget()
346
347 -def splitInSequence(f):
348 349 # a file is a sequence if it is expressed in this way: 350 # filename.####.ext 1-10 351 # filename_####.ext 1-10 352 # filename####.ext 1-10 353 354 idx = f.find('#') 355 if idx == -1: 356 return [f] 357 358 # find the sub string that needs to be substituted with the frame number 359 subst = '' 360 for x in range(idx, len(f)): 361 if f[x] != '#': 362 break 363 subst = subst + '#' 364 365 # split the file name in filename,frange 366 sfile = f.split(' ') 367 368 # get the frame range 369 try : 370 frame_range = nuke.FrameRange( sfile[1] ) 371 except ValueError: 372 return [f] 373 374 args = "%(#)0" + str(len(subst)) + "d" 375 376 sequences = [] 377 for r in frame_range: 378 # replace in filename the pattern #### with the right frame range 379 filename = sfile[0].replace( subst, args % {"#":r} ) 380 sequences.append( filename ) 381 382 return sequences
383
384 -def findNextName(name):
385 i = 1 386 while nuke.toNode ( name + str(i) ) != None: 387 i += 1 388 389 return name + str(i)
390
391 -def allign_nodes(nodes, base):
392 # allign an array of node over a node 393 394 nodeSize = 100 395 396 left = right = nodes[0].xpos() 397 top = bottom = nodes[0].ypos() 398 399 for n in nodes: 400 401 if n.Class() == "Dot": 402 continue 403 404 if n.xpos() < left: 405 left = n.xpos() 406 407 if n.xpos() > right: 408 right = n.xpos() 409 410 if n.ypos() < top: 411 top = n.ypos() 412 413 if n.ypos() > bottom: 414 bottom = n.ypos() 415 416 xpos = base.xpos() 417 ypos = base.ypos() 418 419 for n in nodes: 420 x = n.xpos() - right 421 y = n.ypos() - bottom 422 n.setXYpos( x + xpos, y + ypos - nodeSize )
423
424 -def udim_group(nodes):
425 # collaspe all udim tree nodes in a group 426 for n in nodes: 427 n["selected"].setValue ( True ) 428 group_node = nuke.collapseToGroup(False) 429 group_node.autoplace() 430 431 return group_node
432
433 -def udim_import( udim_parsing_func = parseUdimFile, udim_column_label = "UDIM" ):
434 435 """ Imports a sequence of UDIM files and creates the node material tree needed. 436 This function simplifies the process of importing textures. It generates a tree of nodes which 437 adjusts the texture coordinates at rendering time for a model containing multiple texture tiles. 438 In general a tile texture coordinate can be expressed with a single value(UDIM) or with a tuple(ST or UV). 439 The udim_import function can decode a UDIM number from a filename. 440 To determine the tile coordinate encoding for a generic filename convention, the udim_import script can use an 441 external parsing function. 442 443 The redefined parsing function needs to decode a filename string and return the udim or the u,v tile coordinate 444 as an integer or tuple of integers. It should return None if the tile coordinate id can not be determined. 445 446 @param udim_parsing_func: The parsing function. This parses a filename string and returns a tile id. 447 @param udim_column_label: The name of the column in the dialog box used to show the tile id. 448 @return: None 449 """ 450 451 452 # get the UDIM sequence 453 p = UDIMOptionsDialog(parsing_func = udim_parsing_func, udim_label = udim_column_label) 454 455 try: 456 p.importUdimFiles() 457 except ValueError as e: 458 nuke.message(str(e)) 459 return 460 461 if p.ForceToExit: 462 return 463 464 result = p.exec_() 465 466 if result == False: 467 if p.ErrorMsg != None: 468 nuke.message(p.ErrorMsg) 469 return 470 471 UdimMap = p.UdimMap 472 postagestamp = p.PostageStampCheckBox.isChecked() 473 makegroup = p.GroupNodesCheckBox.isChecked() 474 makesingleread = (p.ReadModeComboBox.currentIndex() == 0) 475 476 uvtile = [] 477 nodes = [] 478 479 read_node_width = 80 480 dot_node_width = 12 481 read_node_height = 78 482 dot_node_height = 12 483 frame_hold_width = 80 484 frame_hold_height = 28 485 other_node_height = 18 486 487 if postagestamp == False: 488 read_node_height = 28 489 490 h_spacing = 30 491 v_spacing = 20 492 493 udim_file_count = 0 494 495 # check all valid udim file 496 for u in UdimMap: 497 # skip disabled udim 498 if u.enabled == False: 499 continue 500 501 if os.path.isfile(u.filename) == False: 502 u.enabled = False 503 continue 504 505 udim_file_count += 1 506 507 # nothing to do 508 if udim_file_count == 0: 509 return 510 if udim_file_count == 1: 511 makesingleread = False 512 513 selected_nodes = nuke.selectedNodes() 514 515 # deselect all nodes, needed for the group creation 516 for n in selected_nodes: 517 n["selected"].setValue ( False ) 518 519 groupBaseName = None 520 single_read_node = None 521 udim_file_sequence = "" 522 sequence_index = 1 523 parent_dot_frame_hold = None 524 525 # create a single read node that keep all udim files 526 if makesingleread: 527 single_read_node = nuke.nodes.Read() 528 parent_dot_frame_hold = single_read_node 529 530 for u in UdimMap: 531 532 # skip disabled udim 533 if u.enabled == False: 534 continue 535 536 # split the tuble 537 udim = u.udim 538 uv = u.uv 539 img = u.filename 540 541 if groupBaseName == None: 542 groupBaseName = os.path.basename(img) 543 544 xpos = None 545 ypos = None 546 udim_node = None 547 548 # compose the sequence of udim files 549 if single_read_node != None: 550 udim_file_sequence += img + "\n" 551 552 # create the dot that connect the single read to the frame hold node 553 frame_hold_xpos = single_read_node.xpos() + (frame_hold_width + h_spacing) * (sequence_index-1) 554 555 dot_node = nuke.nodes.Dot( xpos = frame_hold_xpos + (frame_hold_width - dot_node_width) / 2, 556 ypos = single_read_node.ypos() + read_node_height + v_spacing ) 557 dot_node.setInput(0, parent_dot_frame_hold) 558 parent_dot_frame_hold = dot_node 559 560 # create the frame hold node 561 udim_node = nuke.nodes.FrameHold( xpos = frame_hold_xpos, 562 ypos = dot_node.ypos() + dot_node_height + v_spacing ) 563 564 udim_node['first_frame'].setValue(sequence_index) 565 udim_node.setInput(0, dot_node) 566 567 xpos = udim_node.xpos() 568 ypos = udim_node.ypos() + frame_hold_height + v_spacing 569 570 nodes.append(dot_node) 571 572 # next udim file inside sequence 573 sequence_index += 1 574 else: 575 # create the read node 576 udim_node = nuke.nodes.Read() 577 udim_node['file'].setValue( img ) 578 udim_node['postage_stamp'].setValue( postagestamp ) 579 udim_node.autoplace() 580 581 xpos = udim_node.xpos() 582 ypos = udim_node.ypos() + read_node_height + v_spacing 583 584 # create the UV Tile node 585 uvtile_node = nuke.nodes.UVTile2(xpos=xpos, ypos=ypos) 586 uvtile_node.setInput(0, udim_node) 587 588 if uv == None: 589 uvtile_node['udim_enable'].setValue(True) 590 uvtile_node['udim'].setValue(udim) 591 else: 592 u,v = uv 593 uvtile_node['tile_u'].setValue( u ) 594 uvtile_node['tile_v'].setValue( v ) 595 596 uvtile.append(uvtile_node) 597 598 nodes.append(udim_node) 599 nodes.append(uvtile_node) 600 601 if (len(uvtile) == 0): 602 return 603 604 if single_read_node != None: 605 single_read_node['file'].setValue("[lindex [knob sequence] [expr [frame]-1]]") 606 single_read_node['postage_stamp'].setValue( postagestamp ) 607 single_read_node['sequence'].setValue(udim_file_sequence) 608 single_read_node['last'].setValue(sequence_index-1) 609 single_read_node['origlast'].setValue(sequence_index-1) 610 611 latest_merge = uvtile[0] 612 613 if len(uvtile) > 1: 614 615 xpos = latest_merge.xpos() 616 ypos = latest_merge.ypos() + other_node_height + v_spacing 617 618 dot_node = nuke.nodes.Dot( xpos=xpos + (read_node_width - dot_node_width) / 2, 619 ypos=ypos + (other_node_height - dot_node_height) / 2) 620 621 dot_node.setInput(0, latest_merge) 622 nodes.append(dot_node) 623 latest_merge = dot_node 624 625 for x in range(1, len(uvtile)): 626 627 xpos = uvtile[x].xpos() 628 ypos = uvtile[x].ypos() + other_node_height + v_spacing 629 630 mergemat_node = nuke.nodes.MergeMat(xpos=xpos, ypos=ypos) 631 mergemat_node.setInput(0, latest_merge) 632 mergemat_node.setInput(1, uvtile[x]) 633 634 latest_merge = mergemat_node 635 nodes.append(mergemat_node) 636 637 # enable the multi texture udim optimization only for the root 638 # node of the udim shading tree 639 if len(uvtile) > 1: 640 xpos = latest_merge.xpos() 641 ypos = latest_merge.ypos() + other_node_height + v_spacing 642 643 multitexture = nuke.nodes.MultiTexture(xpos=xpos, ypos=ypos) 644 multitexture.setInput(0, latest_merge) 645 latest_merge = multitexture 646 nodes.append(latest_merge) 647 648 for n in nodes: 649 n["selected"].setValue ( True ) 650 651 if makegroup == True: 652 latest_merge = udim_group( nodes ) 653 654 # set the group name 655 split = re.split("[._]+", groupBaseName) 656 name = split[0] 657 658 latest_merge.setName( findNextName(name) ) 659 660 nodes = [] 661 nodes.append( latest_merge ) 662 663 if single_read_node != None: 664 single_read_node["selected"].setValue ( True ) 665 666 if len(selected_nodes) == 1: 667 if single_read_node != None: 668 nodes.append(single_read_node) 669 670 allign_nodes( nodes, selected_nodes[0] ) 671 672 for n in selected_nodes: 673 n["selected"].setValue ( True ) 674 if n.Class() == 'ReadGeo2': 675 n.setInput(0, latest_merge)
676