UI를 하기 앞서 카메라의 특성에 대해 알아본다.
유니티에서 카메라의 옵션 중 Projection을 Perspective에서 Orthographic 카메라로 바꾸면 절두체가 가두리 양식장이 아닌 직사각형 모양이 된다. 어떤 물체가 깊이 있건 가까이 있건 상관없이 동일한 크기로 화면에 나온다.
원근법을 무시하고 정확한 크기로 유지할 때 유용한다.
전체 화면을 찍고 오려붙이는 개념이다.
이전에 텍스쳐나 붙여서 하는 방식에서 그려지는 물체들은 쉐이더를 통해서 결정이 되는 게 문제다.
다양한 방법이 있지만 물체를 만든다음에 무조건 near에 붙이는 것도 방식이 될 수 있고,
유니티에서는 UI를 만들면 제3의 공간에 만들어진다.
카메라가 여러개가 되면 된다.
별도의 UI쉐이더를 만들고 카메라 신경 쓰지 않고 무조건 직교투영으로 해서 만들어도 되고,
카메라 여러개로 하나는 일반적 원근 투영, 하나는 UI 전용으로 해서 UI들만 모아서 찍어줘서 화면에 띄어주는 식으로 합쳐주면 된다.
원근 투영, 직교 투영을 조합해서 완성된 결과를 만들어야 한다.
직교투영, 원근투영 구분하고,
카메라가 여러개가 등장해서 찍어야 되는 물체를 확실히 구분할 수 있게 만들어줘야 한다.
Camera가 어떤 레이어면 어떤 물체만 찍는다거나 그런 규칙을 정해주는 게 합리적이다.
orthographic 카메라를 이용해서 UI를 만들어 준다가 핵심 과제다.
목적은 Button을 만들어서 클릭했을 때 영역을 인지할 수 있게 만들어주면 UI도 거의 끝난 것이다.
1. OrthographicDemo 클래스 만들고 세팅하기
Client/Game필터에 있는 Demo시리즈를 삭제하고, CollisionDemo를 복붙 해서 이름을 OrthographicDemo로 한다. 그리고 Client/Game필터에 넣는다.
코드를 OrthographicDemo에 맞게 수정한다.
CollisionDemo 클래스도 삭제한다.
Main도 정리하고 세팅한다.
desc.app = make_shared<OrthographicDemo>(); // 실행 단위
Client를 빌드한다.
지난 시간에 충돌 테스트까지 했다.
이제 카메라가 여러개가 등장해서 중첩해서 만들어주게끔 구조를 바꿔줘야 하고, 그뿐만 아니라 카메라가 orthographic 카메라도 적용하게끔 만들어 줘야 한다.
2. Camera클래스에 Projection 타입 변경, 컬링에 필요한 것들을 추가하고, 그릴 물체를 모으고, 그리는 함수를 정의해, Scene의 Render에서 호출하기
1) ProjectionType 변수 생성 및 헬퍼 함수 정의하기
Camera 클래스로 가서 수정을 해본다.
Camera.h에
ProjectionType _type = ProjectionType::Perspective;
기본값을 원근 투영으로 세팅한다.
필요한 헬퍼 함수
float GetWidth() { return _width; }
float GetHeight() { return _height; }
void SetProjectionType(ProjectionType type) { _type = type; }
ProjectionType GetProjectionType() { return _type; }
를 추가한다.
그리고 Camera.cpp에서
Camera::UpdateMatrix에서
_matView = S_MatView = ::XMMatrixLookAtLH(eyePosition, focusPosition, upDirection);
_matProjection = S_MatProjection = ::XMMatrixPerspectiveFovLH(_fov, _width / _height, _near, _far);
Perspective나 Orthographic이냐에 따라 View, Projection 둘 다 달라질까 하나만 달라질까
View는 딱히 영향을 받지 않고, Projection만 수정을 하면 된다.
Camera::UpdateMatrix에서 매 프레임마다 S_MatView와 S_MatProjection가 업데이트되는 부분을 지운다. 카메라를 여러 개 쓰기 때문이다.
인스턴싱도 모든 물질에 적용되게 만들면 안 되고, 옵션을 빼서 만드는 게 일반적이다.
Scene::Update에서
// INSTANCING
vector<shared_ptr<GameObject>> temp;
temp.insert(temp.end(), objects.begin(), objects.end());
INSTANCING->Render(temp);
Instancing 코드가 호출이 되면서 InstancingManager::Render가 호출되고 있는데
void InstancingManager::Render(vector<shared_ptr<GameObject>>& gameObjects)
{
ClearData();
RenderMeshRenderer(gameObjects); // 매 프레임마다 모으는 작업
RenderModelRenderer(gameObjects);
RenderAnimRenderer(gameObjects);
}
이때 RenderMeshRenderer, RenderModelRenderer, RenderAnimRenderer를 모아서 호출해 주고 있다. 이때 static으로 되어 있는 S_MatView와 S_MatProjection을 이용해서 최종적으로 그 카메라가 찍고 있다고 가정을 하고 연산을 하고 있다 보니까 카메라마다 갱신을 해주면서 만들어야겠다는 생각이 든다.
카메라마다 물체를 찍을 수 있다 보니까 카메라가 Rendering하는 코드가 들어가야 할 것이고, 렌더링 하는 카메라마다 렌더를 하게끔 Scene::Update의 //Instancing 부분이 수정이 되어야 하는데 그렇게 되면 모든 카메라가 모든 물체를 찍는 게 아니라 카메라마다 찍는 물체가 있을 거고, 안 찍는 물체가 있을 테니까 그 부분을 나눠줄 필요가 있다.
2) GameObject에 _layerIndex 변수 및 Get, Set 함수 정의하기
GameObject.h로 가서
uint8 _layerIndex = 0;
void SetLayerIndex(uint8 layer) { _layerIndex = layer; }
uint8 GetLayerIndex() { return _layerIndex; }
를 추가한다.
3) Camera에서 컬링을 제어하는 변수와 함수 추가하기
Camera.h에 가서
public:
void SetCullingMaskLayerOnOff(uint8 layer, bool on) // 특정 레이어만 on, off 하겠다.
{
if (on)
_cullingMask |= (1 << layer);
else
_cullingMask &= ~(1 << layer);
}
void SetCullingMaskAll() { SetCullingMask(UINT32_MAX); } // 아무것도 그리지 않겠다. 싹 다 1로
void SetCullingMask(uint32 mask) { _cullingMask = mask; }
bool IsCulled(uint8 layer) { return (_cullingMask & (1 << layer)) != 0; }
private:
uint32 _cullingMask = 0;
};
컬링을 제어하는 코드를 추가한다.
4) 그려줄 물체를 모으는 SortGameObject와 모은 물체들을 그리는 Render_Foward 함수 정의하기
Camera에 대한 세팅이 끝났다면
지금 구조에서는 Scene::Update에서
// INSTANCING
vector<shared_ptr<GameObject>> temp;
temp.insert(temp.end(), objects.begin(), objects.end());
INSTANCING->Render(temp);
}
main카메라로 찍는다고 가정을 하고 했지만,
이제는 Camera마다 이 // INSTANCING 부분을 진행을 할 것이다.
Camera.h에 먼저 관련있는 물체들을 긁어 오는 함수를 하나 파줄 것이고,
void SortGameObject(); // 관련있는 물체들을 sorting해서 갖고 올 것이고,
void Render_Forward(); // 나중에 Render_Deferred도 나오는지 forward가 기본방식이라
vector<shared_ptr<GameObject>> _vecForward; // 그려줄 물체들을 모아서 관리
SortGameObject에서는 무엇을 할 것이냐
Gamera.cpp에서
#include "Scene.h"
void Camera::SortGameObject()
{
shared_ptr<Scene> scene = CUR_SCENE;
unordered_set<shared_ptr<GameObject>>& gameObjects = scene->GetObjects();
_vecForward.clear();
for (auto& gameObject : gameObjects)
{
if (IsCulled(gameObject->GetLayerIndex()))
continue;
if (gameObject->GetMeshRenderer() == nullptr
&& gameObject->GetModelRenderer() == nullptr
&& gameObject->GetModelAnimator() == nullptr)
continue;
_vecForward.push_back(gameObject);
}
}
먼저 매 프레임마다 씬에 있는 아이들 중에서 그려줘야 하는 애를 체크하는 부분을 먼저 실행한다고 보면 된다.
void Camera::Render_Forward()
{
S_MatView = _matView;
S_MatProjection = _matProjection;
GET_SINGLE(InstancingManager)->Render(_vecForward); // 원래 Scene::Update에서 그렸던 걸 여기서 하는 거
}
Scene::Update의 //INSTANCING에서 하든 걸 여기로 옮겼으니
Scene::Update의
// INSTANCING
vector<shared_ptr<GameObject>> temp;
temp.insert(temp.end(), objects.begin(), objects.end());
INSTANCING->Render(temp);
를 삭제하고
5) Scene에 Render함수를 만들고, 거기에서 카메라들을 순회하며 SortGameObject와 Render_Forward를 호출하게 하기
Camera마다 모든 애들을 순회하면서 그려주게 될 거니까
Scene.h에 Render하는 단계를 하나 더 만든다.
virtual void Render();
Render와 관련된 부분을 여기에 몰빵 해서 만들어 준다.
void Scene::Render()
{
for (auto& camera : _cameras)
{
camera->GetCamera()->SortGameObject();
camera->GetCamera()->Render_Forward();
}
}
카메라가 2대가 있을 것이다. 일반 물체를 찍는 perspective
UI를 찍는 카메라가 나머지를 찍는 식으로 작업을 하려 한다.
3. Scene에서 GetCamera를 GetMainCamera, GetUICamera로 나누기
Scene.h에서
GetCamera가
shared_ptr<GameObject> GetCamera() { return _cameras.empty() ? nullptr : *_cameras.begin(); }
첫번째 카메라를 가져오고 있었는데 이름을 GetMainCamera로 수정한다. 코드 내용도 수정할 것이다.
shared_ptr<GameObject> GetMainCamera();
shared_ptr<GameObject> GetUICamera();
이렇게 GetUICamera도 추가한다.
shared_ptr<GameObject> Scene::GetMainCamera()
{
for (auto& camera : _cameras)
{
if (camera->GetCamera()->GetProjectionType() == ProjectionType::Perspective)
return camera;
}
return nullptr;
}
shared_ptr<GameObject> Scene::GetUICamera()
{
for (auto& camera : _cameras)
{
if (camera->GetCamera()->GetProjectionType() == ProjectionType::Orthographic)
return camera;
}
return nullptr;
}
경우에 따라 카메라가 없을 수도 있다.
처음에 했던 것과 달라진 부분은 렌더링을 할 때 카메라마다 렌더링에 개입을 해서 내가 렌더링을 할 차례라는 식으로 이렇게 지금 등장하는 것이고, 그래서 Scene.h에서
unordered_set<shared_ptr<GameObject>> _cameras;
이렇게 camera를 들고 있었다고 보면 된다.
set보다는 vector로 들고 있는게 나을 거긴 하지만 그냥 진행한다.
Engine을 빌드하면 에러가 나는데
void Camera::SortGameObject()에서
unordered_set<shared_ptr<GameObject>>& gameObjects = scene->GetObjects();
레퍼런스로 받고 있으니
Scene.h에서
unordered_set<shared_ptr<GameObject>>& GetObjects() { return _objects; }
리턴 값에 &를 붙인다.
이제 빌드가 된다.
4. SceneManager::Update에서 2.4)에서 만든 _currentScene->Render()를 호출하기
바뀐 부분에 대해 세팅을 해줘야 한다.
SceneManager::Update에
_currentScene->Render();
를 넣어준다.
실행을 하면 아까와 동일하게 동작을 한다.
5. OrthographicDemo에서 카메라를 2개 배치해 테스트하기
카메라가 여러개라도 동작을 할 것이다.
OrthographicDemo에서 카메라를 2개를 배치해 본다.
OrthographicDemo::Init에 카메라가 하나 배치되어 있다.
// Camera
{
auto camera = make_shared<GameObject>();
camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
camera->AddComponent(make_shared<Camera>());
camera->AddComponent(make_shared<CameraScript>());
CUR_SCENE->Add(camera);
}
모든 물체를 찍어야 하지만 UI는 찍을 필요가 없을 것이고,
나머지 카메라는 UI만 찍어야 하고 이런 식으로 규칙이 서로 엇갈리는 상태다.
각각을 막아주면 된다.
#include "Camera.h"
을 추가하고
1) Define.h에 enum LayerMask 정의하기
레이어를 정해줘야 한다.
Define.h에
enum LayerMask
{
Layer_Default = 0,
Layer_UI = 1
};
이렇게 만든다.
2) 첫 번째 카메라는 UI를 제외하게 하고, 두 번째 카메라는 UI만 그리게 세팅하기
OrthographicDemo::Init()에서
camera->GetCamera()->SetCullingMaskLayerOnOff(Lyaer_UI, true);
UI는 culling 하겠다. 안 그리겠다 하는 게 된다.
두 번째 카메라를 하나 더 만든다.
// Camera
{
auto camera = make_shared<GameObject>();
camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
camera->AddComponent(make_shared<Camera>());
camera->AddComponent(make_shared<CameraScript>());
camera->GetCamera()->SetCullingMaskLayerOnOff(Layer_UI, true);
CUR_SCENE->Add(camera);
}
// UI_Camera
{
auto camera = make_shared<GameObject>();
camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
camera->AddComponent(make_shared<Camera>());
camera->GetCamera()->SetProjectionType(ProjectionType::Orthographic);
camera->GetCamera()->SetNear(1.f);
camera->GetCamera()->SetFar(100.f);
camera->GetCamera()->SetCullingMaskAll();
camera->GetCamera()->SetCullingMaskLayerOnOff(Layer_UI, false);
CUR_SCENE->Add(camera);
}
Terrain은 삭제한다. 기존의 obj와 MoveScript 포함된 obj도 삭제한다.
3) 버튼 역할을 하는 물체 하나와 일반 물체를 만들어 scene에 추가하기
버튼 역할을 하는 물체 하나와 일반 물체 하나를 추가해 본다.
// Mesh
{
auto obj = make_shared<GameObject>();
obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f, 200.f, 0.f));
obj->GetOrAddTransform()->SetScale(Vec3(200.f));
obj->AddComponent(make_shared<MeshRenderer>());
obj->SetLayerIndex(Layer_UI);
{
obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
}
{
auto mesh = RESOURCES->Get<Mesh>(L"Quad");
obj->GetMeshRenderer()->SetMesh(mesh);
obj->GetMeshRenderer()->SetPass(0);
}
CUR_SCENE->Add(obj);
}
// Mesh
{
auto obj = make_shared<GameObject>();
obj->GetOrAddTransform()->SetLocalPosition(Vec3(0.f));
obj->GetOrAddTransform()->SetScale(Vec3(2.f));
obj->AddComponent(make_shared<MeshRenderer>());
{
obj->GetMeshRenderer()->SetMaterial(RESOURCES->Get<Material>(L"Veigar"));
}
{
auto mesh = RESOURCES->Get<Mesh>(L"Sphere");
obj->GetMeshRenderer()->SetMesh(mesh);
obj->GetMeshRenderer()->SetPass(0);
}
CUR_SCENE->Add(obj);
}
클릭하면 삭제되는 부분도 테스트가 끝났으니 OrthographicDemo::Update의 코드를 삭제한다.
MoveScript도 삭제한다.
4) 실행하기
여기서 하고 싶은 테스트는 카메라를 가지고 이것저것 장난을 해보면서 어떤 물체가 찍히는지 안 찍히는지 테스트를 해보는 것이다.
실행을 하면
이렇게 뜨고 있다.
여기서 느낄 수 있는 건 카메라를 움직임에 따라 Sphere는 커지고 작아진다. 움직이는 카메라가 Perspective camera라서 그런 것이다.
Orthographic camera는 quad를 직교투영 방식으로 찍고 있어서 흔들림이 없다.
5) Camera Move 스크립트를 Orthographic 카메라에 넣어서 테스트하기
만약 Camera Move 스크립트를 첫 번째 카메라가 아니라 두 번째 카메라에 넣어서
// Camera
{
auto camera = make_shared<GameObject>();
camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
camera->AddComponent(make_shared<Camera>());
//camera->AddComponent(make_shared<CameraScript>());
camera->GetCamera()->SetCullingMaskLayerOnOff(Layer_UI, true);
CUR_SCENE->Add(camera);
}
// UI_Camera
{
auto camera = make_shared<GameObject>();
camera->GetOrAddTransform()->SetPosition(Vec3{ 0.f, 0.f, -5.f });
camera->AddComponent(make_shared<Camera>());
camera->GetCamera()->SetProjectionType(ProjectionType::Orthographic);
camera->GetCamera()->SetNear(1.f);
camera->GetCamera()->SetFar(100.f);
camera->AddComponent(make_shared<CameraScript>());
camera->GetCamera()->SetCullingMaskAll();
camera->GetCamera()->SetCullingMaskLayerOnOff(Layer_UI, false);
CUR_SCENE->Add(camera);
}
실행을 해보면
카메라가 좌우로는 움직이는데 앞뒤로는 움직여도 아무런 영향을 안 주고 있다.
앞으로 계속 가면 사라진다.
near, far 범위를 설정했기 때문에 너무 가깝거나 멀어지면 사라지게 된다.
두 개의 카메라로 찍고 있는 게 되다 보니까 UI 전용 카메라를 배치해서 UI만 찍고 있게 된다 하면 Perspective 카메라가 찍은 물체와 포개져서 나오게 될 것이다.
Shader도 신경 쓰지 않으려면 UI를 기본 쉐이더로 쓰게끔 두거나 하는 것도 방법이 될 것이다.
UI는 인스턴싱이 들어갈 필요는 없지만 지금 구조상 인스턴싱이 아닌 건 날려버렸기 때문에 지금 상태에선 이렇게 코드를 만들면 된다.
중요한 건 카메라가 여러 개 배치될 수 있다는 걸 깨달은 것이고, 여러 타입이 있고, 타입에 따라 할 수 있는 게 다르다. 그 결과물들이 합쳐져서 하나의 완성된 화면으로 만들어진다.
영화를 촬영한 다음에 CG를 입힌 셈이구나라고 보면 된다.
UI를 사용할 때 신경 쓸 것을 다음 시간에 해 본다.
'DirectX' 카테고리의 다른 글
88_빌보드 #1_풀심기 (0) | 2024.04.03 |
---|---|
87_Button 실습 (0) | 2024.04.01 |
85_Raycasting(수학) (0) | 2024.03.31 |
84_Intersection (수학) (0) | 2024.03.31 |
83_Point Test (수학) (0) | 2024.03.30 |
댓글