github: https://github.com/GaoYuanBob/LaplaceDeformation
https://blog.csdn.net/Bob__yuan/article/details/81778875,这里是我的第一篇Laplace形变的学习记录,在这第二篇学习记录中,想将做出的几个结果放上来看一下(红色是固定区域,绿色是移动区域,由于上一篇文章说了,Laplace Deformation是可以三个轴分着计算的,所以只是将绿色区域在Y方向上进行拖动,所有点的坐标也只更新Y坐标):
我目前是先实现的最简单的Laplace Deformation,就是点的Laplace坐标就是单纯的用周围一圈点的坐标平均(同样的权重)然后和该点坐标做减法。从结果上看还是比较对的,为了检查在内存有洞的情况下是否会影响,加的大图的测试(红色为移动锚点,进行旋转,蓝色未固定锚点,固定不动)。
具体到代码实现流程如下:
1、先选取锚点
对一个要进行形变的模型,我们先选择出锚点,包括在形变过程中不动的锚点,我称之为固定锚点,记录这些点的索引到fixed_anchor_idx(vector)中;然后选取我们确定形变之后在哪里的点,我称之为移动锚点,记录这些点的索引到move_anchor_idx(vector)中。
2、计算点的连接关系
这个功能在VCG库里其实是有的,导入模型之后自动就有这种属性,但是为了实现流程,还是自己写了一下,大致思想就是遍历所有的面(必须是三角形),由于是三角形面片,所以三角形上的三个点肯定是相连接的,对于所有面片,将每个点的连接点vector中,添加上这个三角形面片的另外两个点,遍历完成,所有点的连接关系就确定完成了。
3、计算Laplace矩阵 - L
处理的模型有 N 个点,Laplace矩阵L就是 N * N 的方阵(稀疏矩阵,大多数位置值为0),对角线上代表,第i个点有几个邻接点,也就是用到了我们上边计算出来的连接关系。然后 i,j 位置是 -1 则表示第 j 个点和第 i 个点连接,如果是0,则没有相连接。如果觉得自己的矩阵可能有问题,可以输出出来,看看每一行的 -1 的个数加起来和这一行对角线上的值是不是一样的,不一样一定是问题的!
具体到代码部分,我用到的是VCG中的Eigen库,头文件方面需要如下几个。
首先是创建Laplace矩阵L以及之后需要扩充的矩阵Ls,L的大小是N * N,Ls的大小是 (M + N) * N(M是锚点总个数)。然后根据上述方式进行赋初值。这个过程过后,Laplace矩阵L就计算完成了!L用来和形变前模型的所有点坐标组成的矩阵相乘,得到等式右边的Laplace坐标,(忘了是啥可以看一下第一篇文章)。Ls还没有计算完成,还需要添加锚点信息。
比如说对于只有19的圆盘来说,Laplace矩阵L就如下图所示。
接下来添加固定锚点和移动锚点信息,添加方式相同,就是每一个锚点添加一行,这一行中除了这个点的索引处为1,其余位置全为0;同样是上边的模型,Ls矩阵就是多了两行的L,如下所示。(可以看出,只有两个锚点,分别是第0号,和第16号点)。
输出矩阵信息的时候,Eigen库重载了cout,所以直接 cout << L << endl; 即可输出整个矩阵,并且是对齐的。但是矩阵很小时输出能够看出一些信息,矩阵大了肉眼就看不出什么信息了。
4、计算等式左边 - LsTLs
上图公式左边,和x相乘的就是Ls矩阵,计算了Ls之后,就是计算等式 左边了。也就是求出Ls矩阵的转置,然后左乘上Ls本身,我称之为 LsTLs 矩阵。所有的矩阵库都肯定有矩阵求转置的方法,Eigen库中直接 LsT = Ls.transpose(); LsTLs = LsT * Ls; 就可以求出我们需要的 LsTLs 矩阵了。
5、计算等式右边 - LsTb
等式右边的矩阵称为 b,上边的部分就是用我们之前算过的Laplace矩阵左乘上所有点的坐标的矩阵,得到的所有点的Laplace坐标矩阵,因为xyz三个轴可以分别计算,所以每个轴其实是一个很长的向量。b 矩阵下边部分是Laplace Deformation的重点!它包括两个部分,固定锚点和移动锚点,b里的每一行,对应着Ls中的每一行。这些表示锚点信息的行,是决定形变方式以及效果的数据。一行和一行之间的对应一定要对!
每一行的赋值,对于固定锚点和移动锚点,赋值的原理是一样的,每一行除了点的索引位置上有数,其余位置都是0,索引位置上的数,是这锚点的形变后坐标!!
但是,对于固定锚点,因为固定,所有其实形变后坐标也是形变前坐标,固定锚点的每一行就是在上述数学公式中的含义就是,这个点,在形变过程中,坐标没有变。
对于移动锚点,因为移动了,所有不再是形变前坐标,这时就需要用到我们指定的这些点形变后的坐标,要自己指定。
从代码也可以看出,就是先用Laplace矩阵L计算所有点的Laplace坐标,也就是 b 矩阵的上半部分,然后通过Eigen库中的 conservativeResize 方法将 b 矩阵扩展为 N + M 行,然后用上述方法指定锚点信息,我这里只对Y坐标进行改动,对所有移动锚点,将其Y坐标减去5,看看形变效果。(从这里就可以明显的看出,如果所有点只是在Y轴方向上移动,那么模型上所有点的X和Z坐标形变前后是都不会变的!)。
6、计算形变后的坐标
得到形变后的坐标 x' 的方式就是解 LsTLs * x' = LsTb 这个线性方程组,方程组左右我们都知道了,并且用矩阵的形式表示,我们使用Eigen库中提供的Cholesky分解加速计算。vy_new = LsTLs.llt().solve(LsTby); 一行即可计算出 vy_new 这个 Eigen::VectorXf 类型向量,向量的大小是自动计算出来的,不需要指定。
7、对模型进行形变
得到的 vy_new 向量包括了所有点的 y 坐标(x和z同样方式计算),但不是所有点的 y 坐标都需要进行更新,因为我们有锚点,所以更新模型的方式就是,遍历vy_new向量:
(1)如果这个索引上的点不是锚点,那么这个点的新坐标就变为 vy_new 对应位置的值
(2)如果这个索引上的点是固定锚点,那么这个点的坐标不用调整
(3)如果这个索引上的点是移动锚点,那么这个点的坐标调整为指定的形变后坐标
也就是说,只有不是锚点的点的坐标是需要计算的,锚点的形变后坐标的我们在形变前就已经知道了,如下图所示赋值。
这样,模型的所有点的坐标也已经更新了,完整的Laplace形变就完成了。
如果有更多要求限制,比如旋转不变性,保体积性,等等,也是在这个流程的基础上添加约束信息,中心思想是一样的。