在Niagara中基于LBM优化流体性能(下)

悲催,本章开头就要纠正上篇文章的小错误。上篇文章提到梯度算子,这个说法并不准确。应该叫做哈密顿算子或者nabla算子。至于梯度、散度和旋度是另一种概念。由此可见,本篇文章也仅作为参考,因为很多都是我极其主观的理解。尤其是接下来涉及官方流体的示例,欢迎大家一起交流。
打开官方示例(需要在项目中启用流体插件才能看到),一条一条往下看。看到最后,单独来看每一条感觉好像懂了。但连起来还是不懂,于是又看一遍还是不懂。然后闲着没事,数了下左边的变量数目。最多的Emitter命名空间就有200多个!这么多变量来回捯饬,弄得我头昏脑胀。没办法,只能用其他方法来帮助自己理解。

这个思维导图其实只是我理清关系用的,在后面并没有多大的用,没必要去细看。在之前的研究中,虽然没有弄明白整体的思路。但能感觉到重心是几个官方定义的Grid3D Collection。
这样就能够大致了解每个Grid的功能了。SimGrid主要涉及到模拟部分;TransientGrid与边界有关;PressureGrid与求解压力梯度有关;LightingGrid与光照有关;ScalarGrid主要用来传递参数;Temporary与计算旋度与散度有关;RasterGrid与旁边的另一个发射器有关。
我们还需要在开始研究发射器之前再回顾一些基本知识。我觉得自己描述的不够准确,请上GPT为我们讲解。Grid3D Collection是一种数据结构,用于存储在三维网格中的粒子数据。它可以看作是一个三维的数组,每个元素(或单元格)都可以存储一个或多个粒子。Grid3D Collection可以用于各种效果,例如体积渲染、流体模拟等。错了不要怪我,甩锅给AI。在这里,一定要和之前的粒子渲染区分开来,由于之前我一直把自己的思维拘束在粒子上。导致写代码时的核心逻辑出现巨大偏差。对于粒子渲染,计算针对的是每一个粒子;而在流体渲染中,计算针对的是模拟的一个个小方格或者小方块。尽管有从其他发射器获取粒子数据,但这是不同的。
Simulation Stages这里可以看看知乎大佬Feilon的相关文章,我就简单提下。普通情况下,我们需要执行两个操作A和B。对于粒子或者Grid Collection来说,A,B它们有的先被计算,有的后被计算完成。但是在流体中,我们需要所有对象A执行完了,B需要用到A中得到数据,才能去执行B操作,这个时候就需要用到Simulation Stages了。
官方设置了这么多的Grid3D Collection,主要的目的有两个。一是模拟和渲染的网格分辨率是不同的;二是数据分组可以避免不必要的复制操作带来的性能损耗。
每一个模块我只是简单提一下它有啥子用,不然深入讲的话,我整个五一就没有了,而且有些模块我自己也懒得深入研究,等要用到的时候,再去研究。


Grid 3D LBM CONTROLS SPAWN
Grid 3D LBM SCALABILITY SPAWN
这两个模块和官方是一致的,只是稍微调整了下。主要的目的是将模拟需要调整的参数集成在一起。这样调整参数,就不用到处找了。
然后就是创建SimGrid、TransientGrid、DistributionGrid、LightingGrid、ScalarGrid这几个Grid3D Collection,其中DistributionGrid是储存分布函数用的。
Grid 3D Set Resolution
接下来就是设置各个Grid3D Collection的分辨率了,这里注意一下分辨率官方是如何设置的。ScalarGrid是用户手动设置的,LightingGrid、SimGrid,在继承ScalarGrid的分辨率的同时还可以用参数手动调节自身的分辨率。
Grid 3D Modify Grid Scale
在这里进行的就是空间的离散化了,数学表达式的dx就是来自这里。但这里还多了几个额外的dx_render等,这也是和上面提到的渲染和模拟的分辨率是不同的有关。有了更多的可操作性。

LBM Create Grid Attribute Indices
这里就是创建属性的地方,比如在一个网格中,我存了一些信息,速度,颜色。怎么才能让程序知道那个数据指的是速度,那个数据指的是颜色。这个时候就是通过属性索引来让程序分辨了。在这里官方的命名具有迷惑性,在其他网格创建的索引最后以SimGrid作为前缀命名。当然,这只是看待问题的角度不同导致的,并无对错之分。之前提到过,我们采用D3Q7模型,所以需要7个索引来存储分布函数。对应的属性数据类型是1个Float,2个Vector。其他的没有太多变化。
往下看也是创建属性,并且规定重力加速度g的数值。

我们把算好的数据需要传递到Render Target Volume里进行渲染,注意这里需要设置正确的浮点数精度。
Grid 3D Init RT
这个就是初始化上面RT的大小,这里是根据ScalarGrid来进行设置的。
下面一系列操作和上面一样,只不过操作的对象不一样罢了,也没有新玩意出现,考虑文章篇幅,就不赘述了。

最后两个一个是设置Particle Attribute Reader,这个就是指向旁边发射器的。我们每帧需要从哪里获得源信息嘛。举个例子,旁边的发射器就是设置燃料如何运动,至于燃料产生的烟雾,燃烧的火焰如何运动就是这个发射器做的事。
还有一个是Debug用的,不要小看Debug了!!!如此多的模块。做的时候,一个字母打错了,一个逻辑没想明白。系统就不会正常运行,这个时候没有做好Debug,想要解决问题,难度极其极其之大。虽然为了篇幅原因,我自己也偷懒不会过多介绍这方面内容,但这一部分说是整个发射器最重要的部分也不为过。大家可以仔细研究学习。


LBM GAS CONTROLS UPDATE
Grid 3D GAS SCALABILITY UPDATE
这个我改来改去还是和官方一样的,忽略命名不同。主要就是调节流体的美术效果用的。
Sourcing Comp Modes

这里的CompModes是与下面Sourcing的Simulation Stages搭配使用,就是处理速度,温度与密度的值。要三种方法可选,直接相加,取最大值,和一个给定阈值取其中之一,不同方式会有不同的美术表现。
接下来就是时间的离散化了,dt。官方有两个可选,一个就是自己设置;一个用系统的DeltaTime。我就没搞那么复杂,直接用系统的,然后乘以一个变量方便控制。
Grid 3D Create Unit to World Transform
进行坐标转换,涉及到常用的世界空间,本地空间;以及对应Grid3D的坐标空间。这些都是方便数学计算用的。然后赋值给变量,后面可以直接使用。
Grid 3D Set Bounds
设置模拟空间的边界。
Grid 3D Rigid Mesh Find Colliders
根据User传递来的模型,进行碰撞体的查找。防止流体拖进场景就产生交互,官方设置了Tag,只有名为collider的Tag才会产生交互。


这个Position是与Mesh Render里的Position Binding联系起来的,官方直接设置的GridCenterPosition。但是疑似由于官方引入Position类型用于区分Vector导致我怎么也无法将GridCenterPosition直接绑定到Position上,只好这样解决了。



接下来就是一系列初始化Grid3D Collection。这里首先注意子命名空间的用法,就是指明变量向那个Grid3D Collection写入数据,这里写自定义的时候,要注意写法首字母大写,不要有任何其他的符号,包括空格。我在这里也卡很长一段时间。最后发现自己多打了一个空格才拖不进去,内心有些小波动。
迭代源是数据接口,和之前提到计算对象是Grid3D中的每一个格子而不是粒子联系起来了。当涉及数据写入的时候,一定要确定写入的对象和数据接口是一致的,像上面的DistributionGrid就不能换成其他Grid。我测试了一下,涉及数据读取倒没有非常大限制只要有数据就行。执行的行为,只要涉及初始化的选择在模拟重置时执行(On Simulation Reset)就可以了。后面其他的模拟阶段选择Not On Simulation Reset后面我就不再重复提及了。
Grid 3D Gas Particle Scatter Source
这里就是从旁边的发射器获得数据,并且将数据传到RasterGrid里了,这个里面有些复杂,可以多花点时间看看如何传递数据的。


接下来就是和官方示例有不同的地方了,官方在这里初始化TemporaryGrid然后计算了旋度。由于使用LBM,所以我们不需要,直接跳过!

我们直接开始模拟速度的预处理,Pre Sim Velocity。
Grid 3D Resample Float
从ScalarGrid获得密度,温度数据,给到临时类型变量。这种类型的变量正如名字一样,转瞬即逝,每一帧都可以不一样。
Grid 3D Compute Boundary
这个计算边界的,里面好复杂。我大概看了下,最后输出的Boundary。0代表流体单元,1代表固体单元,2代表空单元。
下面那个是与固体速度有关的,跳过跳过,浪费性能。
接下来一大堆节点都是计算外力的影响,比较有意思的是计算RadialForce,这个力的方向是格子与中心(默认0,0,0)的连线方向是一致的。这一部分就解决了流体方程关于外力的影响,后面计算的时候,就不要想着,唉,我重力还没算了。因为都在这个部分解决了。最后算好的速度通过变量写入到SimGrid中。


首先获得速度,而后将值给到临时变量里,这里如果开启Debug,值就会变为0。然后采样RasterGrid所有属性,全部拿来吧你。在赋值给变量的时候,有不同的操作。
在Grid3D中,数据的写入和读取是比较特别的,读取的是初始值,或者理解为上一时刻,上一帧的值。经过一系列的计算,我们写入的是修改后的值,我们不能在同一个Simulation Stages里读取我们修改后的值。这是我比较主观的理解,如有理解偏差,希望大家指出来。
所以可以看到速度,密度和温度是采用之前提到过的ADD模式叠加来的,而Alpha和RGB采用其它方式处理的。
Grid 3D Gas Init 没用到,跳过。
然后从TransientGrid获得Boundary信息,最后写入ScalarGrid的值都要和Boundary进行一次判断,如果是0即流体单元才传值,否则就是0。

这里是对速度进行平流,一般来说,对于流体格子。我们考虑格子里面的流体要到哪里去,但这样在数学上会出现问题。所以我们考虑当前的流体从哪里来,即逆向思考。如果不好理解的话,可以简化问题考虑最简单的情况然后用数值计算帮助理解。后面的Grid 3D Set Value说实话这一块我也不太清楚为什么这样做,官方写的注释也只是提到为解算做准备。希望大佬能指出来。好在并不影响其他东西,这个先暂时搁置一下。


在求解之前,官方又计算了一次散度。我们不用,直接跳过。
而后官方就是进行了压力求解,而且还算了6遍,LBM在这里也只是算一遍。简单提一下官方的求解吧。一般来说,求解压力我们可以想象数值不断地向正确值逼近,每算一次值会向正确值逼近一点。每次前进的距离是有限的,我们当然希望它逼近的速度快一些,所以就有了SOR方法,引入权重,这个权重会随着每次计算还不断改变。权重的初始值的选取也不能太离谱,否则会离准确值越来越远,导致不能收敛。在此基础上,还用了红黑高斯-塞德尔迭代解法进一步加快速度。这个方法我还没详细研究,就不多提了。
至此,就可以感受到在计算上有多大的提升了。


首先从SimGrid获得速度与密度,其实密度到后面没有用到。之后会详细解释的。
终于到LBM了,再回顾一下重要式子。
平衡分布函数:
碰撞后的分布函数:
与宏观的联系:



我把代码尽量写好注释。
其它的和官方的没什么不同。
这里还需要注意权重和平衡函数的计算,权重如何取值。涉及到公式的推导,我试着推了一遍,可始终有个2没有消掉,只能拿出结论来用。这个部分可以看书。上面的代码是按照格子速度与声速的平方比为4算的。如果按照3来算,把数据带进去我看了下好像肉眼看不出来有啥子区别。平衡函数这里我直接把密度变为了常数1,一开始这里本是Density,但是运行起来总是一开始有流体,后面就消失了。当时这里卡了很长时间,后来想想,density变为常数1。举个例子,一个墨水瓶掉进湖里,里面墨水经过一段时间肉眼就看不到了,常数1意味着违背了物质守恒,这个墨水瓶源源不断的冒出墨水,无穷无尽,所以始终看得到墨水。



得到碰撞后的分布函数就可以得到我们想要的流体的宏观速度了。

其他的和官方一样了,只需要把得到的速度(OutVelocity)和官方的压力梯度替换就行了。
Dissipate Vector(Float)
控制变量更快的消散(趋向于0)


后面我没有进行任何修改,只是替换了自己改的模块的变量。后面就是对Scalar中的温度和密度进行平流,还有光照的相关处理,最后把结果给到RT上。


材质这一块就是和官方一样的,官方将材质里的变量和Niagara中绑定到一起。然后材质中比较重要的是一个宏,MARCH_SINGLE_RAY_EMISSION
,该宏可能是一个体积光线追踪的方法,这里我就没有往下研究了,用到了再去深入吧。
最后提一下如何调节美术表现吧,这里可以了解蜡烛燃烧的过程。首先最上面白烟或者黑烟的是没有燃烧充分的燃料,这个颜色可以通过线性颜色重采样。中间的黄色部分是黑体辐射,这个也可以用线性颜色重采样。最下面的蓝色部分是电子跃迁产生的,好像不用考虑。