Simulation Stages

显然之前的Particle Update和Emitter Update都是只执行一次的,但Simulation Stages(好长,后文简称模拟阶段好了)可以进行迭代,相当于Houdini的Solver,官方案例说它像一个For Loop。 这东西非常强大,不仅可以进行迭代式的解算,还可以用来制作有迭代过程的后处理效果,非常恐怖。这东西相当于一个Compute Shader了。

开始之前

在UE4.26时,模拟阶段还是实验性功能,需要在Emitter Properties里面手动开启Enable Simulation Stages。 POPO-screenshot-20230424-095023.png 设置好之后还要开启GPU模拟,否则会报错(至少4.26会)。

Grid2D Collection

相当于一个二维的Buffer,用来储存二维的各种数据,需要在Emitter Spawn阶段使用Set Parameter方法创建。网格的每个点可以成为Cell,而后我们可以在模拟阶段对Grid进行读写。

  • 在模拟阶段的Module中可以用Execution Index to Unit来获取当前迭代次数,在原Grid2D中的UV位置。
  • 可以在模拟阶段用Module将Texture按上个tip获取的uv进行采样然后储存到Grid2D中。
  • 可以用Get Num Cells来获取其整数形式的网格大小。
  • 用Get Vector 4 Attributre Index来获取指定名称的属性在这个Grid2D Collection中的index。

Render Texture

恐怖的是,Niagara还能生成RT,RT需要在Emitter Spawn阶段使用Set Parameter方法创建。

  • 可以使用Set Render Target Value来对RT进行赋值。

将材质储存到Grid2D并渲染到RT

此过程如下,数据流向(不准确的描述)为Texture->Grid2D->RTPOPO-screenshot-20230424-103344.png

将Grid2D内容填充到RT

先对Grid进行读取,然后使用当前Grid的index作为参数填充RT。 POPO-screenshot-20230424-103133.png 由此可见,Grid2D和RT在形式上是高度一致的,上图根本没有进行转换。

Important

Linear在Grid2D或3D的语境下,都是指当前cell的一维的index,一维的index不太直观,因此都有Linear To Index的方法,将一维的index转换为三维的Index

使用材质渲染RT

可以创建一个拥有Texture Object变量的材质作为Sprite Material,而后在Material Parameter Bindings里面添加一个变量的绑定,将Texture Object设置为我们的RT。 POPO-screenshot-20230424-102935.png

Neighbor Grid3D

基于哈希表的三维网格划分。使用这个主要用来加速需要对周围粒子进行交互的模拟阶段模块。目前也只支持GPU模拟。 相关内容比Grid2D复杂,学习基础可以看这篇文章

  • 在创建Neighbor Grid3D时有Max Neighbor Per Cell属性,此属性控制了每个小网格能够储存的数据(整形)的数量,主要用来记录处于该网格内的粒子的index。
  • 使用GetWorldBBoxSize获取网格的大小,注意这是获取的网格的总大小,并不是每个小网格的大小。后文用大网格称呼网格总大小,小网格称呼每个划分后的网格。
  • 使用SimulationToUnit方法将模拟坐标系(也就是粒子位置)转换到单位坐标系(也就是大网格的坐标系)下。

将粒子写入Neighbor Grid3D

Neighbor Grid3D单位坐标系

在官方示例中,官方使用hlsl节点计算了从模拟坐标系到单位坐标系下的转换矩阵,此转换矩阵中使用来0.5的偏移,这是因为单位坐标系是以大网格的左下角为坐标原点的。 v2-54f8896c40de85525c7e9c59a8082734_720w.jpg 因此构造将粒子从模拟坐标系转换到单位坐标系的转换矩阵的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这一行。

使用Neighbor Grid3D将粒子进行划分

主要要进行以下的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里复用。)

使用Neighbor Grid3D查找邻近的点

为做区分,将前文中已经储存的点称之为大点,现在要进行运算的点称为小点,我们现在要寻找里小点最近的大点。 首先是将小点也进行网格划分,查找自己属于哪一个小网格,然和查找小网格中已经记录的大点的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个粒子,则前者规模为,后者为,如果大点和小点数量相同(例如小点内部产生交互),规模则变成了