Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion client/ayon_maya/api/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,8 @@ def load(

loaded_containers = []
for c in range(0, count):
namespace = lib.get_custom_namespace(custom_namespace)
if not namespace:
namespace = lib.get_custom_namespace(custom_namespace)
group_name = "{}:{}".format(
namespace,
custom_group_name
Expand Down
121 changes: 121 additions & 0 deletions client/ayon_maya/plugins/load/load_anim.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import os
import json

import maya.cmds as cmds
import pymel.core as pm

from ayon_maya.api import plugin
from ayon_core.pipeline.load.utils import get_representation_context
from ayon_core.pipeline.load import get_loaders_by_name
from ayon_maya.api import lib


class AnimLoader(plugin.Loader):
Copy link
Member

@moonyuet moonyuet Jun 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: Please take references from how the setdress and layout loader implemented, super helpful for your case.
SetDress: https://github.com/ynput/ayon-maya/blob/develop/client/ayon_maya/plugins/load/load_assembly.py
Layout: https://github.com/ynput/ayon-maya/blob/develop/client/ayon_maya/plugins/load/load_layout.py

"""Load anim on character"""

product_types = {"animation"}
representations = {"anim"}

label = "Load Anim"
order = -5
icon = "code-fork"
color = "orange"

def load(self, context, name, namespace, data):
project_name = context['project']['name']
anim_file = self.filepath_from_context(context)
assets = context['version']['data'].get("assets", [])
current_asset = [asset for asset in assets if asset['namespace'] in anim_file]
if not current_asset:
self.log.warning(f"Asset not found in version data for animation file: {anim_file}")
return
current_asset = current_asset[0]
asset_context = get_representation_context(project_name, current_asset['representation_id'])
# check if the asset is already loaded
if not cmds.namespace(exists=current_asset['namespace']):
self.log.info(f"Asset namespace {current_asset['namespace']} does not exist, loading asset.")
data['attach_to_root'] = True
loader_classes = get_loaders_by_name()
reference_loader = loader_classes.get('ReferenceLoader')()
reference_loader.load(context=asset_context, name=asset_context['product']['name'],
namespace=current_asset['namespace'], options=data)
ctrl_set = pm.ls(f"{current_asset['namespace']}:{asset_context['product']['name']}_controls_SET")
if not ctrl_set:
self.log.warning("No control set found in instance data")
return
ctrls = pm.listConnections(ctrl_set[0], source=1, type='transform')
if not ctrls:
self.log.warning("No controls found in instance data")
return
self.read_anim(filepath=anim_file, objects=ctrls)


def read_anim(self, filepath, objects, namespace=None):
if not os.path.exists(filepath):
return [False, 'invalid filepath']
with open(filepath, "r", encoding='utf-8') as reader:
anim_data = json.loads(reader.read())
for j, obj in enumerate(objects):
obj_shot_name = obj.name()
obj_longname = obj.longName()
if namespace:
obj_longname = obj_longname.replace(namespace, '{namespace}')
ctrl_value = anim_data.get(obj_longname, [])
if not ctrl_value:
continue
for attrs in ctrl_value:
if not hasattr(obj, attrs):
continue
if attrs in ['lock']:
continue
try:
cur_attr = getattr(obj, attrs)
except AttributeError as e:
self.log.warning(e)
self.log.warning('skipping for {0} as attribute {1} was not found'.format(obj_shot_name, attrs))
continue
key_type = ctrl_value[attrs]['type']
if key_type == 'static':
key_value = ctrl_value[attrs]['value']
connected = pm.listConnections(cur_attr, destination=False, source=True)
if not connected and not cur_attr.isLocked():
cur_attr.set(key_value)
if key_type == 'keyed':
key_values = ctrl_value[attrs]['keys']
infinity_data = ctrl_value[attrs]['infinity']
pre_infinity = json.loads(infinity_data.get('preInfinity'))
post_infinity = json.loads(infinity_data.get('postInfinity'))
weighted_tangents = json.loads(infinity_data.get('weightedTangents'))
for keys in key_values:
time = json.loads(keys.get('key'))
value = json.loads(keys.get('value'))
breakdown = json.loads(keys.get('breakdown'))
tan_lock = json.loads(keys.get('lock'))
weight_lock = json.loads(keys.get('weightLock'))
in_type = json.loads(keys.get('inTangentType'))
out_type = json.loads(keys.get('outTangentType'))
tan1 = json.loads(keys.get('inAngle'))
tan2 = json.loads(keys.get('outAngle'))
weight1 = json.loads(keys.get('inWeight'))
weight2 = json.loads(keys.get('outWeight'))
pm.setKeyframe(cur_attr, time=time, value=value, bd=breakdown)
if weighted_tangents:
pm.keyTangent(cur_attr, weightedTangents=True, edit=True)
try:
pm.keyTangent(cur_attr, lock=tan_lock, time=time)
except Exception as e:
self.log.warning(e)

if weighted_tangents:
pm.keyTangent(cur_attr, time=time, weightLock=weight_lock)
if in_type != 'fixed' and out_type != 'fixed':
pm.keyTangent(cur_attr, e=1, a=1, time=time, itt=in_type, ott=out_type)
if in_type == 'fixed' and out_type != 'fixed':
pm.keyTangent(cur_attr, e=1, a=1, time=time, inAngle=tan1, inWeight=weight1, itt=in_type,
ott=out_type)
if in_type == 'fixed' and out_type == 'fixed':
pm.keyTangent(cur_attr, e=1, a=1, time=time, inAngle=tan1, inWeight=weight1, outAngle=tan2,
outWeight=weight2, itt=in_type, ott=out_type)

pm.setInfinity(cur_attr, poi=post_infinity, pri=pre_infinity)
return None
144 changes: 144 additions & 0 deletions client/ayon_maya/plugins/publish/export_anim_curve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import os
import json
import pymel.core as pm
import maya.cmds as cmds

from ayon_maya.api import plugin, pipeline
from pyblish.api import ExtractorOrder


class ExtractAnimCurve(plugin.MayaExtractorPlugin):
order = ExtractorOrder
label = "Extract Animation curves"
families = ["animation"]
hosts = ["maya"]

def process(self, instance):
staging_dir = self.staging_dir(instance)
filename = "{0}.anim".format(instance.data['variant'])
out_path = os.path.join(staging_dir, filename)
controls = [x for x in instance.data['setMembers'] if x.endswith("controls_SET")]
if not controls:
self.log.warning("No controls found in instance data")
return
ctrls = pm.listConnections(controls[0], source=1, type='transform')
self.log.info(f"controls: {controls}")
self.write_anim(objects=ctrls, filepath=os.path.realpath(out_path))
if "representations" not in instance.data:
instance.data["representations"] = []
representation = {
'name': 'anim',
'ext': 'anim',
'files': os.path.basename(out_path),
'stagingDir': staging_dir.replace("\\", "/")
}
version_data = instance.data.get("versionData", {})
assets = version_data.get("assets", [])
if not assets:
version_data["assets"] = []
asset_data = self.get_asset_data(instance)
version_data["assets"].append(asset_data)
instance.data["versionData"] = version_data
self.log.info(f"representation: {representation}")
instance.data["representations"].append(representation)

@staticmethod
def get_asset_data(instance):
members = [member.lstrip('|') for member in instance.data['setMembers']]
grp_name = members[0].split(':')[0]
containers = cmds.ls("{}*_CON".format(grp_name))
rep_id = cmds.getAttr(containers[0] + '.representation')
name_space = cmds.getAttr(containers[0] + '.namespace')
product_name = cmds.getAttr(containers[0] + '.name')
asset_data = {
"namespace": name_space,
"product_name": product_name,
"representation_id": rep_id
}
return asset_data

def write_anim(self, objects, filepath, namespace=None):
self.log.info(f"objects: {objects}")
self.log.info(f"Writing animation curves to {filepath}")
self.log.info(f"namespace: {namespace}")
anim_data = {}
for j, obj in enumerate(objects):
obj_shot_name = obj.name()
obj_longname = obj.longName()
if namespace:
obj_longname = obj_longname.replace(namespace, '{namespace}')
anim_data[obj_longname] = {}

channels = obj.listConnections(type='animCurve', connections=True, s=1, d=0)
channel_dict = {}
for i, channel in enumerate(channels):
channel = channel[1]
split_name = obj_shot_name
channel_name = (channels[i][0].name().split(split_name + '.')[1])
if channel_name not in channel_dict:
channel_dict[channel_name] = {}
channel_dict[channel_name]['type'] = 'keyed'

keys = pm.animation.keyframe(channel, q=True)
values = pm.animation.keyframe(channel, q=True, valueChange=True)
breakdown = pm.animation.keyframe(channel, q=True, breakdown=True)
in_tangent_type = pm.animation.keyTangent(channel, q=True, inTangentType=True)
out_tangent_type = pm.animation.keyTangent(channel, q=True, outTangentType=True)
lock = pm.animation.keyTangent(channel, q=True, lock=True)
weight_lock = pm.animation.keyTangent(channel, q=True, weightLock=True)
in_angle = pm.animation.keyTangent(channel, q=True, inAngle=True)
out_angle = pm.animation.keyTangent(channel, q=True, outAngle=True)
in_weight = pm.animation.keyTangent(channel, q=True, inWeight=True)
out_weight = pm.animation.keyTangent(channel, q=True, outWeight=True)
weighted_tangents = pm.animation.keyTangent(channel, q=True, weightedTangents=True)[0]

pre_infinity = channel.preInfinity.get()
post_infinity = channel.postInfinity.get()
channel_dict[channel_name]['infinity'] = {
'preInfinity': json.dumps(pre_infinity),
'postInfinity': json.dumps(post_infinity),
'weightedTangents': json.dumps(weighted_tangents),
}

channel_dict[channel_name]['keys'] = []
for y, key in enumerate(keys):
bd = 0
for bd_item in breakdown:
if bd_item == key:
bd = 1
channel_dict[channel_name]['keys'].append({
'key': json.dumps(keys[y]),
'value': json.dumps(values[y]),
'breakdown': json.dumps(bd),
'inTangentType': json.dumps(in_tangent_type[y]),
'outTangentType': json.dumps(out_tangent_type[y]),
'lock': json.dumps(lock[y]),
'weightLock': json.dumps(weight_lock[y]),
'inAngle': json.dumps(in_angle[y]),
'outAngle': json.dumps(out_angle[y]),
'inWeight': json.dumps(in_weight[y]),
'outWeight': json.dumps(out_weight[y])
})
static_chans = pm.listAnimatable(obj)
for static_chan in static_chans:
test_it = pm.keyframe(static_chan, q=True)
connected = pm.listConnections(static_chan, destination=False, source=True)
if test_it or connected:
logger.warning('skipping for {0} as attribute {1} is connected'.format(obj_shot_name, static_chan))
continue
if pm.nodeType(static_chan.name().split(".")[0]) == "camera":
static_name = static_chan.name().split('.')[1]
else:
static_name = static_chan.name().split(obj_shot_name + '.')
if not len(static_name) > 1:
continue
static_name = static_name[1]
if static_name not in channel_dict:
channel_dict[static_name] = {'type': 'static'}
channel_dict[static_name]['value'] = static_chan.get()
anim_data[obj_longname] = channel_dict
if not os.path.exists(os.path.dirname(filepath)):
os.makedirs(os.path.dirname(filepath))
with open(filepath, 'w') as json_file:
json.dump(anim_data, json_file, indent=4)
return filepath