粒子效果
存在类似于框架一样的东西,将空间分为了8块。在大约1s的时间间隔下,粒子会随机出现在着八个格子内,并且在有粒子的每个格子中,又存在一个较大的Sprite粒子和多个较小的Sprite粒子,他们共享着相同的颜色。
Niagara蓝图部分
本系列开始介绍并着重研究一个新的结构——Neighbor Grid 3D,这是一种基于位置的哈希查找结构。
哈希表是什么?简单来讲就是通过键与哈希函数来为对应的数据分配存储位置。哈希表的优势是什么?快速查找。Neighbor Grid 3D即是希望起到这样的作用。它的使用中随时伴随着各种各样的“哈希函数”来进行空间分配和查找。
最后还需要注意一点,即蓝图中透露现在Neighbor Grid 3D仅限于GPU粒子。
回到蓝图中,其中有三个发射器,Grid_Visualizer是负责将Grid 3D显示出来,Grid_Write和Grid_Read是分别负责对Neighbor Grid 3D进行写入和读取的发射器。此外,因为Neighbor Grid 3D需要被两个多个发射器访问,这里将其定义成系统变量(位于System Update里,意味着每一帧都会发生创建和销毁操作,两者中间又发生着访问、渲染等显示相关的操作,由Emitter决定)。
效果实现分析
我们分别来看三个粒子发射器
Grid_Visualizer——显示Grid 3D
这个发射器和Neighbor Grid 3D的使用关系并不大,更多的是为了将Grid的边界显示出来,作以辅助理解之用。我们这里简要介绍其中的模块。
首先定义了两个Emitter级的参数——GridSize和GridResolution,分别用来控制绘制的Grid的大小(150x150x150,即边长为150的立方体)和分辨率(2x2x2,即总共8个格子)。
Emitter Update部分,使用的是Spawn Burst Instantaneous,其中的数目是自定义的动态输入模块GridVisualizerCount。
GridVisualizerCount中的运算,换句话说其实就是6x9,6个面,每个面上9个粒子。
Particle Spawn阶段的Grid Visualizer Location模块写入了每个粒子的位置Position和RibbonID两个属性,用以定义条带Ribbon渲染器中渲染的顺序(即哪个粒子连接哪个粒子)。
Grid_Write——写入Neighbor Grid 3D
这个发射器是负责生成立方体内的那些大粒子,并将一些信息写入了Grid 3D中,其中写入信息是在Simulation Stage里完成的。我们重点看这个自定义模块的内容。
模块主要是读粒子的位置,并将Execution Index数据写入到Grid中。具体来说,主要看其中的两端HLSL代码片段。
其中World BBox Size是之前定义好的。
左侧较小的HLSL代码片段中,输入Scale是一个Vector类型,输入的值是Neighbor Grid 3D大小的倒数;输出OutMatrix是一个Matrix类型。
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;
其实就是用Scale构建了一个矩阵
O
u
t
M
a
t
r
i
x
=
(
1
/
150
0
0
0
0
1
/
150
0
0
0
0
1
/
150
0.5
0.5
0.5
0.5
1
)
\mathbf{OutMatrix} = \begin{pmatrix} 1/150 & 0 & 0 & 0 \\ 0 & 1/150 & 0 & 0\\ 0 & 0 & 1/150 & 0.5\\ 0.5 & 0.5 & 0.5 & 1\\ \end{pmatrix}\\
OutMatrix=⎝⎜⎜⎛1/150000.501/15000.5001/1500.5000.51⎠⎟⎟⎞
较大的HLSL代码片段中,输入有NeighborGrid,即我们读取的Neighbor Grid 3D,Position是粒子的Position,SimulationToUnit是上面的OutMatrix矩阵,ExecIndex是当前粒子的索引。具体解读见代码注释
AddedToGrid = false;
#if GPU_SIMULATION
//求出粒子的位置转化为的在Grid 3D中的位置(单位位置,相当于世界空间转局部空间,再将局部坐标做一个归一化)
// Derive the Neighbor Grid Index from the world position
float3 UnitPos;
NeighborGrid.SimulationToUnit(Position, SimulationToUnit, UnitPos);
//将粒子的单位位置转化为所在格子的索引
int3 Index;
NeighborGrid.UnitToIndex(UnitPos, Index.x,Index.y,Index.z);
// Verify that the derived index is valid.
int3 NumCells;
NeighborGrid.GetNumCells(NumCells.x, NumCells.y, NumCells.z);
//确保求出的索引不超过前面设定格子总数(在三个维度上)
if (Index.x >= 0 && Index.x < NumCells.x &&
Index.y >= 0 && Index.y < NumCells.y &&
Index.z >= 0 && Index.z < NumCells.z)
{
//将三维的索引转化为1维索引(目的都是为了用索引确定唯一的一个格子)
int LinearIndex;
NeighborGrid.IndexToLinear(Index.x, Index.y, Index.z, LinearIndex);
//当前该索引下的格子所拥有的属性之一Neighbor Count自增1表示,格子自生所能容纳的信息数+1(Neighbor即是信息的容器,在本例中,可以简单得理解为大粒子,即一个格子中有几个大粒子,就存了几个Neighbor)。
// Increment the neighbor count for this cell. This records the number of overlaps
// and can return a higher count than the MaxNeighborsPerCell
int PreviousNeighborCount;
NeighborGrid.SetParticleNeighborCount(LinearIndex, 1, PreviousNeighborCount);
//每个格子所能存储的最多信息数,是我们自己设定好的
int MaxNeighborsPerCell;
NeighborGrid.MaxNeighborsPerCell(MaxNeighborsPerCell);
//当我们增加的Neighbor Count超过了所能容纳的最大值时,就不再继续往里塞东西了。
// Limit the number of neighbors added to each cell
if (PreviousNeighborCount < MaxNeighborsPerCell)
{
AddedToGrid = true;
//此时,我们可以通过Index找到格子,但是怎么找到某一个存进去的Neighbor,所以需要给这个新人Neighbor一个Index索引(一维的)
int NeighborGridLinear;
NeighborGrid.NeighborGridIndexToLinear(Index.x, Index.y, Index.z, PreviousNeighborCount, NeighborGridLinear);
//本例中,是将ExecIndex信息,存入了Neighbor
int IGNORE;
NeighborGrid.SetParticleNeighbor(NeighborGridLinear, ExecIndex, IGNORE);
}
}
#endif
迭代的结果就是,Grid_Write这帧的这些粒子被分别写入了不同的格子当中(其中如果有某个格子满了,就不再往那个格子里塞了)。
是不是看得有点懵。我们模拟一下,现在要弄清楚靠前面的左上角的格子存了一个什么信息,怎么查?首先搞清楚这个格子的索引,假如是1,那么要继续在这个索引是1的格子内部看看它存了多少信息,打开一看Neighbor Count是2,是2个Neighbor信息,再仔细看看索引是?的Neighbor里放的是什么,哇,原来是个ExecIndex,是某个粒子的执行索引啊。那么我们拿到这个执行索引可以干嘛呢?这就是后面Grid_Read里完成的工作了。
Grid_Read
对Neighbor Grid 3D的读取还伴随着粒子属性的读取(Particle Attribute Reader),最终通过Sumulation Stage : Query Grid来完成整合。其中主要是两个自定模块——Find Closest Neighbor和Copy Color。
Find Closest Neighbor做的事情就是找到距离当前粒子最近的那个格子所存储的Neighbor中的那个ExecIndex。
NeighborIndex = -1;
#if GPU_SIMULATION
bool Valid;
//找到粒子所在的格子的索引
// Derive the Neighbor Grid Index from the world position
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;
// loop over all neighbors in this cell
int MaxNeighborsPerCell;
NeighborGrid.MaxNeighborsPerCell(MaxNeighborsPerCell);
//遍历找到的格子中所有的Neighbor
for (int i = 0; i < MaxNeighborsPerCell; ++i)
{
// Find the ExecIndex for the current neighbor particle
//从0到最大,挨个翻开来看里面是不是存了东西
int NeighborLinearIndex;
NeighborGrid.NeighborGridIndexToLinear(Index.x, Index.y, Index.z, i, NeighborLinearIndex);
//把里面村的东西拿出来赋予到当前变量上
int CurrNeighborIdx;
NeighborGrid.GetParticleNeighbor(NeighborLinearIndex, CurrNeighborIdx);
// Only proceed if the returned index is valid. This is most often triggered
// by there being fewer neighbors in the cell than the MaxNeighborsPerCell limit.
if (CurrNeighborIdx != -1)
{
// Use the Attribute Reader to query the position of the neighbor particle
float3 NeighborPos;
//通过Attribute Reader以及拿到的ExecIndex来查询里面粒子的位置,写入到上面的变量里
AttributeReader.GetVectorByIndex<Attribute="Position">(CurrNeighborIdx, Valid, NeighborPos);
// Compare the distance found maintaining the closest
const float3 delta = Position - NeighborPos;
const float dist = length(delta);
//跌打查找距离当前粒子最近的那个存入的Neighbor粒子
if( dist < neighbordist )
{
neighbordist = dist;
NeighborIndex = CurrNeighborIdx;
}
}
}
#endif
这个模块输出一个NeighborIndex,在上面的代码中,已经用其进行了一次位置查找,后面的Copy Color其实是一样的,是做了一次颜色查找,过程更为简单。
总结
本例已经是涉及到一些高级的数据结构,和代码语言,单单看表层代码已经很难去理解代码片段的功能了。所以此次的话借助了源码,即通过联系代码中的上下文(包括代码和注释),找到函数的用途和功能,从而帮助理解。