5.2流体更新,LBM方法纠错
距离之前的LBM方法已经过去了很长时间,后来在应用的时候发现我的方法有极大的错误。由于太想借鉴官方的模板,结果导致我没有真正区分NS与LBM的不同。不过犯错不要紧,根据实践论,本来就是一个认识、实践、再认识的循环过程。现在这篇就是实践再认识的总结阶段了。我还是本着通俗的语言去讲解,但这会有一个矛盾。数学中有句话说得好,“普适的代价是抽象”,那么通俗的讲,必然有着片面性,以及本人能力有限,如果有错误或者过于片面的地方也欢迎大家补充和指正。废话到此,直接开始吧。
版本,官方对于流体和Niagara有着比较大的改动。(虽然都快出了!)首先,就是HLSL有编译报错了,如果我没记错的话,你写HLSL如果写错了字母和忘记了分号,那是没有提醒的,只能极其折磨的一行一行的找,或者借助其他软件帮忙。现在好多了,有提醒哪里错了。再也没有Debug半天发现字母写错了的那种欲哭无泪的感觉了。
(资料图片仅供参考)
其次就是流体官方进行了整理,简洁了很多,没有版本动不动上百的Emitter变量,虽然现在还是很多。然后各种功能也进一步整合到Module里了,烟雾的光照模块删除了(我目前只看了3D烟雾,其他的不太了解)。
这次我按照官方的模板,用LBM模拟速度场,该有的功能都有。在时间间隔为的情况下,不同的分辨率下两者的在编辑器查看的耗时为(显卡是6G 2060):
由此可见,LBM确实比官方的要更加的省,但好像又没那么大的用,在我的预计中,最起码不说优化个十倍,七八倍总得有吧。现实往往是残酷的,不过也算是省了一些。当然,有可能是我代码写的不够好,等下拿出来给大伙瞧瞧有没有改进的地方。这也只是作为速度场的结果,本来我还想试试烟雾的渲染,看了下资料还涉及到LBM两相流的问题,我实在暂时没有太多的精力去深入了,这个先暂时放一下吧。速度场对于我接下来要做的事情,也够用了。
LBM的基本原理之前文章提到过,我这里就不再赘述了。我们具体看是如何实现的吧。之前犯的错误我也会提及的。
Emitter Spawn
以LBM开头的都是根据官方进行相关修改得到的,至于官方把之前杂七杂八的全部揉在一起,之前的文章也有所提及,这里就跳过不再赘述,也可以点进去看下,我只提及修改的部分,免得使文章过于冗杂。
LBM Gas Initial Emitter
从左侧就可以看出我删除了对于LBM不需要的Grid 3DCollection,由于LBM需要7个分布函数,分别对应粒子可以往七个方向跑。为了更直观,就用UE的坐标系,X轴正向为右,Y轴正向为前,Z轴正向为上。那么七个方向分别为前后左右上下,再加上一个呆在原地的。那为啥不能斜着跑了,因为性能不够,再加上斜着跑,估计开销得更上一层楼,有人感兴趣可以做下D3Q19或者D3Q27,也就是19个方向和27个方向。
注意别忘了把DistributionGrid的Num Attributes的数量设为7,不然你会得到一些奇奇怪怪的Bug。与其它通过英文单词去访问对应的Attributes不同,7个分布函数就用索引0、1、2、3、4、5、6来进行数据的读取写入。经过测试,如果你设置的Num Attributes为7,那么你一定要通过索引9(大于6)写入数据,那么你写的数据会被强制放在索引0中。
在ScalarGrid中我多加了个Force的属性,用来表示外力,比如风力等等。
LBM Gas Debug Display
这个就是Debug用的嘛,把枚举UI换成LBM的相关参数就可以了。
第一个就是我们Debug用的枚举了,第二个是用于向Grid 3DCollection读取写入数据用到的,一共就改了这两个枚举UI,就一起拿出来了。
以前这玩意可以直接搜索到,但现在好像只能直接拖进Module里。
然后选择红线的选项才可以在变量中搜到我们自定义的枚举。
Emitter Update
这部分没什么改变,相关功能之前文章提到过,就不赘述了。
接下来我想先提提一些其他的东西,我刚刚接触流体,看过很多大佬的文章,有些概念很难和实际(Niagara)联系起来,好歹也是一个理科生,我想其他美术相关的同学可能和我有同样的疑惑。如果看流体的相关文章,有一些相关的专业名词,甚至再来点张量,实在有些劝退。流体的文章中,有拉格朗日视角,欧拉视角,甚至半拉格朗日视角,开始搞得我一头雾水,其实就是描述流体运动的两种不同视角。这在Niagara中的也有所体现。拉格朗日视角最好理解,也是最直观的,就是把流体当作很多很多的粒子看待,这对平常我们做一些基础特效最为熟悉不过的了。欧拉视角更像是一个万能探测器,我只关心某一个空间位置,这个位置上流体的属性如何变化,把这个万能探测器放在某个点上,就可以得到这个点附近的流体属性(温度,密度,速度~~~)值。
在Niagara Simulation Stage中,对这两中视角最好的体现就是Iteration Stage的两种模式了,一种是Particle,相关计算是针对于每个粒子;一种就是Data Interface,而Data Interface我们经常用到的Grid 3D就是把空间分成一个一个小格子,计算是针对于每个小格子的。(版本还增加了一个Direct Set,这个暂时还没研究)。
Init Scalar/Sim Grids
这个功能没什么说的,就是初始化相关参数。
LBM SetFluidAttribute
这个模块就是根据官方修改的,换成上面所提及过的枚举变量就可以了。作用是向对应的Grid写入相关属性的数据,注意不要弄错对应的Grid名字,以及分布函数的数据读取写入直接用索引编号就行了。
接下来就是与LBM相关的东西了,我先把公式放上来。这次加了一些东西。
演化方程:
平衡分布函数:
宏观流体密度:
宏观流体速度:
外力项:
相关参数对应着什么,之前文章提到过就不赘述了,这次与之前不同的地方是,我还加入了外力项。LBM外力项的加入也有好几种方法,我用的是LBGK模型,还有其他例如He-Luo模型感兴趣也可以尝试一下,我只选择其中一种。(其中)
Init Distribution/Raster Scalar/Raster Velocity Grid
在初始化流体时,我处理的方法是把初始流体密度设为(方便计算),将速度等于0代入到平衡分布函数作为初始的分布函数。权重系数我进行重新计算,作为D3Q7模型的权重系数与之前做的不一样,我用的是1/4,1/8作为权重系数。
其它的初始值全部设为0就行了。
其实可以通过数值计算得到,如果没有外力的情况下,这样的初始值就是一个稳定态。
Splat Particle Data Into Raster Grids
这个模块就是接受另一个发射器传过来的数据。
Pre Sim Velocity
这个模块首先处理了边界问题,主要有两种边界,一种是开放性边界。一种是封闭边界。开放性边界我做了两种方法的处理,一种是周期性边界,从上面出去的流体会从下面出来;左边流出的流体从右边出来。还有一种到达边界了,直接让分布函数为0,视频演示的就是直接为0的这种。封闭边界很好理解,分布函数从哪里来回那里去,就是可以理解为反弹。这里也涉及交互的部分,就不展开了。
而后进行了一些可视化的处理,毕竟在3D空间去查看向量的值,确实需要一番功夫,官方已经帮我们做了。
再者就是数据的读取与写入了,还有各种外力。
LBM Get Fliud Attribute
这个和之前写入数据相对应嘛,有写入数据就有读取数据。也是根据官方改的,主要的核心我用图片展示出来了,没办法展示全部,直接一张图肯定密密麻麻看不清楚,如果分成小部分又要好多图。做视频太费时间了,图文的信息密度大些,也花不了我多少时间。矛盾!所以我就展示核心部分了,其他的照着葫芦画瓢很快就能弄出来的。
LBM IntegrateForces
这一部分就是把之前的外力全部加在一起了,相对于官方的我删除了速度,。然后写入Force到Scalar Grid上。
Sourcing
这一模块就是将另一个源发射器的数据,每帧写入到Grid中。还有就是速度向量的可视化我放在这里了。
LBM Gas Get Particle Data from Raster Grid
这一部分官方的做法首先获取速度,密度(温度)的值,与另一个发射器传过来的值进行处理(这种处理包括让两者Add或Max等)。这里我的处理我认为可能会有问题,这里大家自行判断。我的处理就是将另一个源发射器传递的速度全部作为外力处理,一般我们对外力比如场力这样处理很好理解,比如重力,电磁力,风力等等。但是对于另一个发射器传过来的速度作为外力可能确实有些牵强,首先它并不是作用于全部流体,速度作为力这样也比较不符合量纲。我只能强行给自己找个理由,传过来的速度理解为在极短的时间内的速度变化值,就是力的体现。毕竟速度本身就不是一个瞬时值,一张行驶的汽车照片你是无法得出它的速度的。
总之,我就修改了速度,只获取旁边发射器的数据。其他的密度(温度)没有修改。这一块是我最没有底的部分,为什么要这么处理了,因为简单啊。这一部分我再强调一边,不具有太多参考性吧。
Core LBM
到LBM的核心部分了,首先获取计算所需的外力Force,和初始宏观速度。这里的初始(这个初始为了避免歧义是相对初始的意思,从经验上看就是之前写入Grid的Velocity数据,比如在第一帧计算时,获取的是之前最开始速度初始值0;又或者是上一帧或者上一Simulation Stage的值,取决于实际情况,其实就盯着Grid里的数据如何变化就可以了。)代码我已经写好注释了,就不多说了,如果大家觉得哪里还有优化的地方可以提出来。说不定十倍性能优化就达成了。
这里之前所犯的错误之一就是将NS的平流项与LBM的迁移项没有理解清楚,无论是LBM的迁移还是NS的平流,可以说是对流体运动的阐释。我之前的做法就是生搬硬套,两个模块都有,这一看就是错误的。当然还有其他许多问题,比如权重系数的的选取,格子声速的选取等等。
这里还需要补充的地方是,对于开放边界,我有两个方法。目前是到达边界直接为分布函数直接为0,被注释掉的是周期性边界条件,感兴趣可以试试。
对于场景里与流体交互的东西,全部作为固体(或者说是封闭)边界来看待,那么后面对交互有一些额外的处理。比如球(此处假设球作为交互物体)以一定速度向左运动,那么在这一帧里,就判断有哪些格子的右侧是球。那么该格子的速度的水平(左右方向上)分量是与球的速度一样,注意是分量。比如某个格子经过计算得到的速度为(0,0,1),即此刻的速度是向上的,它的右边是球,而此球以(-1,0,0)速度向左运动,那么格子最终速度是向左上方运动。差不多就是这个意思。 至于球内部,速度就直接是球的速度了。
还有一个棘手的问题,这个也折磨了我很长时间。就是LBM的速度不能太大,否则分分钟就发散给你看。真是一点道理也不讲。这个我看了下文章,有的就是加一个判断,如果速度对于某一个值就按照速度的大小成比例减少。这我试了一下,有用,但还是不够稳定。最后我选择在计算完平衡函数后,把平衡函数限制在(0,1)之间,就变得稳定多了。其实这个也蛮好理解的,如果不考虑密度的变化(也就是流体不可压缩的情况下),平衡分布函数是说明什么,在一个格子内有多少比例粒子往某个方向跑,那么无论格子速度是多离谱多大,平衡分布函数也是大于0的,只能无限接近0。那么反之也成立,也不可能超过1,超过粒子总数,也是只能无限接近1。
在处理这个问题也解答我另一个问题了,LBM是一种统计方法,对于流体,哪怕格子分的再小(计算机承受范围内),实际在一个格子中的流体粒子也是极其多的。之前我在想为什么只能往隔壁格子跑了,假设其中一个粒子有着很大很大的向左运动速度,不能直接跨一个格子传递嘛。这个时候,我就忽略了统计的思想,哪怕真的某个粒子具有极大的向左速度,也会在运动过程中遭受其它粒子的阻碍,碰撞(或者称之相互作用更加准确),那么它在单位时间跨一个甚至多个格子的概率是基本不可能发生的。像极了被生活磨平了棱角,但是这并不意味着毫无意义,此粒子向左的趋势并没有消失,而是在相互作用中,使得更多的粒子具有向左的趋势。在平衡分布函数的表现上,即向左的平衡分布函数的值会增加。
这些也只是通过经典的牛顿视角去解释LBM方法,只是我自己的理解 ,希望大家辩证的看待(就是==如有错误,概不负责,只是作为一个参考,疯狂叠甲)。
后面就是写入数据的过程,没什么好说的。
LBM InnerRepel
这个模块是为了美术效果后面加上去的。 具体的效果就是交互的物体内部,此效果只发生在物体内部。基于空间位置得得到一个向量,向量的方向是表现为远离球心,大小就是距离球心越近,值就越大。就是将交互物体内部的东西排斥出去嘛,没什么稀奇的。
RGBA这个是已经偏离了它命名的含义了,这个就是用作于向SimRT传入数据的桥梁。
最后输出的值为速度加上InnerRepelForce,然后再乘以一个系数,用来调节整体的大小。
Post Sim Scalars
这个模块首先获得上面提到过的RGBA值,传递给SimRT里面,然后还需要一张RT存储存储风场的一些信息,这个主要作用是用于坐标系的转化。
这个详细在下篇文章再讲吧,涉及的东西有点多。其实很久之前就对Niagara的旋转和一些变换想系统总结一下。之前看到四元数,旋转向量,矩阵,欧拉角,旋转角度以及各种坐标系变换就一头雾水,似懂非懂。而且官方用的实例也蛮多的,这个我先放放,看实际用的多不,再考虑是否系统讲讲。
好了LBM就暂时告一段落了。