1
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
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
24
25 for s in sequences:
26 try:
27 udim_value = int(s)
28 except ValueError:
29
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
41 u,v = uv
42 return 1001 + u + 10 * v
43
60
63
65 - def __init__(self, parent=None, error_msg="", udim_label="UDIM"):
66 super(UDIMErrorDialog, self).__init__(parent)
67
68
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
78 layout = QtWidgets.QVBoxLayout()
79 layout.addWidget(self.Text)
80 layout.addLayout(hlayout)
81
82
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
89 self.Text.setReadOnly(True)
90 self.Text.setText(error_msg)
91
92
93 self.OkButton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
94 self.OkButton.clicked.connect(self.accept)
95
96
104
107
109
111 QtWidgets.QStyledItemDelegate.initStyleOption(self, option, index)
112 if index.column() == 1:
113 option.textElideMode = QtCore.Qt.ElideLeft
114
116
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
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
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
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
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
188 self.AddFilesButton.clicked.connect(self.importUdimFiles)
189 self.OkButton.clicked.connect(self.accept)
190 self.CancelButton.clicked.connect(self.reject)
191
267
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
279 self.updateTableWidget()
280
282
283
284 for u in self.UdimMap:
285 if u.filename == udim_file.filename:
286 return
287
288 self.UdimMap.append(udim_file)
289
291 default_dir = None
292
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
303 for f in files:
304
305
306 sequences = splitInSequence(f)
307
308 for s in sequences:
309
310 if os.path.isfile(s) == False:
311 continue
312
313
314 udim = self.UdimParsingFunc(s)
315
316
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
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
345 self.updateTableWidget()
346
348
349
350
351
352
353
354 idx = f.find('#')
355 if idx == -1:
356 return [f]
357
358
359 subst = ''
360 for x in range(idx, len(f)):
361 if f[x] != '#':
362 break
363 subst = subst + '#'
364
365
366 sfile = f.split(' ')
367
368
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
379 filename = sfile[0].replace( subst, args % {"#":r} )
380 sequences.append( filename )
381
382 return sequences
383
390
423
432
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
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
496 for u in UdimMap:
497
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
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
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
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
533 if u.enabled == False:
534 continue
535
536
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
549 if single_read_node != None:
550 udim_file_sequence += img + "\n"
551
552
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
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
573 sequence_index += 1
574 else:
575
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
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
638
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
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