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