UDIM.py
1    from __future__ import print_function, division, absolute_import
2    import sys
3    import os
4    import wx
5    from collections import namedtuple
6    from random import randint
7    from PIL import Image, ImageDraw, ImageColor
8    import webbrowser
9    
10   if sys.version_info.major > 2:
11       basestring = str
12   else:
13       range = xrange
14   
15   try:
16       import poser
17   except ImportError:
18       from PoserLibs import POSER_FAKE as poser
19   
20   SCENE = poser.Scene()
21   TVERT = namedtuple("TVERT", "U V material")
22   BLACK = (0, 0, 0, 0xff)
23   WHITE = (0xf0, 0xf0, 0xf0, 0xff)
24   
25   
26   class UDIM_page(object):
27       __slots__ = "tverts", "material"
28   
29       def __init__(self, tverts=None, material=None):
30           self.tverts = tverts or list()
31           self.material = set()
32           if material:
33               self.material.add(material)
34   
35   
36   UDIM_PAGES_PER_ROW = 10
37   is_UDIM_page = lambda page: isinstance(page, UDIM_page)
38   UDIM_page_empty = lambda page: is_UDIM_page(page) and len(page.tverts) == 0
39   is_UDIM_row = lambda row: isinstance(row, list) and len(row) == UDIM_PAGES_PER_ROW and \
40                             all(is_UDIM_page(page) for page in row)
41   UDIM_row_empty = lambda row: is_UDIM_row(row) and \
42                                all(UDIM_page_empty(page) for page in row)
43   is_UDIM_collection = lambda collection: isinstance(collection, list) and \
44                                           all(is_UDIM_row(row) for row in collection)
45   UDIM_collection_empty = lambda collection: is_UDIM_collection(collection) and \
46                                              all(UDIM_row_empty(row) for row in collection)
47   new_UDIM_row = lambda: [UDIM_page() for _ in range(UDIM_PAGES_PER_ROW)]
48   
49   
50 def acceptableActor(ac): 51 return (isinstance(ac, poser.ActorType) and 52 ac.IsBodyPart() and 53 hasattr(ac, "Geometry") and 54 ac.Geometry() is not None and 55 ac.Geometry().NumVertices() > 0) 56 57
58 def show_message(msg, style=wx.ICON_ERROR): 59 assert isinstance(msg, basestring) 60 with wx.MessageBox(parent=None, message=msg, 61 caption="UDIM", style=style) as dlg: 62 return dlg.ShowModal() 63 64
65 def collect_tvertices(figure, materialnames=None): 66 """ 67 Collect data needed to create UV maps for <figure> based on the 68 figures texture vertices. 69 70 This function returns a UDIM collection, even if the figures 71 texture vertices does not support UDIM. 72 73 The returned collection may be seen like an excel sheet: arranged 74 in rows and columns, where each row/column adresses an UDIM page 75 (tex-vertices used to draw an image with UV data). 76 77 The smallest UDIM collection is one row and 10 columns, where 78 some columns may be unused. Actually, a simple texture may only 79 use row 0, column 0; the other 9 columns in row 0 are left empty. 80 81 Which vertices appear on which row/column is decided based on the 82 vertex data. Rule of thumb: Data for a certain material appear 83 on the same page. But probably together with other materials 84 (if not suppressed). 85 86 If materialnames are given, only vertices for those materials are 87 collected. Be careful: By default they may appear somewhere in a row/column. 88 """ 89 assert isinstance(figure, poser.FigureType) 90 if isinstance(materialnames, basestring): 91 # materialnames in a string; may be one or more separated by whitespace. 92 materialnames = materialnames.split() 93 if materialnames is None: 94 materialnames = set(m.Name() for m in figure.Materials()) 95 else: 96 assert hasattr(materialnames, "__iter__") 97 materialnames = set(materialnames) 98 99 UDIM_collection = [new_UDIM_row(), ] # force the first row with empty pages. 100 101 for actor in [_ac for _ac in figure.Actors() if acceptableActor(_ac)]: 102 """ 103 We collect all tex vertices for the whole figure. You may implement 104 a filter if you want. 105 """ 106 geom = actor.Geometry() 107 assert isinstance(geom, poser.GeomType) 108 109 current_matidx = -1 110 tverts = list() # just to make my editor happy 111 112 for poly, tpoly in zip(geom.Polygons(), geom.TexPolygons()): # type: (poser.PolygonType, poser.TexPolygonType) 113 # TexVerts from the same material should be on the same UDIM page 114 if current_matidx != poly.MaterialIndex(): 115 if not poly.MaterialName() in materialnames: 116 continue 117 current_matidx = poly.MaterialIndex() 118 page_nr = int(divmod(tpoly.TexVertices()[0].U(), 1.0)[0]) 119 # UDIM row in older versions may go with V. 120 # Regular is U modulo 10 (my understanding). 121 page_row = int(divmod(tpoly.TexVertices()[0].V(), 1.0)[0]) 122 if page_row == 0 and page_nr > 10: 123 page_row = page_nr // 10 124 page_nr = page_nr % 10 125 126 # Add some rows to the UDIM collection if required. 127 while len(UDIM_collection) <= page_row: 128 UDIM_collection.append(new_UDIM_row()) 129 130 # Get the UDIM page to work with. 131 udim_page = UDIM_collection[page_row][page_nr] 132 udim_page.material.add(poly.MaterialName()) 133 tverts = udim_page.tverts 134 135 # Add tex-verts from this tex-polygon to current UDIM page 136 # while removing all offsets. 137 tverts.append([TVERT(vt.U() % 1.0, vt.V() % 1.0, current_matidx) 138 for vt in tpoly.TexVertices()]) 139 return UDIM_collection 140 141
142 def compact_UDIM_collection(UDIM_collection): 143 """ 144 Compact an UDIM collection. 145 Returns a new array of rows/colums with UDIM pages without 146 space in between. This may come in handy if you collect data 147 for just one or a few materials. Because this can result in data 148 spread over several rows/columns. 149 """ 150 assert is_UDIM_collection(UDIM_collection) 151 152 new_collection = [new_UDIM_row(), ] # the first row 153 row = column = 0 154 155 for udim_row in UDIM_collection: 156 for udim_page in udim_row: 157 if not UDIM_page_empty(udim_page): 158 new_collection[row][column] = udim_page 159 column += 1 160 if column >= UDIM_PAGES_PER_ROW: 161 column = 0 162 new_collection.append(new_UDIM_row()) 163 row += 1 164 165 return new_collection 166 167
168 def UDIM_draw(UDIM_collection, 169 materialnames=None, 170 pages=None, 171 imagesize=(1000, 1000)): 172 if isinstance(materialnames, basestring): 173 materialnames = materialnames.split() 174 assert materialnames is None or hasattr(materialnames, "__iter__") 175 if isinstance(pages, int): 176 pages = [pages, ] 177 assert pages is None or hasattr(pages, "__iter__") 178 179 matcolors = list() 180 181 def get_fillcolor(idx): 182 while len(matcolors) - 1 < idx: 183 matcolors.append((randint(128, 250), randint(128, 250), randint(128, 250), 0)) 184 return matcolors[idx] 185
186 def do_draw(tvertlist, draw, sz=imagesize[0]): 187 for tverts in tvertlist: 188 color = get_fillcolor(tverts[0].material) 189 draw.polygon([(int(tv[0] * sz), int(sz - tv[1] * sz)) for tv in tverts], 190 fill=color, outline=BLACK) 191
192 def drawPage(row, col): 193 """ 194 Draw a given UDIM page to PNG. 195 """ 196 if row > len(UDIM_collection): 197 return show_message("Unknown UDIM row %s" % row) 198 elif col > len(UDIM_collection[row]): 199 return show_message("Unknown UDIM column %s for row %s" % (col, row)) 200 201 page = UDIM_collection[row][col] 202 image = Image.new("RGB", imagesize, WHITE) 203 draw = ImageDraw.Draw(image) 204 do_draw(page.tverts, draw) 205 imgname = "_".join(sorted(list(page.material))) 206 filename = os.path.join(poser.TempLocation(), "UDIM_1%d0%d_%s.png" % (row, col, imgname)) 207 image.save(filename) 208 del draw 209 webbrowser.open(filename) 210 211 if materialnames is not None: 212 """ 213 If materialnames are given all UDIM-pages are created with one 214 of the given materialnames in it. 215 Materialnames can be given as a list of names or as a string with 216 one or more materialnames in it (whitespace separated). 217 218 It doesn't matter if the UDIM collection is just a vertical list 219 or arranged in rows/columns. 220 """ 221 for ridx, row in enumerate(UDIM_collection): 222 for pidx, page in enumerate(row): 223 if any(m in materialnames for m in page.material): 224 drawPage(ridx, pidx) 225 226 if pages is not None: 227 """ 228 It's also possible to address UDIM pages directly. 229 For this a single pagenumber may be given (for vertically arranged UDIMs) 230 or (row, column). 231 pages may be a list with single numbers or (row, column). 232 """ 233 for p in pages: 234 row, col = p if hasattr(p, "__iter__") else p, 0 235 drawPage(row, col) 236 237 238 figure = SCENE.CurrentFigure() 239 with wx.BusyInfo("Creating UV map(s) for %s" % figure.Name()): 240 matnames = [m.Name() for m in figure.Materials()] # we use all mats for demo. 241 """ 242 If materialnames are provided while collecting vertices only the 243 wanted materials are drawn on the resuling picture(s). 244 If materialnames are provided for the actual draw function, 245 even other materials may appear if they are on the same UDIM page. 246 """ 247 UDIM_draw(UDIM_collection=collect_tvertices(figure, materialnames=matnames), 248 materialnames=matnames, 249 imagesize=(1000, 1000)) 250