038 客户端 物理 游戏中的物理引擎,主要是用于模拟自然中的物理现象。比如以一定的初速度抛出小球后,小球 运动轨迹的模拟;小球碰撞到物体后,反弹轨迹的模拟;再比如一块布料的模拟。 物理引擎的基本原理是计算物体受到的外力,计算加速度,然后计算速度。根据速度,计算物体 下一帧的位置。 物理引擎和渲染同步,只用将模拟后的位置同步给渲染。物理引擎每帧tick 的时间,不需要和渲 染引擎一样。渲染引擎获取物体位置时,物理引擎只要能根据时间对物体的位置进行插值,交给 渲染引擎就可以了。 因为物理引擎的需求比较固定,自定义需求少,开发工程量大,一般选用第三方的插件。目前, 比较流行的3D 物理引擎有Havok、PhysX、Bullet 等。 Havok 引擎是比较成熟的物理引擎。它的功能齐全,物理模拟的效率高,并且模拟效果好,很少 出现不自然的模拟结果。但是Havok 引擎是收费,并且不开源。该引擎是一个跨平台的物理引擎, 但是被微软收购后,不再支持其他平台。 PhysX 是Nvidia 公司的一款开源、免费的物理引擎,它的功能较为完善,效率也不错,模拟效 果也还行,支持多个平台,是目前比较好的选择。 Bullet 是一款开源、免费、跨平台的物理引擎;但是它的效率比前两个引擎较差。 在物理引擎的一帧之内,一般会经过如图3.1 的模拟过程。 物理 Physics 03 3.1 游戏中物理引擎简介 3.2 物理引擎的碰撞检测系统 039 Physics 01/03 图3.1 物理引擎一帧的模拟过程 在这个架构中,碰撞检测系统是物理引擎最重要的系统,它是整个物理引擎的基石。物理引擎的 用户,依赖碰撞检测来查询场景中的信息。物理引擎的运动模拟部分,依赖碰撞检测系统提供的 信息,让刚体在碰到东西之后,能发生反弹。碰撞检测部分是物理引擎性能消耗的主要部分,它 的性能直接决定了整个物理引擎的性能。 碰撞检测部分,主要提供三个功能:碰撞过滤系统,碰撞回调机制,碰撞查询接口,下面分别 介绍。 3 .2.1 碰撞过滤系统 物理引擎的碰撞过滤系统,提供了一种机制,让用户可以自定义刚体在运动时,是否发生碰撞。 这套过滤机制,同样也适用于碰撞查询接口。物理引擎一般通过一个过滤函数给用户自定义,这 个函数的接口声明如下: FilterFlags FilterShaderFunction(FilterData filterData0, FilterData filterData1); 其中FilterData 是过滤标签,每个刚体会有一个这个类型的属性,每次查询的输入也会包含这个 类型的变量。物理引擎在判定是否发生碰撞前,会调用FilterShaderFunction 函数,并把有可 能发生碰撞的两个刚体的FilterData 传入该函数,由该函数决定是否会发生碰撞。由于这个函数 在物理引擎模拟的时候会被频繁调用,所以用户提供的这个函数,效率必须非常高。 FilterShaderFunction 函数需要考虑灵活性和效率,一般有两种常用的方法。一种是PhysX 提供的默认方式,这种方式把FilterData 解释成一个编号,然后内部有个表,记录每个编号之 间是否发生碰撞,另外一种方式,是Havok 提供的Layer-System-SubSystem 系统。这 个系统中,把FilterData 分为几个部分,分别记录Layer,System 和SubSystem 的编号, 以及不发生碰撞的SubSystem 的编号,对于不同System 的物体,通过layer 来设置碰撞 规则,其规则和PhysX 的方式类似。对于同System 的物体,通过SubSystem 来设置碰撞 规则。 040 客户端 物理 3 .2.2 碰撞回调机制 用户往往希望在两个刚体发生碰撞时,获得物理引擎的通知,以便做出相应的逻辑处理。物理引 擎的碰撞回调机制,就是为用户提供这个功能的。在这个机制中,用户向物理引擎提供一个处理 碰撞回调的对象,当发生碰撞时,物理引擎就会调用这个对象的接口,来通知用户。处理碰撞回 调的类,其声明形式一般如下所示: class PxSimulationEventCallback { public: virtual void onContact(const PxContactPairHeader& pairHeader, const PxC ontactPair* pairs, PxU32 nbPairs) = 0; } 其中PxContactPairHeader 包含了发生碰撞的两个刚体的信息,而PxContactPair 包含了碰 撞发生的位置和法向量等信息。 3 .2.3 碰撞查询接口 碰撞查询是物理引擎提供给用户的一个功能,用户可以通过这个系统,查询场景里的刚体。碰撞 查询一般有三种类型:射线查询,形状求交查询和扫描查询。 射线查询是由用户给定一条线段的起点和终点,物理引擎会找到和这条线段相交的所有刚体以及 所有交点和交点处的法向量,如图3.2 所示。 图3.2 射线查询示意图(图片引自参考文献[4]) 射线查询的接口声明一般如下所示: bool raycast( const PxVec3& origin, const PxVec3& unitDir, const PxReal distance, PxRaycastCallback& hitCall, const PxQueryFilterData& filterData ); 041 Physics 01/03 其中filterData 用于碰撞过滤系统,hitCall 用于搜集物理引擎返回的碰撞信息。 求交查询是由用户提供一个形状,并提供形状的位置和朝向,物理引擎会找到和这个形状相交的 所有刚体,如图3.3 所示。 图3.3 求交查询示意图(图片引自参考文献[4]) 求交查询的接口声明一般如下所示: bool overlap( const PxGeometry& geometry, const PxTransform& pose, PxOverlapCallback& hitCall, const PxQueryFilterData& filterData ); 其中geometry 为形状,pose 为形状所在的位置和朝向,filterData 用于碰撞过滤系统,hitCall 搜集查询结果。 扫描查询是由用户提供一个形状,和它的初始位置及朝向,然后提供这个形状平移的方向和距离, 物理引擎会找到这个形状在移动过程中碰到的所有刚体,如图3.4 所示。 图3.4 扫描查询示意图(图片引自参考文献[4]) 扫描查询的接口声明一般如下所示: bool sweep( const PxGeometry& geometry, const PxTransform& pose, const PxVec3& unitDir, const PxReal distance, PxSweepCallback& hitCall, const PxQueryFilterData& filterData ); 042 客户端 物理 其中filterData 用于碰撞过滤系统,hitCall 搜集查询结果。 在所有的查询接口中,都会提供一个Callback 类,物理引擎通过这个类来向用户提供查询结果。 这个类的声明,一般如下所示: template struct PxHitCallback { virtual PxAgain processTouches(const HitType* buffer, PxU32 nbHits) = 0; }; 在碰撞查询的过程中,物理引擎每收集到一个碰撞,就会调用PxHitCallback 的process Touches,让用户进行处理。用户一般可以根据需要,保存或丢弃这个信息。另外,用户可以通 过这个函数的返回值,告诉物理引擎是继续查询还是立刻返回。比如用户只是需要找到一个刚体 就行了,那么在物理引擎第一次调用这个函数时,用户就可以终止查询过程,节省时间。 3 .2.4 碰撞系统的架构 物理引擎的碰撞检测系统,一般采用分层的架构,这样可以优化效率。碰撞系统的分层架构,一 般包括粗检测阶段和细检测阶段。有些物理引擎,如Havok,会在粗细阶段之间,加入一个中间 阶段。在碰撞系统的分层架构中,粗阶段用运算量非常小的算法,判断两个物体是否发生碰撞。 把不可能发生碰撞的物体对剔除掉,把有可能发生碰撞的物体对交给细检测阶段。细检测阶段会 用精确的算法,判断两个物体是否发生碰撞。 粗检测阶段的算法,是碰撞检测系统最重要的算法,它直接决定了碰撞检测系统的效率。粗检 测阶段,使用AABB 来描述物体,通过AABB 是否相交,来进行剔除。为了加速物体之间的 碰撞检测,粗检测阶段一般会用到几种加速算法。下面介绍其中的两种,Bounding Volume Hierarchies 和Sweep-and-prune。 Bounding Volume Hierarchies 是通过对AABB 做一个多层结构来加速,如图3.5 所示。 图3.5 Bounding Volume Hierarchies 示意图(图片引自参考文献[3]) 图3.5 中,树的每个节点都是一个AABB,父节点的AABB 完全包含所有子节点的AABB。 场景中的每个物体的AABB 作为树的叶子节点,当进行查询的时候,如果父节点的AABB 和查 043 Physics 01/03 询相交,那么检测其子节点;否则,就表示这 个节点的所有子节点都不会和这个查询相交。 Bounding Volume Hierarchies 一般用于静 态的物体。 Sweep-and-prune 算法的思路如图3.6 所示。 图3.6 Sweep-and-prune 算法示意图(图片引自参考文献[5]) 把场景中所有物体的AABB 按照某个坐标轴进 行投影,记录每个AABB 在轴上的最大最小 值。设两个物体A 和B 的AABB,其最大和 最小值分别为MinA、MaxA、MinB、MaxB, 如果MinA>MaxB, 或者MaxA