I’m trying to write a python script to validate meshes to be clean before they are exported to Unity. This validating is for non-manifold edges, faces, verts and edges with Zero length.
I believe I have the non-manifold covered with cmds.polyInfo() but struggle to find a command for edges with Zero Length. This would be the same function within Maya’s cleanup options tool. Any suggestion would be appreciated.
Here’s a sample using Maya Api 2.0 in Python. Just swap out ‘pCube1’ for whatever mesh you want to test and set the min_length to 0.0 or some small number if you want to catch tiny edges as well.
import maya.api.OpenMaya as om2
zero_length_edges = om2.MSelectionList()
min_length = 0.1
sel = om2.MGlobal.getSelectionListByName('pCube1')
component = sel.getComponent(0)
itr_mesh = om2.MItMeshEdge(*component)
while not itr_mesh.isDone():
if itr_mesh.length() <= min_length:
zero_length_edges.add((component[0], itr_mesh.currentItem()))
itr_mesh.next()
# Select Edges with Zero Length
om2.MGlobal.setActiveSelectionList(zero_length_edges)
I’ve always found relying on Maya just doing this for you was way faster than calculating it myself through Python, you could use maya’s polySelectConstraint for that. Maybe the speed is different now in more recent versions of Maya with the Python 2.0 API. Using polySelectConstraint is a bit annoying since it requires you to make the active selection, etc. instead of just returning a list of the invalid edges so I’ve wrapped polySelectConstraint in a way so that you can pass it the same arguments but have it just return the resulting selection. Still for massive polycounts that would be only 0.1s at most where the Python API iterating would just be massively slow for those cases.
It’s used for example here to get zero length edges with the implementation of polyConstraint here. As you can see the implementation has some workarounds to avoid it being slow (like being in the Scale tool is slower than being in the Select tool when making huge component selections with the select constraints.)
The benefit to this implementation is that you could use it for any of the polySelectConstraint capabilities if you need that, like listing all NGONs.
However, if you only want to know whether there are any zero length edges instead of finding all of them it can of course be much more optimal by iterating like in Narwhal’s code so you can break the loop directly as soon as you have found any zero length edge.
Agree, any time you can leverage maya’s built in functionality that is not py/mel based, you will get much better results. Especially if those commands can batch process a bunch of data in a single call.
Oh, I totally agree! Anytime you can leverage and internal command it’s probably going to be way faster than iterating through python. I did run a test on a 2 million polygon sphere, if you are looking to just detect zero length edges (not add them to a selection) it takes 0.77 seconds to run this. Adding all of them to selection is slower, 18+ seconds.
I’ve always used maya.cmds for my tools. I’ve put off learn api due to the steep learning curve, but seeing the feed back. It seems it’s the way forward.
I’m going to tackle this with the api approach and may use some maya.cmds to fill in the loose ends such as selecting all meshes within the scene to validate. That’s if I cant do it with api first.
I shall update my findings very soon. Thanks everyone
One minor optimization is to check the squaredistance (the “manhattan distance”) rather than the actual distance between the points. It’s the sum of the components of abs(pointA - point B) and apart from a different tolerance setting will catch zero length edges a bit quicker than the proper length calculation.
Another shortcut is just to look for coincident points, which you can do by collecting all the verts and creating a set of their positions. If that set – which has no duplicates – is not the same length as the vert list you have at least one coincident point:
t = None
import maya.cmds as cmds
v = cmds.xform("your_object_here.vtx[*]", q=True, t=True)
results = set()
for i in range(0, len(v), 3):
results. add ( (v[i], v[i+1], v[i+2]))
has_coincident_points = len(results) < (len(v) // 3)
polySelectConstraint is a great tool for this sort of thing, however – just be sure to turn the constraints OFF after you used them for informational purposes!
The Get zero area geometry edges function is implemented in Maya as follows:
whatIs polyCleanupArgList;
// Result: Mel procedure found in: C:/Program Files/Autodesk/Maya2024/scripts/others/polyCleanupArgList.mel
// Get zero area geometry edges
if ($egeom)
{
select -r $selectedItems;
//print ("Select edges between " + $minedge + " " + $maxedge + "\n");
polySelectConstraint -m 3 -t 0x8000 -l on -lb $minedge $maxedge;
$selected = `ls -sl`;
concat( $edges, $selected );
polySelectConstraint -m 0 -t 0x8000 -l off;
}
From the code, we see that the workhorse is the polySelectConstraint command.
In cycles, it works veery slowly.
But its advantage is manifested when working with a huge array of data passed once to this command as an argument.
Putting the command cmds.polySelectConstraint in a cumbersome wrapper (like lib.py) is rather doubtful from the point of view of expediency. This does not provide any benefits (outside the context of your Pipeline), but complicates the code and reduces (!!!) performance!
Regarding MItMeshEdge:
It is important to remember that without specifying the MSpace flag, many functions in OpenMaya default to Coordinate Space Type: Object Space.
Therefore, it is correct to always specify space, like so:
OpenMaya.MItMeshEdge.length(space = OpenMaya.MSpace.kWorld)
or OpenMaya.MItMeshEdge.length(4)
Naturally, translating coordinate values from object space to world space takes some time.
On a large data set, the difference in the time spent on the execution of the function becomes very noticeable.
Concerning adding objects to MSelectionList in a cycle:
This is a very, very bad idea. The execution time will increase linearly as the number of added objects increases.
Component indexes need to be added to a temporary Python List or MIntArray (whatever you prefer, it doesn’t affect speed very much).
Once the list is complete, the components can be added to MSelectionList in one go (almost instantly).
Obviously, adding component indices to a temporary list or MIntArray() takes up most of the loop’s execution time. But the increase in time will no longer be so dramatic and will be acceptable for most use cases.
import maya.cmds as cmds
import maya.api.OpenMaya as om2
maya_useNewAPI = True
try:
from time import perf_counter as t_clock
except ImportError:
from time import clock as t_clock
# The classic option for generating a list of edges whose length is in the specified range.
# Using Python cmds.polySelectConstraint()
def cmds_sel_edges_for_len(geo, maxedge, minedge = 0.0):
cmds.select(geo, replace = True)
# print ('Select edges between: {} {}\n'.format(minedge, maxedge)) # optional
cmds.polySelectConstraint(mode = 3, type = 0x8000, length = 1, lengthbound = (minedge, maxedge))
selected = cmds.ls(selection = True)
cmds.polySelectConstraint(mode = 0, type = 0x8000, length = 0)
# cmds.select(clear = True) # optional
return selected
# The classic option for generating a list of edges whose length is in the specified range.
# Using maya.api.OpenMaya.MItMeshEdge()
def om2_sel_edges_for_len_v00(geo, maxedge, m_space = om2.MSpace.kObject, minedge = 0.0):
sel = om2.MSelectionList()
sel.add(geo)
node, components = sel.getComponent(0)
edges_iter = om2.MItMeshEdge(node, components)
edges_index_array = om2.MIntArray()
while not edges_iter.isDone():
edge_len = edges_iter.length(m_space)
if (edge_len <= maxedge) and (edge_len >= minedge):
edges_index_array.append(edges_iter.index())
edges_iter.next()
edges_sic = om2.MFnSingleIndexedComponent()
edges_components = edges_sic.create(om2.MFn.kMeshEdgeComponent)
edges_sic.addElements(edges_index_array)
edges_sel_list = om2.MSelectionList()
edges_sel_list.add((node, edges_components)) # !!! ((node, components))
# om2.MGlobal.setActiveSelectionList(edges_sel_list) # optional
return edges_sel_list.getSelectionStrings()
# Create test polygon geometry: polySphere - 4 000 000 polygons, 7 998 000 edges:
geo = cmds.polySphere(radius = 100,
subdivisionsX = 2000,
subdivisionsY = 2000,
axis = (0, 1, 0),
createUVs = 2,
constructionHistory = False)[0]
# for test #1
minedge = 0.0
maxedge = 0.1
# for test #2
minedge = 0.0
maxedge = 0.5
start = t_clock()
cel_edges = cmds_sel_edges_for_len(geo, maxedge)
print('Total time: {:.4f} s.'.format(t_clock() - start))
# Total time: 0.5715 s.( 824 000 edges from length ranges 0-0.1)
# Total time: 0.5735 s.(8 000 000 edges from length ranges 0-0.5)
start = t_clock()
cel_edges = om2_sel_edges_for_len_v00(geo, maxedge, 2) # OpenMaya.MSpace.kObject
print('Total time: {:.4f} s.'.format(t_clock() - start))
# Total time: 2.6758 s.( 824 000 edges from length ranges 0.0-0.1, object space)
# Total time: 3.6949 s.(8 000 000 edges from length ranges 0.0-0.5, object space)
start = t_clock()
cel_edges = om2_sel_edges_for_len_v00(geo, maxedge, 4) # OpenMaya.MSpace.kWorld
print('Total time: {:.4f} s.'.format(t_clock() - start))
# Total time: 4.1402 s.( 824 000 edges from length ranges 0.0-0.1, world space)
# Total time: 5.1129 s.(8 000 000 edges from length ranges 0.0-0.5, world space)
# Not the correct version of the function using maya.api.OpenMaya.MItMeshEdge()
# You shouldn't add components one by one to MSelectionList() inside the loop. This is very time consuming.
start = t_clock()
zero_length_edges = om2.MSelectionList()
sel = om2.MGlobal.getSelectionListByName(geo)
component = sel.getComponent(0)
itr_mesh = om2.MItMeshEdge(*component)
while not itr_mesh.isDone():
if itr_mesh.length() <= maxedge:
zero_length_edges.add((component[0], itr_mesh.currentItem()))
itr_mesh.next()
# Select Edges with Zero Length
om2.MGlobal.setActiveSelectionList(zero_length_edges)
print('Total time: {:.4f} s.'.format(t_clock() - start))
# Total time: 13.3418 / 16.6200 s. ( 824 000 edges from length ranges 0-0.1, object space/world space)
# Total time: 107.5833 / 113.7870 s. (8 000 000 edges from length ranges 0-0.5, object space/world space)
With all due respect, this is a false assumption.
Obviously, a grid can contain many different vertices that have the same coordinates but do not have common edges.
The edge length is equal to zero if the coordinates of the vertices are the same and these vertices HAVE COMMON EDGES!
N.B.
It is important to keep in mind one feature of Maya when generating lists of components.
If slices cannot be used efficiently to write a list of components (because the components are single and scattered), then performance in the case of forming huge “flattened lists” (in the case of polySelectConstraint) can drop by several orders of magnitude…
I wish you all harmony, good luck and effective self-improvement!
Sorry for my bad English, it’s not my native language…
A mesh may have coincident points and no zero-length edges – but a mesh with no coincident points _cannot _ have zero-length edges unless somebody has manually messed with the poly list to repeat the same vertex in a polygon… which I’m not sure is possible. Whether it’s a worthwhile optimization depends a lot on the expected data: if one mesh in a hundred has zero-length edges it’s worth doing, if one of two does then maybe not so much.
@VVVSLAVA When I wrote that snippet, I poorly assumed that this was to gather a handful of zero length edges and didn’t consider that it might be used with large meshes. Thanks for the tip on using MFnSingleIndexedComponent! I wasn’t aware you could plug that in for one go, but that is great to know. As others have mentioned, it’s always best to avoid resizing arrays (especially during iteration) since memory allocation is slow. Another thing you might want do is to set the length of the int array to the count before and trim it after. Although… I just did a test and time savings appear to be negligible, 0.05 seconds.