〖Array王锐大神力作〗osg与PhysX结合系列内容——地形碰撞体
“烘焙”物理碰撞体
在上一篇文章中,我们介绍了多种不同的PxGeometry,它们被用来描述物理碰撞体的具体形状。简单的几何体类型不需要再做赘述;本节内容我们将着重讨论一下HeightField和TriangleMesh这两种几何体类型,基于它们分别构建三维地形的物理碰撞体,并且运行一些简单的刚体运动测试。
这两种几何体类型,再加上常用于运动对象构建的“凸包”类型(ConvexMesh),都是需要预先烘焙(cook)之后才可以使用的。这一点从对应的PxGeometry类的构造函数声明就可以看出:
- PxHeightFieldGeometry(PxHeightField* hf, …);
- PxTriangleMeshGeometry(PxTriangleMesh* mesh, …);
- PxConvexMeshGeometry(PxConvexMesh* mesh, …);
对应的hf或者mesh参数都需要通过PxCooking烘焙对象生成,并且需要提前准备一个名称后缀为Desc的类,来定义被烘焙几何体的原始信息。
相比之下,其它碰撞体的构建过程就简单许多了:
- PxBoxGeometry(float hx, float hy, float hz); // 设置立方体三条边的长度值
- PxSphereGeometry(float ir); // 设置半径值
HeightField与TriangleMesh
PxCooking烘焙对象是全局存在的,一个程序只需要一个就可以了。它的创建过程为:
physx::PxCookingParams defParams(physx::PxTolerancesScale());
_cooking = PxCreateCooking(PX_PHYSICS_VERSION, _foundation, defParams);
这里的_foundation也就是我们一开始就创建了的PxFoundation对象。除此之外还有用于创建各种物理对象的PxPhysics对象_sdk。这三者都应当直接全局记录下来,并随时使用。在osgPhysX中它们被共同安置在osgPhysics::Engine类中。
PxHeightField的创建需要调用_cooking->createHeightField()来完成,其中需要提供一个PxHeightFieldDesc作为输入参数。定义PxHeightFieldDesc并填充数据的代码片段为:
PxHeightFieldDesc heightFieldDesc;
heightFieldDesc.nbColumns = numColumns; // 设置地形网格的列数
heightFieldDesc.nbRows = numRows; // 设置地形网格的行数
heightFieldDesc.format = PxHeightFieldFormat::eS16_TM; // 地形数据格式,只可选S16
heightFieldDesc.thickness = thickness; // 设置地面的厚度,它会影响到求交判断的结果
void* sData = (void*)malloc(sizeof(PxHeightFieldSample) * numColumns * numRows);
heightFieldDesc.samples.data = sData; // 保存具体的地形网格数据内容
heightFieldDesc.samples.stride = sizeof(PxHeightFieldSample);
……
PxU8* ptr = (PxU8*)heightFieldDesc.samples.data;
{ // 此处开始遍历所有的地形网格数据,并进行赋值; // 每个采样点可以被认为是一块四边形区域,它被分成2个三角形,并分别设置对应的地形材质
PxHeightFieldSample* sample = (PxHeightFieldSample*)ptr;
sample->height = <Int16类型的高度值>;
sample->materialIndex0 = <三角形区域0的材质ID>;
sample->materialIndex1 = <三角形区域1的材质ID>;
ptr += heightFieldDesc.samples.stride;
}
这个流程虽然比较繁琐,但是其中的数据定义与我们通常定义高度图/DEM数据的方式并没有很大区别,因此不难理解。完成PxHeightFieldDesc对象的录入之后,需要用下面的代码将它烘焙为一个PxHeightField对象,并及时释放不必要的内存数据:
PxHeightField* heightField = _cook->createHeightField(
heightFieldDesc, _sdk->getPhysicsInsertionCallback());
free(sData); // 非常重要!Desc对象在烘焙完成后已经没用了,因此要释放掉其中额外分配的内存
createHeightField()中还有一个额外的PxPhysicsInsertionCallback参数,现阶段我们还不需要用到它,直接使用默认回调即可。以后如果有机会尝试做运行时的实时地形创建的话,也许会自定义这个参数。
同理,PxTriangleMesh的创建需要调用_cooking->createTriangleMesh()来完成,其中需要提供一个PxTriangleMeshDesc作为输入参数。定义PxTriangleMeshDesc并填充数据的代码片段为:
std::vector verts; // 预设的三角网格顶点数组
std::vector indices; // 预设的三角网格索引数组
……
PxTriangleMeshDesc meshDesc;
meshDesc.points.count = verts.size(); // 顶点的总数
meshDesc.points.stride = sizeof(PxVec3);
meshDesc.points.data = &(verts[0]); // 顶点数组的起始指针
meshDesc.triangles.count = indices.size() / 3; // 三角形的总数(不是索引值的总数)
meshDesc.triangles.stride = 3 * sizeof(PxU32);
meshDesc.triangles.data = &(indices[0]); // 索引数组的起始指针
PxTriangleMesh* mesh= _cook->createTriangleMesh(
meshDesc, _sdk->getPhysicsInsertionCallback());
注意,如果是创建ConvexMesh,那么整个过程几乎和上面的代码相同,主要的区别就是不需要再设置triangles(不需要考虑索引),以及TriangleMesh替换成ConvexMesh而已。但是如此烘焙得到的结果将是凸包类型的碰撞体几何形状。
此外,PxTriangleMeshDesc还有一个materialIndices属性,可以设置每个三角面的具体材质ID。
物理材质的概念与使用
在之前的章节中,我们一直在使用默认的物理材质。不过对于实际应用来说,让不同的物体或者地面区域具有不同的物理属性,这无疑可以实现更为准确的物理效果和更为丰富的表现力。PhysX中使用PxMaterial类来表示物理材质。用户需要使用_sdk->createMaterial()来创建新的材质;然后在必要的时候,通过PxMaterial::release()来释放之前分配的材质。
定义和创建物理材质的基本过程如下:
float staticFriction = 0.5f, dynamicFriction = 0.5f, restitution = 0.5f;
PxMaterial* mtl = _sdk->createMaterial(staticFriction, dynamicFriction, restitution);
这里的staticFriction,dynamicFriction,restitution分别表示静摩擦系数,动摩擦系数和恢复系数。下面的简单表格演示了一些常见的理想物理材质对应的参数设置:
在创建HeightField和TriangleMesh的时候,我们可以详细设置逐三角面的材质ID,从而精确地定义地面或者三角网格表面的材质类型和特点(例如,平原/山地/山顶雪原)。如果设置了材质ID,那么传递给对应PxShape对象的材质对象也需要是一个数组的形式,并且数组中元素的数量与材质ID相互对应。
从前文中我们已知,PxShape对象通过_sdk->createShape()从具体的几何体类型创建,然后通过PxActor::attachShape()关联到角色对象。在关联到角色之前,我们可以通过setMaterials()给这个PxShape设置一个材质数组。如下所示:
std::vector<PxMaterial*> materials; // 设置材质数组
……
shape->setMaterials(&materials[0], materials.size()); // 关联一个材质数组
actor->attach(shape);
如果希望获取某个材质ID对应的材质对象,也可以在角色创建之后,重新获取它的碰撞体形状,并通过PxShape::getMaterialFromInternalFaceIndex()获取ID对应的材质对象。这一过程简述如下:
PxShape* shape = NULL; actor->getShapes(&shape, 1); // 注意这里假设只有1个形状
PxMaterial* mtl = shape->getMaterialFromInternalFaceIndex(0); // ID=0的材质
直接读取高度图数据
OSG的examples目录下有一组历史悠久的高度图数据,在OSG源代码目录中,找到:examples/osghangglide/terrain_coords.h这个文件,打开它之后发现是一组38 * 39个XYZ数据(变量float vertex[][][3]),其中Z数据保存的就是具体的高度值。
我们将这组数据传递给physx::PxHeightFieldDesc,从而构建一个高度图碰撞体,并进而构建一个PxRigidStatic对象(静态对象,显然它不太可能是动态的)。
这里要特别注意的是PhysX中的高度图坐标系问题。在之前的章节中我们没有特别关心PhysX的坐标系设定,因为它与OSG一样采用了右手坐标系,并且无论是重力方向还是具体的碰撞体坐标值,都可以根据自己的需要设置,不用考虑Y-up还是Z-up的问题。
但是对于高度图对象,PhysX默认认为它的“行”方向为X+,“列”方向为Z+,高度方向为Y+;这就和OSG高度图对象的默认方向(X+,Y+,高度Z+)有区别了,如图所示。
与osg::HeightField结合使用
为了确保PhysX构建的高度图可以在OSG中正确渲染显示,首先我们需要将生成的PxRigidStatic对象沿着X方向旋转90度,即:
PxRigidStatic* actor = _sdk->createRigidStatic(
PxTransform(PxQuat(osg::PI_2, PxVec3(1.0f, 0.0f, 0.0f))));
这样可以确保生成的物理碰撞体也是Z-up的状态,同时,如果我们要构建这个物理碰撞体对应的渲染模型的话,可以直接使用osg::HeightField和osg::ShapeDrawable,并且正确调整上层Transform节点:
PxHeightFieldGeometry hfGeom;
PxHeightField* hf = hfGeom.heightField;
……
osg::HeightField* grid = new osg::HeightField;
grid->allocate(hf->getNbColumns(), hf->getNbRows());
grid->setXInterval(hfGeom.columnScale);
grid->setYInterval(hfGeom.rowScale);
for (unsigned int r = 0; r < hf->getNbRows(); ++r)
for (unsigned int c = 0; c < hf->getNbColumns(); ++c)
{
const PxHeightFieldSample& s = hf->getSample(r, c);
grid->setHeight(c, r, (float)s.height * hfGeom.heightScale);
}
geode->addDrawable(new osg::ShapeDrawable(grid));
transform->addChild(geode); // 假设geode和transform都已经定义好了
transform->setMatrix(osg::Matrix::rotate(-osg::PI_2, osg::Y_AXIS)
* osg::Matrix::rotate(-osg::PI_2, osg::Z_AXIS)
* osgPhysics::toMatrix(PxMat44(actor->getGlobalPose())));
也可以考虑在输入DEM信息到PhysX的时候就进行变换来确保数据坐标系的一致,这里的设置仅作参考。
Pvd调试环境
这种高度图坐标系匹配的问题,如果单纯依靠OSG的渲染画面来做判断和对齐,那必然是一个非常痛苦的过程。并且随着物理场景的构建愈发复杂,能够在“渲染流程”之外看到物理世界自己的仿真运行结果,并据此进行判断和程序调试——这无疑成了一个不可或缺的需求。PhysX考虑到了这一点,因此它为开发者提供了一个相当强大的“利器”,即PhysX Visual Debugger(简称PVD)。
这个工具目前需要注册NVIDIA开发者之后才能够下载:https://developer.nvidia.com/physx-visual-debugger
PVD只有在DEBUG,CHECKED或者PROFILE配置下才能够使用,RELEASE配置下是无法启用的。它可以被理解为是一个在线的调试器/仿真器,和用户程序通过TCP/IP协议进行通讯,并显示当前物理场景的动态更新和变化结果。
创建一个PVD连接的过程如下(一个用户程序中只需要创建一次,销毁则调用PVD对象的release()函数):
physx::PxPvd* pvd = PxCreatePvd(_foundation);
physx::PxPvdTransport pvdTransport = PxDefaultPvdSocketTransportCreate(“127.0.0.1”, 5425, 10); // 默认为localhost:5425
pvd->connect(*pvdTransport, PxPvdInstrumentationFlag::eALL);
构建测试场景并运行
osgPhysics中提供了一个专门的例子physics_heightfield,在DEBUG模式下运行这个例子(按“1”键发射动态物理对象)以及PVD的效果如下图所示:
暂无评论内容