-
-
Save tbttfox/9ca775bf629c7a1285c27c8d9d961bca to your computer and use it in GitHub Desktop.
from maya import OpenMaya as om | |
import numpy as np | |
from ctypes import c_float, c_double, c_int, c_uint | |
_CONVERT_DICT = { | |
om.MPointArray: (float, 4, c_double, om.MScriptUtil.asDouble4Ptr), | |
om.MFloatPointArray: (float, 4, c_float , om.MScriptUtil.asFloat4Ptr), | |
om.MVectorArray: (float, 3, c_double, om.MScriptUtil.asDouble3Ptr), | |
om.MFloatVectorArray: (float, 3, c_float , om.MScriptUtil.asFloat3Ptr), | |
om.MDoubleArray: (float, 1, c_double, om.MScriptUtil.asDoublePtr), | |
om.MFloatArray: (float, 1, c_float , om.MScriptUtil.asFloatPtr), | |
om.MIntArray: (int , 1, c_int , om.MScriptUtil.asIntPtr), | |
om.MUintArray: (int , 1, c_uint , om.MScriptUtil.asUintPtr), | |
} | |
def _swigConnect(mArray, count, util): | |
''' | |
Use an MScriptUtil to build SWIG array that we can read from and write to. | |
Make sure to get the MScriptUtil from outside this function, otherwise | |
it may be garbage collected | |
The _CONVERT_DICT holds {mayaType: (pyType, numComps, cType, ptrType)} where | |
pyType: The type that is used to fill the MScriptUtil array. | |
numComps: The number of components. So a double4Ptr would be 4 | |
cType: The ctypes type used to read the data | |
ptrType: An unbound method on MScriptUtil to cast the pointer to the correct type | |
I can still call that unbound method by manually passing the usually-implicit | |
self argument (which will be an instance of MScriptUtil) | |
''' | |
pyTyp, comps, ctp, ptrTyp = _CONVERT_DICT[type(mArray)] | |
cc = (count * comps) | |
util.createFromList([pyTyp()] * cc, cc) | |
#passing util as 'self' to call the unbound method | |
ptr = ptrTyp(util) | |
mArray.get(ptr) | |
if comps == 1: | |
cdata = ctp * count | |
else: | |
# Multiplication follows some strange rules here | |
# I would expect (ctype*3)*N to be an Nx3 array (ctype*3 repeated N times) | |
# However, it gets converted to a 3xN array | |
cdata = (ctp * comps) * count | |
# int(ptr) gives the memory address | |
cta = cdata.from_address(int(ptr)) | |
# This makes numpy look at the same memory as the ctypes array | |
# so we can both read from and write to that data through numpy | |
npArray = np.ctypeslib.as_array(cta) | |
return npArray, ptr | |
def mayaToNumpy(mArray): | |
''' Convert a maya array to a numpy array | |
Parameters | |
---------- | |
ary : MArray | |
The maya array to convert to a numpy array | |
Returns | |
------- | |
: np.array : | |
A numpy array that contains the data from mArray | |
''' | |
util = om.MScriptUtil() | |
count = mArray.length() | |
npArray, _ = _swigConnect(mArray, count, util) | |
return np.copy(npArray) | |
def numpyToMaya(ary, mType): | |
''' Convert a numpy array to a specific maya type array | |
Parameters | |
---------- | |
ary : np.array | |
The numpy array to convert to a maya array | |
mType : type | |
The maya type to convert to out of: MPointArray, MFloatPointArray, MVectorArray, | |
MFloatVectorArray, MDoubleArray, MFloatArray, MIntArray, MUintArray | |
Returns | |
------- | |
: mType : | |
An array of the provided type that contains the data from ary | |
''' | |
util = om.MScriptUtil() | |
# Add a little shape checking | |
comps = _CONVERT_DICT[mType][1] | |
if comps == 1: | |
if len(ary.shape) != 1: | |
raise ValueError("Numpy array must be 1D to convert to the given maya type") | |
else: | |
if len(ary.shape) != 2: | |
raise ValueError("Numpy array must be 2D to convert to the given maya type") | |
if ary.shape[1] != comps: | |
msg = "Numpy array must have the proper shape. Dimension 2 has size {0}, but needs size {1}" | |
raise ValueError(msg.format(ary.shape[1], comps)) | |
count = ary.shape[0] | |
mArray = mType(count) | |
npArray, ptr = _swigConnect(mArray, count, util) | |
np.copyto(npArray, ary) | |
return mType(ptr, count) | |
_NTYPE_DICT={ | |
om.MFnNumericData.kInvalid: (om.MDataHandle.asDouble, om.MDataHandle.setDouble), | |
om.MFnNumericData.kFloat: (om.MDataHandle.asDouble, om.MDataHandle.setDouble), | |
om.MFnNumericData.kDouble: (om.MDataHandle.asDouble, om.MDataHandle.setDouble), | |
om.MFnNumericData.kByte: (om.MDataHandle.asInt, om.MDataHandle.setInt), | |
om.MFnNumericData.kChar: (om.MDataHandle.asChar, om.MDataHandle.setChar), | |
om.MFnNumericData.kShort: (om.MDataHandle.asShort, om.MDataHandle.setShort), | |
om.MFnNumericData.kInt: (om.MDataHandle.asInt, om.MDataHandle.setInt), | |
#om.MFnNumericData.kInt64: (om.MDataHandle.asInt, om.MDataHandle.setInt64), | |
om.MFnNumericData.kAddr: (om.MDataHandle.asInt, om.MDataHandle.setInt), | |
om.MFnNumericData.kLong: (om.MDataHandle.asInt, om.MDataHandle.setInt), | |
om.MFnNumericData.kBoolean: (om.MDataHandle.asBool, om.MDataHandle.setBool), | |
om.MFnNumericData.k2Short: (om.MDataHandle.asShort2, om.MDataHandle.set2Short), | |
om.MFnNumericData.k2Long: (om.MDataHandle.asInt2, om.MDataHandle.set2Int), | |
om.MFnNumericData.k2Int: (om.MDataHandle.asInt2, om.MDataHandle.set2Int), | |
om.MFnNumericData.k3Short: (om.MDataHandle.asShort3, om.MDataHandle.set3Short), | |
om.MFnNumericData.k3Long: (om.MDataHandle.asInt3, om.MDataHandle.set3Int), | |
om.MFnNumericData.k3Int: (om.MDataHandle.asInt3, om.MDataHandle.set3Int), | |
om.MFnNumericData.k2Float: (om.MDataHandle.asFloat2, om.MDataHandle.set2Float), | |
om.MFnNumericData.k2Double: (om.MDataHandle.asDouble2, om.MDataHandle.set2Double), | |
om.MFnNumericData.k3Float: (om.MDataHandle.asFloat3, om.MDataHandle.set3Float), | |
om.MFnNumericData.k3Double: (om.MDataHandle.asDouble3, om.MDataHandle.set3Double), | |
} | |
_DTYPE_DICT = { | |
om.MFn.kPointArrayData: (om.MFnPointArrayData, om.MPointArray), | |
om.MFn.kDoubleArrayData: (om.MFnDoubleArrayData, om.MDoubleArray), | |
om.MFn.kFloatArrayData: (om.MFnFloatArrayData, om.MFloatArray), | |
om.MFn.kIntArrayData: (om.MFnIntArrayData, om.MIntArray), | |
om.MFn.kUInt64ArrayData: (om.MFnUInt64ArrayData, om.MPointArray), | |
om.MFn.kVectorArrayData: (om.MFnVectorArrayData, om.MVectorArray), | |
} | |
def getNumpyAttr(attrName): | |
''' Read attribute data directly from the plugs into numpy | |
This function will read most numeric data types directly into numpy arrays | |
However, some simple data types (floats, vectors, etc...) have api accessors | |
that return python tuples. These will not be turned into numpy arrays. | |
And really, if you're getting simple data like that, just use cmds.getAttr | |
Parameters | |
---------- | |
attrName : str or om.MPlug | |
The name of the attribute to get (For instance "pSphere2.translate", or "group1.pim[0]") | |
Or the MPlug itself | |
Returns | |
------- | |
: object : | |
The numerical data from the provided plug. A np.array, float, int, or tuple | |
''' | |
if isinstance(attrName, basestring): | |
sl = om.MSelectionList() | |
sl.add(attrName) | |
plug = om.MPlug() | |
sl.getPlug(0, plug) | |
elif isinstance(attrName, om.MPlug): | |
plug = attrName | |
#First just check if the data is numeric | |
mdh = plug.asMDataHandle() | |
if mdh.isNumeric(): | |
# So, at this point, you should really just use getattr | |
ntype = mdh.numericType() | |
if ntype in _NTYPE_DICT: | |
return _NTYPE_DICT[ntype][0](mdh) | |
elif ntype == om.MFnNumericData.k4Double: | |
NotImplementedError("Haven't implemented double4 access yet") | |
else: | |
raise RuntimeError("I don't know how to access data from the given attribute") | |
else: | |
# The data is more complex than a simple number. | |
try: | |
pmo = plug.asMObject() | |
except RuntimeError: | |
# raise a more descriptive error. And make sure to actually print the plug name | |
raise RuntimeError("I don't know how to access data from the given attribute") | |
apiType = pmo.apiType() | |
# A list of types that I can just pass to mayaToNumpy | |
if apiType in _DTYPE_DICT: | |
fn, dtype = _DTYPE_DICT[apiType] | |
fnPmo = fn(pmo) | |
ary = fnPmo.array() | |
return mayaToNumpy(ary) | |
elif apiType == om.MFn.kComponentListData: | |
fnPmo = om.MFnComponentListData(pmo) | |
mirs = [] | |
mir = om.MIntArray() | |
for attrIndex in xrange(fnPmo.length()): | |
fnEL = om.MFnSingleIndexedComponent(fnPmo[attrIndex]) | |
fnEL.getElements(mir) | |
mirs.append(mayaToNumpy(mir)) | |
return np.concatenate(mirs) | |
elif apiType == om.MFn.kMatrixData: | |
fnPmo = om.MFnMatrixData(pmo) | |
mat = fnPmo.matrix() | |
return mayaToNumpy(mat) | |
else: | |
apiTypeStr = pmo.apiTypeStr() | |
raise NotImplementedError("I don't know how to handle {0} yet".format(apiTypeStr)) | |
raise NotImplementedError("Fell all the way through") | |
def setNumpyAttr(attrName, value): | |
''' Write a numpy array directly into a maya plug | |
This function will handle most numeric plug types. | |
But for single float, individual point, etc.. types, consider using cmds.setAttr | |
THIS DOES NOT SUPPORT UNDO | |
Parameters | |
---------- | |
attrName : str or om.MPlug | |
The name of the attribute to get (For instance "pSphere2.translate", or "group1.pim[0]") | |
Or the MPlug itself | |
value : int, float, tuple, np.array | |
The correctly typed value to set on the attribute | |
''' | |
if isinstance(attrName, basestring): | |
sl = om.MSelectionList() | |
sl.add(attrName) | |
plug = om.MPlug() | |
sl.getPlug(0, plug) | |
elif isinstance(attrName, om.MPlug): | |
plug = attrName | |
else: | |
raise ValueError("Data must be string or MPlug. Got {0}".format(type(attrName))) | |
#First just check if the data is numeric | |
mdh = plug.asMDataHandle() | |
if mdh.isNumeric(): | |
# So, at this point, you should really just use setattr | |
ntype = mdh.numericType() | |
if ntype in _NTYPE_DICT: | |
_NTYPE_DICT[ntype][1](mdh, *value) | |
plug.setMObject(mdh.data()) | |
elif ntype == om.MFnNumericData.k4Double: | |
NotImplementedError("Haven't implemented double4 access yet") | |
else: | |
raise RuntimeError("I don't know how to set data on the given attribute") | |
else: | |
# The data is more complex than a simple number. | |
try: | |
pmo = plug.asMObject() | |
except RuntimeError: | |
# raise a more descriptive error. And make sure to actually print the plug name | |
raise RuntimeError("I don't know how to access data from the given attribute") | |
apiType = pmo.apiType() | |
if apiType in _DTYPE_DICT: | |
# build the pointArrayData | |
fnType, mType = _DTYPE_DICT[apiType] | |
fn = fnType() | |
mPts = numpyToMaya(value, mType) | |
dataObj = fn.create(mPts) | |
plug.setMObject(dataObj) | |
return | |
elif apiType == om.MFn.kComponentListData: | |
fnCompList = om.MFnComponentListData() | |
compList = fnCompList.create() | |
fnIdx = om.MFnSingleIndexedComponent() | |
idxObj = fnIdx.create(om.MFn.kMeshVertComponent) | |
mIdxs = numpyToMaya(value, om.MIntArray) | |
fnIdx.addElements(mIdxs) | |
fnCompList.add(idxObj) | |
plug.setMObject(compList) | |
return | |
else: | |
apiTypeStr = pmo.apiTypeStr() | |
raise NotImplementedError("I don't know how to handle {0} yet".format(apiTypeStr)) | |
raise NotImplementedError("WTF? How did you get here??") | |
################################################################################ | |
def test(): | |
import time | |
from maya import cmds | |
meshName = 'pSphere1' | |
bsName = 'blendShape1' | |
meshIdx = 0 | |
bsIdx = 0 | |
# A quick test showing how to build a numpy array | |
# containing the deltas for a shape on a blendshape node | |
numVerts = cmds.polyEvaluate(meshName, vertex=True) | |
baseAttr = '{0}.it[{1}].itg[{2}].iti[6000]'.format(bsName, meshIdx, bsIdx) | |
inPtAttr = baseAttr + '.inputPointsTarget' | |
inCompAttr = baseAttr + '.inputComponentsTarget' | |
start = time.time() | |
points = getNumpyAttr(inPtAttr) | |
idxs = getNumpyAttr(inCompAttr) | |
ret = np.zeros((numVerts, 4)) | |
ret[idxs] = points | |
end = time.time() | |
print "IDXS", idxs.shape | |
print "OUT", points.shape | |
print "RET", ret.shape | |
print "TOOK", end - start | |
if __name__ == "__main__": | |
test() |
The one from this link works in 2018 and after
https://forums.autodesk.com/t5/maya-programming/numpy-1-13-1-scipy-0-19-1-for-maya-2018/td-p/7362541
Also, you can install pip for mayapy.exe, and then install numpy. From a windows command prompt, navigate to the maya bin
folder then do this:
mayapy.exe -m ensurepip --upgrade
mayapy.exe -m pip install <path-to-that-numpy-wheel>
Thanks! I actually saw this link before, but there is no more numpy wheel in that Google Drive folder, only a numexpr wheel :/
I ended up finding another site which pointed to a repo with Maya-compatible packages, here's the procedure:
enabling pip for Maya:
see https://discourse.techart.online/t/numpy-1-13-1-scipy-0-19-1-for-maya-2018/9121/11
and https://forums.autodesk.com/t5/maya-programming/numpy-1-13-1-scipy-0-19-1-for-maya-2018/td-p/7362541
- Get pip to run on MayaPY: form an elevated command prompt navigate to mayapy folder and run:
mayapy -m ensurepip
- Update pip:
mayapy -m pip install --upgrade pip
mayapy -m pip install --upgrade setuptools
installing numpy/scipy:
pip install -i https://pypi.anaconda.org/carlkl/simple numpy
pip install -i https://pypi.anaconda.org/carlkl/simple scipy
... But there is a numpy wheel in that google drive folder from the autodesk link. I checked before I posted. It was down for a while, but he re-uploaded it about 3 months ago according to the last message in that thread.
Also the stuff on the anaconda link is latest 1.11.0
Hi thanks for sharing these scripts they are very useful!
I am encountering a problem trying to get the targetWeights attribute from a blendshape target.
baseAttr = '{0}.inputTarget[{1}].inputTargetGroup[{2}].targetWeights'.format(bsName, meshIdx, bsIdx)
I only get one value out of that. Is this one of those circumstances where you advise to use the getAttr cmds?
Thank you!
Short answer: Yep, use cmds.getAttr
Longer answer:
See the input points target returns a "typed" type and "False" for multi
attr = 'inputTarget.inputTargetGroup.inputTargetItem.inputPointsTarget'
cmds.attributeQuery(attr.split('.')[-1], node='blendShape1', attributeType=True) # returns "typed"
cmds.attributeQuery(attr.split('.')[-1], node='blendShape1', multi=True) # returns "false"
However, the targetWeights will return "Float" and "True"
attr = 'inputTarget.inputTargetGroup.targetWeights'
cmds.attributeQuery(attr.split('.')[-1], node='blendShape1', attributeType=True) # returns "float"
cmds.attributeQuery(attr.split('.')[-1], node='blendShape1', multi=True) # returns "True"
This library handles when the type of the single attribute is some kind of arrayType (like MFloatArray or MPointArray) which is reported as a "typed" attributeType.
However, targetWeights
is not a single attribute, it's a multi-attribute. So you'd have to loop over (in this case) the vertex indices with a baseAttr like this:
baseAttr = '{0}.inputTarget[{1}].inputTargetGroup[{2}].targetWeights[{3}]'.format(bsName, meshIdx, bsIdx, vertIdx)
And I'm 99% sure that'd be slower than just using cmds.getAttr
Thanks for the answer and the detailed explanation!
I wonder if there is anything faster than getAttr. I am not having the best performances using it.
Cheers
Hey, can I ask where you got a Maya-compatible numpy? any chance you could share a build, for Maya 2020 + Win64?