본문 바로가기

DirectX 기반 그래픽스/그래픽스 이론

[DirectX] 픽킹, 교차 판정 - 경계 구체와 반직선(광선)

목표

  • 픽킹 광선을 이용한 모델 픽킹 방법의 이해

들어가기 전 

본 글에서 직접적으로 설명하지 않지만, 언급하고 있는 내용입니다. 

  • 뷰포트 변환
  • 아핀공간
  • 역행렬
  • 이차방정식과 근의 공식
  • 광선

픽킹


픽킹이란?

사용자가 마우스로 클릭한 화면상의 위치와 대응되는 3D 물체를 가려내는 기술

 

픽킹은 원하는 위치를 얻어낼 때도 사용된다.

픽킹 광선은 뷰스페이스의 원점에서 시작하여 클릭된 스크린 포인트와 대응되는 투영창의 포인트를 통과한다.

투영창 = NDC 좌표계

픽킹을 하기 위한 절차 4가지

1. 마우스로 클릭한 스크린 포인트 s를 역변환하여 투영창에 대응되는 포인트 p를 얻는다.

2. 뷰스페이스 상의 원점에서 출발하여 p를 통과하여 발사되는 광선을 계산한다.

3. 픽킹 광선과 모델을 동일한 공간으로 변환한다.

4. 픽킹 광선과 교차하는 물체를 알아낸다. 교차된 물체는 선택된 스크린 물체와 대응된다.

 


1. 클릭한 스크린 상의 좌표를 투영창으로 변환

스크린 좌표를 투영 상의 좌표로 얻는 식 (뷰포트 변환식 참고)

X, Y는 사용자가 지정한 뷰포트 시작 위치 (대부분 (0, 0))

Width, Height는 사용자가 지정한 뷰포트 크기 (일반적으로 스크린 크기와 동일함.)

p는 투영창의 포인트

 

대부분의 경우 뷰포트의 시작 위치는 0이므로, 사용자가 지정한 시작 위치 X와 Y가 0이라고 가정하면 아래와 같다.

근본적인 정의상 DirectX3D의 투영 윈도우(NDC)는 평면 z=1과 일치한다.

 

뷰포트 역변환 식

투영 x: (2 * 스크린 공간 좌표 x / 뷰포트 너비) - 1.0f; // 스크린 x / (뷰포트 너비 * 0.5f) - 1.f;
투영 y: -(2 * 스크린 공간 좌표 y / 뷰포트 높이) + 1.0f; // 스크린 y / -(뷰포트 높이 * 0.5f) + 1.f;
투영 z: 1 // 보통 투영창의 z평면은 1로 정의한다.

2. 픽킹 광선 계산

광선의 시작점은 뷰 스페이스의 원점 $p_{0}(0, 0, 0)$이다.

픽킹 광선은 방향벡터인 $p - p_{0}$로 얻을 수 있다. (뷰 스페이스의 픽킹 광선 얻음)

POINT ptMouse{};
GetCursorPos(&ptMouse);
ScreenToClient(g_hWnd, &ptMouse); // 스크린 -> 뷰포트

// 뷰포트 -> 투영
_vector vMousePos = XMVectorSet(ptMouse.x / (g_iWinSizeX * 0.5f) - 1.f, ptMouse.y / -(g_iWinSizeY * 0.5f) + 1.f, 0.f, 1.f);

// 투영 위치벡터 * 투영 역행렬 = 뷰스페이스 위치벡터
_matrix	ProjMatrix = pGameInstance->Get_Transform_Matrix(CPipeLine::D3DTS_PROJ);
ProjMatrix = XMMatrixInverse(nullptr, ProjMatrix); // 뷰 스페이스가 돼써
vMousePos = XMVector3TransformCoord(XMVectorSet(XMVectorGetX(vMousePos), XMVectorGetY(vMousePos), 0.f, 1.f), ProjMatrix);

// 뷰스페이스상의 광선 계산
_vector		vRayPos, vRayDir;
vRayPos = { 0.f, 0.f, 0.f };
vRayDir = vMousePos - vRayPos; // 광선은 뷰스 좌표

3. 광선을 모델과 동일한 공간으로 변환

위의 계산을 통해 얻은 광선은 뷰 스페이스에서 표현된 것이다.

광선-물체 교차를 위해서 광선과 물체는 반드시 동일한 좌표 시스템 내에 위치해야 하므로

광선을 변환 행렬을 사용하여 광선을 월드 또는 지역 스페이스로 변환한다.

변환 행렬

void TransformRay(d3d::Ray* ray , D3DXMATRIX* T) 

    1. 점 변환에 사용하는 함수 // w = 1로 인식
    D3DXVec3TransformCoord( 
        &ray->_origin, 
        &ray->_origin, 
        T) ; 
    2. 벡터 변환에 사용하는 함수 // w = 0로 인식
    D3DXVec3TransformNormal( 
        &ray-> direction, 
        &ray-> direction, 
        T) ; 
    3. 방향을 정규화한다. 
    D3DXVec3Normalize(&ray->_direction, &ray->_direction); 
}

※ 아핀공간에 따르면 3차원 공간에서 w = 1인 공간은 이동이 가능한 공간 (점들로 구성)

      w = 0인 공간은 회전, 크기 변환이 가능한 공간 (벡터로 구성) 그 외는 아핀 공간을 벗어나므로 존재할 수 없음.

광선의 공간을 변환하는 예제

// 뷰스페이스 상의 광선
vRayPos = { 0.f, 0.f, 0.f };
vRayDir = vMousePos - vRayPos;

// 뷰스페이스 광선 * 뷰스페이스 역행렬 = 월드 스페이스 광선
D3DXVec3TransformCoord(&vRayPos, &vRayPos, &matInverseView); //행렬 * 위치벡터(광선의 원점)
D3DXVec3TransformNormal(&vRayDir, &vRayDir, &matInverseView); //행렬 * 방향벡터(광선의 방향)

4. 광선과 물체 교차

광선과 물체를 교차하는 방법은 두 가지가 있다.

삼각형 단위의 연산은 너무나 많은 시간을 소요하기 때문에 경계 구체를 이용한 방법이 효율 면에서 유리하다.

  • 오브젝트가 삼각형 메쉬로 이루어져 있으므로 모든 셀에 대해 차례대로 광선 교차 테스트 수행
  • 경계 구체를 이용하여 경계 구체와 광선 교차 시 해당 오브젝트를 선택한 것으로 처리 (정확성 ↓ 효율성 ↑)

※ 경계 입체: 삼각형들의 조합으로 이루어진 메시를 감싸는 3D 입체 (경계 구, 경체 상자, 경계 실린더)

4.1. 모든 셀에 대해 광선 교차 검사하여 오브젝트 선택하는 방법

Intersects 함수를 이용하여 광선의 원점 및 방향과 셀의 세 정점 정보를 넘겨서 교차 검사를 진행한다.

// namespace TriangleTests
inline bool XM_CALLCONV Intersects(
   FXMVECTOR Origin, FXMVECTOR Direction // 광선 원점, 광선 방향
   , FXMVECTOR V0,GXMVECTOR V1, HXMVECTOR V2 // 셀의 세 정점(위치)
   , float& Dist) noexcept // 픽킹된 셀과 광선 원점간의 거리(Out)
   
// D3DX의 경우
D3DXIntersectTri(&v0, &v1, &v2, &newRay.origin_, &newRay.direction_, &u, &v, &t);

4.2. 경계 구체를 이용하여 오브젝트 선택하는 방법

 

4.2. 경계 구체를 이용하여 오브젝트 선택하는 방법

경계 구체와 광선의 교차 검사는 광선의 방정식과 구의 방정식을 전개하여 이차 방정식을 만든 뒤,

근의 공식을 이용하여 교차 검사를 진행한다. 근의 공식을 통해 얻은 해(t)가 하나라도 양수이면 교차한 것으로 간주한다.

아래는 구의 방정식을 만족시키는 인자 t를 풀어 교차점 (s)를 구하는 과정이다.

광선의 방정식

광선의 방정식

 구의 방정식 (포인트 p가 구 표면에 있는지 확인하는 용도)

암시적 구 방정식

구의 방정식에 광선을 대입하고, 2차 방정식으로 전개

구 방정식에 광선을 대입한다.

 

식을 전개하여 2차 방정식을 얻는다.

이차 방정식
2차 방정식 형태에 맞게 전개한 형태

더보기

전개 과정

내적을 사용하여 제곱 전개

정리하면

표준 2차 방정식 형태에 맞춰 모든 항을 한쪽으로 옮기기

 

A, B, C를 얻을 수 있음. 추후 이차 방정식의 근의 공식을 이용해서 해를 구함.

 

 

※2차방정식: $ax^2 + bx + c = 0, (a ≠ 0)$

근의 공식을 이용하여 해를 구하기

근의 공식

 

u를 정규화할 경우 A = 1이므로 해는 다음과 같이 나올 수 있다.

 

해(t)가 하나라도 양수이면 광선과 구체는 교차한다.

두 가지 해(t)에 따라 가능한 결과와 이 결과의 기하학적 의미

 

광선과 구체 교차 검사 코드 예제

광선이 구체를 교차할 경우 True, 빗나갈 경우 False를 반환한다.

bool CheckRaySphereIntersection(Ray* pRay, BoundingSphere* pSphere)
{
    XMVECTOR vRayPos = pRay->GetOrigin();
    XMVECTOR vRayDir = pRay->GetDirection();
    XMVECTOR vLook = vRayPos - pSphere->GetCenter();
    
    /** 구 방정식에 광선을 대입하여 2차 방정식 얻기 (At^2 + Bt + C = 0) */
    // a는 광선을 정규화할 경우, 1이므로 생략함.
    float b = 2.f * XMVector3Dot(vRayDir, vLook); 
    float c = XMVector3Dot(v, v) ? (pSphere->GetRadius() * pSphere->GetRadius()); // 거짓일 경우 할당X
    
    /** 근의 공식으로 해 구하기 */
    // 판별식을 찾는다.
    float discriminant = (b * b) - (4.f * c);
    
    // 가상의 수에 대한 테스트
    if(0.f < discriminant)
    	return false;
        
    discriminant = sqrtf(discriminant);
    
    float t0 = (-b + discriminant) / 2.f;
    float t1 = (-b - discriminant) / 2.f;
    
    // 해가 하나라도 0 이상일 경우 구체는 교차한다.
    if(0.f <= t0 || 0.f <= t1)
    	return true;
        
    return false;
}

 

 

전체 코드 예제

더보기
<Engine::Calculator>
- 핸들은 클라에서 생성하므로 엔진은 받아서 사용해야 함
- GetCursorPos로 받는 마우스 좌표는 뷰 포트
POINT ptMouse{};

GetCursorPos(&ptMouse);
ScreenToClient(hWnd, &ptMouse);

_vec3	vMousePos;

1. 뷰포트 가져옴
D3DVIEWPORT9	ViewPort;
ZeroMemory(&ViewPort, sizeof(D3DVIEWPORT9));
m_pGraphicDev->GetViewport(&ViewPort);

2. 뷰포트 -> 투영
vMousePos.x = ptMouse.x / (ViewPort.Width * 0.5f) - 1.f;
vMousePos.y = ptMouse.y / -(ViewPort.Height * 0.5f) + 1.f;
vMousePos.z = 0.f;

3. 투영행렬을 얻어온다.
_matrix		matProj;
m_pGraphicDev->GetTransform(D3DTS_PROJECTION, &matProj);
4. 투영 -> 뷰스페이스 (역행렬로 구함)
D3DXMatrixInverse(&matProj, 0, &matProj);
5. 벡터 * 행렬 곱하기 (투영 마우스 -> 뷰스페이스 마우스)
D3DXVec3TransformCoord(&vMousePos, &vMousePos, &matProj);

_vec3   vRayPos, vRayDir;

6. 광선의 위치, 방향 초기화
vRayPos = { 0.f, 0.f, 0.f };
vRayDir = vMousePos - vRayPos;

7. 뷰스페이스 -> 월드
// 하지만 직교투영은 뷰행렬은 항등행렬이기 때문에 굳이 안해줘도 되지만
// 혹시 모르니 변환해준다.
_matrix		matView;
m_pGraphicDev->GetTransform(D3DTS_VIEW, &matView);
D3DXMatrixInverse(&matView, 0, &matView);

9. 행렬 * 위치벡터
D3DXVec3TransformCoord(&vRayPos, &vRayPos, &matView);
10. 행렬 * 방향벡터
D3DXVec3TransformNormal(&vRayDir, &vRayDir, &matView);

// 월드까지만 해줘도 되긴 함
11. 월드 -> 로컬 (역행렬로 구하기)
_matrix		matWolrd;
CGameObject* pObj = nullptr;
CTransform* pTransCom = CGameObject::World->Get_Component<CTransform>(pObj);
pTransCom->Get_WorldMatrix(&matWolrd);
D3DXMatrixInverse(&matWolrd, 0, &matWolrd);
12. 행렬 * 위치벡터
D3DXVec3TransformCoord(&vRayPos, &vRayPos, &matWolrd);
13. 행렬 * 방향벡터
D3DXVec3TransformNormal(&vRayDir, &vRayDir, &matWolrd);


CUITex* pUITex = CGameObject::World->Get_Component<CUITex>(pObj);

const UIVTXTEX* pVtxUITex = pUITex->Get_VtxTexUIBuffer();
const INDEX32* pIndex = pUITex->Get_IndexUIBuffer();

if (pVtxUITex == nullptr || pIndex == nullptr)
	return nullptr;

_float  fU = 0.f, fV = 0.f, fDist = 0.f;

for (size_t i = 0; i < 2; ++i)
{
	if (D3DXIntersectTri(&pVtxUITex[pIndex[i]._1].vPos,
		&pVtxUITex[pIndex[i]._2].vPos,
		&pVtxUITex[pIndex[i]._0].vPos,
		&vRayPos,
		&vRayDir,
		&fU, &fV, &fDist))
	{
		//*_v = p0 + (fu * (p1 - p0)) + (fv * (p2 - p0));
		*_v = pVtxUITex[pIndex[i]._1].vPos +
			(fU * (pVtxUITex[pIndex[i]._2].vPos - pVtxUITex[pIndex[i]._1].vPos))
			+ (fV * (pVtxUITex[pIndex[i]._0].vPos - pVtxUITex[pIndex[i]._1].vPos));
		return true;
	}
}
 
참고문헌

 

[1] Frank D. Luna, 최현호 역, DirectX 9를 이용한 3D 게임 프로그래밍 입문, 정보문화사, 2008, pp.347-355.

[2] 김용준, 3D 게임 프로그래밍 개정판, 한빛미디어, 2010, p.784.