Contents

UE4 物理系统实现

虚幻引擎4使用 PhysX 3.3 物理引擎来模拟物理效果。所有物理运动(坠落或受力的物理形体)以及碰撞(物理形体的相互作用)都由 PhysX 管理。

一.Physx

1.1Physx简介

UE4.21前的版本采用的是NVIDIA的PhysX做为其默认的物理引擎,用于计算3D世界的碰撞查询与物理模拟。自4.21版本开始重构了代码调用,兼容使用Chaos物理系统,4.26才会实装,如要使用的话是需要自己构建的。

由于Epic 和NVIDIA的PY交易。Epic为UE4开发者们提供PhysX 3.3.3的基于CPU实现的二进制代码访问权,而且还包括其C++源代码访问权,以及布料库和可破坏物体库。 现在除了可以获得虚幻引擎4的完整C++源代码外,还可以查看和修改此PhysX代码。

参见:https://ue4community.wiki/legacy/physx-integrating-physx-code-into-your-project-ryzw4tj3

Physx文档:https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/Index.html

1.1.1使用debug 模式检测物理

UE4 使用NVIDIA 的PhysX 3.3物理引擎来模拟物理效果,使用 APEX 模拟 destruction 和 clothing。对于PhyX,它对于UE4来说就是一个提供输入然后获取输出的黑 盒,不过可以通过NVIDIA提供的PhysX Visual Debugger(PVD)来进行可视化调试在编辑器运行游戏 输入pvd connect我们就可以得到实时的物理调试结果。

整个debug软件是比较重的,功能众多,如果大家在项目中遇到了一下物理上的性能问题,强烈建议可以打开pvd看下场景里的动态rigidbody情况,碰撞穿插解算的复杂度,一般严重的性能问题都是在这方面的。

1.1.2基本数据结构

  1. PxPhysics:用来创建Physx各种实用组件,像场景,Shape,Rigidbody,布料,粒子系统等。
  2. PxScene:物理场景,是碰撞盒组件的模拟环境,它的创建是根据PxSceneDesc属性来的。一个大型系统可以有多个场景,不同场景的组件相互不影响,比如UE4,使用了2个场景,用来模拟同步和异步的物理模拟功能。
  3. PxSceneDesc:有如下属性:
    1. gravity 重力大小和方向,
    2. PxSimulationEventCallback 模拟事件回调,
    3. PxContactModifyCallback 碰撞解算修改回调,
    4. PxCCDContactModifyCallback CCD的碰撞解算修改回调,
    5. filterShader 全局的碰撞分类处理函数,
    6. cpuDispatcher Cpu线程分配器
  4. PxCooking:碰撞盒得有形状,我们知道物理引擎里碰撞盒不仅仅是只有胶囊体,长方形,球形,还有凸包体,复杂的静态地表碰撞盒,三角形面片构成的碰撞盒。对于比较复杂的碰撞盒,Phyx支持用PxCooking类接受面片数据,然后create出可以使用的碰撞mesh。
  5. Rigidbody: https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxguide/_images/ObjectModel.png

它首先是Actor,作为Scene的基础实体,然后可以分成静态的和动态的,静态的如房子,各种场景部件,有自带的预处理功能,处理碰撞等运算性能高很多,动态的像人,车,可以运动,但性能会差一些,动态Rigidbody里用的比较多的是PxRigidDynamic,而Articulation是专门给类似布娃娃这样的系统设计的。

  1. PxShape:SimulationFilterData 这个是做碰撞模拟类别划分的,这边设定好SimulationFilterData,在场景下的filterShader进行区分,可以控制哪些碰撞shape之间是可以相互进行碰撞计算的。
  2. PxMaterial:DynamicFriction,StaticFriction动摩擦,静摩擦,Restitution弹性系数,FrictionCombineMode 摩擦力计算的方式,比如两个物体碰撞了,摩擦力双方不同,可以选择取最小的,最大的,或者平均一下,RestitutionCombineMode,弹力计算模式,摩擦力的一样。

1.1.3几何体

UE4 里面使用的几何体完全匹配于Physx的类型。

  1. 胶囊体
  2. box
  3. 平面
  4. 凸包
  5. 三角形Mesh

https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomTypeSphere.png

https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomTypeCapsule.png

https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomTypeBox.png

https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomTypePlane.png

https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomTypeConvex.png

需要注意的问题是,凸包的在Physx里面限定的个数是255.

https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomTypeMesh.png

1.1.3.1Cooking

对于凸包和三角Mesh 的来说,将会发生Cooking。这是PhysX提供的一种转换、序列化批量数据的一种方式。这是一种对于凸包和Mesh 的预处理。Physx需要将你提供的凸包和三角形变成他们自己的结构去加速他们自己的计算。

在UE4中,有复杂碰撞和简单碰撞的区别,复杂碰撞仅仅指的是将整个的Mesh传入的形式,而凸包依旧属于简单碰撞。

1.1.4Phyx的场景查询

场景查询的种类持支三种,分别是Raycast,Sweep和Overlap检测。其中:

https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomQueryRaycast.png

Raycast检测是从一点,投射定长长度的线段,检测物理场景中跟这个线段订交的碰撞体(PxShape); https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomQueryOverlap.png

overlop它是给定某置位的一个形体(PxGeometry),检测物理天下中跟这个形体相交的碰撞体(PxShape)。

https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomQuerySweep.png

Sweep检测跟Raycast检测似类,但是投射出去的是一个或者一组形体(PxGeometry),检测物理天下中跟这个/组形体相交的碰撞体(PxShape);

在场景查询执行的方式上, PhysX SDK为我们供提了两种执行的方式,一种是即立行执行的单次提交,马上回返结果的式方;另一种是批理处查询(Batch Query),我们可以把一帧中可以会合到一同执行的查询,都加入到一个批理处中执行,这个在场景查询很多的时候,可以带来能性上的升提升。

1.2Phyx的多线程处理机制

1.2.1Physx的双缓存机制

https://gameworksdocs.nvidia.com/PhysX/4.1/documentation/physxguide/_images/timeSlots.png

Physx的tick分成simulate和fetchResult 2个步骤,其中simulate是负责每一个物理帧模拟计算,具体的步骤比如粗测(BroadPhase)->细测(Narrow Phase)->碰撞解算(Solver Stage),而fetchResult是结合simulate的数据buffer和用户更改后得到的数据buffer,得到最终的物理模拟数据。

在图中的1阶段时,物理引擎本身的模拟并没有运行,程序员可以调用physx的api做各种操作,更改碰撞实体的位置,加力,增加减少实体,在调用simulate的时候,之前处理后的数据结果被保存到一个buffer里,提交引擎进行模拟计算,这是第一个buffer。

在2阶段的时候,simulate这个接口调用完毕后,就已经把数据提交物理线程处理,注意是其他的线程,已经和主线程分开了,这里会有2个buffer同时存在的状态,引擎模拟计算的时候会独立使用它的buffer数据,在这个时候如果程序员在主线程调用api更改碰撞盒实体,这个api调用是有效的,但是更改的是主线程存在的数据buffer,两者在2阶段相互不干扰,程序开发在这个阶段调用的api也能马上获得结果,和1阶段没有区别,但是相应api的更改对引擎可见还需要fetchResult后,下一次调用simulate才管用。

另外在fetchResult时,程序在api调用的改动结果会覆盖引擎模拟的结果,以这样的规矩合并buffer。

1.2.2Physx的多线程任务分配

Physx内部其实有他自己的一套流程的,在他初始化启动的时候,会定义好CpuDispatcher和TaskManager,然后产生几个常驻的物理线程,当有新的任务产生的时候,会采用引用计数的方式建立不同任务的依赖,根据顺序运行。

TaskManager是管理任务间依赖关系,然后分发准备好的任务给负责的dispatcher,CPU和GPU各自有一个dispatcher在TaskManager中。TaskManager是由SDK创建的,每一个PxScene会分配自己的TaskManager实例,这个TaskManager实例可以通过PxSceneDesc或者直接通过TaskManager的接口进行配置。

CpuDispatcher就是一个类,SDK用于操作应用程序的线程池,通常来说每一个应用只有一个CpuDispatcher,如果一个应用由多个场景(scenes),CpuDispatcher可能会被多个TaskManager分享。

在UE4引擎中,会根据不同的配置建立不同数量的CpuDispatcher。一般的引擎也都是自定义相应的cpuDispatcher,以便于让Physx的多线程运行和自己的多线程模式相统一,unity是用他的jobSystem做扩展的。

二.UE4 物理系统

2.1物理系统概述

2.1.1物理场景初始化

Physx在UE4的主要功能接口都放在FPhysScene_PhysX这个类里,文件是PhysScene_PhysX。

2.1.2初始化多线程

Physx初始化接口是InitPhysScene,调用位置是在UWorld的InitWorld里,会有个CreatePhysicsScene。首先他会进行设置线程数,分配不同的CpuDispatcher

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
	int64 NumPhysxDispatcher = 0;
	FParse::Value(FCommandLine::Get(), TEXT("physxDispatcher="), $$NumPhysxDispatcher);
	if (NumPhysxDispatcher == 0 && FParse::Param(FCommandLine::Get(), TEXT("physxDispatcher")))
	{
		NumPhysxDispatcher = 4;	//by default give physx 4 threads
	}

	// Create dispatcher for tasks
	if (PhysSingleThreadedMode())
	{
		CPUDispatcher = new FPhysXCPUDispatcherSingleThread();
	}
	else
	{
		if (NumPhysxDispatcher)
		{
			CPUDispatcher = PxDefaultCpuDispatcherCreate(NumPhysxDispatcher);
		}
		else
		{
			CPUDispatcher = new FPhysXCPUDispatcher();
		}

	}

2.1.3建立事件回调

1
2
3
4
	SimEventCallback = SimEventCallbackFactory.IsValid() ? SimEventCallbackFactory->Create(this) : new FPhysXSimEventCallback(this);
	SimFilterCallback = new FSimulationFilterCallback(this);
	ContactModifyCallback = ContactModifyCallbackFactory.IsValid() ? ContactModifyCallbackFactory->Create(this) : nullptr;
	CCDContactModifyCallback = CCDContactModifyCallbackFactory.IsValid() ? CCDContactModifyCallbackFactory->Create(this) : nullptr;

UE4目前设置了Physx的三种事件回调,分别是simulationEventCallback(这个是我们最常用的,包含碰撞,trigger之类的回调),contactModifyCallback以及CCDContactModifyCallback(这个的回调在碰撞迭代之前的,比如我们要做一个这样的功能,扔一个球,当球碰到墙壁的时候,可以检测墙壁对应位置有没有洞,如果有洞,就取消碰撞反馈通过,这样就需要一个碰撞到的通知,但是要在碰撞反馈发生之前,contactModifyCallback就是干这个的,ccd是连续碰撞检测版本)。

我们在使用UE4碰撞系统的时候,只需要在对应的actor里写好固定的回调函数就好。

引擎里是在FPhysXSimEventCallback(继承于PxSimulationEventCallback)里写总的回调接口,捕获Physx的事件,比如onContact,onTrigger,如下:

1
2
void FPhysXSimEventCallback::onContact(const PxContactPairHeader& PairHeader, const PxContactPair* Pairs, PxU32 NumPairs)

PairHeader会带着两个碰撞的actor,注意,这里的actor还是Physx层面的actor,UE4引擎对应的actor保存在物理actor的userData里。

当得到两个actor以及碰撞材质,碰撞点之类的数据后,相关的信息就会被放在一个PendingCollisionNotifies的数组中,这里还是在异步线程里,所以还不能直接调用相关主线程使用的脚本接口。

1
void FPhysScene_PhysX::EndFrame(ULineBatchComponent* InLineBatcher)

然后在物理模拟结束后的endFrame里,调用DispatchPhysNotifications_AssumesLocked,把PendingCollisionNotifies拿出来遍历倒腾一遍,拿出actor,回调DispatchPhysicsCollisionHit,就可以把触发事件分发到各个引擎actor里了。

2.1.4建立PxScene

之后我们会创建PxSceneDesc,进而生成PxScene。

2.2物理场景tick

1
void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )

在world::Tick中,我们会首先更新我们的物理场景

  1. TG_PrePhysics 在物理模拟前运行
  2. TG_DuringPhysics 和物理模拟一起运行
  3. TG_PostPhysics 在物理模拟之后运行
  4. TG_PostUpdateWork 在所有Tick 运行之后(包括Timer和Tickableobject)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
	if (bDoingActorTicks)
		{
			SetupPhysicsTickFunctions(DeltaSeconds);
			TickGroup = TG_PrePhysics; // reset this to the start tick group
			FTickTaskManagerInterface::Get().StartFrame(this, DeltaSeconds, TickType, LevelsToTick);

			SCOPE_CYCLE_COUNTER(STAT_TG_StartPhysics);
				SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_StartPhysics"), 10);
				CSV_SCOPED_SET_WAIT_STAT(StartPhysics);
				RunTickGroup(TG_StartPhysics);
			}
			{
				SCOPE_CYCLE_COUNTER(STAT_TG_DuringPhysics);
				SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_DuringPhysics"), 10);
				CSV_SCOPED_SET_WAIT_STAT(DuringPhysics);
				RunTickGroup(TG_DuringPhysics, false); // No wait here, we should run until idle though. We don't care if all of the async ticks are done before we start running post-phys stuff
			}
			TickGroup = TG_EndPhysics; // set this here so the current tick group is correct during collision notifies, though I am not sure it matters. 'cause of the false up there^^^
			{
				SCOPE_CYCLE_COUNTER(STAT_TG_EndPhysics);
				SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_EndPhysics"), 10);
				CSV_SCOPED_SET_WAIT_STAT(EndPhysics);
				RunTickGroup(TG_EndPhysics);
			}
			{
				SCOPE_CYCLE_COUNTER(STAT_TG_PostPhysics);
				SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_PostPhysics"), 10);
				CSV_SCOPED_SET_WAIT_STAT(PostPhysics);
				RunTickGroup(TG_PostPhysics);
			}
		}
		...
		if (bDoingActorTicks)
		{
			SCOPE_CYCLE_COUNTER(STAT_TickTime);
			{
				SCOPE_CYCLE_COUNTER(STAT_TG_PostUpdateWork);
				SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - PostUpdateWork"), 5);
				RunTickGroup(TG_PostUpdateWork);
			}
			{
				SCOPE_CYCLE_COUNTER(STAT_TG_LastDemotable);
				SCOPE_TIME_GUARD_MS(TEXT("UWorld::Tick - TG_LastDemotable"), 5);
				RunTickGroup(TG_LastDemotable);
			}

			FTickTaskManagerInterface::Get().EndFrame();
		}

这里的Group是根据Actor里面的SetTickGroup,来确定其是属于什么Group

1
AActor::SetTickGroup(ETickingGroup NewTickGroup)

在SetTickGroup之前,会运行SetupPhysicsTickFunctions 其中会进行两个TickFunction 的Setup

  1. StartPhysicsTickFunction
  2. EndPhysicsTickFunction

其中StartPhysicsTickFunction是在 被标注为TG_StartPhysics,EndPhysicsTickFunction被标注为TG_EndPhysics。所有他们两个会在指定的Group时机调用。

2.2.1开始物理场景tick

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
USTRUCT()
struct FStartPhysicsTickFunction : public FTickFunction
{
	GENERATED_USTRUCT_BODY()

	class UWorld*	Target;

	virtual void ExecuteTick(float DeltaTime, enum ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent) override;

	virtual FString DiagnosticMessage() override;
	virtual FName DiagnosticContext(bool bDetailed) override;
};

其中tick 的主要逻辑是调用到

1
2
FPhysScene_PhysX::StartFrame()
FPhysScene_PhysX::TickPhysScene

在这个函数里面,主要是4个功能的处理,物理deltaTime的计算,与骨骼绑定的Kinematic碰撞盒位置计算,进行simulate或者substep simulate,以及运行SceneCompletionTask做最后的fetchResult。

2.2.2结束物理场景tick

主要执行到

1
void FPhysScene_PhysX::EndFrame(ULineBatchComponent* InLineBatcher);
  1. 首先EndFrame里主要执行的就是DispatchPhysNotifications_AssumesLocked,为了把这一整个渲染帧的时间里,物理模拟得到的碰撞盒事件反馈分发到各个UE的对象上。

  2. 另外就是调用AddDebugLines,做一些物理debug方面的绘制。

2.3组件的物理创建

这里将介绍在Mesh的粒度上的物理是如何使用和运行的。

这里的创建可以分为两个不同的理解范畴,第一种是对于这个Mesh的物理形状的定义和编辑,另一种是在实例化一个Mesh Component 或者一个MeshActor,拿之前定义好的的物理形状真正的添加到场景中并进行初始化的过程。

2.3.1物理碰撞体的创建

我们拿到一个Static Mesh 后, 我们有两种方法去定义他的物理资源,一种是可以通过美术指定的低面数的Mesh来进行指定,另一种是可以在引擎编辑器中去生成其物理碰撞资源。 不管是哪种形式,其最终都会把物理资源存储在一个名为UBodySetup上面。

2.3.2普通Mesh 的碰撞体的创建

UBodySetup并不在Component上的固定位置,对于不同的MeshComponent,其存储位置是不同的。

例如对于StaitcMeshComponent, UBodySetup存储在其内部的UStaitcMesh身上。而对于BrushComponent或者ProduralMeshComponent等动态生成的Mesh,由于本身并没有序列化的Mesh资源和实例化的能力,所以他的 UBodySetup是直接存储在自己的Component上面。

我们可以从UE里面的碰撞添加的地方,自动的添加动态算好的物理碰撞,并且我们能够在编辑器里进行拖动和修改。

对于一个UBodySetup来说,里面可以存储多个不同的物理碰撞体,也就是说,我们实际是可以通过多种不同的物理碰撞体来拼凑出一个复杂的static ,这里和skeletalMesh 的处理方式并不一致,skeletalMesh是具有多个不同的UBodySetup,这能够让他可以不保持局部的相对位置,而是通过约束在在一起。

BodySetup里面,能够包含的物理性状是有限的,其类型是跟PhysX里面的类型是一致的。其存储在UBodySetup::AggGeom中。

2.3.3SkeletalMesh 的碰撞体的创建

骨骼网格物理的创建较复杂,其物理数据需要UPhysicsAsset的物理资产单独提供。

USkeletalMesh类似于UStaticMesh,不过对应的是物理资产编辑器。UPhysicsAsset::BodySetup是一个TArray的数组,在物理资产编辑器中,你可以为每个骨骼添加不同形状(胶囊体、Box、球体和凸面体)的简单碰撞,编辑器会自动调用MakeNewBody函数为该骨骼创建一个新的UBodySetup,并加入到UPhysicsAsset::BodySetup数组中。

跟静态网格一样,碰撞体的几何数据保存在UBodySetup::AggGeom结构中。在该函数创建完成后,会遍历所有使用该物理资产的骨骼网格组件,并调用USkleletalMeshComponent::RecreatePhysicsState()函数重新创建组件的物理状态,该函数接下来会介绍。

2.4物理碰撞体的初始化

2.4.1普通Mesh 的碰撞体的初始化

除了较为复杂的skeletalMesh,物理集合体构建后都会存储在UPrimitiveComponent的BodyInstance中,并且Setup一一对应一个instance。

而基本所有的物理的初始化都是在Component进行注册的时候RegisterComponent UActorComponent::CreatePhysicsState调用对应子类的OnCreatePhysicsState

值得注意的是,通常在此时,会顺带进行寻路系统的数据更新,FNavigationSystem::UpdateComponentData(*this);.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void UPrimitiveComponent::OnCreatePhysicsState()
{
	Super::OnCreatePhysicsState();

	if(!BodyInstance.IsValidBodyInstance())
	{

		UBodySetup* BodySetup = GetBodySetup();
		if(BodySetup)
		{
			// Create new BodyInstance at given location.
			FTransform BodyTransform = GetComponentTransform();


			const FVector BodyScale = BodyTransform.GetScale3D();
			if(BodyScale.IsNearlyZero())
			{
				BodyTransform.SetScale3D(FVector(KINDA_SMALL_NUMBER));
			}

			// Create the body.
			BodyInstance.InitBody(BodySetup, BodyTransform, this, GetWorld()->GetPhysicsScene());		
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
			SendRenderDebugPhysics();
#endif // !(UE_BUILD_SHIPPING || UE_BUILD_TEST)


		}
	}
}

2.4.2SkeletalMesh 的碰撞体的初始化

对于 SkeletalMeshComponent来说,我们将所有的数据存储在UPhysicsAsset中。

1
2
3
4
5
6
7

	UPROPERTY(instanced)
	TArray<USkeletalBodySetup*> SkeletalBodySetups;


	UPROPERTY(instanced)
	TArray<class UPhysicsConstraintTemplate*> ConstraintSetup;

这里我们将有两种重要的SetUp,第一个是SkeletalBodySetups,其类型USkeletalBodySetup是继承自UBodySetup。主要是增加了一些查找函数。

而ConstraintSetup也就是一个约束。是对于skeletalmesh所独有的部分,其链接两个不同的USkeletalBodySetup,作为一个关节来使用。这在Physx中称作Articulations。

对于SkeletalMesh的初始化依旧是在OnCreatePhysicsState函数中进行的,不过这里会有一些不同的地方。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void USkeletalMeshComponent::OnCreatePhysicsState()
{
	// Init physics
	if (bEnablePerPolyCollision == false)
	{
		InitArticulated(GetWorld()->GetPhysicsScene());
		USceneComponent::OnCreatePhysicsState(); // Need to route CreatePhysicsState, skip PrimitiveComponent
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
		SendRenderDebugPhysics();
#endif
	}
	else
	{
		CreateBodySetup();
		BodySetup->CreatePhysicsMeshes();
		Super::OnCreatePhysicsState();	//If we're doing per poly we'll use the body instance of the primitive component
	}

	// Notify physics created
	OnSkelMeshPhysicsCreated.Broadcast();
}

2.5物理模拟

再说