제 15 장 픽킹
픽킹 스크린 좌표를 이용해 선택한 물체를 알아내는 테크닉 원점에서 시작하여 p를 통과하도록 광선을 발사하면 p를 둘러싼 투영을 가지는 물체와 교차 픽킹 광선을 계산한 다음에는 장면 내의 각 물체를 대상으로 광선과 교차하는지를 테스트 광선과 교차하는 물체가 바로 사용자가 선택한 물체임 픽킹은 모든 종류의 게임과 3D 애플리케이션에 적용 2009-1학기 컴퓨터게임(DirectX)
(그림1) p를 통과하도록 발사된 광선은 p를 둘러싸는 물체와 교차 투영창, Direct3D는 평면 z=1에 일치하도록 정의하고 있다 투영의 중심 (그림1) p를 통과하도록 발사된 광선은 p를 둘러싸는 물체와 교차 2009-1학기 컴퓨터게임(DirectX)
픽킹 구현 방법 스크린 포인트 s를 통해 투영창에 대응되는 포인트 p를 얻는다. 픽킹 광선과 모델을 동일한 공간으로 변환한다. 픽킹 광선과 교차하는 모델을 알아낸다. 교차된 물체는 선택된 스크린 물체와 대응된다. 2009-1학기 컴퓨터게임(DirectX)
스크린에서 투영창으로의 변환 Height Height Height 2009-1학기 컴퓨터게임(DirectX)
2009-1학기 컴퓨터게임(DirectX)
2009-1학기 컴퓨터게임(DirectX)
픽킹 광선의 계산 광선의 위치를 나타내는 원점이 p0이고 방향을 나타내는 벡터가 u라 하면, 인수 방정식 p(t)=p0+tu를 통해 광선을 표현할 수 있음 광선의 원점은 또한 뷰 스페이스의 원점임을 알 수 있으므로 p0=(0, 0, 0) 방향벡터 u = p – p0 =(px, py, 1)-(0, 0, 0)=p로 얻을 수 있음 2009-1학기 컴퓨터게임(DirectX)
d3d::Ray CalcPickingRay(int x, int y) { float px = 0.0f; 다음의 메서드는 스크린 스페이스 상의 클릭된 포인트의 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); 2009-1학기 컴퓨터게임(DirectX)
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._origin = D3DXVECTOR3(0.0f, 0.0f, 0.0f); ray._direction = D3DXVECTOR3(px, py, 1.0f); return ray; } Where Ray is defined as: struct Ray { D3DXVECTOR3 _origin; D3DXVECTOR3 _direction; }; 2009-1학기 컴퓨터게임(DirectX)
광선의 변환 앞에서의 픽킹 광선은 뷰 스페이스에서 표현된 것이며, 광선-물체 교차 테스트를 위해서는 광선과 물체가 반드시 동일한 좌표 시스템 내에 위치해야 함 모든 물체를 뷰 스페이스로 변환하는 것보다는 월드 스페이스나 혹은 물체의 지역 스페이스로 픽킹 광선을 변환하는 것이 수월 변환 행렬을 이용해 광선 r(t)=p0+tu의 원점 p0과 방향 u를 변환할 수 있음. 다음과 같은 함수를 이용해 광선을 변환 2009-1학기 컴퓨터게임(DirectX)
void TransformRay(d3d::Ray* ray, D3DXMATRIX* T) { // transform the ray's origin, w = 1. D3DXVec3TransformCoord( &ray->_origin, T); // transform the ray's direction, w = 0. D3DXVec3TransformNormal( &ray->_direction, 2009-1학기 컴퓨터게임(DirectX)
// normalize the direction D3DXVec3Normalize(&ray->_direction, &ray->_direction); } 2009-1학기 컴퓨터게임(DirectX)
광선-물체 교차 동일한 좌표 시스템에 픽킹 광선과 물체가 준비되면 광선이 물체를 교차하는지를 테스트할 준비가 완료된 것 첫 번째 방법은 물체를 이루는 모든 삼각형들에 차례대로 광선 교차 테스트를 수행 하지만 장면의 모든 삼각형을 대상으로 교차 테스트를 수행한다는 것은 효율 면에서 그리 좋은 선택이 아님 다소 정확성이 떨어지기는 하지만 각 물체의 경계 구체를 이용하는 방법이 효율 면에서 좀더 유리. 2009-1학기 컴퓨터게임(DirectX)
(그림15.3) p-c로 구성된 벡터의 길이 ||p-c||는 구의 중심점 c와 반지름 r이 있다고 할 때 다음과 같은 암시적 방정식을 이용해 포인트 p를 테스트할 수 있음 ||p-c||-r=0 방정식이 만족되면 p가 구 내에 있음. +y c p-c p +x (그림15.3) p-c로 구성된 벡터의 길이 ||p-c||는 p가 구체 내에 있을 경우 구체의 반지름과 같음 2009-1학기 컴퓨터게임(DirectX)
광선과 구체의 교차 확인 광선 p(t) = p0 + tu가 구체와 교차하는지를 확인하기 위해서는 광선을 암시적 구방정식에 넣고 구 방정식을 만족시키는 인자 t를 풀어 교차점 (s)를 구해야 함 우선 구 방정식에 광선을 넣음 ||p(t) - c|| - r = 0 ||p0 + tu - c|| - r = 0 ||p0 + tu - c||2 – r2 = 0 u2 t2 + 2u(p0 – c)t + (p0 – c)2 – r2 = 0 2009-1학기 컴퓨터게임(DirectX)
A=u·u, B=2(u·(p0-c)), 그리고 C=(p0-c) ·(p0-c)-r2에서 u를 정규화할 경우 A=1임 여기에서 다음과 같은 2차 방정식을 얻음 At2+Bt+C=0 A=u·u, B=2(u·(p0-c)), 그리고 C=(p0-c) ·(p0-c)-r2에서 u를 정규화할 경우 A=1임 u가 정규화 되었다고 가정하면 t0과 t1을 풀어낼 수 있음 2009-1학기 컴퓨터게임(DirectX)
다음의 메서드는 전달된 광선이 전달된 구체를 교차할 경우 True를 리턴하며, 빗나갈 경우 False를 리턴 Bool PickApp::RaySphereIntersectionTest(Ray* ray, BoundingSphere* sphere) { D3DXVECTOR3 v = ray->_origin - sphere->_center; float b = 2.0f * D3DXVec3Dot(&ray->_direction, &v); float c = D3DXVec3Dot(&v, &v) - (sphere->_radius * sphere->_radius); // find the discriminant float discriminant = (b * b) - (4.0f * c); 2009-1학기 컴퓨터게임(DirectX)
// test for imaginary number if( discriminant < 0.0f ) return false; discriminant = sqrtf(discriminant); float s0 = (-b + discriminant) / 2.0f; float s1 = (-b - discriminant) / 2.0f; // if a solution is >= 0, then we intersected the sphere if( s0 >= 0.0f || s1 >= 0.0f ) return true; } 2009-1학기 컴퓨터게임(DirectX)
(a) (b) (c) (d) (e) 광선이 구체내부에 있음. 하나는 양수, 하나는 음수 광선이 구체와 접촉, t0=t1, 구체를 빗나감 광선이 구체내부에 있음. 하나는 양수, 하나는 음수 광선이 구체와 접촉, t0=t1, 해는 양수 광선이 구체를 교차 T0, t1 이 모두 양수 광선이 구체의 중심을 교차 t0, t1이 다 음수 2009-1학기 컴퓨터게임(DirectX)
예제 애플리케이션: 픽킹 주전자를 둘러싼 구를 마우스로 클릭할 수 있고, 만약 구체를 맞추면 이를 알려주는 메시지 상자가 열림 WM_LBUTTONDOWN 메시지를 이용해 마우스 클릭 이벤트를 처리 case WM_LBUTTONDOWN: // compute the ray in view space given the clicked screen point d3d::Ray ray = CalcPickingRay(LOWORD(lParam), HIWORD(lParam)); // transform the ray to world space D3DXMATRIX view; Device->GetTransform(D3DTS_VIEW, &view); 2009-1학기 컴퓨터게임(DirectX)
D3DXMATRIX viewInverse; D3DXMatrixInverse(&viewInverse, 0, &view); TransformRay(&ray, &viewInverse); // test for a hit if( RaySphereIntTest(&ray, &BSphere) ) ::MessageBox(0, "Hit!", "HIT", 0); break; 2009-1학기 컴퓨터게임(DirectX)
2009-1학기 컴퓨터게임(DirectX)
예제 프로그램: 픽킹 #include "d3dUtility.h" IDirect3DDevice9* Device = 0; const int Width = 640; const int Height = 480; ID3DXMesh* Teapot = 0; ID3DXMesh* Sphere = 0; D3DXMATRIX World; d3d::BoundingSphere BSphere; d3d::Ray CalcPickingRay(int x, int y) { float px = 0.0f; float py = 0.0f; 2009-1학기 컴퓨터게임(DirectX)
Device->GetViewport(&vp); D3DXMATRIX proj; 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._origin = D3DXVECTOR3(0.0f, 0.0f, 0.0f); ray._direction = D3DXVECTOR3(px, py, 1.0f); return ray; } 2009-1학기 컴퓨터게임(DirectX)
void TransformRay(d3d::Ray* ray, D3DXMATRIX* T) { // transform the ray's origin, w = 1. D3DXVec3TransformCoord( &ray->_origin, T); // transform the ray's direction, w = 0. D3DXVec3TransformNormal( &ray->_direction, // normalize the direction D3DXVec3Normalize(&ray->_direction, &ray->_direction); } 2009-1학기 컴퓨터게임(DirectX)
bool RaySphereIntTest(d3d::Ray* ray, d3d::BoundingSphere* sphere) { D3DXVECTOR3 v = ray->_origin - sphere->_center; float b = 2.0f * D3DXVec3Dot(&ray->_direction, &v); float c = D3DXVec3Dot(&v, &v) - (sphere->_radius * sphere->_radius); // find the discriminant float discriminant = (b * b) - (4.0f * c); // test for imaginary number if( discriminant < 0.0f ) return false; discriminant = sqrtf(discriminant); 2009-1학기 컴퓨터게임(DirectX)
float s0 = (-b + discriminant) / 2.0f; // if a solution is >= 0, then we intersected the sphere if( s0 >= 0.0f || s1 >= 0.0f ) return true; return false; } bool Setup() { D3DXCreateTeapot(Device, &Teapot, 0); BYTE* v = 0; Teapot->LockVertexBuffer(0, (void**)&v); 2009-1학기 컴퓨터게임(DirectX)
D3DXComputeBoundingSphere( (D3DXVECTOR3*)v, Teapot->GetNumVertices(), D3DXGetFVFVertexSize(Teapot->GetFVF()), &BSphere._center, &BSphere._radius); Teapot->UnlockVertexBuffer(); // Build a sphere mesh that describes the teapot's bounding sphere. D3DXCreateSphere(Device, BSphere._radius, 20, 20, &Sphere, 0); // Set light. D3DXVECTOR3 dir(0.707f, -0.0f, 0.707f); D3DXCOLOR col(1.0f, 1.0f, 1.0f, 1.0f); D3DLIGHT9 light = d3d::InitDirectionalLight(&dir, &col); 2009-1학기 컴퓨터게임(DirectX)
Device->SetLight(0, &light); Device->LightEnable(0, true); Device->SetRenderState(D3DRS_NORMALIZENORMALS, true); 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 V; D3DXMatrixLookAtLH(&V, &pos, &target, &up); Device->SetTransform(D3DTS_VIEW, &V); // Set projection matrix. D3DXMATRIX proj; 2009-1학기 컴퓨터게임(DirectX)
D3DXMatrixPerspectiveFovLH( &proj, D3DX_PI * 0.25f, // 45 - degree (float)Width / (float)Height, 1.0f, 1000.0f); Device->SetTransform(D3DTS_PROJECTION, &proj); return true; } void Cleanup() { d3d::Release<ID3DXMesh*>(Teapot); d3d::Release<ID3DXMesh*>(Sphere); 2009-1학기 컴퓨터게임(DirectX)
bool Display(float timeDelta) { if( Device ) // Update: Update Teapot. static float r = 0.0f; static float v = 1.0f; static float angle = 0.0f; D3DXMatrixTranslation(&World, cosf(angle) * r, sinf(angle) * r, 10.0f); // transfrom the bounding sphere to match the teapots position in the // world. BSphere._center = D3DXVECTOR3(cosf(angle)*r, sinf(angle)*r, 10.0f); 2009-1학기 컴퓨터게임(DirectX)
v = -v; // reverse direction if( r <= 0.0f ) r += v * timeDelta; if( r >= 8.0f ) v = -v; // reverse direction if( r <= 0.0f ) angle += 1.0f * D3DX_PI * timeDelta; if( angle >= D3DX_PI * 2.0f ) angle = 0.0f; // Render Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0); Device->BeginScene(); 2009-1학기 컴퓨터게임(DirectX)
Device->SetTransform(D3DTS_WORLD, &World); // Render the teapot. Device->SetTransform(D3DTS_WORLD, &World); Device->SetMaterial(&d3d::YELLOW_MTRL); Teapot->DrawSubset(0); // Render the bounding sphere with alpha blending so we can see // through it. Device->SetRenderState(D3DRS_ALPHABLENDENABLE, true); Device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA); Device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA); D3DMATERIAL9 blue = d3d::BLUE_MTRL; blue.Diffuse.a = 0.25f; // 25% opacity Device->SetMaterial(&blue); Sphere->DrawSubset(0); 2009-1학기 컴퓨터게임(DirectX)
Device->SetRenderState(D3DRS_ALPHABLENDENABLE, false); Device->EndScene(); Device->Present(0, 0, 0, 0); } return true; // WndProc LRESULT CALLBACK d3d::WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch( msg ) case WM_DESTROY: ::PostQuitMessage(0); break; 2009-1학기 컴퓨터게임(DirectX)
if( wParam == VK_ESCAPE ) ::DestroyWindow(hwnd); break; case WM_KEYDOWN: if( wParam == VK_ESCAPE ) ::DestroyWindow(hwnd); break; case WM_LBUTTONDOWN: // compute the ray in view space given the clicked screen point d3d::Ray ray = CalcPickingRay(LOWORD(lParam), HIWORD(lParam)); // transform the ray to world space D3DXMATRIX view; Device->GetTransform(D3DTS_VIEW, &view); D3DXMATRIX viewInverse; D3DXMatrixInverse(&viewInverse, 0, &view); 2009-1학기 컴퓨터게임(DirectX)
TransformRay(&ray, &viewInverse); // test for a hit if( RaySphereIntTest(&ray, &BSphere) ) ::MessageBox(0, "Hit!", "HIT", 0); break; } return ::DefWindowProc(hwnd, msg, wParam, lParam); // WinMain int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE prevInstance, PSTR cmdLine, int showCmd) { 2009-1학기 컴퓨터게임(DirectX)
if(!d3d::InitD3D(hinstance, Width, Height, true, D3DDEVTYPE_HAL, &Device)) { ::MessageBox(0, "InitD3D() - FAILED", 0, 0); return 0; } if(!Setup()) ::MessageBox(0, "Setup() - FAILED", 0, 0); d3d::EnterMsgLoop( Display ); Cleanup(); Device->Release(); 2009-1학기 컴퓨터게임(DirectX)