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,以及實現接口和數據正確,剩下的就交給框架。