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=Width200TopLeftX+fracWidth20Heiht20TopLeftY+Heiht200MaxDepthMinDepthMinDepth0001 M = [ W i d t h 2 0 0 0 0 − H e i h t 2 0 0 0 0 M a x D e p t h − M i n D e p t h 0 T o p L e f t X + f r a c W i d t h 2 T o p L e f t Y + H e i h t 2 M i n D e p t h 1 ]

视口矩阵的变量指的是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/200w/20h/20h/200100001 M = [ w / 2 0 0 0 0 − h / 2 0 0 0 0 1 0 w / 2 h / 2 0 1 ]

现在让 pndc=(xndc,yndc,zndc,1) p n d c = ( x n d c , y n d c , z n d c , 1 ) 成为归一化设备空间中的一个点(即 1xndc1,1yndc1,0zndc1) − 1 ≤ x n d c ≤ 1 , − 1 ≤ y n d c ≤ 1 , 0 ≤ z n d c ≤ 1 ) 。将 pndc p n d c 转换为屏幕空间收益率:
[xndc,yndc,zndc,1]w/200w/20h/20h/200100001=[xndcw+w2,yndch+h2,zndc,1] [ x n d c , y n d c , z n d c , 1 ] [ w / 2 0 0 0 0 − h / 2 0 0 0 0 1 0 w / 2 h / 2 0 1 ] = [ x n d c w + w 2 , − y n d c h + h 2 , z n d c , 1 ]

坐标 zndc z n d c 只被深度缓冲区使用,我们不关心拾取的任何深度坐标。 与 pndc p n d c 对应的2D屏幕点 ps=(xs,ys)xy p s = ( x s , y s ) 就 是 变 换 的 x 和 y 坐 标 : xs=xndcw+w2ys=yndch+h2 x s = x n d c w + w 2 y s = − y n d c h + h 2 上 面 的 公 式 给 出 了 屏 幕 点 p_s , 以 归 一 化 的 设 备 点 p_{ndc} ps 和 视 口 尺 寸 表 示 。 但 是 , 在 我 们 的 采 摘 情 况 下 , 我 们 最 初 给 出 了 屏 幕 点 p s 和 视 口 尺 寸 , 并 且 我 们 想 要 查 找 p_{ndc} 。 解 决 p_{ndc} 产 生 的 前 面 的 等 式 : xndc=2xsw1yndc=2ysh+1 x n d c = 2 x s w − 1 y n d c = − 2 y s h + 1 NDC线§5.6.3.3NDCxr 我 们 现 在 有 N D C 空 间 中 的 点 击 点 。 但 是 为 了 拍 摄 采 光 线 , 我 们 真 的 希 望 屏 幕 点 在 视 图 空 间 中 。 回 想 一 下 § 5.6.3.3 , 我 们 将 投 影 点 从 视 图 空 间 映 射 到 N D C 空 间 , 方 法 是 将 x 坐 标 除 以 纵 横 比 r : rxr1x/r1 − r ≤ x ′ ≤ r − 1 ≤ x ′ / r ≤ 1 NDCx 因 此 , 为 了 回 到 视 图 空 间 , 我 们 只 需 要 将 N D C 空 间 中 的 x 坐 标 乘 以 长 宽 比 。 因 此 在 视 图 空 间 中 点 击 的 点 是 : xv=r(2sxw1)yv=2syh+1 x v = r ( 2 s x w − 1 ) y v = − 2 s y h + 1 $

NOTE:在NDC空间中,在视图空间中投影的y坐标是相同的。这是因为我们选择了视角空间中投影窗口的高度来覆盖区间[-1,1]。

现在回顾一下§5.6.3.1,投影窗与原点的距离为 d=cot(α2) d = cot ⁡ ( α 2 ) ,其中α是垂直视场角。所以我们可以通过投影窗口上的点 (xy,yv,d) ( x y , y v , d ) 拍摄拾取光线。但是,这需要我们计算 d=cot(α2) d = cot ⁡ ( α 2 ) 。更简单的方法是从图16.4中观察到:

xv=xvd=xvcot(α2)=xvtanα2=(2sxw1)rtanα2yv=yvd=yvcot(α2)=yvtanα2=(2syh+1)tanα2 x v ′ = x v d = x v c o t ( α 2 ) = x v · t a n α 2 = ( 2 s x w − 1 ) r t a n α 2 y v ′ = y v d = y v c o t ( α 2 ) = y v · t a n α 2 = ( − 2 s y h + 1 ) t a n α 2

16-4
图16.4 通过类似的三角形, yvd=yv1 y v d = y v ′ 1 xvd=xv1 x v d = x v ′ 1

回想一下,在投影矩阵 P00=1rtan(α2) P 00 = 1 r t a n ( α 2 ) P11=1tan(α2) P 11 = 1 t a n ( α 2 ) 中,我们可以将其重写为:

xv=(2sxw1)/P00yv=(2syh+1)/P11 x v ′ = ( 2 s x w − 1 ) / P 00 y v ′ = ( − 2 s y h + 1 ) / P 11

因此,我们可以通过 (xv,yv,1) ( x v ′ , y v ′ , 1 ) 这一点拍摄我们的采摘光线。请注意,这会产生与点 (xv,yv,d) ( x v , y v , 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 r v ( t ) = q + t u 是视图空间拾取射线而V是视图矩阵,那么世界空间拾取射线由下式给出:

rw((t))=qV1+tuV1=qw+tuw r w ( ( t ) ) = q V − 1 + t u V − 1 = q w + t u w

注意,射线原点q被变换为点(即, qw=1 q w = 1 ),并且射线方向u被变换为矢量(即, uw=0 u w = 0 )。

在世界空间中定义了一些对象的情况下,世界空间拾取射线可能非常有用。但是,大多数情况下,对象的几何体是相对于对象本身的本地空间定义的。因此,要执行光线/物体相交测试,必须将光线转换为物体的局部空间。如果W是物体的世界矩阵,则矩阵 W1 W − 1 将几何体从世界空间转换到物体的局部空间。因此,本地空间采集射线是:

rL(t)=qwW1+tuwW1 r L ( t ) = q w W − 1 + t u w W − 1

通常,场景中的每个对象都有其自己的本地空间。因此,必须将射线转换为每个场景对象的局部空间以进行相交测试。

有人可能会建议将网格转换为世界空间并在那里进行相交测试。但是,这太昂贵了。网格可能包含数千个顶点,并且所有这些顶点都需要转换到世界空间。 将光线转换为物体的局部空间效率更高。

以下代码显示了拾取光线如何从视图空间转换为对象的局部空间:

// 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 r ( t ) = q + t u ,最后一个参数输出产生实际交点p的射线参数 t0 t 0

p=r(t0)=q+t0u p = r ( t 0 ) = q + t 0 u

16.3.2射线/球形交叉点

XNA碰撞库中还提供了射线/球体相交测试:

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

为了给出这些测试的风味,我们展示了如何导出射线/球体相交测试。 具有中心c和半径r的球体表面上的点p满足等式:

||pc||=r | | p − c | | = r

r(t)=q+tu r ( t ) = q + t u 是射线。我们希望求解 t1 t 1 t2 t 2 ,使得 r(t1) r ( t 1 ) r(t2) r ( t 2 ) 满足球面方程(即沿着产生交点的射线的参数 t1 t 1 t2 t 2 )。
r=||r(t)c||r2=(r(t)c)×(r(t)c)r2=(q+tuc)×(q+tuc)r2=(qc+tu)×(qc+tu) r = | | r ( t ) − c | | r 2 = ( r ( t ) − c ) × ( r ( t ) − c ) r 2 = ( q + t u − c ) × ( q + t u − c ) r 2 = ( q − c + t u ) × ( q − c + t u )

为了符号方便,令 m=qc m = q − c
(m+tu)(m+tu)=r2mm+2tmu+t2uu=r2t2uu+2tu+mmr2=0 ( m + t u ) · ( m + t u ) = r 2 m · m + 2 t m · u + t 2 u · u = r 2 t 2 u · u + 2 t · u + m · m − r 2 = 0

这只是一个二次方程:
a=uub=2(mn)c=mmr2 a = u · u b = 2 ( m · n ) c = m · m − r 2

如果射线方向是单位长度,那么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 v 0 和轴 v1v0 v 1 − v 0 v2v0 v 2 − v 0 的偏斜坐标系。

当u≥0,v≥0,u+v≤1时,令 r(t)=q+tu r ( t ) = q + t u 为一条射线, T(u,v)=v0+u(v1v0)+v(v2v0) T ( u , v ) = v 0 + u ( v 1 − v 0 ) + v ( v 2 − v 0 ) 一个三角形(见图16.5)。我们希望同时求解t,u,v,使得 r(t)=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 r ( t ) = T ( u , v ) q + t u = v 0 + u ( v 1 − v 0 ) + v ( v 2 − v 0 ) − t u + u ( v 1 − v 0 ) + v ( v 2 − v 0 ) = q − v 0

为了符号方便,令 e1=v1v0,e2=v2v0,m=qv0 e 1 = v 1 − v 0 , e 2 = v 2 − v 0 , m = q − v 0

tu+ue1+ve2=mue1e2tuv=m − t u + u e 1 + v e 2 = m [ ↑ ↑ ↑ − u e 1 e 2 ↓ ↓ ↓ ] [ t u v ] = [ ↑ m ↓ ]

考虑矩阵方程Ax=b,其中A是可逆的。然后克莱默法则告诉我们 xi=detAi/detA x i = d e t A i / d e t A ,其中 Ai A i 通过交换A中的第i列向量与b而得到。因此,
t=detme1e2/detue1e2u=detume2/detue1e2v=detue1m/detue1e2 t = d e t [ ↑ ↑ ↑ m e 1 e 2 ↓ ↓ ↓ ] / d e t [ ↑ ↑ ↑ − u e 1 e 2 ↓ ↓ ↓ ] u = d e t [ ↑ ↑ ↑ − u m e 2 ↓ ↓ ↓ ] / d e t [ ↑ ↑ ↑ − u e 1 e 2 ↓ ↓ ↓ ] v = d e t [ ↑ ↑ ↑ − u e 1 m ↓ ↓ ↓ ] / d e t [ ↑ ↑ ↑ − u e 1 e 2 ↓ ↓ ↓ ]

利用 detue1e2=a(b×c) d e t [ ↑ ↑ ↑ − u e 1 e 2 ↓ ↓ ↓ ] = 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 = − m · ( e 1 × e 2 ) / u · ( e 1 × e 2 ) u = u · ( m × e 2 ) / u · ( e 1 × e 2 ) v = u · ( e 1 × m ) / u · ( e 1 × e 2 )

为了优化计算,我们可以使用这样一个事实,即每次交换矩阵中的列时,行列式的符号都会发生变化:
t=e2(m×e1)/e1(u×e2)u=m(u×e2)/e1(u×e2)v=u(m×e1)/e1(u×e2) t = e 2 · ( m × e 1 ) / e 1 · ( u × e 2 ) u = m · ( u × e 2 ) / e 1 · ( u × e 2 ) v = u · ( m × e 1 ) / e 1 · ( u × e 2 )

并注意可以在计算中重复使用的常见交叉产品: m×e1,u×e2 m × e 1 , u × e 2

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 r ( t ) = q + t u 。请注意,原点转换为一个点(w = 1),并将方向视为向量(w = 0)。
4.要测试光线是否与物体相交,我们对物体中的每个三角形执行光线/三角形相交测试。如果射线与其中一个三角形相交,那么它必须碰到三角形所属的网格。否则,射线会错过网格。通常,我们需要最近的三角形交点,因为如果三角形相对于射线重叠,则射线可能会与几个网格三角形相交。
5.光线/网格相交测试的性能优化首先执行射线和近似网格的边界体积之间的相交测试。如果光线没有包围体积,那么光线必然会错过三角形网格,因此不需要进一步计算。如果射线与边界体积相交,则我们进行更精确的射线/网格测试。假设射线将错过场景中的大部分边界体积,这为我们节省了许多射线/三角形相交测试。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值