前言

  总结一下这段时间开发表情相似方案来替代RBF变形器的方案。
  最近一直在专研这个方案,中途有些困惑的地方不清楚的地方也是通过张峥前辈的讲解才算明白。
  中间也有过一些其他的想法,走了不少弯路。
  这一篇文基本上就是 7 月份第一周的所有工作内容了。

初期准备 - 弯路

  最开始前辈有给我讲了大概的计算方案。

  • 给模型画曲线
  • 将曲线压成二维空间进行相似度比较
  • 算出相似度来模拟RBF的输出值

  最初我听完前辈的讲解只获取到了这些信息
  (有时候真的应该做些记录的,前辈讲解很长,有很多细节都没有记下来)

  考虑到要相似计算,于是就到网上搜索了相似计算的相关算法
  其中涉及到大都是数据分析,机器学习之类的东西,感觉和我想象的图形学相差甚远。
  不过没关系,既来之,则安之。
  首先我找到了一篇很棒的文章,讲解了最常用的五种计算数据相似的方案。

国外相似方案
  后面又在国内的网站上找到更多关于相似度计算的方案

csdn

  于是大概花了两天,都在研究并将相关的相似算法封装到py中进行调用

  主要封装了 minkowski_distance 闵可夫斯基距离 和 cosine_similarity 夹角余弦 相似

minkowski_distance
cosine_similarity

  研究到这里之后,我发现要判断曲线的相似需要判断曲线上的点的相似。
  于是我就开始了插件开发。

曲线生成

  很明显曲线是需要人工去生成的。
  这个过程需要用到简模上的点来生成曲线,因为简模上的每一个点都代表了一个底层骨骼。
  
  最初的方案:

  • 获取模型上的点
  • 生成曲线
  • 重建曲线 - 让曲线上的顶点数量和选择的模型点数一致
  • 遍历曲线上的点匹配到模型的点上

  原本以为这个方案生成曲线已经万无一失了,但是经过我一步步实现之后,还是出现了问题。
  最后一步的位置匹配上,由于模型上的点的选择顺序是混乱的。
  这将会导致位置匹配的混乱

选择所有点生成出来比较夸张的结果
  为了解决这个问题,我想到了我以前做的选点成盒插件。

  当时也遇到选点生成混乱的问题,最后是通过开启选择顺序的追踪实现了按选择顺序获取列表的效果。
  不过我觉得这样的交互体验不好,选择的时候必须要按照对应的顺序选择才可以正确生成曲线。
  于是我想有没有更好的方案。
  经过一番搜索,在网上找到了一个blender的解决方案。

stackexchange
stackexchange

  问题的需求和我所先要实现的效果基本一致的。
  于是我参照Blender的API改成了Maya的实现方式

maya

  这个方案通过选择的边进行遍历对应,最后实现顺序选择的效果。
  当然这个方案的缺点就是算法复杂度较高,执行效率很低。
  不过考虑到我们生成的曲线也不会有有太高的精度,也不失为一个智能的解决方案。

生成效果

曲线匹配

  曲线生成成功了,我可以手动将每个表情的曲线创建出来
  模拟曲线匹配的效果了。
  于是我创建了一个只有几个曲线用于匹配的文件。

曲线匹配

  通过上面的相似算法,就可以计算各个点的余弦角相似度以及距离相似度
  ╮(╯▽╰)╭ 其实相似度算法算出来的压根就不是什么相似度。
  就是算出了 距离 和 夹角的cos值
  因此在相似匹配上没有太好的对比方法。
  于是我不得已又去咨询了张峥前辈。

  然后才知道前辈想要的计算方法根本就不是这样的。
  我需要计算曲线上每个点到坐标系的距离、点相邻的两条边的边长、点两边的夹角角度。
  然后根据上面获取到的数值逐一和运动曲线进行比值运算。
  通过比值的百分比就可以知道当前点的相似度。

  这个想法非常有道理,也不复杂,或许是因为自己在网上找到的相似算法给框住了,完全没有想到这种比值的运算方法。
  于是我赶紧将相关的数据或取出来。

相关数据获取

  上面是最终获取到的数据列表,最后一列是数据相乘的结果。
  获取比值应该有大有小,还需要将比值限制在 0 - 1 的区间内

相关数据获取

  最后的权重应该如何输出我也咨询过前辈。
  前辈说要通过 距离比值作为可输出的阈值,角度值作为百分比乘上去。
  但是这样输出的话还是有问题,我们可以将输出值整理一下,按照曲线的点去排序。

最终输出

  可以看到即便已经有数值取 1.0 完全相似,其他表情的相似度数值也依然高居不下。
  或许会想到,用条件判断,如果数值为1.0的话就将其他数值归零。
  然而因为是条件判断会导致数据不连续。
  也就是说在 0.99 的时候函数还有很多修型效果,到 1.0 的瞬间表情就跳了。

数值优化

  关于上面的数值输出优化,我想了很多很多,但也没有想到好的解决方案。
  为此我都开始翻数据分析、统计学的教程来尝试解决这个数据问题。
  然后最终那些零零碎碎的概念也没有派上多大用场 (:зゝ∠)

  制作这个真的深刻体会到了自己数学知识的匮乏 /(ㄒoㄒ)/~~
  不管有用没用,我先将有可能用到的数据输出出来。
最终输出

  因为找不到数据间的关系,我还一度打算吃透RBF,在RBF的基础上改良吧。
  RBF终究是太过神秘了,我想找个简单的说明,看看RBF是怎么实现这样的计算的。

RBF的研究尝试

  无论是之前研究 weightDriver 的时候还是现在研究相似,我都有看过网上说的RBF到底是什么
  然而在一大堆数学名词的辅助下,成功让我这个门外汉看到自闭了 ╮(╯▽╰)╭
  我打算先找一篇简单一点的说明文章吧,起码要说人话吧。
  没想到还真的找到了一片相当不错的科普

  在平面上看数据可能是混乱的,但是如果我们用数学的方式延伸到更高的维度,或许就可以在高维度上找到线性可分的数学计算方法。
  然后我们将高维度的数据复原回低维度就可以获取到我们认为不可能划分的区域。
  这个思想怎么让我想到了三体 (:зゝ∠) ,真的 tgl :-)
  不过看到文章后面的 泰勒级数、维度延伸 之后。再度自闭了。
  不是失去了兴趣,而是根本就没有这么多时间来研究这个东西了。
  于是至此我就暂时把 RBF 的研究放到一边了。

样本过滤

  因为自己的能力有限,只能运用记忆模糊的高中知识来尝试解题了。(大学的高数真的白学了 (ㄒoㄒ))
  首先要找到一些过滤数据有用的方法,于是在网上我找到了一些有用的数学方法。
  比如将数值范围 归一化

归一化

  封装成函数如下

归一化

  关于样本数据,我首先想到要将一些相似度比较低的数据过滤出去。
  比如说只保留3个或者4个表情数据,其他的表情相似一律不再进行考虑,那么需要处理的数据就会大大减少。
  按照这个想法可以先获取数组中最大的四个值,经过网上一番搜索,可以通过 heapq 的原生包来实现。

1
2
3
4
import heapq
value_list = [0.261, 0.332, 0.0, 1.0, 0.059]
heapq.nlargest(4, value_list) # 从数组中抽取最大的四个元素返回数组
# 返回 [0.261, 0.332, 1.0, 0.059]

  于是根据这个想法可以过滤出最大的四个元素,其他表情一概不再考虑。
  但是当数值最大的四个元素发生变动的时候,还是会有数值跳动的情况。
  如何才能确保数值不会跳动,而是连续呢?
  我想到了数据重映射,类似于Houdini fit 函数来改变数据的区间。

过滤

  将最大区域的四个数进行归一化处理,范围就在最大区域的区间上
  最大区域以外的数就会被映射为负数了
  这样就可以进行统一过滤,见所有的负数变为0。
  这样会牺牲掉最大区域中最小的数,这个数会被转换为0,但是也因为这个数作为桥梁。
  才实现了数值过滤同时不会出现数值跳动的问题。 :-)

样本压缩

  经过了样本过滤之后,剩下的数值可能还是会过大。
  即便有的点已经达到了 1.0 ,也可能和附近的某个表情位置有 0.8 的相似程度
  但是表亲相似不允许数值相加之后过大,很容易造成表情叠加产生各种问题。
  因此这里要想办法压缩其他的样本数值,促进两极分化才可以。
  我需要一个在 0 - 1 区间内单调递增的函数。

RBF

  上图是之前研究RBF曲线修正的时候绘制的。
  我发现我们想要的效果正是之前处心积虑要修正的 RBF 输出曲线(:зゝ∠)
  只有通过这样的曲线输出方才而已将其他数值压缩,而保持最相似数值的不变 ╮(╯▽╰)╭
  没办法,先把数据压缩实现了吧 ……
  符合这样增长的曲线就是指数函数了。

指数函数

  指数函数过定点 (0,1) 只要在x轴往右挪动一个单位就可以实现过定点 (1,1)
  定点 (1,1) 正是我们所期待的的输出值。
  只要指数函数的底数是大于1,就可以确保指数函数是单调递增的。
  不过这里底数也不应该是个定值,这里的底数会影响到指数函数的斜率变化。
  底数越大,斜率变化也就越大,(其实求导就可以知道的)
  我们希望在输入的数据中,如果数值没有那么两级分化,那么应该减少压缩的效果。
  反之亦然,因此这里的底数应该根据样本的两极分化程度来标定的
  更具统计学的定义,标准差就是来判断数据之间的相差程度的。
  (因为数值都在 0 - 1 的区间,传个最大值其实区别也不大)
  这里应该传入最大值,因为做了传入值为 1 的时候过滤其他数值为0的操作
  这里通过if判断进行过滤是因为到其他数值已经压缩了,数值跳动不会很大。

样本压缩

  通过上面的方法可以有效的压缩无关数值,但也会造成和之前RBF节点输出的问题。
  小数值输出会更小,只有很接近的时候才可以让修型体现出来(:зゝ∠)

输出对照

  remap_list 是经过样本过滤的值
  shrink_list 是在经过样本压缩的输出值

插件优化

多表情同步

  考虑到生成的曲线是对应多个表情,因此在一个模型上创建的曲线需要同时在多个其他表情上创建出来。
多个曲线

  实现原理也不复制,for循环表情数组逐个选择顶点创建即可,毕竟顶点序号是一致的。
  另外,为了实现每次打开的状态记录,也沿用了之前开发 RBF_Panel 的方法,通过一个数据节点记录插件内部数据,实现场景关闭打开也可以读取相关的数据。

驱动曲线

  经过上面的步骤,终于将需要匹配的曲线创建出来了。
  不过运动模型上的曲线还需要让曲线跟着模型运动,这样才可以实现和其他表情曲线比较。
  如何让曲线跟着模型运动呢?
  最初想到的方案就是 cluster curve 让曲线上的每一个顶点生成簇,然后用毛囊对簇进行约束。
  但是想想就知道这个方案有多麻烦。还有找到毛囊与簇的对应关系,麻烦死人。
  于是我又想到了以前学习绑定时候的 wire deformer
  线变形器可以让线来控制器模型的表面效果。
wire deformer

  然而很明显这个效果恰恰和我们要的完全相反了。
  我们要模型驱动曲线,而不是曲线驱动模型(:зゝ∠)
  不过变形器终究还是启发了我。
  maya还有一个 wrap 变形器可以实现模型带动效果
wrap deformer

  于是我尝试让曲线包裹到模型上,成功实现了模型带动曲线运动的效果。
  这个方案比一开始设想的方案要简单得太多太多了。
  手动操作已经实现了,理论上代码实现也不会有太大的问题,然而这里还是有坑。
  maya所有的变形器都是通过 cmds.deformer 命令去创建的。
  因此就完全不知道 wrap deformer 需要传入什么参数,链接什么相关的模型才能正常工作。
  而代码的回显只有一句
命令回显

  我尝试复制这段代码,执行,但是却无法实现wrap的效果,这行代码仅仅是创建了一个变形器附着到目标模型上而已。
  于是经过研究深层的代码回显才找到了核心代码。
命令回显

  只要按照顺序选择模型,通过这行代码就可以实现包裹变形器的绑定。
  总算是大功告成!