假设在屏幕上单击,击中的位置为点s=(x,y)。由图可以看出,用户选中了茶壶。但是仅给出点s,应用程序还无法立即判断出茶壶是否被选中。所以针对这类问题,我们需要采用一项称为“拾
取(Picking)”的技术。
茶壶和屏幕点s之间的一种联系是茶壶被投影到了一个包含了s的区域中。更准确地说,茶壶被投影到了投影窗口中一个包含点p(点P位于投影窗口中)的区域中,其中点P对应于屏幕中的点s。
我们看到,如果自坐标原点发出一条拾取射线(picking ray)该射线将与那些其投影包围了点p的物体(即茶壶)相交。所以,一旦计算出了拾取射线,我们就可对场景中的每个物体进行遍历,并逐个测试射线是否与其相交,与射线相交的物体即为用户所拾取的物体。如果拾取射线不与场景中的任意物体相交,则用户便没有拾取到任何物体,而是选中了屏幕中的背景或些我们并不感兴趣的东西。
拾取原理步骤
- 给定所单击的屏幕点s,求出它在投影窗口中的对应点P。
- 计算拾取射线。即自坐标原点发出且通过点P的那条射线。
- 将拾取射线和物体模型变换至同一坐标系中。
- 进行物体/射线的相交判断。相交的物体即为用户所拾取的屏幕对象。
屏幕到投影窗口的变换
视口变换的矩阵为
(里面的参数为D3DVIEWPORT9中的参数)
对投影窗口中的点实施视口变换(将p放入1x4矩阵后与视口变换矩阵相乘),就得到了屏幕点。
,
视口变换之后z坐标并不作为2D图像的一部分进行存储,而是被保存在深度缓存中。解上面的方程后得到,,
假定视口的X和Y成员都为0(一般情况下如此,因为是相对父窗口的大小),这样就得到,,
按照定义投影窗口与平面z=1重合,所以,但是,至此我们还有一些工作要做。由于投影矩阵已对投影窗口中的点进行了比例变换以模拟不同的视场,即呈现出近大远小的效果。为了反求出缩放之前该点的位置,我们必须对该点做一次比例变换的逆运算,设P为投影矩阵,由于项和是该变换矩阵中对应于x和y坐标的比例系数(可根据投影矩阵转换反推得到),所以有
,,
拾取射线的计算
射线可用参数方程,其中是射线的起点,它描述了射线的位置,u是一个描述了射线方向的向量,由15.2可看出射线的起点与坐标原点重合,所以,如果射线经过了投影窗口中的p点,则方向向量u为,,则下面函数用于在给定屏幕坐标系中选定点的x和y坐标条件下,计算观察坐标系中的拾取射线。
struct Ray
{
D3DXVECTOR3 m_origin; //射线起点
D3DXVECTOR3 m_direction; //射线方向
};
//通过给定屏幕坐标系中选定点(x,y),计算观察坐标系中拾取射线
d3d::Ray CalcPickingRay(int x, int y)
{
float px = 0.0f;
float py = 0.0f;
//视口结构
D3DVIEWPORT9 vp;
Device->GetViewport(&vp);
D3DXMATRIX proj;
Device->GetTransform(D3DTS_PROJECTION, &proj);
px = (((2.0f * x) / vp.Width) - 1.0f) / proj(0, 0);
py = (((-2.0f * y) / vp.Height) + 1.0f) / proj(1, 1);
d3d::Ray ray;
ray.m_origin = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
ray.m_direction = D3DXVECTOR3(px, py, 1.0f);
return ray;
}
对射线进行变换
在上节中我们计算所得的拾取射线是在观察坐标系中描述的。为了进行射线/物体相交测试,射线
和物体必须位于同一坐标系中。我们并不打算将所有的物体都变换至观察坐标系中,这是因为将射线变换至世界坐标系甚至某个物体的局部坐标系往往更容易。
借助变换矩阵对其起点和方向u分别进行变换,就实现了射线的变换。注意,起点是按照点来变换的,而方向是按照向量来变换的。本章的例程中实现了如下函数用于对射线进行变换。
void d3d::TransformRay(Ray* ray, D3DXMATRIX* T)
{
//对起点进行变换 w=1
D3DXVec3TransformCoord(&ray->m_origin, &ray->m_origin, T);
//对方向进行变换 w=0
D3DXVec3TransformNormal(&ray->m_direction, &ray->m_direction, T);
D3DXVec3Normalize(&ray->m_direction, &ray->m_direction);
}