OpenGL/개발 일지

[OpenGL] 게임 엔진 개발 (6) - Scene Hierarchy

ciel45 2024. 7. 30. 01:09

전체 소스코드:https://github.com/sys010611/YsEngine

 

 

씬에 배치될 게 더 많아지기 전에 Scene Hierarchy 창을 간단하게 만들어보았다.

 

UI 작업에 들어가기 전, 게임 오브젝트들(Model, Light 등)을 한꺼번에 다루는 클래스가 있어야겠다는 생각이 들었다.

게임 오브젝트들을 다루는 다른 클래스들은 최대한 해당 클래스에만 의존하도록 하고 싶다. (DIP)

 

DIP에 대한 자세한 내용은

https://ciel45.tistory.com/22

 

[UE5] 블루아카이브 TPS게임 개발일지 (10) - 사격 기능 구현 준비 작업 + DIP

사격 애니메이션까지는 만들어 두었으므로, 실제로 총이 발사되는 메커니즘을 만들고자 한다. 우선 발사를 구현하는 데에는 두가지 방법이 있다. 라인 트레이싱(Line Tracing)과 투사체(Projectile)이

ciel45.tistory.com

 

 

그래서 Entity 클래스를 만들고, 모든 게임 오브젝트들은 Entity를 상속받도록 하였다.

 

Entity.h:

#pragma once

#include <string>

#include <glm/glm.hpp>
#include <glm\gtc\matrix_transform.hpp>
#include <glm\gtc\type_ptr.hpp>

class Entity
{
public:
	Entity();

	virtual std::string GetName() = 0;
	virtual void ShowProperties() = 0;
	virtual glm::mat4 GetModelMat() = 0;
	virtual void UpdateTransform(glm::mat4 newModelMat) = 0;

	uint32_t GetID() { return this->id; }

	static uint32_t counter;

protected:
	std::string name;
	glm::mat4	modelMat;

private:
	uint32_t id;
};

 

Entity.cpp

#include "Entity.h"

uint32_t Entity::counter = 0;

Entity::Entity()
{
	id = counter;
	counter++;
}

 

static 변수를 이용해서 각 Entity는 고유한 ID를 가지도록 하였다. (Entity를 만들 때마다 1 씩 증가)

왜냐하면 ImGui로 Hierarchy 창을 구현할 때 고유한 ID가 필요할 예정이기 때문이다. (자세한 건 후술)

 

순수 가상함수를 많이 사용했는데, 동적 바인딩을 최대한 써먹어주기 위해서이다.

웬만한 다른 클래스는 Entity 하나만 알면 되도록 하고 싶기 때문이다.

 

함수 중 GetNameGetModelMat은 이름 그대로의 의미이고,

ShowProperties는 Inspector를 적절하게 채워주는 역할,

UpdateTransform은 ImGuizmo의 gizmo 조작에 대응하여 실제로 오브젝트를 움직여주는 역할을 맡을 것이다.

 

지금까지 만든 클래스 중 Entity를 상속받게 할 클래스는 Model, Light(PointLight, DirectionalLight)가 있었다.

 

Model.h

#pragma once
//...

class Model : public Entity
{
public:
	//...

	virtual std::string GetName() override;
	virtual void ShowProperties() override;
	virtual glm::mat4 GetModelMat() override;
	virtual void UpdateTransform(glm::mat4 newModelMat) override;

private:
	//...
};

 

Model.cpp

//...

std::string Model::GetName()
{
	return "Model";
}

void Model::ShowProperties()
{
	// Transform
	ImGui::Text("Transform");

	ImGui::InputFloat3("Translate", GetTranslate());
	ImGui::InputFloat3("Rotate", GetRotate());
	ImGui::InputFloat3("Scale", GetScale());

	// Material
	Material* currMaterial = GetMaterial();
	ImGui::Text("Material");

	ImGui::SliderFloat("Specular", &currMaterial->specular, 0.f, 5.f);
	ImGui::SliderFloat("Shininess", &currMaterial->shininess, 0.f, 512.f);
}

glm::mat4 Model::GetModelMat()
{
	// model Matrix 구성
	glm::mat4 T = glm::translate(glm::mat4(1.f), glm::vec3(translate[0], translate[1], translate[2]));
	glm::mat4 R = glm::mat4_cast(glm::quat(glm::vec3(glm::radians(rotate[0]), glm::radians(rotate[1]), glm::radians(rotate[2]))));
	glm::mat4 S = glm::scale(glm::mat4(1.f), glm::vec3(scale[0], scale[1], scale[2]));
	return modelMat = T * R * S;
}

void Model::UpdateTransform(glm::mat4 newModelMat)
{
	glm::vec3 translation, rotation, scale;
	ImGuizmo::DecomposeMatrixToComponents(glm::value_ptr(newModelMat), &translation[0], &rotation[0], &scale[0]);

	SetTranslate(translation);
	SetRotate(rotation);
	SetScale(scale);
}

//...

 

PointLight.h

#pragma once
#include "Light.h"
class PointLight : public Light
{
public:
//...
	virtual std::string GetName() override { return "PointLight"; }
	virtual void ShowProperties() override;
	virtual glm::mat4 GetModelMat() override;
	virtual void UpdateTransform(glm::mat4 newModelMat) override;
//...
};

 

PointLight.cpp

#include "PointLight.h"

#include "imgui.h"
#include "ImGuizmo.h"

void PointLight::ShowProperties()
{
	// Transform
	ImGui::Text("Transform");

	ImGui::InputFloat3("Position", &position[0]);
	ImGui::InputFloat4("Color", &color[0]);
	ImGui::SliderFloat("Ambient", GetAmbientIntensity(), 0.f, 5.f);
	ImGui::SliderFloat("Diffuse", GetDiffuseIntensity(), 0.f, 5.f);
}

glm::mat4 PointLight::GetModelMat()
{
	return modelMat = glm::translate(glm::mat4(1.f), position);
}

void PointLight::UpdateTransform(glm::mat4 newModelMat)
{
	glm::vec3 translation, rotation, scale;
	ImGuizmo::DecomposeMatrixToComponents(glm::value_ptr(newModelMat), &translation[0], &rotation[0], &scale[0]);

	position = translation;
}

 

DirectionalLight.h

#pragma once
#include "Light.h"
class DirectionalLight : public Light
{
public:
	//...
	virtual std::string GetName() override { return "DirectionalLight"; }
	virtual void ShowProperties() override;
	virtual glm::mat4 GetModelMat() override;
	virtual void UpdateTransform(glm::mat4 newModelMat) override;

	glm::vec3 direction;
};

 

DirectionalLight.cpp

#include "DirectionalLight.h"

#include "imgui.h"
#include "ImGuizmo.h"

void DirectionalLight::ShowProperties()
{
	// Directional Light
	ImGui::Text("DirectionalLight");

	ImGui::InputFloat3("Direction", &direction[0]);
	ImGui::InputFloat4("Color", &color[0]);
	ImGui::SliderFloat("Ambient", GetAmbientIntensity(), 0.f, 5.f);
	ImGui::SliderFloat("Diffuse", GetDiffuseIntensity(), 0.f, 5.f);
}

glm::mat4 DirectionalLight::GetModelMat()
{
	return modelMat = glm::translate(glm::mat4(1.f), direction);
}

void DirectionalLight::UpdateTransform(glm::mat4 newModelMat)
{
	glm::vec3 translation, rotation, scale;
	ImGuizmo::DecomposeMatrixToComponents(glm::value_ptr(newModelMat), &translation[0], &rotation[0], &scale[0]);

	direction = translation;
}

 

 

이렇게 구현을 해두었다.

 

그리고 기존의 난잡했던 InspectorPanel::Update는 다음과 같이 수정했다.

void InspectorPanel::Update()
{
	ImGui::Begin("Inspector");

	if(currEntity)
		currEntity->ShowProperties();

	ImGui::End();
}

이제 InspectorPanel의 Update 함수를 호출하면, 그 안에서 각 Entity의 실제 타입에 맞는 ShowProperties 함수가 호출되어 Inspector가 적절한 요소들을 보여줄 것이다.

 

 

 

이제 대망의 HierarchyPanel 클래스이다.

HierarchyPanel.h

#pragma once

#include <vector>
#include "Panel.h"

class Entity;
class InspectorPanel;
class ScenePanel;

class HierarchyPanel : Panel
{
public:
	HierarchyPanel(std::vector<Entity*>& entityList, ScenePanel* sp);
	virtual void Update() override;
	void UpdateEntityList(std::vector<Entity*>& entityList);
	~HierarchyPanel();

private:
	void DrawEntityNode(Entity* entity);

	std::vector<Entity*>& entityList;
	Entity* selection;

	InspectorPanel* inspectorPanel;
	ScenePanel* scenePanel;
};

 

HierarchyPanel.cpp

#include "HierarchyPanel.h"

#include "imgui.h"
#include "Entity.h"
#include "InspectorPanel.h"
#include "ScenePanel.h"

#include <string>

HierarchyPanel::HierarchyPanel(std::vector<Entity*>& entityList, ScenePanel* sp)
	: entityList(entityList), selection(nullptr)
{
	inspectorPanel = new InspectorPanel();
	scenePanel = sp;
}

void HierarchyPanel::Update()
{
	ImGui::Begin("Hierarchy");

	for (auto entity : entityList)
		DrawEntityNode(entity);

	ImGui::End();

	inspectorPanel->Update();
}

void HierarchyPanel::UpdateEntityList(std::vector<Entity*>& entityList)
{
	this->entityList = entityList;
}

void HierarchyPanel::DrawEntityNode(Entity* entity)
{
	const auto& name = entity->GetName();

	ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | (entity == selection ? ImGuiTreeNodeFlags_Selected : 0);
	bool opened = ImGui::TreeNodeEx((void*)entity->GetID(), flags, name.c_str());

	if (ImGui::IsItemClicked())
	{
		selection = entity;
		inspectorPanel->SetEntity(selection);
		scenePanel->SetEntity(selection);
	}

	if (opened)
	{
		ImGui::TreePop();
	}
}

HierarchyPanel::~HierarchyPanel()
{
	delete inspectorPanel;
}

 

보시다시피 Entity의 vector를 통해 게임 오브젝트를 관리하도록 하였다.

 

inspectorPanel은 아예 hierarchy가 만들어서 쓰도록 하였다.

inspector 안에 뭘 띄워줄지는 hierarchy에서 결정하는 것이기 때문에, 이 편이 적절하다고 생각하였다.

 

scenePanel도 gizmo의 표시를 위해 참조할 필요가 있었지만,

scenePanel은 hierarchy가 만들어 쓰는 것은 부적절한 것 같아, 생성자로 포인터를 넘겨주도록 하였다.

 

Update 함수를 통해 entity마다 DrawEntityNode가 실행되는데,

해당 함수의 코드는 ImGui Demo Window를 참고하였다.

 

ImGui::TreeNodeEx가 리스트의 아이템을 만들어주는 함수라고 할 수 있다.

인자로 entity의 ID를 사용한 것을 볼 수 있는데, 바로 여기서 ID가 필요한 것이다.

 

내부가 정확히 어떻게 구현되어있는지는 몰라도, ID를 이용해 관리하는 것으로 보인다.

 

IsItemClicked() 조건을 확인 후, inspectorPanel, scenePanel에 현재 선택된 Entity를 알려주도록 하였다.

 

 

 

ScenePanel 클래스도 변경점이 있다. 원래 Gizmo를 Model에만 그렸는데, 이젠 Entity를 바꿔가며 선택할 때마다 거기에 그려줄 것이기 때문.

//...
void ScenePanel::Update()
{
	// Render ImGui
	ImGui::Begin("Scene", NULL);

	//...
	// 프레임버퍼 텍스처를 ImGui 윈도우에 렌더링
	//...

	if(selectedEntity)
		DrawGizmo(pos);
//...
	ImGui::End();
}

//...

void ScenePanel::SetEntity(Entity* e)
{
	selectedEntity = e;
}

void ScenePanel::DrawGizmo(ImVec2 pos)
{
	// Gizmos
	ImGuizmo::SetOrthographic(false);
	ImGuizmo::SetDrawlist();
	ImGuizmo::SetRect(pos.x, pos.y, width, height);

	glm::mat4 model = selectedEntity->GetModelMat();
	glm::mat4 view = camera->GetViewMatrix();
	const glm::mat4& projection = camera->GetProjectionMatrix(width, height);

	ImGuizmo::Manipulate(glm::value_ptr(view), glm::value_ptr(projection),
		(ImGuizmo::OPERATION)currOperation, ImGuizmo::LOCAL, glm::value_ptr(model));

	if (ImGuizmo::IsUsing())
	{
		selectedEntity->UpdateTransform(model);
	}
}

//...

 

UpdateTransform도 가상함수로 만들어놓았으므로, Entity의 실제 타입마다 맞게 transform이 업데이트된다.

 

예를 들면 Light는 Model과 다르게 scale이 상관없으므로 그런건 무시한다던가.

 

 

실행 결과:

 

 

유니티, 언리얼 처럼 오브젝트간 부모/자식 관계를 만드는 것까지 해버릴까 싶었지만.. 일단 다른 하고 싶은게 많아 그건 좀 미뤘다.