Files
MetaBox/Scripts/Modeling/Edit/LDMT/faceTransfer/__init__.py
2025-04-17 04:52:48 +08:00

413 lines
11 KiB
Python

"""
Retarget your blendshapes between meshes with the same topology.
.. figure:: https://github.com/robertjoosten/rjRetargetBlendshape/raw/master/README.png
:align: center
`Link to Video <https://vimeo.com/170360738>`_
Installation
============
Copy the **rjRetargetBlendshape** folder to your Maya scripts directory
::
C:/Users/<USER>/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)