简介:PhysX-Modeling是一套面向PhysX 3物理引擎入门的Visual Studio示例工程集合,适合游戏开发、实时仿真方向初学者,以及希望快速搭建物理场景的开发者。压缩包共15个文件,以cpp源文件、h头文件和sln、vcxproj等工程配置为主,整体大小约20KB,轻量精简,便于直接编译运行并对照学习。已有213人学习浏览该资源。内容按章节递进组织,从Hello Physx环境配置、刚体创建,到碰撞检测等关键环节均有对应示例,可结合源码理解物理场景搭建、碰撞组设定与物体属性调整,并逐步延伸至关节约束和柔体动力学。工程中每种示例均配有独立项目,sln文件可一键打开,filters文件帮助梳理源码结构,降低上手门槛。通过这套资料,读者能减少环境摸索成本,快速掌握PhysX 3的基础工作流,同时参考工程文件结构熟悉Visual Studio下的物理引擎项目组织方式,为后续在游戏或仿真项目中集成真实物理效果打下扎实基础。
1. 物理引擎建模不是画模型,是给物体写物理身份
一个名为 PhysX-Modeling.rar 的资源包,最容易让人误以为是 3D 建模资产。实际上物理引擎建模(Modeling)要解决的是另一件事:物体用什么碰撞形状、质量怎么分布、摩擦和阻尼给多少值、关节允许转几个自由度,这些参数合在一起,才决定了一个仿真场景在物理上是"能看"还是"能算"。同样是往桌面放一个杯子,直接拿渲染网格当碰撞体和用胶囊体加凸包组合建模,PhysX 的求解稳定性与 CPU 开销完全不同。接下来按 PhysX 的 Scene-Actor-Shape 结构、复合碰撞体、关节约束、仿真同步与排错验证的顺序展开,适合机器人仿真、Unity 物理调试,以及想从 MuJoCo 物理引擎、Rapier 物理引擎的对照中反观 PhysX 的工程师。
2. PhysX 建模的第一层:Scene、Actor 与 Shape
PhysX 里搭一个物理场景,不需要先考虑渲染,先把三层结构立起来:Scene 是求解器,Actor 是物理对象的载体,Shape 是挂在 Actor 上参与碰撞的形状。一个 Scene 装多个 Actor,一个 Actor 挂零到多个 Shape,每个 Shape 通过 PxMaterial 拿到表面材质属性。在 Unity 里看到的那套 Rigidbody、Collider、PhysicMaterial,就是这套结构的上层封装,搞清底层命名后排查问题会直接很多。
2.1 Scene 不是容器,是求解器的配置面板
PxSceneDesc 里 gravity 和 tolerancesScale 这两个参数最容易被忽略出问题。gravity 常规设 (0, -9.81, 0),但仿真场景如果放在平面坐标系而不是世界坐标系,重力方向要跟着改,很多人只改数值不改方向。tolerancesScale 是 PhysX 特有的尺度配置,默认 1.0 适配"米"单位场景;如果建模时物体尺寸在毫米级或千米级,接触计算会由于浮点精度产生抖动,毫米场景设 length=0.001,千米场景设 length=1000。另外 filterShader 决定了哪些 Actor 之间要过滤碰撞,机器人仿真中机械臂本体不参与自碰撞、夹爪要"穿透"目标物体这类需求,都在 filterShader 里做,而不是事后去调碰撞体形状。
2.2 Actor 与 Shape:静态地面和动态物体的建模差异
PhysX 把 Actor 分成 PxRigidStatic 和 PxRigidDynamic 两类,这是建模时第一个要做的分类决策。地面、墙壁这类不动物体用 Static,会受力移动的物体用 Dynamic。分类决策会直接影响性能:Static 不参与求解,一个静态 Actor 挂几十个 Shape 几乎没有开销;而 Dynamic 每次 simulate 都要参与积分和约束求解,Shape 越复杂越吃 CPU。给同一个 Dynamic Actor 挂多个 Shape 的场景在第 3 章展开,这里先记住一个原则:能静态就不动态。
动态物体的质量不能靠猜。PhysX 里如果创建了 PxRigidDynamic 却不调用质量设置函数,物体默认被当作无限大质量,碰撞时对方不会得到正确的反作用力,这在重物压轻物的场景里表现得很明显。质量设置可以按密度或按总质量走,两个 API 分别是 updateMassAndInertia 和 setMassAndUpdateInertia,多数人习惯用总质量再自动算惯性张量,这个习惯在 PhysX 和 MuJoCo 物理引擎里都适用。
2.3 最小可运行场景:一段能看见立方体落地的代码
以下用 C++ API 搭了 PhysX 里最简的建模与仿真循环,对应一个立方体从 10 米高度落到 1 米高的地面上:
#include <PxPhysicsAPI.h> #include <cstdio> using namespace physx; PxDefaultErrorCallback gError; PxDefaultAllocator gAlloc; PxFoundation* gFoundation = PxCreateFoundation(PX_PHYSICS_VERSION, gAlloc, gError); PxPhysics* gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, PxTolerancesScale()); PxSceneDesc desc(gPhysics->getTolerancesScale()); desc.gravity = PxVec3(0.0f, -9.81f, 0.0f); desc.cpuDispatcher = PxDefaultCpuDispatcherCreate(2); desc.filterShader = PxDefaultSimulationFilterShader; PxScene* scene = gPhysics->createScene(desc); PxMaterial* mat = gPhysics->createMaterial(0.6f, 0.5f, 0.1f); // 静态地面:半尺寸为 50*0.5*50 的盒体 PxRigidStatic* ground = gPhysics->createRigidStatic(PxTransform(PxVec3(0, 0, 0))); PxShape* groundShape = ground->createShape(PxBoxGeometry(50.0f, 0.5f, 50.0f), *mat); scene->addActor(*ground); // 动态立方体:边长 1 米,质量 10kg,初始高度 10 米 PxRigidDynamic* cube = gPhysics->createRigidDynamic(PxTransform(PxVec3(0, 10, 0))); PxShape* cubeShape = cube->createShape(PxBoxGeometry(0.5f, 0.5f, 0.5f), *mat); PxRigidBodyExt::setMassAndUpdateInertia(*cube, 10.0f); scene->addActor(*cube); // 模拟 100 帧,打印 y 轴高度 for (int i = 0; i < 100; i++) { scene->simulate(1.0f / 60.0f); scene->fetchResults(true); PxTransform t = cube->getGlobalPose(); printf("frame=%d y=%.3f\n", i, t.p.y); }代码逻辑拆开看:第 1 到第 4 行创建 foundation 和 physics 两个全局对象,它们是所有场景的父级;第 6 到第 11 行配置场景,其中PxDefaultCpuDispatcherCreate(2)里的 2 表示用两个线程跑求解,多核平台可以调大,但场景物体不多时线程切换开销反而明显。地面用PxBoxGeometry(50, 0.5, 50)时传入的是半尺寸,所以地面实际厚度 1 米、长宽各 100 米;材质的 0.6、0.5、0.1 对应静摩擦、动摩擦、恢复系数。立方体用setMassAndUpdateInertia(*cube, 10.0f)直接设总质量 10kg,PhysX 会按形状自动计算惯性张量,如果换成updateMassAndInertia(*cube, 1.0f)则是按密度 1.0 计算,单位是 kg/m³,给真实材质赋值时用后一种更接近物理直觉。
提示:PhysX 的 PxBoxGeometry 构造函数接收的是半尺寸,50 表示单边 50,不是总长 100。渲染侧习惯用全尺寸时,这里最容易差出一倍。
这一段执行后应该看到 y 从 10 开始递减,落地后停在 1.0 附近。如果 y 值一直不变或者反弹越来越高,优先检查三个位置:材质恢复系数是否大于 0.8、tolerancesScale 是否与场景尺度匹配、地面 Shape 半尺寸是否写成了全尺寸。后两个错误在 PhysX 建模里属于"不报错但结果不对"的典型,把几何参数单独抽成具名常量,便于和渲染比例对账。
材质参数在 PhysX 建模中集中在一张表里,调参时对照着改不容易漏:
| 参数 | 典型范围 | 作用 | 常见误用 |
|---|---|---|---|
| staticFriction | 0.1 ~ 1.0 | 决定物体能否从静止被推动 | 设 0 后物体会在地面漂移 |
| dynamicFriction | 0.0 ~ 1.0 | 滑动时的摩擦阻力 | 常大于静摩擦导致走走停停 |
| restitution | 0.0 ~ 1.0 | 碰撞反弹强度 | 超过 0.8 容易能量发散 |
| linearDamping | 0.0 ~ 5.0 | 线性速度衰减 | 过高让物体看起来在水中运动 |
| angularDamping | 0.0 ~ 5.0 | 角速度衰减 | 回转体不设角阻尼会转不停 |
3. 复合碰撞体与关节约束:PhysX 建模的进阶动作
基础场景能跑通之后,真正的建模工作量集中在两类动作上:把不规则物体拆成多个基本形状组合,以及用关节把多个物体连接成机构。这两件事在 PhysX 里分别对应复合碰撞体和约束建模,也是 PhysX 物理引擎和 MuJoCo 物理引擎、Rapier 物理引擎拉开差距的地方——后两者倾向于从格式层约束建模方式,而 PhysX 把自由度完全交给开发者,模型好坏全看参数。
3.1 复合碰撞体:一个 Actor 挂多个 Shape
常见的做法是给同一个 PxRigidDynamic 挂上多个 PxShape,每个 Shape 用局部变换偏移到对应位置。这样做比直接用三角形网格碰撞体省 CPU,而且接触特征稳定——网格碰撞体在边角处的接触法线容易跳变,基本几何组合则不会。拿一个"T"形工作台为例,一个竖直 Box 加一个水平 Box 就能建模:
// T 形工作台:竖直支撑 + 水平台面 复合建模 PxRigidDynamic* table = gPhysics->createRigidDynamic(PxTransform(PxVec3(0, 5, 0))); // 台面:水平放置的薄板 PxShape* top = table->createShape( PxBoxGeometry(1.0f, 0.05f, 0.6f), *mat, PxTransform(PxVec3(0, 0, 0))); // 支撑:竖直放置的细柱,向下偏移 0.55 米 PxShape* leg = table->createShape( PxBoxGeometry(0.8f, 0.55f, 0.05f), *mat, PxTransform(PxVec3(0, -0.55f, 0))); PxRigidBodyExt::setMassAndUpdateInertia(*table, 8.0f);注意 createShape 的第三个参数,它是 Shape 相对 Actor 质心的局部变换。这里支撑柱在 y 方向偏移 -0.55 是为了让两个 Box 拼出 T 形,而不是在原点叠成十字。很多人在这里直接沿用 Actor 的世界坐标,导致复合体拼起来位置全错,排查方法也很简单:把每个 Shape 的局部 pose 单独打出来,和渲染子部件的相对坐标对一遍。
复合碰撞体也常用于人体建模,机器人仿真里只用球和胶囊组合出肢体,MuJoCo 物理引擎的 MJCF 里也默认推荐这种 primitive 组合思路。PhysX 在复合建模中同样能处理凸包网格,但凸包的接触计算比基本几何贵一个量级,能用 Box、Capsule、Sphere 表达的形状就不要生成 ConvexMesh。
3.2 关节约束:铰链、球关节与 D6 的参数选择
多体建模躲不开关节。PhysX 提供 RevoluteJoint、SphericalJoint、PrismaticJoint、D6Joint 四类常用约束,建模时先选类型再配参数,顺序反了容易陷入细节里。选型参照下表:
| 关节类型 | 约束掉的自由度 | 适用场景 | 关键参数 |
|---|---|---|---|
| RevoluteJoint | 平移全锁,只留 1 个旋转 | 门、轮子、折叠机构 | limit、driveVelocity |
| PrismaticJoint | 旋转全锁,只留 1 个滑动 | 滑轨、活塞、升降台 | limit、spring stiffness |
| SphericalJoint | 平移全锁,旋转自由 | 机械臂腕部、万向节 | swing limit |
| D6Joint | 由运动锁定自由组合 | 复杂机构、车辆悬挂 | 6 个自由度逐个配置 |
D6Joint 是 PhysX 最有特色的关节,它把 6 个自由度拆成三组平移和三组旋转,每组可以设 lock、limited、free 三种状态。车辆悬挂建模时摆臂稳定不掉链子,靠的就是 D6Joint 把某个方向 lock、另一个方向 limited 并配置 spring。相比之下 Rapier 物理引擎也覆盖这些关节类型,但它的接口更依赖类型系统在编译期确定自由度,而 PhysX 是运行时配置,自由度灵活但少了编译期检查,错一个自由度设置在场景跑起来之前不会报错。
3.3 CCD 与迭代次数:稳定性与性能的取舍
高速物体穿透是建模完后第一个翻车点。子弹在 60Hz 步长下每帧移动 30 米,窄碰撞体很容易直接穿过,解决办法是给高速物体开启连续碰撞检测(CCD):
cube->setRigidBodyFlag(PxRigidBodyFlag::eENABLE_CCD, true);CCD 不是全局开关而是逐物体开关,建模时只给运动速度可能超过"单帧位移大于自身碰撞体尺寸"的对象打开,否则 PhysX 会对场景里所有接触对做额外的 swept 检测,开销非线性上涨。同样的问题在 MuJoCo 物理引擎里处理方式不同,它默认用更保守的接触几何来规避,代价是高速物体响应偏软。迭代次数方面,PxSceneDesc 里 solverIterationCounts 默认位置 4、速度 1,堆箱子总是不稳时先把位置迭代提到 8,弹跳异常时提速度迭代,两个参数都不建议超过 16,再高只是把误差从可见抖动压到不可见的微抖动,CPU 时间却成倍加。
4. 把建模结果跑进仿真循环:步长、插值与引擎设计对照
PhysX 建模的收尾不是导出一份配置完事,而是把参数正确地带进仿真循环。这一章处理三件事:质量与惯性怎么给、物理步长和渲染帧率如何协同、以及 PhysX 与 MuJoCo 物理引擎、Rapier 物理引擎在建模入口上的差异。
4.1 质量、惯性张量与 setMassAndUpdateInertia 的重载
第 2 章里用了setMassAndUpdateInertia(*cube, 10.0f),这个调用的第二个参数是总质量,单位 kg。PhysX 内部会结合 Shape 的形状和尺寸计算惯性张量,均匀密度假设下这个近似对大多数场景足够。如果物体的质量分布明显不均匀,比如跷跷板一端挂重块,则要显式设置质心的局部位置和惯性张量,不能在 setMassAndUpdateInertia 之后再手动 setMass,两者会相互覆盖。建模时把质量、质心偏移、惯性张量三个值写成一组结构体,便于后续对不同刚体参数做脚本化批量修改。
4.2 固定时间步长:不随渲染帧波动的模拟
PhysX 的 simulate 需要开发者自己保证累加时间均衡。常见做法是固定步长累加器,下面按 C++ 风格写循环骨架,getFrameDeltaTime 换成你自己的帧计时函数即可:
float accumulator = 0.0f; const float dt = 1.0f / 60.0f; // 物理步长固定 60Hz while (running) { float frameTime = getFrameDeltaTime(); // 渲染帧耗时 accumulator += frameTime; while (accumulator >= dt) { scene->simulate(dt); scene->fetchResults(true); accumulator -= dt; } // prevFramePose 在本帧结束时更新为 cube 的姿态 float alpha = accumulator / dt; // 插值权重,0~1 PxTransform prevPose = prevFramePose; PxTransform currPose = cube->getGlobalPose(); renderPose.p = prevPose.p + (currPose.p - prevPose.p) * alpha; renderPose.q = prevPose.q.slerp(currPose.q, alpha); }为什么不能直接scene->simulate(frameTime)?因为渲染帧率波动时,物理求解器会在不同步长之间切换,接触稳定性会被破坏,明明建模参数没变,同一个斜坡有时滚得快有时滚得慢。固定步长让每次求解的时间间隔一致,累积误差变得可预测。插值部分解决的是"物理 60Hz、渲染更高刷新率"的显示平滑问题,出现物体运动一顿一顿的情况时先检查插值有没有写,而不是先怀疑建模参数。
4.3 三种物理引擎的建模入口对照
把 PhysX 建模做熟了再去看 MuJoCo 物理引擎,会发现它把建模格式统一成了 MJCF/URDF 的 XML,所有碰撞体、关节、驱动器都从配置文件加载,优点是复现性强,缺点是一旦遇到配置表达不了的特殊几何,扩展只能靠改源码。Rapier 物理引擎作为 Rust 生态里的代表,建模思路更偏"代码即配置",所有碰撞体和约束用结构体直接构造,没有中间格式,调试直观但不利于跨语言复现。PhysX 介于两者之间:有 C++ API 和工具链,又有 Unity、Unreal 等引擎的成熟封装,建模自由度最大,但对开发者的物理直觉要求也最高。
| 引擎 | 建模入口 | 碰撞体默认形式 | 约束表达 | 调参方式 |
|---|---|---|---|---|
| PhysX | C++ API / Unity 封装 | Box、Sphere、Capsule、ConvexMesh | 4 类关节,D6 自由度最灵活 | 运行时参数直接生效 |
| MuJoCo 物理引擎 | MJCF/URDF 文件 | 球、胶囊、椭球等基本几何 | joint 节点定义自由度与限位 | 改 XML 后重新加载 |
| Rapier 物理引擎 | Rust 结构体 | 基本形状与凸包 | 类型系统表达自由度 | 改代码重新编译 |
这张表是主流工作流的简化对照,MuJoCo 也有 C++ 底层 API,Rapier 也有 Python/JavaScript 绑定,但工程上大多数人的建模入口就是表格里这个方向。同一份工作台建模,PhysX 里错一个局部偏移,和 MuJoCo 里错一个 XML 标签一样难查,只是报错形式不同。
5. 建模验证与排错:从穿模到抖动的三条排查路径
建模完成后的验收标准不是"不报错",而是把物体放回物理规律里看结果。穿模、抖动、运动轨迹异常是三个最常出现的问题,下面按排查路径整理。
5.1 穿模:先查 Shape 尺寸和局部变换
物体穿过地面,很多人第一反应是开 CCD,其实第一步该做的是打印 Shape 的世界包围盒,和渲染网格对一比。常见错误有两类:BoxGeometry 把半尺寸写成了全尺寸,或者复合碰撞体中某个 Shape 的局部变换没有跟着渲染子节点一起移动。用 PxGeometryQuery 可以同时验证这两种情况:
// 计算 Shape 在世界空间下的 AABB,用于对账碰撞范围 PxBounds3 bounds; PxGeometryQuery::getWorldBounds(cubeShape->getGeometry(), cube->getGlobalPose() * cubeShape->getLocalPose(), bounds); printf("min=(%f,%f,%f) max=(%f,%f,%f)\n", bounds.minimum.x, bounds.minimum.y, bounds.minimum.z, bounds.maximum.x, bounds.maximum.y, bounds.maximum.z);这段代码把 Shape 的全局变换和几何参数合在一起算出世界坐标包围盒,打印出来后与渲染网格对比,建模穿模问题基本能定位。注意需要包含 PxGeometryQuery.h,否则编译期内联会报未定义类型。包围盒正确仍穿模的情况,再检查物体单帧位移是否大于自身碰撞体厚度,如果是,就针对该物体的运动速度开启 CCD,而不是全局打开。
5.2 抖动:位置迭代、restOffset 与 sleep 三连
堆叠物体持续抖动,先把 solverIterationCounts 位置迭代从 4 提到 8,大部分场景这一步就稳了。还抖,把 restOffset 从 0 调到 0.02,让两个碰撞体在完全接触之前就生成接触信息,避免接触状态每帧反复切换。最后看睡眠阈值,PhysX 默认 sleep threshold 是 0.005 速度单位,物体速度总压不下去时适当调高到 0.02 或 0.05,让静止的堆叠体尽早进入睡眠状态,CPU 占用立刻下降。这三个参数的调整顺序不要反,先精度后容差再睡眠,否则会掩盖真正的问题。
5.3 一个快速自检脚本模板
不打开可视化工具也能验收:跑一段 120 帧仿真,打印一个动态物体的 y 轴轨迹,检查落地高度和反弹节奏是否符合预期。落点应该在 1.0 附近,y 轨迹呈"自由落体、接触、小幅反弹、静止"四阶段。锯齿形抖动回查 5.2 的三个参数,穿透负值回查 5.1 的包围盒。PhysX 建模是否可信,最终体现在这条轨迹能否在脚本中稳定复现,不依赖可视化工具的人工观察。
本文还有配套的精品资源,点击获取