Maya python script that converts triangulated mesh to quads from uv layout

Hi,

I’m writing a workflow script for Maya that attempts to convert a triangulated mesh into quads based on how the mesh UVs are laid out. I’m looking for feedback about 1) why the script is running so slow and 2) how to get the UV shell border as a selection of UVs through a python command.

The script assumes that the user is working with a specific kind of mesh, ie. strokes exported from the VR app Gravity Sketch.

On the mesh UVs, the central portion of the mesh is laid out in a grid-like fashion, as seen in the image below.

UVs

My thinking was that since the triangles can already be easily identified visually at a glance on the UVs, the conversion to quads shouldn’t be too difficult. I tried doing ‘Mesh > Quadrangulate’ on a variety of meshes, but I found that this command was not reliable in all cases. My guess is that ‘Mesh > Quadrangulate’ interprets the mesh in world space (not UV space, as I am trying to do).

My script goes like this: Iterate through each UV on the model. Check its neighbouring UVs as to whether they are lined up or not in UV space. If they are not lined up, consider the edge between as an edge causing triangulation, then delete the edge.

After running the script on the UV selection shown above, the edges causing triangles are deleted.

The problem is that the script runs super slowly. I’m not sure what’s going on here or if there is a way to speed this up.

I am also having a hard time getting the UV shell border through a command (or series of commands). I am wondering whether ‘Select > Convert Selection > To UV Shell Border’ exists as a command? I have not been able to find it.

Here is my script and a demo fbx file for testing. Thanks!

sFBXASC0460938.fbx


# the user selects a set of uvs and runs the script
# the script deletes the edges causing triangles

import maya.cmds as cmds
import math

# defines a value to not delete an edge based on whether the u or v values of two uvs in question are within this threshold
keepTol = 0.0001

# user selects a set of uvs
uvSelection = cmds.ls( selection=True, flatten=True )
# for all uvs in the selection
for uv in uvSelection :
    # set the first uv
    uv1 = uv
    # get the uv value
    uv1Val = cmds.polyEditUV( uv1, query=True )
    # get the adjacent edges
    adjEdges = cmds.polyListComponentConversion( uv1, toEdge=True )
    # get the adjacent uvs
    adjUvs = cmds.polyListComponentConversion( adjEdges, toUV=True )
    # select the uvs
    cmds.select( adjUvs )
    # remove the first uv from the selection
    cmds.select( uv1, deselect=True )
    # set the adjacent uvs
    adjUvs = cmds.ls( selection=True, flatten=True )
    # for the adjacent uvs
    for xy in adjUvs :
        # set the second uv
        uv2 = xy
        # get the uv values
        uv2Val = cmds.polyEditUV( uv2, query=True )
        # if the first and second uvs don't match within a given tolerance
        if abs( uv1Val[0] - uv2Val[0] ) > keepTol and abs( uv1Val[1] - uv2Val[1] ) > keepTol : # proceed to delete the edge between
            # get the vertices
            verts = cmds.polyListComponentConversion( uv1, uv2, toVertex=True )
            # get the edge
            edge = cmds.polyListComponentConversion( verts[0], verts[1], toEdge=True, internal=True )
            # delete the edge
            cmds.polyDelEdge( edge, cv=False )
            # exit the loop
            break

I was going to suggest OpenMaya API for faster speeds.

But the reason it is slow is because you’re running individual delete commands inside a loop, and the poly history is accumulating.

If you collect the edges in a list, “edgesToDelete”, and then delete all the edges in one single command, the script executes in 0.26 seconds instead of 734 seconds.

Using the OpenMaya API could potentially get it down drastically faster.

# the user selects a set of uvs and runs the script
# the script deletes the edges causing triangles

import maya.cmds as cmds
import math
import time

# start the timer
timeStart = time.clock()

# defines a value to not delete an edge based on whether the u or v values of two uvs in question are within this threshold
keepTol = 0.0001

# user selects a set of uvs
uvSelection = cmds.ls( selection=True, flatten=True )
# for all uvs in the selection
edgesToDelete = list()
for uv in uvSelection :
    # set the first uv
    uv1 = uv
    # get the uv value
    uv1Val = cmds.polyEditUV( uv1, query=True )
    # get the adjacent edges
    adjEdges = cmds.polyListComponentConversion( uv1, toEdge=True )
    # get the adjacent uvs
    adjUvs = cmds.polyListComponentConversion( adjEdges, toUV=True )
    # select the uvs
    cmds.select( adjUvs )
    # remove the first uv from the selection
    cmds.select( uv1, deselect=True )
    # set the adjacent uvs
    adjUvs = cmds.ls( selection=True, flatten=True )
    # for the adjacent uvs
    for xy in adjUvs :
        # set the second uv
        uv2 = xy
        # get the uv values
        uv2Val = cmds.polyEditUV( uv2, query=True )
        # if the first and second uvs don't match within a given tolerance
        if abs( uv1Val[0] - uv2Val[0] ) > keepTol and abs( uv1Val[1] - uv2Val[1] ) > keepTol : # proceed to delete the edge between
            # get the vertices
            verts = cmds.polyListComponentConversion( uv1, uv2, toVertex=True )
            # get the edge
            edge = cmds.polyListComponentConversion( verts[0], verts[1], toEdge=True, internal=True )
            edgesToDelete.extend(edge)
            # exit the loop
            break
cmds.polyDelEdge( edgesToDelete, cv=False )
# stop the timer
timeStop = time.clock()
print('Execution time: %s seconds.'%(timeStop-timeStart))
1 Like

Maya has a built-in command to convert triangles back to Quads. It’s not 100% perfect, but it might get you pretty close.

Thank you @clesage ! This is great. Yeah I was wondering whether moving the delete outside of the loop would speed things up, but I never realized it would do so this much.

I see that you are storing the edges to a list variable. I haven’t used this kind of variable before so I am not totally sure about how it works. Where do I find info about python variable types? A quick Google search brought me here…
LearnPython.org
Although it’s not directly related to Maya. Can the info on this site be carried over to Python in Maya?

In your script when you declare the ‘edgesToDelete’ variable, could I achieve the same thing as you by doing this?

edgesToDelete = []

I have a related question (and perhaps a list variable could be a possible solution but I am not sure). I am wondering how to compare two selections of components (in my case, uvs) and remove one from the other. In the script, I attempt to do this in a somewhat clunky way (below) but maybe there is a more suitable approach using a set…

# get the adjacent uvs
adjUvs = cmds.polyListComponentConversion( adjEdges, toUV=True )
# select the uvs
cmds.select( adjUvs )
# remove the first uv from the selection
cmds.select( uv1, deselect=True )
# set the adjacent uvs
adjUvs = cmds.ls( selection=True, flatten=True )

Regarding OpenMaya API, I have never used it and I am finding it difficult to translate my existing knowledge of Maya Python commands to OpenMaya API. To get started, where can I find commands for OpenMaya API? Google brought me to this page from 2010…
Maya API Reference

myFancyList = list() and myFancyList = [] are equivalent. (edit: nearly equivalent. It seems there are some subtle differences. python - [] and {} vs list() and dict(), which is better? - Stack Overflow) Both make an empty list. You can also use dict() to make an empty dictionary, or set() to make an empty set.

I don’t have a decent answer for you about removing the original UVs. You can remove elements from a list. You might also use a set instead of a list. A set can only contain unique elements, so if there are times when you end up with a UV edge being adjacent to multiple elements, a set would prevent it from being added multiple times. And sets have methods you may be able to use to easily remove elements you don’t want. (unions and intersections, etc.)

OpenMaya API is a slog… It’s extremely fast to run, but it’s difficult to learn and write. Nothing is straight-forward. You can’t just run simple commands. You’ll need to learn to parse the documentation, find examples online, and perhaps read Practical Maya Programming. Just avoid it if you don’t need it. But if you are doing tools that need to be used many times, the optimization is well worth it.

Thanks @clesage ! This info regarding lists, sets and dictionaries is helpful. I was just exploring sets briefly and it seems like they are a good fit for what I’m trying to do with mesh components (like uvs) where there should only be one entry per component, and no duplicates.

And thank you for the info about OpenMaya API. Good to know!

I wonder if your script is well written. I have been paying attention to it

@aken2046 here is the script I wrote. It was meant to remove tris on a very specific kind of mesh. The script works by selecting one mesh object and running the script. Here is the test object I used when testing it:
https://drive.google.com/file/d/1BgUWi4ILr-3k43v9hG_DiFOdtnfcI2vY/

And here is the script itself (below). It’s very much a work in progress and not thoroughly tested. I was trying to get the script to unfold the UV’s in a more sophisticated way, which I managed to have working, but I commented out those sections at the bottom.

Give it a try. I am curious to know how you would like to use it. Maybe I could help modify it to suit your needs.

import maya.cmds as cmds
import math

def mergeVertices( obj ):
	# merge vertices
	cmds.polyMergeVertex( obj, d=0.0001, am=1, ch=1 )

def getPoles( obj ):
	# get all vertices in the object
	verts = cmds.polyListComponentConversion( obj, toVertex=True )
	# flatten the list
	verts = cmds.ls( verts, flatten=True )
	# create a list to hold the number of edges connected to each vertex
	edgeCounts = []
	# create a list to hold the poles
	poles = []
	# create a variable to hold highest number of edges
	mostEdges = 0
	# for each vertex
	for v in verts :
		# get the edges connected to it and store them in a list
		edges = cmds.polyListComponentConversion( v, toEdge=True )
		# flatten the list
		edges = cmds.ls( edges, flatten=True )
		# add the number of edges to a list
		edgeCounts.append( len(edges) )
		# if the vertex has more edges than the edge count
		if len(edges) > mostEdges :
			# set the new edge count
			mostEdges = len(edges)
	# for each vertex
	for i in range( len(verts) ) :
		# if the vertex has an equal or greater number of edges connected to it than the maximum
		if edgeCounts[i] >= mostEdges :
			# add the vertex to the poles list
			poles.append( verts[i] )
	# return the poles list
	return poles

def getCenterUvShell( obj ) :
	# get all vertices in the object
	verts = cmds.polyListComponentConversion( obj, toVertex=True )
	# flatten the list
	verts = cmds.ls( verts, flatten=True )
	# get the middle vertex
	middleVert = verts[ int( math.floor( len(verts) / 2 ) ) ]
	# get the center uv shell
	centerUvShell = cmds.polyListComponentConversion( middleVert, toUV=True, uvShell=True )
	# convert to vertices
	#centerUvShell = cmds.polyListComponentConversion( centerUvShell, toVertex=True )
	# flatten the list
	centerUvShell = cmds.ls( centerUvShell, flatten=True )
    # return the cap edge list
	return centerUvShell

def getCapEdgesFromShell( shell ) :
	capEdges = cmds.polyListComponentConversion( shell, toEdge=True, border=True )
	# flatten the list
	capEdges = cmds.ls( capEdges, flatten=True )
    # return the cap edge list
	return capEdges

def walkAlongEdgeFromVertex( edges, vertex, steps ) :

	# convert the edges to a list of vertices
	lvEdges = cmds.polyListComponentConversion( edges, toVertex=True )
	# flatten the list
	lvEdges = cmds.ls( lvEdges, flatten=True )
	# convert to a set
	svEdges = set( lvEdges )
	
	# add the vertex to its own set
	sVertex = set( [vertex] )
	
	# set next vertex
	nextVert = vertex
	
	# create a mask set
	svMask = set()
	
	# loop begin
	for i in range(steps) :
	
		leExpand = cmds.polyListComponentConversion( nextVert, toEdge=True )
		# convert the edges to vertices
		lvExpand = cmds.polyListComponentConversion( leExpand, toVertex=True )
		# flatten the list
		lvExpand = cmds.ls( lvExpand, flatten=True )
		# convert to a set
		svExpand = set( lvExpand )
		
		# get intersection of the expanded verts with the edges
		svTwoEdges = svExpand.intersection( svEdges )
		
		# first index
		if i==0 :
			# remove the vertex from the set
			svOuterVerts = svTwoEdges.difference( sVertex )
			# use the first as the next vertex
			nextVert = list( svOuterVerts )[0]
			# add the expanded set to the mask set
			svMask = svMask.union(svExpand)
		
		# other indexes
		else :
			#two edges - mask
			nextVert = list( svTwoEdges.difference( svMask ) )[0]
			# add the expanded set to the mask set
			svMask = svMask.union(svExpand)
	
	cmds.select( nextVert )
	return nextVert

def deleteTrisFromUv ( uvs, seamEdge ) :
	# defines a value to not delete an edge based on whether the u or v values of two uvs in question are within this threshold
	keepTol = 0.0001
	# define edges to exclude
	sEdgeExclude = set( seamEdge )
	# user selects a set of uvs
	uvSelection = uvs
	# store the edges to delete in a list
	edgesToDelete = list()
	# for all uvs in the selection
	for uv in uvSelection :
		# set the first uv
		uv1 = uv
		# get the uv value
		uv1Val = cmds.polyEditUV( uv1, query=True )
		# get the adjacent edges
		adjEdges = cmds.polyListComponentConversion( uv1, toEdge=True )
		# get the adjacent uvs
		adjUvs = cmds.polyListComponentConversion( adjEdges, toUV=True )
		# select the uvs
		cmds.select( adjUvs )
		# remove the first uv from the selection
		cmds.select( uv1, deselect=True )
		# set the adjacent uvs
		adjUvs = cmds.ls( selection=True, flatten=True )
		# for the adjacent uvs
		for xy in adjUvs :
			# set the second uv
			uv2 = xy
			# get the uv values
			uv2Val = cmds.polyEditUV( uv2, query=True )
			# if the first and second uvs don't match within a given tolerance
			if abs( uv1Val[0] - uv2Val[0] ) > keepTol and abs( uv1Val[1] - uv2Val[1] ) > keepTol : # proceed to delete the edge between
				# get the vertices
				verts = cmds.polyListComponentConversion( uv1, uv2, toVertex=True )
				# flatten the list
				verts = cmds.ls( verts, flatten=True )
				#newVert = cmds.polyListComponentConversion( uv2, toVertex=True )
				# get the edge
				edge = cmds.polyListComponentConversion( verts[0], verts[1], toEdge=True, internal=True )
				# convert to set
				sEdge = set( edge )
				# exclude the uv shell border
				sEdge = sEdge.difference( sEdgeExclude )
				# edit the edge variable
				edge = list(sEdge)
				# add the edge to the list
				edgesToDelete.extend(edge)
				# exit the loop
				#break

	# delete all edges in the list
	cmds.polyDelEdge( edgesToDelete, cv=False )

def getSeamEdgeFromShell ( shell ) :
	# declare variables
	minU = float()
	maxU = float()
	tol = 0.001
	uvSeam = []
	
	# select the shell
	cmds.select( shell )
	# select only the border uvs
	mel.eval( 'PolySelectTraverse 3' )
	# store the uv border
	uvBorder = cmds.ls ( selection=True, flatten=True )
	# clear the selection
	cmds.select ( clear=True )
	# select the uv border
	cmds.select ( uvBorder )
	# print
	#print (uvBorder)

	# for each border uv
	for i in range( len( uvBorder) ) :
		# get the u and v values
		uvVal = cmds.polyEditUV( uvBorder[i], query=True )
		# for the first uv
		if ( i==0 ) :
			# set minU and maxU
			minU = uvVal[0]
			maxU = uvVal[0]
		# for all other uvs
		else :
			# if the u value is less than minU
			if ( uvVal[0] < minU ) :
				# set minU
				minU = uvVal[0]
			# if the u value is greater than maxU
			if ( uvVal[0] > maxU ) :
				# set maxU
				maxU = uvVal[0]

	# for each border uv
	for uv in uvBorder :
		# get the u and v values
		uvVal = cmds.polyEditUV( uv, query=True )
		# if the uv is aligned with minU or maxU
		if ( ( ( uvVal[0] < ( minU + tol ) ) and ( uvVal[0] > ( minU - tol ) ) ) or ( ( uvVal[0] < ( maxU + tol ) ) and ( uvVal[0] > ( maxU - tol ) ) ) ) :
			# add the uv to the list
			uvSeam.append( uv )
	
	eUvSeam = cmds.polyListComponentConversion( uvSeam, toEdge=True, internal=True )
	return eUvSeam

def getCapFaces( stroke, shell ) :
	# convert the stroke to uvs
	lStrokeUv = cmds.polyListComponentConversion( stroke, toUV=True )
	# flatten the list
	lStrokeUv = cmds.ls( lStrokeUv, flatten=True )
	# store them in a set
	sStrokeUv = set( lStrokeUv )
	# store the center shell in a set
	sShellUv = set( shell )
	# get the difference of the two
	sCapUv = sStrokeUv.difference( sShellUv )
    # convert to a list
	lCapUv = list( sCapUv )
	# convert the uvs to faces
	faces = cmds.polyListComponentConversion( lCapUv, toFace=True )
	# flatten the list
	faces = cmds.ls( faces, flatten=True )
	# return
	return faces

def planarMapUv( faces ) :
	# planar map faces
	cmds.polyProjection( faces, type="Planar", ibd=True, kir=True, md="x" )

def quadrangulateWithAngle( faces, angle ) :
	cmds.polyQuad( faces, a=angle, kgb=True, ktb=True, khe=False, ws=True )

def getCapSeamFromEdge( seamEdges, shell ) :
	# select the edges
	cmds.select( seamEdges )
	# set selection type to edges
	#cmds.selectType( pe=True )
	# select contiguous edges
	cmds.polySelectConstraint( pp=4, m2a=30.0, m3a=90.0, t=0x8000 )
	# store the contiguous edges in a list
	lCntgEdges = cmds.ls( selection=True, flatten=True )
	# store the contiguous edges in a set
	sCntgEdges = set( lCntgEdges )
	# store the seam edges in a set
	#sSeamEdges = set( seamEdges )
	
	# convert the shell to edges
	lShellEdges = cmds.polyListComponentConversion( shell, toEdge=True )
	# flatten the list
	lShellEdges = cmds.ls( lShellEdges, flatten=True )
	# convert to a set
	sShellEdges = set( lShellEdges )
	
	# get the difference
	sCapEdges = sCntgEdges.difference( sShellEdges )
	# store them in a list
	lCapEdges = list( sCapEdges )
	# return
	return lCapEdges

def cutAndUnfold( edges, faces ) :
	# cut
	cmds.polyMapCut( edges )
	# unfold
	cmds.u3dUnfold( faces, ite=8, p=0, bi=1, tf=1, ms=1024, rs=0 )

def moveAndSew( edges ) :
	# move and sew uv edges
	cmds.polyMapSewMove( edges, lps=0 )

def unfoldAndLayout( obj ) :
	# unfold
	cmds.u3dUnfold( obj, ite=8, p=0, bi=1, tf=1, ms=1024, rs=0 )
	# layout
	cmds.u3dLayout( obj, res=256, scl=1 )

# get the selected object
sel = cmds.ls( selection=True )
# create a variable to hold the stroke
stroke = sel[0]

# merge verts on the stroke
mergeVertices( stroke )

#poles = getPoles( stroke )

# define the main uv shell
shell = getCenterUvShell( stroke )
# define the seam edges
seamEdges = getSeamEdgeFromShell( shell )
# delete tris
deleteTrisFromUv( shell, seamEdges )

'''
# define the cap faces
capFaces = getCapFaces( stroke, shell )
# planar map the cap faces
planarMapUv( capFaces )

# re-define the main uv shell
shell = getCenterUvShell( stroke )
# re-define the seam edges
seamEdges = getSeamEdgeFromShell( shell )
# define the cap seam
capSeam = getCapSeamFromEdge( seamEdges, shell )
# cut and unfold the cap
cutAndUnfold( capSeam, capFaces )
# delete tris on the cap faces
quadrangulateWithAngle( capFaces, 1.0 )

# re-define the cap faces
#capFaces = getCapFaces( stroke, shell )

# get the cap edges
capEdges = getCapEdgesFromShell ( shell )
# move and sew the edges
moveAndSew( capEdges )

# unfold and layout
unfoldAndLayout( stroke )

'''

# test the selection
#cmds.select( capEdges )


'''
deleteTrisFromUv( shell, seamEdges )
quadrangulateWithAngle( capFaces, 1.0 )
'''

'''
capEdges = getCapEdgesFromShell ( shell )
seamPoint = walkAlongEdgeFromVertex( capEdges, poles[0], 3 )
'''


1 Like

There are still many problems with this script. After using it, only the edge of the object is left