PhysX中raycast和sweep对block和touch的处理逻辑
零、说明
测试代码基于PhysX_3.4
一、raycast和sweep的特殊性
在场景查询中,raycast/sweep相对于overlap来说有一个重要的特性,就是前两者是有明确方向性的,也就是有一个起点加上一个终点。这个和overlap完全不同,因为overlap是在一个范围内的无差别覆盖。这一点会引导出查询时的一个很重要的概念:touching和blocking。
在PhysX的使用手册中,说明了两者的区别
https://docs.nvidia.com/gameworks/content/gameworkslibrary/physx/guide/Manual/SceneQueries.html#touching-and-blocking-hits
For queries with multiple results we distinguish between touching and blocking hits. The choice of whether a hit is touching or blocking is made by the user-implemented filtering logic. Intuitively a blocking hit prevents further progress of a raycast or a sweep along its path, and a touching hit is recorded but allows the ray or sweep to continue. So a multiple-hit query will return the closest blocking hit if one exists, together with any touching hits that are closer. If there are no blocking hits, all touching hits will be returned.
在查询的时候,{pre/post}Filter函数将会通过返回值决定此次碰撞时一个touching还是一个blocking。blocking将会阻止在该路径上的进一步查询,而touching则不会。典型的例子是子弹穿过树叶、玻璃、帷幕等,最后被墙体阻挡。在这个过程中,遇到墙体前的树叶等都是touching,而墙体则是blocking。这个不难理解。
问题在于:
对于返回的touching和blocking,它们是不是按照碰撞点离起始点的远近位置顺序排列的?先说结论:不是的。
但是背后的原因却和查询时候使用的查询树有关系。
二、查询树
在PhysX中,查询树通过AABB树构建,这是一种基于AABB包围盒构成的查询树。可以看到,这种架构下,并不存在一个所谓的“距离有序”的概念。更为糟糕的是,在PhysX 3.4版本中,对于数量小于NB_OBJECTS_PER_NODE(4)个的时候,这些图形在叶子节点内部的便利顺序是和添加顺序相同的
这里举一个简单的例子,就是一个倾斜的挡板挡在一个物体之前的场景:
在这种场景中,如果从y坐标(垂直于水平面的海拔方向上)负方向进行射线检测,那么此时给出的touching顺序就是和添加顺序相同。下图是一个例子,在大的挡板后面(相对于raycast方向)添加两个挡板,AABB查询树遍历的时候按照添加顺序遍历,有兴趣的同学可以验证下查询结果。
三、如何找到最近blocking
这个没有办法,其实需要找到所有的满足AABB包围盒的图形进行判断,即使找到一个blocking,还是需要继续处理,因为可能有更近的blocking。
四、如何保证所有touching都比blocking小
这一点其实和第三点有关:因为blocking点可能会变小,那么随着blocking的变小,那么之前小于blocking的touching可能就不再满足,所以需要进行再次过滤。
这个过滤发生在两个位置:
1、当找到的touching数量超过用户提供的最大容量,此时需要尝试进行一次修剪(purge):在MultiQueryCallback::invoke函数中mHitCall.nbTouches == mHitCall.maxNbTouches分支内部。
2、当所有AABB都遍历完毕,在返回给用户之前进行一次修剪:在IssueCallbacksOnReturn::~IssueCallbacksOnReturn()函数内部。
两处都是调用了clipHitsToNewMaxDist函数,从这个函数的实现就可以看到,它并没有保证查询到的touching的顺序,而只是把可以删除的元素和最后一个元素互换。
五、上图部分测试代码
inline PxQuat EulerAngleToQuat(const PxVec3 &rot)
{
PxQuat qx(physx::shdfnd::degToRad(rot.x), PxVec3(1.0f, 0.0f, 0.0f));
PxQuat qy(physx::shdfnd::degToRad(rot.y), PxVec3(0.0f, 1.0f, 0.0f));
PxQuat qz(physx::shdfnd::degToRad(rot.z), PxVec3(0.0f, 0.0f, 1.0f));
return qz * qy * qx;
}
enum
{
TOUCH_SHAPE_1_FLAG = 0x1,
TOUCH_SHAPE_2_FLAG = 0x2,
BLOCK_SHAPE_FLAG = 0x4,
};
struct MyFilter:public PxQueryFilterCallback
{
virtual PxQueryHitType::Enum preFilter(
const PxFilterData& filterData, const PxShape* shape, const PxRigidActor* actor, PxHitFlags& queryFlags)
{
(void)shape; (void)actor; (void)queryFlags; (void)filterData;
PxU32 stWord0 = shape->getQueryFilterData().word0;
if (stWord0 & (TOUCH_SHAPE_1_FLAG | TOUCH_SHAPE_2_FLAG))
{
return PxQueryHitType::eTOUCH;
}
if (stWord0 & BLOCK_SHAPE_FLAG)
{
return PxQueryHitType::eBLOCK;
}
return PxQueryHitType::eNONE;
}
virtual PxQueryHitType::Enum postFilter(const PxFilterData& filterData, const PxQueryHit& hit)
{
(void)filterData; (void)hit;
return PxQueryHitType::eNONE;
}
};
void initPhysics(bool interactive)
{
(void)interactive;
gFoundation = PxCreateFoundation(PX_FOUNDATION_VERSION, gAllocator, gErrorCallback);
gPvd = PxCreatePvd(*gFoundation);
PxPvdTransport* transport = PxDefaultPvdSocketTransportCreate(PVD_HOST, 5425, 10);
gPvd->connect(*transport, PxPvdInstrumentationFlag::eALL);
gPhysics = PxCreatePhysics(PX_PHYSICS_VERSION, *gFoundation, PxTolerancesScale(), true, gPvd);
PxSceneDesc sceneDesc(gPhysics->getTolerancesScale());
sceneDesc.gravity = PxVec3(0.0f, 0.0f, 0.0f);
gDispatcher = PxDefaultCpuDispatcherCreate(0);
sceneDesc.flags |= PxSceneFlag::eENABLE_CCD;
sceneDesc.cpuDispatcher = gDispatcher;
sceneDesc.filterShader = contactReportFilterShader;
gScene = gPhysics->createScene(sceneDesc);
gScene->setSimulationEventCallback(&gstEvetnCallback);
PxPvdSceneClient* pvdClient = gScene->getScenePvdClient();
if (pvdClient)
{
pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONSTRAINTS, true);
pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_CONTACTS, true);
pvdClient->setScenePvdFlag(PxPvdSceneFlag::eTRANSMIT_SCENEQUERIES, true);
}
gMaterial = gPhysics->createMaterial(0, 0, 0);
PxRigidStatic* pstStaticBox;
PxShape* pxShape = nullptr;
PxFilterData stFilter;
PxBoxGeometry stBox2(10, 1, 10);
pstStaticBox = PxCreateStatic(*gPhysics, PxTransform(PxVec3(0, 100, 0)),
stBox2, *gMaterial);
gScene->addActor(*pstStaticBox);
stFilter.word0 = BLOCK_SHAPE_FLAG;
pstStaticBox->getShapes(&pxShape, 1, 0);
pstStaticBox->userData = (void*)3;
pxShape->setQueryFilterData(stFilter);
PxBoxGeometry stBox1(5, 1, 5);
pstStaticBox = PxCreateStatic(*gPhysics, PxTransform(PxVec3(0, 10, 0)),
stBox1, *gMaterial);
gScene->addActor(*pstStaticBox);
stFilter.word0 = TOUCH_SHAPE_1_FLAG;
pstStaticBox->getShapes(&pxShape, 1, 0);
pstStaticBox->userData = (void*)2;
pxShape->setQueryFilterData(stFilter);
PxBoxGeometry stBox(200, 1, 200);
PxQuat stQuat = EulerAngleToQuat(PxVec3(0, 0 , 45));
pstStaticBox = PxCreateStatic(*gPhysics, PxTransform(PxVec3(100, 100, 100), stQuat),
stBox, *gMaterial);
gScene->addActor(*pstStaticBox);
pstStaticBox->getShapes(&pxShape, 1, 0);
pstStaticBox->userData = (void*)1;
stFilter.word0 = BLOCK_SHAPE_FLAG;
pxShape->setQueryFilterData(stFilter);
PxRaycastBufferN<10> stBuff;
PxQueryFilterData stFilterData;
stFilterData.flags = PxQueryFlag::ePREFILTER | PxQueryFlag::eDYNAMIC | PxQueryFlag::eSTATIC;
MyFilter stFilterCallback;
gScene->raycast(PxVec3(0, -10, 0), PxVec3(0, 1, 0), 10000, stBuff, PxHitFlags(PxHitFlag::eDEFAULT), stFilterData, &stFilterCallback);
}