PhysX 3.4高级碰撞检测实战指南:从碰撞过滤到CCD性能优化

发布时间:2026/10/22 4:06:57
PhysX 3.4高级碰撞检测实战指南:从碰撞过滤到CCD性能优化
做物理模拟这几年我最怕听到的不是“引擎崩了”而是“子弹怎么又从墙上穿过去了”和“角色怎么卡进地板里了”。这两个问题十有八九都出在碰撞检测这一层更准确地说是高级碰撞检测没有配好。PhysX 3.4 文档里《Advanced Collision Detection》这一章内容不算多但信息密度极高几乎每个函数、每个参数都对应着真实项目里的一个坑。这篇文章不打算逐行翻译文档而是把我自己从读文档到落地实现、再到调优踩坑的完整过程整理出来。如果你正在做游戏物理、刚体仿真、或者任何用到 PhysX 3.4 的项目看完应该能少走几条弯路。1. 高级碰撞检测是干什么的先理清那两条流水线1.1 碰撞检测不是“检测”这么简单很多同学第一次接触 PhysX 时以为碰撞检测就是“两个物体碰到了一起就回调一下”。真正把项目跑起来之后才发现引擎里其实有两条完全独立的“检测通道”一条是模拟管线里的碰撞检测另一条是场景查询Scene Query。模拟管线里的碰撞检测负责的是刚体之间的真实接触。它又分成两个阶段Broadphase 和 Narrowphase。Broadphase 做的事非常粗糙就是把所有物体的包围盒AABB互相比较找出“有可能碰到的候选对”Narrowphase 才对候选对做精确的形状相交检测生成接触点、法线、穿透深度交给求解器去算。场景查询则是另一码事它是给游戏逻辑用的。你要检测“这条射线打中了谁”、“这个扇形区域里有哪些物体”、“这个胶囊体往前推会不会撞到墙”用的都是场景查询而不是把临时刚体丢进物理世界。《Advanced Collision Detection》这章的精髓就是教你怎么把上面两条流水线都调明白。它的内容可以粗略分成四块碰撞过滤Filtering、连续碰撞检测CCD、场景查询的高级用法、以及特殊几何体比如地形网格的碰撞处理。我按这几个方向逐个说。1.2 为什么默认配置“看起来能用一上量就废”PhysX 3.4 的默认参数在设计上是偏保守的它优先保证“不出错”而不是“高性能”。默认情况下接触偏移contactOffset很小、CCD 是关闭的、场景查询也没有任何自定义过滤所有逻辑都靠引擎内置规则跑。这种配置跑一个小 Demo 完全没问题。但一旦场景里有几十个高速飞行的炮弹、一个需要精确感知周围环境的 AI、或者一块非常大的地形网格默认配置就会暴露出三个问题第一漏检。高速物体在离散时间步里跨过了薄墙物理引擎永远不知道它们曾经“穿过”。第二误报。接触偏移设置不当物体还没真正贴到墙就被接触信息提前催生了一堆错误力表现就是角色“粘墙”或者布娃娃在空气里乱飘。第三性能失控。没有过滤机制时Broadphase 之后产生的候选对可能非常多Narrowphase 再逐个精确计算帧时间一下就顶满了。高级碰撞检测这部分文档的价值就在于它告诉你每个参数背后对应什么成本什么情况下该开什么开关。我分开细聊。2. 碰撞过滤Filtering不只是杀 Pair 那么随便2.1 Filter Shader 和 Filter Callback什么时候该用谁PhysX 3.4 的碰撞过滤体系有两个核心角色Filter Shader 和 Filter Callback。很多教程把它们统称为“过滤函数”其实两者机制完全不同用错了会影响正确的碰撞结果甚至拖垮性能。Filter Shader 是同步执行的它跑在 Narrowphase 做精确相交计算之前。引擎在 Broadphase 阶段发现一对可能碰撞的形状之后会立刻调用 Shader 做一次“预审”。Shader 的返回值决定这对形状是彻底忽略eKILL、直接参与碰撞生成接触点eDEFAULT、还是调用更重的后置回调eCALLBACK。你可以在 Shader 里传入自定义的 FilterData通常是一个包含类型、分组信息的结构体然后在 Shader 函数里读取它进行位运算判断。我的习惯是尽量在 Shader 里完成所有“一刀切”的决策因为它便宜、同步、可预测而且不会产生额外的调度开销。Filter Callback 则不同它是异步回调在接触点已经生成之后才被调用。这个回调里你能拿到的信息更完整可以修改接触点、法线方向甚至在最后一刻把接触流整个掐掉。它适合做“需要精确几何形状参与判断”的过滤比如只有子弹击中敌人头部才造成暴击、只有物体正面碰撞才触发机关。我见过一个项目把所有过滤逻辑都塞进 Filter Callback结果同一帧碰撞回调数量爆炸主线程被拖死。正确做法是能用 Shader 做的粗筛绝不留到 Callback 里做。Shader 负责把不相关的 pair 批量干掉Callback 只处理剩余少量需要精细决策的接触。2.2 Contact Offset 和 Rest Offset两个参数决定手感说到接触生成就绕不开 physx 里两个形影不离的参数contactOffset和restOffset。这两个参数挂在 Shape 上很多人直接忽略但它们直接影响一切碰撞判定。contactOffset是接触生成的“提前量”。当两个形状的距离小于 contactOffset 时引擎就认为它们处于接触状态开始生成接触点。它相当于一个吸力范围太小了物体容易高速穿透太大了物体会在还没贴合时就互相排斥或者抖动。restOffset是休息距离。当两个形状达到 restOffset 这个距离时接触力会趋于零物体自然静止。它的意义在于让两个紧贴的物体之间留出一层极小的缝隙避免接触点反复交替导致“抖动”。我给一套实际项目里常用的初始值contactOffset用 0.02 左右以米为单位如果你的游戏尺度是 1 单位 1 米restOffset用 0.001。注意restOffset一定要小于contactOffset否则物理世界会一直产生推斥力表现为物体之间的“静电反弹”。2.3 过滤逻辑里最容易踩的三个坑第一个坑在 Shader 里做重量级逻辑。Shader 的调用频率极高每个候选对都会触发。你千万别在里面做字符串匹配、访问外部对象池、或者查很重的配置表。Shader 应该是纯函数式逻辑最好只做位运算和整型比较。第二个坑对 Trigger触发器也用 eKILL。触发器靠 eNOTIFY_TOUCH_FOUND 这类事件通知来工作如果你在过滤阶段直接把 pair 杀掉了那么触发器的进入/离开事件永远不会触发。真实表现就是玩家明明走进了机关区域机关却纹丝不动。第三个坑忘了 constantBlock 参数。PhysX 允许你在两个角色间传递一份共享数据块constantBlock很多过滤规则是可以预计算好塞进这个 block 的但项目里经常出现“每次 Shader 都重新算一遍”的浪费写法。把常用规则预计算后塞进去可以白捡不少性能。3. CCD连续碰撞检测别再让子弹穿墙了3.1 先理解为什么物体会穿透物理引擎默认是“离散碰撞检测”。它把时间切成帧每一帧检测一次碰撞。如果一帧内某个物体的位移超过了薄物体的厚度引擎就会漏掉碰撞上一帧子弹还在墙左边下一帧子弹已经跑到墙右边了中间没有任何一帧检测到相交。要解决穿透思路就是“连续碰撞检测”不是等物体到了新位置再去检测而是预测物体从旧位置到新位置的过程中扫过的体积有没有撞到东西。PhysX 3.4 在这方面提供了两类做法一类是“线性扫掠”把形状沿着运动轨迹扫过检测扫掠体与环境相交另一类是“投机性 CCG”提前预测接触点扩大接触生成范围。对应到引擎里就是刚体标志位eENABLE_CCD和eENABLE_SPECULATIVE_CCD。3.2 手动开启 CCD该开谁、怎么调在 PhysX 3.4 里CCD 不是全局开关而是挂在刚体上的一个标志位。你需要对指定的 PxRigidBody 执行body-setRigidBodyFlag(PxRigidBodyFlag::eENABLE_CCD, true);开启了之后SSccd 也不是全部轨道都能扫线性扫掠对快速物体与静态环境之间的碰撞效果很好尤其是子弹、飞镖这种体积小、速度快的物体。但动态体与动态体之间的高速碰撞线性扫掠的支持相对有限这时候就需要投机性方案。我的经验是全场景压低数量只给关键物体开 CCD。一个游戏里可能有几千个刚体全开 CCD 会让性能雪崩。更合理的策略是给玩家角色、重要 NPC、快速子弹单独开 CCD普通小物件任其自生自灭物理表现也不会差。配套还要调一个东西网格的精度。CCD 扫掠是拿形状的“扫掠体”去做碰撞如果你的 Mesh 是那种极度细长的三角形网格扫掠体相交计算的误差会放大表现就是“偶尔还是会穿”。这种情况下用凸包替换三角网格往往立竿见影。3.3 开了 CCD 还是穿从这三个方向查最常见的情况是CCD 开了子弹还是穿墙。我排查这种问题基本固定在三个原因上。第一速度还不够但你要先看子弹每帧位移是不是已经超过了接触阈值。把contactOffset调大一点让接触生成范围覆盖住“上一帧位置到下一帧位置”的轨迹。第二开启了eENABLE_CCD但没注意它和休眠状态的关系。物体进入休眠后不会参与任何碰撞检测如果子弹明明没动却被判定为休眠它自然就不检测了。你需要确认没有把PxActorFlag::eDISABLE_SIMULATION这类标志位错误地挂到要开 CCD 的物体上。第三滤波器把 CCD 接触给杀掉了。有些项目里 Shader 会把子弹与墙壁的 pair “干净利落地 kill”用来避免子弹卡在墙里的表现结果这个操作连 CCD 预测这个 pair 时也一并拦截了。4. 场景查询Scene Query射线、扫描体、重叠检测的精细控制4.1 一次说清 Raycast、Sweep、Overlap场景查询三件套是物理引擎的日常武器。我做一个简单的横向对比表方便你直接对着选型查询类型用途返回核心信息适用场景Raycast射线检测从起点沿方向发射射线命中点、法线、距离、形状对象准星瞄准、视线判定、点击拾取Sweep跳跃扫描把一个形状沿方向移动检测路径上的碰撞最近命中点、距离、法线、形状对象角色移动预判、推力检测、AI寻路避障Overlap重叠检测检查一个形状覆盖范围内有哪些对象重叠的形状列表无距离和法线范围拾取、触发器检测、预警扫描Raycast 适合“一根线”的问题Sweep 适合“一块体积”的问题Overlap 适合“一个区域里有什么”的问题。看起来简单但选错的情况非常多。最常见的是用 Sweep 去做本来该用 Overlap 的区域检测白白付出额外命中的计算成本。反过来用 Raycast 去模拟 Sweep 也常见比如把射线打成扇面发一堆结果角度稀疏处漏检。4.2 命中类型和查询 Buffer别再把所有命中当一回事PhysX 3.4 的查询结果分为 blockHit 和 touchedHits。blockHit 是最先阻挡查询的物体touchedHits 是查询路径上经过的所有物体。你可以在查询标志里指定要收集哪些数据关键标志包括距离、法线、面索引、冲击点等。很多新手第一次用场景查询会把返回的所有命中都当作“撞到了”。这里有个非常隐蔽的坑Raycast 默认会把多个命中全部返回你需要自己排序、自己判断法线朝向尤其是射线从物体内部发射的时候第一个触碰未必是你想要的那个“最近的阻挡”。我在做角色脚底探测时就曾经把一个从内部穿出的 hit 当成地面支撑点导致角色在半空中被“撑住”。实操上我建议返回数据直接用 PxRaycastBuffer、PxSweepBuffer、PxOverlapBuffer 这类专用缓冲区而不是自己开裸数组。一方面代码更清晰另一方面引擎内部可以复用内存避免每帧都产生动态分配。如果你把查询封装在一个高频调用函数里这个差异会非常明显。PxRaycastBuffer hitBuffer; PxQueryFilterData filterData(PxFilterData(...), PxQueryFlag::eDYNAMIC | PxQueryFlag::eSTATIC); bool ok scene-raycast(origin, unitDir, maxDist, hitBuffer, PxHitFlag::eDEFAULT, filterData); if (ok hitBuffer.hasAnyHits()) { const PxRaycastHit hit hitBuffer.getAnyHit(); // 处理命中信息 }4.3 场景查询的性能把每一毫秒都抠出来场景查询的耗时会随着场景复杂度增长。我在项目里做过一轮优化效果最明显的几个点按优先级排第一用好过滤器。场景查询在过滤时PxQueryFilterCallback 的 preFilter 阶段会先跑一遍如果你在 preFilter 里快速 reject 掉 90% 不相关的对象后续的窄相位检测量会骤减。这和碰撞过滤里的 Shader 哲学一模一样粗筛尽量前置。第二用eANY_HIT模式。当你只需要知道“有没有命中”而不关心具体是哪个物体时开启 anyHit 标志可以提前终止查询不需要遍历整个 BVH 的所有可能节点。典型的用途是角色能不能站到某个区域、AI 是否被障碍物挡住视线。第三尽量不要每帧在多个线程里乱发场景查询。PhysX 的场景查询线程安全性和版本有关3.4 下最好把查询集中在固定的阶段执行让引擎底层加速结构处于已知状态。跨线程高频查询带来的锁竞争往往比查询本身还贵。5. 高级形状的碰撞细节地形、凸包、三角网格5.1 Heightfield 地形建的方便用的讲究地形碰撞在 PhysX 3.4 里最推荐的方案是 Heightfield高度场。它比三角网格地形快得多内存占用也小。构建 Heightfield 的核心是把地形的高程数据烹饪成一个 PxHeightField 资源再把资源挂到静态刚体上。材料准备上你需要按行存储的PxHeightFieldSample数组里面填好每个点的高度值short 类型和材质索引。烹饪时要注意一个反直觉的细节高度场默认是单面的从“下方”看过去是透明的碰撞可能不会发生。PhysX 提供了 thickness 参数给地形加一层“厚度”之后从地下往上撞也能触发碰撞。我的经验是 thickness 不要给 0给一个略大于角色默认步进的值能避免很多角色掉出地形的怪毛病。另外还有 Tesselation 等级。高度场烹饪时会自动生成三角形网格tessellation 级别控制网格的细分程度。这个值不是越高越好因为物理网格越精细Narrowphase 计算越贵。对于地形这类大体量静态物我建议在精度满足要求的前提下尽量用低细分。5.2 凸包与三角网格物理引擎为什么偏心凸包PhysX 的稳定碰撞实现里凸包Convex Mesh有专门的快速算法而三角网格Triangle Mesh只能做静态/运动学物体的碰撞动态刚体无法直接使用三角网格作为碰撞体。这是很多人前期规划时忽略的硬约束。如果你需要让一块复杂的动态物体既有好看的外观又有物理碰撞正确的做法是给动态体配一个近似的凸包碰撞体而不是把高模丢给它。凸包的优势在于碰撞检测稳定、接触点数量可控、作为 CCD 扫掠体也快。在烹饪三角网格时PhysX 提供了网格焊接、清理退化三角形等一系列选项。我建议不要关掉清理选项尤其不能忽略eWIND_COUNTERCLOCKWISE这类绕序设置。网格绕序反了会导致碰撞法线方向错误表现就是物体“从上面掉不进地面反而会被地面向外推”。5.3 复合形状动态体的组合碰撞体一个动态刚体上可以挂多个 Shape它们组合成一个复合碰撞体。这个机制非常实用比如一个角色模型可以用胶囊体做躯干再加几个球体做四肢的碰撞判定。但复合形状也有一个常见的性能坑形状之间如果互相重叠Narrowphase 会产生大量重复接触对。PhysX 提供了PxShapeFlag::eSIMULATION_SHAPE这个标志位可以精确控制哪个 Shape 参与模拟碰撞、哪个 Shape 只参与场景查询。合理组合这些标志让复合体中的每个 Shape 各司其职比单纯堆数量要高效得多。6. 性能优化与排查工具从“能跑”到“跑得稳”6.1 休眠与接触管理是隐藏的优化开关物理引擎的性能瓶颈往往不在 Narrowphase 本身而在“管理了多少对接触”。场景里如果长期存在大量静止或低速物体每一帧还在重复生成相似接触点这非常浪费。PhysX 的刚体休眠机制能自动让静止物体退出模拟但前提是你没有不小心在每一帧给它们施加哪怕一点微小的速度扰动。我的排查习惯是如果发现场景中大量物体不睡先去看看是不是接触参数导致它们一直处于“微小抖动”状态。调高 restOffset、并把材质摩擦系数设成合理值通常能让物体们安静下来。另外还有一个容易被忽略的点接触点报告的压力阈值。PhysX 支持设置接触力阈值低于阈值的接触不会产生回调通知。如果你每一帧都收到一堆“接触点”但根本不用它们记得用PxPairFlag::eNOTIFY_THRESHOLD_FORCE_FORCE这套机制做过滤这样可以大幅减少回调处理开销。6.2 用调试工具定位碰撞问题PhysX 自带的 PVDPhysX Visual Debugger是我调试高级碰撞问题时的首要工具。它能实时显示场景里每个碰撞体的形状、过滤后的 Pair、以及场景查询的结果。当你觉得“碰撞结果不对”的时候先开 PVD 看几何体是不是你想象中的样子很多时候结论是网格绕序反了、凸包比模型大了一圈、或者 contactOffset 画出来的“隐形范围”吞掉了旁边的物体。PVD 里的可视化还能帮你判断 CCD 扫掠体的形状和扫掠长度排查高速穿透问题的效率会高很多。如果条件允许把目标物体的形状画出来对比实际模型最容易发现“碰撞体和视觉不一致”的隐性 bug。6.3 一份可复用的问题排查速查表现象优先怀疑处理办法高速物体穿过薄墙CCD 没开或开了仍扫掠失败开启 eENABLE_CCD检查 contactOffset给物体换凸包碰撞体物体轻微抖动无法静止restOffset 设置过大或接触力反复下调 restOffset检查是否每帧施加微扰动触发器没有触发Filter 阶段 kill 掉了 pair检查 Shader 返回值确认 eNOTIFY_TOUCH_FOUND 保留地形从下方穿透Heightfield 厚度为 0给厚度设置合理正值射线命中位置不准把 touchedHits 当 blockHit 用确认查询模式只拿最近阻挡命中动态物体碰撞不稳定使用了三角网格做动态体换成凸包或复合形状7. 结尾一些真实的项目体会把这套高级碰撞检测配置下来最深的感受是PhysX 3.4 的文档本身并不难懂难的是把它放到真实项目里做选择。每个开关、每个偏移量都不是独立生效的它们互相影响。频率最高的几个配置链路——过滤 Shader、CCD 标志位、场景查询缓存、接触偏移——我建议在做项目初期就定好默认策略而不是等项目出了问题再逐个调。我个人在实际操作中还有一个习惯每次修改碰撞相关参数都把修改记录和现象变化对应起来。穿模问题很多时候是“改了 A 修好了换了 B 场景又出来了”没有记录很难回头复盘。如果你也想找一套稳妥的上手路径可以先严格按默认参数跑通一个新场景然后只改一个参数、观察一次表现这样一步步建立自己项目的“碰撞参数指纹”。希望这篇基于 PhysX 3.4《Advanced Collision Detection》的实践梳理能帮你把那些藏在文档角落里的关键设置用起来让子弹稳稳地打在墙上角色稳稳地站在地上。