Files
Nexus/2023/scripts/animation_tools/studiolibrary/mutils/pose.py
2025-11-24 00:15:32 +08:00

592 lines
17 KiB
Python

# 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/>.
"""
#
# pose.py
import mutils
# Example 1:
# Save and load a pose from the selected objects
objects = maya.cmds.ls(selection=True)
mutils.savePose("/tmp/pose.json", objects)
mutils.loadPose("/tmp/pose.json")
# Example 2:
# Create a pose object from a list of object names
pose = mutils.Pose.fromObjects(objects)
# Example 3:
# Create a pose object from the selected objects
objects = maya.cmds.ls(selection=True)
pose = mutils.Pose.fromObjects(objects)
# Example 4:
# Save the pose object to disc
path = "/tmp/pose.json"
pose.save(path)
# Example 5:
# Create a pose object from disc
path = "/tmp/pose.json"
pose = mutils.Pose.fromPath(path)
# Load the pose on to the objects from file
pose.load()
# Load the pose to the selected objects
objects = maya.cmds.ls(selection=True)
pose.load(objects=objects)
# Load the pose to the specified namespaces
pose.load(namespaces=["character1", "character2"])
# Load the pose to the specified objects
pose.load(objects=["Character1:Hand_L", "Character1:Finger_L"])
"""
import logging
import mutils
try:
import maya.cmds
except ImportError:
import traceback
traceback.print_exc()
__all__ = ["Pose", "savePose", "loadPose"]
logger = logging.getLogger(__name__)
_pose_ = None
def savePose(path, objects, metadata=None):
"""
Convenience function for saving a pose to disc for the given objects.
Example:
path = "C:/example.pose"
pose = savePose(path, metadata={'description': 'Example pose'})
print(pose.metadata())
# {
'user': 'Hovel',
'mayaVersion': '2016',
'description': 'Example pose'
}
:type path: str
:type objects: list[str]
:type metadata: dict or None
:rtype: Pose
"""
pose = mutils.Pose.fromObjects(objects)
if metadata:
pose.updateMetadata(metadata)
pose.save(path)
return pose
def loadPose(path, *args, **kwargs):
"""
Convenience function for loading the given pose path.
:type path: str
:type args: list
:type kwargs: dict
:rtype: Pose
"""
global _pose_
clearCache = kwargs.get("clearCache")
if not _pose_ or _pose_.path() != path or clearCache:
_pose_ = Pose.fromPath(path)
_pose_.load(*args, **kwargs)
return _pose_
class Pose(mutils.TransferObject):
def __init__(self):
mutils.TransferObject.__init__(self)
self._cache = None
self._mtime = None
self._cacheKey = None
self._isLoading = False
self._selection = None
self._mirrorTable = None
self._autoKeyFrame = None
def createObjectData(self, name):
"""
Create the object data for the given object name.
:type name: str
:rtype: dict
"""
attrs = maya.cmds.listAttr(name, unlocked=True, keyable=True) or []
attrs = list(set(attrs))
attrs = [mutils.Attribute(name, attr) for attr in attrs]
data = {"attrs": self.attrs(name)}
for attr in attrs:
if attr.isValid():
if attr.value() is None:
msg = "Cannot save the attribute %s with value None."
logger.warning(msg, attr.fullname())
else:
data["attrs"][attr.attr()] = {
"type": attr.type(),
"value": attr.value()
}
return data
def select(self, objects=None, namespaces=None, **kwargs):
"""
Select the objects contained in the pose 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 cache(self):
"""
Return the current cached attributes for the pose.
:rtype: list[(Attribute, Attribute)]
"""
return self._cache
def attrs(self, name):
"""
Return the attribute for the given name.
:type name: str
:rtype: dict
"""
return self.object(name).get("attrs", {})
def attr(self, name, attr):
"""
Return the attribute data for the given name and attribute.
:type name: str
:type attr: str
:rtype: dict
"""
return self.attrs(name).get(attr, {})
def attrType(self, name, attr):
"""
Return the attribute type for the given name and attribute.
:type name: str
:type attr: str
:rtype: str
"""
return self.attr(name, attr).get("type", None)
def attrValue(self, name, attr):
"""
Return the attribute value for the given name and attribute.
:type name: str
:type attr: str
:rtype: str | int | float
"""
return self.attr(name, attr).get("value", None)
def setMirrorAxis(self, name, mirrorAxis):
"""
Set the mirror axis for the given name.
:type name: str
:type mirrorAxis: list[int]
"""
if name in self.objects():
self.object(name).setdefault("mirrorAxis", mirrorAxis)
else:
msg = "Object does not exist in pose. " \
"Cannot set mirror axis for %s"
logger.debug(msg, name)
def mirrorAxis(self, name):
"""
Return the mirror axis for the given name.
:rtype: list[int] | None
"""
result = None
if name in self.objects():
result = self.object(name).get("mirrorAxis", None)
if result is None:
logger.debug("Cannot find mirror axis in pose for %s", name)
return result
def updateMirrorAxis(self, name, mirrorAxis):
"""
Update the mirror axis for the given object name.
:type name: str
:type mirrorAxis: list[int]
"""
self.setMirrorAxis(name, mirrorAxis)
def mirrorTable(self):
"""
Return the Mirror Table for the pose.
:rtype: mutils.MirrorTable
"""
return self._mirrorTable
def setMirrorTable(self, mirrorTable):
"""
Set the Mirror Table for the pose.
:type mirrorTable: mutils.MirrorTable
"""
objects = self.objects().keys()
self._mirrorTable = mirrorTable
for srcName, dstName, mirrorAxis in mirrorTable.matchObjects(objects):
self.updateMirrorAxis(dstName, mirrorAxis)
def mirrorValue(self, name, attr, mirrorAxis):
"""
Return the mirror value for the given name, attribute and mirror axis.
:type name: str
:type attr: str
:type mirrorAxis: list[]
:rtype: None | int | float
"""
value = None
if self.mirrorTable() and name:
value = self.attrValue(name, attr)
if value is not None:
value = self.mirrorTable().formatValue(attr, value, mirrorAxis)
else:
logger.debug("Cannot find mirror value for %s.%s", name, attr)
return value
def beforeLoad(self, clearSelection=True):
"""
Called before loading the pose.
:type clearSelection: bool
"""
logger.debug('Before Load "%s"', self.path())
if not self._isLoading:
maya.cmds.refresh(cv=True)
self._isLoading = True
maya.cmds.undoInfo(openChunk=True)
self._selection = maya.cmds.ls(selection=True) or []
self._autoKeyFrame = maya.cmds.autoKeyframe(query=True, state=True)
maya.cmds.autoKeyframe(edit=True, state=False)
maya.cmds.select(clear=clearSelection)
def afterLoad(self):
"""Called after loading the pose."""
if not self._isLoading:
return
logger.debug("After Load '%s'", self.path())
self._isLoading = False
if self._selection:
maya.cmds.select(self._selection)
self._selection = None
maya.cmds.autoKeyframe(edit=True, state=self._autoKeyFrame)
maya.cmds.undoInfo(closeChunk=True)
logger.debug('Loaded "%s"', self.path())
@mutils.timing
def load(
self,
objects=None,
namespaces=None,
attrs=None,
blend=100,
key=False,
mirror=False,
additive=False,
refresh=False,
batchMode=False,
clearCache=False,
mirrorTable=None,
onlyConnected=False,
clearSelection=False,
ignoreConnected=False,
searchAndReplace=None,
):
"""
Load the pose to the given objects or namespaces.
:type objects: list[str]
:type namespaces: list[str]
:type attrs: list[str]
:type blend: float
:type key: bool
:type refresh: bool
:type mirror: bool
:type additive: bool
:type mirrorTable: mutils.MirrorTable
:type batchMode: bool
:type clearCache: bool
:type ignoreConnected: bool
:type onlyConnected: bool
:type clearSelection: bool
:type searchAndReplace: (str, str) or None
"""
if mirror and not mirrorTable:
logger.warning("Cannot mirror pose without a mirror table!")
mirror = False
if batchMode:
key = False
self.updateCache(
objects=objects,
namespaces=namespaces,
attrs=attrs,
batchMode=batchMode,
clearCache=clearCache,
mirrorTable=mirrorTable,
onlyConnected=onlyConnected,
ignoreConnected=ignoreConnected,
searchAndReplace=searchAndReplace,
)
self.beforeLoad(clearSelection=clearSelection)
try:
self.loadCache(blend=blend, key=key, mirror=mirror,
additive=additive)
finally:
if not batchMode:
self.afterLoad()
# Return the focus to the Maya window
maya.cmds.setFocus("MayaWindow")
maya.cmds.refresh(cv=True)
def updateCache(
self,
objects=None,
namespaces=None,
attrs=None,
ignoreConnected=False,
onlyConnected=False,
mirrorTable=None,
batchMode=False,
clearCache=True,
searchAndReplace=None,
):
"""
Update the pose cache.
:type objects: list[str] or None
:type namespaces: list[str] or None
:type attrs: list[str] or None
:type ignoreConnected: bool
:type onlyConnected: bool
:type clearCache: bool
:type batchMode: bool
:type mirrorTable: mutils.MirrorTable
:type searchAndReplace: (str, str) or None
"""
if clearCache or not batchMode or not self._mtime:
self._mtime = self.mtime()
mtime = self._mtime
cacheKey = \
str(mtime) + \
str(objects) + \
str(attrs) + \
str(namespaces) + \
str(ignoreConnected) + \
str(searchAndReplace) + \
str(maya.cmds.currentTime(query=True))
if self._cacheKey != cacheKey or clearCache:
self.validate(namespaces=namespaces)
self._cache = []
self._cacheKey = cacheKey
dstObjects = objects
srcObjects = self.objects()
usingNamespaces = not objects and namespaces
if mirrorTable:
self.setMirrorTable(mirrorTable)
search = None
replace = None
if searchAndReplace:
search = searchAndReplace[0]
replace = searchAndReplace[1]
matches = mutils.matchNames(
srcObjects,
dstObjects=dstObjects,
dstNamespaces=namespaces,
search=search,
replace=replace,
)
for srcNode, dstNode in matches:
self.cacheNode(
srcNode,
dstNode,
attrs=attrs,
onlyConnected=onlyConnected,
ignoreConnected=ignoreConnected,
usingNamespaces=usingNamespaces,
)
if not self.cache():
text = "No objects match when loading data. " \
"Turn on debug mode to see more details."
raise mutils.NoMatchFoundError(text)
def cacheNode(
self,
srcNode,
dstNode,
attrs=None,
ignoreConnected=None,
onlyConnected=None,
usingNamespaces=None
):
"""
Cache the given pair of nodes.
:type srcNode: mutils.Node
:type dstNode: mutils.Node
:type attrs: list[str] or None
:type ignoreConnected: bool or None
:type onlyConnected: bool or None
:type usingNamespaces: none or list[str]
"""
mirrorAxis = None
mirrorObject = None
# Remove the first pipe in-case the object has a parent
dstNode.stripFirstPipe()
srcName = srcNode.name()
if self.mirrorTable():
mirrorObject = self.mirrorTable().mirrorObject(srcName)
if not mirrorObject:
mirrorObject = srcName
msg = "Cannot find mirror object in pose for %s"
logger.debug(msg, srcName)
# Check if a mirror axis exists for the mirrorObject otherwise
# check the srcNode
mirrorAxis = self.mirrorAxis(mirrorObject) or self.mirrorAxis(srcName)
if mirrorObject and not maya.cmds.objExists(mirrorObject):
msg = "Mirror object does not exist in the scene %s"
logger.debug(msg, mirrorObject)
if usingNamespaces:
# Try and use the short name.
# Much faster than the long name when setting attributes.
try:
dstNode = dstNode.toShortName()
except mutils.NoObjectFoundError as msg:
logger.debug(msg)
return
except mutils.MoreThanOneObjectFoundError as msg:
logger.debug(msg)
for attr in self.attrs(srcName):
if attrs and attr not in attrs:
continue
dstAttribute = mutils.Attribute(dstNode.name(), attr)
isConnected = dstAttribute.isConnected()
if (ignoreConnected and isConnected) or (onlyConnected and not isConnected):
continue
type_ = self.attrType(srcName, attr)
value = self.attrValue(srcName, attr)
srcMirrorValue = self.mirrorValue(mirrorObject, attr, mirrorAxis=mirrorAxis)
srcAttribute = mutils.Attribute(dstNode.name(), attr, value=value, type=type_)
dstAttribute.update()
self._cache.append((srcAttribute, dstAttribute, srcMirrorValue))
def loadCache(self, blend=100, key=False, mirror=False, additive=False):
"""
Load the pose from the current cache.
:type blend: float
:type key: bool
:type mirror: bool
:rtype: None
"""
cache = self.cache()
for i in range(0, len(cache)):
srcAttribute, dstAttribute, srcMirrorValue = cache[i]
if srcAttribute and dstAttribute:
if mirror and srcMirrorValue is not None:
value = srcMirrorValue
else:
value = srcAttribute.value()
try:
dstAttribute.set(value, blend=blend, key=key,
additive=additive)
except (ValueError, RuntimeError):
cache[i] = (None, None)
logger.debug('Ignoring %s', dstAttribute.fullname())