상세 컨텐츠

본문 제목

HumanGL: 계층적 모델링과 변환 행렬 스택 구현 - (4)

Computer Graphics/HumanGL

by Banjosh 2024. 10. 14. 15:10

본문

이번에는 SCOP 프로젝트에서 배운 내용을 바탕으로 계층적 모델링변환 행렬 스택을 적용하여 Human Model을 구현해보았다.

고려 사항

Human Model을 구현하면서 고려해야 할 주요 사항은 다음과 같았다:

  1. Box로 구현: 각 파트는 1 x 1 x 1 크기의 Box로 이루어져 있으며, scale, translate, rotate 등을 통해 변형한다.
  2. 계층적 구조: 각 파트는 계층적으로 구성되어야 한다.
  3. 상대적 좌표: 각 파트의 좌표는 부모 파트의 좌표를 기준으로 상대적인 위치에 있어야 한다.
  4. 변환 행렬 스택: 자식 파트들은 부모 파트의 변환에 함께 영향을 받아야 하며, 이를 위해 변환 행렬 스택을 사용한다.

이러한 요구사항을 충족하기 위해 다음과 같은 클래스를 설계했다.


클래스 설계

1. Mesh 클래스

Mesh 클래스는 1 x 1 x 1 크기의 Box를 그리기 위한 간단한 기능만을 제공한다.

  • createBox(): Box Mesh를 생성하는 함수
  • draw(): Box를 그리는 함수
  • VAO, VBO, EBO를 다루는 멤버 변수를 포함
# include "vertexArray.h"
# include "buffer.h"

struct Vertex {
	glmath::vec3 pos;
	glmath::vec2 texCoord;
	glmath::vec3 normal;
};

class Mesh {
public:
	static std::unique_ptr<Mesh> createBox();
	void draw();

private:
	Mesh() = default;
	void init(std::vector<Vertex>& vertices, std::vector<uint32_t>& indices);

	std::unique_ptr<VertexArray> m_vertexArray;
	std::shared_ptr<Buffer> m_vertexBuffer; 
	std::shared_ptr<Buffer> m_elementBuffer;
	GLsizei m_elementSize;
};

#endif

 

2. Model 클래스

Model 클래스는 Human Model의 각 파트를 나타낸다. 주요 특징은 다음과 같다:

  1. Box Mesh: 모든 파트는 Box로 이루어지며, Box Mesh는 createBox()를 통해 생성된다.
  2. PartInfo 구조체: 각 파트는 변형에 필요한 정보를 담고 있는 PartInfo 구조체를 통해 관리된다.
  3. 계층적 구조: 자식 파트들은 std::vector에 저장되어 있으며, createHuman() 함수를 통해 재귀적으로 생성된다.
  4. draw(): 각 파트를 그리는 함수로, 부모 파트가 그려진 후 자식 파트의 draw()가 호출되어 계층적으로 그리기가 진행된다.
#ifndef MODEL_H
# define MODEL_H

# include "mesh.h"
# include "program.h"
# include <cmath>
# include <algorithm>
# include <map>
# include <string>
# include <stack>

enum class ePart
{
	BODY,
	HEAD,
	LEFT_UPPER_ARM,
	LEFT_LOWER_ARM,
	RIGHT_UPPER_ARM,
	RIGHT_LOWER_ARM,
	LEFT_UPPER_LEG,
	LEFT_LOWER_LEG,
	RIGHT_UPPER_LEG,
	RIGHT_LOWER_LEG,
	NONE,
};

struct PartInfo {
	std::string name; // 파트 이름
	glmath::vec3 position;    // 각 파트의 상대 위치
	glmath::vec3 translation; // 각 파트의 이동
	glmath::vec3 rotateTranslation; // 각 파트의 회전축으로의 이동
	glmath::vec3 eulerAngle;    // 각 파트의 회전 (x, y, z 축의 각도)
	glmath::vec3 scale;       // 각 파트의 크기
	glmath::vec3 color;       // 각 파트의 색상
};

class Model {
public:
	static std::unique_ptr<Model> createHuman(ePart part = ePart::BODY);
	static std::stack<glmath::mat4> s_stack;
	void draw(Program* program);

private:
	void createMesh(ePart part);
	bool createChildren(std::vector<ePart> parts);
	PartInfo getPartInfo(ePart part);
	std::vector<ePart> getPartChildrenInfo(ePart part);
	
	std::vector<std::unique_ptr<Model>> m_children;
	std::unique_ptr<Mesh> m_mesh;
	PartInfo m_partInfo;
};

#endif

 


핵심 함수 설명

1. createHuman() 함수

	static std::unique_ptr<Model> createHuman(ePart part = ePart::BODY);

 

Human Model을 계층적으로 생성하는 핵심 함수이다. ePart 파라미터로 각 파트를 지정하고, 그에 맞는 createMesh() 함수와 createChildren() 함수를 실행한다.

 

std::unique_ptr<Model> Model::createHuman(ePart part) {
	std::unique_ptr<Model> model(new Model());
	// 1단계
	model->createMesh(part);

	// 2단계
	if (!model->createChildren(model->getPartChildrenInfo(part))) {
		return nullptr;
	}

	std::cout << "Part " << model->m_partInfo.name << " is complete!\n";

	return model;
}

 

1단계 createMesh() 함수

void Model::createMesh(ePart part) {
	m_mesh = Mesh::createBox();
	m_partInfo = getPartInfo(part);
}

PartInfo Model::getPartInfo(ePart part) {
	static std::map<ePart, PartInfo> partInfoMap = {
		{ ePart::BODY, {"BODY", glmath::vec3(0.0f, 0.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f), glmath::vec3(2.0f, 4.0f, 1.0f), glmath::vec3(1.0f, 0.5f, 0.3f) } },  // 몸통
		{ ePart::HEAD, {"HEAD", glmath::vec3(0.0f, 2.5f, 0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f), glmath::vec3(1.0f), glmath::vec3(0.95f, 0.80f, 0.72f) } },  // 머리
		{ ePart::LEFT_UPPER_ARM, {"LU_ARM", glmath::vec3(-1.5f, 1.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f, 1.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(1.0f, 2.0f, 1.0f), glmath::vec3(0.95f, 0.80f, 0.72f) } }, // 왼쪽 상부 팔
		{ ePart::LEFT_LOWER_ARM, {"LL_ARM", glmath::vec3(0.0f, -2.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f, 1.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(1.0f, 2.0f, 1.0f), glmath::vec3(0.95f, 0.80f, 0.72f) } }, // 왼쪽 하부 팔
		{ ePart::RIGHT_UPPER_ARM, {"RU_ARM", glmath::vec3(1.5f, 1.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f, 1.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(1.0f, 2.0f, 1.0f), glmath::vec3(0.95f, 0.80f, 0.72f) } }, // 오른쪽 상부 팔
		{ ePart::RIGHT_LOWER_ARM, {"RL_ARM", glmath::vec3(0.0f, -2.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f, 1.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(1.0f, 2.0f, 1.0f), glmath::vec3(0.95f, 0.80f, 0.72f) } }, // 오른쪽 하부 팔
		{ ePart::LEFT_UPPER_LEG, {"LU_LEG", glmath::vec3(-0.5f, -3.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f, 1.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(1.0f, 2.0f, 1.0f), glmath::vec3(0.3f, 1.0f, 0.3f) } }, // 왼쪽 상부 다리
		{ ePart::LEFT_LOWER_LEG, {"LL_LEG", glmath::vec3(0.0f, -2.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f, 1.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(1.0f, 2.0f, 1.0f), glmath::vec3(0.3f, 1.0f, 0.3f) } }, // 왼쪽 하부 다리
		{ ePart::RIGHT_UPPER_LEG, {"RU_LEG", glmath::vec3(0.5f, -3.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f, 1.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(1.0f, 2.0f, 1.0f), glmath::vec3(0.3f, 1.0f, 0.3f) } }, // 오른쪽 상부 다리
		{ ePart::RIGHT_LOWER_LEG, {"RL_LEG", glmath::vec3(0.0f, -2.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(0.0f, 1.0f, 0.0f), glmath::vec3(0.0f), glmath::vec3(1.0f, 2.0f, 1.0f), glmath::vec3(0.3f, 1.0f, 0.3f) } }  // 오른쪽 하부 다리
	};
	return partInfoMap.at(part);
}

 

  • createMesh(): Box Mesh를 생성하고, 파트에 맞는 변형 정보를 getPartInfo()에서 가져와 저장한다.
  • getPartInfo(): 현재 파트에 대한 정보를 가져온다.

 

 2단계 createChildren() 함수

bool Model::createChildren(std::vector<ePart> parts) {
	for (ePart part : parts) {
		if (part == ePart::NONE) {
			continue;
		}
		if (std::unique_ptr<Model> child = createHuman(part)) {
			m_children.push_back(std::move(child));
		} else {
			return false;
		}
	}
	return true;
}

std::vector<ePart> Model::getPartChildrenInfo(ePart part) {
	static std::map<ePart, std::vector<ePart>> partChildrenInfo = {
		{ ePart::BODY, { ePart::HEAD, ePart::LEFT_UPPER_ARM, ePart::LEFT_UPPER_LEG, ePart::RIGHT_UPPER_ARM, ePart::RIGHT_UPPER_LEG } },
		{ ePart::HEAD, { ePart::NONE } },
		{ ePart::LEFT_UPPER_ARM, { ePart::LEFT_LOWER_ARM } },
		{ ePart::LEFT_UPPER_LEG, { ePart::LEFT_LOWER_LEG } },
		{ ePart::RIGHT_UPPER_ARM, { ePart::RIGHT_LOWER_ARM } },
		{ ePart::RIGHT_UPPER_LEG, { ePart::RIGHT_LOWER_LEG } },
		{ ePart::LEFT_LOWER_ARM, { ePart::NONE } },
		{ ePart::LEFT_LOWER_LEG, { ePart::NONE } },
		{ ePart::RIGHT_LOWER_ARM, { ePart::NONE } },
		{ ePart::RIGHT_LOWER_LEG, { ePart::NONE } },
	};

	return partChildrenInfo.at(part);
}
  • createChildren(): 자식 파트들을 생성하며, 각 자식 파트에 대해 createHuman()을 재귀적으로 호출해 계층적 구조를 완성한다.
  • getPartChildrenInfo(): 현재 파트에 대한 자식 파트들의 목록을 가져온다. 

 

2. draw() 함수

변환 행렬 스택을 사용하여 부모 파트의 변환을 자식에게도 적용하며 그리는 과정이다.

  1. 변환 행렬 계산: 부모의 변환 행렬을 기반으로 자식의 변환 행렬을 계산해 스택에 push한다.
  2. 스케일 변환 제외: 스케일 변환은 자식에게 영향을 주지 않으므로, 따로 처리한다.
  3. 계층적 그리기: 부모 파트의 draw()가 끝나면 자식 파트들의 draw()를 호출한다. 자식들이 그려진 후에는 스택을 pop하여 변환 정보를 복원한다.

 

1. 변환 행렬 계산

glmath::mat4& parentsTransform = s_stack.top();
glmath::mat4 childTransform = glmath::translate(glmath::mat4(1.0f), m_partInfo.position) *
                glmath::translate(glmath::mat4(1.0f), m_partInfo.translation) *
                glmath::translate(glmath::mat4(1.0f), m_partInfo.rotateTranslation) *
                glmath::mat4_cast(glmath::quat(m_partInfo.eulerAngle)) *
                glmath::translate(glmath::mat4(1.0f), -1 * m_partInfo.rotateTranslation);

s_stack.push(parentsTransform * childTransform);

 

childTransform은 적용되는 순서대로 다음과 변환들이 곱해진다.

 1. 회전 중심축으로 이동

 2. part 고유의 rotatation

 3. 다시 원위치로 이동

 4. part 고유의 translation

 5. 부모의 위치에 대한 상대적인 위치로 이동

 

2. 스케일 변환 제외

program->setUniform("color", m_partInfo.color);
program->setUniform("transform", s_stack.top() * glmath::scale(glmath::mat4(1.0f), m_partInfo.scale));

m_mesh->draw();

 

 

3. 계층적 그리기

 

현재 part를 다 그렸으면 이제 stack에 현재 part의 변환이 추가된 상태로 자식들의 draw를 호출한다.

그리고 자식들의 draw호출도 종료됐다면 현재 part의 변환 정보를 stack에서 pop()한다.

for (std::unique_ptr<Model>& child : m_children) {
    child->draw(program);
}

s_stack.pop();

 


구현 결과

계층적 구조에 맞게 Human Model을 성공적으로 구현한 후, PartInfo에 저장된 색상 정보를 이용해 각 파트를 색칠하여 그린 결과는 다음과 같다.

 

 

각 파트가 계층적으로 잘 렌더링된 것을 확인할 수 있다.

 


앞으로의 계획

다음 단계로는 마우스와 키보드 입력을 받아 카메라 제어 기능을 구현할 계획이다. 이를 통해 Human Model을 다양한 각도에서 조작하고 관찰할 수 있는 기능을 추가할 것이다.

관련글 더보기