코승호딩의 메모장

[Camera] 본문

DirectX12/DirectX12 응용

[Camera]

코승호딩 2023. 10. 9. 15:37

이전까지는 오브젝트당 상수버퍼에 간단한 Vec4 형식의 offset을 넘겨줘서 정점 쉐이더에서 입력 정점에 offset을 더하기만 하고 따로 행렬을 사용하여 MVP 변환을 하지 않았다. 이번 글에서는 카메라 컴포넌트를 생성하여 오브젝트당 상수버퍼에 MVP 행렬을 넘겨줘서 정점 버퍼에 알맞은 행렬변환을 수행하도록 구현한다. 하지만 한 가지 유의할 점은 월드 변환은 오브젝트당 수행이 일어나지만 카메라 변환, 투영 변환은 오브젝트 당 일어나는 것이 아닌 프레임 당 변환이 일어난다. 따라서 원래는 월드 변환만 오브젝트 당 상수 버퍼에 넘겨줘야 하지만 이번 글에서는 간단하게 MVP 변환을 모두 넘겨주고 다음 글에서 패스(프레임) 당 상수버퍼를 새로 만들어서 따로 정보를 넘겨주도록 하겠다. 더 나아가 카메라를 두 개 두고 메인 카메라를 변경해 가며 카메라를 변경해 가며 사용할 수 있도록 구현한다.


 

SimpleMath

The DirectX Tool Kit (aka DirectXTK) is a collection of helper classes for writing DirectX 11.x code in C++ - microsoft/DirectXTK

github.com

우선 위 깃허브 사이트에서 SimpleMath를 다운로드하여서 h, cpp, inl 파일을 엔진 프로젝트에 추가하도록 하자. SimpleMath는 DirectXMath에서 사용할 수 있는 연산을 쉽게 사용하기 위해서 XMFLOAT 형식을 상속받아 편의 함수들을 추가한 것이다.

 

우선 현재까지 만든 씬의 라이프 사이클에 FinalUpdate를 추가한다. 맨 마지막에 카메라 계산을 하기 위함이다. 따라서 카메라 컴포넌트는 FinalUpdate 부분에서 뷰 변환과 투영 변환 행렬을 업데이트하게 된다. 그다음 변경해야 할 점은 기존 렌더링 함수는 MeshRenderer 클래스에서 Update 가상 함수에 Render 함수를 따로 생성하고 호출하는 방식이었지만 이제부터는 카메라가 추가되기 때문에 메인 카메라에서 게임 오브젝트를 받아 렌더를 진행하도록 변경한다. 

 

Camera Component

enum class PROJECTION_TYPE
{
	PERSPECTIVE, // 원근 투영
	ORTHOGRAPHIC, // 직교 투영
};

class Camera : public Component
{
public:
	virtual void FinalUpdate() override;
	void Render();

private:
	PROJECTION_TYPE _type = PROJECTION_TYPE::PERSPECTIVE;

	float _near = 1.f;
	float _far = 1000.f;
	float _fov = XM_PI / 4.f;
	float _scale = 1.f;

	Matrix _matView = {};
	Matrix _matProjection = {};

public:
	static Matrix S_MatView;
	static Matrix S_MatProjection;
};

우선 카메라의 투영 타입에 따라 원근, 직교로 나누고 FinalUpdate 부분에서 각각에 맞게 관련 함수를 호출하도록 한다. 각각의 카메라는 자신의 뷰 행렬과 프로젝션 행렬을 가지고 있으며 카메라마다 공유하는 정적 뷰, 프로젝션 행렬을 갖도록 한다. 이유는 정적으로 선언하여 다른 코드에서도 가져다 사용하기 쉽도록 한 것이다.

 

void Camera::FinalUpdate()
{
	_matView = GetTransform()->GetWorldMatrix().Invert();

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

	if (_type == PROJECTION_TYPE::PERSPECTIVE)
		_matProjection = ::XMMatrixPerspectiveFovLH(_fov, width / height, _near, _far);
	else
		_matProjection = ::XMMatrixOrthographicLH(width * _scale, height * _scale, _near, _far);

	S_MatView = _matView;
	S_MatProjection = _matProjection;
}

다음과 같이 뷰 행렬에 카메라 자신의 월드 변환의 역행렬을 저장해 준다. 뷰 행렬이 카메라의 월드 행렬의 역행렬인 것은 다들 아는 사실일 것이다. 그리고 투영 타입에 따라 프로젝션 행렬을 생성하고 해당 카메라의 행렬을 정적 행렬에 저장한다.

 

void Camera::Render()
{
	shared_ptr<Scene> scene = GET_SINGLE(SceneManager)->GetActiveScene();

	const vector<shared_ptr<GameObject>>& gameObjects = scene->GetGameObjects();

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

		gameObject->GetMeshRenderer()->Render();
	}
}

마지막으로 앞서 말했듯이 렌더 함수를 카메라가 갖도록 변경하고 씬에서 게임 오브젝트를 가져와서 렌더링 한다.


Transform Component

class Transform : public Component
{
public:
	virtual void FinalUpdate() override;
	void PushData();

public:
	// Parent 기준 Get, Set 함수들

public:
	void SetParent(shared_ptr<Transform> parent) { _parent = parent; }
	weak_ptr<Transform> GetParent() { return _parent; }

private:
	Vec3 _localPosition = {};
	Vec3 _localRotation = {};
	Vec3 _localScale = { 1.f, 1.f, 1.f };

	Matrix _matLocal= {};
	Matrix _matWorld = {};

	weak_ptr<Transform> _parent;
};

이전까지 Transform 컴포넌트는 딱히 기능을 하지 않았지만 앞으로 행렬 변환을 사용할 것이기 때문에 온갖 필요한 Get, Set 함수들을 만들어준다. 그리고 Transform은 자신의 부모로부터 행렬 변환이 수행되어야 하므로 부모의 컴포넌트도 들고 있도록 한다.

 

void Transform::FinalUpdate()
{
	Matrix matScale = Matrix::CreateScale(_localScale);
	Matrix matRotation = Matrix::CreateRotationX(_localRotation.x);
	matRotation *= Matrix::CreateRotationY(_localRotation.y);
	matRotation *= Matrix::CreateRotationZ(_localRotation.z);
	Matrix matTranslation = Matrix::CreateTranslation(_localPosition);

	_matLocal = matScale * matRotation * matTranslation;
	_matWorld = _matLocal;

	shared_ptr<Transform> parent = GetParent().lock();
	if (parent != nullptr)
	{
		_matWorld *= parent->GetWorldMatrix();
	}
}

void Transform::PushData()
{
	Matrix matWVP = _matWorld * Camera::S_MatView * Camera::S_MatProjection;
	CONST_BUFFER(CONSTANT_BUFFER_TYPE::OBJECT)->PushData(&matWVP, sizeof(matWVP));
}

그리고 내부 변수로 SRT 정보를 가지고 있기 때문에 SRT 정보가 변경되었다면 FinalUpdate에서 새로운 행렬을 생성하여 월드 변환 행렬을 계산한다. 꼭 SRT 순서대로 해줘야 정상적으로 출력이 된다. 그리고 부모가 있다면 자신의 행렬에 부모의 월드 변환 행렬을 곱해줌으로 기준을 부모로 둔다. 이제 각각의 Transform 컴포넌트를 사용하기 때문에 자신의 행렬 정보에 카메라, 프로젝션 변환을 수행하고 오브젝트 당 상수 버퍼에 복사해 준다.


Render

void SceneManager::Render()
{
	if (_activeScene == nullptr)
		return;

	const vector<shared_ptr<GameObject>>& gameObjects = _activeScene->GetGameObjects();
	for (auto& gameObject : gameObjects)
	{
		if (gameObject->GetCamera() == nullptr)
			continue;

		gameObject->GetCamera()->Render();
	}
}

앞서 설명했듯이 렌더 함수를 카메라 컴포넌트로 변경했기 때문에 씬 매니저에 새로운 렌더링 함수를 생성하고 내부에서 게임 오브젝트 중 카메라를 찾았다면 카메라 객체를 렌더링 하도록 변경하였다.

 

void Engine::Update()
{
	//...
	GET_SINGLE(SceneManager)->Update();
	Render();
}

void Engine::Render()
{
	RenderBegin();
	GET_SINGLE(SceneManager)->Render();
	RenderEnd();
}

이제 엔진에서 씬 매니저를 업데이트하여 현재 활성화된 activeScene을 Update, LateUpdate, FinalUpdate를 진행한다. 그리고 렌더 함수에서 카메라를 찾아 렌더링을 진행하는 씬 매니저의 렌더 함수를 호출하도록 한다.


Camera Script

다음으로 카메라를 키 입력에 따라 움직일 수 있도록 카메라 스크립트를 생성한다. 카메라의 이동은 LateUpdate에서 진행해야 한다. 왜냐하면 FinalUpdate에서는 변경된 내부 SRT 정보를 가지고 행렬 변환을 수행하는 단계이기 때문에 이전 LateUpdate에서 SRT 정보를 변경해야 하기 때문이다.

 

class TestCameraScript : public MonoBehaviour
{
public:
	virtual void LateUpdate() override;

private:
	float		_translationSpeed = 200.f;
	float		_rotationSpeed = 2.f;
};

스크립트이기 때문에 MonoBehaviour을 상속받도록 하고 내부적으로 카메라 이동, 회전 속도를 위한 변수를 넣어준다. 

 

void TestCameraScript::LateUpdate()
{
	Vec3 pos = GetTransform()->GetLocalPosition();

	if (KEY_PRESSED('W'))
		pos += GetTransform()->GetLook() * _translationSpeed * DELTA_TIME;
	//...

	if (KEY_PRESSED(VK_LEFT))
	{
		Vec3 rotation = GetTransform()->GetLocalRotation();
		rotation.y -= _rotationSpeed * DELTA_TIME;
		GetTransform()->SetLocalRotation(rotation);
	}
	//...
    
	GetTransform()->SetLocalPosition(pos);
}

LateUpdate에서 해당 스크립트가 물고 있는 오브젝트의 포지션 값을 가져와서 이동 변환 행렬을 수행하고 로테이션 값을 가져와서 회전 변환 행렬을 수행하여 해당 오브젝트의 포지션을 업데이트한다.

 

struct ObjectConstants
{
    row_major matrix gMatWVP;
};

VS_OUT VS_Main(VS_IN input)
{
    VS_OUT output = (VS_OUT)0;

    output.pos = mul(float4(input.pos, 1.f), gObjConstants.gMatWVP);
    output.color = input.color;
    output.uv = input.uv;

    return output;
}

이제 상수 버퍼의 내용도 바뀌었으니 당연히 쉐이더 코드도 변경되어야 한다. 다들 알고 있듯이 float3 벡터의 마지막 w 요소에 1을 추가해야 이동 변환 행렬을 수행할 수 있으므로 추가하여 뒤에 MVP 변환 행렬을 곱해준다.

 

결과적으로 다음과 같이 키 입력에 따라 뉴진스 사진을 자유롭게 이동할 수 있게 되었다.

그러나 아쉬운 점은 카메라가 한 개라는 점이다. 따라서 살짝 수정하여 카메라를 두 개 쓰도록 변경해 보자.


Multiple Camera

enum NUMBER_CAMERA
{
	FIRST_CAMERA,
	SECOND_CAMERA,
	THIRD_CAMERA,
	FOURTH_CAMERA,
	FIFTH_CAMERA,

	CAMERA_COUNT,
};

class Scene
{
public:
	//...
	void AddGameObject(sptr<GameObject> gameObject);
	void AddCameraObject(NUMBER_CAMERA number, sptr<GameObject> gameObject);

	void RemoveGameObject(sptr<GameObject> gameObject);

	const vector<sptr<GameObject>>& GetGameObjects() { return _gameObjects; }

	void SetMainCamera(NUMBER_CAMERA cameraNum) { _mainCamera = _cameraObjects[cameraNum]; }
	const sptr<GameObject>& GetMainCamera() { return _mainCamera; }
    
private:
	vector<sptr<GameObject>> _gameObjects;
	
	array<sptr<GameObject>, CAMERA_COUNT> _cameraObjects;
	sptr<GameObject> _mainCamera;
};

이전에는 카메라를 씬의 멤버 변수 _gameObjects에서 함께 관리하였다. 그러나 카메라와 다른 게임 오브젝트를 따로 관리하는 것이 편리하다 생각하여 최대 5개까지 가지는 카메라 배열을 생성하였으며 현재 어떠한 카메라가 렌더링 되는지 메인 카메라도 만들어줬다. 총 5개 중 메인 카메라를 설정할 때는 그냥 카메라 배열에서 해당 카메라 순서를 넣어주면 된다. 

 

void Scene::Update()
{
	for (const sptr<GameObject>& gameObject : _gameObjects)
	{
		gameObject->Update();
	}

	if (_mainCamera != nullptr)
		_mainCamera->Update();
}

그러나 이전엔 카메라가 씬의 게임 오브젝트로써 모든 게임 오브젝트 루프를 돌며 라이프 사이클에 따른 함수를 호출하였지만 이제는 메인 카메라를 따로 설정하였기 때문에 메인 카메라의 라이프 사이클에 따른 함수를 따로 호출해 준다. 

 

shared_ptr<Scene> SceneManager::LoadTestScene()
{
#pragma region Camera1
	{
		shared_ptr<GameObject> camera = make_shared<GameObject>();
		camera->Init();
		camera->AddComponent(make_shared<Camera>()); 
		camera->AddComponent(make_shared<TestCameraScript>());
		camera->GetTransform()->SetLocalPosition(Vec3(-100.f, 100.f, 0.f));
		scene->AddCameraObject(FIRST_CAMERA, camera);
	}
#pragma endregion

#pragma region Camera2
	{
		shared_ptr<GameObject> camera = make_shared<GameObject>();
		camera->Init();
		camera->AddComponent(make_shared<Camera>());
		camera->AddComponent(make_shared<TestCameraScript>());
		camera->GetTransform()->SetLocalPosition(Vec3(100.f, 100.f, 0.f));
		scene->AddCameraObject(SECOND_CAMERA, camera);
	}
#pragma endregion

	scene->SetMainCamera(FIRST_CAMERA);

	return scene;
}

이제 테스트용 씬을 생성하는 과정에서 카메라를 생성할 때, 씬에 각 카메라를 등록하고 메인 카메라도 등록한다. 

 

void SceneManager::Update()
{
	if (KEY_PRESSED('1')) 
		_activeScene->SetMainCamera(FIRST_CAMERA);
	if (KEY_PRESSED('2'))
		_activeScene->SetMainCamera(SECOND_CAMERA);
}

void SceneManager::Render()
{
	const shared_ptr<GameObject>& cameraObjects = _activeScene->GetMainCamera();
	cameraObjects->GetCamera()->Render();
}

씬이 여러 개의 카메라 중 메인 카메라만 라이프 사이클에 대한 함수를 호출해 주기 때문에 총 5개 중 메인 카메라에 해당하는 하나의 카메라만 렌더링 하면 된다. 이전처럼 굳이 모든 게임 오브젝트를 루프를 돌며 카메라를 찾지 않아도 되므로 훨씬 간단하고 좋다. 씬 매니저에서는 카메라를 변경하고 싶다면 키 입력에 따라 SetMainCamera만 호출하면 된다.

 

이제 1번 키와 2번 키를 번갈아 가며 누르게 되면 다음과 같이 뉴진스 사진을 찍는 카메라가 바뀌게 된다. 오브젝트의 행렬을 통해 움직임을 변경해 줬기 때문에 복잡하다고 생각할 수 있지만, 간단하게 오브젝트 당 상수 버퍼에 하드 코딩된 값이 아닌 Matrix 즉 행렬 값을 넘겨줬다고 생각하면 어렵지 않다. 이 행렬을 만들기 위한 일련의 과정을 카메라 컴포넌트에서 수행한 것이다.

 

또한 이전 하드코딩된 offset 값을 그냥 입력 정점에 곱해줬을 때와 현재 행렬 변환을 수행하였을 때의 차이점은 오브젝트가 투영 행렬을 통해 변환되어 원근 투영 나누기도 진행되었기 때문에 거리에 따라 작아지며 직사각형이 아닌 정사각형으로 만들어졌다는 점이다. 당연히 오브젝트 사이즈를 정사각형으로 만들어줬기 때문에 이전에는 투영 변환이 없기 때문에 윈도우 사이즈 크기에 따라 늘어났지만 이제는 원래의 오브젝트 크기에 맞게 잘 출력된다.


Camera는 Direct3D를 처음 공부하면서 나에게 가장 어려운 부분이었다. 갖가지 상수 버퍼와 선형대수가 들어가있어서 그럴까 높은 산이었다. 그러나 단지 Camera는 상수 버퍼에 데이터를 넘기기 위한 일련의 과정이라고 생각하니 마음이 한결 가벼워졌다. 또한 컴포넌트 패턴을 사용하다 보니 서로 커플링 되어 있는 부분이 많이 없었기에 더욱 Camera에 대해서 이해하기 쉬웠다. 쉽게 생각해 보면 그냥 카메라 행렬 한 개를 상수 버퍼로 넘기거나 오브젝트 당 월드 변환 행렬에 카메라 행렬과 투영 변환 행렬을 곱해주기만 하는 과정이다. 어렵게 느껴지더라도 첫 발걸음부터 하나하나 신중하게 코드를 작성하다 보면 분명히 더 높게 오를 수 있을 것이다. 

'DirectX12 > DirectX12 응용' 카테고리의 다른 글

[Light-2]  (1) 2023.10.11
[Light-1]  (1) 2023.10.10
[Resources]  (1) 2023.10.10
[SceneManager]  (1) 2023.10.08
[Component]  (0) 2023.10.07
[InputManager와 GameTimer]  (1) 2023.10.07
[Material]  (0) 2023.10.07