35. DirectX11 3D 입문_카메라
언리얼 방식으로 할지, 유니티 방식으로 할지 정해야 하는데
언리얼 방식은 상속을 받아서 하는 것이기 때문에 조금 더 빠를 수 있다.
유니티 기반은 빈 게임 오브젝트부터 시작해서 붙이고 붙여서 조립을 해 만들어야 하다 보니
카메라를 만들어야 한다면 빈 게임 오브젝트 만들고, 카메라 컴포넌트 붙이고, 카메라 움직이기 위한 카메라 스크립트를 붙이는 식으로 부품을 여러개 붙여야 한다.
유니티상에서는 드래그 앤 드롭으로 붙일 수 있으니 문제가 안되는데
DX11 포폴을 만들 때는 수동으로 코드 상에서 붙이는 걸 만들어 줘야 한다.
그게 약간 부담이 된다.
유니티, 언리얼 스타일 둘을 섞어도 된다.
언리얼 이라도 부품이 없는게 아니다. 컴포넌트라는게 있다. 상속 구조도 최대한 활용하는게 언리얼 방식이라면 유니티는 게임 오브젝트가 상속을 받는 개념이 아니라 추가적인 기능을 하고 싶다고 하면 부품을 늘려서 작업을 한다.
상속+컴포넌트가 언리얼,
오직 컴포넌트가 유니티다.
여기서는 일단 유니티 방식으로 해본다.
얼마든지 언리얼 방식으로 수정이 가능하다.
아직 카메라가 없어서 _view, _projection 행렬을 Identity로 해 놓았는데
Camera컴포넌트를 만들어서 그걸 이용해서 카메라를 붙일 것이다.
view, projection을 만드려면 카메라가 등장해야 하고, 카메라맨이 찍는 화면 자체가 어떻게 보면 뷰 행렬인 것이다.
view랑 projeciton은 복습을 해보면 world로 넘어간 다음에 그 카메라가 배치된 그 시야각으로 찍어야 되는 것이기 때문에 카메라가 바라보는 방향을 z축으로, 카메라가 있는 위치를 원점으로 하는게 카메라 스페이스였다. 그걸로 일단 좌표 변환을 해주는 것이고, 반대로 카메라를 기준으로 하는 좌표계, 카메라의 로컬 영역으로 모든 물체들을 다 변환하는 거랑 사실상 똑같다.
그렇게 한 다음에 투영을 시켜서 2D좌표로 변환하는 단계를 우리가 거치게 되는데, 카메라라는 걸 도입을 하려면 여러가지가 있어야 한다. 카메라가 찍는 시야각이나, 카메라가 어디에 위치해 있는지, 어느 방향을 바라 보는지 등등이 있어야 한다.
어디에 위치해 있고, 어디를 바라보냐를 어떤 부품으로 만들어 놨을까? 위치를 관리하는 transform 이다. 위치를 관리하고, rotation에 따라서 내가 회전을 어느 방향으로 하고 있다고 하면 회전 방향에 따라 가지고 어느 방향을 바라보고 있는지를 얻어올 수가 있다.
결국 카메라를 구현하려면 위치 관련 1. transform과
실제로 Camera자체를 나타내는 2. Camera 부품이 있어야 하고,
WASD 같은 키를 이용해서 카메라를 움직인다면 3. 카메라의 transform 위치를 조작할 수 있는 부품을 만들어야 한다.
유니티 방식으로 가게 되면 사실상 이렇게 3가지가 필요하게 된 것이다.
언리얼이라고 해도 크게 다르진 않다. 언리얼이라고하면 마지막에 카메라를 조작하는 부품을 새로 파는게 아니라 Actor를 상속 받아서 CameraActor라고 하는 식으로 만드는게 일반적이다.
DX로 코드를 만드는 건 여러 장점이 있지만, 일단은 그래픽스를 공부하며 생판 모르던 개념들에 대해 알게 된다는 게 가장 큰 장점이긴 하지만 외적으로도 코딩 연습을 하는데서 유용하다.
포폴을 만들면서 다양한 문법을 실습을 하고, 이런 저런 설계도 해보고 작업을 하다 보면, 예를 들어 간단한 RPG를 만들면 전투 구현이나 온갖것을 구현하게 되는데서 얻어가는게 많다.
프로그래밍이라는게 알고리즘 대회에 출전할게 아니라면 간단한 코드를 얼마나 빠르게 잘 쓰고, 경험이 축적이 되면 게임에서 업적이나 어떤걸 만들 떄 생각도 안한다. 어자피 만들어본 경험이 많다보니 어떻게 만들어야 할지가 머리속에 그려진다. 처음에는 모든것을 백지에서 시작해서 헤멜 확률이 높다.
설계 관점에서도 생각할 게 많아진다. 많이 만들다 보면 어느정도 구조가 잡힌다.
DX에서도 학원에서 만들 떄는 엔진 구조는 설명을 안하니까 무작정 만들어 보는데, 이왕이면 스타일을 하나 정해서 언리얼 스타일이나 유니티 스타일로 설계를 하는 것도 도움이 많이 된다.
카메라를 만들어 보는 실습을 하자.
아직 비어있는 GameObject와 Component필터를 채우며 복습을 하면서 어떤 식으로 만들지 의논을 해보면 된다 .
1. Component , GameObject , Transform, MonoBehaviour, Camera 코드 가져오기
탐색기로 Engine 프로젝트 폴더를 열고, 여기에 새로운 부품들을 복원해준다.
이전에 만들었던 Component클래스를 가져오고, 솔루션 탐색기의 Compnent 필터에도 넣어준다.
그 다음에 GameObject클래스도 가져와서 GameObject 필터에 넣어준다.
Transform 클래스도 가져와서 Component 필터에 넣어준다.
MonoBehaviour는 이름 자체가 유니티 스타일을 따르는 건데 일반적인 스크립트를 이야기 한다고 보면 된다. 이걸 상속 받아서 스크립트를 만든다. Component 필터에 넣는다.
Camera라는 스크립트를 추가해서 작업을 한다. Camera 클래스도 가져와서 Component 필터에 넣는다.
GameObject.h에서
protected:
std::array<shared_ptr<Component>, FIXED_COMPONENT_COUNT> _components;
vector<shared_ptr<MonoBehaviour>> _scripts;
_compnents는 meshRenderer나 Transform 같은 애들은 하나만 있어야 하기 떄문에 배열로 고정 개수로 들고 있지만
_scripts는 몇개를 붙여도 상관 없기 때문에 이렇게 구분해서 만들어 놨다.
이게 유니티 방식이다.
GameObject에 뭔가 생명력을 붙이고 싶다면 GameObject에서 수정하는게 아니다. 엔진 코드이기 때문에 수정할 수 없고 반드시 MonoBehaviour를 상속을 받은 스크립트를 만들고 거기다가 기능을 붙여 넣는 식으로 동작을 시키게 된다.
에러가 안나게 자잘한 수정을 하고 빌드를 한다.
Camera.h에서
static Matrix S_MatView;
static Matrix S_MatProjection;
static으로 하여 언제 어디서든 접근할 수 있게 열어 놓았다.
Cmaera, Component, MonoBehaviour, Transform 코드를 넣어 줬고,
카메라를 이용해서 뭔가 해주는 게 목표다.
텍스쳐를 어떤 물체에 입힌 다음에 그 물체를 살펴보는 역할까지 만들 것이다.
단계별로 하기 위해서 먼저 Camera라는 개념을 도입한다.
2. CameraDemo 클래스에서 _camera 오브젝트에 Transform, Camera와 Transform을 조작하는 부품을 넣기
1) CameraDemo 클래스 생성하고 _camera 오브젝트에 Transform, Camera 넣기
Client 프로젝트에 Main 필터 에 Camera 필터를 만든다.
03. ConstantBufferDemo클래스를 복제해서 이름을 04. CameraDemo라고 하고 Client의 Game 필터에 넣는다. CameraDemo에 맞게 코드를 수정한다.
CameraDemo는 어떠한 카메라가 동작을 해서, 카메라라는 물체를 개입 시켜서 그 물체를 그려 주는게 목표다.
Main.cpp에서도
#include "04. CameraDemo.h"
를 추가하고, WinMain에서
desc.app = make_shared<CameraDemo>(); // 실행 단위
이렇게 desc.app을 CameraDemo로 세팅한다.
빈 게임 오브젝트를 만들어서 뭔가 해야 된다고 했으니까
뷰랑 프로젝션이 카메라로 바뀌어야 한다.
04. CameraDemo.h의
// Camera
shared_ptr<GameObject> _camera;
얘를 이용해 조립을 해야 한다.
Client프로젝트를 빌드하면 빌드가 된다.
04. CameraDemo.cpp Init 에서 Camera를 만들어 본다.
#include "Camera.h"
를 추가하고
// Camera
_camera = make_shared<GameObject>();
_camera->GetOrAddTransform();
_camera->AddComponent(make_shared<Camera>());
Transform과 Camera 컴포넌트를 넣는다.
이제 하고 싶은건 WSAD 같은 키를 입력했을 때 물체가 아니라 이제 카메라의 위치를 이동 시키는 부품을 넣는 것이다.
CameraDamo::Update안에 있던 물체를 이동 시키는 코드를 삭제한다.
Tramsform을 _camera에 넣어 줬으니 헤더의 _translation도 삭제한다.
2) 위치를 조절하는 부품인 CameraScript 클래스 만들기
어떤 키를 눌렀을 때 카메라를 움직이게 한다.
유니티 방식에서는 모든 걸 부품화 해서 깔끔하게 만드는게 핵심이다.
Client 프로젝트 안의 Main / Camera 필터 안에 CameraScript라는 클래스를 만든다. TestCamera용 스크립트다. GameObject에 붙이는 부품인데 일반 부품이 아니라 스크립트에 역할을 하는 부품이다 할 때는 MonoBehaviour를 상속 받아야 한다.
#pragma once
#include "MonoBehaviour.h"
class CameraScript : public MonoBehaviour
{
public:
virtual void Start() override;
virtual void Update() override;
float _speed = 10.f;
};
#include "pch.h"
#include "CameraScript.h"
#include "Transform.h"
void CameraScript::Start()
{
}
void CameraScript::Update()
{
float dt = TIME->GetDeltaTime();
Vec3 pos = GetTransform()->GetPosition();
if (INPUT->GetButton(KEY_TYPE::W))
pos += GetTransform()->GetLook() * _speed * dt;
if (INPUT->GetButton(KEY_TYPE::S))
pos -= GetTransform()->GetLook() * _speed * dt;
if (INPUT->GetButton(KEY_TYPE::A))
pos -= GetTransform()->GetRight() * _speed * dt;
if (INPUT->GetButton(KEY_TYPE::D))
pos += GetTransform()->GetRight() * _speed * dt;
GetTransform()->SetPosition(pos);
if (INPUT->GetButton(KEY_TYPE::Q))
{
Vec3 rotation = GetTransform()->GetLocalRotation();
rotation.x += dt * 0.5f;
GetTransform()->SetLocalRotation(rotation);
}
if (INPUT->GetButton(KEY_TYPE::E))
{
Vec3 rotation = GetTransform()->GetLocalRotation();
rotation.x -= dt * 0.5f;
GetTransform()->SetLocalRotation(rotation);
}
if (INPUT->GetButton(KEY_TYPE::Z))
{
Vec3 rotation = GetTransform()->GetLocalRotation();
rotation.y += dt * 0.5f;
GetTransform()->SetLocalRotation(rotation);
}
if (INPUT->GetButton(KEY_TYPE::C))
{
Vec3 rotation = GetTransform()->GetLocalRotation();
rotation.y -= dt * 0.5f;
GetTransform()->SetLocalRotation(rotation);
}
}
이렇게 카메라를 조종하는 스크립트를 간략하게 만들어 보았다.
3) _camera 오브젝트에 CameraScript 넣기
CameraDemo.cpp로 가서
부품으로 방금 만들어준 스크립트를 넣어준다.
#include "CameraScript.h"
를 추가하고
CameraDemo::Init에서
_camera->AddComponent(make_shared<CameraScript>());
Transform과 Camera에 이어 CameraScript를 이렇게 추가한다.
_camera->GetOrAddTransform();
_camera->AddComponent(make_shared<Camera>());
여기까지는 엔진에서 추가한 거고
_camera->AddComponent(make_shared<CameraScript>());
이건 컨텐츠 레벨에서 추가한 세번째 부품이라고 볼 수 있는 것이다.
4) CameraDemo::Update에서 _camera->Update() 호출하기
업데이트를 할 때에는
void CameraDemo::Update()
{
_camera->Update();
}
여기서 호출해주면 된다.
GameObject::Update가 실행되면서 추가한 component들과 script들의 Update들이 다 실행된다.
씬에다가 배치하면 씬이 모든 GameObject를 들고 있기 때문에 알아서 되겠지만, 지금은 씬이 없으니까 인위적으로 직접한 것이다.
정상적으로 빌드가 되는지 확인을 한다.
Camera의 위치를 등장시켜서 이동시키는 부분이라고 보면 된다.
5) Shader는 그대로 03. ConstantBuffer 사용하기
아직 Shader 작업 남았다.
탐색기의 Shaders폴더에 가서
하나의 파일을 복제한 뒤 04. World.fx라고 이름을 짓고 Shaders 필터에 넣는다. 일반적인 공용으로 사용할 셰이더를 만들어 보았다. World.fx는 나중에 UV좌표 적용할 때 고쳐주고 아직 이걸 사용하지 않고 03. ConstantBuffer.fx를 계속 사용한다.
void CameraDemo::Init()
{
_shader = make_shared<Shader>(L"03. ConstBuffer.fx");
6) CameraDemo::Render에서 _shader에 View와 Projection 행렬을 Camera::UpdateMatrix()에서 갱신해준 Camera의 것으로 세팅하기
CameraDemo::Render에서
void CameraDemo::Render()
{
// ?
_shader->GetMatrix("World")->SetMatrix((float*)&_world);
_shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
_shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
이렇게 Camera의 View와 MatProjection을 넣어준다.
Camera::UpdateMatrix()에서 값을 갱신해 주었다.
실행을 하면
아무것도 안보이는데 wasd를 눌러 보며 카메라를 이동시키면 보이게 된다.
카메라가 0, 0, 0 위치에 있고, QUAD도 0, 0, 0 위치에 있기 떄문이다.
qe로 카메라를 위아래로 회전 가능하고, zc로 좌우로 회전도 가능하다.
나중에 마우스로 움직이게 할 수 있다.
카메라를 구현하기 위해서 뭘 했는지 생각을 해보면
Transform, Camera, CameraScript 부품 3개를 조립해서 카메라를 만들었다.
7) _view, _projection 변수 삭제하기
view, projection 개념이 없어지고, _camera랑 같이 움직이게 되었다.
CameraDemo.h에 Matrix _view, _projection 을 삭제한다.
3. 맺음말
// Object
shared_ptr<Geometry<VertexColorData>> _geometry;
shared_ptr<VertexBuffer> _vertexBuffer;
shared_ptr<IndexBuffer> _indexBuffer;
Matrix _world = Matrix::Identity;
유니티 방식으로 한다면 CameraDemo.h의 이 Obejct 부분도 빼줘야 한다.
사각형 물체를 MeshRenderer라는 걸로 그려줄 것이고,
그려주기 위한 좌표를 _world라는 행렬로 관리하는게 아니라 물체의 좌표와 관련된 부품인 Transform이란 컴포넌트로 관리해야 한다.
모든 애들이 한줄 짜리로 바뀌어야 한다.
CameraDemo::Init에서
// Camera
_camera = make_shared<GameObject>();
_camera->GetOrAddTransform();
_camera->AddComponent(make_shared<Camera>());
_camera->AddComponent(make_shared<CameraScript>());
이렇게 묶어주는 부분도 나중 드래그 드롭 방식으로 구현하면 개발 속도가 빨라질 수 있다.
이렇게 카메라를 움직이는 거 까지 해 보았다.
만약 언리얼 방식으로 만든다면 GameObject는 Actor라는 애가 될 것이고,
언리얼 엔진도 다 상속하는 건 아니다. Camera랑 Transform은 부품으로 되어 있다.
MonoBehaviour를 상속받는게 아니라, Actor를 상속 받아서 CameraActor 가 되는 느낌으로, 하나의 엑터이지만 여기서 기능을 구사하게 된다.
GameObject를 상속 받아서 거기다가 기능을 불어넣는 느낌이다.
시작했을 때 아무것도 안보이는게 마음에 걸린다면
CameraDemo::Init에서
_camera->GetTransform()->SetPosition(Vec3(0.f, 0.f, -2.f));
이렇게 하면 실행 하자마자 사각형이 보인다.