力学部分


UE5正式版中,Niagara Fluid插件提供了相对完整的流体仿真解决方案,其中模拟部分采用了混合欧拉-拉格朗日视角,使用了PIC/FLIP方法。 本文将就3DLiquidDamBreak粒子系统为出发点,分析其中粒子位置更新的计算过程。 不完整或有错误的地方将在后续学习过程中逐步完善。 ## 1 初始化数据 ### 1.1 Grid 3D Set Resolution 这一部分初始化了Grid的分辨率,即欧拉网格的分辨率。代码如下: ```cpp float CellSize = max(WorldGridExtents.z, max(WorldGridExtents.x, WorldGridExtents.y)) / NumCellsMaxAxis; NumCellsX = floor(WorldGridExtents.x / CellSize); // step1:取各个方向尺寸的最大值,统一cellsize,再计算每个方向有多少个cell ... // step2:处理特殊情况,为部分NumCells+1 Out_WorldGridExtents = float3(NumCellsX, NumCellsY, NumCellsZ) * CellSize; // step3:用上边得到的cellsize和cellnum重新计算在世界空间下模拟的尺寸 ``` ### 1.2 粒子初速度

下边对照之前所学的混合欧拉-拉格朗日视角过程,逐层分析


## 2 P2G Particle to Grid transfer 对应粒子系统中的Rasterize Particles:Grid 3D FLIP Rasterize Particles部分 ```cpp int IGNORE = 0; int XIndexInt = floor(Index.x); int YIndexInt = floor(Index.y); int ZIndexInt = floor(Index.z); //向下取整到对应的Index,相当于直接存到了粒子所在网格的左上角 for (int x = 0; x <= 1; ++x) { for (int y = 0; y <= 1; ++y) { for (int z = 0; z <= 1; ++z) { const float GridWeightXYZ = 1;//代码中权重均取1 RasterizationGrid_velocity.InterlockedAddFloatGridValueSafe(XIndexInt+x, YIndexInt+y, ZIndexInt+z, 0, GridWeightXYZ*Velocity.x, IGNORE); RasterizationGrid_velocity.InterlockedAddFloatGridValueSafe(XIndexInt+x, YIndexInt+y, ZIndexInt+z, 1, GridWeightXYZ*Velocity.y, IGNORE); RasterizationGrid_velocity.InterlockedAddFloatGridValueSafe(XIndexInt+x, YIndexInt+y, ZIndexInt+z, 2, GridWeightXYZ*Velocity.z, IGNORE); RasterizationGrid_velocity.InterlockedAddFloatGridValue(XIndexInt+x, YIndexInt+y, ZIndexInt+z, 3, GridWeightXYZ, IGNORE); //分别累加xyz方向的速度到格点上、权重 BoundaryGrid.SetGridValue(XIndexInt+x, YIndexInt+y, ZIndexInt+z, BoundaryIndex, 3, IGNORE); //记录BoundaryIndex到BoundaryGrid }}} ``` 参考:[Niagara RasterizationGrid3D Data Interface](https://forums.unrealengine.com/t/niagara-rasterizationgrid3d-data-interface/502692) RasterizationGrid3D可用于迭代源为Particle时并行的累加(InterlockedAddFloatGridValueSafe)

## 3 Grid operations ### 3.1 Boundaries conditions 在TransientGrid上存储边界类型和固体速度

遍历网格TransientGrid,存储当前Index处的边界类型,用于后续计算: ```cpp //边界类型 const int FLUID_CELL = 0; // 0 流体内部 const int SOLID_CELL = 1; // 1 固体边界 const int EMPTY_CELL = 2; ``` 和边界处的固体的速度SolidVelocity

### 3.2 PIC方法 修正:这里其实可以看作还是P2G的一部分,做的还是在gather速度和处理边界 从RasterizationGrid_velocity中gather速度,若cell处于边界上,则gather所有附近的类型为fluid的cell 代码部分只保留了关键部分 ```cpp // 从RasterizationGrid_velocity中Gather速度 Grid.GetFloatGridValue(IndexX, IndexY, IndexZ, 0, OutVelocity.x); Grid.GetFloatGridValue(IndexX, IndexY, IndexZ, 1, OutVelocity.y); Grid.GetFloatGridValue(IndexX, IndexY, IndexZ, 2, OutVelocity.z); Grid.GetFloatGridValue(IndexX, IndexY, IndexZ, 3, TmpWeight); OutVelocity /= TmpWeight; // if we have a boundary cell, then gather value from closest neighbor cell // 如果在边界上 [branch] if (CellType != FLUID_CELL) { // 共采样了3x3x3 = 27个 跳过了不是FLUID_CELL类型的cell for (int xx = -ExtrapolationHalfWidth; xx <= ExtrapolationHalfWidth; ++xx) { for (int yy = -ExtrapolationHalfWidth; yy <= ExtrapolationHalfWidth; ++yy) { for (int zz = -ExtrapolationHalfWidth; zz <= ExtrapolationHalfWidth; ++zz) { // only extrapolate from fluid cells // don't extrapolate from the boundary of the domain to allow particles to pass through as ballistic when they leave if (TmpCellType == FLUID_CELL) { float Weight = 1./length2(float3(xx,yy,zz)); // gather邻居 Grid.GetFloatGridValue(IndexX+xx, IndexY+yy, IndexZ+zz, 0, TmpVelocity.x); Grid.GetFloatGridValue(IndexX+xx, IndexY+yy, IndexZ+zz, 1, TmpVelocity.y); Grid.GetFloatGridValue(IndexX+xx, IndexY+yy, IndexZ+zz, 2, TmpVelocity.z); Grid.GetFloatGridValue(IndexX+xx, IndexY+yy, IndexZ+zz, 3, TmpWeight); TmpVelocity /= TmpWeight; // 所在cell的速度 OutVelocity += TmpVelocity * Weight; // 按照距离平方的倒数作为权重累加 TotalWeight += Weight; // 累加权重 } }}} OutVelocity /= TotalWeight; } ``` gather得到的速度,直接作为SimGrid的 (aka. StartVelocity and Velocity) ### 3.3 Projection

#### 3.3.1 Compute Divergence 计算散度 ```cpp Div = (Vx_right - Vx_left + Vy_up - Vy_down + Vz_front - Vz_back) / (2. * dx); ``` 对应


计算完成后将Div赋值给TemporaryGrid,同时初始化PressureGrid中Pressure值为0 #### 3.3.2 Solve Pressure 解算压力 使用SOR方法迭代50次解算出一个压力场(即Pressure Grid中的值),使散度为0 代码中使用了Red-Black Successive Over-Relaxation (SOR)方法 其中 SOR方法参考:[线性方程组的SOR迭代法](https://blog.csdn.net/qq_37925680/article/details/105475373) Red-Black Ordering方法参考:[Red-Black Ordering Lecture 67 Numerical Methods for Engineering](https://www.youtube.com/watch?v=giTZ89q-Bpk) 首先,介绍本文中需要求解的矩阵 Ax = b,参考[Games201学习笔记3:欧拉视角](https://blog.csdn.net/qq_36005498/article/details/124667635)4. Prjection部分 压力对加速度的贡献

对方程展开, 替换为下一时刻速度-当前速度,移项,左右同时求散度 由于下一时刻散度为0,所以 项消去,化简的到最后结果

左边一项采用中心差分法近似拉普拉斯(三维空间下为上下左右前后的邻居,其中4 中的4替换为周围cell类型为fluid或empty的个数(FluidCellCount),代码如下 ```cpp float Scale = dx / dt; int CellType_right = round(B_right); if (CellType_right == SOLID_CELL) { FluidCellCount--;//从6递减 BoundaryAdd += Scale * (Velocity.x - SV_x_right);//固体边界的相对速度提供的压力 P_right = 0;//若边界不是fluid则压力为0 } else if (CellType_right == EMPTY_CELL) { P_right = 0; } ```

右边式子中的散度已知,ρ取了1,故得到以下等式

化简
<!— $$
P{center} = (P{center} +P{right} + P{left} + P{up} + P{down} + P{front} + P{back} +P_{Boundary} - \frac{\Delta x^2 \times \nabla·u}{\Delta t})/FluidCellCount

x^{k+1}_i = (1-\omega) x^k_i + \omega b_i

P{center} = (P{center} +P{right} + P{left} + P{up} + P{down} + P{front} + P{back} +P_{Boundary} - \frac{\Delta x^2 \times \nabla·u}{\Delta t})/FluidCellCount

$$ —>

综上,对应代码

if (FluidCellCount > 0) 
{
    float JacobiPressure = (P_right + P_left + P_up + P_down + P_front + P_back -  dx * dx * Divergence / dt + BoundaryAdd) / FluidCellCount;
    Pressure = (1.f - Weight) * P_center + Weight * JacobiPressure;
}

共迭代50次后,得到一个散度近似为0的压力场,记录在Pressure Grid中

3.3.4 Project Pressure

将p带入公式求速度,同时处理边界问题


</br>

step1:求梯度 ,由邻居的pressure近似梯度Gradient

Grad = float3(S_right - S_left, S_up - S_down, S_front - S_back) / (2.0 * dx);

step2:求加速度


</br>

step3:处理边界(以左右边界为例,后边处理上下,前后同理)

//只考虑一边是边界,而非左右都是边界的极端情况
if (CellType_left == SOLID_CELL)
{
    VelocityOut.x = SV_x_left; //左边为边界,则从边界中读取速度并赋值
}
else if (CellType_right == SOLID_CELL)
{
    VelocityOut.x = SV_x_right; 
}
else
{
    VelocityOut.x = Velocity.x; //上下都不是边界,则正常赋值
}

得到的速度赋给SimGrid保存

3.3.5 Extrapolate Velocities Again

在SimGrid上处理cell类型为边界的值,相当于模糊了边界速度值,同3.2

3.4 小结

至此,新的速度场已经计算得到,存储在SimGrid中,下边将使用PIC/FLIP方法,更新速度到粒子上。旧的速度场在第一次Extrapolate Velocities时,也记录到了SimGrid,使用了不同的下标(AttributeIndex)

4 G2P Grid to Particle transfer

这里开始Simulation Stage的迭代源为particle,之前的迭代源为Grid
UE这里使用的方法为将particle的坐标进行2次矩阵乘法(SimulationToWorld,WorldToUnit),输出位置为UnitPos,一般是0-1的float,超出范围的不再更新速度,只按照当前速度更新位置


</br>

然后用unitpos在SimGrid网格中插值采样得到速度,插值的代码没有看到,盲猜是三维空间下的线性插值。


</br>

5 Particle operations

5.1 Move particle

边界的处理:
若粒子处于固体边界中,速度取0,位置不变
若粒子处于空cell中,速度不变,位置按照当前粒子速度乘以dt更新


</br>

若不在以上边界,速度的更新则按照PIC/FLIP方法混合更新。
回顾这2种方法:

  1. PIC: 直接从网格中采样得到并更新
  2. FLIP: 从网格中采样速度的增量,累加到原来的速度上

蓝图中如下表示


</br>

位置的更新略有不同,完全使用了PIC方法,即直接使用网格中采样得到新的速度场更新粒子位置


</br>

6 总结

至此,力学部分的计算过程总结完成,遗留了渲染得到水的部分,将在下一篇中更新