您的位置:首页 > 博客中心 > 电脑问题 >

DirectX11 With Windows SDK--18 使用DirectXCollision库进行碰撞检测

时间:2022-03-18 10:30

前言

在DirectX SDK中,碰撞检测的相关函数位于xnacollision.h中。但是现在,前面所实现的相关函数都已经转移到Windows SDK的DirectXCollision.h中,并且处于名称空间DirectX内。这里面主要包含了四种包围盒(Bounding Volumes),并且是以类的形式实现的:

  1. BoundingSphere类--包围球(Bounding Box)
  2. BoundingBox类--轴对齐包围盒(Axis-aligned bounding box),又称AABB盒
  3. BoundingOrientedBox类--有向包围盒(Oriented bounding box),又称OBB盒
  4. BoundingFrustum类--包围视锥体(Bounding Frustum)

除此之外里面还包含有三角形(射线)与其余物体的碰撞检测。

后续的项目将会使用该碰撞库。

常见包围盒

包围球(Bounding Box)

技术分享图片

一个球体只需要使用圆心坐标和半径就可以表示。结构体的一部分如下:

struct BoundingSphere
{
    XMFLOAT3 Center;            // 球体中心坐标
    float Radius;               // 球体半径
    
    // 构造函数
    BoundingSphere() : Center(0,0,0), Radius( 1.f ) {}
    XM_CONSTEXPR BoundingSphere(const XMFLOAT3& center, float radius )
        : Center(center), Radius(radius) {}
    BoundingSphere(const BoundingSphere& sp )
        : Center(sp.Center), Radius(sp.Radius) {}
        
    // ...
    
    // 静态创建方法
    static void CreateMerged(BoundingSphere& Out, const BoundingSphere& S1, const BoundingSphere& S2 );

    static void CreateFromBoundingBox(BoundingSphere& Out, const BoundingBox& box );
    static void CreateFromBoundingBox(BoundingSphere& Out, const BoundingOrientedBox& box );

    static void CreateFromPoints(BoundingSphere& Out, size_t Count, const XMFLOAT3* pPoints, size_t Stride);

    static void CreateFromFrustum(BoundingSphere& Out, const BoundingFrustum& fr );

};

其中BoundingSphere::CreateMergerd静态方法将场景中的两个包围球体用一个更大的包围球紧紧包住。

BoundingSphere::CreateFromBoundingBox静态方法则是从AABB盒或OBB盒创建出外接包围球。

然后还需要着重说明一下BoundingSphere::CreateFromPoints静态方法的使用,因为通常情况下我们是用自定义的结构体来描述一个顶点,然后再使用的顶点数组,所以该方法也适用于从顶点数组创建出一个包围球。

参数Count说明了顶点的数目。

参数pPoints需要填上的是顶点数组第一个元素中位置向量的地址。

参数Stride即可以说是顶点结构体的字节大小,也可以说是跳到下一个元素中的位置向量需要偏移的字节数。

下面是一个使用示例:

struct VertexPosNormalTex
{
    XMFLOAT3 pos;
    XMFLOAT3 normal;
    XMFLOAT3 tex;
};

VertexPosNormalTex vertices[20];
// 省略初始化操作...
BoundingSphere sphere;
BoundingSphere::CreateFromPoints(sphere, 20, &vertices[0].pos, sizeof(VertexPosNormalTex));

轴对齐包围盒(Axis-aligned bounding box)

技术分享图片

一个物体若拥有AABB盒,那AABB盒的六个面都会和物体紧紧贴靠在一起。它可以用两个点描述:Vmax和Vmin。其中Vmax的含义是:分别取物体所有顶点中x, y, z分量下的最大值以构成该顶点。Vmin的含义则是:分别取物体所有顶点中x, y, z分量下的最小值以构成该顶点。

获取了这两个点后,我们就可以用另一种表述方式:中心位置C和一个3D向量E,E的每个分量的含义为中心位置到该分量对应轴的两个面的距离(距离是相等的)。

该碰撞库使用的则是第二种表述方式,但也支持用第一种方式来构建。结构体的一部分如下:

struct BoundingBox
{
    static const size_t CORNER_COUNT = 8;   // 边界点数目

    XMFLOAT3 Center;            // 盒中心点
    XMFLOAT3 Extents;           // 中心点到每个面的距离
    
    // 构造函数
    BoundingBox() : Center(0,0,0), Extents( 1.f, 1.f, 1.f ) {}
    XM_CONSTEXPR BoundingBox(const XMFLOAT3& center, const XMFLOAT3& extents)
        : Center(center), Extents(extents) {}
    BoundingBox(const BoundingBox& box) : Center(box.Center), Extents(box.Extents) {}

    // ...
    
    // 静态创建方法
    static void CreateMerged(BoundingBox& Out, const BoundingBox& b1, const BoundingBox& b2 );

    static void CreateFromSphere(BoundingBox& Out, const BoundingSphere& sh );

    static void XM_CALLCONV CreateFromPoints(BoundingBox& Out, FXMVECTOR pt1, FXMVECTOR pt2 );
    static void CreateFromPoints(BoundingBox& Out, size_t Count, const XMFLOAT3* pPoints,size_t Stride );
};

BoundingBox::CreateMerged静态方法创建一个最小的AABB盒,能够同时包含这两个AABB盒。

BoundingBox::CreateFromSphere静态方法给球体创建外接立方体包围盒。

BoundingBox::CreateFromPoints静态方法中的参数pt1pt2即可以为包围盒某一斜对角线上的两个顶点,也可以是一个包含所有点中xyz分量最大值和最小值的两个构造点。

有向包围盒(Oriented bounding box)

技术分享图片

对某些物体来说,在经过一系列变换后它的包围盒也需要随之改变。但例如一些默认情况下宽度、深度值比高度大得多的物体,比如飞机、书本等,经过变换后它的AABB盒可能会变得特别大,暴露了许多空余的位置,从而不能很好地将物体包住。

技术分享图片

因为AABB盒的边界是轴对齐的,没有办法记录旋转属性。这时候我们可以考虑使用OBB盒,除了包含AABB盒应当记录的信息外,它还记录了旋转相关的信息。结构体部分如下:

struct BoundingOrientedBox
{
    static const size_t CORNER_COUNT = 8;   // 边界点数目

    XMFLOAT3 Center;            // 盒中心点
    XMFLOAT3 Extents;           // 中心点到每个面的距离
    XMFLOAT4 Orientation;       // 单位旋转四元数(物体->世界)

    // 构造函数
    BoundingOrientedBox() : Center(0,0,0), Extents( 1.f, 1.f, 1.f ), Orientation(0,0,0, 1.f ) {}
    XM_CONSTEXPR BoundingOrientedBox(const XMFLOAT3& _Center, const XMFLOAT3& _Extents, const XMFLOAT4& _Orientation)
        : Center(_Center), Extents(_Extents), Orientation(_Orientation) {}
    BoundingOrientedBox(const BoundingOrientedBox& box)
        : Center(box.Center), Extents(box.Extents), Orientation(box.Orientation) {}

    // ...
    
    // 静态创建方法
    static void CreateFromBoundingBox(BoundingOrientedBox& Out, const BoundingBox& box );

    static void CreateFromPoints(BoundingOrientedBox& Out, size_t Count,
        const XMFLOAT3* pPoints, size_t Stride );
};

其中BoundingOrientedBox::CreateFromBoundingBox静态方法创建出跟AABB盒和一样的OBB盒,并且单位旋转四元数是默认的(即没有产生旋转)。

包围视锥体(Bounding Frustum)

为了描述一个视锥体,一种方式是以数学的形式指定视锥体的六个边界平面:左/右平面,顶/底平面,近/远平面。这里假定六个视锥体平面是朝向内部的。

技术分享图片

虽然我们可以用六个4D平面向量来存储一个视锥体,但这还是有更节省空间的表示方法。

首先在物体坐标系中,取摄像头的位置为原点,正前方观察方向为Z轴,右方向为X轴,上方向为Y轴,这样就可以得到右(左)平面投影到zOx平面下的直线斜率为X/Z(-X/Z),以及上(下)平面投影到zOy平面下的直线斜率为Y/Z(-Y/Z)。同时也可以得到近(远)平面到原点的距离,也即是对应的Z值。

然后使用一个3D位置向量和单位旋转四元数来表示视锥体在世界中的位置和朝向。这样我们也可以描述一个视锥体了。

下面展示了视锥体包围盒结构体的部分内容:

struct BoundingFrustum
{
    static const size_t CORNER_COUNT = 8;

    XMFLOAT3 Origin;            // 摄像机在世界中的位置,物体坐标系下默认会设为(0.0f, 0.0f, 0.0f)
    XMFLOAT4 Orientation;       // 单位旋转四元数

    float RightSlope;           // 右平面投影到zOx平面的直线斜率+X/Z
    float LeftSlope;            // 左平面投影到zOx平面的直线斜率-X/Z
    float TopSlope;             // 上平面投影到zOy平面的直线斜率+Y/Z
    float BottomSlope;          // 下平面投影到zOy平面的直线斜率-Y/Z
    float Near, Far;            // Z值对应近(远)平面到摄像机物体坐标系原点的距离

    // 构造函数
    BoundingFrustum() : Origin(0,0,0), Orientation(0,0,0, 1.f), RightSlope( 1.f ), LeftSlope( -1.f ),
                        TopSlope( 1.f ), BottomSlope( -1.f ), Near(0), Far( 1.f ) {}
    XM_CONSTEXPR BoundingFrustum(const XMFLOAT3& _Origin, const XMFLOAT4& _Orientation,
                    float _RightSlope, float _LeftSlope, float _TopSlope, float _BottomSlope,
                    float _Near, float _Far)
        : Origin(_Origin), Orientation(_Orientation),
          RightSlope(_RightSlope), LeftSlope(_LeftSlope), TopSlope(_TopSlope), BottomSlope(_BottomSlope),
          Near(_Near), Far(_Far) {}
    BoundingFrustum(const BoundingFrustum& fr)
        : Origin(fr.Origin), Orientation(fr.Orientation), RightSlope(fr.RightSlope), LeftSlope(fr.LeftSlope),
          TopSlope(fr.TopSlope), BottomSlope(fr.BottomSlope), Near(fr.Near), Far(fr.Far) {}
    BoundingFrustum(CXMMATRIX Projection) { CreateFromMatrix( *this, Projection ); }

    // ...
    // 静态创建方法
    static void XM_CALLCONV CreateFromMatrix(BoundingFrustum& Out, FXMMATRIX Projection);

}

通常情况下,我们会通过传递投影矩阵来创建一个包围视锥体,而不是直接指定上面的这些信息。

包围盒的相交、包含、碰撞检测及变换

包围盒与平面的相交检测

对于包围盒与平面的相交检测,返回结果使用了枚举类型PlaneIntersectionType来描述相交情况:

enum PlaneIntersectionType
{
    FRONT = 0,              // 包围盒在平面的正面区域
    INTERSECTING = 1,       // 包围盒与平面有相交
    BACK = 2,               // 包围盒在平面的背面区域
};

上面提到的四种包围盒都具有重载方法Intersects用于检测该包围盒与平面的相交情况:

PlaneIntersectionType XM_CALLCONV Intersects(FXMVECTOR Plane) const;

正/背面的判定取决于一开始平面法向量的设定。比如一个中心在原点,棱长为2的正方体,与平面-z+2=0(对应4D平面向量(0.0f,0.0f,-1.0f,2.0f), 平面法向量(0.0f,0.0f,-1.0f) )的相交结果为:物体在平面的正面区域。

包围盒与包围盒的包含检测

对于两个包围盒的包含检测,返回结果使用了枚举类型ContainmentType来描述包含情况:

enum ContainmentType
{
    DISJOINT = 0,       // 两个包围盒相互分离
    INTERSECTS = 1,     // 两个包围盒有相交
    CONTAINS = 2,       // 两个包围盒存在包含关系
};

这四种包围盒相互之间都有对应的方法来测试:

ContainmentType Contains(const BoundingSphere& sp) const;
ContainmentType Contains(const BoundingBox& box) const;
ContainmentType Contains(const BoundingOrientedBox& box) const;
ContainmentType Contains(const BoundingFrustum& fr) const;

包围盒与包围盒的碰撞检测

如果我们只需要检查两个包围盒之间是否发生碰撞(相交和包含都算),则可以使用下面的这些方法。四种包围盒相互之间都能进行碰撞测试:

bool Intersects(const BoundingSphere& sh) const;
bool Intersects(const BoundingBox& box) const;
bool Intersects(const BoundingOrientedBox& box) const;
bool Intersects(const BoundingFrustum& fr) const;

包围盒的变换

四种包围盒都包含下面两个方法,一个是任意矩阵的变换,另一个是构造世界矩阵的变换(这里用BoundingVolume来指代这四种包围盒):

void XM_CALLCONV Transform(BoundingVolume& Out, FXMMATRIX M ) const;
void XM_CALLCONV Transform(BoundingVolume& Out, float Scale, FXMVECTOR Rotation, FXMVECTOR Translation) const;

要注意的是,第一个参数都是用于输出变换后的包围盒,Rotation则是单位旋转四元数。

包围盒的其它方法

获取包围盒的八个顶点

除了包围球外的其它包围盒都拥有方法GetCorners

void GetCorners(XMFLOAT3* Corners) const;

这里要求传递的参数Corners是一个可以容纳元素个数至少为8的数组。

获取包围视锥体的六个平面

BoundingFrustum::GetPlanes方法可以获取视锥体六个平面的平面向量:

void GetPlanes(XMVECTOR* NearPlane, XMVECTOR* FarPlane, XMVECTOR* RightPlane,
    XMVECTOR* LeftPlane, XMVECTOR* TopPlane, XMVECTOR* BottomPlane) const;

包围视锥体在检测是否包含某一包围盒的时候内部会调用待测包围盒的ContainedBy静态重载方法,参数为视锥体提供的六个平面。故下面的方法通常我们不会直接用到:

ContainmentType XM_CALLCONV ContainedBy(FXMVECTOR Plane0, FXMVECTOR Plane1, FXMVECTOR Plane2,
    GXMVECTOR Plane3, HXMVECTOR Plane4, HXMVECTOR Plane5 ) const;
// Test frustum against six planes (see BoundingFrustum::GetPlanes)

三角形、射线

三角形的表示需要用到三个坐标点向量,而射线的表示则需要一个Origin向量(射线起点)和一个Direction向量(射线方向)。

射线(三角形)与非包围盒的相交检测

下面这三个常用的方法都在名称空间DirectX::TriangleTests中(ContainedBy函数不会直接使用故不列出来):

namespace TriangleTests
{
    bool XM_CALLCONV Intersects(FXMVECTOR Origin, FXMVECTOR Direction, FXMVECTOR V0, GXMVECTOR V1, 
        HXMVECTOR V2, float& Dist );
        // 射线与三角形的相交检测

    bool XM_CALLCONV Intersects(FXMVECTOR A0, FXMVECTOR A1, FXMVECTOR A2, GXMVECTOR B0, HXMVECTOR B1, 
        HXMVECTOR B2 );
        // 三角形与三角形的相交检测

    PlaneIntersectionType XM_CALLCONV Intersects(FXMVECTOR V0, FXMVECTOR V1, FXMVECTOR V2, 
        GXMVECTOR Plane );
        // 平面与三角形的相交检测

    // 忽略...
};

其中Dist返回的是射线起点到交点的距离,若没有检测到相交,Dist的值为0.0f

射线(三角形)与包围盒的相交检测

四种包围盒都包含了下面的两个方法:

bool XM_CALLCONV Intersects(FXMVECTOR Origin, FXMVECTOR Direction, float& Dist) const;
// 射线与包围盒的相交检测
bool XM_CALLCONV Intersects(FXMVECTOR V0, FXMVECTOR V1, FXMVECTOR V2) const;
// 三角形与包围盒的相交检测

演示程序

关于碰撞检测库的演示可以在下面的链接找到,这里就没有必要再写一个演示程序了:

下载(克隆)到本地后找到Collision文件夹,选择合适的解决方案打开并编译运行即可。这里我选择的是Collision_Desktop_2017_Win10.sln。成功运行的话效果如下:

技术分享图片

热门排行

今日推荐

热门手游