PhysX中的四类物理对象
PhysX中的这四类物理对象为:
| 类型 | PhysX 类型 | 质量 | 受力/重力 | 谁驱动它 | 典型用途 |
|---|---|---|---|---|---|
| Static | PxRigidStatic |
无限 | ❌ | 不动 | 地形、墙体、关卡几何 |
| Kinematic | PxRigidDynamic + eKINEMATIC 标志 |
无限 | ❌ | 代码/动画显式设目标 | 电梯、移动平台、门、角色胶囊 |
| Dynamic | PxRigidDynamic |
有限 | ✅ | 求解器 | 桶、箱子、碎块、布娃娃 |
| Cloth | 逐粒子 | ✅ | 约束求解(PBD/FEM) | 披风、旗帜、软体表面 |
注:前三者是刚体,走同一套 rigid body solver(刚体解算器);Cloth 是可变形体,走完全独立的求解管线
与UE的对应关系
| UE 概念 | PhysX 对应 |
|---|---|
| Mobility = Static | PxRigidStatic |
| Mobility = Movable + Simulate Physics 关 | Kinematic PxRigidDynamic |
| Mobility = Movable + Simulate Physics 开 | Dynamic PxRigidDynamic |
SetActorLocation on Movable |
内部走 setKinematicTarget |
| Clothing Tool | NvCloth(UE4) |
碰撞交互矩阵
| Type | Static | Kinematic | Dynamic | Cloth |
|---|---|---|---|---|
| Static | ❌ | ❌* | ✅ | ❌(需手动加碰撞体) |
| Kinematic | ❌* | ❌* | ✅ 单向推 | ❌(需手动) |
| Dynamic | ✅ | ✅ 被推 | ✅ | ❌(需手动) |
| Cloth | — | — | — | 支持inter-collision(内部碰撞)、self-collision(自碰撞) |
注1:* 可通过 PxSceneDesc 的 filtering mode 开启事件报告
注2:NvCloth 完全不知道 PhysX 场景的存在。不参与刚体的 broad phase,开发者必须把角色的骨骼胶囊、场景碰撞体手动喂给 cloth 的 collision 数据。这是布料"穿模"最常见的原因
Static(静态刚体)
PxRigidStatic* ground = PxCreateStatic(
*gPhysics,
PxTransform(PxVec3(0, 0, 0)),
PxPlaneGeometry(),
*gMaterial);
gScene->addActor(*ground);
特点
- 没有速度、没有质量属性、不参与积分
- 存放在静态 pruner(静态 AABB 树)里,场景查询(raycast/sweep/overlap)针对它做过深度优化
- Static 之间永不产生接触
关键坑:不要移动 Static
// ❌ 能编译能跑,但代价极高
staticActor->setGlobalPose(newPose);
移动静态 actor 会导致静态 pruner 重建或增量刷新,帧率会明显掉。真要移动就用 Kinematic
PhysX 提供
PxPruningStructureType::eDYNAMIC_AABB_TREE和eSTATIC_AABB_TREE,后者假设不变、重建成本高但查询最快。
Kinematic(运动学刚体)
它本质上是 Dynamic,只是打了标志位:
PxRigidDynamic* platform = PxCreateDynamic(
*gPhysics, PxTransform(PxVec3(0,5,0)),
PxBoxGeometry(2,0.2f,2), *gMaterial, 10.0f);
platform->setRigidBodyFlag(PxRigidBodyFlag::eKINEMATIC, true);
gScene->addActor(*platform);
驱动方式(重点)
// ✅ 正确:设置目标位姿,PhysX 会反推速度,从而正确推动 dynamic
platform->setKinematicTarget(PxTransform(newPos, newRot));
// ❌ 错误:瞬移,速度为 0,dynamic 会被"穿透"或直接挤爆
platform->setGlobalPose(newPose);
setKinematicTarget 的意义在于:PhysX 用 (target - current) / dt 算出隐含速度,接触求解时才知道该给上面的箱子多少推力。
用 setGlobalPose 的话平台会直接"闪现"到新位置,箱子要么掉下去要么被弹飞。
碰撞行为
| 组合 | 是否产生接触 |
|---|---|
| Kinematic ↔ Dynamic | ✅ Kinematic 单方向推开 Dynamic |
| Kinematic ↔ Kinematic | ❌ 默认不碰撞 |
| Kinematic ↔ Static | ❌ 不碰撞 |
想让 Kinematic 之间/与 Static 产生触发或报告事件,需要:
sceneDesc.kineKineFilteringMode = PxPairFilteringMode::eKEEP;
sceneDesc.staticKineFilteringMode = PxPairFilteringMode::eKEEP;
再配合 filter shader 里返回 eNOTIFY_TOUCH_FOUND 等。
其他要点
- Kinematic 无法被推动、无法被力影响,
addForce无效 - Kinematic ↔ Dynamic 的 CCD 需显式开启
PxRigidBodyFlag::eENABLE_CCD - 切回 Dynamic:
setRigidBodyFlag(eKINEMATIC, false),此时会继承当前速度(记得先setLinearVelocity)
Dynamic(动态刚体)
PxRigidDynamic* box = PxCreateDynamic(
*gPhysics, PxTransform(PxVec3(0,10,0)),
PxBoxGeometry(0.5f,0.5f,0.5f), *gMaterial, 1.0f);
PxRigidBodyExt::updateMassAndInertia(*box, 10.0f); // 密度 10
box->setLinearDamping(0.05f);
box->setAngularDamping(0.05f);
box->setSolverIterationCounts(8, 2); // 位置迭代/速度迭代
box->setRigidBodyFlag(PxRigidBodyFlag::eENABLE_CCD, true);
gScene->addActor(*box);
box->addForce(PxVec3(0, 100, 0), PxForceMode::eIMPULSE);
核心属性
- 质量 / 质心 / 惯性张量(
updateMassAndInertia会自动从形状算) - 线速度、角速度、阻尼
- 求解器迭代次数:堆叠、关节链多的场景要调高
- 睡眠机制:速度低于
sleepThreshold持续一段时间后进入 sleep,不再消耗 CPU
box->setSleepThreshold(0.05f);
box->setWakeCounter(2.0f);
if (box->isSleeping()) box->wakeUp();
CCD(连续碰撞检测) 高速小物体穿墙的解法。需要三处同时开:
sceneDesc.flags |= PxSceneFlag::eENABLE_CCD; // 1. 场景
body->setRigidBodyFlag(PxRigidBodyFlag::eENABLE_CCD, true); // 2. 刚体
// 3. filter shader 里返回 PxPairFlag::eDETECT_CCD_CONTACT
锁定自由度(做 2D 游戏或直立角色很有用)
body->setRigidDynamicLockFlags(
PxRigidDynamicLockFlag::eLOCK_ANGULAR_X |
PxRigidDynamicLockFlag::eLOCK_ANGULAR_Z);
Cloth(布料)
| PhysX 版本 | 布料方案 | 状态 |
|---|---|---|
| 3.3 / 3.4 | PxCloth + PxClothFabric |
3.4 起标记 deprecated 注:UE4.22到4.27的版本中直接集成的独立库NvCloth |
| 4.x | 已移除,剥离为独立库 NvCloth | 需单独集成 |
| 5.x | PxFEMCloth / 粒子布料(GPU),后续更名为 deformable surface 系列 |
较新,API 演进中 |
整体流程
三角网格
↓ cook
Fabric(约束拓扑,可多个 Cloth 共享)
↓ + 粒子初始位置 & invMass
Cloth(一个布料实例)
↓ addCloth
Solver(每帧求解)
↓ 每帧
喂碰撞体 → simulate → 读回粒子 → 更新渲染网格
Fabric 和 Cloth 的关系
Fabric = 图纸,Cloth = 按图纸造出来的实体
Fabric(不含位置!) Cloth(实例)
───────────────── ─────────────
"顶点 3 和 4 之间有弹簧, "顶点 3 现在在 (1.2, 0.5, 0.3)"
静止长度 0.1" "顶点 4 现在在 (1.3, 0.5, 0.3)"
"这些约束可以并行" 碰撞球、重力、阻尼、刚度...
注意 Fabric 里没有任何坐标,只有"谁和谁相连"。这正是它能被共享的原因:
Fabric* capeFabric = Cook(capeMesh); // 烘焙一次
for (int i = 0; i < 100; ++i)
npc[i].cloth = createCloth(
startPositions[i], // 各自的位置状态
*capeFabric); // ← 100 个 NPC 共用这一份图纸
100 个 NPC 的披风,连接表只存一份
初始化(全局一次)
NvCloth 要求你先注册内存/错误回调,否则崩在第一行。
#include <NvCloth/Callbacks.h>
#include <NvCloth/Factory.h>
#include <NvCloth/Solver.h>
#include <NvCloth/Fabric.h>
#include <NvCloth/Cloth.h>
#include <NvClothExt/ClothFabricCooker.h>
// 简单实现四个回调
class MyAllocator : public physx::PxAllocatorCallback {
public:
void* allocate(size_t size, const char*, const char*, int) override {
return _aligned_malloc(size, 16); // NvCloth 要求 16 字节对齐
}
void deallocate(void* ptr) override { _aligned_free(ptr); }
};
class MyErrorCallback : public physx::PxErrorCallback {
public:
void reportError(physx::PxErrorCode::Enum, const char* msg,
const char* file, int line) override {
printf("[NvCloth] %s (%s:%d)\n", msg, file, line);
}
};
class MyAssert : public nv::cloth::PxAssertHandler {
public:
void operator()(const char* exp, const char* file, int line, bool& ignore) override {
printf("[NvCloth ASSERT] %s (%s:%d)\n", exp, file, line);
}
};
static MyAllocator gAlloc;
static MyErrorCallback gError;
static MyAssert gAssert;
void InitNvCloth()
{
nv::cloth::InitializeNvCloth(&gAlloc, &gError, &gAssert, nullptr);
}
创建 Factory(决定 CPU 还是 GPU 求解):
// CPU
nv::cloth::Factory* factory = NvClothCreateFactoryCPU();
// CUDA(需要先建 CUDA context)
// nv::cloth::Factory* factory = NvClothCreateFactoryCUDA(cudaContext);
// DX11
// nv::cloth::Factory* factory = NvClothCreateFactoryDX11(dxContextManager);
Step 1:烘焙 Fabric
Fabric的实质是:一张「哪些顶点之间用弹簧连着」的连接表
美术制作的是:顶点坐标 + 三角形索引 —— 一个用于渲染的网格
而PBD 布料求解器要的是:一堆距离约束,形式是 (粒子A, 粒子B, 静止长度)。它每帧做的事就是
对每条约束: 当前长度 = |位置A - 位置B| 如果 当前长度 ≠ 静止长度: 把 A 和 B 沿连线互相拉近/推远
烘焙Fabric就是完成从「三角形网格」到「弹簧网络」的翻译,把「一块布长什么样」翻译成「哪些点之间用弹簧连着、按什么顺序并行解算」
// 假设你有一块网格:positions (PxVec3), indices (uint32)
nv::cloth::ClothMeshDesc meshDesc;
meshDesc.points.data = positions.data();
meshDesc.points.stride = sizeof(physx::PxVec3);
meshDesc.points.count = (uint32_t)positions.size();
meshDesc.triangles.data = indices.data();
meshDesc.triangles.stride = sizeof(uint32_t) * 3;
meshDesc.triangles.count = (uint32_t)indices.size() / 3;
// invMasses: 0 = 完全钉死,>0 = 可动
meshDesc.invMasses.data = invMasses.data();
meshDesc.invMasses.stride = sizeof(float);
meshDesc.invMasses.count = (uint32_t)invMasses.size();
// 重力方向影响 tether / 约束生成方向
physx::PxVec3 gravity(0.0f, -9.81f, 0.0f);
nv::cloth::Vector<int32_t>::Type phaseTypeInfo; // cook 会填充每个 phase 的类型
nv::cloth::Fabric* fabric = NvClothCookFabricFromMesh(
factory,
meshDesc,
gravity,
&phaseTypeInfo,
/*useGeodesicTether*/ true);
phaseTypeInfo 很重要 —— 后面配置刚度要靠它区分哪些约束是 vertical / horizontal / bending / shearing
Tether(系绳):限制粒子离锚点的最大距离,防止布料被拉成面条。geodesic tether 沿网格表面测距,比 euclidean 更自然但 cook 慢
Step 2:创建 Cloth 实例
粒子是 PxVec4:xyz = 位置,w = invMass
std::vector<physx::PxVec4> particles(positions.size());
for (size_t i = 0; i < positions.size(); ++i)
{
float invMass = isPinned(i) ? 0.0f : 1.0f; // 顶边钉住
particles[i] = physx::PxVec4(positions[i], invMass);
}
nv::cloth::Cloth* cloth = factory->createCloth(
nv::cloth::Range<physx::PxVec4>(
particles.data(), particles.data() + particles.size()),
*fabric);
nv::cloth::Range<T>是 NvCloth 到处用的轻量切片类型,传[begin, end)两个指针。
基础参数:
cloth->setGravity(gravity);
cloth->setSolverFrequency(300.0f); // 内部子步频率,越高越稳越慢
cloth->setDamping(physx::PxVec3(0.1f)); // 各轴阻尼
cloth->setDragCoefficient(0.3f); // 空气拖拽
cloth->setLiftCoefficient(0.1f); // 升力(旗帜飘动感)
cloth->setFriction(0.5f); // 与碰撞体的摩擦
cloth->setStiffnessFrequency(10.0f);
cloth->setTetherConstraintScale(1.0f);
cloth->setTetherConstraintStiffness(1.0f);
cloth->setSelfCollisionDistance(0.05f); // >0 才开启自碰撞
cloth->setSelfCollisionStiffness(0.5f);
Step 3:PhaseConfig —— 控制布料手感的核心
这是最容易被忽略但最影响效果的一步。不配的话布料会软得像塑料袋
const uint32_t numPhases = fabric->getNumPhases();
std::vector<nv::cloth::PhaseConfig> phases(numPhases);
for (uint32_t i = 0; i < numPhases; ++i)
{
phases[i].mPhaseIndex = (uint16_t)i;
// 按 cook 返回的 phase 类型分别调参
switch (phaseTypeInfo[i])
{
case nv::cloth::ClothFabricPhaseType::eVERTICAL:
phases[i].mStiffness = 1.0f; // 竖向拉伸:硬
phases[i].mStiffnessMultiplier = 1.0f;
phases[i].mCompressionLimit = 1.0f;
phases[i].mStretchLimit = 1.0f;
break;
case nv::cloth::ClothFabricPhaseType::eHORIZONTAL:
phases[i].mStiffness = 1.0f; // 横向拉伸:硬
phases[i].mStiffnessMultiplier = 1.0f;
phases[i].mCompressionLimit = 1.0f;
phases[i].mStretchLimit = 1.0f;
break;
case nv::cloth::ClothFabricPhaseType::eBENDING:
phases[i].mStiffness = 0.5f; // 弯曲:软 → 布料柔顺
phases[i].mStiffnessMultiplier = 0.5f;
phases[i].mCompressionLimit = 0.5f;
phases[i].mStretchLimit = 1.0f;
break;
case nv::cloth::ClothFabricPhaseType::eSHEARING:
phases[i].mStiffness = 0.7f; // 剪切:中等
phases[i].mStiffnessMultiplier = 0.7f;
phases[i].mCompressionLimit = 1.0f;
phases[i].mStretchLimit = 1.0f;
break;
}
}
cloth->setPhaseConfig(nv::cloth::Range<const nv::cloth::PhaseConfig>(
phases.data(), phases.data() + phases.size()));
四种 phase 的物理含义
| Phase | 约束方向 | 调硬的效果 | 调软的效果 |
|---|---|---|---|
| Vertical | 沿 UV 竖向的边 | 不下垂 | 像橡皮筋一样拉长 |
| Horizontal | 沿 UV 横向的边 | 不横向变形 | 横向松垮 |
| Bending | 跨边的弯曲约束 | 硬纸板 / 皮革 | 丝绸 / 薄纱 |
| Shearing | 对角剪切 | 抗扭曲,保持网格方形 | 容易斜切变形 |
手感配方参考
丝绸/薄纱: V/H = 1.0, Bending = 0.1, Shearing = 0.3
棉布: V/H = 1.0, Bending = 0.4, Shearing = 0.6
皮革: V/H = 1.0, Bending = 0.8, Shearing = 0.9
金属链甲: V/H = 1.0, Bending = 0.95, Shearing = 1.0
Step 4:Solver(解算器)
nv::cloth::Solver* solver = factory->createSolver();
solver->addCloth(cloth);
一个 Solver 可以管很多 Cloth,批量求解效率更高
Step 5:每帧碰撞体 (重点)
接上文的关键点:NvCloth 完全不知道 PhysX 场景的存在。 你必须每帧手动把碰撞体喂进去
球(Sphere)
// PxVec4: xyz = 圆心,w = 半径
std::vector<physx::PxVec4> spheres;
spheres.push_back(physx::PxVec4(0, 1.0f, 0, 0.3f));
spheres.push_back(physx::PxVec4(0, 1.5f, 0, 0.25f));
cloth->setSpheres(
nv::cloth::Range<const physx::PxVec4>(
spheres.data(), spheres.data() + spheres.size()),
0, cloth->getNumSpheres()); // 替换掉旧的
胶囊(Capsule)
胶囊 = 两个球的索引对,不是独立结构:
// 索引 0 和 1 组成一个胶囊
std::vector<uint32_t> capsuleIndices = { 0, 1 };
cloth->setCapsules(
nv::cloth::Range<const uint32_t>(
capsuleIndices.data(),
capsuleIndices.data() + capsuleIndices.size()),
0, cloth->getNumCapsules());
这个设计意味着:做角色骨骼碰撞时,把每根骨头的两端各放一个球,再用索引连起来,球可复用。
平面 + 凸体(Convex)
// 平面:PxVec4(nx, ny, nz, d),满足 dot(n, p) + d = 0
std::vector<physx::PxVec4> planes = {
physx::PxVec4(0, 1, 0, 0) // 地面 y = 0
};
cloth->setPlanes(
nv::cloth::Range<const physx::PxVec4>(
planes.data(), planes.data() + planes.size()),
0, cloth->getNumPlanes());
// Convex = 平面的位掩码组合(最多 32 个平面)
std::vector<uint32_t> convexMasks = { 0x1 }; // 只用第 0 个平面
cloth->setConvexes(
nv::cloth::Range<const uint32_t>(
convexMasks.data(), convexMasks.data() + convexMasks.size()),
0, cloth->getNumConvexes());
三角形碰撞(精确但贵)
// 每 3 个 PxVec3 一个三角形
cloth->setTriangles(
nv::cloth::Range<const physx::PxVec3>(
tris.data(), tris.data() + tris.size()),
0, cloth->getNumTriangles());
性能提示:优先球/胶囊,三角形只用于必须精确的局部。角色布料几乎总是球+胶囊够用。
从 PhysX 场景同步碰撞体
如果你确实想让布料撞到 PhysX 刚体,得自己写桥接:
void SyncCollidersFromPhysX(nv::cloth::Cloth* cloth,
const std::vector<physx::PxRigidActor*>& actors)
{
std::vector<physx::PxVec4> spheres;
std::vector<uint32_t> capsuleIdx;
for (auto* actor : actors)
{
physx::PxShape* shapes[16];
uint32_t n = actor->getShapes(shapes, 16);
for (uint32_t i = 0; i < n; ++i)
{
physx::PxTransform pose =
physx::PxShapeExt::getGlobalPose(*shapes[i], *actor);
if (shapes[i]->getGeometryType() == physx::PxGeometryType::eSPHERE)
{
physx::PxSphereGeometry g;
shapes[i]->getSphereGeometry(g);
spheres.push_back(physx::PxVec4(pose.p, g.radius));
}
else if (shapes[i]->getGeometryType() == physx::PxGeometryType::eCAPSULE)
{
physx::PxCapsuleGeometry g;
shapes[i]->getCapsuleGeometry(g);
physx::PxVec3 axis = pose.q.getBasisVector0() * g.halfHeight;
uint32_t base = (uint32_t)spheres.size();
spheres.push_back(physx::PxVec4(pose.p - axis, g.radius));
spheres.push_back(physx::PxVec4(pose.p + axis, g.radius));
capsuleIdx.push_back(base);
capsuleIdx.push_back(base + 1);
}
// Box / ConvexMesh 需要转成 convex planes,较麻烦
}
}
cloth->setSpheres(nv::cloth::Range<const physx::PxVec4>(
spheres.data(), spheres.data() + spheres.size()),
0, cloth->getNumSpheres());
cloth->setCapsules(nv::cloth::Range<const uint32_t>(
capsuleIdx.data(), capsuleIdx.data() + capsuleIdx.size()),
0, cloth->getNumCapsules());
}
Step 6:驱动仿真
单线程实现
void Tick(float dt)
{
SyncCollidersFromPhysX(cloth, actors);
// 角色移动时更新根变换,产生惯性摆动
cloth->setTranslation(characterPos);
cloth->setRotation(characterRot);
// 想要"传送不甩动"时:cloth->clearInertia();
solver->beginSimulation(dt);
for (int i = 0; i < solver->getSimulationChunkCount(); ++i)
solver->simulateChunk(i);
solver->endSimulation();
}
多线程实现
simulateChunk(i) 之间互相独立,可以直接丢进线程池:
solver->beginSimulation(dt);
const int chunks = solver->getSimulationChunkCount();
parallel_for(0, chunks, [&](int i) {
solver->simulateChunk(i);
});
solver->endSimulation(); // 必须在所有 chunk 完成后调用
Step 7:读回粒子送渲染
nv::cloth::MappedRange<physx::PxVec4> current = cloth->getCurrentParticles();
for (uint32_t i = 0; i < current.size(); ++i)
{
renderVertices[i].position = physx::PxVec3(
current[i].x, current[i].y, current[i].z);
}
// MappedRange 析构时自动 unmap,注意作用域
UpdateVertexBuffer(renderVertices);
RecomputeNormals(renderVertices, indices); // 法线要自己重算
注意事项
- 粒子位置是世界空间(除非你自己管 local)
- 法线 NvCloth 不算,得自己从三角形重建
MappedRange在 GPU factory 下涉及回读,尽量一帧只 map 一次getPreviousParticles()可拿上一帧,用于算速度做运动模糊
运行时钉住 / 松开顶点
{
nv::cloth::MappedRange<physx::PxVec4> p = cloth->getCurrentParticles();
// 钉住:invMass = 0,同时写死位置
p[idx] = physx::PxVec4(attachPos, 0.0f);
// 松开:恢复 invMass
p[idx].w = 1.0f;
}
Motion Constraint 是更柔和的方案 —— 允许顶点在锚点周围一个球内活动:
nv::cloth::MappedRange<physx::PxVec4> mc = cloth->getMotionConstraints();
for (uint32_t i = 0; i < mc.size(); ++i)
mc[i] = physx::PxVec4(anchorPos[i], radius[i]); // xyz=中心, w=半径
cloth->setMotionConstraintScaleBias(1.0f, 0.0f);
这比硬钉住自然得多,做贴身衣物时常用。
在 UE4 里的位置
NvCloth 就是 UE4 Clothing Tool 的后端:
Engine/Source/ThirdParty/NvCloth/
Engine/Source/Runtime/ClothingSystemRuntimeNv/ ← 封装层
Engine/Source/Editor/ClothPainter/ ← 编辑器绘制工具
UE4 里 Cloth Paint 刷的 Max Distance 就是上面的 motion constraint 半径,Backstop 是额外的单侧约束。
UE5 已换成 Chaos Cloth(
ClothingSystemRuntimeInterface+ChaosCloth模块),求解器重写,但 Max Distance / Backstop / 刚度分组这套编辑器概念保留了。所以上面的调参直觉在 UE5 依然适用,只是底层 API 全变了。
浙公网安备 33010602011771号