假设用户点击了屏幕上的点 s (x, y)。 从图15.1我们能看到用户选取了茶壶。无论如何,应用程序无法根据给定的s点就立即确定茶壶是被选取。
我们知道一些知识:关于茶壶和它的关联点s,茶壶投影在围绕s点的区域,更准确的说是:它投影到投影窗口上围绕p点的区域,与它对应的屏幕点是s。因为这个问题依赖于3D物体与它的投影之间的关系,我们看图15.2就可以了解。
图15.2我们看到如果我们发射一条选取射线,从原点发出,经过点p,会与围绕p点投影的对象相交,即茶壶。所以一旦我们计算选取射线,我们可以遍例场景中的每个对象并测试,看射线是否与它相交。与射线相交的对象即是用户选择的对象,在这个例子中用户选取的对象是茶壶。
上面的例子讲解了点s与茶壶的关系。通常我们任意点击屏幕上的点,我们遍例场景中的每个对象,如果对象与射线相交,那么这个对象就是用户选取的对象。例如,图15.1中,如果用户没有点击5个对象中的一个,而是点击了白色的背景区域,射线将不能相交任何对象。因此,结论是:如果射线没有与场景中的任何对象相交,则用户没有点击任何一个对象,其它的我们不关心。
“选取”适用于所有种类的游戏和3D程序。例如,玩家通过用鼠标点击来影响3D世界中的不同对象,玩家可能点击向敌人射击,或点击拾取物品。好的程序会适当做出反应,程序需要知道哪个对象被选取(是敌人还是物品),和在3D空间中的位置(开枪会击中哪?或玩家将要移动到哪去拾取物品?)。选取回答了我们这些问题。
我们将选取分解成四步:
1) 给一个屏幕点s,找出它在投影窗口上相交的点,即p。
2) 计算射线,它是从原点出发并经过点p。
3) 转换射线与模型到同一空间。
4) 测试与射线相交的对象,相交的对象即是屏幕上点击的对象。
15.1屏幕到投影窗口的转换
首先,转换屏幕点到投影窗口,视口变换矩阵是:
因为前面的定义,投影窗口就是z=1的平面,所以pz = 1。
投影矩阵缩放投影窗口上的点,来模拟不同的视角。为了返回缩放前的点值,我们必须用与缩放相反的操作来转换点。P是投影矩阵,因为P00和 P11转换距阵缩放点的x和y坐标,我们得到:
15.2计算射线
回忆一下,射线能够描述参数方程:p(t) = p0 + tu。其中p0是射线的起点,用来描述它的位置,u是向量,用来描述它的方向。
如图15.2,我们知道射线的起点总是视图空间的原点,所以p0 = (0, 0, 0),如果p是射线穿过投影窗口上的点,方向向量u给出:u = p - p0 = (px, py, 1) - (0, 0, 0) = p。
下面的方法用来计算选取射线(从屏幕空间点击的点所对应的视图空间的点x、y坐标):
struct sRay
{
D3DXVECTOR3 origin;
D3DXVECTOR3 direction;
};
sRay calculate_picking_ray(int x, int y)
{
D3DVIEWPORT9 viewport;
g_device->GetViewport(&viewport);
D3DXMATRIX proj_matrix;
g_device->GetTransform(D3DTS_PROJECTION, &proj_matrix);
float px = ((( 2.0f * x) / viewport.Width) - 1.0f) / proj_matrix(0, 0);
float py = (((-2.0f * y) / viewport.Height) + 1.0f) / proj_matrix(1, 1);
sRay ray;
ray.origin = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
ray.direction = D3DXVECTOR3(px, py, 1.0f);
return ray;
}
15.3变换射线
选取射线的计算被描述在视图空间,为了完成射线的相交的测试,射线和对象必须在同一个坐标系统。通常转换射线到世界空间(甚至对象在本地空间)要好于将所有对象转换到视图空间。
我们能够将一个变换矩阵转换为一条原点为p0,方向为u的射线r(t) = p0 + tu,注意:原点转换为一个点,方向转换为一个向量,下列函数转换一条射线:
void
transform_ray(sRay* ray, D3DXMATRIX* trans_matrix)
{
// transform the ray's origin, w = 1.
D3DXVec3TransformCoord(&ray->origin, &ray->origin, trans_matrix);
// transform the ray's direction, w = 0.
D3DXVec3TransformNormal(&ray->direction, &ray->direction, trans_matrix);
// normalize the direction
D3DXVec3Normalize(&ray->direction, &ray->direction);
}
D3DXVec3TransformCoord和D3DXVec3TransformNormal接受一个Ray类型参数(包含二个3D向量成员)。 D3DXVec3TransformCoord函数中,射线的原点(origin)向量的第四部分w = 1。相反,函数D3DXVec3TransformNormal中,射线的方向(direction)向量的第四部分w = 0。
这样,当我们向世界空间转换时,能够用D3DXVec3TransformCoord转换一个点,用D3DXVec3TransformNormal转换一个向量。
15.4射线-对象 交点
我们将射线和对象转换到同一坐标系统后,准备测试哪个对象与射线相交。因为我们将对象描述为三角形组成的网络,下面详细说明这种方法。遍例场景中每个对象的三角形列表并测试,如果射线相交于一个三角形,它就与三角形所在的对象相交。然而,通过遍例场景中的每个三角形来实现射线相交在计算上会增加时间,一种比较快的方法,虽然准确性会差一点。它将每个对象围成一个近似的球形(边界球),这样我们就能通过遍例每个边界球来测试射线相交。用边界球来描述相交的对象。
注意:射线可能相交多个对象,然而离照相机近的对象会被选取。因为近距离对象遮挡了后面的对象。
给出一个边界球的圆心c和半径r,使用下列恒等式能够测试点p是否在边界球上:
||p-c||-r = 0
如果恒等式满足,则点p在边界球上。如图15.3
假定射线p(t) = p0 + tu相交于边界球,我们将射线代入球的恒等式中,使参数t满足了球的恒等式。
将射线p(t) = p0 + tu代入球的恒等式:
||p(t) - c|| - r = 0 à ||p0 + tu - c|| - r = 0
通过以上推导,我们得到二次方程:
At2 + Bt + C = 0
其中A = u · u, B = 2(u · (p0 - c)),而C = (p0 - c) . (p0 - c) – r 2。
如果u是标准化的,那么A = 1。
因为u是标准化的,我们解t0和 t1:
图15.4显示可能返回的t0和 t1,并显示了一些返回值的几何意义:
下列函数测试如果射线与边界球相交,返回true;射线错过边界球,返回false。
bool
ray_sphere_intersect(sRay* ray, cBoundingSphere* sphere)
{
D3DXVECTOR3 v = ray->origin - sphere->m_center;
float
b = 2.0f * D3DXVec3Dot(&ray->direction, &v);
float
c = D3DXVec3Dot(&v, &v) - (sphere->m_radius * sphere->m_radius);
float
discriminant = (b * b) - (4.0f * c);
if
(discriminant < 0.0f)
return
false
;
discriminant = sqrt(discriminant);
float
s0 = (-b + discriminant) / 2.0f;
float
s1 = (-b - discriminant) / 2.0f;
// if one solution is >= 0, then we intersected the sphere.
return
(s0 >= 0.0f || s1 >= 0.0f);
}
15.5例子程序:选取
下图显示了该示例的屏幕截图,茶壶绕着屏幕移动,你可以用鼠标试着点击它。如果你点击到茶壶的边界球上,一个消息框将弹出,表示你点中了。我们通过测试WM_LBUTTONDOWN消息来处理鼠标点击事件。
执行程序:
#include "d3dUtility.h"
#pragma warning(disable : 4100)
const
int
WIDTH = 640;
const
int
HEIGHT = 480;
IDirect3DDevice9* g_device;
ID3DXMesh* g_teapot;
ID3DXMesh* g_sphere;
D3DXMATRIX g_world_matrix;
cBoundingSphere g_bounding_sphere;
///////////////////////////////////////////////////////////////////////////////////////////////////
/
sRay calculate_picking_ray(
int
x,
int
y)
{
D3DVIEWPORT9 viewport;
g_device->GetViewport(&viewport);
D3DXMATRIX proj_matrix;
g_device->GetTransform(D3DTS_PROJECTION, &proj_matrix);
float
px = ((( 2.0f * x) / viewport.Width) - 1.0f) / proj_matrix(0, 0);
float
py = (((-2.0f * y) / viewport.Height) + 1.0f) / proj_matrix(1, 1);
sRay ray;
ray.origin = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
ray.direction = D3DXVECTOR3(px, py, 1.0f);
return
ray;
}
void
transform_ray(sRay* ray, D3DXMATRIX* trans_matrix)
{
// transform the ray's origin, w = 1.
D3DXVec3TransformCoord(&ray->origin, &ray->origin, trans_matrix);
// transform the ray's direction, w = 0.
D3DXVec3TransformNormal(&ray->direction, &ray->direction, trans_matrix);
// normalize the direction
D3DXVec3Normalize(&ray->direction, &ray->direction);
}
bool
ray_sphere_intersect(sRay* ray, cBoundingSphere* sphere)
{
D3DXVECTOR3 v = ray->origin - sphere->m_center;
float
b = 2.0f * D3DXVec3Dot(&ray->direction, &v);
float
c = D3DXVec3Dot(&v, &v) - (sphere->m_radius * sphere->m_radius);
float
discriminant = (b * b) - (4.0f * c);
if
(discriminant < 0.0f)
return
false
;
discriminant = sqrt(discriminant);
float
s0 = (-b + discriminant) / 2.0f;
float
s1 = (-b - discriminant) / 2.0f;
// if one solution is >= 0, then we intersected the sphere.
return
(s0 >= 0.0f || s1 >= 0.0f);
}
bool
setup()
{
D3DXCreateTeapot(g_device, &g_teapot, NULL);
// compute the bounding sphere
BYTE* v;
g_teapot->LockVertexBuffer(0, (
void
**)&v);
D3DXComputeBoundingSphere(
(D3DXVECTOR3*)v,
g_teapot->GetNumVertices(),
D3DXGetFVFVertexSize(g_teapot->GetFVF()),
&g_bounding_sphere.m_center,
&g_bounding_sphere.m_radius);
g_teapot->UnlockVertexBuffer();
// build a sphere mesh that describes the teapot's bounding sphere
D3DXCreateSphere(g_device, g_bounding_sphere.m_radius, 20, 20, &g_sphere, NULL);
// set light
D3DXVECTOR3 dir(0.707f, -0.0f, 0.707f);
D3DXCOLOR color(1.0f, 1.0f, 1.0f, 1.0f);
D3DLIGHT9 light = init_directional_light(&dir, &color);
g_device->SetLight(0, &light);
g_device->LightEnable(0, TRUE);
g_device->SetRenderState(D3DRS_NORMALIZENORMALS, TRUE);
g_device->SetRenderState(D3DRS_SPECULARENABLE, FALSE);
// Set view matrix
D3DXVECTOR3 pos(0.0f, 0.0f, -10.0f);
D3DXVECTOR3 target(0.0f, 0.0f, 0.0f);
D3DXVECTOR3 up(0.0f, 1.0f, 0.0f);
D3DXMATRIX view_matrix;
D3DXMatrixLookAtLH(&view_matrix, &pos, &target, &up);
g_device->SetTransform(D3DTS_VIEW, &view_matrix);
// set the projection matrix
D3DXMATRIX proj;
D3DXMatrixPerspectiveFovLH(&proj, D3DX_PI/4.0f, (
float
)WIDTH/HEIGHT, 1.0f, 1000.0f);
g_device->SetTransform(D3DTS_PROJECTION, &proj);
return
true
;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/
void
cleanup()
{
safe_release<ID3DXMesh*>(g_teapot);
safe_release<ID3DXMesh*>(g_sphere);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/
bool
display(
float
time_delta)
{
// update teapot
static
float
radius = 0.0f;
static
float
angle = 0.0f;
D3DXMatrixTranslation(&g_world_matrix, cos(angle) * radius, sin(angle) * radius, 10.0f);
// transform the bounding sphere to match the teapot's position in the world
g_bounding_sphere.m_center = D3DXVECTOR3(cos(angle) * radius, sin(angle) * radius, 10.0f);
static
float
velocity = 1.0f;
radius += velocity * time_delta;
if
(radius >= 8.0f || radius <= 0.0f)
velocity = -velocity;
// reverse direction
angle += D3DX_PI * time_delta;
if
(angle >= D3DX_PI * 2.0f)
angle = 0.0f;
// render now
g_device->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0x00000000, 1.0f, 0);
g_device->BeginScene();
g_device->SetTransform(D3DTS_WORLD, &g_world_matrix);
g_device->SetMaterial(&RED_MATERIAL);
g_teapot->DrawSubset(0);
// render the bounding sphere with alpha blending so we can see through it
g_device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
g_device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
g_device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
D3DMATERIAL9 yellow_material = YELLOW_MATERIAL;
yellow_material.Diffuse.a = 0.25f;
// 25% opacity
g_device->SetMaterial(&yellow_material);
g_sphere->DrawSubset(0);
g_device->SetRenderState(D3DRS_ALPHABLENDENABLE, FALSE);
g_device->EndScene();
g_device->Present(NULL, NULL, NULL, NULL);
return
true
;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/
LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM word_param, LPARAM long_param)
{
switch
(msg)
{
case
WM_DESTROY:
PostQuitMessage(0);
break
;
case
WM_KEYDOWN:
if
(word_param == VK_ESCAPE)
DestroyWindow(hwnd);
break
;
case
WM_LBUTTONDOWN:
// compute the ray in view space given by the clicked screen point
sRay ray = calculate_picking_ray(LOWORD(long_param), HIWORD(long_param));
// transform the ray to world space
D3DXMATRIX view_matrix, view_inverse_matrix;
g_device->GetTransform(D3DTS_VIEW, &view_matrix);
D3DXMatrixInverse(&view_inverse_matrix, NULL, &view_matrix);
transform_ray(&ray, &view_inverse_matrix);
if
(ray_sphere_intersect(&ray, &g_bounding_sphere))
MessageBox(NULL, "Hit teapot's bounding sphere!", "HIT", MB_OK);
break
;
}
return
DefWindowProc(hwnd, msg, word_param, long_param);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/
int
WINAPI WinMain(HINSTANCE inst, HINSTANCE, PSTR cmd_line,
int
cmd_show)
{
if
(! init_d3d(inst, WIDTH, HEIGHT,
true
, D3DDEVTYPE_HAL, &g_device))
{
MessageBox(NULL, "init_d3d() - failed.", 0, MB_OK);
return
0;
}
if
(! setup())
{
MessageBox(NULL, "Steup() - failed.", 0, MB_OK);
return
0;
}
enter_msg_loop(display);
cleanup();
g_device->Release();
return
0;
}