当前位置: 移动技术网 > IT编程>开发语言>C/C++ > DirectX11 With Windows SDK--21 鼠标拾取

DirectX11 With Windows SDK--21 鼠标拾取

2018年10月18日  | 移动技术网IT编程  | 我要评论

瑞星杀毒软件2006,上传歌曲的网站,乐视网甄嬛传

前言

由于最近在做项目,不得不大幅减慢更新速度。现在可能一个月1-2章。

拾取是一项非常重要的技术,不论是电脑上用鼠标操作,还是手机的触屏操作,只要涉及到ui控件的选取则必然要用到该项技术。除此之外,一些类似魔兽争霸3、星际争霸2这样的3d即时战略游戏也需要通过拾取技术来选中角色。

给定在2d屏幕坐标系中由鼠标选中的一点,并且该点对应的正是3d场景中某一个对象表面的一点。 现在我们要做的,就是怎么判断我们选中了这个3d对象。

在阅读本章之前,先要了解下面的内容:

章节
05 键盘和鼠标输入
06 directxmath数学库
10 摄像机类
18 使用directxcollision库进行碰撞检测

github项目源码

核心思想

龙书11上关于鼠标拾取的数学原理讲的过于详细,这里尽可能以简单的方式来描述。

因为我们所能观察到的3d对象都处于视锥体的区域,而且又已经知道摄像机所在的位置。因此在屏幕上选取一点可以理解为从摄像机发出一条射线,然后判断该射线是否与场景中视锥体内的物体相交。若相交,则说明选中了该对象。

当然,有时候射线会经过多个对象,这个时候我们就应该选取距离最近的物体。

一个3d对象的顶点原本是位于局部坐标系的,然后经历了世界变换观察变换投影变换后,会来到ndc空间中,可视物体的深度值(z值)通常会处于0.0到1.0之间。而在ndc空间的坐标点还需要经过视口变换,才会来到最终的屏幕坐标系。在该坐标系中,坐标原点位于屏幕左上角,x轴向右,y轴向下,其中x和y的值指定了绘制在屏幕的位置,z的值则用作深度测试。而且从ndc空间到屏幕坐标系的变换只影响x和y的值,对z值不会影响。

而现在我们要做的,就是将选中的2d屏幕点按顺序进行视口逆变换投影逆变换观察逆变换,让其变换到世界坐标系并以摄像机位置为射线原点,构造出一条3d射线,最终才来进行射线与物体的相交。在构造屏幕一点的时候,将z值设为0.0即可。z值的变动,不会影响构造出来的射线,相当于在射线中前后移动而已。

现在回顾一下视口类d3d11_viewport的定义:

typedef struct d3d11_viewport {
    float topleftx;
    float toplefty;
    float width;
    float height;
    float mindepth;
    float maxdepth;
} d3d11_viewport;

从ndc坐标系到屏幕坐标系的变换矩阵如下:

\[ \mathbf{t}=\begin{bmatrix} \frac{width}{2} & 0 & 0 & 0 \\ 0 & -\frac{height}{2} & 0 & 0 \\ 0 & 0 & maxdepth - mindepth & 0 \\ topleftx + \frac{width}{2} & toplefty + \frac{height}{2} & mindepth & 1 \end{bmatrix}\]

现在,给定一个已知的屏幕坐标点(x, y, 0),要实现鼠标拾取的第一步就是将其变换回ndc坐标系。对上面的变换矩阵进行求逆,可以得到:

\[ \mathbf{t^{-1}}=\begin{bmatrix} \frac{2}{width} & 0 & 0 & 0 \\ 0 & -\frac{2}{height} & 0 & 0 \\ 0 & 0 & \frac{1}{maxdepth - mindepth} & 0 \\ -\frac{2topleftx}{width} - 1 & \frac{2toplefty}{height} + 1 & -\frac{mindepth}{maxdepth - mindepth} & 1 \end{bmatrix}\]

尽管directxmath没有构造视口矩阵的函数,我们也没必要去直接构造一个这样的矩阵,因为上面的矩阵实际上可以看作是进行了一次缩放和平移,即对向量进行了一次乘法和加法:
\[\mathbf{v}_{ndc} = \mathbf{v}_{screen} \cdot \mathbf{scale} + \mathbf{offset}\]
\[\mathbf{scale} = (\frac{2}{width}, -\frac{2}{height}, \frac{1}{maxdepth - mindepth}, 1)\]
\[\mathbf{offset} = (-\frac{2topleftx}{width} - 1, \frac{2toplefty}{height} + 1, -\frac{mindepth}{maxdepth - mindepth}, 0)\]

由于可以从之前的camera类获取当前的投影变换矩阵和观察变换矩阵,这里可以直接获取它们并进行求逆,得到在世界坐标系的位置:
\[\mathbf{v}_{world} = \mathbf{v}_{ndc} \cdot \mathbf{p}^{-1} \cdot \mathbf{v}^{-1} \]

射线类ray

ray类的定义如下:

struct ray
{
    ray();
    ray(const directx::xmfloat3& origin, const directx::xmfloat3& direction);

    static ray screentoray(const camera& camera, float screenx, float screeny);

    bool hit(const directx::boundingbox& box, float* poutdist = nullptr, float maxdist = flt_max);
    bool hit(const directx::boundingorientedbox& box, float* poutdist = nullptr, float maxdist = flt_max);
    bool hit(const directx::boundingsphere& sphere, float* poutdist = nullptr, float maxdist = flt_max);
    bool xm_callconv hit(directx::fxmvector v0, directx::fxmvector v1, directx::fxmvector v2, float* poutdist = nullptr, float maxdist = flt_max);

    directx::xmfloat3 origin;       // 射线原点
    directx::xmfloat3 direction;    // 单位方向向量
};

其中静态方法ray::screentoray执行的正是鼠标拾取中射线构建的部分,其实现灵感来自于directx::xmvector3unproject函数,它通过给定在屏幕坐标系上的一点、视口属性、投影矩阵、观察矩阵和世界矩阵,来进行逆变换,得到在物体坐标系的位置:

inline xmvector xm_callconv xmvector3unproject
(
    fxmvector v, 
    float     viewportx, 
    float     viewporty, 
    float     viewportwidth, 
    float     viewportheight, 
    float     viewportminz, 
    float     viewportmaxz, 
    fxmmatrix projection, 
    cxmmatrix view, 
    cxmmatrix world
)
{
    static const xmvectorf32 d = { { { -1.0f, 1.0f, 0.0f, 0.0f } } };

    xmvector scale = xmvectorset(viewportwidth * 0.5f, -viewportheight * 0.5f, viewportmaxz - viewportminz, 1.0f);
    scale = xmvectorreciprocal(scale);

    xmvector offset = xmvectorset(-viewportx, -viewporty, -viewportminz, 0.0f);
    offset = xmvectormultiplyadd(scale, offset, d.v);

    xmmatrix transform = xmmatrixmultiply(world, view);
    transform = xmmatrixmultiply(transform, projection);
    transform = xmmatrixinverse(nullptr, transform);

    xmvector result = xmvectormultiplyadd(v, scale, offset);

    return xmvector3transformcoord(result, transform);
}

将其进行提取修改,用于我们的ray对象的构造:

ray ray::screentoray(const camera & camera, float screenx, float screeny)
{
    //
    // 节选自directx::xmvector3unproject函数,并省略了从世界坐标系到局部坐标系的变换
    //
    
    // 将屏幕坐标点从视口变换回ndc坐标系
    static const xmvectorf32 d = { { { -1.0f, 1.0f, 0.0f, 0.0f } } };
    xmvector v = xmvectorset(screenx, screeny, 0.0f, 1.0f);
    d3d11_viewport viewport = camera.getviewport();

    xmvector scale = xmvectorset(viewport.width * 0.5f, -viewport.height * 0.5f, viewport.maxdepth - viewport.mindepth, 1.0f);
    scale = xmvectorreciprocal(scale);

    xmvector offset = xmvectorset(-viewport.topleftx, -viewport.toplefty, -viewport.mindepth, 0.0f);
    offset = xmvectormultiplyadd(scale, offset, d.v);

    // 从ndc坐标系变换回世界坐标系
    xmmatrix transform = xmmatrixmultiply(camera.getviewxm(), camera.getprojxm());
    transform = xmmatrixinverse(nullptr, transform);

    xmvector target = xmvectormultiplyadd(v, scale, offset);
    target = xmvector3transformcoord(target, transform);

    // 求出射线
    xmfloat3 direction;
    xmstorefloat3(&direction, xmvector3normalize(target - camera.getpositionxm()));
    return ray(camera.getposition(), direction);
}

此外,在构造ray对象的时候,还需要预先检测direction是否为单位向量:

ray::ray(const directx::xmfloat3 & origin, const directx::xmfloat3 & direction)
    : origin(origin)
{
    // 射线的direction长度必须为1.0f,误差在10e-5f内
    xmvector dirlength = xmvector3length(xmloadfloat3(&direction));
    xmvector error = xmvectorabs(dirlength - xmvectorsplatone());
    assert(xmvector3less(error, xmvectorreplicate(10e-5f)));

    xmstorefloat3(&this->direction, xmvector3normalize(xmloadfloat3(&direction)));
}

构造好射线后,就可以跟各种碰撞盒(或三角形)进行相交检测了:

bool ray::hit(const directx::boundingbox & box, float * poutdist, float maxdist)
{
    
    float dist;
    bool res = box.intersects(xmloadfloat3(&origin), xmloadfloat3(&direction), dist);
    if (poutdist)
        *poutdist = dist;
    return dist > maxdist ? false : res;
}

bool ray::hit(const directx::boundingorientedbox & box, float * poutdist, float maxdist)
{
    float dist;
    bool res = box.intersects(xmloadfloat3(&origin), xmloadfloat3(&direction), dist);
    if (poutdist)
        *poutdist = dist;
    return dist > maxdist ? false : res;
}

bool ray::hit(const directx::boundingsphere & sphere, float * poutdist, float maxdist)
{
    float dist;
    bool res = sphere.intersects(xmloadfloat3(&origin), xmloadfloat3(&direction), dist);
    if (poutdist)
        *poutdist = dist;
    return dist > maxdist ? false : res;
}

bool xm_callconv ray::hit(fxmvector v0, fxmvector v1, fxmvector v2, float * poutdist, float maxdist)
{
    float dist;
    bool res = triangletests::intersects(xmloadfloat3(&origin), xmloadfloat3(&direction), v0, v1, v2, dist);
    if (poutdist)
        *poutdist = dist;
    return dist > maxdist ? false : res;
}

至于射线与网格模型的拾取,有三种实现方式,对精度要求越高的话效率越低:

  1. 将网格模型单个obb盒(或aabb盒)与射线进行相交检测,精度最低,但效率最高;
  2. 将网格模型划分成多个obb盒,分别于射线进行相交检测,精度较高,效率也比较高;
  3. 将网格模型的所有三角形与射线进行相交检测,精度最高,但效率最低。而且模型面数越大,效率越低。这里可以先用模型的obb(或aabb)盒与射线进行大致的相交检测,若在包围盒内再跟所有的三角形进行相交检测,以提升效率。

在该演示教程中只考虑第1种方法,剩余的方法根据需求可以自行实现。

最后是一个项目演示动图,该项目没有做点击物体后的反应。鼠标放到这些物体上会当即显示出当前所拾取的物体。其中立方体和房屋使用的是obb盒。

github项目源码

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网