""" Retarget your blendshapes between meshes with the same topology. .. figure:: https://github.com/robertjoosten/rjRetargetBlendshape/raw/master/README.png :align: center `Link to Video `_ Installation ============ Copy the **rjRetargetBlendshape** folder to your Maya scripts directory :: C:/Users//Documents/maya/scripts Usage ===== Command line :: import rjRetargetBlendshape rjRetargetBlendshape.convert( source, blendshape, target, scale=True, rotate=True, smooth=0, smoothIterations=0, space=OpenMaya.MSpace.kObject, ) Display UI :: import rjRetargetBlendshape.ui rjRetargetBlendshape.ui.show() Note ==== Retarget your blendshapes between meshes with the same topology. There are a few options that can be helpful to achieve the desired results. * Scaling your delta depending on the size difference between the source and the target vertex. * Rotating the delta depending on the normal difference between the source and the target vertex. * Smoothing based on the vertex size between the retarget mesh and the blendshape mesh. Code ==== """ import math from maya import OpenMaya, cmds __author__ = "Robert Joosten" __version__ = "0.8.2" __email__ = "rwm.joosten@gmail.com" # ---------------------------------------------------------------------------- def convert( source, blendshape, target, scale=True, rotate=True, smooth=0, smoothIterations=2, space=OpenMaya.MSpace.kObject ): """ Create a new target based on the difference between the source and blendshape. The target mesh is duplicated and the difference between source and target is transfered onto the duplicated mesh. When transfering both scale and rotation of the delta vectors can be taken into account. Once the data is transfered an Laplacian smoothing algorithm can be applied onto the newly created target to create a more desired result. :param str source: :param str blendshape: :param str target: :param bool scale: Take scale between delta vectors into account :param bool rotate: Take rotation between normal vectors into account :param float smooth: Smoothing strength :param int smoothIterations: Times the smooth algorithm repeats :param OpenMaya.MSpace space: :raises RuntimeError: If the vertex count doesn't match :return: Name of transfered blendshape mesh :rtype: str """ # convert objects to dag dags = [] meshes = [] for i, name in enumerate([source, target, blendshape]): dags.append(asMDagPath(asMObject(name))) meshes.append(asMFnMesh(dags[i])) sourceD, targetD, blendshapeD = dags sourceM, targetM, blendshapeM = meshes # compare vertex count count = set([m.numVertices() for m in meshes]) if len(count) != 1: raise RuntimeError( "Input geometry doesn't have matching vertex counts!" ) # duplicate target to manipulate mesh targetB = getBasename(target) blendshapeB = getBasename(blendshape) target = cmds.duplicate( target, rr=True, n="{0}_{1}".format(targetB, blendshapeB) )[0] # parent duplicated target if cmds.listRelatives(target, p=True): target = cmds.parent(target, world=True)[0] targetD = asMDagPath(asMObject(target)) targetM = asMFnMesh(targetD) # iterate vertices count = next(iter(count)) positions = OpenMaya.MPointArray() # variables dags = [sourceD, targetD, blendshapeD] points = [OpenMaya.MPoint(), OpenMaya.MPoint(), OpenMaya.MPoint()] normals = [OpenMaya.MVector(), OpenMaya.MVector()] length = [0,0,0] lengths = [[],[],[]] # get new positions for i in range(count): # initialize component component = asComponent(i) # loop meshes for j, dag in enumerate(dags): # get points meshes[j].getPoint(i, points[j], space) # get length l = getAverageLength(dag, component, space) length[j] = l lengths[j].append(l) # variables sourceP, targetP, blendshapeP = points sourceL, targetL, blendshapeL = length # difference vector vector = blendshapeP - sourceP # handle scale if scale: scaleFactor = targetL/sourceL if sourceL and targetL else 1 # scale vector vector = vector * scaleFactor # handle normal rotation if rotate: # get normals for j, mesh in enumerate(meshes[:-1]): mesh.getVertexNormal(i, True, normals[j], space) # get quaternion between normals sourceN, targetN = normals quaternion = sourceN.rotateTo(targetN) # rotate vector vector = vector.rotateBy(quaternion) positions.append(targetP + vector) # set all points targetM.setPoints(positions, space) # loop over smooth iterations for _ in range(smoothIterations): # loop over vertices for i, sourceL, targetL, blendshapeL in zip(range(count), *lengths): # smooth vertex setLaplacianSmooth( targetD, i, space, sourceL, targetL, blendshapeL, smooth ) return target # ---------------------------------------------------------------------------- def setLaplacianSmooth( dag, index, space, sourceL, targetL, blendshapeL, smooth ): """ Laplacian smoothing algorithm, where the average of the neighbouring points are used to smooth the target vertices, based on the factor between the average length of both the source and the target mesh. :param OpenMaya.MDagPath dag: :param int index: Component index :param OpenMaya.MSpace space: :param float sourceL: :param float targetL: :param float blendshapeL: :param float smooth: """ # calculate factor component = asComponent(index) avgL = getAverageLength(dag, component, space) targetF = blendshapeL/sourceL if sourceL and targetL else 1 blendshapeF = avgL/targetL if sourceL and blendshapeL else 1 factor = abs((1-targetF/blendshapeF)*smooth) factor = max(min(factor, 1), 0) # ignore if there is not smooth factor if not factor: return # get average position component = asComponent(index) vtx = OpenMaya.MItMeshVertex(dag, component) origP, avgP = getAveragePosition(dag, component, space) # get new position avgP = avgP * factor origP = origP * (1-factor) newP = avgP + OpenMaya.MVector(origP) # set new position vtx.setPosition(newP, space) def getAveragePosition(dag, component, space): """ Get average position of connected vertices. :param OpenMaya.MDagPath dag: :param OpenMaya.MFnSingleIndexedComponent component: :param OpenMaya.MSpace space: :return: Average length of the connected edges :rtype: OpenMaya.MPoint """ averagePos = OpenMaya.MPoint() # get connected vertices connected = OpenMaya.MIntArray() iterate = OpenMaya.MItMeshVertex(dag, component) iterate.getConnectedVertices(connected) # get original position originalPos = iterate.position(space) # ignore if no vertices are connected if not connected.length(): return averagePos # get average component = asComponent(connected) iterate = OpenMaya.MItMeshVertex(dag, component) while not iterate.isDone(): averagePos += OpenMaya.MVector(iterate.position(space)) iterate.next() averagePos = averagePos/connected.length() return originalPos, averagePos def getAverageLength(dag, component, space): """ Get average length of connected edges. :param OpenMaya.MDagPath dag: :param OpenMaya.MFnSingleIndexedComponent component: :param OpenMaya.MSpace space: :return: Average length of the connected edges :rtype: float """ total = 0 lengthUtil = OpenMaya.MScriptUtil() lengthPtr = lengthUtil.asDoublePtr() # get connected edges connected = OpenMaya.MIntArray() iterate = OpenMaya.MItMeshVertex(dag, component) iterate.getConnectedEdges(connected) # ignore if no edges are connected if not connected.length(): return 0 # get average component = asComponent(connected, OpenMaya.MFn.kMeshEdgeComponent) iterate = OpenMaya.MItMeshEdge(dag, component) while not iterate.isDone(): iterate.getLength(lengthPtr, space) total += lengthUtil.getDouble(lengthPtr) iterate.next() return total/connected.length() # ---------------------------------------------------------------------------- def getBasename(name): """ Strip the parent and namespace data of the provided string. :param str name: :return: Base name of parsed object :rtype: str """ return name.split("|")[-1].split(":")[-1] # ---------------------------------------------------------------------------- def asMIntArray(index): """ index -> OpenMaya.MIntArray :param int/OpenMaya.MIntArray index: indices :return: Array of indices :rtype: OpenMaya.MIntArray """ if type(index) != OpenMaya.MIntArray: array = OpenMaya.MIntArray() array.append(index) return array return index def asComponent(index, t=OpenMaya.MFn.kMeshVertComponent): """ index -> OpenMaya.MFn.kComponent Based on the input type it will create a component type for this tool the following components are being used. * OpenMaya.MFn.kMeshVertComponent * OpenMaya.MFn.kMeshEdgeComponent :param int/OpenMaya.MIntArray index: indices to create component for :param OpenMaya.MFn.kComponent t: can be all of OpenMaya component types. :return: Initialized components :rtype: OpenMaya.MFnSingleIndexedComponent """ # convert input to an MIntArray if it not already is one array = asMIntArray(index) # initialize component component = OpenMaya.MFnSingleIndexedComponent().create(t) OpenMaya.MFnSingleIndexedComponent(component).addElements(array) return component # ---------------------------------------------------------------------------- def asMObject(path): """ str -> OpenMaya.MObject :param str path: Path to Maya object :rtype: OpenMaya.MObject """ selectionList = OpenMaya.MSelectionList() selectionList.add(path) obj = OpenMaya.MObject() selectionList.getDependNode(0, obj) return obj def asMDagPath(obj): """ OpenMaya.MObject -> OpenMaya.MDagPath :param OpenMaya.MObject obj: :rtype: OpenMaya.MDagPath """ return OpenMaya.MDagPath.getAPathTo(obj) def asMFnMesh(dag): """ OpenMaya.MDagPath -> OpenMaya.MfnMesh :param OpenMaya.MDagPath dag: :rtype: OpenMaya.MfnMesh """ return OpenMaya.MFnMesh(dag)