前言

  最近翻 RJ 的博客,找到了一个可以K帧轴心的绑定设置 链接
  恰巧动画组有人也有相关的轴心K帧的需求,于是我就在 RJ 的基础上进行了进一步研究。

  这篇文章是 2019 年末的研究成果了,因为各种原因拖到了现在才开始写博客做记录。
  最近临近春节,年会等事情比较多,暴饮暴食,结果也胖了不少(:з」∠)
  说会正题,这次尝试了 Script Job 来实现自动执行代码效果,也算是花了不少心思了。

问题分析

  几个星期前动画人员做动画,遇到了个挪动轴心点K帧的问题。
  通过挪动轴心点来实现不同轴心的旋转定义,如此旋转效果才比较合理。

  比如说角色用剑砸到地上,这个时候剑的轴心在手上
  砸到地上之后,希望剑来反向控制角色,这个时候轴心应该是剑接触的地面上。

解决方案 - 轴心点控制

  最简单的方法就是控制 transform 节点的 rotatePivot 属性
  RJ大佬就是比较经典的绑定制作方案,其实也可以更加简化操作。

alt

  这样连接 Locator 就可以控制到物体的轴心点位置了。

alt

  但是这种搭建方法会有一些问题

alt

  没错,这种方案的轴心点一旦移动旋转了之后,再次移动会给模型带来位置的偏移。
  于是下一步的解决方案就是重点解决这个问题。

解决方案 - 事件钩子调用

alt

  首先可以看到,如果是正常的 pivot 模式下是可以任意移动而不会造成模型偏移的。

  因此要实现这个方案需要研究这种 pivot 模式下是如何让模型不产生偏移的。
  最初我打算使用 绑定的方式 来实现这种效果,但是始终解决不了问题。
  后来我发现 transform 节点下有 rotatePivotTranslate 属性
  当我移动轴心点的时候,模型偏移的数值会自动计算赋值到这个属性上。

alt

  因此只要算出这个数值然后赋值到这个属性上就可以了。
  然而我折腾了好久,发现如果不通过节点来计算的话,就必须要移动过程中更新这个属性,需要用到 scirptJob
  但是这样其实和直接移动轴心操作是没有区别的(:з」∠)

  于是最初我是采用了 scirptJob 的生成方案
  但是 scirptJob 有一个缺点就是不能保存下来,还需要使用 scriptNode 实现打开加载功能,同时还要避免生成过多的垃圾 scriptNode 节点。
  因此整一个实现方法还是有些许复杂,不比开发一个节点简单多少

OpenMaya 事件钩子调用

github 代码链接

   scriptJob 解决方案有一些问题,因此代码上面注释掉了。
   scirptJob 是有 attributeChange 事件可以捕获物体的移动然后执行相应的脚本
  但是这个更新是当物体移动停止之后才开始执行的,没有办法实现类似于节点的操作,一边移动一边更新
  因此后面我将代码升级成了 OpenMaya MMessage 的方案。

  调用 OpenMaya.MNodeMessage.addAttributeChangedCallback 函数可以实现节点动态改变动态更新效果。
  具体的实现方法参考了autodesk论坛的回答 链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def pivotEvent(sel,loc,msg, m_plug, otherMplug, clientData):

# msg kIncomingDirection = 2048
# msg kAttributeSet = 8
# 2048 + 8 = 2056
if msg == 2056:
if not pm.objExists(sel) or not pm.objExists(loc):
return
node,attr = m_plug.name().split(".")
if str(loc) == node and "translate" in attr and pm.objExists(loc):
pm.xform(sel, piv=pm.xform(loc,q=1,ws=1,t=1), ws=1)

# 此处省略代码 ...
idx = om.MNodeMessage.addAttributeChangedCallback(_node, partial(pivotEvent,sel,loc))

  代码层面上 sel 和 loc 都是我额外传入的参数
   msg 后面的参数是这个函数回调传入的参数。

  msg 是类型数字 这里回调涉及到所有的情况,因此需要通过 msg 来过滤属性变化的情况。
  后续就是判断传入的参数是否存在,如果存在的话更新 sel 变量的 旋转轴心
  相当于一个物体的移动,无时无刻都在同步另一个物体轴心点位置。

scirptJob 删除钩子

  传入的回调事件如果不删除的话会一直循环执行的,如果不及时删除可能会严重影响Maya的性能,甚至导致Maya崩溃。
  因此这里通过 scriptJob runOnce 的方案来解决掉 OpenMaya 当中的事件

1
2
3
4
5
6
# 此处省略代码 ...
idx = om.MNodeMessage.addAttributeChangedCallback(_node, partial(pivotEvent,sel,loc))
deleteEvent = lambda:om.MMessage.removeCallback(idx)
pm.scriptJob( ro=1,nd= [str(loc),deleteEvent], protected=True)
pm.scriptJob( ro=1,e= ["SceneOpened",deleteEvent], protected=True)

  因此两个 scirptJob 会分别在 节点删除以及打开新场景的时候删除事件。

ScriptNode 重置效果

  无论是 scriptJob 还是 MMessage 事件都是无法保存到文件里面的
  因此需要在打开文件的时候重新创建事件,确保脚本运行正常。
  而这里可以通过 ScriptNode 来实现。

  scriptNode 命令提供了几种监听状态来实现

  • 0 Execute on demand.
  • 1 Execute on file load or on node deletion.
  • 2 Execute on file load or on node deletion when not in batch mode.
  • 3 Internal
  • 4 Execute on software render
  • 5 Execute on software frame render
  • 6 Execute on scene configuration
  • 7 Execute on time changed

  文档里面有说明,而且命令运行完之后会生成一个 scriptNode 节点

  为了保证文件的干净,这里使用 UUID 来生成全局唯一标识符
  通过这个标识符命名 scriptNode 就可以让这个 ScriptNode 几乎在所有的 Maya 场景中是唯一的,
  也就可以光明正大删除 ScriptNode 而不会造成误删的情况。
(注:最初我是想用序号的方式的,想想就难办)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
from textwrap import dedent
import uuid
UUID = str(uuid.uuid4()).replace("-","_")
node = 'pivotFollow_%s' % UUID

script = dedent("""
import pymel.core as pm
from functools import partial

def pivotEvent(sel,loc,msg, m_plug, otherMplug, clientData):
if msg == 2056:
if not pm.objExists(sel) or not pm.objExists(loc):
return
node,attr = m_plug.name().split(".")
if str(loc) == node and "translate" in attr and pm.objExists(loc):
pm.xform(sel, piv=pm.xform(loc,q=1,ws=1,t=1), ws=1)

# 判断相关节点是否已经被删除了
if pm.objExists("{sel}") and pm.objExists("{loc}"):
sel_list = om.MSelectionList()
sel_list.add("{loc}")
_node = sel_list.getDependNode(0)
idx = om.MNodeMessage.addAttributeChangedCallback(_node, partial(pivotEvent,"{sel}","{loc}"))
deleteEvent = lambda: om.MMessage.removeCallback(idx)
pm.scriptJob( ro=1,nd= ["{loc}",deleteEvent], protected=True)
pm.scriptJob( ro=1,e= ["SceneOpened",deleteEvent], protected=True)

# 如果已经被删除了则删除自身
elif pm.objExists("{node}"):
pm.delete("{node}")
""".format(sel=sel,loc=loc,node=node))
pm.scriptNode( st=1, bs=script, n=node, stp='python')


  虽然这样设置可以完美实现移动轴心的效果
  但是却会丢失物体原先的位置,K帧可能会导致物体无法归位,还是有很严重的隐患(:з」∠)

alt

  这是鱼和熊掌不可兼得,两个方案都各有各自的问题。
  相比于隐藏在暗处的方案二,我更加推崇简单的方案一。
  毕竟方案二很可能会导致前面的动画毁于一旦的,方案一的问题至少还是肉眼可见的。

总结

  这次又进一步尝试了 Maya 的事件系统,感觉用起来还是有点危险的。
  测试 OpenMaya 的时候就忘记了过滤 msg 在输出,结果造成了永动机,被迫强制关闭 Maya 。

  回想起最初接触 scriptJob 还是在 2年前,那个时候看教程,通过 scriptjob 实现 IKFK 自动切换的黑科技
  简直是印象深刻,如今发现 Maya API 里面的事件系统更加全面,能实现的 黑科技 可谓是数不胜数呀。