资讯动态

PhysX源码级拆解:从游戏物理到Omniverse工业仿真的底层逻辑

发布时间:2026/9/20 10:54:30 来源:尧图企业网站定制
1. 物理引擎的底层逻辑与PhysX的定位1.1 从游戏显卡到工业仿真PhysX的角色演变很多人第一次听说PhysX是在游戏里那些碎玻璃、飘布料、翻滚的刚体堆叠场景。当年Ageia推出PhysX物理加速卡的时候思路很直接——CPU算物理太慢那就单独插一张卡来算。后来NVIDIA把Ageia收了PhysX变成了CUDA生态里的一套SDK再后来它悄悄变成了Omniverse的物理底座。这个演变路径其实很说明问题。游戏场景对物理精度的要求是“看起来对就行”帧率优先稳定性可以妥协。但工业仿真完全反过来——一个机械臂抓取零件的接触力算错了整个产线节拍分析就废了。PhysX在Omniverse里的定位就是从“视觉可信”升级到“工程可信”。源码层面能看出这个转变。早期PhysX 2.x的solver迭代次数默认很低接触缓存也做得比较粗糙。到了PhysX 4.x和5.xTGS solverTemporal Gauss-Seidel成了默认选项GPU rigid body pipeline也完全重写了。这些改动不是为了游戏帧率而是为了在长时间仿真中保持能量守恒和约束收敛。1.2 为什么企业级仿真开始关注PhysX源码做数字孪生和机器人仿真的团队迟早会遇到一个瓶颈商用仿真软件的黑盒求解器调不动了。接触参数怎么设、约束求解顺序怎么排、GPU和CPU的结果为什么对不上——这些问题在文档里找不到答案只能翻源码。Omniverse的PhysX扩展是闭源分发的但NVIDIA把PhysX SDK本身在GitHub上开源了。这就给了一个切口通过PhysX SDK的源码可以反推Omniverse里物理行为的底层逻辑。比如Omniverse里两个刚体穿透了你可以去PhysX源码里查contact generation的margin设置比如仿真跑久了能量爆炸你可以去看solver的warm starting策略。我见过不少团队在Omniverse里调物理参数全靠试试了三天不如翻一遍PhysX的solver源码。这篇东西就是把这个路径整理出来给做机器人仿真、自动驾驶场景重建、工业数字孪生的同行一个参考。1.3 源码实证评测的方法论“源码实证”这四个字得拆开说。实证意味着不能只看文档和API签名得实际编译、跑case、对比结果。评测意味着要有基准不能凭感觉说“这个solver好”。我的做法是从PhysX SDK的GitHub仓库拉指定版本的源码在Ubuntu 22.04上编译然后用Omniverse Kit的PhysX扩展做对照实验。具体来说同一个场景分别在Omniverse里跑和用裸PhysX SDK跑对比接触力、穿透深度、求解时间。差异大的地方就去源码里找原因。这个方法的好处是Omniverse的物理行为不再是黑盒。你知道它底层用的是哪个版本的PhysX、开了哪些编译选项、solver迭代了多少次。对于需要做仿真可信度评估的团队这些信息比任何文档都值钱。2. PhysX核心架构的源码级拆解2.1 场景描述与宽相碰撞检测PhysX的场景管理核心是PxScene但真正干活的是Sc::Scene这个内部类。源码里Sc::Scene::simulate是整个仿真循环的入口它依次调用collide、solve、integrate三个阶段。宽相碰撞检测用的是SAPSweep and Prune和MBPMulti-Box Pruning的混合方案。源码在Sc::BroadPhase里具体实现是Sc::SAPBroadPhase和Sc::MBPBroadPhase。选择逻辑在Sc::BroadPhase::create里根据场景里动态物体的数量和分布来决定用哪个。这里有个实操细节如果你的场景里大量物体集中在一个小区域MBP会比SAP快很多因为MBP对空间划分更敏感。但MBP的内存开销更大源码里Sc::MBPBroadPhase的mMBP结构会为每个区域维护一个box列表。在Omniverse里做密集物体仿真时这个选择是自动的但你可以通过PxSceneDesc里的broadPhaseType强制指定。窄相碰撞检测在Sc::NPhaseCore里核心是Sc::NPhaseCore::runNarrowPhase。它遍历所有broad phase pair调用具体的碰撞函数。PhysX支持三种碰撞几何凸包、三角网格、高度场。源码里对应的类是ConvexMesh、TriangleMesh、HeightField。凸包碰撞用的是GJKEPA算法源码在Gu::GJK和Gu::EPA。三角网格碰撞用的是SATSeparating Axis Theorem的变体源码在Gu::TriangleMesh。高度场碰撞比较特殊用的是基于网格的快速查询源码在Gu::HeightField。注意三角网格和高度场的碰撞检测在GPU上是不支持的只能跑CPU。如果你的场景里有大量三角网格碰撞GPU rigid body pipeline会把这些pair回退到CPU处理性能反而可能不如纯CPU。2.2 约束求解器TGS与PGS的源码对比PhysX 4.x之前默认用的是PGSProjected Gauss-Seidel求解器4.x之后引入了TGSTemporal Gauss-Seidel。源码里Sc::SolverCore是求解器的基类Sc::PGSolver和Sc::TGSolver是两个具体实现。PGS的思路是逐约束迭代每次迭代更新一个约束的冲量然后立即影响下一个约束。这个方法的优点是实现简单、内存占用小缺点是收敛慢尤其是约束之间耦合强的时候。源码里Sc::PGSolver::solve的迭代次数由PxSceneDesc::solverIterationCounts控制默认是4次速度迭代1次位置迭代。TGS的思路不一样。它把时间步内的约束求解分成多个子步每个子步内做PGS迭代但子步之间会重新线性化约束。源码里Sc::TGSolver::solve的mNbSubSteps控制子步数默认是2。这个改动让TGS在同样迭代次数下收敛更好尤其是对高刚度约束。实测数据一个1000个刚体堆叠的场景PGS需要20次速度迭代才能稳定TGS只需要8次。但TGS的单次迭代开销更大因为要维护子步的状态。源码里Sc::TGSolver比Sc::PGSolver多了一个mSubStepState数组。在Omniverse里PhysX扩展默认用的是TGS。你可以在omni.physx的配置里改solverType但一般不建议改回PGS除非你的场景约束很少且对内存极度敏感。2.3 GPU Rigid Body Pipeline的源码实现GPU rigid body pipeline是PhysX 3.4引入的源码在PxGpu和Sc::GpuRigidBodyPipeline。它的核心思路是把碰撞检测和求解都放到GPU上用CUDA kernel实现。源码里Sc::GpuRigidBodyPipeline::simulate的流程是先做broad phaseGPU上的SAP然后做narrow phaseGPU上的GJK最后做solverGPU上的TGS变体。每个阶段都是一个CUDA kernel数据在GPU显存里流转不回到CPU。这里有个关键细节GPU pipeline的solver和CPU的TGS不完全一样。源码里Sc::GpuSolver用的是Jacobi迭代而不是Gauss-Seidel因为Jacobi更容易并行化。Jacobi的收敛性比Gauss-Seidel差所以GPU solver需要更多迭代次数。源码里默认是8次可以通过PxSceneDesc::gpuSolverIterationCounts调整。另一个细节是接触缓存。CPU pipeline的接触缓存是持久的帧与帧之间可以复用。GPU pipeline的接触缓存是每帧重建的因为GPU显存管理更复杂。源码里Sc::GpuContactCache只在同一个帧内有效。这意味着GPU pipeline在接触状态频繁变化的场景里比如大量物体翻滚优势明显但在稳定堆叠场景里优势不大。提示在Omniverse里启用GPU rigid body pipeline需要在omni.physx的配置里设置gpuCollisionStackSize和gpuMaxNumPartitions。这两个参数直接影响GPU显存占用设太小会回退到CPU设太大浪费显存。2.4 场景查询与射线检测的源码路径场景查询在PhysX里是独立于仿真的源码在Sc::SceneQuery。核心接口是PxScene::raycast、PxScene::sweep、PxScene::overlap。射线检测的源码在Sc::SceneQueryManager::raycast。它先用broad phase做粗筛然后用narrow phase做精确求交。对于凸包用的是射线-凸包求交源码在Gu::raycastConvex。对于三角网格用的是射线-三角形求交源码在Gu::raycastTriangle。这里有个性能陷阱射线检测的broad phase用的是和仿真一样的SAP结构但射线检测是单次查询没有帧间复用。如果你的场景里每帧要做大量射线检测比如激光雷达仿真broad phase的开销会很大。源码里Sc::SceneQueryManager有一个mRaycastBatch机制可以把多个射线打包成一个batch共享broad phase遍历。在Omniverse里做LiDAR仿真时这个batch机制是默认开启的。Sweep查询和射线检测类似但多了个形状。源码在Sc::SceneQueryManager::sweep核心是Gu::sweepConvex和Gu::sweepTriangle。Sweep的精度受PxSceneQueryDesc::sweepHitDistance影响这个参数控制sweep的初始距离设太小会漏检设太大浪费计算。3. Omniverse物理引擎的企业级源码尽调3.1 Omniverse Kit的PhysX扩展架构Omniverse Kit的物理功能是通过omni.physx扩展提供的。这个扩展的源码不在GitHub上但它的二进制包里包含了PhysX SDK的静态库。通过ldd和nm可以反推出它链接了哪些PhysX模块。实测发现omni.physx链接的是PhysX 5.1的静态库编译时开了GPU pipeline、TGS solver、和所有碰撞几何支持。但有几个编译选项是关掉的PX_SUPPORT_GPU是开的PX_SUPPORT_PVD是关的PX_SUPPORT_OMNI_PVD是开的这是Omniverse自己的可视化调试协议。omni.physx的Python API是对PhysX C API的封装。比如omni.physx.get_physx_interface().create_scene()对应的是PxCreateScene。但封装层做了一些默认设置比如PxSceneDesc::flags里默认开了eENABLE_CCD和eENABLE_STABILIZATION。这些默认值在PhysX SDK里是关的Omniverse为了仿真稳定性把它们打开了。注意eENABLE_STABILIZATION会引入额外的位置修正让堆叠更稳定但会牺牲一些物理准确性。如果你的仿真需要精确的接触力建议在Omniverse里手动关掉这个flag。3.2 从Omniverse到PhysX SDK的参数映射Omniverse的物理参数界面很友好但每个参数对应PhysX SDK里的哪个字段文档里没写全。我整理了一个映射表基于源码和实测。Omniverse参数PhysX SDK字段默认值影响Solver TypePxSceneDesc::solverTypeTGS求解器选择Velocity IterationsPxSceneDesc::solverIterationCounts.velocity8速度收敛Position IterationsPxSceneDesc::solverIterationCounts.position2位置收敛GPU CollisionPxSceneDesc::flags eENABLE_GPU_DYNAMICStrueGPU pipelineCCDPxSceneDesc::flags eENABLE_CCDtrue连续碰撞检测StabilizationPxSceneDesc::flags eENABLE_STABILIZATIONtrue位置稳定Bounce ThresholdPxSceneDesc::bounceThresholdVelocity0.2反弹阈值Friction TypePxSceneDesc::frictionTypePATCH摩擦模型Restitution CombinePxSceneDesc::restitutionCombineModeAVERAGE恢复系数合并这个表的价值在于当你在Omniverse里调参调不动的时候可以去PhysX源码里查这个字段的具体实现。比如bounceThresholdVelocity源码里在Sc::BodyCore::updateVelocities里用低于这个速度的碰撞不产生反弹。设大了物体不弹设小了物体乱弹。3.3 企业级场景的物理配置策略企业级仿真场景和游戏场景的配置策略完全不同。游戏场景追求帧率可以牺牲精度。企业场景追求精度可以牺牲帧率。我的配置策略是先关掉所有“优化”选项用最精确的solver跑一遍作为基准。然后逐步打开优化选项看精度损失是否可接受。具体来说第一步关掉eENABLE_STABILIZATION关掉eENABLE_CCD用TGS solver速度迭代16次位置迭代4次。这个配置跑出来的结果作为ground truth。第二步打开eENABLE_STABILIZATION看堆叠稳定性是否提升接触力是否变化。如果变化在5%以内可以接受。第三步打开eENABLE_CCD看高速物体的穿透是否减少。CCD会引入额外的sweep计算性能开销大概10-20%。第四步如果场景物体超过5000个考虑开GPU pipeline。但要注意GPU pipeline的solver是Jacobi精度比CPU的TGS差。如果精度要求高还是用CPU。提示在Omniverse里做机器人抓取仿真时接触力的精度比视觉稳定性重要。建议关掉eENABLE_STABILIZATION用CPU TGS速度迭代12次以上。3.4 源码级调试与性能剖析在Omniverse里调试物理问题光看界面不够得用源码级工具。第一个工具是PhysX的PVDPhysX Visual Debugger。虽然omni.physx默认关了PVD但你可以通过环境变量PX_PVD_HOST和PX_PVD_PORT打开。PVD可以实时看接触点、约束力、solver迭代过程。第二个工具是CUDA profiler。如果开了GPU pipeline用nsys或ncu可以看每个kernel的耗时。源码里Sc::GpuRigidBodyPipeline的kernel名字是gpuCollideKernel、gpuSolveKernel、gpuIntegrateKernel。第三个工具是PhysX的统计接口。PxScene::getScenePvdFlags和PxScene::getStats可以拿到每帧的碰撞对数、约束数、solver迭代次数。在Omniverse里可以通过omni.physx.get_physx_interface().get_scene_stats()拿到。实测数据一个10000个刚体的场景CPU TGS solver每帧耗时约15msGPU pipeline每帧耗时约4ms。但GPU pipeline的接触力误差比CPU高约8%。这个误差在视觉上看不出来但在力控仿真里不能忽略。4. 常见问题与排查技巧实录4.1 物理仿真不稳定的排查路径物理仿真不稳定表现是物体抖动、穿透、爆炸。排查路径得从源码层面走。第一步查solver迭代次数。源码里Sc::SolverCore::solve的迭代次数不够约束残差就大。在Omniverse里把速度迭代调到16位置迭代调到4看是否改善。第二步查接触生成。源码里Sc::NPhaseCore::runNarrowPhase的contact margin设太小接触点就少约束就不足。在Omniverse里把contactOffset调大默认是0.02可以试0.05。第三步查质量比。源码里Sc::BodyCore::computeMassProperties对质量差异大的物体很敏感。如果一个物体质量是另一个的1000倍solver很难收敛。在Omniverse里把质量比控制在100以内。第四步查时间步。源码里Sc::Scene::simulate的dt太大积分误差就大。在Omniverse里把timeStepsPerSecond从60调到120看是否改善。注意调timeStepsPerSecond会成倍增加计算量。如果场景复杂先调solver迭代次数再调时间步。4.2 GPU与CPU结果不一致的源码原因GPU pipeline和CPU pipeline的结果不一致这是常见问题。源码层面的原因有三个。第一个是solver不同。CPU用TGSGauss-SeidelGPU用Jacobi。Jacobi的收敛性差同样迭代次数下残差更大。源码里Sc::GpuSolver::solve的迭代次数默认是8可以调到16。第二个是接触缓存不同。CPU的接触缓存跨帧持久GPU的每帧重建。这导致GPU在稳定接触场景里接触力波动更大。源码里Sc::GpuContactCache只在帧内有效。第三个是浮点精度不同。CPU用doubleGPU用float。源码里Sc::GpuSolver的mPosition和mVelocity都是float。对于大场景坐标超过1000米float精度不够会导致位置漂移。提示如果场景坐标范围大建议用CPU pipeline。如果必须用GPU把场景原点移到物体附近减少坐标绝对值。4.3 源码编译与版本匹配的坑自己编译PhysX SDK源码坑不少。第一个坑是CUDA版本。PhysX 5.1要求CUDA 11.4以上但Omniverse Kit 2023.1用的是CUDA 11.8。如果你编译的PhysX版本和Omniverse的CUDA版本不一致链接会出错。第二个坑是编译器版本。PhysX 5.1的源码在GCC 11上编译会报错因为std::atomic的某些用法在GCC 11上变了。得用GCC 9或打补丁。第三个坑是Python绑定。PhysX SDK的Python绑定是独立的叫physx-python。它的版本必须和PhysX SDK版本严格匹配否则import physx会失败。第四个坑是静态库和动态库。Omniverse用的是静态库你自己编译默认是动态库。静态库的编译选项PX_PHYSX_STATIC_LIB必须打开否则符号冲突。注意如果你只是想在Omniverse里调物理参数不需要自己编译PhysX。只有当你需要改solver实现或加自定义碰撞几何时才需要编译。4.4 常见问题速查表问题现象可能原因源码位置解决方法物体抖动solver迭代不足Sc::SolverCore::solve增加速度迭代次数物体穿透CCD未开或margin太小Sc::NPhaseCore::runNarrowPhase开CCD调大contactOffset仿真爆炸时间步太大或质量比太大Sc::Scene::simulate减小dt控制质量比GPU结果漂移float精度不足Sc::GpuSolver移近场景原点或用CPU接触力不准stabilization开启Sc::BodyCore::updateVelocities关掉eENABLE_STABILIZATION性能突然下降broad phase退化Sc::SAPBroadPhase检查物体分布考虑MBP射线检测漏检sweepHitDistance太小Sc::SceneQueryManager::sweep调大sweepHitDistance约束不收敛约束刚度太高Sc::ConstraintCore降低刚度或增加迭代这个表是我在实际项目里踩坑总结的每个现象都对应到源码里的具体位置。排查的时候按表走比盲目试参数快得多。4.5 实操心得与避坑建议最后分享几个实操心得。第一个不要迷信默认参数。Omniverse的默认参数是给通用场景调的你的场景可能很特殊。比如做机械臂仿真默认的solverIterationCounts是82但机械臂的关节约束刚度高需要124。第二个善用PVD。虽然Omniverse默认关了PVD但打开它能看到接触点和约束力的实时可视化。很多问题看一眼PVD就明白了比看日志快。第三个源码不是万能的。PhysX源码有几十万行不可能全看。我的做法是遇到问题先看Sc::和Gu::这两个命名空间它们包含了场景管理和几何计算的核心逻辑。第四个版本匹配很重要。Omniverse Kit的版本和PhysX SDK的版本有对应关系。Kit 2023.1对应PhysX 5.1Kit 2022.2对应PhysX 5.0。版本不匹配会导致行为差异。第五个性能剖析要用对工具。CPU瓶颈用perfGPU瓶颈用nsys内存瓶颈用valgrind。PhysX的统计接口只能给个大概精确剖析还得靠系统工具。我在实际项目里遇到过一个案例一个抓取仿真物体总是从夹爪里滑出来。调了摩擦系数、接触刚度、solver迭代都没用。最后翻源码发现Sc::NPhaseCore的contact generation对凸包-凸包碰撞用的是GJKGJK在夹爪这种薄壁结构上接触点生成不稳定。解决办法是把夹爪的碰撞几何从凸包换成三角网格接触点就稳定了。这个坑在文档里绝对找不到只能翻源码。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价