可可西

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 StaticKinematicDynamicCloth
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_TREEeSTATIC_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 全变了。

 

posted on 2026-09-07 20:46  可可西  阅读(4)  评论(0)    收藏  举报

导航