虚幻引擎4使用 PhysX 3.3 物理引擎来模拟物理效果。所有物理运动(坠落或受力的物理形体)以及碰撞(物理形体的相互作用)都由 PhysX 管理。
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
UE4 使用NVIDIA 的PhysX 3.3物理引擎来模拟物理效果,使用 APEX 模拟 destruction 和 clothing。对于PhyX,它对于UE4来说就是一个提供输入然后获取输出的黑 盒,不过可以通过NVIDIA提供的PhysX Visual Debugger(PVD)来进行可视化调试在编辑器运行游戏 输入pvd connect
我们就可以得到实时的物理调试结果。
整个debug软件是比较重的,功能众多,如果大家在项目中遇到了一下物理上的性能问题,强烈建议可以打开pvd看下场景里的动态rigidbody情况,碰撞穿插解算的复杂度,一般严重的性能问题都是在这方面的。
它首先是Actor,作为Scene的基础实体,然后可以分成静态的和动态的,静态的如房子,各种场景部件,有自带的预处理功能,处理碰撞等运算性能高很多,动态的像人,车,可以运动,但性能会差一些,动态Rigidbody里用的比较多的是PxRigidDynamic,而Articulation是专门给类似布娃娃这样的系统设计的。
UE4 里面使用的几何体完全匹配于Physx的类型。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ahgp49fb-1610371783024)(https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomTypeSphere.png)]
需要注意的问题是,凸包的在Physx里面限定的个数是255.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UQ2XCBxG-1610371783032)(https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomTypeMesh.png)]
对于凸包和三角Mesh 的来说,将会发生Cooking。这是PhysX提供的一种转换、序列化批量数据的一种方式。这是一种对于凸包和Mesh 的预处理。Physx需要将你提供的凸包和三角形变成他们自己的结构去加速他们自己的计算。
在UE4中,有复杂碰撞和简单碰撞的区别,复杂碰撞仅仅指的是将整个的Mesh传入的形式,而凸包依旧属于简单碰撞。
场景查询的种类持支三种,分别是Raycast,Sweep和Overlap检测。其中:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HM99m9nm-1610371783033)(https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomQueryRaycast.png)]
Raycast检测是从一点,投射定长长度的线段,检测物理场景中跟这个线段订交的碰撞体(PxShape);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vPWgvOV4-1610371783034)(https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomQueryOverlap.png)]
overlop它是给定某置位的一个形体(PxGeometry),检测物理天下中跟这个形体相交的碰撞体(PxShape)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yJQeFg1d-1610371783035)(https://gameworksdocs.nvidia.com/PhysX/3.3/PhysXGuide/_images/GeomQuerySweep.png)]
Sweep检测跟Raycast检测似类,但是投射出去的是一个或者一组形体(PxGeometry),检测物理天下中跟这个/组形体相交的碰撞体(PxShape);
在场景查询执行的方式上, PhysX SDK为我们供提了两种执行的方式,一种是即立行执行的单次提交,马上回返结果的式方;另一种是批理处查询(Batch Query),我们可以把一帧中可以会合到一同执行的查询,都加入到一个批理处中执行,这个在场景查询很多的时候,可以带来能性上的升提升。
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。
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做扩展的。
Physx在UE4的主要功能接口都放在FPhysScene_PhysX这个类里,文件是PhysScene_PhysX。
Physx初始化接口是InitPhysScene,调用位置是在UWorld的InitWorld里,会有个CreatePhysicsScene。首先他会进行设置线程数,分配不同的CpuDispatcher
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();
}
}
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,如下:
void FPhysXSimEventCallback::onContact(const PxContactPairHeader& PairHeader, const PxContactPair* Pairs, PxU32 NumPairs)
PairHeader会带着两个碰撞的actor,注意,这里的actor还是Physx层面的actor,UE4引擎对应的actor保存在物理actor的userData里。
当得到两个actor以及碰撞材质,碰撞点之类的数据后,相关的信息就会被放在一个PendingCollisionNotifies的数组中,这里还是在异步线程里,所以还不能直接调用相关主线程使用的脚本接口。
void FPhysScene_PhysX::EndFrame(ULineBatchComponent* InLineBatcher)
然后在物理模拟结束后的endFrame里,调用DispatchPhysNotifications_AssumesLocked,把PendingCollisionNotifies拿出来遍历倒腾一遍,拿出actor,回调DispatchPhysicsCollisionHit,就可以把触发事件分发到各个引擎actor里了。
之后我们会创建PxSceneDesc,进而生成PxScene。
void UWorld::Tick( ELevelTick TickType, float DeltaSeconds )
在world::Tick中,我们会首先更新我们的物理场景
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
AActor::SetTickGroup(ETickingGroup NewTickGroup)
在SetTickGroup之前,会运行SetupPhysicsTickFunctions
其中会进行两个TickFunction 的Setup
其中StartPhysicsTickFunction是在 被标注为TG_StartPhysics,EndPhysicsTickFunction被标注为TG_EndPhysics。所有他们两个会在指定的Group时机调用。
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 的主要逻辑是调用到
FPhysScene_PhysX::StartFrame()
FPhysScene_PhysX::TickPhysScene
在这个函数里面,主要是4个功能的处理,物理deltaTime的计算,与骨骼绑定的Kinematic碰撞盒位置计算,进行simulate或者substep simulate,以及运行SceneCompletionTask做最后的fetchResult。
主要执行到
void FPhysScene_PhysX::EndFrame(ULineBatchComponent* InLineBatcher);
首先EndFrame里主要执行的就是DispatchPhysNotifications_AssumesLocked,为了把这一整个渲染帧的时间里,物理模拟得到的碰撞盒事件反馈分发到各个UE的对象上。
另外就是调用AddDebugLines,做一些物理debug方面的绘制。
这里将介绍在Mesh的粒度上的物理是如何使用和运行的。
这里的创建可以分为两个不同的理解范畴,第一种是对于这个Mesh的物理形状的定义和编辑,另一种是在实例化一个Mesh Component 或者一个MeshActor,拿之前定义好的的物理形状真正的添加到场景中并进行初始化的过程。
我们拿到一个Static Mesh 后, 我们有两种方法去定义他的物理资源,一种是可以通过美术指定的低面数的Mesh来进行指定,另一种是可以在引擎编辑器中去生成其物理碰撞资源。
不管是哪种形式,其最终都会把物理资源存储在一个名为UBodySetup上面。
UBodySetup并不在Component上的固定位置,对于不同的MeshComponent,其存储位置是不同的。
例如对于StaitcMeshComponent, UBodySetup存储在其内部的UStaitcMesh身上。而对于BrushComponent或者ProduralMeshComponent等动态生成的Mesh,由于本身并没有序列化的Mesh资源和实例化的能力,所以他的 UBodySetup是直接存储在自己的Component上面。
我们可以从UE里面的碰撞添加的地方,自动的添加动态算好的物理碰撞,并且我们能够在编辑器里进行拖动和修改。
对于一个UBodySetup来说,里面可以存储多个不同的物理碰撞体,也就是说,我们实际是可以通过多种不同的物理碰撞体来拼凑出一个复杂的static ,这里和skeletalMesh 的处理方式并不一致,skeletalMesh是具有多个不同的UBodySetup,这能够让他可以不保持局部的相对位置,而是通过约束在在一起。
BodySetup里面,能够包含的物理性状是有限的,其类型是跟PhysX里面的类型是一致的。其存储在UBodySetup::AggGeom中。
骨骼网格物理的创建较复杂,其物理数据需要UPhysicsAsset的物理资产单独提供。
USkeletalMesh类似于UStaticMesh,不过对应的是物理资产编辑器。UPhysicsAsset::BodySetup是一个TArray的数组,在物理资产编辑器中,你可以为每个骨骼添加不同形状(胶囊体、Box、球体和凸面体)的简单碰撞,编辑器会自动调用MakeNewBody函数为该骨骼创建一个新的UBodySetup,并加入到UPhysicsAsset::BodySetup数组中。
跟静态网格一样,碰撞体的几何数据保存在UBodySetup::AggGeom结构中。在该函数创建完成后,会遍历所有使用该物理资产的骨骼网格组件,并调用USkleletalMeshComponent::RecreatePhysicsState()函数重新创建组件的物理状态,该函数接下来会介绍。
除了较为复杂的skeletalMesh,物理集合体构建后都会存储在UPrimitiveComponent的BodyInstance中,并且Setup一一对应一个instance。
而基本所有的物理的初始化都是在Component进行注册的时候RegisterComponent
UActorComponent::CreatePhysicsState
调用对应子类的OnCreatePhysicsState
。
值得注意的是,通常在此时,会顺带进行寻路系统的数据更新,
FNavigationSystem::UpdateComponentData(*this);
.
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)
}
}
}
对于 SkeletalMeshComponent来说,我们将所有的数据存储在UPhysicsAsset中。
UPROPERTY(instanced)
TArray SkeletalBodySetups;
UPROPERTY(instanced)
TArray ConstraintSetup;
这里我们将有两种重要的SetUp,第一个是SkeletalBodySetups,其类型USkeletalBodySetup是继承自UBodySetup。主要是增加了一些查找函数。
而ConstraintSetup也就是一个约束。是对于skeletalmesh所独有的部分,其链接两个不同的USkeletalBodySetup,作为一个关节来使用。这在Physx中称作Articulations。
对于SkeletalMesh的初始化依旧是在OnCreatePhysicsState函数中进行的,不过这里会有一些不同的地方。
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();
}
再说
公众号:UE4 虚幻引擎源码解析