DirectX

81_Terrain Picking

devRiripong 2024. 3. 29.
반응형

일반적인 물체 말고 땅을 깔았을 때 어떻게 해야할까?

Terrain 처럼 굉장히 큰 Grid를 깔아주는 경우가 있다.

땅을 찍었을 때 raycasting이 되어가지고, 클릭한 쪽으로 움직인다거나 이런 것도 만약에 언젠가 만들고 싶다 라고 하면 그것도 raycasting과 관련된 부분을 넣어줘야 한다.

그걸 어떻게 할까. 

 

1. CollisionDemo::Init에 Terrain 만들기

Terrain을 사용하고 싶으면 Mesh를 만들 때 일반적인 메쉬로 만드는게 아니라 Grid 방식으로 만들어야 한다.

CollisionDemo::Init에서

	// Terrain
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));		
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			auto mesh = make_shared<Mesh>(); 
			mesh->CreateGrid(10, 10); 
			obj->GetMeshRenderer()->SetMesh(mesh); 
			obj->GetMeshRenderer()->SetPass(0); 
		}
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar")); 
		}
		CUR_SCENE->Add(obj); 
	}

이렇게 일단은 땅이 만들어져 있다고 보면 된다.

 

실행을 하면

땅이 있을 뿐이지 뭔가를 해주지는 않고 있다.

클릭했을 때 생각할 수 있는 방법은 엄청나게 큰 콜라이더를 하나 붙여줘서 하면 되지 않을까 하는데 그것도 방식이 될 수 있지만 문제가 되는게 하이트맵이라고 산모양처럼 높고 낮고 그런 경우의 처리가 안된다. 나중에 가면 삼각형 단위로 좌표를 연산해서 그걸 이용해서 세분화해서 클릭이 되게 만들어 주는게 필요하다.

 

2. Terrain 클래스를 만들어 CollisionDemo::Init 에 적용하기

Terrain이란 클래스를 파주고, 나중에 Collider를 분리하는 건 나중 일이고, 일단 기능을 빼서 만들어 준다.

땅 위치를 조절하는 툴을 만들면 멋있는 땅도 만들 수 있다.이게 되려면 raycasting이 땅 위에도 되이야 한다.

Terrain을 일단 Component로 만들어 본다.

Engine/04. Component 필터에 Component를 상속받은 Terrain이란 클래를 만든다.

 

Comoponent.h의 ComponentType에

enum class ComponentType : uint8
{
	Transform,
	MeshRenderer,
	Camera,
	ModelRenderer,
	Animator,
	Light,
	Collider,
	Terrain,
	// ...
	Script, 

	End, 
};

Terrain을 추가한다.

 

그리고 GameObject.h에 가서

class Terrain; 

를 추가하고,

	shared_ptr<Terrain> GetTerrain(); 

헬퍼 함수를 파주고,

GameObject.cpp에

#include "Terrain.h"
shared_ptr<Terrain> GameObject::GetTerrain()
{
	shared_ptr<Component> component = GetFixedComponent(ComponentType::Terrain); 
	return static_pointer_cast<Terrain>(component);
}

GetTerrain을 정의한다.

 

다시 Terrain.cpp로 돌아가서 Terrain Comoponent에 CollisionDemo::Init 에서 //Terrain을 깔기 위해 해줬던 부분들을 이전을 시켜준다.

#include "MeshRenderer.h"

 추가한다.

 

#pragma once
#include "Component.h"
class Terrain : public Component
{
	using Super = Component;

public:
	Terrain();
	~Terrain();

	void Create(int32 sizeX, int32 sizeZ, shared_ptr<Material> material);

	int32 GetSizeX() { return _sizeX; }
	int32 GetSizeZ() { return _sizeZ; }

	bool Pick(int32 screenX, int32 screenY, Vec3& pickPos, float& distance);

private: 
	shared_ptr<Mesh> _mesh; 
	int32 _sizeX = 0; 
	int32 _sizeZ = 0; 
};

 

#include "pch.h"
#include "Terrain.h"
#include "MeshRenderer.h"

Terrain::Terrain() : Super(ComponentType::Terrain)
{
}

Terrain::~Terrain()
{
}

void Terrain::Create(int32 sizeX, int32 sizeZ, shared_ptr<Material> material)
{
	_sizeX = sizeX; 
	_sizeZ = sizeZ; 

	auto go = _gameObject.lock();  // weak_ptr는 관리하는 객체가 이미 소멸되었는지 여부를 확인할 수 있도록 하기 위해 lock()을 사용하여 shared_ptr로 변환해야 한다.

	go->GetOrAddTransform(); 

	if (go->GetMeshRenderer() == nullptr)
		go->AddComponent(make_shared<MeshRenderer>()); 

	_mesh = make_shared<Mesh>(); 
	_mesh->CreateGrid(sizeX, sizeZ); 

	go->GetMeshRenderer()->SetMesh(_mesh); 
	go->GetMeshRenderer()->SetPass(0); 
	go->GetMeshRenderer()->SetMaterial(material);
}

bool Terrain::Pick(int32 screenX, int32 screenY, Vec3& pickPos, float& distance)
{
	return false; 
}

 

CollisionDemo::Init에서 Terrain 클래스를 어떻게 사용할지 보면

	// Terrain
	{
		auto obj = make_shared<GameObject>();
		obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));		
		obj->AddComponent(make_shared<MeshRenderer>());
		{
			auto mesh = make_shared<Mesh>(); 
			mesh->CreateGrid(10, 10); 
			obj->GetMeshRenderer()->SetMesh(mesh); 
			obj->GetMeshRenderer()->SetPass(0); 
		}
		{
			obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar")); 
		}
		CUR_SCENE->Add(obj); 
	}

이 부분은 주석처리를 하고

	// Terrain
	{
		auto obj = make_shared<GameObject>(); 
		obj->AddComponent(make_shared<Terrain>()); 
		obj->GetTerrain()->Create(10, 10, RESOURCES->Get<Material>(L"Veigar")); 

		CUR_SCENE->Add(obj); 
	}
#include "Terrain.h"

이렇게 한다.

 

실행을 하면

실행하면 동일하게 정상적으로 Terrain이 복원이 되었다.

 

3. Terrain에 Picking 기능을 넣기

여기에 피킹 기능을 넣어 볼 것이다.

 

클릭했을 때 사라지거나 어떤 위치라는 걸 인지하게 만들어 준다.

 

Terrain::Pick으로 돌아간다.

지난 시간에 만들었던 Viewport의 Project, UnProject를 이용해서 만들어 볼 것이다.

#include "Camera.h"

를 하고,

 

Mesh.h에서

	shared_ptr<Geometry<VertexTextureNormalTangentData>> GetGeometry() { return _geometry; }

GetGeometry 함수를 추가한다.

 

void Mesh::CreateGrid(int32 sizeX, int32 sizeZ)
{
	_geometry = make_shared<Geometry<VertexTextureNormalTangentData>>();
	GeometryHelper::CreateGrid(_geometry, sizeX, sizeZ);
	CreateBuffers();
}

 

GeometryHelper::CreateGrid

	for (int32 z = 0; z < sizeZ; z++)
	{
		for (int32 x = 0; x < sizeX; x++)
		{
			//  [0]
			//   |	\
			//  [2] - [1]
			idx.push_back((sizeX + 1) * (z + 1) + (x));
			idx.push_back((sizeX + 1) * (z)+(x + 1));
			idx.push_back((sizeX + 1) * (z)+(x));
			//  [1] - [2]
			//      \  |
			//        [0]
			idx.push_back((sizeX + 1) * (z)+(x + 1));
			idx.push_back((sizeX + 1) * (z + 1) + (x));
			idx.push_back((sizeX + 1) * (z + 1) + (x + 1));
		}
	}

Mesh의 CreateGrid에서 호출되는GeometryHelper::CreateGrid를 보면 삼각형 2개를 만들고 그걸 연이어서 만들어서 모형을 만들어 줬다. 그걸 가져와서 삼각형마다 피킹하는 작업을 해줘야 한다. 삼각형이 나중에 가면 높이가 얼마나 될지 예측할 수 없기 때문에 plane으로 할 수 없고, 삼각형 단위로 연산을 해야 한다는 얘기가 된다.

그래서 피킹하는 코드를 Terrain::Pick에 갖고 온다.

 

GeometryHelper::CreateGrid를 보면 vertex buffer만 놓고 보면 0, 1, 2, 3 이렇게 4개가 있고, 순서를 맞춰준 것에 불과하다. 

 

bool Terrain::Pick(int32 screenX, int32 screenY, Vec3& pickPos, float& distance)
{
	Matrix W = GetTransform()->GetWorldMatrix(); 
	Matrix V = Camera::S_MatView; 
	Matrix P = Camera::S_MatProjection; 

	Viewport& vp = GRAPHICS->GetViewport(); 

	// 3D로 변환된 좌표
	Vec3 n = vp.Unproject(Vec3(screenX, screenY, 0), W, V, P); //깊이값이 0이면 near
	Vec3 f = vp.Unproject(Vec3(screenX, screenY, 1), W, V, P); //깊이값이 1이면 far

	Vec3 start = n; 
	Vec3 direction = f - n; 
	direction.Normalize(); 

	Ray ray = Ray(start, direction); 

	const auto& vertices = _mesh->GetGeometry()->GetVertices(); 

	for (int32 z = 0; z < _sizeZ; z++)
	{
		for (int32 x = 0; x < _sizeX; x++)
		{
			uint32 index[4]; 
			index[0] = (_sizeX + 1) * z + x; 
			index[1] = (_sizeX + 1) * z + x + 1; 
			index[2] = (_sizeX + 1) * (z + 1) + x; 
			index[3] = (_sizeX + 1) * (z + 1) + x + 1; 

			Vec3 p[4]; 
			for (int32 i = 0; i < 4; i++)
				p[i] = vertices[index[i]].position; 

			//  [0]
			//   |	\\
			//  [2] - [1]
			if (ray.Intersects(p[0], p[1], p[2], OUT distance))
			{
				pickPos = ray.position + ray.direction * distance; 
				return true; 
			}

			//  [2] - [3]
			//     \\  |
			//        [1]
			if (ray.Intersects(p[3], p[1], p[2], OUT distance)) // 순서는 상관 없다
			{
				pickPos = ray.position + ray.direction * distance;
				return true;
			}
		}
	}

	return false; 
}

terrain은 일반적인 충돌 박스와는 다르게 하고 있다 .

삼각형 단위로 테스트 하고 있기 때문에 Pick은 부하가 많이 걸린다.

 

4. Scene::Pick에 Terrain::Pick 적용하기

Scene::Pick 에서 체크를 할 때 Terrain도 넣는다.

#include "Terrain.h"

를 추가하고,

 

Scene::Pick에 이어서 코드를 넣는다면

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

		Vec3 pickPos;
		float distance = 0.f;
		if (gameObject->GetTerrain()->Pick(screenX, screenY, OUT pickPos, OUT distance) == false)
			continue;

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

	return picked; 
}

이렇게 picked를 갱신시켜 준다.

여기서 원래는 일반 물체들을 대상으로 하고 있었는데 Terrain 대상으론 다른 방식으로 Pick코드를 틀어 봤다.

중요한 건 어떤 방식으로건 누른 위치에 해당하는 ray를 만들어 쏠 수만 있으면 이런 식으로 다양한 방식으로 만들 수 있다는 얘기가 된다.

 

Engine을 빌드하고

실행을 해서 테스트를 한다.

이제 terrain도 클릭을 하면 사라진다.

삭제가 되었을 때 pickPos같은 거 추출해 보면 제대로 된 위치가 추출된다는 것을 확일할 수 있을 것이다.

이렇게 땅도 추출하는 방식을 연습해봤다.

 

5. 맺음말

만약 땅이 아니라 좀 복잡하게 생긴 Mesh랑 충돌을 해야 한다면 응용하면 된다. Mesh도 삼각형으로 이루어졌다 보니까 모든 삼각형을 갖고 와서 그 삼각형들끼리 연산을 해주는 것이다.

Mesh 자체에서 피격 판정을 한다면 원리는 Mesh의 삼각형을 하나씩 순회하면서 체크를 한다는 얘기가 된다.

 

포폴을 만들 때 일반적인 통맵을 이용해 만든다면 Mesh를 이용해서 충돌을 생각해야 한다.

Mesh한테 레이저를 쏴서 할 일이 많이 생기는데 대표적인 예로는 캐릭터가 바닥에 떠 있어야 하니까 뚫고 가지 않게 아래쪽으로 광선을 쏘고 있고, 카메라가 벽에 가리면 시야가 당겨 와야 하는데 카메라도 레이저를 쏴서 충돌 판정을 하고 있는 것이다.

 

통맵을 썼을 때 모든 메쉬에 레이저를 쏴도 괜찮을까? 삼각형이 너무 많아서 난리가 날 것이다. 생 Mesh를 쓰는게 아니라 충돌 판정을 위한 Mesh가 하나 더 있었다. Wow리소스를 보면 그렇게 되어 있는데 충돌 판정을 위한 Mesh는 러프하게 큼지막하게 되어 있다.

중요한 건 이 충돌 Mesh를 이용해서 레이저를 쏴서 raycasting을 한다는 건 굉장히 부하가 많이 걸린다는 걸 이해를 해야 한다. 일반적인 collision box를 사용 하거나 sphere 같은 걸 입히면 부하가 상대적으로 적은데 Mesh를 이용해서 하는 건 부하가 많기 때문에 보여주는 용도의 mesh 하나와 충돌을 담당하는 mesh를 따로 만들어서 관리한다가 결론이 된다.

 

 

Terrain에서 보여준 예제가 mesh도 마찬가지 겠지만 삼각형을 하나씩 스캔하는 것에서 크게 벗어나지 않는다.

만약에 어떤 mesh가 삼각형이 1만개라고 하면 1만번씩 설치를 한다는 것이다. 그러니 어지간하면 mesh를 생으로 하는 거는 조심해야 한다. 

 

 

Scene::CheckCollision()의 이 부분 처럼 코드를 무식하게 만들고 있다.

	// BruteForce
	for (int32 i = 0; i < colliders.size(); i++)
	{
		for (int32 j = i + 1; j < colliders.size(); j++)
		{
			shared_ptr<BaseCollider>& other = colliders[j]; 
			if (colliders[i]->Intersects(other))
			{
				int a = 3; 
			}
		}
	}

나중에 진지한 엔진을 만든다면 어떻게 만들 수 있을까?

 

레이어로 구분할 수 있을 것이다. 더 나아가 엔진에서 제공하긴 애매한데 공간 분할이 들어가서 공간에 따라서 어떤 물체들만 적용을 시킨다거나 이런 부분이 들어가면 좋다.

 

쿼드 트리가 됐건 해당 구역에 있는 애들만 어떻게든 연산하게끔 우리가 만들어줄 필요가 있다는 얘기가 된다. 인접한 애들만 해낼 수 있게 만든다거나 이런 게 필요하다. 그건 알고리즘 쪽으로 넘어가는 건데, 평소에 알고리즘 문제를 열심히 풀고, 쿼드트리 같은 거 적재적소에 자유자재로 사용할 수 있고, 최적화를 넣어주고 그런 거를 기술적으로 어필하는 것이다. 기술 소개소에서 뻔한걸 쓰는게 아니라 평소에 이런 걸 연구해서 넣어주는 게 중요하다. 단기간에 되는 게 아니다.

 

 

반응형

'DirectX' 카테고리의 다른 글

83_Point Test (수학)  (0) 2024.03.30
82_기본 도형 (수학)  (0) 2024.03.30
80_Picking  (0) 2024.03.28
79_Sphere Collider  (0) 2024.03.28
78_Viewport  (0) 2024.03.27

댓글