前言

  最近这段时间根据公司的安排,一直研究Ziva插件要怎么用,如何才能更好更快地匹配到产线的使用上。
  为此我们一直绞尽脑汁想流程方案。
  虽然前路漫漫,不过我也确实得出了一些不错的方案可以参考使用。
  为了配合产线使用,大神指点我们,一定要让Ziva插件模块化,要实现各个身体可以切分的效果。
  尽管这样的操作十分复杂,但是建议确实是有用的。

  于是我们根据切分制定了流程方案,但是却遇到了难题。
  切分出来的模型需要制作厚度,然后进行模拟,然后又要讲模拟的动态效果传递回最初的模型上面。
  这个操作无法使用Blendshape来实现,因为这个过程会导致点的ID发生变化。
  即便模型的拓扑是部分一致的,也无法将动态效果模拟过去。
  所以开发的目标很明确,我们需要改变Blendshape的算法,实现寻找模型上最近的点进行Blendshape对应的效果。

OpenMaya 研究

  由于开发之前,我并不有深入使用过OpenMaya API,所以我只好从头开始学起。
  而且给我的时间也不多,毕竟这个开发的不确定因素实在是太大了,如果浪费过多的时间,会得不偿失的。
  因此我硬着头皮,顶着压力把这个东西开发出来了。

  其实能有这样的成果,也多亏了之前绑定那边发了一个让我们去研究的OpenMaya插件。
  这个国外的插件开发者居然将这门神奇的插件开源了,真是万分感谢。
  这个插件就是 jlcolliderDeformer

  最开始了解到原来 OpenMaya API 有两个版本。
  那么首先想到纠结的就是用哪一个了,只是我在Autodesk的文档里面找来找去也找不到 OpenMaya API 1.0 的文档,无奈只好去学OpenMaya 2.0了。(其实1.0的文档就是 C++ API的文档,用法完全一致的,甚至说明里都提到有些函数无法用Python调用(:зゝ∠)
  而且我看到国外的文章还是很推崇 2.0 的API,因为它是 pythonic 的,比较起 1.0 会容易接受很多。
  而且由于直接对接Maya C++ 的底层API,在运行效率上也非常可观。
  其实从这个角度完全可以吊打pymel了,pymel的优点就是pythonic,缺点就是运行效率太糟糕了。

  所以我就开始从这个方向切入去研究学习。
  针对于如何找到距离模型的点最近的点的这个问题,我开始在 Bing 上面进行大量的搜索。
  特别是 VertexID ComponentID 更是我搜索的重中之重。
  最后我并没有找到太多合适的脚本,但是有一些OpenMaya 1.0 的脚本还是很值得参考的。
ComponentID

  于是我就尝试深入研究这些代码是怎么运作。

MIt 命令 | 算法复杂度的思考

  后面发现原来有一堆 MIt 开头的命令,这些命令都是 iterator 来的。
  最开始我并不理解为什么要使用这个东西来进行遍历,不过这个东西我在研究 C++ SFML 的时候也见到过,也不是什么稀罕的东西了。
  直到我看了 Maya Python API 教程之后我才清楚认识到,原来在数据结构的遍历中这个东西还是非常重要的。
  毕竟数据结构的遍历更多的不是for循环的线性结构了。
  很多都是树形结构了(学过数据结构真好,可以学以致用了)
  所以这个时候遍历就会用到 数据结构典型的 Next() 操作。

  那么这个MIt结构优势在哪里呢?
  很明显就是直接接触底层代码,遍历很明确,那么执行速度就很快。OpenMaya 的执行效率基本就是体现在这个地方
  毕竟根据数据结构课程可以知道 for 循环越多,那么算法的复杂度就越高。
  但是算法的复杂度其实也是有前提条件的,我一直忽视了,那就是在大量数据进行比较的情况下。
  其实如果只是普通的操作,比如说添加一两个按钮,修改几个特定的参数。在数据量很少的情况下,是不需要考虑执行效率的。
  也就是代码越少越好,可读性越高越好,复用性越方便越好,可以减少大量的冗余的代码。
  但是如果考虑到大量数据的操作,那精简未必是好事,精简意味着很多底层调用通过别的API执行了。
  那么经过一层又一层才能到达最底层,这过程的算法复杂度可想而知,那么数据量一大,算法复杂度的问题就体现出来了。

  以后在写Maya脚本的时候,遇到大量数据的处理操作可以考虑使用MIt进行遍历,而不是cmds库
  pymel可能写起来简洁,但是案例比较少,我这个人就是懒(:зゝ∠),不想手写代码,容易错。

OpenMaya 2.0 坑爹之处

  后面在研究OpenMaya 2.0 的如何获取最近点。
  发现 Maya2017 的文档里面有intersector 的构建说明。
  根据文档, intersector 需要执行 create 命令才可以正确操作。
  然而 create 函数我无论怎么传入变量,最后Maya2017都会报错,也不知道是什么原因。
  报错的显示似乎是我传入的参数不对,参数的个数多了,但是我少一个多一个参数都是很明显的传参错误。
  但是两个参数穿进去报的错就有点莫名其妙的参数不对,真的是让我头大。
  后来我决定用 1.0 去试试。
  这个时候我去查 C++ 的文档,打算死马当活马医了,没想到发现 C++ 文档就是 1.0 的文档了。
  于是经过重写之后,就可以运作了。

  那就很奇怪了,随后我去查了2015的文档看看 2.0 的API是怎么写的。
  结果没想到2015的文档居然压根就不存在这个函数,没错就是没有intersector函数。
  那么答案感觉呼之欲出了,恐怕2017的时候 2.0 内部还有Bug 或者说还没有完善好。
  真是巨大的坑,而且一个不完善的API就无法做到向前兼容,那对兼用2015的公司流程来说是非常不好的。
  所以最后我决定不再弄 2.0 API,还是老老实实研究 1.0 去了
  而且看了 C++ 文档之后发现,C++ 有大量的案例,而 1.0 的使用本质和 C++ 差不多,只是用Python调用而已。
  所以从便于学习的角度来说,还是 1.0 真香。

VertexConstarint 开发

  经过上面一波瞎折腾,我终于开始了 VertexConstraint 插件的开发。
  中途还速看了教程 Maya Python API ,我看得很快,特别是写代码的部分,感觉特别啰嗦,所以干脆直接看源码算了。
  后面还得总结一波这个教程,整体感觉还是非常非常不错的。

  我拿了教程的 Ripple 插件观摩学习,在结合 jlcolliderDeformer 融会贯通。
  大致摸清楚了插件几个重要的点。

  Maya插件必须包含注册、解注册函数,还要有 插件 id 插件名称等等。
  不过这些上面的脚本都提供了良好的模板供我复制修改。

  通过上面两个脚本结合,我想到要实现这种类似Blendshape的效果,必须使用 变形器Deformer 来实现。
  在看文档的过程中也发现了一个地方介绍了Maya最基础的几种变形器方案。
http://help.autodesk.com/view/MAYAUL/2018/ENU/?guid=__files_Writing_a_Deformer_Node_htm

  其中 Example 里面提到了四种不错的操作方案,没有想到既然有设置好的 Blendshape 方案 和 SkinCLuster 方案
  因此我产生疑惑了,我到底是用 MPxDeformerNode 还是用 MPxBlendShap 去实现
  最后我还是打算先用Deformer的方案,毕竟这个方案有参考呀。

  按照这个思路我开始写代码,但是在如何将模型的点ID传入到变形器节点的问题上犯难了。
  我查阅了 C++ 的文档,官方确实提供了 ComponentList 这种数据类型传入。
  但是没有任何调用的参考,不知道1.0的 Python 代码要怎么写了
  没办法,只好找 C++ Example 参考了,没先到相关的参考居然是 BasicBlendshape
  就是官方提供的 Blendshape 参考案例。
http://help.autodesk.com/view/MAYAUL/2018/ENU/?guid=__cpp_ref_basic_blend_shape_2basic_blend_shape_8cpp_example_html

  既然如此,一不做二不休,直接去研究 MPxBlendShap 方案吧

MPxBlendShape 研究

  花了不少的时间去研究 BasicBlendshape 案例,一开始将这个案例编译成 mll 去看看Maya的调用效果
  看起来也是一脸懵逼,因为它实现的效果不是和 Blendshape 一个样吗?
  看不出这个代码的优化在哪里呀,而且既然它本身就继承了 MPxBlendShap ,能够实现Blendshape的效果不是理所当然的吗?
  直到后面我将内部的代码修改了之后,发现 Blendshape 不灵了,这个时候我才知道,可能这代码就是Blendshape的实现方法吧。

  后面继续注释研究 BasicBlendshape 的代码,不过有一段自己实在是无法理解。

1
2
3
4
5
6
7
// inputPointsTarget is computed on pull,
// so can't just read it out of the datablock
MPlug plug( thisMObject(), inputPointsTarget );
plug.selectAncestorLogicalIndex( multiIndex, inputTarget );
plug.selectAncestorLogicalIndex( w, inputTargetGroup );
// ignore deformer chains here and just take the first one
plug.selectAncestorLogicalIndex( 6000, inputTargetItem );

  特别是上面的6000,完全不知道是哪里冒出来的数字……
  而且由于C++编译极其不方便的缘故,测试代码也非常麻烦。
  有些注释的地方也不知道自己的理解是否是正确的。
  于是,为了加深代码的理解,也为了方便后续测试。我决定将整个 C++ 转成 1.0 来写。(感觉自己的坑越开越大(:зゝ∠)

2019-5-25 更新

  现在我已经大概看得懂这部分的C++代码是什么原理了。
  这里是获取了 inputPointsTarget 属性 (blendshape 路径可以用 pymel 获取出来 blendShape1.inputTarget[-1].inputTargetGroup[-1].inputTargetItem[-1].inputPointsTarget)
  selectAncestorLogicalIndex 就是跳转到当前属性的序号。

BS属性

  通过截图就很清楚明白 6000 数字是怎么来的了。

2019-5-25 更新

  重写的过程才发现,C++的有些代码特别奇怪。

1
MArrayDataHandle inputTargetGroupMH = inputTargetH.child( inputTargetGroup );

  查文档可以发现,这种情况的Child 应该返回的是 MDataHandle 而不是 MArrayDataHandle
  结果我在重写的过程中又在这两种方案中纠结了一段时间。
  最后测试发现,如果过完全不用Array的话 1.0 无法再现 C++ 的效果。
  所以 1.0 的代码之后写成这样了。

1
2
from maya import OpenMaya
inputTargetGroupMH = OpenMaya.MArrayDataHandle(inputTargetH.child( self.inputTargetGroup ))

  看起来怪怪的,至少能用吧。
  搞了那么一大轮之后,我总算是把 BasicBlendshape 用 1.0 改写了。
  效果都可以实现,但是我发现Blendshape似乎并不适合实现我先要的效果。
  Blendshape的获取点属性内部截取的变量,而且不知道怎么定义哪些顶点跟哪些顶点进行对应
  (其实现在想了一下,也未不能实现,只是要加个plug来获取最近的顶点数据而已)

OpenMaya 获取最近的顶点

  所以经过一大轮的折腾之后,我最后还是决定回到最初的方案,用 MPxDeformerNode 的方案
  但是折腾了那么久我连最重要的功能都还没有实现呢?
  于是赶紧重新研究了一下,其实在搞C++之前,一直都在研究这个,只是那个时候见识还很肤浅而已。
  首先那个时候研究确定了两个方案,一个是 jlcolliderDeformer 里面使用的 intersector 方案。
  另一个则是我在网上搜索下来的方案。
http://www.djx.com.au/blog/2013/07/07/get-closest-vertex-in-maya-using-python/
  两个方案我都测试过,intersector 的方案可能会麻烦一点,还需要矩阵支持。
  于是我就使用了后者的方案。

  其实在研究OpenMaya之前,对于如何获取最近的点也是有方案的,只是计算量可怕得吓人。
  没错最简单的想法就是选一个模型的顶点,获取它的世界坐标。然后遍历另一个模型所有的点,获取每个点的坐标,计算和一开始顶点的距离,然后算出最近的点。
  这种方案也和上面提到 MIt 一样,如果不用OpenMaya的话,只要模型的点一多,算法复杂度的可怕就体现出来了,那就得等很长时间了。
  所以弄懂了 MIt 之后,使用 MIt 遍历就成了不二之选了。
  然而通过上面两个方案的提点,其实我发现遍历所有的点的计算量也是大得惊人的。
  所以发射射线去碰撞的方案才是比较节省资源的方案。(既然是三维数据,为啥还要以看不见数据的角度对待它,当然是用三维空间去搞它啦)

  所以方案2采用的就是 MFnMesh 的 getCloestPoint 方法
  一开始我没有理解清楚两个传入的点信息的含义,直到后面才搞懂了。
  第一个点是外部的点,因为我先要计算两个模型之间最近的点组成对,所以我这里的情况当然是另一个模型上的点。
  第二个点则是射线击中的当前模型的点,其实这个不重要,以为这个点不是 vertex 而是 point。也就是可以在模型的任何位置的点。
  最重要的是第四个参数可以获取当前模型被击中的面ID
  没错,第二个方案就是遍历这个面ID上的点来比较和目标点的距离,这样遍历就从整体缩小到了局部,计算量瞬间就少了很多。
  不过呢,第二个方案使用了pymel去遍历点,我个人来说还是懒得去学习它,既然 OpenMaya 效率更高就用 OpenMaya 写吧。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from maya import OpenMaya
from maya import cmds

sel = OpenMaya.MSelectionList()
OpenMaya.MGlobal.getActiveSelectionList(sel)

selList = cmds.ls(sl=1,tr=1)
if len(selList) == 2:
# Note 冻结变换
cmds.makeIdentity( selList[0],apply=True, t=1, r=1, s=1, n=0,pn=1)
cmds.makeIdentity( selList[1],apply=True, t=1, r=1, s=1, n=0,pn=1)

# Note 选择对应的顶点
nodeDagPath = OpenMaya.MDagPath()
comp = OpenMaya.MObject()
sel.getDagPath(0, nodeDagPath,comp)

# Note 初始化变量
space = OpenMaya.MSpace.kWorld

TargetNodeDagPath = OpenMaya.MDagPath()
sel.getDagPath(1, TargetNodeDagPath)
TargetMfnMesh = OpenMaya.MFnMesh(TargetNodeDagPath)

# Note 获取 iterator
itr = OpenMaya.MItMeshVertex(nodeDagPath, comp)

# Note 定义变量
vertexList = OpenMaya.MSelectionList()
faceList = OpenMaya.MSelectionList()
TargetPoint = OpenMaya.MPoint()
util = OpenMaya.MScriptUtil()
util.createFromInt(0)
idPointer = util.asIntPtr()

while not itr.isDone():

# Note 获取最近的poly面序号
TargetMfnMesh.getClosestPoint(itr.position(), TargetPoint, space, idPointer)
idx = OpenMaya.MScriptUtil(idPointer).asInt()

# Note 将获取的面转为顶点
verticesList = OpenMaya.MIntArray()
TargetMfnMesh.getPolygonVertices(idx,verticesList)

closestVert = None
minLength = None
# Note 遍历顶点找出最靠近的顶点
for i in range(verticesList.length()):
vertexPoint = OpenMaya.MPoint()
TargetMfnMesh.getPoint(verticesList[i],vertexPoint)
thisLength = vertexPoint.distanceTo(itr.position())
if minLength is None or thisLength < minLength:
minLength = thisLength
closestVert = verticesList[i]

# Note 通过序号获取面
faceName = "%s.f[%s]" % (TargetNodeDagPath.fullPathName(),idx)
faceList.add(faceName)

# Note 通过序号获取点
vertexName = "%s.vtx[%s]" % (TargetNodeDagPath.fullPathName(),closestVert)
vertexList.add(vertexName)
itr.next()

# OpenMaya.MGlobal.setActiveSelectionList(faceList)
OpenMaya.MGlobal.setActiveSelectionList(vertexList)

  终于经过这么长时间的折腾,插件最原本的开发终于步入正轨了。
  至于采用的方案也决定是 deformer 了。
  鉴于实现的效果并不复杂,于是我就直接将 Ripple 的脚本复制过来进行修改了。

节点生成

  因为变形器还需要获取其他节点的信息,而且执行之前还需要获取出最近点的信息才可以。
  所以不可以直接就生成节点。
  根据 jlcolliderDeformer 的实现方法,他是将代码写在 Mel 中,通过GLobal proc 来进行全局调用。
  但是这样的实现方案没有办法直接迁移到 使用了 OpenMaya 的情况。
  于是我想到了 Ripple 节点的 AccesoryNodeSetup 函数
  这个函数可以在生成了节点之后在执行相关连接节点的操作。
  然而我在生成 MIt 遍历点的时候出错了,原因不明,毕竟这是插件内部的函数。
  因此只能在节点外部实现效果。
  既然如此,就得想办法让 Python 的函数声明全局调用了,就像MEL 的 global proc 一样。
  可是这个简单的想法却难以实现。
  网上搜了一下,基本上Python是没有全局函数的,只能是 导入相关的模块,或者加到一些 builtin 模块。
  经过了好多测试之后,最后我发现最好的是 Python 的 sys 模块。
  Maya不需要导入这个模块,而且每次重新打开Maya之后,相关的函数就会消失,没有污染。

数据传递

  数据传递又算是回到了我之前开发卡壳的地方了。
  如何将获取到的顶点合适地传入到变形器节点中。
  其实最初的时候没有想到 cmds 的 setAttr ,我到这里的时候想到了 setAttr ,那么理论上是可以将数据传进去的。
  但是我先后尝试了componentArray int64Array stringArray 获取都失败了。
  按理来说数据类型是Array的话,那么获取的handle inputArrayvalue 才对的,然而这么获取就报错了。
  然而使用 inputValue 获取就是空数据。
  最后很无奈,只好将问题简化,直接将点数组的字符串传进去。
  反正在Python 有 eval 函数可以激活数组。
  最后的操作总算是成功了。
  只是每一次都会报出 EOF 的错误。
  于是我又在网上搜了一大轮,一直以为是传入的字符串有问题,需要改为raw的形式,可是各种尝试都无济于事。
  直到最后才偶然发现,这个报错只有第一次生成节点的时候才会发生,后面的代码都是正常的。
  我突然明白第一次生成节点的时候并没有传入任何字符串,也就是eval了空的字符串,所以才会报错。
  后面加入异常处理就好了。

使用效果

测试文件
  这个文件是我用来测试的插件的文件。
  这个文件是生成一个小球,然后复制一个小球,在删除其他面只留下一小部分的制作出来的。
  经过删除历史的操作之后,这个第二个小球的面和与原
测试文件
  如果直接使用Blendshape
测试文件

  会发生点的错落。


  我这里的变形器则可以解决这个问题
测试文件
  首先先选择小的模型再选择大的模型进行匹配
测试文件
  加载插件之后就可以直接执行脚本编辑器上的函数,函数会检测是否选择了两个物体。
测试文件
  运行之后可以看到节点编辑器里面,会多出一个 VertexConstraint 的变形器节点。
测试文件
  这个时候大模型上点的运动就会带动小模型上的点。由于是根据最近的点进行匹配,没有点ID混乱的情况
测试文件
测试文件
  变形器也加入了权重处理,可以简单刷它的权重效果。

总结

  研究OpenMaya API 对我而言机具挑战性,最后能够开发出来我还是很满足的。
  这次研究感觉自己发现了新大陆,并且感觉Maya的C++也并没有那么深奥。
  最后的最后,还是非常非常不喜欢 C++ 的编写 (:зゝ∠)
  其实有一个将 Maya C++ API 的教程提到了两者的区别。

C++ Python
执行效率高 Maya多版本兼容
闭源开发 Debug方便

  总的来说,我个人还是更喜欢Python的编写,而且可以很方便地结合cmds的库一起使用,编写效率贼高。