学习Unity 2019 ECS框架(二)
ECS非常适合用于大规模物件的动画交互,比如这个流体模拟https://connect.unity.com/p/shi-yong-unityde-ecshe-job-systemshi-xian-liu-ti-mo-ni-xiao-guo
他在github里给出了传统SPH实现(MonoBehaviour)的源码,和使用ECS架构后的源码。
先解析下传统单线程实现,也就是MonoBehaviour。
大体思路是在每个粒子的MonoBehaviour里,计算自己和其他粒子在一定密度下受到的力,相互作用力产生的速度与运动方向,再应用到坐标位置上。
private void Start() { InitSPH(); } private void Update() {
// 计算密度压力 ComputeDensityPressure();
// 计算力(含方向) ComputeForces();
// 计算位置 Integrate();
// 计算碰撞 ComputeColliders(); // 应用位置 ApplyPosition(); }
计算粒子间的流体碰撞共使用到下列参数,SPH包括粒子密度渗透。
其中restDensity和smoothingRadius是粒子间超过一定距离上的阈值,则不在计算相互作用力,这也符合力学运动物质趋于稳定的物理学规律。
[System.Serializable] private struct SPHParameters { 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 < parameters[particles[i].parameterID].smoothingRadiusSq) { // 质点 * 圆周的一些参数 * 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 < parameters[particles[i].parameterID].smoothingRadius) { // -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 void Start() { // Imoprt //manager = World.Active.GetOrCreateSystem<EntityManager>(); 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 interface IJobParallelFor { // // 摘要: // 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(int index); }
这样看来Unity ECS的使用并不会比MonoBehaviour更加复杂,我们只需要掌握几个概念ComponentData, World, ComponentSystem,以及实现接口和数据正确,剩下的就交给框架。