# Unreal Chaos 导出为 PhysX 物理文件
# 概述
随着 Unreal 5.0 的发布,PhysX 库也从 Unreal 的源代码中被移除。Unreal 自此不再支持 PhysX 库,转而使用自研的 Chaos。随之而来的问题是:项目如果升级到 Unreal 5.0+ 版本,就必须放弃 PhysX 库,对于客户端来说影响比较有限,毕竟 Unreal 官方提供了全套的解决方案。但对于不使用 DS 的服务器来说,如果需要用到物理场景的数据,就变得棘手了起来。
常规管线中,客户端一般会通过 PhysX 库自带的序列化接口,把场景中 AActor 绑定的物理对象加入到导出容器内,经由 PhysX 库完成导出相关的复杂工序。好在 PhysX 库的集成非常方便,很多服务器也会采用相同版本的 PhysX 作为物理库来使用,通过 PhysX 库的反序列化接口,即可轻松实现物理资源的互通。
回到正题,当客户端不再使用 PhysX 的情况下,服务器如何获取物理资源成了问题。目前 Unreal 官方没有提供教程来对二者进行转换,同时也没提供集成 Chaos 库的解决方案。因此,才有了本文中提到的工具 ——Chaos 物理导出工具。
本文所讨论的转换版本:Unreal5.2、PhysX 3.4
# PhysX 序列化结构梳理
要实现从 Chaos 到 PhysX 的转换,就得了解 PhysX 序列化的结构。下面会对 PhysX 序列化操作做详细的介绍:
# PhysX 序列化流程
PhysX 基本导出逻辑如下:
PxFoundation* gFoundation = PxCreateFoundation(...); | |
PxPhysics* gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, ...); | |
PxSerializationRegistry* sr = PxSerialization::createSerializationRegistry(*gPhysics); | |
PxCollection oCollection = PxCreateCollection(); | |
// 【step.1】添加各种要导出的对象 | |
oCollection.add(PxBase); | |
// 【step.2】补全依赖 | |
PxSerialization::complete(oCollection, sr); | |
// 【step.3】导出 | |
PxSerialization::serializeCollectionToBinary(..., oCollection, *sr); |
业务层面需要收集应该导出的对象。通过 complete 函数完成对象依赖关系的补全。补全操作交由 PhysX 自动完成,下图是 PhysX 中常见的依赖链路,当导出 Actor 的时候,会连带导出 Shape 和 Material 来确保 Actor 数据的完整性。
Figure 1: Left: Complete Collection, Right: Incomplete Collection
正常情况下,导出的 Collection 必须是 Complete 的,但是也可以显示声明为 InComplete,此时就必须给出一个 ExternalCollection,来保证依赖对象可以在外部查找到,类似于 .so 形式的链接库。当一个对象被多个容器共用时,可以通过该方式减少导出次数。
# 二进制文件结构
Collection 在导出时,会按照固定的结构排列组织:
- Header:头信息,包含 PhysX 版本平台信息等内容,用于反序列化校验。
- nbObjectsInCollection:Collection 内的对象数量。
- manifest nb:实际导出对象的数量。
- manifest data:导出对象的大小(偏移)+ 类型
- actor total offset:对象导出后占用的总空间大小,用于反序列化对象后,通过偏移查询 extra data,对应于 export data size。
- importReferences nb && data:外部引用,表示该 Collection 引用了外部对象(ExternalCollection),记录引用关系,用于恢复对象的时候通过外部引用进行关系补全。
- exportReferences nb && data:内部引用,导出内部对象的 ID 和对应的下标。提高内部引用恢复过程的寻址效率。
- internalPtr nb && data:内部地址引用关系,把对象序列化前的地址数值作为唯一 ID 建立的引用映射。
- internalIdx nb && data:内部编号引用关系,把对象序列化前持有的唯一编号作为 ID 建立的引用映射。
- export data && export extra data:对象的实际数据,包含两个部分。
- export data 表示对象的基本信息,这部分数据大小是固定的。
- extra data 追加信息。包含复杂几何的数据,这部分数据大小通常是可变的。
# Chaos 对象到 PhysX
接下来看看 Chaos 里面物理对象之间的关系图:
通过上述内容,简单整理出两者之间的对应关系:
类型 | PhysX | Chaos |
---|---|---|
Static Actor | NpRigidStatic | FGeometryParticle |
Dynamic Actor | NpRigidDynamic | FPBDRigidParticle |
Shape | NpShape | FPerShapeData |
TriangleMesh | PxTriangleMeshGeometryLL | FTriangleMeshImplicitObject |
HeightField | PxHeightFieldGeometryLL | FHeightField |
ConvexMesh | PxConvexMeshGeometryLL | FConvex |
Box | PxBoxGeometry | TBox |
Capsule | PxCapsuleGeometry | FCapsule |
Sphere | PxSphereGeometry | TSphere |
Plane | PxPlaneGeometry | FImplicitPlane3 |
Material | NpMaterial | FChaosPhysicsMaterial |
由于 Chaos 的几何相对更加丰富,因此可以很好的兼容导出类型。但是两边的物理体系不同,PhysX 是基于牛二定律的计算,而 Chaos 是基于位置动力学的,因此某些对象上的属性映射依旧存在差异,下面会对这些属性逐个讲解:
# Material
属性 | NpMaterial | FChaosPhysicsMaterial | 默认值 |
---|---|---|---|
内存分配类型 | mBaseFlags | PxBaseFlagType::eOWNS_MEMORY | |
对象类型 | mConcreteType | PxConcreteType::eMATERIAL | |
mMaterial 属性 | -------------------------- | -------------------------- | -------------------------- |
动摩擦 | dynamicFriction | FChaosPhysicsMaterial.Friction | |
材质标记 | flags | 无对应(有损) | 0 |
摩擦组合模式 | fricRestCombineMode | FChaosPhysicsMaterial.FrictionCombineMode | |
材质编号 | mMaterialIndex | 动态生成,需要导出逻辑内实现,保证 IDX 唯一且小于 Uint32 即可 | |
指向自身的指针,历史遗留无意义 | mNxMaterial | nullptr | |
填充对齐,无意义 | padding | 0 | |
恢复系数 | restitution | FChaosPhysicsMaterial.Restitution | |
静摩擦 | staticFriction | FChaosPhysicsMaterial.StaticFriction |
- MaterialIndex,这个编号在 Chaos 中是不存在的概念,因此导出的时候需要自己伪造一个,要求是 UInt32 范围且唯一可以标识某个 Material。
- flags 这个标记位 Chaos 中不存在对应,但是 Chaos 提供了其他方案来替代,因此这里是有损转换的。
# Geometry
# HeightField
属性 | PxHeightFieldGeometryLL | FHeightField | 默认值 |
---|---|---|---|
行缩放 | rowScale | FHeightField.GetScale().X | |
高度场数据 | heightField | FHeightField | |
运行时高度场数据 | heightFieldData | 运行中填充,不需要导出 | nullptr |
网格标记 | heightFieldFlags | Chaos 中默认全是双面检测 | PxMeshGeometryFlag::eDOUBLE_SIDED |
高度缩放 | heightScale | FHeightField.GetScale().Z | |
引用材质 | materials | FHeightField.GetMaterials() | |
几何类型 | mType | PxGeometryType::eHEIGHTFIELD | |
纯填充无意义 | paddingFromFlags | ||
列缩放 | columnScale | FHeightField.GetScale().Y |
# ConvexMesh
属性 | PxConvexMeshGeometryLL | FConvex | 默认值 |
---|---|---|---|
凸包数据 | convexMesh | FConvex | |
几何类型 | mType | PxGeometryType::eCONVEXMESH | |
gpu 兼容选项 | gpuCompatible | 运行中填充,不需要导出 | false |
凸包运行时数据 | hullData | 运行中填充,不需要导出 | nullptr |
凸包网格标记 | meshFlags | Chaos 中默认全是紧凑模式 | PxConvexMeshGeometryFlag::eTIGHT_BOUNDS |
纯填充无意义 | paddingFromFlags | ||
几何缩放 | scale.scale | FConvex.GetScale() | |
几何旋转 | scale.rotation | Chaos 中几何不存在单独的旋转 | PxIdentity |
# TriangleMesh
属性 | PxTriangleMeshGeometryLL | FTriangleMeshImplicitObject | 默认值 |
---|---|---|---|
运行时材质编号引用 | materialIndices | 运行中填充,不需要导出 | nullptr |
引用材质 | materials | FTriangleMeshImplicitObject.GetMaterials() | |
三角网格运行时数据 | meshData | 运行中填充,不需要导出 | nullptr |
网格标记 | meshFlags | Chaos 中默认全是双面检测 | PxMeshGeometryFlag::eDOUBLE_SIDED |
几何类型 | mType | PxGeometryType::eTRIANGLEMESH | |
纯填充无意义 | paddingFromFlags | ||
几何缩放 | scale.scale | FTriangleMeshImplicitObject.GetScale() | |
几何旋转 | scale.rotation | Chaos 中几何不存在单独的旋转 | PxIdentity |
三角网格数据 | triangleMesh | FTriangleMeshImplicitObject |
其他简单几何就不做介绍了。值得一提的是,由于 PhysX 对于复杂几何的反序列化也涉及到运行时数据,实时数据中有很大一部分是临时计算并存储在内存的,因此实现上非常麻烦,导出的结构也很复杂,这里简单贴一个 Convex 运行时数据的序列化内容:
这些内容恢复起来非常麻烦,Chaos 中的 ConvexMesh 很难与之对应,下面是 ConvexMesh 存储的数据:
TArray<FPlaneType> Planes; | |
TArray<FVec3Type> Vertices; //copy of the vertices that are just on the convex hull boundary | |
FAABB3Type LocalBoundingBox; | |
FConvexStructureData StructureData; | |
FRealType Volume; | |
FVec3Type CenterOfMass; | |
FVec3 UnitMassInertiaTensor; | |
FRotation3 RotationOfMass; |
因此这部分复杂几何的 ExtraData 后面会通过烘培方案进行恢复,这里暂时不做展开,后面再介绍。
# Shape
属性 | NpShape | FPerShapeData | 默认值 |
---|---|---|---|
基础属性 | -------------------------- | -------------------------- | -------------------------- |
Actor 指针 | mActor | FGeometryParticle 地址 | |
内存分配类型 | mBaseFlags | PxBaseFlagType::eOWNS_MEMORY | |
对象类型 | mConcreteType | PxConcreteType::eSHAPE | |
Shape 被引用数 | mExclusiveAndActorCount | Chaos 系统默认每个 Shape 都是被 Actor 独占的,引用计数为 EXCLUSIVE_MASK | oShape.EXCLUSIVE_MASK |
名称 | mName | 默认不导出,因此这里也不赋值 | nullptr |
用户数据 | userData | nullptr | |
对象类型 | mConcreteType | Chaos 系统默认每个 Shape 都是被 Actor 独占的 | ScbType::Enum::eSHAPE_EXCLUSIVE |
mShape 属性 | -------------------------- | -------------------------- | -------------------------- |
导出 Stream 临时指针 | mStreamPtr | 导出时临时赋值的变量,序列化不处理 | nullptr |
所在场景 | mScene | 反系列化后添加进入场景时赋值,序列化不处理 | nullptr |
mShape.mShape 属性 | -------------------------- | -------------------------- | -------------------------- |
场景查询时的过滤规则 | mQueryFilterData | FPerShapeData.GetQueryData() | |
物理模拟时的过滤规则 | mSimulationFilterData | FPerShapeData.GetSimData() | |
接触距离 | mRestOffset | Chaos 中没有平替,暂时不设置初值,PX_MAX_F32 表示未初始化(有损) | PX_MAX_F32 |
oShape.mShape.mShape.mCore 属性 | -------------------------- | -------------------------- | -------------------------- |
基于 Actor 的坐标位移 | transform.p | FPerShapeData.GetLeafRelativeTransform().GetLocation() | |
基于 Actor 的坐标旋转 | transform.q | FPerShapeData.GetLeafRelativeTransform().GetRotation() | |
接触偏移 | contactOffset | Chaos 只能通过 Material 设置,Shape 没有(有损) | -1.0f |
形状标志 | mShapeFlags | Chaos 没有对应,暂时用默认值(有损) | PxShapeFlag::eSCENE_QUERY_SHAPE |
材质编号 | materialIndex | FPerShapeData.GetLeafGeometry().GetMaterials() | |
是否使用自身内存存储材质编号 | mOwnsMaterialIdxMemory | 超过一个材质就需要 alloc 分配额外内存,否则用自己的内存 | |
几何数据 | geometry | FPerShapeData.GetLeafGeometry() |
- mRestOffset && contactOffset 在 Chaos 中统一采用 Material 的 SkinWidth 表示,但是也不能完美映射,因为 Shape 可能存在多个 Material,因此这里暂时不做转换。
- mShapeFlags 的标记也没有 Chaos 的对应,虽然可以通过 Shape 本身的类型在做细分,但感觉用 PxShapeFlag::eSCENE_QUERY_SHAPE 已经够用了,服务器一般情况下不需要做 Simulate 模拟。Trigger 的话可以视情况加上。
- FPerShapeData.GetLeafGeometry () 内部涉及到缩放和边距,需要进一步解析处理,并转换为 PhysX 的几何数据。
# Static Actor
属性 | NpRigidStatic | FGeometryParticle | 默认值 |
---|---|---|---|
基础属性 | ----------------------------------- | -------------------------------------- | -------------------------------------- |
内存分配类型 | mBaseFlags | PxBaseFlagType::eOWNS_MEMORY | |
对象类型 | mConcreteType | PxConcreteType::eRIGID_STATIC | |
连接器数组 | mConnectorArray | 服务器暂时不需要这类约束关系,因此不导出 | nullptr |
场景内的下标 | mIndex | 反序列化加入场景后重新赋值,导出为默认值 | 0xFFFFFFFF |
名称 | mName | 默认不导出,因此这里也不赋值 | nullptr |
用户数据 | userData | nullptr | |
mRigidStatic 属性 | ----------------------------------- | -------------------------------------- | -------------------------------------- |
所在场景 | mScene | 反系列化后添加进入场景时赋值,序列化不处理 | nullptr |
物理对象类型 | mControlState | ScbType::Enum::eRIGID_STATIC | |
导出 Stream 临时指针 | mStreamPtr | 导出时临时赋值的变量,序列化不处理 | nullptr |
mRigidStatic.mStatic 属性 | ----------------------------------- | -------------------------------------- | -------------------------------------- |
Actor 标志 | mActorFlags | 反序列化会重置,导出无意义 | 0 |
Actor 类型 | mActorType | PxActorType::eRIGID_STATIC | |
聚合 ID | mAggregateIDOwnerClient | 服务器不处理聚合物导出(有损) | 0 |
行为标记 | mClientBehaviorFlags | PhysX3.4 已弃用,导出无意义 | 0 |
优势组 | mDominanceGroup | Chaos 无对应(有损) | 0 |
模拟对象指针 | mSim | 运行时计算,不用导出 | nullptr |
mRigidStatic.mStatic.mCore 属性 | ----------------------------------- | -------------------------------------- | -------------------------------------- |
世界坐标位置 | body2World.p | FGeometryParticle.X() | |
世界坐标旋转 | body2World.q | FGeometryParticle.R() | |
标记 | mFlags | Chaos 和 PhysX 用的不同系统,这块没法转换,但服务器一般也不怎么跑 Simulate,影响不大(有损) | 0 |
Body2Actor 变换矩阵是否为单位矩阵 | mIdtBody2Actor | Static 这个属性没意义,不会用上 | true |
求解器迭代次数 | solverIterationCounts | Chaos 中只有全局次数,这里暂时用 PhysX 的默认次数(有损) | 4 |
mShapeManager 属性 | ----------------------------------- | -------------------------------------- | -------------------------------------- |
修剪结构 | mPruningStructure | 运行时计算修剪结构,不导出 | nullptr |
场景查询数据 | mSceneQueryData | 反序列化会重置,导出无意义 | 0xffffffff |
Shape 数据 | mShapes | FGeometryParticle.ShapesArray() 地址 |
- mAggregateIDOwnerClient:一般是类似布娃娃之类的骨骼内才有,服务器导出的话基本不用。
- mDominanceGroup:PhysX 内的独有概念,Chaos 中没有对应。
- mFlags:PhysX 和 Chaos 用的不同物理体系,这些标记基本上对不上。
- solverIterationCounts:PhysX 可以单独控制 Actor 而 Chaos 只有全局的,这里暂时用了 PhysX 的默认迭代次数。
# Dynamic Actor
属性 | NpRigidDynamic | FPBDRigidParticle | 默认值 |
---|---|---|---|
基础属性 | ----------------------------------- | -------------------------------------- | -------------------------------------- |
内存分配类型 | mBaseFlags | PxBaseFlagType::eOWNS_MEMORY | |
对象类型 | mConcreteType | PxConcreteType::eRIGID_DYNAMIC | |
连接器数组 | mConnectorArray | 服务器暂时不需要这类约束关系,因此不导出 | nullptr |
场景内的下标 | mIndex | 反序列化加入场景后重新赋值,导出为默认值 | 0xFFFFFFFF |
名称 | mName | 默认不导出,因此这里也不赋值 | nullptr |
用户数据 | userData | nullptr | |
mBody 属性 | ----------------------------------- | -------------------------------------- | -------------------------------------- |
运行时标记 | mBodyBufferFlags | 反序列化会重置,导出无意义 | 0 |
运行时角速度 | mBufferedAngVelocity | FPBDRigidParticle.W() | |
运行时世界坐标位置 | mBufferedBody2World.p | FPBDRigidParticle.X() | |
运行时世界坐标旋转 | mBufferedBody2World.q | FPBDRigidParticle.R() | |
运行时睡眠状态 | mBufferedIsSleeping | 反序列化会重置,导出无意义 | 1 |
运行时线性速度 | mBufferedLinVelocity | FPBDRigidParticle.V() | |
运行时唤醒计数 | mBufferedWakeCounter | 运行时数据,导出无意义 | 0.0f |
所在场景 | mScene | 反系列化后添加进入场景时赋值,序列化不处理 | nullptr |
物理对象类型 | mControlState | ScbType::Enum::eBODY | |
导出 Stream 临时指针 | mStreamPtr | 导出时临时赋值的变量,序列化不处理 | nullptr |
mBody.mBodyCore 属性 | ----------------------------------- | -------------------------------------- | -------------------------------------- |
Actor 标志 | mActorFlags | 根据 Chaos 的标记做转换(有损) | PxActorFlag::eVISUALIZATION |
Actor 类型 | mActorType | PxActorType::eRIGID_DYNAMIC | |
聚合 ID | mAggregateIDOwnerClient | 服务器不处理聚合物导出(有损) | 0 |
行为标记 | mClientBehaviorFlags | PhysX3.4 已弃用,导出无意义 | 0 |
优势组 | mDominanceGroup | Chaos 无对应(有损) | 0 |
模拟对象指针 | mSim | 运行时计算,不用导出 | nullptr |
模拟对象数据 | mSimStateData | 运行时计算,不用导出 | nullptr |
oDynamic.mBody.mBodyCore.mCore 属性 | ----------------------------------- | -------------------------------------- | -------------------------------------- |
角阻尼 | angularDamping | UPrimitiveComponent.GetAngularDamping() | |
角速度 | angularVelocity | FPBDRigidParticle.W() | |
body2Actor 位置 | body2Actor.p | Chaos 没有对应(有损) | PxVec3::ZeroVector |
body2Actor 旋转 | body2Actor.q | Chaos 没有对应(有损) | PxIdentity |
世界坐标位置 | body2World.p | FPBDRigidParticle.X() | |
世界坐标旋转 | body2World.q | FPBDRigidParticle.R() | |
CCD 推进系数 | ccdAdvanceCoefficient | Chaos 没有对应(有损) | 0.15f |
接触报告阈值 | contactReportThreshold | Chaos 没有对应(有损) | PX_MAX_F32 |
冻结阈值 | freezeThreshold | Chaos 没有对应(有损) | 0 |
惯性张量的逆 | inverseInertia | FPBDRigidParticle.InvI() | |
质量的逆 | inverseMass | FPBDRigidParticle.InvM() | |
快速移动 | isFastMoving | Chaos 没有对应(有损) | false |
线性阻尼 | linearDamping | UPrimitiveComponent.GetLinearDamping() | |
线性速度 | linearVelocity | FPBDRigidParticle.V() | |
锁定标记 | lockFlags | FConstraintInstance | |
最大角速度平方 | maxAngularVelocitySq | Chaos 没有对应(有损) | 49.0f |
最大接触冲量 | maxContactImpulse | Chaos 没有对应(有损) | PX_MAX_F32 |
最大线速度平方 | maxLinearVelocitySq | Chaos 没有对应(有损) | PX_MAX_F32 |
最大穿透偏置 | maxPenBias | Chaos 没有对应(有损) | -PX_MAX_F32 |
标记 | mFlags | Chaos 和 PhysX 用的不同系统,这块没法转换,但服务器一般也不怎么跑 Simulate,影响不大(有损) | PxRigidBodyFlag::Enum::eKINEMATIC | PxRigidBodyFlag::Enum::eUSE_KINEMATIC_TARGET_FOR_SCENE_QUERIES |
Body2Actor 变换矩阵是否为单位矩阵 | mIdtBody2Actor | Chaos 没有这个字段默认为单位矩阵,(有损) | true |
刚体交互次数 | numBodyInteractions | Chaos 没有对应(有损) | 0 |
计数交互次数 | numCountedInteractions | Chaos 没有对应(有损) | 0 |
睡眠阈值 | sleepThreshold | Chaos 没有对应(有损) | 0.005f |
求解器迭代次数 | solverIterationCounts | Chaos 中只有全局次数,这里暂时用 PhysX 的默认次数(有损) | (1 & 0xff) << 8 | (4 & 0xff) |
求解器唤醒计数器 | solverWakeCounter | Chaos 没有对应(有损) | 0.4f |
唤醒计数器 | wakeCounter | Chaos 没有对应(有损) | 0.4f |
mShapeManager 属性 | ----------------------------------- | -------------------------------------- | -------------------------------------- |
修剪结构 | mPruningStructure | 运行时计算修剪结构,不导出 | nullptr |
场景查询数据 | mSceneQueryData | 反序列化会重置,导出无意义 | 0xffffffff |
Shape 数据 | mShapes | FGeometryParticle.ShapesArray() 地址 |
Dynamic Actor 相对来说,缺失的属性就比较多了,出去 Static Actor 的那些,还有很多模拟相关的属性:
- body2Actor:Chaos 中没有从 body 到 actor 的变换。
- ccdAdvanceCoefficient、contactReportThreshold、freezeThreshold、isFastMoving、maxAngularVelocitySq、maxContactImpulse、maxLinearVelocitySq、maxPenBias、numBodyInteractions、numCountedInteractions、sleepThreshold、solverIterationCounts、solverWakeCounter、wakeCounter:都没对应,全部设置为未初始化或者默认值,这些属性大部分应用在 simulate,目前服务器实际运行 simulate 的对象很少,所以影响不大。
# 特殊规则处理
除去上述的导出对应关系以外,还涉及到很多导出时的特殊规则和转换问题,下面一一列举:
# 几何数据烘培
由于复杂几何的数据存储在 ExportExtraData 中,大小和内容都是不固定的,且要在 Chaos 内提取对应的物理结构和数据十分困难。因为有些数据是运行时的,需要通过特定的计算得到,如果要实现 100% 的兼容,就必须参考 PhysX 的计算逻辑对 Chaos 的数据做类似的计算,你可能会问难道两个引擎运行时数据不一样吗。是的,这块数据真的相差甚远,因此采用了一个折中的方案:导出 Chaos 中几何的基础数据,再通过 PhysX 读取并计算出运行时数据。但会引入其他影响:
- 计算运行时数据放在了反序列化过程中,会有一定的耗时(这部分耗时原本从序列化转移到加载中)。
- 精度丢失问题,临时计算出的运行时数据会有一定程度上的精度缺失,但目前看影响不是很大。
# HeightField
根据行列导出每个坐标的高度和材质。
PxHeightFieldDesc heightFieldDesc; | |
heightFieldDesc.nbRows = iNumRows; | |
heightFieldDesc.nbColumns = iNumCols; | |
heightFieldDesc.format = PxHeightFieldFormat::Enum::eS16_TM; // 假设 Chaos 高度值是 32 位浮点数 | |
heightFieldDesc.samples.data = address; | |
heightFieldDesc.samples.stride = sizeof(PxHeightFieldSample); | |
address += sizeof(PxHeightFieldSample) * iNumRows * iNumCols; |
# ConvexMesh
按照顶点顺序导出所有三角形的顶点坐标即可。
PxConvexMeshDesc convexDesc; | |
convexDesc.points.count = iVerticesNum; | |
convexDesc.points.stride = sizeof(PxVec3); | |
convexDesc.points.data = address; | |
convexDesc.flags = PxConvexFlag::eCOMPUTE_CONVEX; | |
address += sizeof(PxVec3) * iVerticesNum; |
# TriangleMesh
TriangleMesh 根据顶点数量分为两种结构,顶点数量过多的情况下,记录时会用 PxI32 否则用 PxU16,主要数据是顶点数据和顶点顺序,不同于凸包,三角网格的大部分顶点都可以复用,因此需要额外的索引来记录三角形组成。
PxTriangleMeshDesc triangleDesc; | |
triangleDesc.points.count = iVerticesNum; | |
triangleDesc.points.stride = sizeof(PxVec3); | |
triangleDesc.points.data = address; | |
address += sizeof(PxVec3) * iVerticesNum; | |
bool bLarge = *reinterpret_cast<bool*>(address); | |
address += sizeof(bool); | |
if (bLarge) | |
{ | |
const PxU32 iIndexNum = *reinterpret_cast<PxU32*>(address); | |
address += sizeof(PxU32); | |
triangleDesc.triangles.count = iIndexNum; | |
triangleDesc.triangles.stride = sizeof(PxI32) * 3; | |
triangleDesc.triangles.data = address; | |
address += sizeof(PxI32) * iIndexNum * 3; | |
} | |
else | |
{ | |
const PxU32 iIndexNum = *reinterpret_cast<PxU32*>(address); | |
address += sizeof(PxU32); | |
triangleDesc.triangles.count = iIndexNum; | |
triangleDesc.triangles.stride = sizeof(PxU16) * 3; | |
triangleDesc.triangles.data = address; | |
address += sizeof(PxU16) * iIndexNum * 3; | |
triangleDesc.flags |= PxMeshFlag::e16_BIT_INDICES; | |
} |
# HeightField 坐标转换
高度场在 PhysX 和 Chaos 的坐标排列和坐标原点不同,因此需要做额外的转换
- 简单来说就是做一次平移,做一次旋转,再做一次反向,实现上参考了 Unreal 里的代码,会考虑 Scale 为负的情况下如何变换:
- 平移:因为两边的 Shape 局部坐标原点不一致。
- 旋转:因为两边的坐标轴不一样,Unreal 是在 xy-plane 绘制,PhysX 是在 xz-plane 绘制,这里顺带也需要做 Scale 的旋转。
- 反向:因为两边的绘制顺序不一致。
// =========== shape 层面 变换(平移) =========== | |
FTransform LandscapeComponentTransform = pLandspaceCmp->GetComponentToWorld(); | |
FMatrix LandscapeComponentMatrix = LandscapeComponentTransform.ToMatrixWithScale(); | |
FVector Scale = LandscapeComponentMatrix.ExtractScaling(); | |
FTransform LandscapeTM = FTransform::Identity; | |
// 镜像 —— 平移 | |
bool bIsMirrored = LandscapeComponentMatrix.Determinant() < 0.f; | |
if (!bIsMirrored) | |
{ | |
LandscapeTM.SetTranslation(FVector(-pLandspaceCmp->CollisionSizeQuads * pLandspaceCmp->CollisionScale * Scale.X, 0, 0)); | |
} | |
oShape.mShape.mShape.mCore.transform.q.w = LandscapeTM.GetRotation().W; | |
oShape.mShape.mShape.mCore.transform.q.x = LandscapeTM.GetRotation().X; | |
oShape.mShape.mShape.mCore.transform.q.y = LandscapeTM.GetRotation().Y; | |
oShape.mShape.mShape.mCore.transform.q.z = LandscapeTM.GetRotation().Z; | |
oShape.mShape.mShape.mCore.transform.p = LandscapeTM.GetLocation(); | |
//scale 实际上也做了旋转 | |
FVector FinalScale(Scale.X * pLandspaceCmp->CollisionScale, Scale.Y* pLandspaceCmp->CollisionScale, Scale.Z* LANDSCAPE_ZSCALE); | |
oHeightField.rowScale = FinalScale.X; | |
oHeightField.columnScale = FinalScale.Y; | |
oHeightField.heightScale = FinalScale.Z; | |
// =========== actor 层面 变换(旋转) =========== | |
// 坐标轴变换 —— 旋转 | |
FTransform LandscapeComponentTransform = pLandspaceCmp->GetComponentToWorld(); | |
FMatrix LandscapeComponentMatrix = LandscapeComponentTransform.ToMatrixWithScale(); | |
// Reorder the axes | |
FVector TerrainX = LandscapeComponentMatrix.GetScaledAxis(EAxis::X); | |
FVector TerrainY = LandscapeComponentMatrix.GetScaledAxis(EAxis::Y); | |
FVector TerrainZ = LandscapeComponentMatrix.GetScaledAxis(EAxis::Z); | |
LandscapeComponentMatrix.SetAxis(0, TerrainX); | |
LandscapeComponentMatrix.SetAxis(2, TerrainY); | |
LandscapeComponentMatrix.SetAxis(1, TerrainZ); | |
// 计算位移矩阵 | |
FTransform LandscapeComponentTransformEd = FTransform(LandscapeComponentMatrix); | |
oStatic.mRigidStatic.mStatic.mCore.body2World.q.w = LandscapeComponentTransformEd.GetRotation().W; | |
oStatic.mRigidStatic.mStatic.mCore.body2World.q.x = LandscapeComponentTransformEd.GetRotation().X; | |
oStatic.mRigidStatic.mStatic.mCore.body2World.q.y = LandscapeComponentTransformEd.GetRotation().Y; | |
oStatic.mRigidStatic.mStatic.mCore.body2World.q.z = LandscapeComponentTransformEd.GetRotation().Z; | |
oStatic.mRigidStatic.mStatic.mCore.body2World.p = LandscapeComponentTransformEd.GetLocation(); | |
// =========== 绘制 HeightField(反向) =========== | |
struct FHeightFieldAccessor | |
{ | |
FHeightFieldAccessor(const ULandscapeHeightfieldCollisionComponent::FHeightfieldGeometryRef& InGeometryRef) | |
: GeometryRef(InGeometryRef) | |
, NumX(InGeometryRef.Heightfield.IsValid() ? InGeometryRef.Heightfield->GetNumCols() : 0) | |
, NumY(InGeometryRef.Heightfield.IsValid() ? InGeometryRef.Heightfield->GetNumRows() : 0) | |
{ | |
} | |
float GetUnscaledHeight(int32 X, int32 Y) const | |
{ | |
return GeometryRef.Heightfield->GetHeight(X, Y); | |
} | |
uint8 GetMaterialIndex(int32 X, int32 Y) const | |
{ | |
return GeometryRef.Heightfield->GetMaterialIndex(X, Y); | |
} | |
const ULandscapeHeightfieldCollisionComponent::FHeightfieldGeometryRef& GeometryRef; | |
int32 NumY = 0; | |
int32 NumX = 0; | |
}; | |
FHeightFieldAccessor Accessor(*pLandspaceCmp->HeightfieldRef.GetReference()); | |
int RowMax = Accessor.NumX; | |
int ColMax = Accessor.NumY; | |
const bool bIsMirrored = pLandspaceCmp->GetComponentToWorld().GetDeterminant() < 0.f; | |
if (bIsMirrored) | |
{ | |
ColMax = Accessor.NumX; | |
RowMax = Accessor.NumY; | |
} | |
oOutArc.Serialize(&RowMax, sizeof(PxU32)); // x | |
oOutArc.Serialize(&ColMax, sizeof(PxU32)); // z | |
for (int32 x = 0; x < ColMax; ++x) | |
{ | |
for (int32 y = 0; y < ColMax; ++y) | |
{ | |
PxHeightFieldSample oSimple; | |
if (bIsMirrored) | |
{ | |
const float CurrHeight = Accessor.GetUnscaledHeight(x, y); | |
const uint8 MaterialIdx = Accessor.GetMaterialIndex(x, y); | |
oSimple.height = CurrHeight; | |
oSimple.materialIndex0.mData = MaterialIdx; | |
oSimple.materialIndex1.mData = MaterialIdx; | |
} | |
else | |
{ | |
const float CurrHeight = Accessor.GetUnscaledHeight(ColMax - x - 1, y); | |
const uint8 MaterialIdx = Accessor.GetMaterialIndex(ColMax - x - 1, y); | |
oSimple.height = CurrHeight; | |
oSimple.materialIndex0.mData = MaterialIdx; | |
oSimple.materialIndex1.mData = MaterialIdx; | |
} | |
oOutArc.Serialize(&oSimple, sizeof(PxHeightFieldSample)); | |
} | |
} |
# 地址对齐和虚函数表问题
PhysX 的导出比较粗暴,通过对对象整个序列化的方式直接导出。因此需要严格遵守对象声明中的地址排布,不然恢复过程中就很容易出现差错:
// 序列化 | |
s.writeData(&obj, sizeof(T)); | |
// 反序列化 | |
T* obj = new (address) T(); |
因此,如果需要伪造正确的导出结果,类型大小和数据排列就很重要,会影响最终的反序列化:
sizeof(NpShape) = 272 | |
sizeof(NpRigidStatic) = 176 | |
sizeof(NpRigidDynamic) = 384 | |
sizeof(NpMaterial) = 80 |
可以参考 PhysX 简单移植一下各个类的属性定义,函数的话只需要移植一个虚函数保证结构匹配即可。
# 地址对齐
这里的对齐分为两种:
- 第一种是每类数据之间的 16 字节对齐
- 第二种是 header 的起始地址必须是 128 字节对齐。
# 烘培数据导出
由于烘培数据不属于 PhysX 序列化的一部分,但又不希望把反序列化流程复杂化,因此这部分内容暂时插入在序列化文件的头部。由于 geometry 的数据并不依赖任何内容,因此可以单独在业务层解析。