본문 바로가기

그래픽스

[DX12] Picking

스타크래프트에서 유닛을 화면을 클릭해서 선택할 수 있는데, 사실 이것은 굉장히 신기한 것이다. 왜냐하면 2차원 화면을 클릭하는데 3차원 공간의 물체를 선택할 수 있는 것이기 때문이다. 그래서 물체를 선택하기 위해 raycast를 이용한다.  

만약 중앙 지점을 클릭한다면 카메라 위치부터 중앙 지점까지 광선을 쏴서 물체에 히트 됐는지 체크한다.

 

 

그러기 위해서는 Screen부터 역으로 거슬러 올라가야 한다. Screen에서 Clip Space(NDC)로 바꾸려면 화면 해상도 크기 만한 화면을 (-1,-1) ~ (1,1)인 좌표로 바꿔야 한다. 위 그림의 식으로 바꿀 수 있다. (0,0) 집어 넣으면 (-1,1)이 나온다.

 

 

NDC에서 View로 바꾸는 것은 View에서 NDC로 바꾸는 것을 생각했을 때 역으로 곱하면 되는데 식은 그림과 같고 이것은 투영 행렬의 x,y의 위치에 있는 값으로 나누어도 된다. 

 

// ViewSpace에서 Picking 진행
	float viewX = (+2.0f * screenX / width - 1.0f) / projectionMatrix(0, 0);
	float viewY = (-2.0f * screenY / height + 1.0f) / projectionMatrix(1, 1);

 

이 둘을 곱하면 한번에 Screen에서 View로 변환할 수 있다. 코드로는 NDC로 변환한 것에 행렬의 x 위치 값과 y 위치 값으로 나누어 한번에 View 좌표계 기준 좌표를 구할 수 있다.

 

 

광선과 물체의 충돌처리는 물체는 보통 충돌처리를 위한 Collider을 가지고 있고 이번엔 Sphere Collider로 구와 광선의 충돌 처리를 해보겠다. 그림처럼 광선을 r 구의 중점을 c, 반지름을 d로 두고 구 위 임의의 점 p 자리에 벡터 r위 임의의 점 q+tu를 집어넣고 식을 유도하면 t에 대한 이차방정식을 만들 수 있다. 식의 근이 1개 이상이면 광선이 구와 충돌한 것이다.

코드에서는 Intersect라는 함수를 이용할 것이다. 

 

shared_ptr<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();

	// ViewSpace에서 Picking 진행
	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 (gameObject->GetCollider() == nullptr)
			continue;

		// ViewSpace에서의 Ray 정의
		Vec4 rayOrigin = Vec4(0.0f, 0.0f, 0.0f, 1.0f);
		Vec4 rayDir = Vec4(viewX, viewY, 1.0f, 0.0f);

		// WorldSpace에서의 Ray 정의
		rayOrigin = XMVector3TransformCoord(rayOrigin, viewMatrixInv);
		rayDir = XMVector3TransformNormal(rayDir, viewMatrixInv);
		rayDir.Normalize();

		// WorldSpace에서 연산
		float distance = 0.f;
		if (gameObject->GetCollider()->Intersects(rayOrigin, rayDir, OUT distance) == false)
			continue;

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

	return picked;
}

 

raycast 계산은 local, world, view 어느 좌표에서 하든 상관이 없는데 여기서는 Collider을 만들 때 World Space 기준으로 만들었으므로 World 기준으로  계산을 했다. 

'그래픽스' 카테고리의 다른 글

[그래픽스] Kernel, Convolution, Gaussian Blur  (0) 2023.05.10
[DX12] Animation  (0) 2022.08.01
[DX12] Terrain  (0) 2022.07.31
[DX12] Tessellation  (0) 2022.07.31
[DX12] Shadow Mapping  (0) 2022.07.30