[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)
76
77      # Create layout and add widgets
78      layout = QtWidgets.QVBoxLayout()
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
90      self.Text.setText(error_msg)
91
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)
133      self.ConflictLabel = QtWidgets.QLabel("")
134      self.Separator = QtWidgets.QFrame()
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()
155
156      hlayout = QtWidgets.QHBoxLayout()
157      hlayout.insertSpacerItem(0, QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum))
160
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)
170
172      self.OkButton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
173      self.CancelButton.setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
175
180
181      self.UdimList.setItemDelegate(TableDelegate())
182      self.UdimList.setHorizontalHeaderLabels( [self.UdimLabel, "filename", ""] )
183      self.UdimList.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
184
186
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
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
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()
475
476    uvtile = []
477    nodes  = []
478
480    dot_node_width = 12
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:
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:
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
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
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
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,
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
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
605      single_read_node['file'].setValue("[lindex [knob sequence] [expr [frame]-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
665
666    if len(selected_nodes) == 1:
669
670      allign_nodes( nodes,  selected_nodes[0] )
671
672    for n in selected_nodes:
673      n["selected"].setValue ( True )