16-選取

在本章中,我們遇到了確定用戶用鼠標光標選取的3D對象(或基元)的問題(參見圖16.1)。換句話說,給定鼠標光標的2D屏幕座標,我們可以確定投影到該點上的3D對象嗎?要解決這個問題,從某種意義上說,我們必須倒退;也就是說,我們通常從3D空間轉換到屏幕空間,但是在這裏我們從屏幕空間轉換回3D空間。當然,我們已經有一個小問題:2D屏幕點不對應於唯一的3D點(即,多於一個3D點可以投影到相同的2D投影窗口點 - 見圖16.2)。因此,在確定究竟選擇哪個對象時存在一些模糊性。但是,這並不是一個大問題,因爲最接近相機的物體通常是我們想要的物體。

考慮圖16.3,該圖顯示了觀看平截頭體。這裏p是投影窗口上對應於點擊屏幕點s的點。現在我們看到,如果我們通過p拍攝起始於眼睛位置的拾取光線,我們將與投影圍繞p的物體(即本例中的圓柱體)相交。因此,我們的策略如下:一旦我們計算拾取光線,我們可以遍歷場景中的每個對象,並測試光線是否與它交叉。射線相交的對象是用戶挑選的對象。如前所述,例如,如果物體沿着光線的路徑但具有不同的深度值,那麼光線可能與幾個場景物體相交(或者根本沒有物體)。在這種情況下,我們可以將距離相機最近的相交對象作爲拾取對象。

16-1
圖16.1 用戶選擇十二面體。

16-2
圖16.2 平截頭體的側視圖。 觀察3D空間中的幾個點可以投影到投影窗口上的一個點上。

16-3
圖16.3 通過p拍攝的射線將與投影圍繞p的物體相交。 請注意,投影窗口上的投影點p對應於點擊的屏幕點s。

目標:
1.瞭解如何實施拾取算法並瞭解其工作原理。我們分解其爲以下四個步驟:
給定點擊的屏幕點s,在投影窗口中找到它的對應點並將其稱爲p。
計算視圖空間中的拾取光線。 也就是說,射線始於原點,視線空間,射過p。
將採光線和待測射線模型轉換到同一個空間。
確定拾取光線相交的物體。 最近的(與相機相交的)對象與拾取的屏幕對象相對應。

16.1用於投影窗口轉換的屏幕
第一項任務是將點擊的屏幕點轉換爲標準化的設備座標(見§5.4.3.3)。 回想一下,視口矩陣將頂點從標準化的設備座標轉換爲屏幕空間; 它在下面給出:

M=[Width20000Heiht20000MaxDepthMinDepth0TopLeftX+fracWidth2TopLeftY+Heiht2MinDepth1]

視口矩陣的變量指的是D3D11_VIEWPORT結構的變量:
typedef struct D3D11_VIEWPORT {
    FLOAT TopLeftX;
    FLOAT TopLeftY;
    FLOAT Width;
    FLOAT Height;
    FLOAT MinDepth;
    FLOAT MaxDepth;
} D3D11_VIEWPORT;

通常,對於遊戲,視口是整個後緩衝器,並且深度緩衝區範圍是0到1.因此,TopLeftX = 0,TopLeftY = 0,MinDepth = 0,MaxDepth = 1,Width = w和Height = h,其中 w和h分別是後緩衝器的寬度和高度。假設事實確實如此,視口矩陣簡化爲:

M=[w/20000h/2000010w/2h/201]

現在讓pndc=(xndc,yndc,zndc,1) 成爲歸一化設備空間中的一個點(即1xndc1,1yndc1,0zndc1) 。將pndc 轉換爲屏幕空間收益率:
[xndc,yndc,zndc,1][w/20000h/2000010w/2h/201]=[xndcw+w2,yndch+h2,zndc,1]

座標zndc 只被深度緩衝區使用,我們不關心拾取的任何深度座標。 與pndc 對應的2D屏幕點ps=(xs,ys)xyxs=xndcw+w2ys=yndch+h2 p_s p_{ndc}ps p_{ndc} p_{ndc}xndc=2xsw1yndc=2ysh+1NDC§5.6.3.3NDCxrrxr1x/r1NDCxxv=r(2sxw1)yv=2syh+1 $

NOTE:在NDC空間中,在視圖空間中投影的y座標是相同的。這是因爲我們選擇了視角空間中投影窗口的高度來覆蓋區間[-1,1]。

現在回顧一下§5.6.3.1,投影窗與原點的距離爲d=cot(α2) ,其中α是垂直視場角。所以我們可以通過投影窗口上的點(xy,yv,d) 拍攝拾取光線。但是,這需要我們計算d=cot(α2) 。更簡單的方法是從圖16.4中觀察到:

xv=xvd=xvcot(α2)=xv·tanα2=(2sxw1)rtanα2yv=yvd=yvcot(α2)=yv·tanα2=(2syh+1)tanα2

16-4
圖16.4 通過類似的三角形,yvd=yv1xvd=xv1

回想一下,在投影矩陣P00=1rtan(α2)P11=1tan(α2) 中,我們可以將其重寫爲:

xv=(2sxw1)/P00yv=(2syh+1)/P11

因此,我們可以通過(xv,yv,1) 這一點拍攝我們的採摘光線。請注意,這會產生與點(xv,yv,d) 相同的拾取光線。計算視圖空間中拾取光線的代碼如下所示:
void PickingApp::Pick(int sx, int sy)
{
XMMATRIX P = mCam.Proj();
// Compute picking ray in view space.
float vx = (+2.0f*sx/mClientWidth - 1.0f)/P(0,0);
float vy = (-2.0f*sy/mClientHeight + 1.0f)/P(1,1);
// Ray definition in view space.
XMVECTOR rayOrigin = XMVectorSet(0.0f, 0.0f, 0.0f, 1.0f);
XMVECTOR rayDir = XMVectorSet(vx, vy, 1.0f, 0.0f);

請注意,由於眼睛位於視圖空間的原點,因此該光線來自視圖空間中的原點。

16.2 世界/本地空間選擇線

到目前爲止,我們在視圖空間中有拾取光線,但是這僅在我們的對象也位於視圖空間時纔有用。由於視圖矩陣將幾何圖形從世界空間轉換爲視圖空間,因此視圖矩陣的逆矩陣將幾何圖形從視圖空間轉換爲世界空間。如果rv(t)=q+tu 是視圖空間拾取射線而V是視圖矩陣,那麼世界空間拾取射線由下式給出:

rw((t))=qV1+tuV1=qw+tuw

注意,射線原點q被變換爲點(即,qw=1 ),並且射線方向u被變換爲矢量(即,uw=0 )。

在世界空間中定義了一些對象的情況下,世界空間拾取射線可能非常有用。但是,大多數情況下,對象的幾何體是相對於對象本身的本地空間定義的。因此,要執行光線/物體相交測試,必須將光線轉換爲物體的局部空間。如果W是物體的世界矩陣,則矩陣W1 將幾何體從世界空間轉換到物體的局部空間。因此,本地空間採集射線是:

rL(t)=qwW1+tuwW1

通常,場景中的每個對象都有其自己的本地空間。因此,必須將射線轉換爲每個場景對象的局部空間以進行相交測試。

有人可能會建議將網格轉換爲世界空間並在那裏進行相交測試。但是,這太昂貴了。網格可能包含數千個頂點,並且所有這些頂點都需要轉換到世界空間。 將光線轉換爲物體的局部空間效率更高。

以下代碼顯示了拾取光線如何從視圖空間轉換爲對象的局部空間:

// Tranform ray to local space of Mesh.
XMMATRIX V = mCam.View();
XMMATRIX invView = XMMatrixInverse(&XMMatrixDeterminant(V), V);
XMMATRIX W = XMLoadFloat4x4(&mMeshWorld);
XMMATRIX invWorld = XMMatrixInverse(&XMMatrixDeterminant(W), W);
XMMATRIX toLocal = XMMatrixMultiply(invView, invWorld);
rayOrigin = XMVector3TransformCoord(rayOrigin, toLocal);
rayDir = XMVector3TransformNormal(rayDir, toLocal);
// Make the ray direction unit length for the intersection tests.
rayDir = XMVector3Normalize(rayDir);

XMVector3TransformCoord和XMVector3TransformNormal函數將3D矢量作爲參數,但請注意,使用XMVector3TransformCoord函數可知第4個組件的w = 1。 另一方面,使用XMVector3TransformNormal函數,第4個組件的w = 0。 因此我們可以使用XMVector3TransformCoord來轉換點,我們可以使用XMVector3TransformNormal來轉換向量。

16.3 RAY / MESH交叉

一旦我們在同一個空間中有拾取光線和一個網格,我們就可以執行相交測試來查看拾取光線是否與網格相交。 以下代碼遍歷網格中的每個三角形並進行光線/三角形相交測試。 如果射線與其中一個三角形相交,那麼它必須碰到三角形所屬的網格。 否則,射線會錯過網格。 通常,我們需要最近的三角形交點,因爲如果三角形相對於射線重疊,則射線可能會與幾個網格三角形相交。

// If we hit the bounding box of the Mesh, then we might have picked
// a Mesh triangle, so do the ray/triangle tests.
//
// If we did not hit the bounding box, then it is impossible that we
// hit the Mesh, so do not waste effort doing ray/triangle tests.
// Assume we have not picked anything yet, so init to -1.
mPickedTriangle = -1;
float tmin = 0.0f;
if(XNA::IntersectRayAxisAlignedBox(rayOrigin, rayDir,
&mMeshBox, &tmin))
{
// Find the nearest ray/triangle intersection.
tmin = MathHelper::Infinity;
for(UINT i = 0; i < mMeshIndices.size()/3; ++i)
{
// Indices for this triangle.
UINT i0 = mMeshIndices[i*3+0];
UINT i1 = mMeshIndices[i*3+1];
UINT i2 = mMeshIndices[i*3+2];
// Vertices for this triangle.
XMVECTOR v0 = XMLoadFloat3(&mMeshVertices[i0].Pos);
XMVECTOR v1 = XMLoadFloat3(&mMeshVertices[i1].Pos);
XMVECTOR v2 = XMLoadFloat3(&mMeshVertices[i2].Pos);
// We have to iterate over all the triangles in order to find
// the nearest intersection.
float t = 0.0f;
if(XNA::IntersectRayTriangle(rayOrigin, rayDir, v0, v1, v2, &t))
{
if(t < tmin)
{
// This is the new nearest picked triangle.
tmin = t;
mPickedTriangle = i;
}
}
}
}

爲了進行拾取,我們保留了網格幾何體(頂點和索引)的系統內存副本。 這是因爲我們無法訪問靜態頂點/索引緩衝區來讀取數據。 通常存儲系統內存的幾何副本,用於諸如拾取和碰撞檢測之類的事情。 有時爲了節省存儲器和計算而存儲網格的簡化版本。

16.3.1射線/ AABB相交

觀察我們首先使用XNA碰撞庫函數XNA :: IntersectRayAxisAlignedBox來查看射線是否與網格的邊界框相交。這與截錐體剔除優化類似。對場景中的每個三角形執行射線交叉測試都會增加計算時間。即使對於不在拾取光線附近的網格,我們仍然必須遍歷每個三角形,以斷定光線錯過了網格;這是浪費和低效的。一種流行的策略是用簡單的邊界體積來近似網格,如球體或盒子。然後,我們首先將射線與邊界體積相交,而不是將射線與網格相交。如果光線沒有包圍體積,那麼光線必然會錯過三角形網格,因此不需要進一步計算。如果射線與邊界體積相交,那麼我們會進行更精確的射線/網格測試。假設射線會錯過場景中的大部分邊界體積,這爲我們節省了許多射線/三角形相交測試。如果光線與盒子相交,XNA :: IntersectRayAxisAlignedBox函數返回true,否則返回false;它的原型如下:

BOOL IntersectRayAxisAlignedBox(
FXMVECTOR Origin, // ray origin
FXMVECTOR Direction, // ray direction (must be unit length)
const AxisAlignedBox* pVolume, // box
FLOAT* pDist); // ray intersection parameter

給定射線r(t)=q+tu ,最後一個參數輸出產生實際交點p的射線參數t0

p=r(t0)=q+t0u

16.3.2射線/球形交叉點

XNA碰撞庫中還提供了射線/球體相交測試:

BOOL IntersectRaySphere(
FXMVECTOR Origin,
FXMVECTOR Direction,
const Sphere* pVolume,
FLOAT* pDist);

爲了給出這些測試的風味,我們展示瞭如何導出射線/球體相交測試。 具有中心c和半徑r的球體表面上的點p滿足等式:

||pc||=r

r(t)=q+tu 是射線。我們希望求解t1t2 ,使得r(t1)r(t2) 滿足球面方程(即沿着產生交點的射線的參數t1t2 )。
r=||r(t)c||r2=(r(t)c)×(r(t)c)r2=(q+tuc)×(q+tuc)r2=(qc+tu)×(qc+tu)

爲了符號方便,令m=qc
(m+tu)·(m+tu)=r2m·m+2tm·u+t2u·u=r2t2u·u+2t·u+m·mr2=0

這只是一個二次方程:
a=u·ub=2(m·n)c=m·mr2

如果射線方向是單位長度,那麼a = u��u = 1。如果解具有虛部,射線會丟失球體。 如果兩個實數解相同,則射線與球體的切點相交。 如果兩個真實的解決方案是不同的,那麼射線會穿透球體的兩個點。 否定的解決方案表示光線後面的交點。 最小的正解給出了最近的相交參數。

16.3.3 射線/三角交點

爲了執行光線/三角形相交測試,我們使用XNA碰撞庫函數XNA :: IntersectRayTriangle:

BOOL IntersectRayTriangle(
FXMVECTOR Origin, // ray origin
FXMVECTOR Direction, // ray direction (unit length)
FXMVECTOR V0, // triangle vertex v0
CXMVECTOR V1, // triangle vertex v1
CXMVECTOR V2, // triangle vertex v2
FLOAT* pDist); // ray intersection parameter

16-5
圖16.5 三角形平面上的點p具有座標(u,v)相對於原點v0 和軸v1v0v2v0 的偏斜座標系。

當u≥0,v≥0,u+v≤1時,令r(t)=q+tu 爲一條射線,T(u,v)=v0+u(v1v0)+v(v2v0) 一個三角形(見圖16.5)。我們希望同時求解t,u,v,使得r(t)=T(u,v) (即光線和三角形相交的點):
r(t)=T(u,v)q+tu=v0+u(v1v0)+v(v2v0)tu+u(v1v0)+v(v2v0)=qv0

爲了符號方便,令e1=v1v0,e2=v2v0,m=qv0

tu+ue1+ve2=m[ue1e2][tuv]=[m]

考慮矩陣方程Ax=b,其中A是可逆的。然後克萊默法則告訴我們xi=detAi/detA ,其中Ai 通過交換A中的第i列向量與b而得到。因此,
t=det[me1e2]/det[ue1e2]u=det[ume2]/det[ue1e2]v=det[ue1m]/det[ue1e2]

利用det[ue1e2]=a·(b×c) 我們可以將上式重新表達:
t=m·(e1×e2)/u·(e1×e2)u=u·(m×e2)/u·(e1×e2)v=u·(e1×m)/u·(e1×e2)

爲了優化計算,我們可以使用這樣一個事實,即每次交換矩陣中的列時,行列式的符號都會發生變化:
t=e2·(m×e1)/e1·(u×e2)u=m·(u×e2)/e1·(u×e2)v=u·(m×e1)/e1·(u×e2)

並注意可以在計算中重複使用的常見交叉產品:m×e1,u×e2

16.4 演示應用

本章的演示渲染汽車網格,並允許用戶通過按下鼠標右鍵來選擇一個三角形。 在我們的射線/三角形交集循環(§16.3)中,我們將所選三角形的索引緩存在變量mPickedTriangle中。 一旦我們知道了所選三角形的索引,我們就可以使用突出顯示所選三角形的材質重新繪製這個三角形(參見圖16.6):

16-6
圖16.6 挑選的三角形突出顯示爲綠色。

// Draw just the picked triangle again with a different material to
// highlight it.
if(mPickedTriangle != -1)
{
// Change depth test from < to <= so that if we draw the same
// triangle twice, it will still pass the depth test. This
// is because we redraw the picked triangle with a different
// material to highlight it. If we do not use <=, the triangle
// will fail the depth test the 2nd time we try and draw it.
md3dImmediateContext->OMSetDepthStencilState(
RenderStates::LessEqualDSS, 0);
Effects::BasicFX->SetMaterial(mPickedTriangleMat);
activeMeshTech->GetPassByIndex(p)->Apply(0, md3dImmediateContext);
// Just draw one triangle—3 indices. Offset to the picked
// triangle in the mesh index buffer.
md3dImmediateContext->DrawIndexed(3, 3*mPickedTriangle, 0);
// restore default
md3dImmediateContext->OMSetDepthStencilState(0, 0);
}

Note:您可以按住’1’鍵以線框模式查看網格。

16.5 總結

1.揀選是用來確定與用戶用鼠標點擊屏幕上顯示的2D投影對象相對應的3D對象的技術。
2.拾取光線是通過投影窗口中與點擊的屏幕點相對應的點拍攝源自視圖空間原點的光線而找到的。
3.我們可以通過用變換矩陣變換其原點q和方向u來轉換射線r(t)=q+tu 。請注意,原點轉換爲一個點(w = 1),並將方向視爲向量(w = 0)。
4.要測試光線是否與物體相交,我們對物體中的每個三角形執行光線/三角形相交測試。如果射線與其中一個三角形相交,那麼它必須碰到三角形所屬的網格。否則,射線會錯過網格。通常,我們需要最近的三角形交點,因爲如果三角形相對於射線重疊,則射線可能會與幾個網格三角形相交。
5.光線/網格相交測試的性能優化首先執行射線和近似網格的邊界體積之間的相交測試。如果光線沒有包圍體積,那麼光線必然會錯過三角形網格,因此不需要進一步計算。如果射線與邊界體積相交,則我們進行更精確的射線/網格測試。假設射線將錯過場景中的大部分邊界體積,這爲我們節省了許多射線/三角形相交測試。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章