UE4物理模块分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012999985/article/details/78242493

一.Mesh组件与物理之间的关系


         关于UE物理的基本使用,官方文档以及我之前的文档已经做了较为详细的介绍。( 官方文档链接UE4蓝图碰撞检测解析UE4碰撞规则详解(2016.7.12更新)

         这里主要是从代码方面,简单分析一下UE4里面的物理是如何使用与生效的,StaticMesh以及SkeletalMesh对应的物理都是如何产生与作用的。第三部分会涉及到一些PhyX引擎的内容,简单谈谈UE与PhysX间的交互。

         首先,在游戏中常见的带有物理的物体一般有5种,胶囊体一类、静态网格物体StaticMesh、骨骼网格物体SkeletalMesh、Landscape地形以及PhysicsVolume(BrushComponent)。这5种类型本质上产生物理的规则都大同小异,为了方便我们只针对StaticMesh与SkeletalMesh来总结。

         对于直接放在场景的石块等,通常是通过3D建模软件导入到引擎中的资产。导入到引擎资源文件夹后就石块模型就以UStaticMesh类存在于引擎中,从资源文件夹拖到场景中后场景中的该对象就变成了StaticMeshActor。对于带动画表现的玩家模型,是通过3D建模软件导入的带骨骼信息的资产。导入到引擎资源文件夹后就以USkeletalMesh类存在于引擎中,从资源文件夹拖到场景中后场景中的该对象就变成了SkeletalMeshActor。当然,单独把没有任何处理的SkeletalMeshActor放在场景中看起来就如一个StaticMeshActor一样,没有任何动画,也可能没有任何物理。

         由于UE4提倡组件式的开发,Actor身上的很多特性都是通过组件提供的,所以物理数据都是挂在组件上的而不是Actor上。任何Actor上面都可以挂上N多个组件,因此一个玩家身上就可以有多个UStaticMeshCompnent与USkeletalMeshCompnent(一般还有一个胶囊体作为根组件)。举个简单的例子,玩家自身的模型是一个USkeletalMeshCompnent,然后身上的衣服装备就可以用一个UStaticMeshCompnent来表示。二者最大的差别就是SkeletalMesh可以产生动画。为了表现的更精确,他的每一个骨骼都可以产生对应的物理,而随着动画的变化他自身的每个骨骼物理也就需要跟随动画而改变,所以相比StaticMesh要复杂不少。

         对于一个静态网格物体StaticMesh,他的物理一般在建模软件里面就应该创建好,导入到编辑器时UE就会根据导入的数据创建物理信息,当然UE4本身也提供了物理碰撞的创建,如图1-1所示。不过无论哪种做法,本质上都是在编辑器里给UStaticMeshComponent构建一个UBodySetup,在开始游戏的时候在创建运行时的基本物理数据UBodyInstance。


图1-1 UE编辑器添加碰撞


(UBodySetup与UBodyInstance:我个人理解UBodySetup就是一个静态的物理数据,一般在在游戏运行前就已经构建好了[当然,你在游戏运行时创建也没什么问题]。你可以理解为一个类,编译以后就存在了。而UBodyInstance是一个在游戏时真正起作用的物理数据,可以理解为通过这个类创建的对象,运行时才真正出现。通过一个UBodySetup是可以创建出多个UBodyInstance的)

而对于骨骼网格物体SkeletalMesh,由于数据比较多,他的物理数据存储在PhysicsAsset里面。在游戏运行的时候,SkeletalMeshComponent会读取物理资产里面的数据UBodySetup随后再通过UBodySetup给角色创建对应的基本物理数据UBodyInstance。再进一步深入就是NVIDA的PhysX物理引擎了(当然你也可以采用BOX2D物理引擎),这篇文档后面会有简单的讲解。

(UE4里面除了SkeletalMeshComponent.cpp以外还有SkeletalMeshComponentPhysics.cpp,PhysAnim.cpp用来专门处理SkeletalMeshComponent物理相关的逻辑)


图1-2 物理资产


下面的图片描述了Mesh、 Component与物理基本类的基本关系


图1-3 物理相关类图


         如果对BodyInstance还是觉得比较陌生的话,不妨结合下面我们熟悉的类图来理解。我们知道在给Mesh设置物理的时候需要设置准确的碰撞通道,才能让不同的物理之间有碰撞效果。仔细看一下,CollisionResponses,ObejctType这些其实都是FBodyInstance里面的成员,我们在编辑器里面设置的这些属性其实就是在给BodyInstance设置(进一步还会去给到PhysX里面的PxRigidActor,后面讲)。

图1-4


         如果我们想在编辑器里直观的看到是否创建了物理就调用控制台命令Show Collision既可。下图就显示了角色的胶囊体碰撞以及对应的骨骼物理碰撞(多个胶囊体组合)。


图1-5


二.物理的创建流程


         前面大致的描述了UE4里面基本组件与物理之间的逻辑关系,我们看到上面无论是Staticmesh还是SkeletalMesh都会通过BodySetup来创建物理,而BodySetup最终又会调用BodyInstance来产生真正的物理。下面我们从游戏内具体的物理初始化流程分析一下。


2.1UStaticMeshComponent物理的创建


         首先是UStaticMeshComponent,可以看到在场景里面加载Actor并注册UActorComponent的时候会对UPrimitiveComponent组件进行物理信息创建。其实除了UStaticMeshComponent以外,所有继承自UPrimitiveComponent的组件(第一部分提到的那5种都是)都会在注册后就创建物理数据(对于直接继承自UActorComponent的组件,如移动组件就不会执行该操作)。因此除了SkeletalMeshComponent以外(这个后面再分析),其他继承自UPrimitiveComponent的组件物理创建的时机都很明确,也就是UActorComponent被注册的时候创建物理。(当然还有一些特殊情况也需要更新物理,比如更换模型的时候)

图2-1 加载场景StaticMesh物理的创建堆栈图



图2-2 玩家出生时胶囊体物理的创建堆栈图


在注册组件时是否要创建物理数据?可以参考下面代码。其实很明显的有三个条件,

1.      是否已经创建过了

2.       是否能获取到当前的物理场景(物理场景的变量为FPhysScene*    PhysicsScene,理解为与游戏世界同时存在的一个物理世界。这个PhysicsScene一般是在初始化World信息,也就是在void InitWorld(const InitializationValues IVSInitializationValues())时创建

3.      是否应该创建ShouldCreatePhysicsState。很明显想控制是否给组件创建对应的物理数据,写在这里最合适不过了。比如,所有继承自UActorComponent而且没有重写该函数的组件都会直接返回false,而UPrimitiveComponent就重写了这个函数。


图2-3 注册时是否创建物理的条件代码截图


2.2 USkeletalMeshComponent物理的创建


         USkeletalMeshComponent与其他带物理组件不同,由于他的物理比较复杂,涉及到PhysicsAsset,所以他需要重写基类的物理创建流程。另外,一般来说我们并不想会让玩家的骨骼物理一直存在着。原因很简单,就是为了提升性能。对于一般的带物理的组件,我们只需要给他配置一个简单的碰撞体既可(PhysX里面就包括了Sphere,Box,Capsule这几种)。这样一个简单的物理组件在游戏运行时的开销是很小的,然而对于一个USkeletalMeshComponent,我们为了精确几乎需要给所有的骨骼都创建一个基本的物理单位,一旦玩家或者NPC过多,这个消耗是非常可观的。然而,我们也不能放弃使用USkeletalMeshComponent的物理,因为一旦我们的游戏想实现精准的打击,攻击不同位置的效果不同的时候,就必须要用到骨骼的物理。因此,常见的解决方案就是在需要的时候创建物理,在不需要的时候就拿掉。(默认引擎的SkeletalMesh物理会一直存在参考图1-5,需要我们自己处理,后面会简单给出一个解决思路)

         我们还是从组件的注册说起,USkeletalMeshComponent的物理的初始化与前面的组件不同,他首先重载了void USkeletalMeshComponent::CreatePhysicsState()函数。并通过调用InitArticulated函数来对所有的骨骼来进行物理的初始化,这是组件初始化时的逻辑代码。我们简单分析一下,


图2-4 重载CreatePhysicsState代码截图


         可以看到USkeletalMeshComponent创建物理有两个执行路径,一种是和其他组件一样使用基类UPrimitiveComponent的方法创建物理数据,另一种是用USkinnedMeshComponent里面的PhyiscsAsset数据。(bEnablePerPolyCollision这个变量默认是0,而且引擎没有修改过)所以,可以看出,正常的USkeletalMeshComponent初始化物理是通过函数void InitArticulated(FPhysScene*PhysScenebool bForceOnDedicatedServer false);来对每一个骨骼来初始化物理的(Articulate表示关节连接的)。如果开发者不做任何处理的话,那么USkeletalMeshComponent的物理数据就会在注册时创建并且在游戏过程中一直存在着。

一般来说,USkeletalMeshComponent在每帧TickComponent的时候都会调用到USkeletalMeshComponent::RefreshBoneTransforms函数,顾名思义就是更新骨骼的坐标旋转等。


图2-5 Tick更新骨骼Transform堆栈图


RefreshBoneTransforms函数里面,可以根据CPU核数等相关参数来决定是否开一个线程来单独更新动画以及相关物理数据(最后还是调用InitArticulated函数创建物理)。


图2-6 开启单独线程来处理动画物理数据


下面的堆栈图就是引擎通过单独开一个线程来处理物理等数据。


图2-7 单独线程来处理动画物理数据调用堆栈图


         前面我们提到要选择让物理在需要的时候去生成,而在一般状态下要拿掉。那这是如何做到的?其实我们可以在USkeletalMeshComponent::UpdateKinematicBonesToAnim 去处理,这个函数意义是根据动画的变换去更新当前的物理数据,每一帧都需要执行。基本思路就是,每帧都去检测是否需要骨骼物理数据,如果需要我们创建对应的物理数据(已经创建过了就直接返回)。如果检测到当前不再需要更新物理,就调用USkeletalMeshComponent::TermArticulated()删除物理数据。我们已经知道,运行中的物理数据全部存储在BodyInstance里面,而这个函数就会把我们当前存储在Bodies里面的所有BodyInstance数据全部清除。忘记Bodies的朋友可以回头看一下USkeletalMeshComponent的类图。

         同时这里还有一段注释可以参考一下:

// This below code produces some interesting resulthere

// - below codes update physics data, so if youdon't update pose, the physics won't have the right result

// - but if we just update physics bone withoutupdate current pose, it will have stale data

// If desired, pass the animation data to thephysics joints so they can be used by motors.

// See if we are going to need to update kinematics

const bool bUpdateKinematics = (KinematicBonesUpdateType != EKinematicBonesUpdateToPhysics::SkipAllBones); 可以把是否更新物理放到这个位置去处理。

       另外,对于USkeletalMeshComponent,初始化物理时会有bPhysicsRequiredOnDediServer等属性来控制在服务器模式下是否创建物理数据。


三.BodyInstance与PhysX物理引擎


         前面我们已经了解到BodyInstance在UE逻辑里是一个运行时的物理的基本单位。而实际在PhysX引擎中,也同样存在一个物理基本单位,这个物理单位就PxRigidActor。一个BodyInstance对应一个PxRigidActor(实际上就是BodyInstance::InitBody时创建一个对应的PxRigidActor),这样我们就可以将UE引擎与PhysX引擎结合起来使用了。
         这个时候,我再提出一个问题,真正的物理碰撞是如何检测的呢?
         这个问题确实值得我们深思,而且不同情况下检测的方法是不一样的。举个例子,想知道两个球是否产生碰撞,那么只要判断两个球心的距离就可以了。而两个复杂模型的碰撞,可能需要通过判断两个三角面是否有交集来判断。我这里提出这个问题,只是想提醒大家,物理引擎里面的Actor也一样需要知道其本身的形状,然后进一步来处理碰撞逻辑。所以,在创建一个基本物理单位PxRigidActor之后,我们还需要给其创建基本的几何形状(在引擎里面叫做Shape),这个逻辑的处理就在函数UBodySetup::AddShapesToRigidActor_AssumesLocked。看到这个函数,我们就知道Shape是通过UBodySetup来创建的,同时这个几何形状的数据也是存储在UBodySetup里面的。

PhysX里面提供的类型有下面几种:

1.       PxSphereGeometry         球形

2.      PxBoxGeometry                          盒子

3.      PxCapsuleGeometry          胶囊体(SkeletalMesh常用)

4.      PxConvexMeshGeometry  凸面体

5.      PxTriangleMeshGeometry 三角面

这5种类型里面,前4种的数据都存储在UBodySetup的FKAggregateGeom AggGeom;里面。

而最后三角面的物理数据则存储在UBodySetup的TriMesh与TriMeshNegX里面。

         凸面体与三角面物理数据特别的是,他需要经过一个物理Cook的过程,这个过程类似渲染,把所有的三角面的顶点信息和索引提供给PhysX引擎随后PhysX利用这些数据Cook出一个完整的碰撞模型,不过这个过程需要一定的时间来执行(在我们创建凸面体碰撞盒或者设置简单碰撞为复杂碰撞的时候)。所以如果想针对自己的一个特殊的ACtor来采用第5种方法,也应该更倾向于在游戏开始前就完成Cook过程,这样就不会影响游戏时的性能与体验。

         另外,我们在创建物理的时候还分为静态与动态两种,他们通过组件上的OwnerComponent->Mobility!= EComponentMobility::Movable来控制。很明显,静态碰撞与动态碰撞的消耗是不同的。

         //创建静态PxRigidActor

         GPhysXSDK->createRigidStatic(PTransform);

         //创建动态PxRigidActor

GPhysXSDK->createRigidDynamic(PTransform);

         截止到这里,我们已经基本上完成了物理数据的初始化。然而,我们知道在游戏里面,还有很多详细的设置,比如碰撞通道,碰撞类型等。这些数据也必须要及时更新与处理,这些逻辑与相关标记的处理在FBodyInstance::UpdatePhysicsFilterData->FBodyInstance::UpdatePhysicsShapeFilterData里。UpdatePhysicsFilterData是总的物理数据的处理(因为可能还有其他的物理引擎,如Box2D等)。而UpdatePhysicsShapeFilterData是真正的UE逻辑与PhysX逻辑交互的地方。实际上我们的平时做的碰撞设置CollisionEnabled对应到Physx里面就是这两个操作。PShape->setFlag(PxShapeFlag::eSCENE_QUERY_SHAPEtrue);PShape->setFlag(PxShapeFlag::eSIMULATION_SHAPEfalse);(参考后面的结构体)。

下面是PhysX里面的Shape标记

structPxShapeFlag

{

           enumEnum

         {

                   eSIMULATION_SHAPE                              =(1<<0),

                   eSCENE_QUERY_SHAPE                                   =(1<<1),

                   eTRIGGER_SHAPE                                               =(1<<2),

                   eVISUALIZATION                                       =(1<<3),

                   ePARTICLE_DRAIN                                             =(1<<4)

         };

};

    关于UEPhysX之间的交互,就简单介绍到这里。至于更详细的内容,大家有兴趣的话就去源码里面进一步了解吧。

    最后,再推荐一篇UE物理相关的文档   UE4物理引擎模块分析


原文链接(转载请标明):http://blog.csdn.net/u012999985/article/details/78242493

没有更多推荐了,返回首页