拾取是一項很是重要的技術,不管是電腦上用鼠標操做,仍是手機的觸屏操做,只要涉及到UI控件的選取則必然要用到該項技術。除此以外,一些相似魔獸爭霸三、星際爭霸2這樣的3D即時戰略遊戲也須要經過拾取技術來選中角色。html
給定在2D屏幕座標系中由鼠標選中的一點,而且該點對應的正是3D場景中某一個對象表面的一點。 如今咱們要作的,就是怎麼判斷咱們選中了這個3D對象。git
在閱讀本章以前,先要了解下面的內容:github
章節 |
---|
05 鍵盤和鼠標輸入 |
06 DirectXMath數學庫 |
10 攝像機類 |
18 使用DirectXCollision庫進行碰撞檢測 |
DirectX11 With Windows SDK完整目錄函數
歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。this
龍書11上關於鼠標拾取的數學原理講的過於詳細,這裏儘量以簡單的方式來描述。spa
由於咱們所能觀察到的3D對象都處於視錐體的區域,並且又已經知道攝像機所在的位置。所以在屏幕上選取一點能夠理解爲從攝像機發出一條射線,而後判斷該射線是否與場景中視錐體內的物體相交。若相交,則說明選中了該對象。code
固然,有時候射線會通過多個對象,這個時候咱們就應該選取距離最近的物體。orm
一個3D對象的頂點本來是位於局部座標系的,而後經歷了世界變換、觀察變換、投影變換後,會來到NDC空間中,可視物體的深度值(z值)一般會處於0.0到1.0之間。而在NDC空間的座標點還須要通過視口變換,纔會來到最終的屏幕座標系。在該座標系中,座標原點位於屏幕左上角,x軸向右,y軸向下,其中x和y的值指定了繪製在屏幕的位置,z的值則用做深度測試。並且從NDC空間到屏幕座標系的變換隻影響x和y的值,對z值不會影響。htm
而如今咱們要作的,就是將選中的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
類的定義以下:
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,偏差在1e-5f內 XMVECTOR dirLength = XMVector3Length(XMLoadFloat3(&direction)); XMVECTOR error = XMVectorAbs(dirLength - XMVectorSplatOne()); assert(XMVector3Less(error, XMVectorReplicate(1e-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盒。
DirectX11 With Windows SDK完整目錄
歡迎加入QQ羣: 727623616 能夠一塊兒探討DX11,以及有什麼問題也能夠在這裏彙報。