Game Programming with DirectX -- 09[那是什么鬼东西]
第九集 那是什么鬼东西
想起第一次面对StarCraft的主界面, 看到旋转的星球, 晃着脑袋的卫星, … , 那是什么?
想知道, 简单, 把鼠标移上去, 它会告诉你的Who am I.
9.1 逆转换
鼠标在屏幕的热点是相对屏幕坐标系, Windows屏幕坐标以左上角为原点, X轴正方向朝右, Y轴正方向朝下. 问题是Windows屏幕坐标和虚拟3D世界坐标不是简单映射的, 这个过程很复杂(在第2集的3D pipeline中有详细的过程描述), 我们只有将鼠标的热点的坐标通过Windows坐标到3D pipeline的屏幕坐标及3D pipeline的逆转换后, 才能得到在3D世界的对应点, 最后确定这个点是否有物体, 这就是Picking.
部分知识参考DX9c SDK文档中的Viewports and Clipping和Transforms主题.
9.1.1 Picking Ray
如图9.1, 我们根据3D pipeline的原理来转换屏幕点S,
图9.1
3D pipeline 中有一个重要的点 – 视点, 屏幕在DirectX Graphics中是Z = 1的视平面(摄影机坐标系). 屏幕显示的是所有在视锥内的局部虚拟3D世界中的物体, 我们从视点通过屏幕上的点S的射线有可能和视锥内的局部虚拟3D世界中的物体有交点, 射线称为Picking Ray, 我们依靠这条射线来确定选取的物体.
Picking Ray 可以由它的端点R0(Rx, Ry, Rz)和方向向量U确定, R(t) = R0 + t * U.
9.1.2 Windows 坐标到3D pipeline的屏幕坐标
现在开始逆转换, DirectX Graphics显示的一帧数据以Surface表示的, 如图9.2, surface中保存着以Windows坐标系统表示的数据. 3D pipeline的屏幕坐标系通过以下矩阵的转换到Windows坐标系统,
| Width / 2 0 0 0 |
| 0 - Height / 2 0 0 |
M = | 0 0 MaxZ – MinZ 0 |
| X + Width / 2 Y + Heught / 2 MinZ 1 |
图9.2
对于鼠标的Windows坐标P(Px, Py), 是由3D pipeline的屏幕坐标系中点S(Sx, Sy, Sz)转换而来的, 满足 [S] * M = [P], [s] = (Sx, Sy, Sz, 1), [P] = (Px, Py, 1, 1),坐标代入得到,
Px = Sx * Width / 2 + X + Width / 2
Py = - Sy * Height / 2 + Y + Height / 2
Pz = Sz = 1
现在我们要计算的是S,
Sx = (2 * Px – 2 * X – Width) / Width
Sy = (-2 * Py + 2 * Y + Height) / Height
Sz = 1
当游戏是以全屏模式运行的时候, X = Y = 0,
Sx = (2 * Px – Width) / Width
Sy = (-2 * Py + Height) / Height
Sz = 1
9.1.3 3D pipeline 的屏幕坐标系到摄影机坐标系的变换
这个变换相对简单, 因为投影矩阵是对角矩阵(参考 2.1.4.3 ), 我们只要除以这个对角矩阵的相应数值就可以了,
Cx = (2 * Px – Width) / (Width * P[0][0])
Cy = (-2 * Py + Height) / (Height * P[1][1])
9.1.4 摄影机坐标系到世界坐标系的变换
这里的变换矩阵就比较复杂了(参考 2.1.4.2 ), 我们需要用到逆矩阵了,
E(Ex, Ey, Ez, Ew) = C(Cx, Cy, Cz, Cw) * M -1
9.1.5 代码实现
//#define XLPARAM(lp) ((LONG)(SHORT)LOWORD(lp))
//#define YLPARAM(lp) ((LONG)(SHORT)HIWORD(lp))
LONG x = XLPARAM(dwPt);
LONG y = YLPARAM(dwPt);
FLOAT Rx = 0.0;
FLOAT Ry = 0.0;
FLOAT b = 0.0;
FLOAT c = 0.0;
D3DVIEWPORT9 mv;
D3DXMATRIX mProj;
D3DXMATRIX mView;
m_pD3DDev->GetViewport(&mv);
m_pD3DDev->GetTransform(D3DTS_PROJECTION, &mProj);
m_pD3DDev->GetTransform(D3DTS_VIEW, &mView);
D3DXMatrixInverse(&mView, NULL, &mView);
b = mProj(0, 0);
c = mProj(1, 1);
Rx = ((x * 2.0) - mv.Width) / (mv.Width * b);
Ry = (mv.Height - (y * 2.0)) / (mv.Height * c);
D3DXVECTOR3 pos(0.0, 0.0, 0.0);
D3DXVECTOR3 n(Rx, Ry, 1.0);
D3DXVec3TransformCoord(&pos, &pos, &mView);
D3DXVec3TransformNormal(&n, &n, &mView);
D3DXVec3Normalize(&n, &n);
9.2 它是谁
终于转换好了, 下面要确定在世界坐标系中谁在相应的坐标了. 其实我们发现射线Picking就是 光线跟踪算法 ( 参考 5.2 ), 可以试着使用BSP来确定需要和射线做相交测试的物体, 想想和一个模型复杂的物体的测试, 可能要抓狂了 -- 天哪!
当然, 实际不会真的和物体模型测试的(除非 … ), 最实际的是和物体模型的最小外接正方体或球等做测试就可以了.
9.2.1 Mesh 的Bounding
DirectX Graphics 中为Mesh提供了计算外接正方体或球的函数,
HRESULT D3DXComputeBoundingBox(CONST D3DXVECTOR3 * pMeshVertex,
DWORD NumVertices,
DWORD dwStride,
D3DXVECTOR3 * pMin,
D3DXVECTOR3 * pMax);
HRESULT D3DXComputeBoundingSphere(CONST D3DXVECTOR3 * pMeshVertex,
DWORD NumVertices,
DWORD dwStride,
D3DXVECTOR3 * pCenter,
FLOAT * pRadius);
9.2.2 Ray & Sphere
一般我们选用球做测试更简单, 球原心向量O(Ox, Oy, Oz), 射线对应向量R0(Rx, Ry, Rz), 求出交点数|| R0 + t * U – O || = r,
A * t^2 + B * t + C = 0
A = (U DOT U) = 1 ( 我们已经将U归一化)
B = 2 (U DOT (R0 - O))
C = ((R0 - O) DOT (R0 - O)) – r^2
a = B^2 - 4 * C, 如果a < 0, 没有交点; 如果a > 0, 那么只有当有一根不为负时, 有交点, 负根表示和当前射线方向相反的方向和球相交.
// m_vCenter 是Mesh外界球的中心向量
// m_fRad 是Mesh外界球的半径
D3DXVECTOR3 v = pos - m_vCenter;
b = 2.0 * D3DXVec3Dot(&n, &v);
c = D3DXVec3Dot(&v, &v) - m_fRad * m_fRad;
FLOAT del = b * b - 4 * c;
if (del > 0.0)
{
del = sqrtf(del);
FLOAT x1 = ( del - b) / 2.0;
FLOAT x2 = (-del - b) / 2.0;
if ((x1 < 0.0) && (x2 < 0.0))
{
return FALSE;
}
return TRUE;
}
return FALSE;
9.3 Who am I
如果物体在被Pick后自己能说: " 我是太阳", 那太好了, 但现在我们还未接触DirectX Audio, 所以我们还是用文字表达吧.
9.3.1 Bitmapped Font
Bitmapped Font 相对英语很简单, 26个字母一16 * 16行成一队列Bitmap, 大小写各一行, 用到字母 "A"只需将(0, 16) -- (16, 32)的Bitmap数据CopyRect就可以了, 这种方式早期的游戏很常见.
9.3.2 ID3DXFont
DirectX Graphics 中有专门的文字显示接口ID3DXFont, 和GDI中的HFONT的创建类似, 显示文字的函数也和GDI中相同. 使用以下两个函数来创建字体,
HRESULT D3DXCreateFont(LPDIRECT3DDEVICE9 pDevice,
INT Height,
UINT Width,
UINT Weight,
UINT MipLevels,
BOOL Italic,
DWORD CharSet,
DWORD OutputPrecision,
DWORD Quality,
DWORD PitchAndFamily,
LPCTSTR pFacename,
LPD3DXFONT * ppFont);
HRESULT D3DXCreateFontIndirect(LPDIRECT3DDEVICE9 pDevice,
CONST D3DXFONT_DESC * pDesc,
LPD3DXFONT * ppFont);
实际上, ID3DXFont的创建和显示字体还是依赖于GDI的FONT的创建和显示字体的函数的, 所以ID3DXFont显示字体并非是由DirectX Graphics中的Direct3D来完成的, 如果想提高性能, 建议少量的字体显示用9.3.1的方式, 大量的字体显示可事先转换成bitmap.
9.4 Pick 的例子
9.4.1 game9 project 代码更新
game9 中只是演示了 Picking, ID3DXLINE 和文字显示.
---------------------------------------------------------------
// 全局变量, 用来保存4个测试球的半径, 原始圆心位置, 变动圆心位置
// 最后两个向量是Picking Ray的端点和方向
FLOAT g_fRad[4];
D3DXVECTOR3 g_vCenter[4];
D3DXVECTOR3 g_vTest[4];
D3DXVECTOR3 g_pos;
D3DXVECTOR3 g_n;
// game9.cpp, LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
// 加入对WM_MOUSEHOVER的消息探测, 大概1秒钟的鼠标停顿产生WM_MOUSEHOVER
// 注意使用WM_MOUSEHOVER 必须定义 #define _WIN32_WINNT 0x0500(0x400以上)
case WM_MOUSEMOVE:
{
TRACKMOUSEEVENT tme = { 0 };
tme.cbSize = sizeof(TRACKMOUSEEVENT);
tme.dwFlags = TME_LEAVE | TME_HOVER;
tme.hwndTrack = hWnd;
tme.dwHoverTime = 0x0400;
TrackMouseEvent(&tme);
if (g_bHover)
{
g_bHover = FALSE;
g_pGame->EndPick();
}
}
break;
case WM_MOUSEHOVER :
{
if (g_bHover == FALSE)
{
g_bHover = g_pGame->Picking((DWORD)lParam);
}
}
break;
// direct9.cpp, 保存Picking Ray的端点和方向, 以为当4个模型自动旋转时有自动Pick的功能
BOOL CD9Game::Picking(DWORD dwPt)
{
LONG x = XLPARAM(dwPt);
LONG y = YLPARAM(dwPt);
FLOAT Rx = 0.0;
FLOAT Ry = 0.0;
FLOAT mx = 0.0;
FLOAT my = 0.0;
D3DVIEWPORT9 mv;
D3DXMATRIX mProj;
D3DXMATRIX mView;
m_pD3DDev->GetViewport(&mv);
m_pD3DDev->GetTransform(D3DTS_PROJECTION, &mProj);
m_pD3DDev->GetTransform(D3DTS_VIEW, &mView);
D3DXMatrixInverse(&mView, NULL, &mView);
mx = mProj(0, 0);
my = mProj(1, 1);
Rx = (FLOAT)((x * 2.0) - mv.Width) / (mv.Width * mx);
Ry = (FLOAT)(mv.Height - (y * 2.0)) / (mv.Height * my);
g_pos.x = 0.0;
g_pos.y = 0.0;
g_pos.z = 0.0;
g_n.x = Rx;
g_n.y = Ry;
g_n.z = 1.0;
D3DXVec3TransformCoord(&g_pos, &g_pos, &mView);
D3DXVec3TransformNormal(&g_n, &g_n, &mView);
D3DXVec3Normalize(&g_n, &g_n);
return WhoAmI();
}
// direct9.cpp, 确定当前选中的物体
BOOL CD9Game::WhoAmI()
{
BOOL bPick = FALSE;
D3DXVECTOR3 v;
FLOAT b, c;
INT i = 0;
for (; i < 4; i++)
{
v = g_pos - g_vTest[i];
b = FLOAT(2.0 * D3DXVec3Dot(&g_n, &v));
c = D3DXVec3Dot(&v, &v) - g_fRad[i] * g_fRad[i];
FLOAT del = b * b - 4 * c;
if (del > 0.0)
{
del = sqrtf(del);
FLOAT x1 = (FLOAT)(( del - b) / 2.0);
FLOAT x2 = (FLOAT)((-del - b) / 2.0);
if ((x1 >= 0.0) || (x2 >= 0.0))
{
bPick = TRUE;
break;
}
}
}
lstrcpyn(g_szPick, g_szObj[i], 512);
return bPick;
}
// direct9.cpp, 渲染物体, 如果自动旋转时有自动Pick
VOID CD9Game::Render()
{
if (m_dwRot >= MAXROT)
{
m_dwRot = 1L;
}
else if (m_bRot)
{
m_dwRot++;
}
D3DXMATRIX mWorld, mWorldX, mWorldY, mWorldZ;
FLOAT rat = m_dwRot * ROT;
D3DXMatrixRotationX(&mWorldX, rat);
D3DXMatrixRotationY(&mWorldY, rat);
D3DXMatrixRotationZ(&mWorldZ, rat);
D3DXMatrixMultiply(&mWorld, &mWorldX, &mWorldY);
D3DXMatrixMultiply(&mWorld, &mWorld, &mWorldZ);
D3DXMATRIX m1, m2, m3, m4;
D3DXMatrixMultiply(&m1, &g_m1, &mWorld);
D3DXMatrixMultiply(&m2, &g_m2, &mWorld);
D3DXMatrixMultiply(&m3, &g_m3, &mWorld);
D3DXMatrixMultiply(&m4, &g_m4, &mWorld);
if (m_bRot)
{ // 如果自动旋转时有自动Pick
D3DXVec3TransformCoord(&g_vTest[0], &g_vCenter[0], &m1);
D3DXVec3TransformCoord(&g_vTest[1], &g_vCenter[1], &m2);
D3DXVec3TransformCoord(&g_vTest[2], &g_vCenter[2], &m3);
D3DXVec3TransformCoord(&g_vTest[3], &g_vCenter[3], &m4);
WhoAmI();
}
m_pD3DDev->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER,
D3DCOLOR_XRGB(0, 0, 0), 1.0f, 0);
m_pD3DDev->BeginScene();
m_pD3DDev->SetTransform(D3DTS_WORLD, &m1);
m_pMeshShip->Render();
m_pD3DDev->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
m_pMeshSphere->DrawSubset(0);
m_pD3DDev->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);
m_pD3DDev->SetTransform(D3DTS_WORLD, &m2);
m_pMesh1->DrawSubset(0);
m_pD3DDev->SetTransform(D3DTS_WORLD, &m3);
m_pMesh2->DrawSubset(0);
m_pD3DDev->SetTransform(D3DTS_WORLD, &m4);
m_pMesh3->DrawSubset(0);
// 画Y轴向上或向下的偏移, X轴向左或向右的偏移
D3DXVECTOR2* pLine = g_vy;
m_pLine->Begin();
m_pLine->Draw(pLine, 8, D3DCOLOR_XRGB(0, 128, 0));
pLine += 8;
m_pLine->Draw(pLine, 2, D3DCOLOR_XRGB(0, 255, 0));
pLine += 2;
m_pLine->Draw(pLine, 8, D3DCOLOR_XRGB(0, 128, 0));
pLine += 8;
m_pLine->Draw(pLine, 2, D3DCOLOR_XRGB(0, 255, 0));
m_pLine->End();
// 显示每秒帧数和选择物体的类型
RECT rect = { 16, 16, 512, 48 };
wsprintf(g_szFPS, _T("Current FPS : %ld"), g_dwFPS);
m_pFont->DrawText(NULL, g_szFPS, -1, &rect,
DT_NOCLIP | DT_VCENTER, D3DCOLOR_XRGB(0, 128, 0));
rect.top = 48;
rect.bottom += 32;
m_pFont->DrawText(NULL, g_szPick, -1, &rect, DT_NOCLIP, D3DCOLOR_XRGB(0, 128, 0));
m_pD3DDev->EndScene();
m_pD3DDev->Present(NULL, NULL, NULL, NULL);
}
---------------------------------------------------------------
9.4.2 game9 project 说明
为代码简单, 在鼠标停留在某地一定时间产生 WM_MOUSEHOVER 消息时, 才会有Pick功能的调用, 物体类型在左上角显示 . 自动旋转/停止用Space切换, 自动旋转时有自动Picking功能, 也就是鼠标不动, 当物体旋转到鼠标位子将自动识别物体.
识别物体有延迟现象, 作为例子就不改的太复杂了. 还有这种Picking技术是最普通的, 物体少的时候用还不错, 实际的游戏可以使用BSP, 反馈法, 直接映射法等.
第九集 小结
这一集我们学习了要进行DirectX Graphics 3D编程中的物体Pick和文字显示.