为什么要开这个坑:
其实我原本可以选择GAMES103的。。。但是一方面GAMES103用的Unity引擎做作业,我不太适应。另一方面,比较下来感觉CSC417对物理原理和数学推导的阐述比GAMES103更清楚(至少对我来说如此)。因此用这门课的作业来学习物理模拟。
本篇blog直接从Lab-3开始,因为前面的弹簧质点说实话。。。不难,就是麻烦其实是我第二个实验不知道为什么怎么写都炸,当然比GAMES101那个绳子模拟要难些,但是总体而言新东西不多,除了从拉格朗日视角推出了模拟的微分方程。
这次Lab重点在于引入了有限元这一重要方法,推导过程也较长较复杂。
0. 基本原理前置
这方面前置需要了解的东西是最小作用量原理/变分法,拉格朗日量以及Euler-Lagrange方程。这些在数学物理方法以及课程里都有讲,不再赘述。
当然,我在那些地方涉及到这个的时候也不会赘述
1. 有限元法推导
在这次Lab中,我们面对的是四面体网格,下面我们从头推导出完整的算法。
1.1. 计算线性插值形状函数
我们以四面体为考虑的单元,拿到的信息也是四面体顶点的信息,但是为了后面对整个体积求积分,我们必须先通过四面体顶点插值出四面体内部的点的性质。在这里我们采用简单的线性插值,系数也就是形状函数。
下面的推导全部默认向量为列向量。
我们设一个四面体在未形变时(也就是物质空间)四个顶点的坐标分别是X1...4,在物质空间下四面体内部一点X可以写成四个顶点的凸组合(下面使用了爱因斯坦求和约定):
X=ϕi(X)Xi
并且满足∑iϕi(X)=1
因此可以列出如下线性方程组:
[X11X21X31X41]⎣⎢⎢⎢⎡ϕ1(X)ϕ2(X)ϕ3(X)ϕ4(X)⎦⎥⎥⎥⎤=[X1]
即可解出所有的ϕi(X),我们不妨设N(X)=⎣⎢⎢⎢⎡ϕ1(X)ϕ2(X)ϕ3(X)ϕ4(X)⎦⎥⎥⎥⎤
或者我们作另一种表示,用来方便后续的计算,只采用3个变元ϕ
[X2−X1X3−X1X4−X1]⎣⎢⎡ϕ2(X)ϕ3(X)ϕ4(X)⎦⎥⎤=X−X1
而ϕ1可以由凸组合得到。这个写法的好处是便于求逆,也方便后续的求导计算。
1.2. 计算四面体动能
实际上我们的四面体是会动起来的,我们当然要用运动的观点看问题考虑形变以后的情况。不妨设物质空间下坐标为X的点现在在x(X),四个顶点此时的坐标为x1,x2,x3,x4,根据我们使用的线性插值,有x=ϕi(X)xi。
接下来我们计算任意时刻四面体的动能,四面体的形状可能变了,但是质量没有变,我们考虑对原始的四面体来分析。
对于原始四面体,我们的假设是其密度,为常数ρ。考虑物质空间下的一个小体元dΩ,其质量为ρdΩ,于是可以写出其动能为
dT=21ρvTvdΩ
整个四面体动能就是
T=21ρ∫ΩvTvdΩ
这里Ω是原始的四面体。
已知四个顶点的速度v1...4,我们使用线性插值计算四面体中每个点的速度,于是有
v(X)=[v1v2v3v4]⎣⎢⎢⎢⎡ϕ1(X)ϕ2(X)ϕ3(X)ϕ4(X)⎦⎥⎥⎥⎤=NT(X)V
其中N=⎣⎢⎢⎢⎡ϕ1(X)ϕ2(X)ϕ3(X)ϕ4(X)⎦⎥⎥⎥⎤,于是代入前面的积分就得到
T=21ρ∫ΩVTN(X)NT(X)VdΩ=21ρVT∫ΩN(X)NT(X)dΩV
我们能把V提出来,是因为V是个跟四面体内部点无关的量(V只和四个顶点的速度相关)。
ρ是参数,所以实际上我们只需要算∫ΩN(X)NT(X)dΩ即可,这就是爆算而已:因为已知四个顶点的坐标,根据点的坐标X即可算出N(X),进而可以算出积分,这种事情属于体力劳动,一般用符号计算软件解决。
1.3. 计算四面体势能
为了计算势能,我们引入形变梯度(可以参考连续介质力学笔记)以及应变能密度,首先考虑如何计算形变梯度F。
回忆F的定义:
F=∂X∂x
也就是说我们要计算∂X∂x,尴尬的是每次到这里我都会短路傻掉忘记怎么推。
我们已知
x(X)=x1+[−ϕ2(X)−ϕ3(X)−ϕ4(X)]x1+ϕ2(X)x2+ϕ3(X)x3+ϕ4(X)x4
=x1+[x1x2x3x4]⎣⎢⎢⎢⎡−ϕ2−ϕ3−ϕ4ϕ2ϕ3ϕ4⎦⎥⎥⎥⎤
=x1+[x1x2x3x4][−1−1−1I]⎣⎢⎡ϕ2ϕ3ϕ4⎦⎥⎤
=x1+[x1x2x3x4][−1−1−1I][X2−X1X3−X1X4−X1]−1(X−X1)
这样求导就很方便,直接可得
F=[x1x2x3x4][−1−1−1I][X2−X1X3−X1X4−X1]−1
如果用四个ϕ变元这里会很麻烦。这里好算的原因个人认为是由于自由度是3,用3个变量就刚好能算而且不多余。
我们可以发现这个形变梯度有个很好的性质,它和X无关,而且是线性映射,所以我们设 D=[−1−1−1I][X2−X1X3−X1X4−X1]−1,在任何时刻,用D乘以x1...4立刻就可以得到形变梯度,这个矩阵在后面十分有用。
对于应变能密度,我们可以自己推导,也可以选择已有的模型。当然,这并不是这个作业的重难点,因此选用对弹性体模拟非常适合的neo-hookean模型。于是有
V=∫Ωψ(F)dΩ
F我们前面算出来了,而且很幸运是个和X无关的量,也就是说形变张量在四面体内处处相同。所以我们要计算这个积分值,直接用四面体体积乘上应变能密度即可。
V=ψ(F)×vol(Ω)
1.4. 矩阵展开为向量
我们计算了单个四面体的这些那些,现在可以把他们组装起来成为一个四面体网格系统。
能量是可加的,因此可以计算总动能
T=21ρi=1∑nViT∫ΩiNi(X)NiT(X)dΩVi
到这里事情不太好做了,因为我们要处理一堆矩阵的乘法,如果用选择矩阵来表示V,由于V的列数和广义速度q˙的列数不一致,我们就不能这么做。实际上更麻烦的地方还在后面,等到对矩阵求导的时候求出来一堆高阶张量就不好收拾了。
这里的核心在于我们重新排列矩阵的元素,或者说将矩阵展平成向量。其实对于质点弹簧系统我们也做过这样的事情,只不过那个时候每个考虑的单元是2个顶点,这里是4个。
考虑重新定义V和N:
V=⎣⎢⎢⎢⎡v1v2v3v4⎦⎥⎥⎥⎤,N(X)=[ϕ1(X)Iϕ2(X)Iϕ3(X)Iϕ4(X)I]
同样的,我们表示四个顶点的x的时候,也把它们展成一个向量。我们这么做,前面的式子还是满足的,只不过有些矩阵的形状和内容要相应地修改一下。
比如说求微分用的矩阵D,原本D是需要乘上x1...4拼成的矩阵,现在需要乘上x1...4拼成的大向量,当然,我们输出的结果也要是一个大向量。
这个倒没啥难的,把D矩阵的元素合理分配一下就行。我们的F也展开成一个9维向量,因此我们实际需要的矩阵是一个9×12的矩阵
∂X∂x=⎣⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎢⎡D11D12D13000000000D11D12D13000000000D11D12D13D21D22D23000000000D21D22D23000000000D21D22D23D31D32D33000000000D31D32D33000000000D31D32D33D41D42D43000000000D41D42D43000000000D41D42D43⎦⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎥⎤⎣⎢⎢⎢⎡x1x2x3x4⎦⎥⎥⎥⎤
这玩意儿写出来感觉也没啥技巧。。。暴力展开也没啥问题,毕竟我们已经把x1...4组织成列向量了,去考虑矩阵每一个行向量和这个列向量的点积会比较方便。
为了方便后续的使用我们不妨称这个矩阵为Bi,这时来组装就很方便。
1.5. 组装
前面得到了式子:
T=21ρi=1∑nViT∫ΩiNi(X)NiT(X)dΩVi
按照套路,现在再来用选择矩阵的技术处理,首先记住V是个12维列向量。
我们的选择矩阵是一个12×3n的矩阵,它可以看成从上往下4个3×3n的子矩阵拼成,每一个大概长成这个样子:
[OOO...IO...O]
也就是说,除了该四面体的四个顶点对应的位置是三阶单位阵,其他的位置都是零矩阵。
然后事情就好说了,V=Siq˙,代入前面的求和式
T=21ρq˙Ti=1∑nSiT∫ΩiNi(X)NiT(X)dΩSiq˙
我们成功把q˙从里提取了出来,中间求和式里有了选择矩阵,意义就十分明确了,将积分得到的矩阵组装成一整个大矩阵就行,按照定义,组装成的大矩阵就是质量矩阵M。
也就是说:
M=i=1∑nSiT∫ΩiNi(X)NiT(X)dΩSi
计算T正是为了得到这个M。
然后是势能:
V=i=1∑nψ(Fi)×vol(Ωi)
为了数值求解微分方程,我们现在需要做的事情是求出势能对q的全微分(也就是负的广义力)。这里实际上会随q变化而变化的量只有F(因为vol(Ωi)是未形变时的体积),于是
∂q∂V=i=1∑nvol(Ωi)∂Fi∂ψ(Fi)∂q∂Fi
=i=1∑nvol(Ωi)∂Fi∂ψ(Fi)∂q∂Fi
∂Fi∂ψ(Fi)我们可以通过符号计算软件求出公式,因此我们实际上只用化简∂q∂Fi
∂q∂Fi=∂q∂(BiSiq)=SiTBiT
所以
∂q∂V=i=1∑nvol(Ωi)SiTBiT∂Fi∂ψ(Fi)
可以看到Si又出现了,所以这还是个组装过程。
我们还需要求出刚度矩阵,也就是V对q的二阶导
∂q2∂2V=i=1∑nvol(Ωi)SiTBiT∂Fi2∂2ψ(Fi)∂q∂Fi
=i=1∑nvol(Ωi)SiTBiT∂Fi2∂2ψ(Fi)BiSi
这样,必须的量我们就推——完——了!
2. 解微分方程
2.1. 转化为优化问题
使用有限元,我们把该求的量都求出来了,现在该考虑考虑怎么解微分方程了。
微分方程是
Mq¨=−∂q∂V
当然,为了方便,我们可以记f(q)=−∂q∂V
我们用白金之星隐式欧拉法来数值求解这个方程的话,写出来式子就是
Mq˙t+1=Mq˙t+f(qt+1)Δt
qt+1=qt+q˙t+1Δt
把下式代入上式,有
Mq˙t+1=Mq˙t+f(qt+q˙t+1Δt)Δt
这是关于q˙t+1 的一元方程,只需要求出它的根就好了,为了好写,设 v=q˙t+1。
这个方程不可以直接解算,下面我们将使用一种重要的手法来求它的数值解。
方程求零点问题往往可以转化为最优化问题,变成最优化问题我们就有很多好用的方法去求解,所以我们考虑对这个方程积分,来得到一个原函数,这样我们求出原函数的极值点,就相当于求出了方程的零点。
Mv−Mq˙t−f(qt+vΔt)Δt=0
对v积分得到
21(v−q˙t)M(v−q˙t)+V(qt+vΔt)
这里其实做了一步近似,就是把f外面的Δt看成∂v∂q然后运用链式法则,倒也没啥毛病。
所以说现在我们需要求的就是
v∗=vargmin[21(v−q˙t)M(v−q˙t)+V(qt+vΔt)]
不妨记上面这个优化目标为E。
2.2. Newton’s Method
如果不能够数值求解,那么几乎肯定只能慢慢迭代,我们首先选定一个方向d,然后沿着这个方向走一小步,不断循环直到我们发现结果几乎不再变化就可以停止了,我们就求出了近似的数值解。
如何选择这个方向呢?我们可以考虑优化目标下降最快的方向,也就是
d=d~argminE(vi+d~)
这个意思很明确,解释一下记号,假设我们现在处于第i+1轮迭代,第i轮迭代后我们处于vi。
但是实际上我们这是把自己绕进了另一个坑。。。另一个不好直接求解的优化问题,这就只能靠近似了,进行二阶展开。
d=argmin[E(vi)+giTd~+21d~THid~]=argmin[giTd~+21d~THid~]
这个要优化起来总算容易些了,先不考虑怎么算梯度和算Hessian矩阵,求个d总归是没问题的。
d=−Hi−1gi
这里其实已经能看出一些牛顿迭代法的味道了。
那么一个问题就是Hi和gi应该怎么算,回想一下E是我们积分求出来的。。。所以gi自然就是原来那个东西,也就是
gi=M(vi−q˙i)−f(qt+viΔt)Δt
进一步求导得Hessian矩阵
Hi=M−∂q∂f(qt+viΔt)Δt2=M+∂q2∂2V(qt+viΔt)Δt2
下一步是选择一个合适的步长α,然后更新vi+1=vi+αd,选择这个α我们还是可以变成一个优化问题
我们现在的目标是
α=α∗argminE(vi+α∗d)
这可以用一点简单的线搜索方法。
-
选择一个初始猜测值αmax
-
如果E(vi+αd)≤E(vi)+cgiTd或者α已经足够小
-
α=pα
当然,这里p,c以及我们认为α“足够小”都是参数也就是说最终还是逃不过调参的命运。
3. 用稀疏网格控制密集网格
最后一步,如果我们的模型网格非常细密,我们不可能继续用上面的方法,这样我们会遇到巨量的四面体,算不了。
所幸,四面体网格的方法在这方面体现了优势。我们可以对一个相对稀疏的网格进行物理模拟,然后运用线性插值的方法,把细密的网格运动给插值出来。
自然而然,我们会发现这个插值系数就是第一步计算出来的线性插值形状函数,所以我们只需要把这些所有的形状函数拼成一个超级巨大的稀疏矩阵,乘以我们计算出来的稀疏网格的广义坐标即可。
4. Coding
理解了上面的推导,写代码就几乎没什么实现上的难度其实上一个实验我也这么自信,但是不知道为何怎么写怎么炸。
真的有些麻烦的主要是那些需要符号计算软件进行求微分/积分的部分不过我懒得用符号计算软件所以直接借鉴别人的。