This script, an improved version of the djPFXUVs Python script by David Johnson, is designed for Maya and primarily used for arranging UVs in a 2x2, 3x3, or 4x4 grid within a 1x1 space. It's especially useful for creating foliage. The script is compatible with Python3 and works with Maya 2020 and later versions.
To use this script in Maya, follow these steps:
- Save the script as djPFXUVs.py in the Maya scripts directory. The scripts directory is typically located under your user's documents folder, in a directory structure similar to Documents/maya/scripts/.
- Once the script is saved in the correct location, you can import and use it in Maya. Open the Python command line or script editor in Maya and execute the following code:
import djPFXUVs djPFXUVs.layoutUI()
This code will import the djPFXUVs module and run the layoutUI() function, which opens the user interface for the script. From here, you can select the source and target UV sets, the type of UV layout, and other options, then apply the layout to your selected meshes.
import maya.OpenMaya as om import maya.cmds as mc import random ''' pfxUVs.py author: David Johnson contact: david@djx.com.au web: http://www.djx.com.au version: 1.4 date: 10/26/2023 Description: Script is for automatically laying out uv's for meshes created by converting paintFX to polys By default the uv's for leaves and grass are the same for each leaf or blade and unitized. By offsetting and scaling the uv's for each shell it is possible to create a fileTexture with a number of variations and have these randomly distributed to the leaves or blades. Can either be run from the UI or directly as a command (marking menu or shelf) layoutUI() create UI leafLayout() lays out leaf uvs into tiles in a 2x2, 3x3, or 4x4 array grassLayout() lays out grass blades uv's in thin strips, randomly placed along u ''' #........................................................................................................ # Query the current setting of the Unfold Method current_unfold_method = mc.optionVar(q='polyUnfold3dMethod') def layoutUI(): # build UI windowName = 'pfxUVsLayoutUI' windowTitle = 'djPFXUVs Layout' if mc.window(windowName, exists=True): mc.deleteUI(windowName, window=True) mc.window(windowName, title=windowTitle) mc.columnLayout(columnAttach=("both", 5), rowSpacing=10, columnWidth=400) mc.rowColumnLayout( numberOfColumns=2, columnWidth=[(1, 100),(2, 220)], columnAttach=[(1, 'both', 5), (2, 'both', 0)], columnAlign=[(1,'left'),(2,'left')]) # uv set dropdown menu mc.text( label='UV Set ') mc.optionMenu('UVSetSourceSelection', label='', cc=pfxUVSetSourceCC) mc.text( label='UV Set ') mc.optionMenu('UVSetTargetSelection', label='', cc=pfxUVSetTargetOptionVar) mc.setParent('..') mc.rowColumnLayout( numberOfColumns=2, columnWidth=[(1, 100),(2, 100)], columnAttach=[(1, 'both', 5), (2, 'both', 0)], columnAlign=[(1,'left'),(2,'left')]) # layout types mc.text( label='UV Layout Type') mc.optionMenu('UVLayout', label='', cc=pfxUVLayoutSetOptionVar) # layout options mc.text( 'UVLayoutParam1Text', label='') mc.floatField('UVLayoutParam1Val', minValue=0, maxValue=0.05, precision=3, value=0, cc=pfxUVLAyoutParam1SetOptionVar) mc.setParent('..') # doIt button mc.button('pfxUVDoItButton', label='', command=pfxUVLayoutDoIt, h=50) # fill in the blanks refreshWindow() # create scriptJob to refresh UI if selection changes mc.scriptJob(parent=windowName, event=['SelectionChanged',refreshWindow] ) mc.showWindow(windowName) #........................................................................................................ def refreshWindow(): # clear the existing menu items menus = ('UVSetSourceSelection', 'UVSetTargetSelection', 'UVLayout') for m in menus: menuItems = mc.optionMenu(m,q=True,ill=True) if menuItems is not None: mc.deleteUI( menuItems, mi=True) # rebuild 'UVSetSourceSelection' selList = om.MSelectionList() om.MGlobal.getActiveSelectionList(selList) uvSets = getUVSets(selList) for s in uvSets: mc.menuItem( parent=menus[0], label=s ) if len(uvSets)>1: mc.menuItem( parent=menus[0], label='' ) # rebuild 'UVSetTargetSelection' (build drop-down later - depends on source) mc.menuItem( parent=menus[1], label='' ) # rebuild 'UVLayout' layoutList = ('2x2', '3x3', '4x4', 'grass') for layout in layoutList: mc.menuItem( parent=menus[2], label=layout ) # apply prefs if len(uvSets): if mc.optionVar(exists='pfxUVSetSource'): pref_uvSet = mc.optionVar(q='pfxUVSetSource') if pref_uvSet=='' and len(uvSets)>1: i=len(uvSets)+1 else: i = pfxUVMenuArrayIndex(pref_uvSet,uvSets) mc.optionMenu(menus[0], edit=True, sl=i) # now build target drop-down if mc.optionMenu(menus[0], q=True, v=True)=='': # is a special case where target must be source uv set mc.optionMenu(menus[1], edit=True, sl=1) mc.optionMenu(menus[1], edit=True, enable=False) else: mc.optionMenu(menus[1], edit=True, enable=True) for s in uvSets: mc.menuItem( parent=menus[1], label=s ) mc.menuItem( parent=menus[1], label='' ) if mc.optionVar(exists='pfxUVSetTarget'): pref_uvSet = mc.optionVar(q='pfxUVSetTarget') if pref_uvSet=='': i=len(uvSets)+1 else: i = pfxUVMenuArrayIndex(pref_uvSet,uvSets) mc.optionMenu(menus[1], edit=True, sl=i) if mc.optionVar(exists='pfxUVLayout'): pref_uvLayout = mc.optionVar(q='pfxUVLayout') i = pfxUVMenuArrayIndex(pref_uvLayout,layoutList) mc.optionMenu(menus[2], edit=True, sl=i) else: pref_uvLayout = '2x2' refreshLayoutParam1(pref_uvLayout) # doIt button if selList.length(): doItLabel = 'Layout UVs' doItState = True doItBgc = (0.6, 0.7, 0.6) else: doItLabel = '::: You need to select at least 1 poly mesh :::' doItState = False doItBgc = (0.7, 0.7, 0.5) mc.button('pfxUVDoItButton', edit=True, label=doItLabel, bgc=doItBgc, enable=doItState ) #........................................................................................................ def refreshLayoutParam1(layout): mc.text( 'UVLayoutParam1Text', edit=True, label=layoutParam1Text(layout)) mc.floatField('UVLayoutParam1Val', edit=True, v=pfxUVLAyoutParam1GetOptionVar(layout)) #........................................................................................................ def layoutParam1Text(layout='2x2'): t = '' if layout == '2x2' or layout == '3x3' or layout == '4x4': t = 'tile separation' elif layout == 'grass': t = 'blade width' return t #........................................................................................................ # get menu choices and launch def pfxUVLayoutDoIt(*args): refresh = 0 # uvset is one or all uvSetSource = mc.optionMenu('UVSetSourceSelection', q=True, v=True) uvSetTarget = mc.optionMenu('UVSetTargetSelection', q=True, v=True) # get new target uv set name if uvSetTarget == '': result = mc.promptDialog(title='New Target UV Set Name', message='Enter Name: ', text='%s_copy' % uvSetSource, button=['OK', 'Cancel'], defaultButton='OK', cancelButton='Cancel', dismissString='Cancel') if result == 'OK': uvSetTarget = mc.promptDialog(query=True, text=True) refresh +=1 else: print(' chickened out before doing anything') return # layout layout = mc.optionMenu('UVLayout', q=True, v=True) # param1 param1 = mc.floatField('UVLayoutParam1Val', q=True, v=True) # layout uv's if layout == 'grass': grassLayout(uvSetSource, param1, uvSetTarget) else: leafLayout(uvSetSource, float(layout[0]), param1, uvSetTarget) # new uv sets if refresh: refreshWindow() #........................................................................................................ def pfxUVSetCopy(uvSetSource='map1', uvSetTarget='map2'): pass #........................................................................................................ def getUVSets(selList): ''' get list of all possible uv sets from selected objects shapeNodes ''' uvSets = [] pathToShape = om.MDagPath() selListIter = om.MItSelectionList(selList, om.MFn.kMesh) while not selListIter.isDone(): pathToShape = om.MDagPath() print(' %s' % pathToShape.partialPathName()) selListIter.getDagPath(pathToShape) shapeFn = om.MFnMesh(pathToShape) uvSetsThisMesh = [] shapeFn.getUVSetNames(uvSetsThisMesh) for s in uvSetsThisMesh: if s not in uvSets: uvSets.append(s) selListIter.next() return uvSets #........................................................................................................ # menu change commands def pfxUVSetSourceCC(uvSet): pfxUVSetSourceOptionVar(uvSet) refreshWindow() #........................................................................................................ # optionVars handlers # def pfxUVSetSourceOptionVar(uvSet): mc.optionVar(sv=('pfxUVSetSource', uvSet)) def pfxUVSetTargetOptionVar(uvSet): mc.optionVar(sv=('pfxUVSetTarget', uvSet)) def pfxUVLayoutSetOptionVar(uvLayout): mc.optionVar(sv=('pfxUVLayout', uvLayout)) refreshLayoutParam1(uvLayout) def pfxUVLAyoutParam1SetOptionVar(val): layoutParamText = mc.text( 'UVLayoutParam1Text', q=True, label=True) if layoutParamText == 'tile separation': mc.optionVar(fv=('pfxUVTileSeparation', val)) elif layoutParamText == 'blade width': mc.optionVar(fv=('pfxUVBladeWidth', val)) def pfxUVLAyoutParam1GetOptionVar(layout): v = 0.0 if layout == '2x2' or layout == '3x3' or layout == '4x4': if mc.optionVar(exists='pfxUVTileSeparation'): v = mc.optionVar(q='pfxUVTileSeparation') elif layout == 'grass': if mc.optionVar(exists='pfxUVBladeWidth'): v = mc.optionVar(q='pfxUVBladeWidth') return v #........................................................................................................ def pfxUVMenuArrayIndex(item, list): # menu item lists are 1 based try: i=list.index(item) + 1 except: i=1 return i #........................................................................................................ # compute constants for the leaf layout math def leafLayoutConstants(subdivs, separation): s = 1.0/subdivs - separation # scale x = (1-s)/2 # pivot back to 0.5,0.5 after scale o = {2:1.0/(subdivs*2), 3:1.0/subdivs, 4:1.0/(subdivs*2)} # build cell offset table (there's gotta be a better way!) if subdivs == 2: cellOffsets = ((-o[2],-o[2]), (o[2],-o[2]), (-o[2],o[2]), (o[2],o[2])) elif subdivs == 3: cellOffsets = ((-o[3],-o[3]), (0,-o[3]), (o[3],-o[3]), (-o[3],0), (0,0), (o[3],0), (-o[3],o[3]), (0,o[3]), (o[3],o[3])) elif subdivs == 4: cellOffsets = ((-3*o[4],-3*o[4]), (-o[4],-3*o[4]), (o[4],-3*o[4]), (3*o[4],-3*o[4]), (-3*o[4],-o[4]), (-o[4],-o[4]), (o[4],-o[4]), (3*o[4],-o[4]), (-3*o[4],o[4]), (-o[4],o[4]), (o[4],o[4]), (3*o[4],o[4]), (-3*o[4],3*o[4]), (-o[4],3*o[4]), (o[4],3*o[4]), (3*o[4],3*o[4])) return s,x,cellOffsets #........................................................................................................ def leafLayout(uvSet='map1', subdivs=3, sep=0.01, uvSetTarget=''): ''' leafLayout(uvSet, subdivs, separation [,uvSetTarget]) Scale and offset the shell uvs from uvSet into the number of tiles defined by subdivs and separation If uvSet == '' then process all uvsets. subdivs can be 2, 3 or 4, resulting in 4, 9 or 16 tiles separation, the distance between tiles, should be between 0 and 0.1 (large values will create strange layouts) ''' # Set the Unfold Method to Legacy mc.optionVar(sv=('polyUnfold3dMethod', 0)) print('%s leafLayout start' % (__name__)) # check arguments if subdivs not in [2,3,4]: print('subdivs=%i not valid. Must be 2, 3 or 4.' % (subdivs)) return f = -1 if sep<0: f=0 elif sep>0.05: f=0.05 if f != -1: print('sep=%d outside range 0 to 0.05 Using %d' % (sep,f)) sep=f # get the constants for the layout math s, x, cellOffsets = leafLayoutConstants(subdivs, sep) # step through the objects in selection list selList = om.MSelectionList() om.MGlobal.getActiveSelectionList(selList) if selList.isEmpty(): print(' Nothing selected. Select a poly mesh and try again') return selListIter = om.MItSelectionList(selList, om.MFn.kMesh) while not selListIter.isDone(): pathToShape = om.MDagPath() selListIter.getDagPath(pathToShape) shapeFn = om.MFnMesh(pathToShape) uvShellArray = om.MIntArray() uArray = om.MFloatArray() vArray = om.MFloatArray() shells = om.MScriptUtil() shells.createFromInt(0) shellsPtr = shells.asUintPtr() print(' %s' % pathToShape.partialPathName()) # check specified uvSet exists on this mesh uvSets = [] shapeFn.getUVSetNames(uvSets) if uvSet != '': if uvSet in uvSets and shapeFn.numUVs(uvSet): if uvSetTarget != '' and uvSetTarget != uvSet: uvSet = shapeFn.copyUVSetWithName(uvSet, uvSetTarget) uvSets = [uvSet] else: print(' **** uv set %s not found.' % (uvSet)) print('') selListIter.next() continue uvSetsString='' for uvs in uvSets: uvSetsString += uvs + ', ' print(' uvSets: %s' % (uvSetsString[:-2])) print('') # remember current uv set currentUVSet = shapeFn.currentUVSetName() for thisUVSet in uvSets: # thisUVSet needs to be current shapeFn.setCurrentUVSetName(thisUVSet) print(' %s' % (thisUVSet)) shapeFn.getUvShellsIds(uvShellArray, shellsPtr, thisUVSet) print(' %s shells' % shells.getUint(shellsPtr)) shapeFn.getUVs(uArray, vArray, thisUVSet) print(' %s uvs' % uArray.length()) uvDict = {} uvOffDict = {} for i in range(uArray.length()): if not uvShellArray[i] in uvDict: uvOffDict[uvShellArray[i]] = random.randint(0,pow(subdivs,2)-1) uvDict[uvShellArray[i]] = [i] else: uvDict[uvShellArray[i]].append(i) # compute the new uv's for i in range(shells.getUint(shellsPtr)): for j in uvDict[i]: uArray.set((x + uArray[j]*s + cellOffsets[uvOffDict[i]][0]),j) vArray.set((x + vArray[j]*s + cellOffsets[uvOffDict[i]][1]),j) # write new uv's shapeFn.setUVs(uArray, vArray, thisUVSet) print(' done') print('') uvShellArray.clear() uArray.clear() vArray.clear() # restore current uv set shapeFn.setCurrentUVSetName(currentUVSet) selListIter.next() print('') # Set the Unfold Method back to its original setting mc.optionVar(sv=('polyUnfold3dMethod', current_unfold_method)) print('%s leafLayout done\n' % (__name__)) #........................................................................................................ def grassLayout(uvSet='map1', bladeWidth=0.01, uvSetTarget=''): ''' grassLayout(uvSet, bladeWidth [,uvSetTarget]) Scales the uv's so each blade covers a strip specified by bladeWidth Offsets each shell randomly along u ''' print('%s grassLayout start' % (__name__)) # validate arguments f = -1 if bladeWidth<0: f=0 elif bladeWidth>0.5: f=0.5 if f != -1: print('bladeWidth=%d outside valid range 0 to 0.5 Using %d' % (bladeWidth,f)) bladeWidth=f x = (1-bladeWidth)/2 selList = om.MSelectionList() om.MGlobal.getActiveSelectionList(selList) if selList.isEmpty(): print(' Nothing selected. Select a poly mesh and try again') return selListIter = om.MItSelectionList(selList, om.MFn.kMesh) while not selListIter.isDone(): pathToShape = om.MDagPath() selListIter.getDagPath(pathToShape) shapeFn = om.MFnMesh(pathToShape) uvShellArray = om.MIntArray() uArray = om.MFloatArray() vArray = om.MFloatArray() shells = om.MScriptUtil() shells.createFromInt(0) shellsPtr = shells.asUintPtr() print(' %s' % pathToShape.partialPathName()) # check specified uvSet exists on this mesh uvSets = [] shapeFn.getUVSetNames(uvSets) if uvSet != '': if uvSet in uvSets and shapeFn.numUVs(uvSet): if uvSetTarget != '' and uvSetTarget != uvSet: uvSet = shapeFn.copyUVSetWithName(uvSet, uvSetTarget) uvSets = [uvSet] else: print(' **** uv set %s not found.' % (uvSet)) print('') selListIter.next() continue uvSetsString='' for uvs in uvSets: uvSetsString += uvs + ', ' print(' uvSets: %s' % (uvSetsString[:-2])) print('') # remember current uv set currentUVSet = shapeFn.currentUVSetName() for thisUVSet in uvSets: # thisUVSet needs to be current shapeFn.setCurrentUVSetName(thisUVSet) print(' %s' % (thisUVSet)) shapeFn.getUvShellsIds(uvShellArray, shellsPtr, thisUVSet) print(' %s shells' % shells.getUint(shellsPtr)) shapeFn.getUVs(uArray, vArray, thisUVSet) print(' %s uvs' % uArray.length()) uvDict = {} uvOffDict = {} for i in range(uArray.length()): if not uvShellArray[i] in uvDict: uvOffDict[uvShellArray[i]] = random.uniform(-x,x) uvDict[uvShellArray[i]] = [i] else: uvDict[uvShellArray[i]].append(i) # compute the new u's for i in range(shells.getUint(shellsPtr)): for j in uvDict[i]: uArray.set((x + uArray[j]*bladeWidth + uvOffDict[i]),j) # write new u's shapeFn.setUVs(uArray, vArray, thisUVSet) print(' done') print('') uvShellArray.clear() uArray.clear() vArray.clear() # restore current uv set shapeFn.setCurrentUVSetName(currentUVSet) selListIter.next() print('') print('%s grassLayout end\n' % (__name__)) # Deselect all selected objects mc.select(clear=True) #........................................................................................................
Here is video tutorial on how to use the tool: https://youtu.be/Dqr70n1gU1c