Python CAN be fast for Maya vertex access

Accessing vertex data in Python can be trying at times. Whether you go through cmds or OpenMaya, the time it takes to get, and especially set the data can be the difference between building a tool in Python, or pulling out the big guns and writing a c++ plugin. But fear not, there is a way around this fast data access so long as you have numpy installed for maya.

Edit: Since this got a little traction on Facebook, here’s a link to a github gist with a module that can freely convert between numpy and Maya’s array types.
https://gist.github.com/tbttfox/9ca775bf629c7a1285c27c8d9d961bca

If you don’t have numpy installed for maya, you can get it for 2014-2016 here, and 2018 here. One of those should work for 2017, but I don’t have it installed to test with.

Speaking of testing, I’m going to subdivide the heck out of a sphere and see how long it takes to get a python list of the verts using both cmds and OpenMaya.

I subdivided a poly sphere 5 times to get it up to about 400k verts just to make the numbers bigger, and then I ran this completely un-scientific test:

from maya import OpenMaya as om
from maya import cmds

def getPointsOpenMaya(pyList=True):
	sel = om.MSelectionList()
	sel.add('pSphere1')

	dagPath = om.MDagPath()
	sel.getDagPath(0, dagPath)
	verts = om.MPointArray()
	fnMesh = om.MFnMesh(dagPath)
	fnMesh.getPoints(verts, om.MSpace.kWorld)

	if pyList:
		return [([verts[i][0], verts[i][1], verts[i][2]]) for i in range(verts.length()) ]
	return verts

def getPointsCmds():
	flatverts = cmds.xform("pSphere1.vtx[*]", translation=1, query=1, worldSpace=False)
	args = [iter(flatverts)] * 3
	return zip(*args)


import time
start = time.time()
getPointsCmds()
took = time.time() - start
print "cmds Took", took

start = time.time()
getPointsOpenMaya(True)
took = time.time() - start
print "OpenMaya Took", took

start = time.time()
getPointsOpenMaya(False)
took = time.time() - start
print "MPointArray Took", took

#cmds Took 0.594000101089
#OpenMaya Took 2.10800004005
#MPointArray Took 0.0269999504089

So cmds took about half a second to get that data. That’s not bad if you only have to do it once. Turning an MPointArray into a list of tuples took about 2 seconds. Ugh. But just getting the MPointArray by itself took 1/40 of a second. That’s not too shabby, but we can’t really do anything with it without running it through that python list conversion that took 100x longer. But there’s a better way.

The trick is the MScriptUtil, and the PlainOldData getters/setters on the Maya numeric types return SWIG objects. There’s also an undocumented function OpenMaya.MFnMesh.getRawPoints() (you can find it in the C++ docs though) that returns a SWIG Float Pointer. So what does that give us? Well, if you call int() on that object, it returns that object’s memory address. And that’s a step in the right direction … but it’s not quite there yet. But numpy does have ctypeslib for reading C style data, so if we convert this SWIG object to a ctypes array, we should be able to pass that into numpy.

from maya import OpenMaya as om
from ctypes import c_float
import numpy as np
import time

start = time.time()
sel = om.MSelectionList()
sel.add('pSphere1')
dagPath = om.MDagPath()
sel.getDagPath(0, dagPath)

fnMesh = om.MFnMesh(dagPath)
# rawPts is a SWIG float pointer
rawPts = fnMesh.getRawPoints()
ptCount = fnMesh.numVertices()


# Cast the swig double pointer to a ctypes array
cta = (c_float * ptCount * 3).from_address(int(rawPts))
# Memory map the ctypes array into numpy
out = np.ctypeslib.as_array(cta)
# ptr, cta, and out are all pointing to the same memory address now
# so changing out will change ptr
# but in this case

# for safety, make a copy of out so I don't corrupt memory
out = np.copy(out)
end = time.time()

print "Getting the numpy interface took:", end - start

# Getting the numpy interface took: 0.00299978256226

So yeah, getting that data straight into numpy is another 8x faster than just accessing the MPointArray. Be careful though because you can very easily corrupt memory and cause all kinds of havoc. All of this is dealing with pointers and memoryviews, which don’t increase python’s reference counting.  So just like MScriptUtil objects, you either have to pass the pointer object around, or get your data out of it before the pointer gets garbage collected. In the script above, you can see I run np.copy(out) to do just that.

However, this isn’t just a one way street.  If you were to use MScriptUtil to build a float pointer, you could fill that pointer with data from numpy which would then be accessible to OpenMaya functions.

Now if only Autodesk would include the imathnumpy module (which is part of pyalembic) so I could write a stupidly fast alembic exporter in python.

Leave a Reply

Your email address will not be published. Required fields are marked *