Simulation Stages
显然之前的Particle Update和Emitter Update都是只执行一次的,但Simulation Stages(好长,后文简称模拟阶段好了)可以进行迭代,相当于Houdini的Solver,官方案例说它像一个For Loop。 这东西非常强大,不仅可以进行迭代式的解算,还可以用来制作有迭代过程的后处理效果,非常恐怖。这东西相当于一个Compute Shader了。
在UE4.26时,模拟阶段还是实验性功能,需要在Emitter Properties里面手动开启Enable Simulation Stages。
设置好之后还要开启GPU模拟,否则会报错(至少4.26会)。
相当于一个二维的Buffer,用来储存二维的各种数据,需要在Emitter Spawn阶段使用Set Parameter方法创建。网格的每个点可以成为Cell,而后我们可以在模拟阶段对Grid进行读写。
恐怖的是,Niagara还能生成RT,RT需要在Emitter Spawn阶段使用Set Parameter方法创建。
此过程如下,数据流向(不准确的描述)为Texture->Grid2D->RT。

先对Grid进行读取,然后使用当前Grid的index作为参数填充RT。
由此可见,Grid2D和RT在形式上是高度一致的,上图根本没有进行转换。
Linear在Grid2D或3D的语境下,都是指当前cell的一维的index,一维的index不太直观,因此都有Linear To Index的方法,将一维的index转换为三维的Index。
可以创建一个拥有Texture Object变量的材质作为Sprite Material,而后在Material Parameter Bindings里面添加一个变量的绑定,将Texture Object设置为我们的RT。

基于哈希表的三维网格划分。使用这个主要用来加速需要对周围粒子进行交互的模拟阶段模块。目前也只支持GPU模拟。 相关内容比Grid2D复杂,学习基础可以看这篇文章。
在官方示例中,官方使用hlsl节点计算了从模拟坐标系到单位坐标系下的转换矩阵,此转换矩阵中使用来0.5的偏移,这是因为单位坐标系是以大网格的左下角为坐标原点的。
因此构造将粒子从模拟坐标系转换到单位坐标系的转换矩阵的HLSL代码如下,其中Scale是大网格大小的倒数。
OutMatrix[0][0] = Scale.x;
OutMatrix[1][1] = Scale.y;
OutMatrix[2][2] = Scale.z;
OutMatrix[3][3] = 1.0f;
OutMatrix[1][0] = 0.0f;
OutMatrix[2][0] = 0.0f;
OutMatrix[3][0] = .5f;
OutMatrix[0][1] = 0.0f;
OutMatrix[2][1] = 0.0f;
OutMatrix[3][1] =.5f;
OutMatrix[0][2] = 0.0f;
OutMatrix[1][2] = 0.0f;
OutMatrix[3][2] = .5f;
OutMatrix[0][3] = 0.0f;
OutMatrix[1][3] = 0.0f;
OutMatrix[2][3] = .5f;
但我完全不懂为什么会有OutMatrix[2][3] = .5f这一行。
主要要进行以下的HLSL处理,其中Position是粒子位置、SimulationToUnit是从模拟坐标系到单位坐标系的转换矩阵、ExecIndex是当前粒子的index。
AddedToGrid = false;
#if GPU_SIMULATION
// 将粒子从模拟空间转换到单位空间
float3 UnitPos;
NeighborGrid.SimulationToUnit(Position, SimulationToUnit, UnitPos);
// 将单位空间的位置转换到对应的小网格的index(按照位置和小网格的大小进行计算,即使不存在这样index的小网格)
int3 Index;
NeighborGrid.UnitToIndex(UnitPos, Index.x,Index.y,Index.z);
// 获取小网格总数
int3 NumCells;
NeighborGrid.GetNumCells(NumCells.x, NumCells.y, NumCells.z);
// 如果当前粒子处在大网格中,需要注意index从零开始
if (Index.x >= 0 && Index.x < NumCells.x &&
Index.y >= 0 && Index.y < NumCells.y &&
Index.z >= 0 && Index.z < NumCells.z)
{
// 将index转换为linear
int LinearIndex;
NeighborGrid.IndexToLinear(Index.x, Index.y, Index.z, LinearIndex);
// 获取粒子所在小网格之前的已有粒子数量,注意neighbor在当前语境下表示粒子
int PreviousNeighborCount;
NeighborGrid.SetParticleNeighborCount(LinearIndex, 1, PreviousNeighborCount);
// 获取小网格最大可记录的粒子数
int MaxNeighborsPerCell;
NeighborGrid.MaxNeighborsPerCell(MaxNeighborsPerCell);
// 如果之前的粒子数小雨最大粒子数
if (PreviousNeighborCount < MaxNeighborsPerCell)
{
// 添加到网格
AddedToGrid = true;
// 求出储存位置的下标
int NeighborGridLinear;
NeighborGrid.NeighborGridIndexToLinear(Index.x, Index.y, Index.z, PreviousNeighborCount, NeighborGridLinear);
// 将粒子index进行储存
int IGNORE;
NeighborGrid.SetParticleNeighbor(NeighborGridLinear, ExecIndex, IGNORE);
}
}
#endif
上面的两段代码其实都是非常general的,都可以直接抄,抄抄你的。 (其实完全可以写到一个Niagara Module Script里复用。)
为做区分,将前文中已经储存的点称之为大点,现在要进行运算的点称为小点,我们现在要寻找里小点最近的大点。 首先是将小点也进行网格划分,查找自己属于哪一个小网格,然和查找小网格中已经记录的大点的index,然后再使用AttributeReader即可读读取大点的属性数据了。完整HLSL代码如下:
NeighborIndex = -1;
#if GPU_SIMULATION
bool Valid;
// 转换到单位坐标系
float3 UnitPos;
NeighborGrid.SimulationToUnit(Position, SimulationToUnit, UnitPos);
// 查找对应的小网格
int3 Index;
NeighborGrid.UnitToIndex(UnitPos, Index.x,Index.y,Index.z);
// Initialize the closest distance to a really large number
float neighbordist = 3.4e+38;
// 获取对应小网格的最大粒子数,而后遍历
int MaxNeighborsPerCell;
NeighborGrid.MaxNeighborsPerCell(MaxNeighborsPerCell);
for (int i = 0; i < MaxNeighborsPerCell; ++i)
{
// 查找大粒子的index
int NeighborLinearIndex;
NeighborGrid.NeighborGridIndexToLinear(Index.x, Index.y, Index.z, i, NeighborLinearIndex);
// 不知道在做什么转换,总之copy
int CurrNeighborIdx;
NeighborGrid.GetParticleNeighbor(NeighborLinearIndex, CurrNeighborIdx);
// 如果合法
if (CurrNeighborIdx != -1)
{
// 使用AttributeReader读取属性数据
float3 NeighborPos;
AttributeReader.GetVectorByIndex<Attribute="Position">(CurrNeighborIdx, Valid, NeighborPos);
// Compare the distance found maintaining the closest
const float3 delta = Position - NeighborPos;
const float dist = length(delta);
if( dist < neighbordist )
{
neighbordist = dist;
NeighborIndex = CurrNeighborIdx;
}
}
}
#endif
可见真正起作用的部分就是if (CurrNeighborIdx != -1)之内的部分。
让我们算一算问题的规模,假设大点有10,000个,小点有1,000,000个,每个小网格最多有100个粒子,那么原来问题的规模为10,000×10,000,000=100,000,000,000,而使用Neighbor Grid3D后的问题规模为10,000+10,000,000×100=1,000,010,000,少了两位数,可见大点数量越多则效率越高。
用时间复杂度来说,大点B个小点S个每个小网格最多N个粒子,则前者规模为