学习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,以及实现接口和数据正确,剩下的就交给框架。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章