본문 바로가기
DirectX/[Inflearn_rookiss] Part2: DirectX12

32. Picking

by 헛둘이 2023. 2. 20.

RTS 게임을 보면 마우스로 물체를 클릭하면 그 물체가 선택된다.

그런데 실제로 물체가 존재하는 공간은 3D 공간이고 내가 클릭하는 공간은 마우스 공간인데, 

선택되었는지 어떻게 판별하지?

 

레이캐스팅이란?

- 이런 문제를 해결해주는 기술

- 이번 예제에서는 카메라 위치에서 레이를 발사해서 그 레이와 충돌여부를 검사한 후 그 결과로 클릭여부를 판별한다

- 레이캐스팅은 3D 공간에서 충돌처리를 할 때 많이 사용되는 기술인데,

- 지형을 이동할 때 플레이어 배꼽 위치에서 아래로 광선을 쏴서 지형이 있는지를 검사하고, 판별하게 된다.

- 내 앞에 광선을 쏴서 벽으로 막혔는지, 이동이 가능한지 확인하기도 한다.

(거리를 확인할 수 있는데, 거리가 N 이하라면 이동불가 로직을 짜면 될 듯)

 

레이와 마우스 클릭한 좌표가 만나려면 두 좌표가 같은 좌표공간 상에 있어야 한다

- 마우스가 클릭한 공간은 스크린 공간이고, 물체가 존재하는 공간은 월드 공간이기 때문에,

마우스 클릭 공간을 물체가 존재하는 공간으로 변환해야 하는데, 우리가 원래 했던 공간 변환을 역순으로 진행하면 된다.

 

원래 공간 변환

로컬 -> 월드 -> 뷰 -> 클립 -> 투영(NDC) -> 스크린 이었다면,

 

이번엔 반대로

스크린 -> 투영(NDC) -> 클립 -> 뷰 -> 월드로 변환한다.

 

스크린 공간에서 NDC 좌표로 넘어가려면 아래와 같은 공식을 사용해야 한다.

Xndc = 2x / w - 1

Yndc = -2y / h + 1

 

이 결과로 클릭한 x, y 좌표의 ndc 공간 좌표를 얻을 수 있는데,

여기서 view 좌표로 넘어가려면 아래의 과정이 필요하다

 

Xview = Xndc * 1/r tan(θ/2)

Yview = Yndc * 1/tan(θ/2)

 

이는 결국 Xndc를 r tan(θ/2)로 나누고, Yndc를 tan(θ/2)로 나눈다는 말과 동일한데,

r tan(θ/2), tan(θ/2)는 만들어뒀던 투영행렬의 (0,0)번째, (1,1)번째 요소와 동일하다.

 

따라서 아래와 같이 식이 구성된다.

float viewX = (+2.0f * screenX / width - 1.0f) / projectionMatrix(0, 0);
float viewY = (-2.0f * screenY / height + 1.0f) / projectionMatrix(1, 1);

 

 

이제 이 좌표를 이용해서 광선을 발사한 후 해당 물체와 충돌했는지 검출한다.

물체에 대한 충돌체는 보통 캡슐을 사용하지만 이번 예제에서는 구를 사용한다.

 

구의 방정식과 직선의 방정식을 혼합시켜서 두 방정식의 해가 존재하는지를 판별식을 통해 검사한다.

2차방정식의 해는 2개이거나, 1개이거나 없을 수 있는데, 해가 1개 이상일 경우 충돌했다고 판별할 수 있다.

 


레이와 충돌할 콜라이더 작성

 

- BaseCollider를 상속받는 구 콜라이더를 만들고, 구 콜라이더는 DX에서 제공하는 BoundingSphere 클래스를 보유한다.

- 크기와 중심점 정보를 들고 있게 해서 유동적으로 변경 가능토록 한다.

 

- 구와 레이가 충돌했는지를 검사하는 Intersects 의 구현부에 BoundingSphere의 멤버함수 Intersects를 호출해서

검사 결과를 반환한다.

 

- 구는 처음에 로컬 기준인데, FinalUpdate에서 자신의 GameObject의 Transform에서 월드좌표를 가져와서 중심점을 세팅한다. (매 프레임마다 적용)

 

- 그리고 스케일 X, Y, Z중 가장 큰 값으로 구의 스케일을 세팅한다.

 

CameraScript에서 우클릭 시 SceneManager의 Pick 함수를 호출하게 되고,

임시로 SceneManager에서 Pick 여부를 검사한다. (원래는 충돌 매니저를 따로 배치해야 한다)

 

shared_ptr<class GameObject> SceneManager::Pick(int32 screenX, int32 screenY)
{
	shared_ptr<Camera> camera = GetActiveScene()->GetMainCamera();

	float width = static_cast<float>(GEngine->GetWindow().width);
	float height = static_cast<float>(GEngine->GetWindow().height);

	Matrix projectionMatrix = camera->GetProjectionMatrix();

	float viewX = (+2.0f * screenX / width - 1.0f) / projectionMatrix(0, 0);
	float viewY = (-2.0f * screenY / height + 1.0f) / projectionMatrix(1, 1);

	Matrix viewMatrix = camera->GetViewMatrix();
	Matrix viewMatrixInv = viewMatrix.Invert();

	auto& gameObjects = GET_SINGLE(SceneManager)->GetActiveScene()->GetGameObjects();

	float minDistance = FLT_MAX;
	shared_ptr<GameObject> picked;

	for (auto& gameObject : gameObjects)
	{
		if (nullptr == gameObject->GetCollider())
			continue;

		// 이 코드는 밖으로 빼는게 더 좋을듯
		Vec4 rayOrigin = Vec4(0.f, 0.f, 0.f, 1.f); // 뷰스페이스에서 카메라의 위치
		Vec4 rayDir = Vec4(viewX, viewY, 1.0f, 0.0f); // 광선의 방향

		// 월드좌표로 레이를 이동시킨다
		rayOrigin = XMVector3TransformCoord(rayOrigin, viewMatrixInv);
		rayDir = XMVector3TransformNormal(rayDir, viewMatrixInv);
		rayDir.Normalize();

		// 충돌여부 검사
		float distance = 0.f;
		if (gameObject->GetCollider()->Intersects(rayOrigin, rayDir, OUT distance) == false)
			continue;


		if (distance < minDistance)
		{
			minDistance = distance;
			picked = gameObject;
		}
	}


	return picked;
}

- Pick에서는 현재 마우스의 좌표를 가져와서 물체와 같은 좌표공간상에 위치시킨 뒤에,

- 충돌여부를 검사해서 충돌된 경우, picked를 반환한다.

- 이걸 사용하는 부분에서 반환값을 체크해서 nullptr이 아니라면 별도의 처리를 해주는 식으로 활용할 수 있다.

 

 

 

'DirectX > [Inflearn_rookiss] Part2: DirectX12' 카테고리의 다른 글

34. Animation - 개념  (0) 2023.02.22
33. Mesh  (0) 2023.02.21
31. Terrain  (2) 2023.02.20
30. Tessellation  (0) 2023.02.19
29. Shadow Mapping  (0) 2023.02.18

댓글