他在github里给出了传统SPH实现(MonoBehaviour)的源码,和使用ECS架构后的源码。
先解析下传统单线程实现,也就是MonoBehaviour。
大体思路是在每个粒子的MonoBehaviour里,计算自己和其他粒子在一定密度下受到的力,相互作用力产生的速度与运动方向,再应用到坐标位置上。
private voidStart()
{
InitSPH();
}private voidUpdate()
{
// 计算密度压力
ComputeDensityPressure();
// 计算力(含方向)
ComputeForces();
// 计算位置
Integrate();
// 计算碰撞
ComputeColliders();
// 应用位置
ApplyPosition();
}
计算粒子间的流体碰撞共使用到下列参数,SPH包括粒子密度渗透。
其中restDensity和smoothingRadius是粒子间超过一定距离上的阈值,则不在计算相互作用力,这也符合力学运动物质趋于稳定的物理学规律。
[System.Serializable]private structSPHParameters
{public float particleRadius; //粒子半径
public float smoothingRadius; //平滑半径
public float smoothingRadiusSq; //平衡半径开方
public float restDensity; //休息密度
public float gravityMult; //重力加速
public float particleMass; //质点
public float particleViscosity; //颗粒粘度
public float particleDrag; //粒子牵引
#pragma warning restore 0649}
InitSPH()
初始化粒子的位置,将位置摆放为x * y * z,添加一定的位置扰动。
// 计算抖动:增加一定随机性,将随机值的值域映射到【-1,1】,将抖动缩小到0.1
float jitter = (Random.value * 2f - 1f) * parameters[parameterID].particleRadius * 0.1f;
粒子位置摆放x z方向都加上了Random.Range(-0.1f, 0.1f)的随机值,这个随机值和扰动都不用太大,只是给初始位置增加一点移动,避免运行后的流体只是单纯下落。
ComputeDensityPressure()
计算相互间的作用里,所以需要两个for,进行O(n^2)的遍历计算
Vector3 rij = particles[j].position - particles[i].position; // 指向j的方向向量,是i粒子对j粒子的作用力
// 如果之前距离小于平滑半径,则需要进行密度计算
if (r2
{//质点 * 圆周的一些参数 * pow(平滑半径,9) * pow(平滑半径距离 - 实际距离, 3)
particles[i].density += parameters[particles[i].parameterID].particleMass *(315.0f / (64.0f * Mathf.PI * Mathf.Pow(parameters[particles[i].parameterID].smoothingRadius, 9.0f)))* Mathf.Pow(parameters[particles[i].parameterID].smoothingRadiusSq - r2, 3.0f);
}
计算并存储粒子受到压力particles[i].pressure,压力值与粒子间密度有关。
计算作用力与速度,因为是受所有粒子的作用力,是个累加值,速度同样的道理。下面的代码稍微简化了下,不是原代码。
//小于平滑阈值,则计算相互作用力(压力)
if (r
{//-rij 指向自己//计算自己受到的压力值,计算压力的粒子距离减去平滑半径,也即是不受力的距离
forcePressure += -rij.normalized * particleMass *(particles[i].pressure+ particles[j].pressure) / (2.0f * particles[j].density) *(-45.0f / smoothingRadius, 6.0f))) *smoothingRadius- r, 2.0f);
forceViscosity+= particleViscosity *particleMass* (particles[j].velocity - particles[i].velocity) / particles[j].density *(45.0f / smoothingRadius, 6.0f))) *(smoothingRadius-r);
}
ComputeColliders()
这个操作我想是计算与地面/墙壁的碰撞,对于其他碰撞体,都加上SPHCollider标签,for循环计算particles数组内每个粒子和GameObject.FindGameObjectsWithTag("SPHCollider")场景内所有SPHCollider标签的物体进行‘碰撞检测’,大致实现是:在粒子球体半径范围内,通过叉积计算碰撞面的法线方向,然后在地面/墙面的投影计算渗透长度与位置?没看懂。
最后计算一堆点积累加计算各个方向的力,应用到粒子位置上。
JobSystem & Unity ECS
1. SPHCollider : IComponentData
2. SPHParticle : ISharedComponentData
3. SPHVelocity : IComponentData
先使用ComponentData接口实现数据,这在Unity ECS中被归为Component组件,尽管在传统MVC被认为是Model,但这里和Component联系更紧密,就像GameObject挂载Component也有一堆Serializable的字段一样。
SPHManager : MonoBehaviour
它构建了整个场景,给墙壁/地面添加Collider,排列粒子位置,感觉有点像World的一部分。
private voidStart()
{//Imoprt//manager = World.Active.GetOrCreateSystem();
manager =World.Active.EntityManager;//Setup
AddColliders();
AddParticles(amount);
}
SPHSystem : JobComponentSystem
JobHandle OnUpdate(JobHandle inputDeps)每帧调用里,处理了各个IComponentData & IJobParallelFor,IJobParallelFor只定义了Execute处理每帧当前数据需要做的/执行的操作,属于行为,行为与Component解耦,虽然原本也没在一起,但如果说MonoBehaviour里处理了行为叫做Component的行为,好吧。
总之按顺序new 这些实现了IJobParallelFor的结构体,Unity内部会去注册这些Execute方法并分布运行,我们只需要保证这些接口实现的调用时正确顺序就行。
[BurstCompile]
private struct ComputeForces : IJobParallelFor // 行为
[JobProducerType(typeof(IJobParallelForExtensions.ParallelForJobStruct<>))]public interfaceIJobParallelFor
{//
//摘要://Implement this method to perform work against a specific iteration index.//
//参数://index://The index of the Parallel for loop at which to perform work.
void Execute(intindex);
}
这样看来Unity ECS的使用并不会比MonoBehaviour更加复杂,我们只需要掌握几个概念ComponentData, World, ComponentSystem,以及实现接口和数据正确,剩下的就交给框架。