这次不讲“工厂、接口、模块”,我们换成一个可以在脑海里播放的场景:
你有一套神奇的积木玩具。搭好以后,按下播放键,积木就会遵守重力、碰撞和摩擦规律,自己动起来。
我们用它搭建一个“滚球撞积木”的小游戏,看看PxPhysics到底在做什么。
一、先摆出两只玻璃箱
桌面上有两只互不相通的玻璃箱:
玻璃箱 A 玻璃箱 B 普通重力 低重力 ● 小球 ● 小球 ╱ ╱ ╱ 斜坡 ╱ 斜坡 ▣▣▣ 积木 ▣▣▣ 积木 ──────────── ────────────你希望:
- A 箱的小球快速滚下来;
- B 箱的小球缓慢下落;
- 两边互不影响。
在 PhysX 中:
玻璃箱 A → PxScene A 玻璃箱 B → PxScene B每个PxScene就是一个独立的物理世界。
那么PxPhysics是哪一个?
它不是玻璃箱,而是整套神奇玩具的“总管家”。
你向它申请:
给我两个物理世界,再给我几个小球、积木、斜坡,以及它们需要的碰撞资源。
它负责创建这些东西,并把它们纳入 PhysX 的对象体系。
一个 PxPhysics ├── 创建玻璃箱 A ├── 创建玻璃箱 B ├── 创建小球和积木 └── 创建可以复用的物理资源玩具可以由同一个管家创建,但具体在哪个箱子里运动,由 Scene 决定。
二、神奇玩具有个特点:外观和“物理身体”是分开的
你拿出一个画得非常精美的小球:
- 表面有条纹;
- 会发光;
- 看起来是金属做的。
但对物理系统来说,这些都不够。
它还需要知道:
球有多大? 在哪里? 有多重? 是否可以运动? 碰到地面会不会弹起来?所以,一个物体被拆成了几部分。
1. Actor:小球的“运动档案”
档案中记录:
位置:空中 速度:目前为零 质量:1 千克 是否可以运动:可以这对应动态刚体:
PxRigidDynamic它描述物体作为一个整体,如何存在与运动。
而固定不动的斜坡和地面,可以使用:
PxRigidStatic可以简单记成:
Actor 关心“这个东西在哪里,以及它怎么动”。
2. Shape:给小球套上的“隐形碰撞壳”
物理系统并不是看着屏幕上的图片判断碰撞。
它需要一个明确的几何外壳:
球形外壳 盒形外壳 胶囊形外壳 凸网格外壳 ……小球就套上球形碰撞壳,积木套上盒形碰撞壳。
这对应:
PxShape想象一只画得很复杂的玩具小熊,你完全可以先给它套一个简单的胶囊形碰撞壳:
画面里:一只小熊 物理里:一个胶囊体所以:
看起来是什么形状,不代表参与碰撞的就是什么形状。
严格说,PxShape不只有几何外壳,还带着材质、过滤规则以及是否参与碰撞或查询等配置。
3. Material:碰撞壳表面的“手感”
同样一个球形碰撞壳:
- 表面像橡胶,碰撞后可能更容易弹起;
- 表面像冰,滑动时摩擦较小;
- 表面粗糙,更容易阻碍相对滑动。
这些接触属性由:
PxMaterial描述。
但它不是渲染材质:
涂成金色 ≠ 物理上更重 贴上冰纹 ≠ 摩擦自动变小质量也不是常规PxMaterial自动决定的,需要另外设置或计算。
于是,一个小球的物理结构就是:
Actor:位置、速度、质量等运动信息 │ └── Shape:球形碰撞外壳 │ └── Material:摩擦、弹性等接触属性三、为什么“创建了小球”,它却没有掉下来?
因为你只是让管家把玩具拿出来了,还没放进玻璃箱。
对应代码:
PxRigidDynamic*ball=physics->createRigidDynamic(...);这一步的含义只是:
“创建一个动态刚体对象。”
之后还要给它装上 Shape、设置质量等。
然后:
sceneA->addActor(*ball);这才是:
“把小球放进玻璃箱 A。”
但它仍然不会因为加入场景,就在后台自行不停运动。
你还需要按下“时间前进”按钮。
sceneA->simulate(dt);sceneA->fetchResults(true);所以整个过程是:
创建小球 ↓ 装上碰撞壳、配置物理属性 ↓ 放入玻璃箱 ↓ 让箱子里的时间前进一步 ↓ 小球的位置和速度发生变化这就是为什么要区分:
创建对象、加入世界、推进时间。
四、按下播放后,究竟发生了什么?
现在按下按钮,让世界前进1/60秒。
你看到小球撞向积木。系统内部并不是一句:
发现碰撞,让它弹开。而是需要回答一连串问题。
第一步:谁可能撞到谁?
箱子里有一千块积木,但小球目前只在斜坡附近。
系统先粗略筛选:
小球与附近几块积木:值得进一步检查 小球与远处积木:暂时不用精查这类似于先看地图缩小搜索范围,称为Broad Phase,广相位检测。
第二步:到底碰上没有?
对于筛出的候选对象,再仔细检查:
是否接触? 接触在哪里? 接触方向是什么? 是否发生穿透?这属于Narrow Phase,窄相位检测。
第三步:撞上以后应该怎么动?
接下来需要同时考虑:
- 小球和积木的质量;
- 当前速度;
- 接触位置;
- 摩擦;
- 恢复系数;
- 其他接触和约束。
例如:
小球撞积木 ↓ 积木又顶着另一块积木 ↓ 两块积木都压在地面上不能只处理小球和第一块积木,而忽略整个接触关系。
约束求解器会计算这些关系下的运动响应。
第四步:拿到新的状态
最终得到:
小球减速 第一块积木倾倒 第二块积木滑动游戏再读取更新后的位姿,把画面中的模型摆到对应位置。
这里描述的是简化的逻辑流程;实际内部执行会有任务并行和更复杂的阶段安排。
这些模拟工作由 Scene 组织,而不是PxPhysics自己每帧遍历所有物体来完成。
五、为什么一千块积木不需要一千份模具?
假设你需要一千块形状相同的复杂积木。
没必要给每一块都保存完整的碰撞网格。
可以这样:
一份复杂碰撞网格 ▲ │ 多个 Shape 引用 │ 一千个独立刚体就像:
同一个模具制造出一千个玩具,但每个玩具的位置和运动状态不同。
这里共享的是几何资源,不是运动状态。
可以共享: 碰撞网格、材质等适合共享的资源 分别保存: 位置、速度、质量等实例状态PxPhysics的一个重要作用,就是创建和管理这类 SDK 级资源,让不同对象可以合理复用它们。
六、把玩具拿出箱子,为什么不等于扔掉玩具?
你把小球从玻璃箱 A 拿出来:
sceneA->removeActor(*ball);表示:
小球不再参加 A 箱的模拟。
但小球对象还存在。你可以在适当条件下把它放进另一个场景。
这与:
ball->release();不同。后者表示释放这个刚体对象。
所以:
从 Scene 移除 ≠ 销毁对象而 Shape、Material 等引用计数对象又有额外规则:释放自己持有的一份引用,不一定立即销毁资源,因为其他对象可能还在使用它。
场景成员关系、对象生命期、资源引用,是三件不同的事。
七、最后回头看 PxPhysics,就清楚了
完整画面如下:
PxFoundation 提供底层内存与错误报告 │ ▼ PxPhysics 整套物理玩具的总管家 │ ├── 创建刚体 ├── 创建碰撞形状和材质 ├── 创建、管理共享资源 │ └── 创建 PxScene │ ├── 容纳物体 ├── 配置这个世界 └── 组织模拟,让时间前进用三句话记住:
PxPhysics 负责:你能创建和使用哪些物理对象与资源。
PxScene 负责:哪些对象在同一个世界里,以及这个世界怎样向前运行。
Actor、Shape、Material 分别描述:怎么动、以什么外形碰撞、接触时如何响应。
因此,当你写:
physics->createRigidDynamic(...);是在向管家领取玩具。
当你写:
scene->addActor(...);是在把玩具放进玻璃箱。
当你写:
scene->simulate(dt);才是在说:
“让这个世界的时间,向前走一步。”