物理世界、碰撞检测、约束求解与并行执行
范围:phcore/phbound/physics/phsolver/pharticulated/phbullet,Framework phframework,并追踪 GTA CPhysics 的时间切片。静态分析,未编译或运行。入口:Summary。
1. 物理系统的分层与对象模型#
RAGE 物理是可流式装入的几何、世界中的实例、活动模拟状态和求解工作集的组合。把这些层分开,才能让大世界里大量静态碰撞存在,而不为每个物件持续支付完整动态刚体成本。
| 层 | 核心对象/目录 | 主要责任 |
|---|---|---|
| 几何资源 | phBound、primitive、geometry、BVH、composite | 几何形状、局部边界、材质与内部加速结构 |
| 共享物理描述 | phArchetype | 质量、惯量及物理资源相关共享描述 |
| 世界实例 | phInst、phLevelNew | 变换、level index、分类、空间索引和活动集合 |
| 模拟状态 | phCollider、phArticulatedCollider | 速度、角速度、力、积分与关节状态 |
| 接触工作集 | overlap pairs、phManifold、phContact | 候选对、持续接触点、累计冲量和约束 |
| 求解 | phContactMgr、phForceSolver | 分组、预处理、迭代冲量、位置修正 |
| 游戏适配 | fwInst、fragment instance、CPhysics | 实体、角色、载具、破碎和回调的时序 |
PHSIM/PHLEVEL 风格接口依赖当前活动实例;simulator.h 明确注明同一时刻只能运行一个 active simulator。这是当前接口的上下文限制,并不表示无法创建多个对象。新项目若需要服务器多房间并行物理或编辑器多世界,需要先消除或隔离这类全局活动上下文。
2. 大世界空间索引与碰撞管线不是同一棵树#
Framework 场景图负责视觉和场景搜索;物理 level 另有对象空间组织、broadphase 及几何内部 BVH。三者工作粒度不同:世界级查询定位实例,broadphase 产生潜在碰撞对,bound BVH 将实例内部大量三角形/子形状缩减成少量候选。
phLevelNodeTree::InitBroadphase 在 ENABLE_UNUSED_PHYSICS_CODE 下保留 AxisSweep3、AxisSweep1、Level、SpatialHash、NxN 等选择;非该分支直接构建 btAxisSweep3。因此目录里有 spatial hash 实现,不等于 GTA 主路径使用 hash broadphase。还存在 secondary broadphase 的条件路径和 level-based 批处理辅助。
Sweep-and-prune 利用 AABB 轴区间的有序性和运动相干性;低速连续运动较友好,大批瞬移、密集互相重叠或大量创建/删除会改变更新成本与候选对数量。这是算法层推断,需要用本项目数据量测,不能直接推成已发生的性能缺陷。
3. 几何 BVH:压缩布局与保守查询#
phBoundBVH 派生于 geometry,持有 phOptimizedBvh。优化节点使用 s16 AABB,含量化/反量化比例;非叶节点存 escape index,叶节点共享字段保存 polygon 起点,叶节点的 escape 隐含为 1。
这种布局的意义是让树可以线性存储、快速跳过整个不相交子树,并减少节点带宽。bound 的构建接口还支持按 BVH 结果重排 primitives,提升叶节点访问局部性。几何在离线阶段为运行时布局服务,而不是只导出一份原始三角形列表。
限制也来自同一设计:量化范围、节点索引宽度和巨大 mesh 的空间跨度影响精度和容量;碰撞边界必须保守,不能因量化把真实接触剔除。把整片世界合成一个巨大 collision bound,可能在精度、流式粒度和更新成本上都不合适。
composite 支持多子形状,并存在延迟 composite BVH 更新。CoreUpdate 注释解释了为什么把重建尽可能推迟:需要写锁,要让已有读者先完成,且已禁止新的读者进入。由此可见“可破坏几何更新”本身就是跨线程事务。
4. 窄相位与连续检测#
collision.cpp 使用形状组合、复合体筛选、BVH、Minkowski/GJK 相关路径;连续检测调用 ContinuousConvexCollision::ComputeTimeOfImpact 或其 GJK 变体。运动扩展的 AABB 负责避免在 broadphase 过早漏掉潜在高速碰撞,TOI 路径再处理细节。
这证明引擎存在 CCD 能力,但不足以证明每个对象、每种形状、每一帧都用完整连续检测。判断一个高速物体是否可靠,必须同时看 CCD 选择条件、几何厚度、时间步、旋转、最大迭代和碰撞过滤。
接触由 manifold 持续维护,并有默认最短点存活次数等策略。保留接触点与累计冲量有利于稳定堆叠和 warm start,减少从零求解;但旧接触需要依据相对运动正确刷新和剔除,不能无限保留。
5. 实际更新顺序:先用已有接触解速度,再建立新接触并推离#
不能仅按 PreCollide/PostCollide 名称猜测主路径。当前 Update 调用 CoreUpdate;其核心顺序如下:
g_MaxPushCollisionIterations 在此文件初值为 2;循环还受 s_NeedsCollision、sm_EnablePushes、finalUpdate 等控制。这是穿透修正相关循环上限,不是所有约束求解仅迭代两次。solver 自己还有 m_NumIterations 和 ContactMgr 的迭代参数。
延迟 SAP 新对象在速度求解之后、位置积分之前提交,注释给出了两个具体原因:破碎对象不会马上被错误裁掉,而且第一帧运动扩展不会丢失。这说明物理阶段顺序包含大量“瞬间创建对象”的正确性条件,随意重排会改变行为。
6. 冲量求解:累积约束与摩擦限幅#
ApplyImpulseMovAndMov 的 ACCUMULATION_SOLVER_MATH==0 分支直接展示了累积冲量路径:
- 从线速度与角速度计算接触点相对速度。
- 结合目标相对速度、分离项和有效质量倒数求冲量增量。
- 将法线累计冲量投影到非负范围,避免接触力把两个物体拉到一起。
- 将增量作用到双方线/角速度。
- 以摩擦系数乘法线累计冲量作为切向冲量上限,并对累计切向冲量限幅。
用抽象形式表达是:λnew = max(λold + Δλ, 0),实际施加 λnew-λold;摩擦限制为 |λt| ≤ μλn。这是对该分支代码的概括,未涵盖所有宏分支、关节和 articulated 专用算子。
逐约束立即更新速度、重复多轮的组织方式可理解为顺序冲量/投影迭代类方法。不要把它描述成精确一次解完整刚体系统的直接矩阵求解器。有限迭代对质量比、深堆叠、长关节链和刚性闭环尤其敏感。
位置 push 与速度冲量分离,意图是在有限成本下纠正穿透,同时减少通过速度偏置引入的非物理能量;具体稳定性仍依赖步长、容差、接触质量和模型调参。
7. 并行求解最难的是共享刚体冲突#
若约束 A-B 和 B-C 同时修改 B 的速度,简单并行 for 会产生数据竞争。RAGE 的 SplitPairs 为 manifold 建立 ParallelGroup,用活动对象 bitset、phase 内对象表和依赖 phase 判断哪些工作能重叠。
注意:一个 phase 内可以重复出现同一对象,因为 phase 内是有序处理;相互并发的 phase 才必须满足共享写状态的约束。这和“每个 phase 内所有碰撞对都完全独立”不是一回事。
主要实现细节:
- phase window 固定为 32,记录窗口内占用对象;窗口推进时释放旧 phase 的对象。
- 依据
sm_MaxPairsPerPhase控制组大小,并对 articulated 数量/组合单独设限。 ManifoldFinder维护按 A/B level index 排序的查找结构,避免每次全量搜索可用约束。- 通过
phasesToRelease、原子 phase index、semaphore 控制安全窗口。 - 完成 phase 可以乱序,但 release atom 的高位记录有限窗口内的完成情况;只有满足依赖才开放后续工作。
- 初始 PreResponse 与迭代阶段共享 phase 组织,跨迭代 wrap 也检查对象冲突。
并行上限由接触图拓扑、组粒度和成本不均衡共同决定。一堆互不相连的物体比同数量紧密相连的约束更容易并行。关节角色和复杂 composite 每对成本又远高于简单盒子;只看“碰撞对数量/线程数”会高估加速。
8. 容量会影响正确性,而不只是速度#
InitParams 将 collider、manifold、contact、constraint、sleep island 和 scratch 作为显式预算。默认值只是引擎构造默认,游戏初始化可以覆盖,不应拿默认 512 collider/4096 contact 断言 GTA 实际全局上限。
SplitPairs 会根据 scratch 容量降低 max phases;末尾还有明确断言:如果 phase 耗尽仍有未分配 manifold,“some objects will not obey constraints”。这比普通性能退化更重要:极端内容可能触发模拟质量或正确性边界。
Update 结束也检查 manifold/contact/composite-pointer pools 是否用尽。高优先级 manifold 预留说明系统承认资源耗尽时需要区别重要对象。新项目应把溢出行为产品化:明确拒绝新刚体、简化次要碎片、合并效果或回退碰撞形状,避免只依赖发行版可能移除的断言。
9. Sleeping:世界静态化的关键#
phSleep 根据有效线速度、角速度、内部运动、motionless/asleep ticks 判断静止程度;phSleepMgr 管理 sleep islands,包含 generation ID。岛级存储与唤醒能够避免逐个活跃模拟所有静止对象。
这是一种把“可交互世界”压缩为小活动集合的核心优化。代价在于唤醒传播、外力/运动学驱动、卸载和对象 ID 重用必须正确处理。大爆炸、车辆穿过道具群等事件会在一帧把大量休眠成本恢复为活动成本,平均活跃对象数不能反映峰值。
sleep.cpp 中 tolerance 的变量名带平方含义,不能直接当作米/秒阈值报告;tick 次数也不是固定墙钟秒数。步长和更新频率改变后,需要重新理解状态转换时间。
10. 时间步、子步和确定性#
CPhysics::Update 用 fwTimer::GetTimeStep()/ms_NumTimeSlices 计算子步,并循环 PreSim/Sim/PostSim。系统时间步小于 1/60 秒时存在强制单 slice 的路径,且缩放后的步长有条件限幅;开发参数、回放和用户宏可改变路径。
所以当前证据不支持“物理无条件固定 60 Hz”。phSimulator::Update 中 profile capture 强制 1/60 只是采样分支,更不能作为全局运行频率证据。
多线程、浮点 SIMD、接触排序、可变时间切片和游戏回调意味着不能从此代码假定跨机器位级确定性。若新项目计划 rollback/lockstep,需要单独验证输入重放、创建顺序、随机数、浮点环境、solver 排序和物理资源版本。
11. 角色、NaturalMotion 与游戏事件#
articulated 系统提供 joint、bodypart、body inertia 和专用 collider;solver 按固定体/普通动态体/关节体及约束类型查表,存在自碰撞时注入关节限制的路径。大量 ApplyImpulse*、PreResponse* 变体体现了对类型组合的专门化,避免热循环中每次走复杂通用分支。
游戏物理在 PreSim、PostSim 与 NaturalMotion 的 StepPhase1/StepPhase2、fragment 更新、rope 和碰撞事件协作。动画提供姿态与外部速度,物理返回接触及身体结果;质量与时延既取决于 solver,也取决于交换发生在子步的什么位置。
车辆操控、角色控制、爆炸与碎片策略主要属于游戏适配层。不能把某个玩法参数或控制器效果全归因于底层物理算法。相关分析见动画与特效和破坏。
12. Shape test 与仿真应分预算#
异步 shape-test manager 暴露 probe 提交、结果就绪判断及有限请求/结果池。AI 视线、脚底探测、镜头避障、武器射线可能比实际动态刚体更密集;只降低 solver iteration 无法降低这些查询成本。
将查询批处理可以摊薄调度成本,但延迟消费的结果已经属于旧世界状态,需要保持被引用对象的生命周期并根据使用场景检查有效性。与模拟共用空间结构时,也必须遵守 deferred update/读写窗口。
13. 新项目实验矩阵#
| 场景 | 必须测量 | 能区分的问题 |
|---|---|---|
| 分散盒子与密集堆叠,数量相同 | broadphase pairs、manifolds、phase 等待、误差 | 接触密度与并行冲突,而非只有刚体数量 |
| 极端质量比与长关节链 | 约束误差、迭代数、能量漂移 | 求解收敛边界 |
| 高速薄几何、旋转长物体 | TOI/CCD 次数、漏碰撞、最小厚度 | 连续检测和几何限制 |
| 大规模碎裂/唤醒 | 池高水位、最大 phase 数、单帧时间 | 有限容量与瞬时峰值 |
| 地图块持续装卸和瞬移 | 空间更新写锁时间、查询有效性 | 大世界生命周期与物理同步 |
| 同输入重复运行 | 状态差异、接触排序、平台差异 | 确定性是否满足项目需求 |
以上均是建议实验,本次未执行。源码中的“约提升 10%”等 prefetch 注释只对应当时样例,不能移作当前平台指标。
14. 源码证据#
| 文件 | 关键内容 |
|---|---|
| simulator.h | 100–154 active simulator;159–220 显式预算 |
| simulator.cpp | 7199–7330 CoreUpdate;7416 起 Update;7566–7576 池耗尽诊断 |
| levelnew.cpp | 653–707 broadphase 条件选择 |
| OptimizedBvh.h | 量化节点、escape index 与连续布局 |
| boundbvh.h | geometry/BVH 关系及 primitive 重排 |
| collision.cpp | 2142–2145 TOI;composite/BVH 筛选 |
| contactmgr.cpp | 1072–1456 SplitPairs;1738 起 warm start 与 solver 参数 |
| forcesolver.cpp | 84–152 类型查表;677–928 phase 推进;941 起单线程对照 |
| applyimpulsemovandmov.cpp | 20–115 法线与摩擦累计冲量分支 |
| sleep.cpp | ComputeStillness、CheckNearlyStill、Update |
| sleepmgr.h | sleep island 与代际标识 |
| asyncshapetestmgr.h | 异步 probe、容量和结果接口 |
| 游戏 physics.cpp | 2190–2320 子步;PreSimUpdate/PostSimUpdate 与 NM 接口 |