Unreal引擎——Chaos物理引擎(不)详解
今天我们来了解一下关于Unreal引擎的物理引擎——Chaos:这是UE自研的更高级的物理引擎。
基本架构
我们首先来看看整个物理引擎的架构:
虚幻引擎的Chaos物理引擎采用了五层分层架构设计,从上到下依次是游戏世界层的UWorld负责整个物理系统的生命周期管理和时间调度,物理场景层的FPhysScene作为中间桥梁管理求解器实例并协调游戏线程与物理线程之间的同步,Chaos求解器层包含核心的FPBDRigidsSolver以及粒子系统、约束系统、几何体系统等专门负责具体的物理计算算法实现,组件集成层通过UPrimitiveComponent、FBodyInstance、Chaos::FPhysicsObject等类将游戏中的Actor和Component转换为物理引擎能够理解和处理的物理对象,最后查询与工具层提供TraceSystem射线检测、OverlapSystem重叠检测、AsyncTrace异步查询等各种物理查询功能。
这个图作为我们的数据流向图,清楚地展示了物理引擎的工作流程中数据的走向。首先输入层接收来自Actor/Component的变换更新、用户输入的力和冲量以及各种碰撞事件,然后在游戏主线程中按照严格的Tick组顺序进行处理——从Pre-Physics的数据同步通过External Data Lock保护开始,到Start Physics启动物理计算、During Physics期间进行异步处理、End Physics等待物理线程完成、最后Post Physics处理结果,与此同时物理线程并行执行固定时间步长的物理计算包括碰撞检测的宽泛阶段和精确阶段、PBD迭代的约束求解、以及位置速度的积分更新,最终通过输出层将计算结果转换为Transform更新同步给渲染系统进行帧间插值,同时触发OnHit、OnOverlap等事件回调通知游戏逻辑,整个流程确保了物理计算的精确性同时最大化了多核CPU的并行处理能力。
与PhsyX的差异
在开始学习这个物理引擎的内容之前,我们先来看看这个物理引擎相比起PhsyX物理引擎来说有什么不一样。
PBD vs 冲量求解
在物理引擎中,物理计算70-80%的时间都花在约束求解上,这个也非常容易理解:设定约束条件总是简单的,但是具体去求满足所有约束条件的解就非常复杂了。在PhysX中,传统的求解思路是这样的:
// PhysX风格的传统求解器(概念性代码)
void PhysXSolver::SolveConstraints(float deltaTime) {// 1. 先积分速度和位置for (Body : AllBodies) {Body.velocity += Body.force / Body.mass * deltaTime;Body.position += Body.velocity * deltaTime;}// 2. 然后处理约束违反(Sequential Impulse)for (int iteration = 0; iteration < maxIterations; ++iteration) {for (Constraint : AllConstraints) {// 计算约束违反量float error = CalculateConstraintError(Constraint);// 通过冲量修正速度Vector3 impulse = CalculateImpulse(error, Constraint);ApplyImpulseToVelocity(Constraint.bodyA, impulse);ApplyImpulseToVelocity(Constraint.bodyB, -impulse);}}
}
在之前的学习中我们已经知道了PhysX引擎关于求解器的做法:我们是先运动后修正——更准确地说,我们基于力和冲量来物体模拟运动并判断这个运动过程是否有违反约束条件,如果有的话我们就会去修改力和冲量。
我们也许要回顾一下为什么我们以力和冲量为基本的求解单位,毋庸置疑,当我们涉及物理计算时力总是第一考虑的因为力是物体发生物理变化的根本原因,而冲量则是我们的力和时间的积分的乘积——代表了动量的变化。动量(质量乘以速度,我们的动量变化导致了不同质量的速度发生变化)则会导致我们的速度发生变化,速度和时间的积分的乘积就是我们物体的位置变化,也就是我们的物理模拟的目的。
不难发现这就是我们在中学时学习的传统力学的内容,这是一个很经典的逻辑链条,力产生了冲量导致了动量的变化,动量的变化导致了速度的变化,速度的变化导致了位移,这就是我们的传统物理模拟的思路,这样的思路伴随着很多问题:
等等,总之就是问题很多,但是我们可以进行优化,这就是Chaos引擎做的事。
我们先来介绍一下Chaos引擎的求解器算法实现底层逻辑:Position Based Dynamics (PBD)算法。
Chaos引擎的思路就是我们不再根据力来驱动整个计算过程而是直接去预测位置然后得到满足约束的位置之后再去反推速度。
在第一步预测阶段我们的处理方法和传统物理方法别无二致:
// 保存之前的速度
Particle.SetPreV(V);
Particle.SetPreW(W);// 应用外力
for (FForceRule ForceRule : ForceRules)
{ForceRule(Particle, Dt);
}// 更新速度(基于力的积分)
V += Particle.Acceleration() * Dt;
W += Particle.AngularAcceleration() * Dt;// 应用冲量
V += Particle.LinearImpulseVelocity();
W += Particle.AngularImpulseVelocity();
但是在后续的计算中我们思路就变化了:
// Solve all the constraints
{SCOPE_CYCLE_COUNTER(STAT_Evolution_ParallelSolve);CSV_SCOPED_TIMING_STAT(PhysicsVerbose, StepSolver_PerIslandSolve);IslandGroupManager.Solve(Dt); // PBD约束求解的核心!
}// Chaos PBD的约束求解(概念性代码)
void ChaosPBDSolver() {// 1. 预测位置(基于当前速度)for (Particle : AllParticles) {Particle.predicted_position = Particle.position + Particle.velocity * dt;}// 2. 迭代求解约束(直接修正位置)for (int iteration = 0; iteration < numIterations; ++iteration) {for (Constraint : AllConstraints) {// 直接计算满足约束的位置修正Vector3 correction = SolveConstraintDirectly(Constraint);// 立即应用位置修正Particle1.predicted_position += correction * ratio1;Particle2.predicted_position -= correction * ratio2;}}// 3. 从位置变化反推新速度for (Particle : AllParticles) {Particle.velocity = (Particle.predicted_position - Particle.position) / dt;Particle.position = Particle.predicted_position;}
}
chaos物理引擎会直接去预测新位置(当然也是基于之前预测阶段计算的新速度),一开始我们不设置约束,但是在后续的过程中我们加入约束来不断对新的位置进行迭代知道满足所有约束要求,最后我们再根据位置反推速度即可。
这样做的好处显而易见:首先我们直接获得正确的位置,根本上避免了传统物理引擎的长链条可能导致的误差累积,这也导致了我们的基于位置的计算方法的约束校正更温和(但也没有完全避免)。除此之外,由于长链条且不同物体之间的约束校正有依赖性(人话就是物体A的速度变化可能要考虑物体B的速度变化),在大场景之中这种复杂且漫长的计算过程会有很惊人的计算开销,而我们直接基于位置的物理模拟计算方法就可以大大避免这个问题。最后不得不提的一点就是传统的PhysX引擎创建数据结构的时候是基于面向对象思想实现的,而chaos物理引擎则是数据导向设计的,相同类型数据内存连续,SIMD指令友好。
内存存储和线程模型
这是传统的物理引擎存储方式。
然后就是线程模型方面的内容:
而对应的:
在这里提到了岛屿并行架构:
Chaos的岛屿并行系统本质上就是一个智能分组+并行计算的架构:系统自动分析大场景中所有物体的相互作用关系(通过约束、碰撞、接触等),将有直接或间接物理连接的物体归为一个"岛屿",将完全独立、互不影响的物体群分到不同"岛屿",然后让多个CPU核心同时并行处理不同岛屿的物理计算,最后合并结果。
关于碰撞检测:PhysX和Chaos本质上差异不大:
关于相关的算法详解可以看之前的笔记。