求解器主流程那篇讲过一帧的管线,其中 BroadPhase 和 NarrowPhase 两步是碰撞检测的地界。这篇往下钻一层:碰撞几何用什么数据结构表达、粗筛和精判各自怎么做、为什么 Chaos 的窄相位没有直接用教科书 GJK。代码在 Engine/Source/Runtime/Experimental/Chaos/Private/Chaos/(UE5.3),几何类型在 Chaos/Framework/ 和 Collision/ 目录。
FImplicitObject:一切皆隐式面
Chaos 的碰撞几何不是三角网格,而是隐式面(implicit surface)——用函数 $f(x) \le 0$ 定义"点在不在体内"。所有碰撞几何继承自 FImplicitObject(Chaos/ImplicitObject.h),核心接口只有两个:
PhiWithNormal(x):返回点 $x$ 的有符号距离 $\phi(x)$(到最近表面的距离,内部为负)和最近点法线;BoundingBox():包围盒,给 broad phase 用。
为什么要隐式面而不是三角形?因为 PBD 求解器的约束投影需要"把穿透点沿什么方向推出去、推多远"这两个量——正好是 signed distance 和它的梯度(法线)。三角网格给的是"这个点在哪条边上",转成距离还要额外计算;隐式面天然就是距离场。一个 box 的 $\phi$ 是几行解析公式,换成三角网格就是几百个三角形逐个求最近点。
FImplicitObject 的主要子类对应物理材质的直觉:box、sphere、capsule、convex(凸包,FConvex)、平面、以及组合用的 FImplicitObjectUnion(多个子几何的并集)和 TImplicitObjectScaled(对任意几何做非均匀缩放包装)。复合结构靠这两个组合子搭:一个骨骼网格的碰撞就是 Union(Scaled(Box), Scaled(Convex), ...) 的树。
BroadPhase:AABB 粗筛
BroadPhase 的任务是从 $O(n^2)$ 对物体里筛出 AABB 重叠的候选对。Chaos 的实现是空间哈希网格(FBroadPhase::Update / SpatialHashBroadPhase,Collision/SpatialHashBroadPhase.cpp):把世界按固定 cell 大小切成三维网格,每个物体的 AABB 覆盖到的 cell 记下它的 id;查询时看新物体覆盖的 cell 里已有哪些 id,交集就是候选。
网格的两个旋钮:cell 太大会退化成"全挤在一个格子里的 $O(n^2)$",太小则一个物体的 cell 列表过长、哈希开销盖过收益。BroadphaseSettings 里可以配,默认按世界尺度自动取。
筛出的候选对交给窄相位前还有一道基于 island 的过滤——不同 island 里的物体对在本帧不可能发生新接触(它们的相对状态没变),直接跳过。island 机制下一篇展开,这里记住 broad phase 的实际输出是"本帧可能产生新接触的 pair 列表",不是全部重叠对。
NarrowPhase:GJK 的正确打开方式
教科书答案:两个凸体的穿透深度用 GJK 算最近点对(距离 $d$),分离时用 EPA 从单纯形展开成多面体找穿透向量。Chaos 的窄相位(Collision/NarrowPhase.cpp 的 FNarrowPhase)主干确实是 GJK/EPA,但工程上有三个和教科书不同的形态:
一,GJK 的输入不是原始几何,而是支撑函数(support function)。 GJK 只需要知道"沿方向 $\hat d$ 哪个点最突出":$s_A(\hat d) = \arg\max_{x \in A} x \cdot \hat d$。隐式几何族实现 support function 都是 $O(1)$ 解析式(box 就是顶点枚举 8 个取最大),所以 GJK 迭代的开销和几何复杂度解耦——一个 50 顶点的凸包和一个 box 的 GJK 成本几乎一样。这就是碰撞几何不用三角网格的第二个理由(第一个是 phi/法线直接服务 PBD)。
二,EPA 只在穿透时用,且深度受 margin 截断。 Chaos 给碰撞几何统一加 CollisionMargin(碰撞余量):GJK 算的是"膨胀 margin 之后"的距离,$|\phi| < \text{margin}$ 就认为接触、不再跑 EPA。margin 的作用是走捷径——浅穿透场景(持续接触的堆叠箱子)用 margin 直接生成 contact,跳过 EPA 的单纯形扩张;只有真正深穿透才走到 EPA。r.Chaos.Collision.Margin 相关设置影响这里。代价是 margin 内的接触精度有损失,布料/小物体堆叠时要注意默认值是否过粗。
三,contact manifold 的持久化。 窄相位输出的不是单点接触,而是流形(manifold):一组带法线和臂(从质心到接触点的向量 $r$)的接触点,喂给求解器的约束构造。Chaos 维护 FCollisionConstraintCandidate / persistent manifold——本帧算出的新流形和上一帧的做匹配(按特征 id 配对),保留旧流形的点做 warm start(求解器初值用上帧的拉格朗日乘子),稳定性大幅提升。没有 warm start 的 PBD 摩擦在静止堆叠时会抖动,这是它不传初值的典型症状。
GJK 的收敛判断也值得注意:Chaos 的迭代终止条件是单纯形的最近点对距离变化小于 epsilon,带最大迭代数兜底。r.Chaos.GJK 相关 debug cvar 可以 dump 迭代过程——卡帧排查时如果窄相位耗时不正常,先怀疑凸包顶点数(美术给的凸包精度太高)或 margin 配错导致 EPA 高频触发。
三角网格怎么办:PTriangleMeshImplicitObject
静态关卡碰撞很多是三角网格(FCustomTriangleMeshImplicitObject / FTriangleMeshImplicitObject)。三角网格不是凸体,GJK 不适用,Chaos 的处理是逐三角形做 GJK——把每个三角形当薄板凸体(三角形+厚度),用 AABB 树(FAABBTree)索引,broad phase 查出候选三角形集合,窄相位对每个候选三角形跑 shape-vs-triangle GJK。
这个路径的性能特征和凸体碰撞完全不同:凸体对的 GJK 是常数开销,网格碰撞的成本随候选三角形数量线性增长——一个大平面三角形多的网格,哪怕物体只在角落里活动,broad phase 划出的候选集也可能很大。优化手段:r.Chaos.Collision.MeshCollisionsCache 类设置、把关卡碰撞拆成更碎的 mesh component(缩小单个 AABB 树范围)、以及对高频交互区域换用凸包近似。
排查清单
- 物理帧时间高、narrow phase 占比大:
Stat Chaos看各部分耗时,凸包顶点数(p.Chaos.Print相关 debug)先查; - 堆叠抖动:persistent manifold 的 warm start 是否生效,margin 是否过大导致接触点稀疏;
- 高速物体穿墙:连续碰撞检测(CCD)走单独路径(swept GJK),
r.Chaos.Collision.CCD.Enable确认开启,且 fast-moving 物体的 CCD 阈值(p.Chaos.SweepThreshold)匹配其实际速度; - 自定义几何:
FImplicitObject子类只需实现 PhiWithNormal + BoundingBox + support 系列,就能接入整条 broad/narrow phase 管线——扩展物理材质的特殊碰撞形状是官方支持路径。
源码入口
- 几何基类:
Chaos/ImplicitObject.h,看PhiWithNormal的签名约定; - 窄相位主循环:
Collision/NarrowPhase.cpp的FNarrowPhase::GenerateConstraints; - GJK/EPA 实现:
Collision/GJK.cpp/Collision/EPA.cpp,注意 margin 在 GJK 距离判定里的注入点; - broad phase:
Collision/SpatialHashBroadPhase.cpp。
Chaos 碰撞的几何设计可以归纳为一句话:把碰撞体表示成"距离查询接口"而不是"形状描述"。接口只需要 phi 和支撑点,就能同时服务 broad phase(包围盒)、窄相位(GJK/EPA)和求解器(约束投影)三个消费者——这是它比"三角网格碰撞"快一个数量级的结构性原因。