├── README.md ├── arapDeformer.py ├── inCaseMayaSucks.py ├── keps └── keps.000.gif └── plug-ins └── arapDeformerNode.py /README.md: -------------------------------------------------------------------------------- 1 | as-rigid-as-possible (arap) maya deformer, in python 2 | 3 | ![keps.000.gif](https://github.com/leventt/arap/blob/master/keps/keps.000.gif "keps.000.gif") 4 | 5 | 6 | Intention of this implementation was first to understand how it works by prototyping it in python and then share it as a reference, if someone may find it useful, as I do, to read it in python. 7 | It is important to mention that inCaseMayaSucks.py is pretty much a rewrite of this: 8 | https://github.com/TanaTanoi/as-rigid-as-possible-deformation 9 | So I get a sense for while re-writing it. 10 | 11 | Having said that, there are way more performant implementations out there and I would hate if you lost time trying to make this one work to find out it doesn't perform as fast for dense meshes as you might expect. 12 | 13 | I would actually recommend the libigl implementation here: 14 | https://github.com/libigl/libigl/blob/master/include/igl/arap.h 15 | or Shizuo Kaji has some great implementations, if you want to use something similar and compare: 16 | https://github.com/shizuo-kaji/CageDeformerMaya 17 | https://github.com/shizuo-kaji/ProbeDeformerMaya 18 | https://github.com/shizuo-kaji/PoissonMLS 19 | (last one isn't ARAP but MLS) 20 | 21 | You may appreciate this demo as well: 22 | https://github.com/libigl/libigl/blob/master/tutorial/406_FastAutomaticSkinningTransformations/main.cpp 23 | 24 | Overall, I felt that MLS (Moving Least Squares) method was more robust and faster. 25 | 26 | If you don't have maya, check out inCaseMayaSucks.py 27 | That should work with python3, scipy and numpy, in which case miniconda is great to run that: 28 | https://docs.conda.io/en/latest/miniconda.html 29 | 30 | inCaseMayaSucks.py also has some adjustments (converting itertool objects to lists and tuples) to make it work in python3. 31 | 32 | 33 | 34 | That out of the way... 35 | If you want to try this one out and you have a maya installation... 36 | 1.) You would need to install numpy and scipy for maya's python interpreter. This is challenging if you are on Windows, so here is a link, before you may waste time trying to figure that out: 37 | https://forums.autodesk.com/t5/maya-programming/guide-how-to-install-numpy-scipy-in-maya-windows-64-bit/td-p/5796722 38 | (There are much cleaner ways to install this I feel like but this may be straight-forward) 39 | 2.) You would need to put the plug-ins folder into your maya plugins path. Straight forward way to do this is to find the maya folder on your system either in your home folder or Documents on Windows... Here are more ways to do it: https://knowledge.autodesk.com/support/maya/learn-explore/caas/CloudHelp/cloudhelp/2016/ENU/Maya/files/GUID-FA51BD26-86F3-4F41-9486-2C3CF52B9E17-htm.html 40 | 3.) You will need to also put arapDeformer.py into a maya scripts path... Most straight-forward one is that same maya folder mentioned before/scripts... 41 | (MAYA_SCRIPT_PATH mentioned here: https://knowledge.autodesk.com/support/maya/learn-explore/caas/CloudHelp/cloudhelp/2015/ENU/Maya/files/Environment-Variables-File-path-variables-htm.html) 42 | 4.) After you can successfully import that arapDeformer module... call the showTool function. 43 | 44 | 45 | 46 | Also check this prototype if you may want to use the libigl library from python, like I was trying before for a prototype: 47 | https://github.com/leventt/elastik 48 | 49 | Cheers \o/ 50 | -------------------------------------------------------------------------------- /arapDeformer.py: -------------------------------------------------------------------------------- 1 | import json 2 | import maya.mel as MM 3 | import maya.cmds as MC 4 | from PySide import QtGui 5 | 6 | 7 | def __load_plugin(): 8 | pluginPath = 'arapDeformerNode.py' 9 | if not MC.pluginInfo(pluginPath, query=True, loaded=True): 10 | if MC.loadPlugin(pluginPath) is None: 11 | MC.warning('Failed to load plugin.') 12 | return False 13 | return True 14 | 15 | 16 | def __apply(mesh): 17 | name = mesh.split('|')[-1] + '_arapDeformer' 18 | name = MM.eval('formValidObjectName("%s");' % name) 19 | return MC.deformer(mesh, name=name, type='arapDeformer') 20 | 21 | 22 | def applyArapDeformer(mesh): 23 | if not __load_plugin(): 24 | return 25 | 26 | return __apply(mesh) 27 | 28 | 29 | class ArapDeformerDialog(QtGui.QDialog): 30 | 31 | instance = None 32 | 33 | def __init__(self, *args): 34 | ''' 35 | build UI 36 | ''' 37 | super(ArapDeformerDialog, self).__init__(*args) 38 | 39 | self.setWindowTitle('ARAP Deformer Tool') 40 | self.setMinimumSize(128, 128) 41 | 42 | mainLayout = QtGui.QVBoxLayout() 43 | self.setLayout(mainLayout) 44 | 45 | addHandleButton = QtGui.QPushButton('Add Handle') 46 | mainLayout.addWidget(addHandleButton) 47 | addHandleButton.clicked.connect(self.addHandleCB) 48 | 49 | def addHandleCB(self, *args): 50 | selection = MC.ls(selection=True) 51 | selectedVertIDs = [] 52 | meshPath = None 53 | for vtxSel in selection: 54 | if meshPath is None: 55 | meshPath = vtxSel.split('.')[0] 56 | if meshPath != vtxSel.split('.')[0]: 57 | MC.warning('select vertices from one mesh!') 58 | return 59 | if '[' in vtxSel: 60 | if ':' in vtxSel: 61 | start, end = vtxSel.split('[')[-1].split(']')[0]. split(':') 62 | start = int(start) 63 | end = int(end) 64 | selectedVertIDs.extend(range(start, end + 1)) 65 | else: 66 | selectedVertIDs.append(int(vtxSel.split('[')[-1].split(']')[0])) 67 | else: 68 | MC.warning('Select vertices only!') 69 | return 70 | if vtxSel.split('.')[-1].split('[')[0] != 'vtx': 71 | MC.warning('Select vertices only!') 72 | return 73 | 74 | bbox = MC.exactWorldBoundingBox(selection) 75 | centerPos = ( 76 | (bbox[0] + bbox[3]) / 2., 77 | (bbox[1] + bbox[4]) / 2., 78 | (bbox[2] + bbox[5]) / 2. 79 | ) 80 | handleControl = MC.spaceLocator(position=(0, 0, 0), name='handleTemp')[0] 81 | MC.move(centerPos[0], centerPos[1], centerPos[2], handleControl) 82 | MC.makeIdentity(handleControl, apply=True, t=True, r=True, s=True, normal=False) 83 | 84 | arapDeformer = MC.listConnections('%s.inMesh' % meshPath, type='arapDeformer') 85 | if arapDeformer: 86 | arapDeformer = arapDeformer[0] 87 | else: 88 | arapDeformer = applyArapDeformer(meshPath)[0] 89 | 90 | try: 91 | handleIDs = json.loads(MC.getAttr('%s.handleIds' % arapDeformer)) 92 | except Exception: 93 | handleIDs = [] 94 | if handleIDs is None: 95 | handleIDs = [] 96 | existingHandleCount = len(handleIDs) 97 | 98 | for i in range(len(selectedVertIDs)): 99 | MC.connectAttr('%s.worldMatrix[0]' % handleControl, '%s.inputMatrices[%s]' % (arapDeformer, i + existingHandleCount), force=True) 100 | 101 | handleIDs.extend(selectedVertIDs) 102 | MC.setAttr('%s.handleIds' % arapDeformer, json.dumps(handleIDs), type='string') 103 | 104 | 105 | def showTool(): 106 | if ArapDeformerDialog.instance is None: 107 | ArapDeformerDialog.instance = ArapDeformerDialog() 108 | ArapDeformerDialog.instance.show() 109 | -------------------------------------------------------------------------------- /inCaseMayaSucks.py: -------------------------------------------------------------------------------- 1 | import math 2 | import itertools 3 | import numpy as np 4 | from scipy.sparse import csgraph 5 | 6 | 7 | # referenced and modified from: https://github.com/TanaTanoi/as-rigid-as-possible-deformation 8 | class ARAP: 9 | def neighboursOf(self, vertID): 10 | return np.where(self.neighbourMatrix[vertID] == 1)[0] 11 | 12 | def assignWeightForPair(self, i, j): 13 | if self.weightMatrix[j, i] == 0: 14 | weightIJ = self.weightForPair(i, j) 15 | else: 16 | weightIJ = self.weightMatrix[j, i] 17 | self.weightSum[i, i] += weightIJ * 0.5 18 | self.weightSum[j, j] += weightIJ * 0.5 19 | self.weightMatrix[i, j] = weightIJ 20 | 21 | def weightForPair(self, i, j): 22 | localTris = [] 23 | for triID in self.vertsToTris[i]: 24 | tri = self.tris[triID] 25 | if i in tri and j in tri: 26 | localTris.append(tri) 27 | 28 | vertexI = self.verts[i] 29 | vertexJ = self.verts[j] 30 | 31 | cotThetaSum = 0 32 | for tri in localTris: 33 | otherVertID = list(set(tri) - set([i, j]))[0] 34 | otherVertex = self.verts[otherVertID] 35 | 36 | vA = vertexI - otherVertex 37 | vB = vertexJ - otherVertex 38 | cosTheta = vA.dot(vB) / (np.linalg.norm(vA) * np.linalg.norm(vB)) 39 | theta = math.acos(cosTheta) 40 | 41 | cotThetaSum += math.cos(theta) / math.sin(theta) 42 | 43 | return cotThetaSum * 0.5 44 | 45 | def input(self, verts, tris): 46 | self.verts = np.array(verts) 47 | self.tris = np.array(tris) 48 | 49 | self.n = len(self.verts) 50 | self.vertsPrime = np.array(self.verts) 51 | self.vertsToTris = [[j for j, tri in enumerate(self.tris) if i in tri] for i in range(self.n)] 52 | 53 | self.vertsPrime = np.asmatrix(self.vertsPrime) 54 | self.neighbourMatrix = np.zeros((self.n, self.n)) 55 | self.neighbourMatrix[ 56 | tuple(zip( 57 | *itertools.chain( 58 | *map( 59 | lambda tri: itertools.permutations(tri, 2), 60 | self.tris 61 | ) 62 | ) 63 | )) 64 | ] = 1 65 | 66 | self.cellRotations = np.zeros((self.n, 3, 3)) 67 | 68 | self.weightMatrix = np.zeros((self.n, self.n), dtype=np.float) 69 | self.weightSum = np.zeros((self.n, self.n), dtype=np.float) 70 | 71 | for vertID in range(self.n): 72 | neighbours = self.neighboursOf(vertID) 73 | for neighbourID in neighbours: 74 | self.assignWeightForPair(vertID, neighbourID) 75 | 76 | def update(self, fixedIDs, handleIDs, deformationMatrices): 77 | deformationMatrices = list(map(np.matrix, deformationMatrices)) 78 | self.deformationVerts = [] 79 | for i in range(self.n): 80 | if i in handleIDs: 81 | deformedVector = np.append(self.verts[i], 1) 82 | deformedVector = deformedVector.dot(deformationMatrices[handleIDs.index(i)]) 83 | deformedVector = np.delete(deformedVector, 3).flatten() 84 | deformedVector = np.squeeze(np.asarray(deformedVector)) 85 | self.deformationVerts.append((i, deformedVector)) 86 | elif i in fixedIDs: 87 | self.deformationVerts.append((i, self.verts[i])) 88 | 89 | # extended laplacian matrix 90 | deformationVertsNum = len(self.deformationVerts) 91 | self.laplacianMatrix = np.zeros([self.n + deformationVertsNum] * 2, dtype=np.float32) 92 | self.laplacianMatrix[:self.n, :self.n] = csgraph.laplacian(self.weightMatrix) 93 | for i in range(deformationVertsNum): 94 | vertID = self.deformationVerts[i][0] 95 | ni = i + self.n 96 | self.laplacianMatrix[ni, vertID] = 1 97 | self.laplacianMatrix[vertID, ni] = 1 98 | 99 | # precompute PiArray 100 | self.PiArray = [] 101 | for i in range(self.n): 102 | vertI = self.verts[i] 103 | neighbourIDs = self.neighboursOf(i) 104 | neighboursNum = len(neighbourIDs) 105 | 106 | Pi = np.zeros((3, neighboursNum)) 107 | 108 | for ni in range(neighboursNum): 109 | nID = neighbourIDs[ni] 110 | 111 | vertJ = self.verts[nID] 112 | Pi[:, ni] = (vertI - vertJ) 113 | self.PiArray.append(Pi) 114 | 115 | def calculateCellRotations(self): 116 | for vertID in range(self.n): 117 | rotation = self.calculateRotationMatrixForCell(vertID) 118 | self.cellRotations[vertID] = rotation 119 | 120 | def applyCellRotations(self): 121 | for i in range(self.n): 122 | self.bArray[i] = np.zeros((1, 3)) 123 | neighbours = self.neighboursOf(i) 124 | for j in neighbours: 125 | wij = self.weightMatrix[i, j] / 2.0 126 | rij = self.cellRotations[i] + self.cellRotations[j] 127 | pij = self.verts[i] - self.verts[j] 128 | self.bArray[i] += (wij * rij.dot(pij)) 129 | 130 | self.vertsPrime = np.linalg.solve(self.laplacianMatrix, self.bArray)[:self.n] 131 | 132 | def calculateRotationMatrixForCell(self, vertID): 133 | covarianceMatrix = self.calculateConvarianceMatrixForCell(vertID) 134 | 135 | U, s, VTranspose = np.linalg.svd(covarianceMatrix) 136 | 137 | rotation = VTranspose.T.dot(U.T) 138 | if np.linalg.det(rotation) <= 0: 139 | U[:0] *= -1 140 | rotation = VTranspose.T.dot(U.T) 141 | return rotation 142 | 143 | def calculateConvarianceMatrixForCell(self, vertID): 144 | vertIPrime = self.vertsPrime[vertID] 145 | 146 | neighbourIDs = self.neighboursOf(vertID) 147 | neighboursNum = len(neighbourIDs) 148 | 149 | Di = np.zeros((neighboursNum, neighboursNum)) 150 | 151 | Pi = self.PiArray[vertID] 152 | PiPrime = np.zeros((3, neighboursNum)) 153 | 154 | for ni in range(neighboursNum): 155 | nID = neighbourIDs[ni] 156 | 157 | Di[ni, ni] = self.weightMatrix[vertID, nID] 158 | 159 | vertJPrime = self.vertsPrime[nID] 160 | PiPrime[:, ni] = (vertIPrime - vertJPrime) 161 | 162 | PiPrime = PiPrime.T 163 | 164 | return Pi.dot(Di).dot(PiPrime) 165 | 166 | def apply(self, iterations): 167 | deformationVertsNum = len(self.deformationVerts) 168 | 169 | self.bArray = np.zeros((self.n + deformationVertsNum, 3)) 170 | for i in range(deformationVertsNum): 171 | self.bArray[self.n + i] = self.deformationVerts[i][1] 172 | 173 | for t in range(iterations): 174 | self.calculateCellRotations() 175 | self.applyCellRotations() 176 | 177 | 178 | def testAllTheTests(verts, tris, fixedIDs, handleIDs, inputMatrices, iterations): 179 | arap = ARAP() 180 | arap.input( 181 | verts=verts, # vertex positions as np array with shape (-1, 3) 182 | # vertex indices per triangle as np array with shape (-1, 3) 183 | tris=np.array(tris).reshape(-1, 3) 184 | ) 185 | arap.update( 186 | fixedIDs=fixedIDs, # vertex IDs that shouldn't move 187 | handleIDs=handleIDs, # vertex IDs that you want to move 188 | # targets transforms per handle (vertexes you want to move in order) 189 | deformationMatrices=inputMatrices, 190 | ) 191 | arap.apply(iterations) 192 | return arap.vertsPrime # returns the resulting verex positions 193 | 194 | 195 | if __name__ == '__main__': 196 | print(testAllTheTests( 197 | # 4 vertex positions (x, y, z) 198 | [ 199 | [-1., .0, .0], 200 | [1., .0, .0], 201 | [.0, .0, -1.], 202 | [.0, .0, 1.] 203 | ], 204 | [[0, 1, 2], [1, 2, 3]], # triangles from vertex IDs (zero indexed) 205 | [], # no fixed verts 206 | [0], # first vert is being moved 207 | # identity matrix isn't very meaningful here I guess, but a good test 208 | np.identity(4).reshape(1, 4, 4), 209 | 1 # 1 iteration 210 | )) # this should print the same vertex positions with some precision loss 211 | -------------------------------------------------------------------------------- /keps/keps.000.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leventt/arap/b96c04a79be93cd83b5f5d5320ee3c6f37550d1b/keps/keps.000.gif -------------------------------------------------------------------------------- /plug-ins/arapDeformerNode.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import math 3 | import itertools 4 | import json 5 | import numpy as np 6 | from scipy.sparse import csgraph 7 | import maya.OpenMayaMPx as OpenMayaMPx 8 | import maya.OpenMaya as OpenMaya 9 | import maya.cmds as MC 10 | 11 | 12 | pluginNodeName = 'arapDeformer' 13 | pluginNodeID = OpenMaya.MTypeId(0x77113355) 14 | 15 | # reference: http://discourse.techart.online/t/maya-2016-python-deformer-plugin/5239/6 16 | kApiVersion = MC.about(apiVersion=True) 17 | if kApiVersion < 201600: 18 | kInput = OpenMayaMPx.cvar.MPxDeformerNode_input 19 | kInputGeom = OpenMayaMPx.cvar.MPxDeformerNode_inputGeom 20 | kOutputGeom = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom 21 | kEnvelope = OpenMayaMPx.cvar.MPxDeformerNode_envelope 22 | else: 23 | kInput = OpenMayaMPx.cvar.MPxGeometryFilter_input 24 | kInputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_inputGeom 25 | kOutputGeom = OpenMayaMPx.cvar.MPxGeometryFilter_outputGeom 26 | kEnvelope = OpenMayaMPx.cvar.MPxGeometryFilter_envelope 27 | 28 | 29 | # referenced and modified from: https://github.com/TanaTanoi/as-rigid-as-possible-deformation 30 | class ARAP: 31 | def neighboursOf(self, vertID): 32 | return np.where(self.neighbourMatrix[vertID] == 1)[0] 33 | 34 | def assignWeightForPair(self, i, j): 35 | if self.weightMatrix[j, i] == 0: 36 | weightIJ = self.weightForPair(i, j) 37 | else: 38 | weightIJ = self.weightMatrix[j, i] 39 | self.weightSum[i, i] += weightIJ * 0.5 40 | self.weightSum[j, j] += weightIJ * 0.5 41 | self.weightMatrix[i, j] = weightIJ 42 | 43 | def weightForPair(self, i, j): 44 | localTris = [] 45 | for triID in self.vertsToTris[i]: 46 | tri = self.tris[triID] 47 | if i in tri and j in tri: 48 | localTris.append(tri) 49 | 50 | vertexI = self.verts[i] 51 | vertexJ = self.verts[j] 52 | 53 | cotThetaSum = 0 54 | for tri in localTris: 55 | otherVertID = list(set(tri) - set([i, j]))[0] 56 | otherVertex = self.verts[otherVertID] 57 | 58 | vA = vertexI - otherVertex 59 | vB = vertexJ - otherVertex 60 | cosTheta = vA.dot(vB) / (np.linalg.norm(vA) * np.linalg.norm(vB)) 61 | theta = math.acos(cosTheta) 62 | 63 | cotThetaSum += math.cos(theta) / math.sin(theta) 64 | 65 | return cotThetaSum * 0.5 66 | 67 | def input(self, verts, tris): 68 | self.verts = np.array(verts) 69 | self.tris = np.array(tris) 70 | 71 | self.n = len(self.verts) 72 | self.vertsPrime = np.array(self.verts) 73 | self.vertsToTris = [[j for j, tri in enumerate(self.tris) if i in tri] for i in range(self.n)] 74 | 75 | self.vertsPrime = np.asmatrix(self.vertsPrime) 76 | self.neighbourMatrix = np.zeros((self.n, self.n)) 77 | self.neighbourMatrix[ 78 | zip( 79 | *itertools.chain( 80 | *map( 81 | lambda tri: itertools.permutations(tri, 2), 82 | self.tris 83 | ) 84 | ) 85 | ) 86 | ] = 1 87 | 88 | self.cellRotations = np.zeros((self.n, 3, 3)) 89 | 90 | self.weightMatrix = np.zeros((self.n, self.n), dtype=np.float) 91 | self.weightSum = np.zeros((self.n, self.n), dtype=np.float) 92 | 93 | for vertID in range(self.n): 94 | neighbours = self.neighboursOf(vertID) 95 | for neighbourID in neighbours: 96 | self.assignWeightForPair(vertID, neighbourID) 97 | 98 | def update(self, fixedIDs, handleIDs, deformationMatrices): 99 | deformationMatrices = map(np.matrix, deformationMatrices) 100 | self.deformationVerts = [] 101 | for i in range(self.n): 102 | if i in handleIDs: 103 | deformedVector = np.append(self.verts[i], 1) 104 | deformedVector = deformedVector.dot(deformationMatrices[handleIDs.index(i)]) 105 | deformedVector = np.delete(deformedVector, 3).flatten() 106 | deformedVector = np.squeeze(np.asarray(deformedVector)) 107 | self.deformationVerts.append((i, deformedVector)) 108 | elif i in fixedIDs: 109 | self.deformationVerts.append((i, self.verts[i])) 110 | 111 | # extended laplacian matrix 112 | deformationVertsNum = len(self.deformationVerts) 113 | self.laplacianMatrix = np.zeros([self.n + deformationVertsNum] * 2, dtype=np.float32) 114 | self.laplacianMatrix[:self.n, :self.n] = csgraph.laplacian(self.weightMatrix) 115 | for i in range(deformationVertsNum): 116 | vertID = self.deformationVerts[i][0] 117 | ni = i + self.n 118 | self.laplacianMatrix[ni, vertID] = 1 119 | self.laplacianMatrix[vertID, ni] = 1 120 | 121 | # precompute PiArray 122 | self.PiArray = [] 123 | for i in range(self.n): 124 | vertI = self.verts[i] 125 | neighbourIDs = self.neighboursOf(i) 126 | neighboursNum = len(neighbourIDs) 127 | 128 | Pi = np.zeros((3, neighboursNum)) 129 | 130 | for ni in range(neighboursNum): 131 | nID = neighbourIDs[ni] 132 | 133 | vertJ = self.verts[nID] 134 | Pi[:, ni] = (vertI - vertJ) 135 | self.PiArray.append(Pi) 136 | 137 | def calculateCellRotations(self): 138 | for vertID in range(self.n): 139 | rotation = self.calculateRotationMatrixForCell(vertID) 140 | self.cellRotations[vertID] = rotation 141 | 142 | def applyCellRotations(self): 143 | for i in range(self.n): 144 | self.bArray[i] = np.zeros((1, 3)) 145 | neighbours = self.neighboursOf(i) 146 | for j in neighbours: 147 | wij = self.weightMatrix[i, j] / 2.0 148 | rij = self.cellRotations[i] + self.cellRotations[j] 149 | pij = self.verts[i] - self.verts[j] 150 | self.bArray[i] += (wij * rij.dot(pij)) 151 | 152 | self.vertsPrime = np.linalg.solve(self.laplacianMatrix, self.bArray)[:self.n] 153 | 154 | def calculateRotationMatrixForCell(self, vertID): 155 | covarianceMatrix = self.calculateConvarianceMatrixForCell(vertID) 156 | 157 | U, s, VTranspose = np.linalg.svd(covarianceMatrix) 158 | 159 | rotation = VTranspose.T.dot(U.T) 160 | if np.linalg.det(rotation) <= 0: 161 | U[:0] *= -1 162 | rotation = VTranspose.T.dot(U.T) 163 | return rotation 164 | 165 | def calculateConvarianceMatrixForCell(self, vertID): 166 | vertIPrime = self.vertsPrime[vertID] 167 | 168 | neighbourIDs = self.neighboursOf(vertID) 169 | neighboursNum = len(neighbourIDs) 170 | 171 | Di = np.zeros((neighboursNum, neighboursNum)) 172 | 173 | Pi = self.PiArray[vertID] 174 | PiPrime = np.zeros((3, neighboursNum)) 175 | 176 | for ni in range(neighboursNum): 177 | nID = neighbourIDs[ni] 178 | 179 | Di[ni, ni] = self.weightMatrix[vertID, nID] 180 | 181 | vertJPrime = self.vertsPrime[nID] 182 | PiPrime[:, ni] = (vertIPrime - vertJPrime) 183 | 184 | PiPrime = PiPrime.T 185 | 186 | return Pi.dot(Di).dot(PiPrime) 187 | 188 | def apply(self, iterations): 189 | deformationVertsNum = len(self.deformationVerts) 190 | 191 | self.bArray = np.zeros((self.n + deformationVertsNum, 3)) 192 | for i in range(deformationVertsNum): 193 | self.bArray[self.n + i] = self.deformationVerts[i][1] 194 | 195 | for t in range(iterations): 196 | self.calculateCellRotations() 197 | self.applyCellRotations() 198 | 199 | 200 | class ArapDeformerNode(OpenMayaMPx.MPxDeformerNode): 201 | # Amount to push the vertices by 202 | attrFixedIDsStr = OpenMaya.MObject() 203 | attrHandleIDsStr = OpenMaya.MObject() 204 | attrInputMatrices = OpenMaya.MObject() 205 | attrIterations = OpenMaya.MObject() 206 | 207 | def __init__(self): 208 | OpenMayaMPx.MPxDeformerNode.__init__(self) 209 | self.once = True 210 | 211 | def postConstructor(self): 212 | selfObj = self.thisMObject() 213 | selfFn = OpenMaya.MFnDependencyNode(selfObj) 214 | self.inputMatricesPlug = selfFn.findPlug('inputMatrices') 215 | self.fixedIDsStrPlug = selfFn.findPlug('fixedIds') 216 | self.handleIDsStrPlug = selfFn.findPlug('handleIds') 217 | 218 | def deform(self, data, geomIter, localToWorldMatrix, geomIDx): 219 | # envelope = data.inputValue(kEnvelope).asFloat() 220 | if self.once: 221 | inputGeomObj = self.getInputGeom(data, geomIDx) 222 | counts = OpenMaya.MIntArray() 223 | tris = OpenMaya.MIntArray() 224 | verts = OpenMaya.MPointArray() 225 | mesh = OpenMaya.MFnMesh(inputGeomObj) 226 | mesh.getTriangles(counts, tris) 227 | mesh.getPoints(verts, OpenMaya.MSpace.kObject) 228 | 229 | self.arapd = ARAP() 230 | self.arapd.input( 231 | verts=[[verts[i].x, verts[i].y, verts[i].z] for i in range(verts.length())], 232 | tris=np.array(tris).reshape(-1, 3) 233 | ) 234 | self.once = False 235 | 236 | dataStr = self.fixedIDsStrPlug.asString() 237 | try: 238 | fixedIDs = json.loads(dataStr) 239 | except Exception: 240 | fixedIDs = None 241 | 242 | dataStr = self.handleIDsStrPlug.asString() 243 | try: 244 | handleIDs = json.loads(dataStr) 245 | except Exception: 246 | handleIDs = None 247 | 248 | inputMatrices = [] 249 | for i in xrange(self.inputMatricesPlug.numElements()): 250 | matPlug = self.inputMatricesPlug.elementByPhysicalIndex(i) 251 | if matPlug.isConnected(): 252 | matrixObj = matPlug.asMObject() 253 | matrixData = OpenMaya.MFnMatrixData(matrixObj) 254 | matrix = matrixData.matrix() 255 | inputMatrices.append( 256 | [ 257 | [matrix(0, j) for j in range(4)], 258 | [matrix(1, j) for j in range(4)], 259 | [matrix(2, j) for j in range(4)], 260 | [matrix(3, j) for j in range(4)], 261 | ] 262 | ) 263 | 264 | if inputMatrices and handleIDs and len(handleIDs) == len(inputMatrices): 265 | if not fixedIDs: 266 | fixedIDs = [] 267 | self.arapd.update( 268 | fixedIDs=fixedIDs, 269 | handleIDs=handleIDs, 270 | deformationMatrices=inputMatrices, 271 | ) 272 | iterationHandle = data.inputValue(ArapDeformerNode.attrIterations) 273 | iterations = iterationHandle.asInt() 274 | self.arapd.apply(iterations) 275 | 276 | while not geomIter.isDone(): 277 | idx = geomIter.index() 278 | geomIter.setPosition(OpenMaya.MPoint(*self.arapd.vertsPrime[idx])) 279 | geomIter.next() 280 | 281 | def getInputGeom(self, data, geomIDx): 282 | inputHandle = data.outputArrayValue(kInput) 283 | inputHandle.jumpToElement(geomIDx) 284 | inputGeomObj = inputHandle.outputValue().child(kInputGeom).asMesh() 285 | return inputGeomObj 286 | 287 | 288 | def nodeCreator(): 289 | return OpenMayaMPx.asMPxPtr(ArapDeformerNode()) 290 | 291 | 292 | def nodeInitializer(): 293 | numAttrFn = OpenMaya.MFnNumericAttribute() 294 | 295 | # Setup attributes 296 | ArapDeformerNode.attrIterations = numAttrFn.create( 297 | 'iterations', 298 | 'iterations', 299 | OpenMaya.MFnNumericData.kInt, 300 | 1 301 | ) 302 | numAttrFn.setMin(1) 303 | numAttrFn.setMax(100) 304 | numAttrFn.setChannelBox(True) 305 | ArapDeformerNode.addAttribute(ArapDeformerNode.attrIterations) 306 | 307 | matrixAttrFn = OpenMaya.MFnMatrixAttribute() 308 | typedAttrFn = OpenMaya.MFnTypedAttribute() 309 | 310 | strFn = OpenMaya.MFnStringData() 311 | defaultValue = strFn.create('null') 312 | ArapDeformerNode.attrFixedIDsStr = typedAttrFn.create('fixedIds', 'fixedIds', OpenMaya.MFnData.kString, defaultValue) 313 | typedAttrFn.setReadable(True) 314 | typedAttrFn.setWritable(True) 315 | typedAttrFn.setStorable(True) 316 | ArapDeformerNode.addAttribute(ArapDeformerNode.attrFixedIDsStr) 317 | 318 | strFn = OpenMaya.MFnStringData() 319 | defaultValue = strFn.create('null') 320 | ArapDeformerNode.attrHandleIDsStr = typedAttrFn.create('handleIds', 'handleIds', OpenMaya.MFnData.kString, defaultValue) 321 | typedAttrFn.setReadable(True) 322 | typedAttrFn.setWritable(True) 323 | typedAttrFn.setStorable(True) 324 | ArapDeformerNode.addAttribute(ArapDeformerNode.attrHandleIDsStr) 325 | 326 | ArapDeformerNode.attrInputMatrices = matrixAttrFn.create('inputMatrices', 'inputMatrices') 327 | matrixAttrFn.setArray(True) 328 | matrixAttrFn.setReadable(True) 329 | matrixAttrFn.setWritable(True) 330 | matrixAttrFn.setStorable(True) 331 | ArapDeformerNode.addAttribute(ArapDeformerNode.attrInputMatrices) 332 | 333 | # Link inputs that change the output of the mesh 334 | ArapDeformerNode.attributeAffects( 335 | ArapDeformerNode.attrIterations, 336 | kOutputGeom 337 | ) 338 | ArapDeformerNode.attributeAffects( 339 | ArapDeformerNode.attrFixedIDsStr, 340 | kOutputGeom 341 | ) 342 | ArapDeformerNode.attributeAffects( 343 | ArapDeformerNode.attrHandleIDsStr, 344 | kOutputGeom 345 | ) 346 | ArapDeformerNode.attributeAffects( 347 | ArapDeformerNode.attrInputMatrices, 348 | kOutputGeom 349 | ) 350 | 351 | 352 | def initializePlugin(mobject): 353 | mplugin = OpenMayaMPx.MFnPlugin(mobject) 354 | try: 355 | mplugin.registerNode( 356 | pluginNodeName, 357 | pluginNodeID, 358 | nodeCreator, 359 | nodeInitializer, 360 | OpenMayaMPx.MPxNode.kDeformerNode 361 | ) 362 | except Exception: 363 | sys.stderr.write('Failed to register node: ' + pluginNodeName) 364 | raise 365 | 366 | 367 | def uninitializePlugin(mobject): 368 | mplugin = OpenMayaMPx.MFnPlugin(mobject) 369 | try: 370 | mplugin.deregisterNode(pluginNodeID) 371 | except Exception: 372 | sys.stderr.write('Failed to deregister node: ' + pluginNodeName) 373 | raise 374 | --------------------------------------------------------------------------------