Direct X 12 – Picking

在本章节中,我们需要解决的问题是,如何找出用户使用鼠标点击的物体(如下图所示)。换句话说,基于鼠标在2D屏幕坐标系中的坐标,我们是否能找到投影到该像素点的物体呢?为了解决这个问题,我们必须逆向思维;也就是说,我们要将一个屏幕空间中点转换至3D空间。而摆在我们面前的一个小问题是:一个2D屏幕中的点并不能对应一个唯一的3D空间中的点(例如,3D空间中有许多点可以投影至屏幕中同一个点)。因此,用户所点击的点是有歧义性的。但是,一般来说距离camera最近的一个物体就是我们想要的。

下图中我们可以看见,视锥体内的3个点都能投影到视窗中的P点。

上图中,视窗中的P点对应屏幕中鼠标点击的s点。现在如果我们从camera的位置射出picking射线,该射线会与多个物体相交。因此,我们的策略为:射出picking射线,检测其是否与场景中的物体相交。再从所有相交的物体中选出距离camera最近的物体,它就是用户所选中的物体。

目标:

1. 理解picking算法与其工作原理。而我们将该算法分解为以下四个步骤:

① 基于鼠标在屏幕上点击的点s,找出其在投影窗口所对应的点p

② 在camera坐标系内计算picking射线。该射线的原点为camera坐标系的原点,且穿过点p

③ 将picking射线与场景中的模型转换为同一坐标系。

④ 进行picking射线与场景中物体的相交检测。距离camera最近的相交物体就是用户在屏幕中选中的物体。

17.1 屏幕至投影窗口的转换

我的第一个任务是将屏幕中鼠标点击的点转换为NDC(normalized device coordinate)空间(忘记的老铁请参考5.6.3.3)。而视口矩阵M将顶点从NDC空间转换至屏幕空间:

视口矩阵中的变量都是基于结构体D3D12_VIEWPORT中的成员变量:

typedef 
struct D3D12_VIEWPORT
    {
        FLOAT TopLeftX;
        FLOAT TopLeftY;
        FLOAT Width;
        FLOAT Height;
        FLOAT MinDepth;
        FLOAT MaxDepth;
    }   D3D12_VIEWPORT;

对于游戏来说,一般视口就是整个back buffer,且depth buffer的范围是[0,1]。因此,TopLeftX=0TopLeftY=0MinDepth=0MaxDepth=1Width=wHeight=h,其中wh分别为back buffer的宽度与高度。因此我们可以将视口矩阵简化为:

现在假设p(ndc)=(x(ndc),y(ndc),z(ndc),1)是位于NDC空间内的一点(-1≤x(ndc)≤1,-1≤y(ndc)≤1,0≤z(ndc)≤1)。将p(ndc)转换为屏幕空间内一点的等式为:

其中z(ndc)只会用于depth buffer,现在我们并不需要该变量。而以上等式基于NDC空间内的点p(ndc)以及视口的尺寸告诉了我们位于屏幕空间内的点p(s)。但是,在demo中,我们需要通过屏幕上一点p(s)与视口的尺寸来找出位于NDC空间的点p(ndc):

现在我们得到了用户点击屏幕后位于NDC空间的坐标点。但是为了得到检测射线,我们真正需要的是位于camera空间的点。在5.6.3.3中我们将坐标点从camera空间投影至NDC空间,只是将x轴上的坐标值除以了视锥体纵横比r:

因此,为了转换为camera坐标系,我只需将NDC空间中x坐标轴的值乘以视锥体纵横比即可。所以,用户点击屏幕后位于camrea坐标系的点为:

坐标轴y上的值从camera坐标系转换至NDC空间后没有发生改变。这是因为我们将投影窗口的高度在camera坐标系中的范围为[-1,1]。

我们在5.6.3.1中提到,投影窗口距离原点d=cot(α/2),其中α为纵向的FOV。所以我们所射出的picking射线将穿过投影窗口上的点(x(v),y(v),d)。然而这需要我们计算d=cot(α/2)。不过,从下图我们可以根据相似三角形的原理推论出y(v)/d=y'(v)/1,x(v)/d=x'(v)/1:

我们可以回想一下投影矩阵,其中P(00)=1/r·tan(α/2),p(11)=1/tan(α/2)。因此,我们可以将x轴与y轴的坐标重写为:

因此,我们的picking射线默认将穿过点(x'(v),y'(v),1)。根据相似三角形的原理,该射线也会穿过点(x(v),y(v),d)。而以下代码展示了如何计算picking射线:

void PickingApp::Pick(int sx, int sy)
{
    XMFLOAT4X4 P = mCamera.GetProj4x4f();

    // 计算位于camera空间的picking射线
    float vx = (2.f * sx / mClientWidth - 1.f) / P(0,0);
    float vy = (2.f * sy / mClientHeight + 1.f) / P(1, 1);

    // picking射线的定义
    XMVECTOR rayOrigin = XMVectorSet(0.f, 0.f, 0.f, 1.f);
    XMVECTOR rayDir = XMVectorSet(vx, vy, 1.f, 0.f);

    ...
}

留下评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据