本片文章深入剖析Chaos的代码结构。

一.代码结构

Chaos的核心代码存储在Source/Runtime/Experimental

而一些之外的代码都是以插件的形式存在于引擎之中。InteractiveToolsFramework并不属于Chaos的模块。

  1. ChaosCore封装了一些基本的数据结构,有Array,Vector,Matrix,其考虑到了没有Unreal基本数据结构支持时应当如何处理。
  2. Voronoi是计算几何术语,在二维空间中的一种空间划分的方案,对于离散点的空间划分中,其具有每条边到点的距离是相等的特点。
  3. GeometryCollectionEngine,实现资源GeometryCollection的声明和应用,还有Gameplay中使用的Component和Actor实现了GeometryCollectionActor\GeometryCollectionComponent\GeometryCollectionComponent等功能。其作为UE的破碎功能和Chaos的结合实体。
  4. FieldSys,实现控制破碎过程中使用的场的功能。如力场,sleep场,kill场等等。
  5. ChaosVehiclesEngine,使用Chaos实现的载具功能,目前处于实验状态。
  6. ChaosSolverEngine,每个破碎功能都必须有一个解算器,如果我们不声明的话,我们会使用默认的解算器。
  7. Chaos,所有的物理计算,进阶和高端的操作都在这个模块当中

在引擎中,physx和chaos是无法兼容进行使用的,在编译阶段就使用宏来进行了所有的隔离操作,以保证上层的使用对底层的实现无法感知。PHYSICS_INTERFACE_PHYSXWITH_CHAOS是完全互斥的两个宏。

当然,一些核心的调度代码存在Engine/Physics/Experimental中。

主要数据结构

  1. 我们知道,我们使用所有破碎都需要通过使用UGeometryCollectionComponent来实现,而我们的UGeometryCollectionComponent对应的破碎数据则是存储在 UGeometryCollection 中,我们在编辑器中指定和配置的都是这个数据。除了UGeometryCollection外,还有一个UGeometryCollectionCache的数据,我们可以使用UGeometryCollectionCache的数据进行与烘培减少在运行时的实时计算。
  2. FPhysScene_Chaos,作为物理世界的物理线程与主线程的桥梁,所有与物理世界的交互都在这里进行。
  3. TPBDRigidsSolver,真正进行物理计算的数据结构,自己玩自己的,只经过FPhysScene_Chaos进行同步交流。如果仅有一个Solver的话,我们所有的物理计算都是在一个空间中和之前是没有区别的;如果我们对场景中不同的物体指定使用不同的物理解算器,不同解算器之间是没有办法进行通信的,可能这在某些情况下会有这种特殊需求。

建立物理状态

对于破碎来讲,UGeometryCollectionComponent与一般的Component一样,也是在注册时进行物理数据的注册和提交。而其他的物理和Phx一样,上层接口上是无感知的。

从UActorComponent开始,会在注册时调用CreatePhysicsState。根据是否需要生成OverlapEvent,分为两个不同的分支。 如果不需要OverlapEvent,我们获得Wolrd 的PhysicsScene的DeferPhysicsStateCreation,延迟的创建

WorldPrivate->GetPhysicsScene()->DeferPhysicsStateCreation(Primitive);

对于需要的,我们将调用OnCreatePhysicsState来进行。

对于每个拥有Mesh 的Component来说,必须含有物理数据,也就是UBodySetup才能够确定其物理世界的表示方案,所以只有保证有其正确的BodySetup。 之后,会加入到PhysScene的DeferredCreate PhysicsStateComponents中。对于其后续的操作则在PhysScene阶段再进行介绍。值得说明的是,我们的初始化操作其实是针对其内部的BodySetup,我们初始化完成BodySetup后,在Scene中依旧会调用OnCreatePhysicsState来生成PhysicsState。

跟渲染一样,由于都是不同线程间进行,根据Unreal 的设计原则,不同线程之间都需要一个Proxy来进行通信和代指,从而保证线程间的安全。

对于Chaos物理系统而言,拥有自己的物理代理。 所有的物理代理都是继承于TPhysicsProxy。其类型有

  1. FSkeletalMeshPhysicsProxy,对SkeletalMesh 的物理代理
  2. FStaticMeshPhysicsProxy,对StaticMesh 的物理代理
  3. TGeometryCollectionPhysicsProxy,对破碎物体的物理代理
  4. TJointConstraintProxy,对约束节点的的物理代理 等等。

每个物体的物理状态的更新OnCreatePhysicsState的时候。在OnDestroyPhysicsState的时候销毁,这两个也分别是在组件创建和销毁的时候进行的。

当我们创建完成一个PhysicsProxy,我们将其加入到PhysicsScene中来。使用FPhysScene_Chaos::AddObject来进行,物理代理和Component的对应关系将会被记录下来。但是其物理真正加入到解算器中并一定是在这里。

在UPrimitiveComponent中首先会根据UBodySetup建立BodyInstance之后调用InitBody,将自己的物理代理加入物理世界。当然我们的破碎由于不是BodyInstance能代表的,其实我们在生成TGeometryCollectionPhysicsProxy加入Scene的时候,注册到物理世界的。

普通的BodyInstance注册到解算器

对于一般的BodyInstance,不管是StaticMesh还是SkeletalMesh,都是使用BodyInstance来表示的,所以他们都是在BodyInstance的InitBody,添加到物理世界中。

调用InitDynamicProperties_AssumesLocked。

GeometryCollection注册到解算器

对于TGeometryCollectionPhysicsProxy来说,我们需要定义三个不同的函数:

FInitFunc InInitFunc;
FCacheSyncFunc InCacheSyncFunc, 
FFinalSyncFunc InFinalSyncFunc 
  1. 初始化函数,这里主要是处理拥有Cache的破碎,Chaos是支持预先烘培的Cache模式,如果我们指定了Cache模式,我们将会在这里进行初始化操作.
  2. 缓存同步函数,这里定义我们每帧从物理线程到底同步哪些东西,这是我们可以自行定义的部分。
  3. 最终同步函数,这里都是编辑器的内容,暂时不是很重要。

除了三个函数之外,建立Proxy还有一些数据需要我们注意

  1. FSimulationParameters,这个数据是从Component上扒下来的
  2. FCollisionFilterData InitialSimFilter;这是对应的物理资产里面的物理模拟的掩码
  3. FCollisionFilterData InitialQueryFilter;这是对应的物理资产里面的查询的掩码
  4. FGeometryDynamicCollection DynamicCollection;用来做破碎的网络同步

虽然对于所有的代理都有对应AddObject,但是对于GeometryCollection来说,我们有单独注册的操作RegisterObject。在物理线程初始化代理数据和完成注册InitializeBodiesPT

初始化物理场景

在之前的文章中我们就已经知道了物理场景创建的时机和方式。是跟随世界场景一起进行创建的。

void UWorld::CreatePhysicsScene(const AWorldSettings* Settings)
{
#if CHAOS_CHECKED
	const FName PhysicsName = IsNetMode(NM_DedicatedServer) ? TEXT("ServerPhysics") : TEXT("ClientPhysics");
	FPhysScene* NewScene = new FPhysScene(nullptr, PhysicsName);
#else
	FPhysScene* NewScene = new FPhysScene(nullptr);
#endif
	SetPhysicsScene(NewScene);
}

如果我们使用Debug模式,比传统的physx多加了一个名字的调试信息,来判断是否是专有服务器上的物理场景。

物理场景的数据结构

所有通过OnCreatePhysicsState添加到Scene里面的数据,都存储在两个Map里面。

	TMap<IPhysicsProxyBase*, UPrimitiveComponent*> PhysicsProxyToComponentMap;

	TMap<UPrimitiveComponent*, TArray<IPhysicsProxyBase*>> ComponentToPhysicsProxyMap;

一个是代理到Component的映射,一个是Component到代理集的映射。

初始化解算器

我们在FChaosScene的构造函数中,会初始化全局的的解算器。

	SceneSolver = ChaosModule->CreateSolver<Chaos::FDefaultTraits>(OwnerPtr,ThreadingMode
#if CHAOS_CHECKED
		,DebugName
#endif
		);

对于解算器来讲,有这么几个重要的参数需要我们注意

  1. EMultiBufferMode BufferMode,这里有Single,Double,Triple,TripleGuarded这么几种类型
  2. EThreadingModeTemp ThreadingMode,有DedicatedThread,TaskGraph,SingleThread三种类型

然后我们会将引擎对物理引擎的配置一起给予解算器。

		UPhysicsSettingsCore* Settings = UPhysicsSettingsCore::Get();
		SceneSolver->EnqueueCommandImmediate([InSolver = SceneSolver, SolverConfigCopy = Settings->SolverOptions]()
		{
			InSolver->ApplyConfig(SolverConfigCopy);
		});

物理的更新

从unreal 的设计原则触发,FChaosScene这个类不是U类型,所以也就没有集成UObject,但是其继承的是FGCObject,提供垃圾回收的注册。

当然,我们的使用的是FPhysScene_Chaos,

物理场景更新开始

如我在之前物理文章中介绍的一样,其依旧是在World的Tick中实现的其基本的物理更新。

	/** Tick function for starting physics*/
	FStartPhysicsTickFunction StartPhysicsTickFunction;
	/** Tick function for ending physics*/
	FEndPhysicsTickFunction EndPhysicsTickFunction;

经过SetupPhysicsTickFunctions后,在TG_StartPhysics中运行。 而 EndPhysicsTickFunction在TG_EndPhysics中保持运行。

添加新的物理数据

开始物理Tick主要就是开启物理场景的更新。对于FPhysScene来说,调用StartFrame。在StartFrame会更新多线程的物理世界,调用解算器的

	for(FPhysicsSolverBase* Solver : SolverList)
	{
		CompletionEvents.Add(Solver->AdvanceAndDispatch_External(UseDeltaTime));
	}
  1. 之后会调用ProcessDeferredCreatePhysicsState,我们将前一帧之前还未处理的DeferredCreatePhysicsStateComponents进行处理。拿到所有的UBodySetup,对其BodySetup调用CreatePhysicsMeshes,对Component调用OnCreatePhysicsState。在最后,我们清空DeferredCreatePhysicsStateComponents。
  2. UpdateKinematicsOnDeferredSkelMeshes:对于运动学控制的物理,例如人物身上的物理,并不是通过物理模拟控制的运动,而是动画驱动的运动,这个时候我们需要做的是将动力学驱动的物理更新物理世界中的位置速度等等。
  3. PhysicsReplication->Tick(UseDeltaTime):网络同步的数据进行修正。如果这个Body物理的Actor是网络同步的并且做了物理模拟,我们就需要根据网络传来的数据修正自己的位置,而不能信任自己本地计算的位置。

物理解算器的物理计算

解算器的计算开始于FPhysScene的StartFrame,其调用有解算器的AdvanceAndDispatch_External。

其会根据我们不同的线程模式,来选择对应的异步逻辑,如果我们是单线程的模式直接运行。如果是GraphTask,则将FPhysicsSolverAdvanceTask交给Graph来执行。

当然不管是那种方式,执行代码都会调用到Solver里面的AdvanceSolverBy,进而执行AdvanceOneTimeStepTask::DoWork(); 从而进行在给定时间间隔内的物理世界的数据。当计算完成后,同步粒子数据,跟动画系统一致,我们需要多个不同的数据buffer来应对读写同步局面,所以我们会把我们计算得到的可写的数据buffer与之前的可读的数据buffer做翻转。

当然这些buffer存储在各自的Proxy身上。

Chaos::TTripleBufferedData<FSkeletalMeshPhysicsProxyInputs> InputBuffers;
Chaos::TBufferedData<FSkeletalMeshPhysicsProxyOutputs> OutputBuffers;
FSkeletalMeshPhysicsProxyInputs* NextInputProducerBuffer;				
const FSkeletalMeshPhysicsProxyOutputs* CurrentOutputConsumerBuffer;	

物理结束

当Game线程中的Tick走完全了,会调用FEndPhysicsTickFunction::ExecuteTick,其会检查异步的PhysicsSolver是否处理完全,如果没有处理完全,会阻断主线程,直到我们物理线程处理完自己的事情。如果处理完成,调用FChaosScene::EndFrame。

在EndFrame中,我们将会把Solve里面的加速结构的数据拷贝出来。然后执行SyncBodies,用拷贝出来的加速数据结构更新Game线程的Mesh数据位置。 在OnSyncBodies中,我们将物理解算器中标记为Dirty的Proxy的PhysicsProxy和对应的Component找到,然后更新他们的位置数据。