This commit is contained in:
2025-11-24 00:15:32 +08:00
parent 9f7667a475
commit 0eeeb54784
256 changed files with 49494 additions and 0 deletions

View File

@@ -0,0 +1,918 @@
# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version. This library is distributed in the
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
"""
# mirrortable.py
import mutils
# Example 1:
# Create a MirrorTable instance from the given objects
mt = mutils.MirrorTable.fromObjects(objects, "_l_", "_r_", MirrorPlane.YZ)
# Example 2:
# Create a MirrorTable instance from the selected objects
objects = maya.cmds.ls(selection=True)
mt = mutils.MirrorTable.fromObjects(objects, "_l_", "_r_", MirrorPlane.YZ)
# Example 3:
# Save the MirrorTable to the given JSON path
path = "/tmp/mirrortable.json"
mt.save(path)
# Example 4:
# Create a MirrorTable instance from the given JSON path
path = "/tmp/mirrortable.json"
mt = mutils.MirrorTable.fromPath(path)
# Example 5:
# Mirror all the objects from file
mt.load()
# Example 6:
# Mirror only the selected objects
objects = maya.cmds.ls(selection=True) or []
mt.load(objects=objects)
# Example 7:
# Mirror all objects from file to the given namespaces
mt.load(namespaces=["character1", "character2"])
# Example 8:
# Mirror only the given objects
mt.load(objects=["character1:Hand_L", "character1:Finger_L"])
# Example 9:
# Mirror all objects from left to right
mt.load(option=mutils.MirrorOption.LeftToRight)
# Example 10:
# Mirror all objects from right to left
mt.load(option=mutils.MirrorOption.RightToLeft)
# Example 11:
# Mirror only the current pose
mt.load(animation=False)
"""
import re
import mutils
import logging
import traceback
from studiovendor import six
try:
import maya.cmds
except ImportError:
traceback.print_exc()
__all__ = [
"MirrorTable",
"MirrorPlane",
"MirrorOption",
"saveMirrorTable",
]
logger = logging.getLogger(__name__)
RE_LEFT_SIDE = "Left|left|Lf|lt_|_lt|lf_|_lf|_l_|_L|L_|:l_|^l_|_l$|:L|^L"
RE_RIGHT_SIDE = "Right|right|Rt|rt_|_rt|_r_|_R|R_|:r_|^r_|_r$|:R|^R"
VALID_NODE_TYPES = ["joint", "transform"]
class MirrorPlane:
YZ = [-1, 1, 1]
XZ = [1, -1, 1]
XY = [1, 1, -1]
class MirrorOption:
Swap = 0
LeftToRight = 1
RightToLeft = 2
class KeysOption:
All = "All Keys"
SelectedRange = "Selected Range"
def saveMirrorTable(path, objects, metadata=None, *args, **kwargs):
"""
Convenience function for saving a mirror table to the given disc location.
:type path: str
:type objects: list[str]
:type metadata: dict or None
:type args: list
:type kwargs: dict
:rtype: MirrorTable
"""
mirrorTable = MirrorTable.fromObjects(objects, *args, **kwargs)
if metadata:
mirrorTable.updateMetadata(metadata)
mirrorTable.save(path)
return mirrorTable
class MirrorTable(mutils.TransferObject):
@classmethod
@mutils.timing
@mutils.unifyUndo
@mutils.showWaitCursor
@mutils.restoreSelection
def fromObjects(
cls,
objects,
leftSide=None,
rightSide=None,
mirrorPlane=None
):
"""
Create a new Mirror Table instance from the given Maya object/controls.
:type objects: list[str]
:type leftSide: str
:type rightSide: str
:type mirrorPlane: mirrortable.MirrorPlane or str
:rtype: MirrorTable
"""
mirrorPlane = mirrorPlane or MirrorPlane.YZ
if isinstance(mirrorPlane, six.string_types):
if mirrorPlane.lower() == "yz":
mirrorPlane = MirrorPlane.YZ
elif mirrorPlane.lower() == "xz":
mirrorPlane = MirrorPlane.XZ
elif mirrorPlane.lower() == "xy":
mirrorPlane = MirrorPlane.XY
mirrorTable = cls()
mirrorTable.setMetadata("left", leftSide)
mirrorTable.setMetadata("right", rightSide)
mirrorTable.setMetadata("mirrorPlane", mirrorPlane)
for obj in objects:
nodeType = maya.cmds.nodeType(obj)
if nodeType in VALID_NODE_TYPES:
mirrorTable.add(obj)
else:
msg = "Node of type {0} is not supported. Node name: {1}"
msg = msg.format(nodeType, obj)
logger.info(msg)
return mirrorTable
@staticmethod
def findLeftSide(objects):
"""
Return the left side naming convention for the given objects
:type objects: list[str]
:rtype: str
"""
return MirrorTable.findSide(objects, RE_LEFT_SIDE)
@staticmethod
def findRightSide(objects):
"""
Return the right side naming convention for the given objects
:type objects: list[str]
:rtype: str
"""
return MirrorTable.findSide(objects, RE_RIGHT_SIDE)
@classmethod
def findSide(cls, objects, reSides):
"""
Return the naming convention for the given object names.
:type objects: list[str]
:type reSides: str or list[str]
:rtype: str
"""
if isinstance(reSides, six.string_types):
reSides = reSides.split("|")
# Compile the list of regular expressions into a re.object
reSides = [re.compile(side) for side in reSides]
for obj in objects:
obj = obj.split("|")[-1]
obj = obj.split(":")[-1]
for reSide in reSides:
m = reSide.search(obj)
if m:
side = m.group()
if obj.startswith(side):
side += "*"
if obj.endswith(side):
side = "*" + side
return side
return ""
@staticmethod
def matchSide(name, side):
"""
Return True if the name contains the given side.
:type name: str
:type side: str
:rtype: bool
"""
if side:
return MirrorTable.rreplace(name, side, "X") != name
return False
@staticmethod
def rreplace(name, old, new):
"""
convenience method.
Example:
print MirrorTable.rreplace("CHR1:RIG:RhandCON", ":R", ":L")
"CHR1:RIG:LhandCON"
:type name: str
:type old: str
:type new: str
:rtype: str
"""
names = []
for n in name.split("|"):
namespaces = ''
if ':' in n:
namespaces, n = n.rsplit(':', 1)
other = mutils.mirrortable.MirrorTable.replace(n, old, new)
if namespaces:
other = ':'.join([namespaces, other])
names.append(other)
return "|".join(names)
@staticmethod
def replace(name, old, new):
"""
A static method that is called by self.mirrorObject.
:type name: str
:type old: str
:type new: str
:rtype: str
"""
# Support for replacing a prefix naming convention.
if old.endswith("*") or new.endswith("*"):
name = MirrorTable.replacePrefix(name, old, new)
# Support for replacing a suffix naming convention.
elif old.startswith("*") or new.startswith("*"):
name = MirrorTable.replaceSuffix(name, old, new)
# Support for all other naming conventions.
else:
name = name.replace(old, new)
return name
@staticmethod
def replacePrefix(name, old, new):
"""
Replace the given old prefix with the given new prefix.
It should also support long names.
# Example:
self.replacePrefix("R_footRoll", "R", "L")
# Result: "L_footRoll" and not "L_footLoll".
self.replacePrefix("Grp|Ch1:R_footExtra|Ch1:R_footRoll", "R_", "L_")
# Result: "Grp|Ch1:L_footExtra|Ch1:L_footRoll"
:type name: str
:type old: str
:type new: str
:rtype: str
"""
old = old.replace("*", "")
new = new.replace("*", "")
if name.startswith(old):
name = name.replace(old, new, 1)
return name
@staticmethod
def replaceSuffix(name, old, new):
"""
Replace the given old suffix with the given new suffix.
It should also support long names.
# Example:
self.replaceSuffix("footRoll_R", "R", "L")
# Result: "footRoll_L" and not "footLoll_L".
self.replaceSuffix("Grp|Ch1:footExtra_R|Ch1:footRoll_R", "_R", "_L")
# Result: "Grp|Ch1:footExtra_L|Ch1:footRoll_L"
:type name: str
:type old: str
:type new: str
:rtype: str
"""
old = old.replace("*", "")
new = new.replace("*", "")
if name.endswith(old):
name = name[:-len(old)] + new
return name
def mirrorObject(self, obj):
"""
Return the other/opposite side for the given name.
Example:
print self.mirrorObject("FKSholder_L")
# FKShoulder_R
:type obj: str
:rtype: str or None
"""
leftSide = self.leftSide()
rightSide = self.rightSide()
return self._mirrorObject(obj, leftSide, rightSide)
@staticmethod
def _mirrorObject(obj, leftSide, rightSide):
"""
A static method that is called by self.mirrorObject.
:type obj: str
:rtype: str or None
"""
dstName = MirrorTable.rreplace(obj, leftSide, rightSide)
if obj == dstName:
dstName = MirrorTable.rreplace(obj, rightSide, leftSide)
if dstName != obj:
return dstName
# Return None as the given name is probably a center control
# and doesn't have an opposite side.
return None
@staticmethod
def animCurve(obj, attr):
"""
:type obj: str
:type attr: str
:rtype: str
"""
fullname = obj + "." + attr
connections = maya.cmds.listConnections(fullname, d=False, s=True)
if connections:
return connections[0]
return None
@staticmethod
def scaleKey(obj, attr, time=None):
"""
:type obj: str
:type attr: str
"""
curve = MirrorTable.animCurve(obj, attr)
if curve:
maya.cmds.selectKey(curve)
if time:
maya.cmds.scaleKey(time=time, iub=False, ts=1, fs=1, vs=-1, vp=0, animation="keys")
else:
maya.cmds.scaleKey(iub=False, ts=1, fs=1, vs=-1, vp=0, animation="keys")
@staticmethod
def formatValue(attr, value, mirrorAxis):
"""
:type attr: str
:type value: float
:type mirrorAxis: list[int]
:rtype: float
"""
if MirrorTable.isAttrMirrored(attr, mirrorAxis):
return value * -1
return value
@staticmethod
def maxIndex(numbers):
"""
Finds the largest number in a list
:type numbers: list[float] or list[str]
:rtype: int
"""
m = 0
result = 0
for i in numbers:
v = abs(float(i))
if v > m:
m = v
result = numbers.index(i)
return result
@staticmethod
def axisWorldPosition(obj, axis):
"""
:type obj: str
:type axis: list[int]
:rtype: list[float]
"""
transform1 = maya.cmds.createNode("transform", name="transform1")
try:
transform1, = maya.cmds.parent(transform1, obj, r=True)
maya.cmds.setAttr(transform1 + ".t", *axis)
maya.cmds.setAttr(transform1 + ".r", 0, 0, 0)
maya.cmds.setAttr(transform1 + ".s", 1, 1, 1)
return maya.cmds.xform(transform1, q=True, ws=True, piv=True)
finally:
maya.cmds.delete(transform1)
@staticmethod
def isAttrMirrored(attr, mirrorAxis):
"""
:type attr: str
:type mirrorAxis: list[int]
:rtype: float
"""
if mirrorAxis == [-1, 1, 1]:
if attr == "translateX" or attr == "rotateY" or attr == "rotateZ":
return True
elif mirrorAxis == [1, -1, 1]:
if attr == "translateY" or attr == "rotateX" or attr == "rotateZ":
return True
elif mirrorAxis == [1, 1, -1]:
if attr == "translateZ" or attr == "rotateX" or attr == "rotateY":
return True
elif mirrorAxis == [-1, -1, -1]:
if attr == "translateX" or attr == "translateY" or attr == "translateZ":
return True
return False
@staticmethod
def isAxisMirrored(srcObj, dstObj, axis, mirrorPlane):
"""
:type srcObj: str
:type dstObj: str
:type axis: list[int]
:type mirrorPlane: list[int]
:rtype: int
"""
old1 = maya.cmds.xform(srcObj, q=True, ws=True, piv=True)
old2 = maya.cmds.xform(dstObj, q=True, ws=True, piv=True)
new1 = MirrorTable.axisWorldPosition(srcObj, axis)
new2 = MirrorTable.axisWorldPosition(dstObj, axis)
mp = mirrorPlane
v1 = mp[0]*(new1[0]-old1[0]), mp[1]*(new1[1]-old1[1]), mp[2]*(new1[2]-old1[2])
v2 = new2[0]-old2[0], new2[1]-old2[1], new2[2]-old2[2]
d = sum(p*q for p, q in zip(v1, v2))
if d >= 0.0:
return False
return True
@staticmethod
def _calculateMirrorAxis(obj, mirrorPlane):
"""
:type obj: str
:rtype: list[int]
"""
result = [1, 1, 1]
transform0 = maya.cmds.createNode("transform", name="transform0")
try:
transform0, = maya.cmds.parent(transform0, obj, r=True)
transform0, = maya.cmds.parent(transform0, w=True)
maya.cmds.setAttr(transform0 + ".t", 0, 0, 0)
t1 = MirrorTable.axisWorldPosition(transform0, [1, 0, 0])
t2 = MirrorTable.axisWorldPosition(transform0, [0, 1, 0])
t3 = MirrorTable.axisWorldPosition(transform0, [0, 0, 1])
t1 = "%.3f" % t1[0], "%.3f" % t1[1], "%.3f" % t1[2]
t2 = "%.3f" % t2[0], "%.3f" % t2[1], "%.3f" % t2[2]
t3 = "%.3f" % t3[0], "%.3f" % t3[1], "%.3f" % t3[2]
if mirrorPlane == MirrorPlane.YZ: # [-1, 1, 1]:
x = [t1[0], t2[0], t3[0]]
i = MirrorTable.maxIndex(x)
result[i] = -1
if mirrorPlane == MirrorPlane.XZ: # [1, -1, 1]:
y = [t1[1], t2[1], t3[1]]
i = MirrorTable.maxIndex(y)
result[i] = -1
if mirrorPlane == MirrorPlane.XY: # [1, 1, -1]:
z = [t1[2], t2[2], t3[2]]
i = MirrorTable.maxIndex(z)
result[i] = -1
finally:
maya.cmds.delete(transform0)
return result
def select(self, objects=None, namespaces=None, **kwargs):
"""
Select the objects contained in file.
:type objects: list[str] or None
:type namespaces: list[str] or None
:rtype: None
"""
selectionSet = mutils.SelectionSet.fromPath(self.path())
selectionSet.load(objects=objects, namespaces=namespaces, **kwargs)
def leftSide(self):
"""
:rtype: str | None
"""
return self.metadata().get("left")
def rightSide(self):
"""
:rtype: str | None
"""
return self.metadata().get("right")
def mirrorPlane(self):
"""
:rtype: lsit[int] | None
"""
return self.metadata().get("mirrorPlane")
def mirrorAxis(self, name):
"""
:rtype: list[int]
"""
return self.objects()[name]["mirrorAxis"]
def leftCount(self, objects=None):
"""
:type objects: list[str]
:rtype: int
"""
if objects is None:
objects = self.objects()
return len([obj for obj in objects if self.isLeftSide(obj)])
def rightCount(self, objects=None):
"""
:type objects: list[str]
:rtype: int
"""
if objects is None:
objects = self.objects()
return len([obj for obj in objects if self.isRightSide(obj)])
def createObjectData(self, name):
"""
:type name:
:rtype:
"""
result = {"mirrorAxis": self.calculateMirrorAxis(name)}
return result
def matchObjects(
self,
objects=None,
namespaces=None,
):
"""
:type objects: list[str]
:type namespaces: list[str]
:rtype: list[str]
"""
srcObjects = self.objects().keys()
matches = mutils.matchNames(
srcObjects=srcObjects,
dstObjects=objects,
dstNamespaces=namespaces,
)
for srcNode, dstNode in matches:
dstName = dstNode.name()
mirrorAxis = self.mirrorAxis(srcNode.name())
yield srcNode.name(), dstName, mirrorAxis
def rightToLeft(self):
""""""
self.load(option=MirrorOption.RightToLeft)
def leftToRight(self):
""""""
self.load(option=MirrorOption.LeftToRight)
@mutils.timing
@mutils.unifyUndo
@mutils.showWaitCursor
@mutils.restoreSelection
def load(
self,
objects=None,
namespaces=None,
option=None,
keysOption=None,
time=None,
):
"""
Load the mirror table for the given objects.
:type objects: list[str]
:type namespaces: list[str]
:type option: mirrorOptions
:type keysOption: None or KeysOption.SelectedRange
:type time: None or list[int]
"""
if option and not isinstance(option, int):
if option.lower() == "swap":
option = 0
elif option.lower() == "left to right":
option = 1
elif option.lower() == "right to left":
option = 2
else:
raise ValueError('Invalid load option=' + str(option))
self.validate(namespaces=namespaces)
results = {}
animation = True
foundObject = False
srcObjects = self.objects().keys()
if option is None:
option = MirrorOption.Swap
if keysOption == KeysOption.All:
time = None
elif keysOption == KeysOption.SelectedRange:
time = mutils.selectedFrameRange()
# Check to make sure that the given time is not a single frame
if time and time[0] == time[1]:
time = None
animation = False
matches = mutils.matchNames(
srcObjects=srcObjects,
dstObjects=objects,
dstNamespaces=namespaces,
)
for srcNode, dstNode in matches:
dstObj = dstNode.name()
dstObj2 = self.mirrorObject(dstObj) or dstObj
if dstObj2 not in results:
results[dstObj] = dstObj2
mirrorAxis = self.mirrorAxis(srcNode.name())
dstObjExists = maya.cmds.objExists(dstObj)
dstObj2Exists = maya.cmds.objExists(dstObj2)
if dstObjExists and dstObj2Exists:
foundObject = True
if animation:
self.transferAnimation(dstObj, dstObj2, mirrorAxis=mirrorAxis, option=option, time=time)
else:
self.transferStatic(dstObj, dstObj2, mirrorAxis=mirrorAxis, option=option)
else:
if not dstObjExists:
msg = "Cannot find destination object {0}"
msg = msg.format(dstObj)
logger.debug(msg)
if not dstObj2Exists:
msg = "Cannot find mirrored destination object {0}"
msg = msg.format(dstObj2)
logger.debug(msg)
# Return the focus to the Maya window
maya.cmds.setFocus("MayaWindow")
if not foundObject:
text = "No objects match when loading data. " \
"Turn on debug mode to see more details."
raise mutils.NoMatchFoundError(text)
def transferStatic(self, srcObj, dstObj, mirrorAxis=None, attrs=None, option=MirrorOption.Swap):
"""
:type srcObj: str
:type dstObj: str
:type mirrorAxis: list[int]
:type attrs: None | list[str]
:type option: MirrorOption
"""
srcValue = None
dstValue = None
srcValid = self.isValidMirror(srcObj, option)
dstValid = self.isValidMirror(dstObj, option)
if attrs is None:
attrs = maya.cmds.listAttr(srcObj, keyable=True) or []
for attr in attrs:
dstAttr = dstObj + "." + attr
srcAttr = srcObj + "." + attr
if maya.cmds.objExists(dstAttr):
if dstValid:
srcValue = maya.cmds.getAttr(srcAttr)
if srcValid:
dstValue = maya.cmds.getAttr(dstAttr)
if dstValid:
self.setAttr(dstObj, attr, srcValue, mirrorAxis=mirrorAxis)
if srcValid:
self.setAttr(srcObj, attr, dstValue, mirrorAxis=mirrorAxis)
else:
logger.debug("Cannot find destination attribute %s" % dstAttr)
def setAttr(self, name, attr, value, mirrorAxis=None):
"""
:type name: str
:type: attr: str
:type: value: int | float
:type mirrorAxis: Axis or None
"""
if mirrorAxis is not None:
value = self.formatValue(attr, value, mirrorAxis)
try:
maya.cmds.setAttr(name + "." + attr, value)
except RuntimeError:
msg = "Cannot mirror static attribute {name}.{attr}"
msg = msg.format(name=name, attr=attr)
logger.debug(msg)
def transferAnimation(self, srcObj, dstObj, mirrorAxis=None, option=MirrorOption.Swap, time=None):
"""
:type srcObj: str
:type dstObj: str
:type mirrorAxis: Axis or None
"""
srcValid = self.isValidMirror(srcObj, option)
dstValid = self.isValidMirror(dstObj, option)
tmpObj, = maya.cmds.duplicate(srcObj, name='DELETE_ME', parentOnly=True)
try:
if dstValid:
self._transferAnimation(srcObj, tmpObj, time=time)
if srcValid:
self._transferAnimation(dstObj, srcObj, mirrorAxis=mirrorAxis, time=time)
if dstValid:
self._transferAnimation(tmpObj, dstObj, mirrorAxis=mirrorAxis, time=time)
finally:
maya.cmds.delete(tmpObj)
def _transferAnimation(self, srcObj, dstObj, attrs=None, mirrorAxis=None, time=None):
"""
:type srcObj: str
:type dstObj: str
:type attrs: list[str]
:type time: list[int]
:type mirrorAxis: list[int]
"""
maya.cmds.cutKey(dstObj, time=time or ()) # remove keys
if maya.cmds.copyKey(srcObj, time=time or ()):
if not time:
maya.cmds.pasteKey(dstObj, option="replaceCompletely")
else:
maya.cmds.pasteKey(dstObj, time=time, option="replace")
if attrs is None:
attrs = maya.cmds.listAttr(srcObj, keyable=True) or []
for attr in attrs:
srcAttribute = mutils.Attribute(srcObj, attr)
dstAttribute = mutils.Attribute(dstObj, attr)
if dstAttribute.exists():
if dstAttribute.isConnected():
if self.isAttrMirrored(attr, mirrorAxis):
maya.cmds.scaleKey(dstAttribute.name(), valueScale=-1, attribute=attr)
else:
value = srcAttribute.value()
self.setAttr(dstObj, attr, value, mirrorAxis)
def isValidMirror(self, obj, option):
"""
:type obj: str
:type option: MirrorOption
:rtype: bool
"""
if option == MirrorOption.Swap:
return True
elif option == MirrorOption.LeftToRight and self.isLeftSide(obj):
return False
elif option == MirrorOption.RightToLeft and self.isRightSide(obj):
return False
else:
return True
def isLeftSide(self, name):
"""
Return True if the object contains the left side string.
# Group|Character1:footRollExtra_L|Character1:footRoll_L
# Group|footRollExtra_L|footRoll_LShape
# footRoll_L
# footRoll_LShape
:type name: str
:rtype: bool
"""
side = self.leftSide()
return self.matchSide(name, side)
def isRightSide(self, name):
"""
Return True if the object contains the right side string.
# Group|Character1:footRollExtra_R|Character1:footRoll_R
# Group|footRollExtra_R|footRoll_RShape
# footRoll_R
# footRoll_RShape
:type name: str
:rtype: bool
"""
side = self.rightSide()
return self.matchSide(name, side)
def calculateMirrorAxis(self, srcObj):
"""
:type srcObj: str
:rtype: list[int]
"""
result = [1, 1, 1]
dstObj = self.mirrorObject(srcObj) or srcObj
mirrorPlane = self.mirrorPlane()
if dstObj == srcObj or not maya.cmds.objExists(dstObj):
# The source object does not have an opposite side
result = MirrorTable._calculateMirrorAxis(srcObj, mirrorPlane)
else:
# The source object does have an opposite side
if MirrorTable.isAxisMirrored(srcObj, dstObj, [1, 0, 0], mirrorPlane):
result[0] = -1
if MirrorTable.isAxisMirrored(srcObj, dstObj, [0, 1, 0], mirrorPlane):
result[1] = -1
if MirrorTable.isAxisMirrored(srcObj, dstObj, [0, 0, 1], mirrorPlane):
result[2] = -1
return result