Rigid Body Collision
Introduction
本节将介绍刚体碰撞的基础知识。
Shapes
Shape描述actor的空间范围(spatial extent)和碰撞属性(collision properties)。它们在PhysX 中用于三个目的:确定rigid objects的接触特征的相交性测试(intersection tests)、场景查询测试(scene query tests)(如raycasts)以及定义触发体积(defining trigger volumes)(当其他Shape与它们相交时生成通知)。
每个Shape都包含一个PxGeometry 对象和一个对 PxMaterial 的引用,这两个对象都必须在创建时指定。下面的代码创建一个具有球体几何图形和特定材质的Shape:
PxShape* shape = physics.createShape(PxSphereGeometry(1.0f), myMaterial, true);
myActor.attachShape(*shape);
shape->release();
方法 PxRigidActorExt::createExclusiveShape() 等效于上面的三行。
用于 createShape() 的参数 "true" 通知 SDK 该Shape不会与其他Actor共享。当有许多具有相同几何图形的 Actor 时,可以使用Shape共享来降低simulate的内存成本,但共享Shape具有非常严格的限制:在共享Shape附加到 Actor 时,您无法更新共享Shape的属性。
(可选)您可以通过指定类型为 PxShapeFlags 的Shape flags来配置Shape。默认情况下,Shape配置为:
simulate Shape(在simulate期间启用接触生成( contact generation ))- 场景查询
Shape(为场景查询启用) - 如果启用了调试渲染,则可视化
为Shape指定几何对象时,该几何对象将复制到该Shape中。对于可以为Shape指定几何图形有一些限制,具体取决于Shape flags和父角色的类型。
- 附加到
dynamic actors的simulate``Shape不支持``TriangleMesh、HeightField和Plane geometries,除非dynamic actors配置为kinematic`。 - 触发器
Shape不支持TriangleMesh和HeightField几何图形。
有关更多详细信息,请参阅以下部分。
如下所示将Shape与Actor分离:
myActor.detachShape(*shape);
Simulation Shapes and Scene Query Shapes
Shape可以独立配置为参与场景查询(scene queries)和接触测试(contact tests)中的一个或两个。默认情况下,Shape将同时参与这两项操作。以下伪代码配置 PxShape 实例,使其不再参与Shape对相交测试(shape pair intersection tests):
void disableShapeInContactTests(PxShape* shape)
{
shape->setFlag(PxShapeFlag::eSIMULATION_SHAPE,false);
}
可以将 PxShape 实例配置为参与Shape对相交测试(shape pair intersection tests),如下所示:
void enableShapeInContactTests(PxShape* shape)
{
shape->setFlag(PxShapeFlag::eSIMULATION_SHAPE,true);
}
要从场景查询测试中禁用PxShape 实例,请执行以下操作:
void disableShapeInSceneQueryTests(PxShape* shape)
{
shape->setFlag(PxShapeFlag::eSCENE_QUERY_SHAPE,false);
}
最后,可以在场景查询测试中重新启用 PxShape 实例:
void enableShapeInSceneQueryTests(PxShape* shape)
{
shape->setFlag(PxShapeFlag::eSCENE_QUERY_SHAPE,true);
}
注意:如果Shape的Actor的运动根本不需要由simulate控制,那么可以通过在Actor上额外禁用simulate来节省内存。例如Shape仅用于场景查询,并在必要时手动移动
Kinematic Triangle Meshes (Planes, Heighfields)
可以创建一个kinematic PxRigidDynamic,它可以具有三角形网格(plane,heighfield)Shape。如果此Shape具有simulate Shape标志,则此Actor必须保持kinematic。如果将标志更改为"not simulated",你甚至可以切换kinematic flag。要设置kinematic triangle mesh,请参阅以下代码:
PxRigidDynamic* meshActor = getPhysics().createRigidDynamic(PxTransform(1.0f));
PxShape* meshShape;
if(meshActor)
{
meshActor->setRigidDynamicFlag(PxRigidDynamicFlag::eKINEMATIC, true);
PxTriangleMeshGeometry triGeom;
triGeom.triangleMesh = triangleMesh;
meshShape = PxRigidActorExt::createExclusiveShape(*meshActor,triGeom,
defaultMaterial);
getScene().addActor(*meshActor);
}
要将kinematic triangle mesh切换为dynamic actor:
PxRigidDynamic* meshActor = getPhysics().createRigidDynamic(PxTransform(1.0f));
PxShape* meshShape;
if(meshActor)
{
meshActor->setRigidDynamicFlag(PxRigidDynamicFlag::eKINEMATIC, true);
PxTriangleMeshGeometry triGeom;
triGeom.triangleMesh = triangleMesh;
meshShape = PxRigidActorExt::createExclusiveShape(*meshActor, triGeom,
defaultMaterial);
getScene().addActor(*meshActor);
PxConvexMeshGeometry convexGeom = PxConvexMeshGeometry(convexBox);
convexShape = PxRigidActorExt::createExclusiveShape(*meshActor, convexGeom,
defaultMaterial);
Broad-phase Algorithms
PhysX 支持多种粗检测算法(broad-phase algorithms):
- sweep-and-prune (SAP)
- multi box pruning (MBP)
PxBroadPhaseTyp::eSAP 是 PhysX 3.2 之前使用的默认算法。这是一个很好的通用选择,当许多物体处于睡眠状态时,它具有出色的性能。但是,当所有对象都在移动时,或者当在粗检测(broad-phase)中添加或删除大量对象时,性能可能会显著下降。此算法不需要定义世界边界(world bounds)即可工作。PxBroadPhaseType::eMBP 是 PhysX 3.3 中引入的一种新算法。它是一种替代的粗检测(broad-phase)算法,当所有对象都在移动或插入大量对象时,它不会遇到与eSAP相同的性能问题。但是,当许多对象处于休眠状态时,其通用性能可能不如 eSAP,并且它要求用户定义世界边界(world bounds)才能工作。所需的粗检测(broad-phase)算法由PxSceneDesc结构中的PxBroadPhaseType枚举控制。
Regions of Interest
感兴趣的区域(Regions of Interest)是围绕粗检测(broad-phase)控制的体积空间中的世界空间AABB包围盒。这些区域中包含的对象由粗检测(broad-phase)正确处理。落在这些区域之外的对象将丢失所有碰撞检测。理想情况下,这些区域应覆盖整个游戏空间,同时限制覆盖的空白空间的数量。区域可以重叠,但为了获得最大效率,建议尽可能减少区域之间的重叠量。请注意,AABB 仅接触的两个区域不被视为重叠。例如,PxBroadPhaseExt::createRegionsFromWorldBounds helper function通过将给定的世界 AABB 细分为常规 2D 网格来创建许多非重叠区域边界。区域可以由PxBroadPhaseRegion 结构定义,也可以由分配给它们的用户数据定义。它们可以在场景创建时或在运行时使用 PxScene::addBroadPhaseRegion 函数进行定义。SDK 返回分配给新创建区域的句柄,稍后可以使用 PxScene::removeBroadPhaseRegion 函数删除区域。新添加的区域可能会重叠已存在的对象。如果设置了 PxScene::addBroadPhaseRegion 调用中的 populateRegion 参数,SDK 可以自动将这些对象添加到新区域。但是,此操作not cheap,并且可能会对性能产生很大影响,尤其是在同一帧中添加多个区域时。因此,建议尽可能禁用它。该区域将被创建为空,并且只会在创建区域后添加到场景中的对象填充,或者在updated时(即当它们移动时)使用先前存在的对象填充该区域。请注意,只有PxBroadPhaseType::eMBP需要定义区域。PxBroadPhaseType::eSAP 算法则不然。此信息在PxBroadPhaseCaps结构中捕获,该结构列出了每个粗检测(broad-phase)算法的信息和功能。此结构可以通过 PxScene::getBroadPhaseCaps 函数检索。有关当前区域的运行时信息可以使用 PxScene::getNbBroadPhaseRegions 和 PxScene::getBroadPhaseRegions 函数进行检索。区域的最大数量目前限制为 256 个。
Broad-phase Callback
可以在 PxSceneDesc 结构体中定义与粗检测相关(broad-phase-related)的事件的回调。当在超出指定的感兴趣区域(即"out of bounds")中发现对象时,将调用此PxBroadPhaseCallback对象。SDK 会禁用这些对象的碰撞检测。一旦对象重新进入有效区域,它就会自动重新启用。由用户决定如何处理越界对象。典型选项包括:
- 删除对象
- 让他们继续运动而不会发生碰撞,直到他们重新进入有效区域
- 手动将他们传送回有效位置
Collision Filtering
在几乎所有应用程序中,除了"trivial"的应用程序之外,都需要免除某些对象对(pairs of objects)的相交性(interacting),或者以特定方式为交互对(interacting pair)配置 SDK 碰撞检测行为。在submarine sample中,如上所述,当潜艇接触水雷或水雷链条时,我们需要得到通知,以便我们可以将它们炸毁。crab中的AI还需要知道crab何时接触到高度场。在了解示例如何实现此目的之前,我们需要了解SDK filtering系统。由于过率潜在的交互对(interacting pair)发生在仿真引擎的最深处,并且需要应用于彼此靠近的所有对象对,因此它对性能特别敏感。实现它的最简单方法是始终为每个可能交互的对(interacting pair)调用回调函数,其中应用程序基于两个对象指针可以使用一些自定义逻辑(例如咨询其游戏数据库)确定该对是否应该进行交互。不幸的是,如果对于一个非常大的游戏世界来说,这会变得太慢,特别是如果碰撞检测处理发生在远程处理器(如GPU)或具有本地内存的其他类型的矢量处理器上,这些处理器必须暂停其并行计算,中断运行游戏代码的主处理器,并让它在继续之前执行回调。即使它要在CPU上执行,它也可能会在多个内核或超线程上同时执行,并且必须放置线程安全代码以确保对共享数据的并发访问是安全的。更好的做法是使用某种可以在远程处理器上执行的固定函数逻辑。这就是我们在 PhysX 2.x 中所做的 - 不幸的是,我们提供的基于组的简单过滤规则(simple group based filtering rules)不够灵活,无法涵盖所有应用程序。在3.0中,我们引入了一个着色器系统(shader system),它允许开发人员使用在矢量处理器上运行的代码实现任意规则系统(因此无法访问主内存中的任何最终游戏数据库(eventual game data base )),这比2.x固定函数过滤更灵活,但同样高效,还有一个完全灵活的回调机制,其中过滤器着色器调用能够访问任何应用程序数据的CPU回调函数, 以性能为代价 -- 有关详细信息,请参阅 PxSimulationFilterCallback。最好的部分是,应用程序可以基于每对(per-pair)进行决策,以进行速度与灵活性的权衡。
让我们先看一下着色器系统:下面是由 SampleSubmarine 实现的filter shader:
PxFilterFlags SampleSubmarineFilterShader(
PxFilterObjectAttributes attributes0, PxFilterData filterData0,
PxFilterObjectAttributes attributes1, PxFilterData filterData1,
PxPairFlags& pairFlags, const void* constantBlock, PxU32 constantBlockSize)
{
// let triggers through
if(PxFilterObjectIsTrigger(attributes0) || PxFilterObjectIsTrigger(attributes1))
{
pairFlags = PxPairFlag::eTRIGGER_DEFAULT;
return PxFilterFlag::eDEFAULT;
}
// generate contacts for all that were not filtered above
pairFlags = PxPairFlag::eCONTACT_DEFAULT;
// trigger the contact callback for pairs (A,B) where
// the filtermask of A contains the ID of B and vice versa.
if((filterData0.word0 & filterData1.word1) && (filterData1.word0 & filterData0.word1))
pairFlags |= PxPairFlag::eNOTIFY_TOUCH_FOUND;
return PxFilterFlag::eDEFAULT;
}
SampleSubmarineFilterShader是一个简单的着色器函数,它是PxFiltering.h中声明的PxSimulationFilterShader原型的实现。shader filter function(如上称为 SampleSubmarineFilterShader)可能不引用函数的参数及其自己的局部堆栈变量以外的任何内存,因为该函数可以在远程处理器上编译和执行。SampleSubmarineFilterShader() 将用于所有彼此靠近的Shape对(pairs of shapes) - 更准确地说:对于在世界空间中发现AABB首次相交的所有Shape对。除此之外的所有行为都由 SampleSubmarineFilterShader() 返回的内容决定。SampleSubmarineFilterShader() 的参数包括两个对象的 PxFilterObjectAttributes 和 PxFilterData,以及一个常量内存块。请注意,指向这两个对象的指针不会传递,因为这些指针引用计算机的主内存,并且正如我们所说,这可能不适用于着色器,因此指针不是很有用,因为取消引用它们可能会导致崩溃。PxFilterObjectAttributes和PxFilterData旨在包含可以从指针中快速收集的所有有用信息。PxFilterObjectAttributes是32位数据,用于编码对象的类型:例如PxFilterObjectType::eRIGID_STATIC,PxFilterObjectType::eRIGID_DYNAMIC,甚至PxFilterObjectType::ePARTICLE_SYSTEM。此外,它还可以让您了解对象是kinematic的还是触发器。PhysX中的每个PxShape和PxParticleBase对象都有一个类型为PxFilterData的成员变量。这是 128 位用户定义的数据,可用于存储与碰撞过滤(collision filtering)相关的应用程序特定信息。这是传递给每个对象的SampleSubmarineFilterShader() 的另一个变量。对于常量块。这是应用程序可以提供给着色器进行操作的每个场景的全局信息块。您将需要使用它来编码有关要过滤的内容和不过滤的内容的规则。最后,SampleSubmarineFilterShader() 还有一个 PxPairFlags 参数。这是一个输出,类似于返回值 PxFilterFlags,尽管用法略有不同。PxFilterFlags 告诉 SDK 它是否应该永久忽略该对 (eKILL),在重叠时忽略该对,但在过滤其中一个对象的相关数据更改时再次询问 (eSUPPRESS),或者在着色器无法决定时调用性能较低但更灵活的 CPU 回调(eCALLBACK)。PxPairFlags 指定了其他标志,这些标志代表simulate将来应针对此对执行的操作。例如,eNOTIFY_TOUCH_FOUND意味着当配对真正开始接触时通知用户,而不仅仅是潜在的。
让我们看看上面的着色器是做什么的:
// let triggers through
if(PxFilterObjectIsTrigger(attributes0) || PxFilterObjectIsTrigger(attributes1))
{
pairFlags = PxPairFlag::eTRIGGER_DEFAULT;
return PxFilterFlag::eDEFAULT;
}
这意味着,如果任一对象是触发器,则执行默认触发器行为(通知应用程序有关触摸的开始和结束),否则在它们之间执行"默认"碰撞检测。
// generate contacts for all that were not filtered above
pairFlags = PxPairFlag::eCONTACT_DEFAULT;
// trigger the contact callback for pairs (A,B) where
// the filtermask of A contains the ID of B and vice versa.
if((filterData0.word0 & filterData1.word1) && (filterData1.word0 & filterData0.word1))
pairFlags |= PxPairFlag::eNOTIFY_TOUCH_FOUND;
return PxFilterFlag::eDEFAULT;
这表示对于所有其他对象,请执行"默认"碰撞处理。此外,还有一个基于 filterDatas 的规则,用于确定我们要求触摸通知(touch notifications)的特定对。要理解这意味着什么,我们需要知道样本赋予 filterDatas 的特殊含义。
示例的需求非常基础,因此我们将使用非常简单的方案来处理它。该示例首先使用自定义枚举为不同的对象类型提供命名代码:
struct FilterGroup
{
enum Enum
{
eSUBMARINE = (1 << 0),
eMINE_HEAD = (1 << 1),
eMINE_LINK = (1 << 2),
eCRAB = (1 << 3),
eHEIGHTFIELD = (1 << 4),
};
};
该示例通过将每个Shape的 PxFilterData::word0 分配给此筛选器组类型来标识每个Shape的类型。然后,它放置一个位掩码,指定每种类型的对象,当被 word0 类型的对象触摸到 word1 中时,这些对象应生成报告。每当创建Shape时,都可以在示例中执行此操作,但由于Shape创建封装在 SampleBase 中,因此在事后使用以下函数完成:
void setupFiltering(PxRigidActor* actor, PxU32 filterGroup, PxU32 filterMask)
{
PxFilterData filterData;
filterData.word0 = filterGroup; // word0 = own ID
filterData.word1 = filterMask; // word1 = ID mask to filter pairs that trigger a
// contact callback;
const PxU32 numShapes = actor->getNbShapes();
PxShape** shapes = (PxShape**)SAMPLE_ALLOC(sizeof(PxShape*)*numShapes);
actor->getShapes(shapes, numShapes);
for(PxU32 i = 0; i < numShapes; i++)
{
PxShape* shape = shapes[i];
shape->setSimulationFilterData(filterData);
}
SAMPLE_FREE(shapes);
}
这将设置属于传递的 actor 的每个Shape的 PxFilterDatas。以下是如何在 SampleSubmarine 中使用它的一些示例:
setupFiltering(mSubmarineActor, FilterGroup::eSUBMARINE, FilterGroup::eMINE_HEAD |
FilterGroup::eMINE_LINK);
setupFiltering(link, FilterGroup::eMINE_LINK, FilterGroup::eSUBMARINE);
setupFiltering(mineHead, FilterGroup::eMINE_HEAD, FilterGroup::eSUBMARINE);
setupFiltering(heightField, FilterGroup::eHEIGHTFIELD, FilterGroup::eCRAB);
setupFiltering(mCrabBody, FilterGroup::eCRAB, FilterGroup::eHEIGHTFIELD);
这个方案可能过于简单,无法在实际游戏中使用,但它显示了filter shader的基本用法,并且它将确保为所有有用的对(interesting pairs)调用SampleSubmarine::onContact()。在扩展函数 PxDefaultSimulationFilterShader 中提供了另一种基于组的过滤机制和源。而且,同样,如果这个基于着色器的系统太不灵活,请考虑使用PxSimulationFilterCallback提供的回调方法。
Aggregates
聚合(Aggregates)是actor的集合。聚合不提供额外的simulate或查询功能,但允许您告诉 SDK 一组actor将聚类在一起,这反过来又允许 SDK 优化其空间数据操作。一个典型的用例是布娃娃,由多个不同的actor组成。如果没有聚集体,这会产生与布娃娃中的Shape一样多的粗检测实体(broad-phase entries)。通常,将布娃娃在粗检测阶段表示为单个实体,并在必要时在第二个pass中执行内部重叠测试(internal overlap tests)会更有效。另一个潜在的用例是具有大量附加Shape的单个Actor。
Creating an Aggregate
从 PxPhysics 对象创建聚合:
PxPhysics* physics; // The physics SDK object
PxU32 nbActors; // Max number of actors expected in the aggregate
bool selfCollisions = true;
PxAggregate* aggregate = physics->createAggregate(nbActors, selfCollisions);
目前,actor的最大数量限制为128个,为了提高效率,数量应尽可能少。如果永远不需要聚合的actor之间的碰撞检测,请在创建时禁用它们。这比使用场景过滤机制(scene filtering mechanism)更有效,因为它绕过了所有内部过滤逻辑。典型的用例是static或kinematic actor的聚合。请注意,最大actor个数和自碰撞属性(self-collision attribute)都是不可变的。
Populating an Aggregate
将actor添加到聚合中,如下所示:
PxActor& actor; // Some actor, previously created
aggregate->addActor(actor);
请注意,如果 Actor 已属于某个场景,则调用将被忽略。将 Actor 添加到聚合,然后将聚合添加到场景中,或者将聚合添加到场景中,然后将 Actor 添加到聚合中。
要将聚合添加到场景中(在填充之前或之后):
scene->addAggregate(*aggregate);
同样,要从场景中移除聚合,请执行以下操作:
scene->removeAggregate(*aggregate);
Releasing an Aggregate
要释放聚合:
PxAggregate* aggregate; // The aggregate we previously created
aggregate->release();
释放 PxAggregate 不会释放聚合的actors。如果 PxAggregate 属于某个场景,则 Actor 会自动重新插入到该场景中。如果您打算同时删除 PxAggregate 及其actors,则最有效的方法是先释放actor,然后在 PxAggregate 为空时释放它。
Amortizing Insertion
在一帧中向场景中添加多个对象可能是一项代价高昂的操作。布娃娃就是这种情况,如前所述,这是 PxAggregate 的良好候选者。另一种情况是局部碎片,其自碰撞(self-collisions)经常被禁用。要将对象插入粗检测结构(broad-phase structure)的成本摊销到多个阶段结构中,请在 PxAggregate 中生成碎片,然后从聚合中删除每个 Actor ,然后通过这些帧将其重新插入到场景中。
Trigger Shapes
Trigger Shapes在场景simulate中不起作用(尽管它们可以配置为参与场景查询)。相反,它们的作用是报告与另一种shape重叠。不会为相交性测试生成触点,因此触点报告不适用于Trigger Shapes。此外,由于触发器在simulate中不起作用,SDK 将不允许同时引发eSIMULATION_SHAPE和 eTRIGGER_SHAPE标志;也就是说,如果引发一个标志,则引发另一个标志的尝试将被拒绝,并且错误将传递到错误流。
SampleSubmarine中使用了Trigger Shapes来确定潜艇是否已经到达宝藏。在下面的代码中,表示宝藏的 PxActor 将其单独Shape配置为Trigger Shapes:
PxShape* treasureShape;
gTreasureActor->getShapes(&treasureShape, 1);
treasureShape->setFlag(PxShapeFlag::eSIMULATION_SHAPE, false);
treasureShape->setFlag(PxShapeFlag::eTRIGGER_SHAPE, true);
与Trigger Shapes的相交性测试通过 PxSampleSubmarine 类(PxSimulationEventCallback 的子类)中的 PxSimulationEventCallback::onTrigger 的实现在 SampleSubmarine 中报告:
void SampleSubmarine::onTrigger(PxTriggerPair* pairs, PxU32 count)
{
for(PxU32 i=0; i < count; i++)
{
// ignore pairs when shapes have been deleted
if (pairs[i].flags & (PxTriggerPairFlag::eREMOVED_SHAPE_TRIGGER |
PxTriggerPairFlag::eREMOVED_SHAPE_OTHER))
continue;
if ((&pairs[i].otherShape->getActor() == mSubmarineActor) &&
(&pairs[i].triggerShape->getActor() == gTreasureActor))
{
gTreasureFound = true;
}
}
}
上面的代码循环访问涉及Trigger Shapes的所有重叠Shape对(all pairs of overlapping shapes)。如果发现宝藏已被潜艇触摸,则旗帜gTreasureFound为真。
Interactions
SDK 在内部为粗检测(broad-phase)报告的每个重叠对创建一个交互对象。这些对象不仅为成对碰撞的刚体创建,而且还为重叠的触发器对创建。一般来说,用户应该假设无论涉及对象的类型(刚体,触发器,布料等)以及所涉及的PxFilterFlag标志如何,都创建了此类对象。目前,每个Actor的此类交互对象限制为 65535 个。如果超过 65535 个交互涉及同一个Actor,则 SDK 会输出一条错误消息,并忽略额外的交互。
浙公网安备 33010602011771号