maya 约束批量导入导出

 

今天整了一整天maya中的约束节点的导入和导出,因为动画需求,需要将旧文件中的所有约束信息导出,并导入到新文件中。其实MGTools3.0中就有该功能,用过的同学应该都知道这个:

maya 约束批量导入导出

这个可以说是非常方便了,不管约束类型,偏移,还有约束关键帧,绝大多数情况下都能准确跨maya复制过去。然而某些情况还是会出现问题,比如:A和B同时对C进行parent约束,不过A只约束translateX,而B只约束translateY(虽然这种约束方式很傻逼),这种情况下约束并不能成功复制过去。而且这种复制只能针对性复制,而我们动画的需求是,将场景中所有非引用类别的约束全部导出。

那我们可以列出所有非引用的约束节点,然后直接导出吗?当然不可以。maya的节点网络是牵一发动全身的,例如约束节点的连接关系长这样:

maya 约束批量导入导出

如果直接导出约束,会将该节点所有有直接连接的节点都一起导出。

那可以直接记录约束节点的连接信息(例如约束类别,偏移等),保存到文件里(例如json),然后在新的文件中按照相关信息重新做约束吗?答案是可以,可惜我没这么做,因为要考虑的东西太多了:被约束的属性,偏移,约束类别,动画曲线。。。

在排除上面两个方法之后我做了如下尝试:将所有动画曲线类别(也就是除了上图左侧蓝色节点以外)的节点连接信息全部记录到json,然后断开,unparent,导出,再在新的场景中导入约束节点,读入json重新做连接。这样既不会导出多余的东西,节点连接信息又能全部保留。思路正确,然而一番尝试之后我又发现了这个不幸的事实:在打断连接之后,约束节点的1W0属性消失了。。。泥煤,导致导入节点之后连不上该属性,百般尝试未果只好作罢。

maya 约束批量导入导出 maya 约束批量导入导出

断开连接之后的1W0属性消失了

只能祭出终极绝招:移花接木。

也就是将上面的思路中的断开操作,变成:新建两个节点replaceA,replaceB,代替之前的约束节点的上游和下游节点,将之前的操作cmds.disconnectAttr(A.a, B.b)变成cmds.connectAttr(replaceA.a, B.b,  force=True),一定要加上force=True,这样才可以把连接强行接过去。于是乎,就相当于新建的节点代替原来的约束源被导出。但是没关系,在新场景中恢复连接之后把自建的节点删除就好。下面是思路图:

maya 约束批量导入导出

变为:(假设新建的节点就是locator)

maya 约束批量导入导出

 

问题成功解决。

附上代码:


#!/usr/bin/env python

# -*- coding: utf-8 -*-

# author: Dango Wang

# time : 2019/1/29



__author__ = "dango wang"



import pymel.core as pm

import maya.cmds as mc

import json

import codecs

import os

import shutil

import logging





def get_all_constr():

    """

    返回场景中所有的非引用类别的约束节点,返回list

    """

    all_constraints = [constraint for constraint in pm.ls(type="constraint") if not pm.referenceQuery(constraint, inr=1)]

    return all_constraints





def get_parent_transform(node):

    """

    返回给定的节点的父节点,返回tuple

    """

    return node.name(), pm.listRelatives(node, p=True)[0].name()





def get_source_constr(constraint_node):

    """

    返回约束节点的约束源:类型为list

    """

    return pm.PyNode(constraint_node).getTargetList()





def get_s_connections(constraint_node):

    """

    获取所有上游连接,要排出1W0这个节点,因为它是自个儿连自个儿

    """

    all_s = pm.listConnections(constraint_node,d=0,s=1,c=1,p=1)

    connections_dict = dict()

    for each_connection in all_s:

        if 'W0' not in each_connection[1].name() and 'W0' not in each_connection[0].name():

            connections_dict[each_connection[1].name()] = each_connection[0].name()

    return connections_dict





def get_d_connections(constraint_node):

    all_d = pm.listConnections(constraint_node,d=1,s=0,c=1,p=1)

    connections_dict = dict()

    for each_connection in all_d:

        if 'W0' not in each_connection[1].name() and 'W0' not in each_connection[0].name():

            connections_dict[each_connection[0].name()] = each_connection[1].name()

    return connections_dict





def get_all_connections(constraint_node):

    """

    返回与该约束节点所有有关的连接,返回类型[‘上游连接’:‘被连接体’,...]

    """

    connections_dict = dict()

    all_s = get_s_connections(constraint_node)

    all_d = get_d_connections(constraint_node)

    connections_dict.update(all_s)

    connections_dict.update(all_d)

    return connections_dict





def remove_anicrv_nodes(connections_dict):

    """

    将所有的连接信息中的动画曲线节点排除,因为动画曲线可以跟随约束导出。返回类型dict

    """

    if not connections_dict:

        return False

    new_connections_dict = dict()

    anicrv_list = ['animCurveTL', 'animCurveTA', 'animCurveTU']

    # print connections_dict

    for k, v in connections_dict.items():

        # print k, v

        if (pm.nodeType(k) in anicrv_list) or (pm.nodeType(v) in anicrv_list):

            continue

        else:

            new_connections_dict[k] = v

    if new_connections_dict:

        return new_connections_dict

    else:

        return False





def break_s_connections(connections_dict):

    """

    打断跟约束节点有关的非动画曲线的连接.这里踩了很多坑,直接打断的话约束节点有些属性会丢失,

    只能再创建个节点代替之前的连接跟随约束节点一起导出

    """

    if not connections_dict:

        return False

    new_node = mc.createNode("transform", n="dango_constraints_record_temp_node")

    for k, v in connections_dict.items():

        try:

            # print k,v

            s_node_name = k.split(".")[0]

            attr = k.split(".")[1]

            if not mc.objExists(new_node+'.'+attr):

                mc.addAttr(new_node, shortName=attr, longName=attr)

            new_k = k.replace(s_node_name, new_node)

            print mc.connectAttr(new_k, v, f=1)    

            try:        

                mc.disconnectAttr(k, v)

            except:

                pass

        except RuntimeError:

            continue

    return True





def break_d_connections(connections_dict):

    """

    同上

    """

    if not connections_dict:

        return False

    new_node = mc.createNode("transform", n="dango_constraints_record_temp_node")

    for k, v in connections_dict.items():

        try:

            # print k,v

            s_node_name = v.split(".")[0]

            attr = v.split(".")[1]

            if not mc.objExists(new_node+'.'+attr):

                mc.addAttr(new_node, shortName=attr, longName=attr)

            new_v = v.replace(s_node_name, new_node)

            print mc.connectAttr(k, new_v, f=1)            

            try:        

                mc.disconnectAttr(k, v)

            except:

                pass

        except RuntimeError:

            continue

    return True





def add_connections(connections_dict):

    """

    重新建立字典中两个节点之间的连接

    """

    if not connections_dict:

        return False

    not_found_connections = list()

    for k, v in connections_dict.items():

        if not pm.objExists(k):

            not_found_connections.append(k)

            continue

        if not pm.objExists(v):

            not_found_connections.append(v)

            continue

        try:

            # print k,v

            mc.connectAttr(k, v, f=1)

        except RuntimeError:

            try:

                mc.connectAttr(v, k, f=1)

            except RuntimeError:                

                # print k, v

                continue

    if not_found_connections:

        return not_found_connections

    else:

        return False



def get_pos(transform):

    translate = mc.getAttr(transform+".t")

    rotate = mc.getAttr(transform+".r")

    translate.extend(rotate)

    return translate





def get_all_positions(constraint_nodes):

    # 获取所有约束相关的物体的位置,在重建约束之前要先把位置打回去

    mc.currentTime(101)

    all_transforms = list()

    all_positions = dict()

    all_sources = map(get_source_constr, constraint_nodes)

    all_destis = [get_parent_transform(each_info)[1] for each_info in constraint_nodes]

    for i in all_sources:

        all_transforms.extend(i)

    all_transforms = [k.name() for k in all_transforms]

    all_transforms.extend(all_destis)

    for each_transform in all_transforms:

        all_positions[each_transform] = get_pos(each_transform)

    return all_positions





def export_constraints(path):

    """

    将场景中所有处理完之后的约束节点导出,同时输出连接及父子信息

    """

    # 创建输出文件

    parent_info_path = path + '/parent_info.json'

    connections_dict_path = path + '/connections_dict.json'

    offset_info_path = path + '/offset.json'

    constraints_ma = path + '/constraints.ma'

    # 获取相关信息

    all_constraints = get_all_constr()

    if not all_constraints:

        return False

    parent_info = map(get_parent_transform, all_constraints)

    all_connections_info_temp = map(get_all_connections, all_constraints)

    all_connections_info = map(remove_anicrv_nodes, all_connections_info_temp)

    all_positions_info = get_all_positions(all_constraints)

    print all_connections_info

    # 打断连接及unparent

    # map(break_connections, all_connections_info)

    all_s = map(get_s_connections, all_constraints)

    all_d = map(get_d_connections, all_constraints)

    map(break_s_connections, all_s)

    map(break_d_connections, all_d)

    # print all_constraints

    # 这里有一个坑,如果直接pm.Unparent(all_constraints)的话会提示找不到目标

    mc.select(all_constraints, r=True)

    if mc.ls(sl=True):

        mc.Unparent()

    # 写入文件

    with codecs.open(parent_info_path, 'w', 'utf-8') as p:

        json.dump(parent_info, p)

    with codecs.open(connections_dict_path, 'w', 'utf-8') as c:

        json.dump(all_connections_info, c)

    with codecs.open(offset_info_path, 'w', 'utf-8') as t:

        json.dump(all_positions_info, t)

    # 导出约束节点

    mc.file(constraints_ma, es=1, force=1, typ="mayaAscii", options="v=0;p=17;f=0")

    pm.delete(all_constraints)

    pm.delete("dango_constraints_record_temp_node*")

    logging.info(u'成功导出文件:{}{}{}{}'.format(parent_info_path, connections_dict_path, constraints_ma,offset_info_path))





def import_constraints(path):

    """

    导入所有的约束节点以及连接信息并重新进行连接

    parent_info长这样:[(node,parent),(node,parent),(node,parent),...]

    all_connections_info长这样:[{k:v,k:v,...},{k:v,k:v,...},{k:v,...},...]

    """

    # 读入信息

    mc.currentTime(101)

    parent_info_path = path + '/parent_info.json'

    connections_dict_path = path + '/connections_dict.json'

    constraints_ma = path + '/constraints.ma'

    offset_info_path = path + '/offset.json'

    with codecs.open(parent_info_path, 'r', 'utf-8') as p:

        parent_info = json.load(p)

        # print parent_info

    with codecs.open(connections_dict_path, 'r', 'utf-8') as c:

        # print type(c)

        all_connections_info = json.load(c)

    with codecs.open(offset_info_path, 'r', 'utf-8') as t:

        all_positions_info = json.load(t)

    # 判断一下约束节点的父节点在不在,重新parent

    all_parents = [each[1] for each in parent_info]

    not_found_parents = [notfound for notfound in all_parents if not pm.objExists(notfound)]

    if not_found_parents:

        error_info = [error for error in not_found_parents]

        logging.error(u'以下节点未找到!请确保文件整理前后命名一致:%s' % error_info)

        return 

    mc.file(constraints_ma,i=True)

    for each_tuple in parent_info:

        mc.parent(each_tuple[0], each_tuple[1])

    #  将所有约束相关的物体放到原位

    for each_obj, each_pos in all_positions_info.items():

        # print  each_pos

        mc.setAttr(each_obj+".t", each_pos[0][0], each_pos[0][1], each_pos[0][2])

        mc.setAttr(each_obj+".r", each_pos[1][0], each_pos[1][1], each_pos[1][2])

    # 增加连接,找不到的连接节点将被返回

    connections = map(add_connections, all_connections_info)

    # print connections

    error_connections = list()

    for each_connect in connections:

        if each_connect:

            error_connections.extend(each_connect)

    if error_connections:

        error_info2 = [error2 for error2 in error_connections]

        logging.error(u'以下节点未找到!请确保文件整理前后命名一致:%s' % error_info2)

        return False

    # if mc.objExists("dango_constraints_record_temp_node*"):

    #     mc.delete("dango_constraints_record_temp_node*")

    return True





if __name__ == '__main__':

    # 导出场景中所有的约束节点和相关信息

    export_constraints("D:/dango_repo/constraint_exp_imp/test")

    # 导入所有的约束节点和重建相关连接

    import_constraints("D:/dango_repo/constraint_exp_imp/test")



    # 删除临时节点(直接写在import_constraints中的话不成功,可能因为map执行效率低导致):

    if mc.objExists("dango_constraints_record_temp_node*"):

         mc.delete("dango_constraints_record_temp_node*")



     # 删除导出的文件(将整个文件夹删除)

     shutil.rmtree("D:/dango_repo/constraint_exp_imp/test")