Scop 구현기 4편: Model 클래스

2025. 4. 14. 09:19·Dev/Grapics

전 편에서 Shader코드들을 분리해 클래스로 따로 만들었다

이제는 기본. obj 파일을 파싱 해서 VAO와 VBO 그리고 정점들에 대한 데이터 들을 클래스 단위로 분리해 보자!

 

Scop에서 가장 중요한 클래스가 아닐까 싶다


1. 왜 Model 클래스를 도입했나?

삼각형 그리기를 생각해 본다면 VAO, VBO, 위치 데이터, 셰이더 정보 등이 전부 흩어져 있다

 

이걸 하나로 묶지 않으면

  • 각 객체마다 VAO/VBO를 일일이 관리해야 하고
  • draw 호출마다 셰이더 바인딩도 반복하고
  • 좌표, 색상, 메시 정보를 통합적으로 다루기 어렵다

그래서 이 모든 "하나의 그릴 대상"을 감싸는 클래스를 만들게 된 것이다.


2. Model 클래스는 어떤 책임을 가져야 할까?

처음에는 단순히 "정점 데이터를 갖는 그릇" 정도로만 생각했다

하지만 직접 만들다 보니, 역할이 꽤 많았다.

역할 설명
정점 데이터 보관 VBO를 생성하고 데이터를 업로드
VAO 설정 버텍스 레이아웃을 GPU에 정의
셰이더 연동 Shader 클래스를 보관하고 사용
변환 행렬 저장 위치/회전/크기를 위한 model matrix
draw() 함수 화면에 실제로 그리는 로직
OBJ 파싱 .obj 파일을 파싱해서 정점 데이터, 메터리얼 등을 저장

 

즉, 단순 그리기 이상으로, 렌더링 파이프라인을 포괄하는 최소 단위가 되었다.


3. Model 클래스의 헤더부터 살펴보자

Model.hpp

생각 보다 Model 클래스의 크기가 커졌다....

천천히 하나하나 설명을 작성해 보겠습니다!


1. 생성자

Model::Model(const char* filePath, Shader* shader)
: mShaderProgram(shader)
, mScaleFactor(1.0f)
, mRotX(0.0f), mRotY(0.0f)
, mPosX(0.0f), mPosY(0.0f), mPosZ(0.0f)
, mbUseTexture(false)
, mBlendFactor(0.0f)
, mUVRotation(UVRotation::None)
{
	LoadOBJ(filePath);
	SetupMesh();
	LoadTexture("resources/sample.png");
}

 

생성자에서 간단하게 초기화해야 하는 멤버 변수들을 초기화하고 Model 클래스의 핵심이라고 할 수 있는 부분인 LoadOBJ로
넘 거 가게 된다.

 

2. LoadOBJ

void Model::LoadOBJ(const char* filePath)
{
	std::ifstream file(filePath);
	if (!file.is_open())
	{
		std::cerr << "Failed to open OBJ file: " << filePath << std::endl;
		return;
	}
	std::vector<Vec3>	temp_V;
	std::vector<Vec2>	temp_Vt;
	std::vector<Vec3>	temp_Vn;
	std::vector<Vec3>	temp_color;
	bool				hasTexCoord;

	std::string line;
	while (std::getline(file, line))
	{
		std::istringstream iss(line);
		std::string prefix;
		iss >> prefix;
		// mtl 파일 파싱
		if (prefix == "mtllib")
		{
			std::string mtlFilename;
			iss >> mtlFilename;
			mMaterials = MTLLoader::Load("resources/" + mtlFilename);
			std::cout << mtlFilename << std::endl;
		}
		else if (prefix == "v") // v 1.0123, 2.123123, -6.1235 -> 정점 좌표 정보일 경우
		{
			Vec3 v;

			iss >> v.x >> v.y >> v.z;
			temp_V.push_back(v);
		}
		else if (prefix == "vt") // vt 0~1, 0~1 -> TextureCoord일 경우
		{
			Vec2 vt;
			iss >> vt.x >> vt.y;
			temp_Vt.push_back(vt);
			hasTexCoord = true;
		}
		else if (prefix == "vn") // vn 1, 2, 5 -> Normal 정보일 경우
		{
			Vec3 vn;
			iss >> vn.x >> vn.y >> vn.z;
			temp_Vn.push_back(vn);
		}
		else if (prefix == "usemtl") // v, vn, vt 정보가 모두 처리 완료 된 후 Usemtl -> 이후 f들에게 적용할 mtl 명
		{
			iss >> mCurrentMaterial;
		}
		else if (prefix == "f") // f 정보가 나오면
		{
			Face face;
			face.mtlname = mCurrentMaterial;
			std::string vertexStr;
			while (iss >> vertexStr)
			{
				int vIdx = -1, vtIdx = -1, vnIdx = -1;
				size_t firstSlash = vertexStr.find('/'); // f 1/2/3 에서 첫번째 슬레쉬 찾기
				size_t secondSlash = vertexStr.find('/', firstSlash + 1); // 두번째 슬레쉬 찾기

				if (firstSlash == std::string::npos)
				{
					vIdx = std::stoi(vertexStr) - 1;
				}
				else if (secondSlash == std::string::npos)
				{
					vIdx = std::stoi(vertexStr.substr(0, firstSlash)) - 1;
					vtIdx = std::stoi(vertexStr.substr(firstSlash + 1)) - 1;
				}
				else if (secondSlash == firstSlash + 1)
				{
					vIdx = std::stoi(vertexStr.substr(0, firstSlash)) - 1;
					vnIdx = std::stoi(vertexStr.substr(secondSlash + 1)) - 1;
				}
				else
				{
					vIdx = std::stoi(vertexStr.substr(0, firstSlash)) - 1;
					vtIdx = std::stoi(vertexStr.substr(firstSlash + 1, secondSlash - firstSlash - 1)) - 1;
					vnIdx = std::stoi(vertexStr.substr(secondSlash + 1)) - 1;
				}

				Vertex v;
				v.position = temp_V[vIdx];
				if (vtIdx >= 0 && temp_Vt.size() != 0)
					v.texCoord = temp_Vt[vtIdx];
				if (vnIdx >= 0 && temp_Vn.size() != 0)
					v.normal = temp_Vn[vnIdx];

				mVertices.push_back(v);
				unsigned int index = mVertices.size() - 1;
				face.vertexIndices.push_back(index);
			}
			if (face.vertexIndices.size() == 3)
			{
				mFaces.push_back(face);
			}
			else if (face.vertexIndices.size() == 4)
			{
				Face f1, f2;
				f1.mtlname = face.mtlname;
				f1.vertexIndices = { face.vertexIndices[0], face.vertexIndices[1], face.vertexIndices[2] };

				f2.mtlname = face.mtlname;
				f2.vertexIndices = { face.vertexIndices[0], face.vertexIndices[2], face.vertexIndices[3] };

				mFaces.push_back(f1);
				mFaces.push_back(f2);
			}

		}
	}
	for (const auto& v : mVertices)
		mOriginalPositions.push_back(v.position);
	file.close();
	if (!hasTexCoord)
	{
		GenerateTexCoordsFromPosition();
	}
	NormalizeVertices(mVertices); // NDC 좌표계로
	ApplyFaceColorsFromIndex();
	PrintAllVertexInfo();
	// PrintAllFaceInfo();
	// 정점 및 인덱스 저장
}

전체 흐름 요약

  1. . obj 파일 열기
  2. v, vt, vb, f , usemtl, mtllib 등 접두어별로 처리
  3. 각 줄을 파싱해 점검, 텍스쳐 좌표, 노멀 벡터 등 임시 저장
  4. f 라인에서 실제 정점 조합 생성 및 mVetices/mFaces 구성
  5. 정점 정규화, 텍스쳐 좌표 자동 생성, 색상 정용 등 후처리

 섹션 상세 설명

1. 파일 열기

std::ifstream file(filePath);
if (!file.is_open())

 

ifstream으로 파일을 연뒤 열리지 않으면 경고 출력 후 리턴.

 

2. 정점/텍스처/ 노멀 저장용 임시 버퍼

std::vector<Vec3>	temp_V;
std::vector<Vec2>	temp_Vt;
std::vector<Vec3>	temp_Vn;

. obj는 f라인보다 v 라인이 먼저 오므로 일단 버퍼에 넣어둔다.

정점마다 (v/vt/vn) 조합이 다르므로 나중에 매칭해야 한다.

 

3.mtlib & usemtl 처리

if (prefix == "mtllib")
{
 // 재질 로딩
}
else if (prefix == "usemtl")
{
 // 현재 페이스에 적용할 재질 이름 저장
}

.obj 파일은. mtl 파일을 함께 사용하며, 재질 (Material) 이름은 Face에 저장된다.

그럼 나중에 렌더링 할 때 해당 재질의 색상을 불러올 수 있다.

 

4.f(face) 파싱

 

이게 핵심 부분이라고 생각한다, 각 정점은 다음 4가지 방식으로 등장할 수 있다.

형식 의미
v 정점 인덱스
v/vt 정점 + 텍스처 좌표
v//vn 정점 + 노멀
v/vt/vn 정점 + 텍스처 + 노멀

이를. find('/')로 판단하고 각 파트를 분리해 index로 처리한다.

 

5. Vertex 객체 생성 및 mVertices 저장

Vertex v;
v.position = temp_V[vIdx];
if (vtIdx >= 0) v.texCoord = temp_Vt[vtIdx];
if (vnIdx >= 0) v.normal = temp_Vn[vnIdx];
mVertices.push_back(v);
  • 각각의 Face는 Vertex들의 index를 참조하여 만든다.
  • . obj는 인덱스 기반 구조이기 때문에, Vertex 중복이 발생할 수 있다.
  • 여기선 간단하게 mVertices.push_back()하며 인덱스를 추적해 넣는다.

6. 4 각형 face 삼각형 분해

if (face.vertexIndices.size() == 4) {
	// 0-1-2 / 0-2-3 으로 분해
}

. obj에서 사각형(f 1 2 3 4) 구조는 삼각형 두 개로 쪼개줘야 한다.

현재 구조에서는 그래야 OpenGL이 이해할 수 있다.

 

7. 텍스처 좌표 없을 경우 자동 생성

if (!hasTexCoord) {
	GenerateTexCoordsFromPosition();
}

텍스처 좌표가 없는. obj 파일도 많기 때문에

그럴 경우 정점 위치 기반으로 UV를 강제로 생성해 준다.

 

8. 정규화 및 색상 적용

NormalizeVertices(mVertices);
ApplyFaceColorsFromIndex();
  • 정점 위치를 OpenGL의 -1.0 ~ 1.0 영역으로 스케일링 (NDC 대응)
  • 각 Face마다 색상을 지정해 색상을 가진 렌더링이 가능하도록 한다 (체크무늬)

3.SetUpMesh

void Model::SetupMesh(void)
{
	glGenVertexArrays(1, &mVAO);
	glGenBuffers(1, &mVBO);
	glGenBuffers(1, &mEBO);

	glBindVertexArray(mVAO);

	glBindBuffer(GL_ARRAY_BUFFER, mVBO);
	glBufferData(GL_ARRAY_BUFFER, mVertices.size() * sizeof(Vertex), &mVertices[0], GL_STATIC_DRAW);
	/*
		Vertex
		->	float * 3 : position
			float * 3 : normal
			float * 2 : texcoord
			flaot * 3 : color
	*/
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mEBO);
		// 각 face의 인덱스를 모아 하나의 인덱스 버퍼 생성
	mAllIndices.clear();
	for (const Face& face : mFaces) {
		mAllIndices.insert(mAllIndices.end(), face.vertexIndices.begin(), face.vertexIndices.end());
	}
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, mAllIndices.size() * sizeof(unsigned int), mAllIndices.data(), GL_STATIC_DRAW);

	glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));
	glEnableVertexAttribArray(2);
	glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, color));
	glEnableVertexAttribArray(3);
	glBindVertexArray(0);
}

전체 흐름 요약

  1. 버퍼 객체 (VAO, VBO, EBO) 생성
  2. 정점 데이터 GPU에 업로드 (glBufferData)
  3. 인덱스 데이터 생성 및 업로드
  4. 정점 속성 포인터 설정 (glVertexAttribPointer)
  5. 마무리 정리 (glBindVertexArray(0))

섹션 상세 설명

1. 버퍼 객체 생성

glGenVertexArrays(1, &mVAO);
glGenBuffers(1, &mVBO);
glGenBuffers(1, &mEBO);
  • VAO: 정점 배열 객체 (Vertex Array object)
  • VBO: 정점 버퍼 객체 (Vertex Array object)
  • EBO: 인덱스 버퍼 객체 (Element Buffer object)

2. VAO 바인딩 시작

glBindVertexArray(mVAO);

 

이제부터의 모든 설정은 VAO에 저장된다.

즉, 한 번만 설정해 두면 glBindVertexArray(mVAO)만 해도 그 설정이 복구됨

 

3. 정점 데이터 GPU에 업로드

glBindBuffer(GL_ARRAY_BUFFER, mVBO);
glBufferData(GL_ARRAY_BUFFER, mVertices.size() * sizeof(Vertex), &mVertices[0], GL_STATIC_DRAW);
  • GL_ARRAY_BUFFER는 정점 데이터를 위한 버퍼다.
  • Vertex는 구조체로서 다음과 같은 레이아웃을 가진다.
struct Vertex {
	Vec3 position;   // 3 floats
	Vec3 normal;     // 3 floats
	Vec2 texCoord;   // 2 floats
	Vec3 color;      // 3 floats
};

총 11 floats = 44 bytes 짜리 구조다.

OpenGL에게 이 구조를 설명해야 하며, mVertices.size만큼 설정해야 한다.

 

4. 인덱스 버퍼 구성 (EBO)

mAllIndices.clear();
for (const Face& face : mFaces) {
	mAllIndices.insert(mAllIndices.end(), face.vertexIndices.begin(), face.vertexIndices.end());
}
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mEBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, mAllIndices.size() * sizeof(unsigned int), mAllIndices.data(), GL_STATIC_DRAW);
  • 각 Face는 세 개의 정점 인덱스를 가진다 (삼각형)
  • 이 인덱스들을 하나의 벡터로 모아 glDrawElemets용으로 만든다.
  • GL_ELEMENT_ARRAY_BUFFER는 EBO의 명칭이다.

5. 정점 속성 포인터 설정

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, position));
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, normal));
glEnableVertexAttribArray(1);
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, texCoord));
glEnableVertexAttribArray(2);
glVertexAttribPointer(3, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, color));
glEnableVertexAttribArray(3);
glBindVertexArray(0);

 

  • layout 0 -> position: 3개의 float (x, y, z)
  • layout 1 -> normal: 3개의 float
  • layout 2 -> texCoord: 2개의 float
  • layout 3 -> color: 3개의 float
glVertexAttribPointer(index, size, type, normalize, stride, offset);
항목 설명
index 셰이더에서 layout(location = index)와 매칭됨
size 몇 개의 요소 (position은 3, texCoord는 2등)
type GL_FLOAT
normalize 정규화 여부 (일반적으로 false)
stride 하나의 정점  전체 크기
offset 구조체 내 해당 필드까지의 거리

 

6. 마무리

glBindVertexArray(0);

VAO를 바인딩 해제함으로써 설정을 마감한다.

이제 나중에 Draw() 함수에서 glBindVertexArray(mVAO)만 하면 바로 사용 가능한 상태가 되었다.


4. Draw() 함수

void Model::Draw(void)
{
	mShaderProgram->Use();
	glBindVertexArray(mVAO);

	float view[16];
	createLookAtMatrix({0, 0, 3}, {0, 0, 0}, {0, 1, 0}, view);

	float projection[16];
	createPerspectiveMatrix(45.0f, 1000.0f/1000.0f, 0.1f, 100.0f, projection);

	float model[16];
	setIdentityMatrix(model);

	float scaleMatrix[16] = {
		mScaleFactor, 0.0f, 0.0f, 0.0f,
		0.0f, mScaleFactor, 0.0f, 0.0f,
		0.0f, 0.0f, mScaleFactor, 0.0f,
		0.0f, 0.0f, 0.0f, 1.0f
	};
	float translate[16];
	createTranslationMatrix(mPosX, mPosY, mPosZ, translate);

	multiplyMatrix(model, scaleMatrix, model);
	rotateY(mRotY, model);
	rotateX(mRotX, model);
	multiplyMatrix(model, translate, model);

	UpdateBlend(mbUseTexture, 0.016f);

	mShaderProgram->SetMat4("model", model);
	// mShaderProgram->SetBool("useTexture",mbUseTexture);
	mShaderProgram->SetMat4("view", view);
	mShaderProgram->SetMat4("projection", projection);
	mShaderProgram->SetFloat("blendFactor", mBlendFactor);
	size_t indexOffset = 0;
	for (const Face& face : mFaces)
	{
		const auto it = mMaterials.find(face.mtlname);
		if (it != mMaterials.end())
		{
			const Material& mat = it->second;
			mShaderProgram->SetVec3("material.diffuse", mat.Kd.r, mat.Kd.g, mat.Kd.b);
			mShaderProgram->SetVec3("material.ambient", mat.Ka.r, mat.Ka.g, mat.Ka.b);
			mShaderProgram->SetVec3("material.specular", mat.Ks.r, mat.Ks.g, mat.Ks.b);
			mShaderProgram->SetFloat("material.shininess", mat.Ns);
		}
		glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, (void*)(indexOffset * sizeof(unsigned int)));
		indexOffset += 3;
	}
	if (mAllIndices.size() % 3 != 0) std::cerr << "⚠️ Broken EBO!" << std::endl;
	glBindVertexArray(0);
}

전체 흐름 요약

  1. 셰이더 활성화
  2. View, Projection, Model 행렬 구성 및 전달
  3. 재질 (Material) 속성 전달
  4. glDrawElements()로 실제 그리기
  5. VAO 해제

섹션 상세 설명

1. 셰이더 프로그램 활성화

mShaderProgram->Use();
glBindVertexArray(mVAO);

 

VAO를 바인딩함으로써 버퍼 세팅 정보를 불러오고,

셰이더 프로그램을 GPU에서 사용하도록 설정한다.

 

2.View Projection, Model 행렬 구성

float view[16];
createLookAtMatrix({0, 0, 3}, {0, 0, 0}, {0, 1, 0}, view);

float projection[16];
createPerspectiveMatrix(45.0f, 1000.0f/1000.0f, 0.1f, 100.0f, projection);
  • View: 카메라 위치를 기준으로 씬을 바라보는 시점 설정
  • Projection: 원근 투영(FOV 45도, near/far 평면)

3. 모델 변환 적용

setIdentityMatrix(model);  // 초기화
multiplyMatrix(model, scaleMatrix, model); // 스케일
rotateY(mRotY, model);                     // 회전
rotateX(mRotX, model);
multiplyMatrix(model, translate, model);  // 이동

정점 데이터를 모델 공간 -> 월드 공간으로 변환한다.

이 과정은 행렬 곱 순서가 매우 중요함

 

4. 유니폼 값 전달

mShaderProgram->SetMat4("model", model);
mShaderProgram->SetMat4("view", view);
mShaderProgram->SetMat4("projection", projection);
mShaderProgram->SetFloat("blendFactor", mBlendFactor);

셰이더에서 사용하는 매트릭스들은 모두 유니폼으로 전달되며,

셰이더에서는 이걸 통해 gl_Position = projection * view * model * vec4(...) 형태로 변환하게 된다.

 

5. 재질 정보 전달 및 드로우 호출

for (const Face& face : mFaces)
{
	const auto it = mMaterials.find(face.mtlname);
	if (it != mMaterials.end())
	{
		const Material& mat = it->second;
		mShaderProgram->SetVec3("material.diffuse", mat.Kd.r, mat.Kd.g, mat.Kd.b);
		mShaderProgram->SetVec3("material.ambient", mat.Ka.r, mat.Ka.g, mat.Ka.b);
		mShaderProgram->SetVec3("material.specular", mat.Ks.r, mat.Ks.g, mat.Ks.b);
		mShaderProgram->SetFloat("material.shininess", mat.Ns);
	}
	glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, (void*)(indexOffset * sizeof(unsigned int)));
	indexOffset += 3;
}
  • material.diffuse, specular, ambient 등은 프래그먼트 셰이더에서 조명 연산에 사용된다.
  • glDrawElements는 EBO에 있는 인덱스를 3개씩 참조하여 하나의 삼각형을 그림.

6. VAO 해제

glBindVertexArray(0);

VAO 바인딩 해제!

다른 모델을 그릴 수 있도록 준비해 주는 단계.

'Dev > Grapics' 카테고리의 다른 글

Scop 구현기 5편: 행렬과 사용 점  (0) 2025.04.17
Scop 구현기 3편: 셰이더 클래스를 만들기로 했다  (0) 2025.04.11
Scop 구현기 2편: VAO와 VBO의 세계  (0) 2025.04.09
Scop 구현기 1편: 윈도우 하나 띄우는 게 뭐라고 (Object Viewer)  (0) 2025.04.09
Rasterization vs Ray Tracing  (0) 2025.04.09
'Dev/Grapics' 카테고리의 다른 글
  • Scop 구현기 5편: 행렬과 사용 점
  • Scop 구현기 3편: 셰이더 클래스를 만들기로 했다
  • Scop 구현기 2편: VAO와 VBO의 세계
  • Scop 구현기 1편: 윈도우 하나 띄우는 게 뭐라고 (Object Viewer)
onepaperhoon
onepaperhoon
한장훈님의 블로그 입니다.
  • onepaperhoon
    OnePaperHoon Blog
    onepaperhoon
  • 전체
    오늘
    어제
    • 분류 전체보기 (14)
      • Dev (14)
        • System Programming (2)
        • Linux (0)
        • CS (0)
        • Network, Protocol (0)
        • Grapics (11)
        • Web, App (0)
        • Design Pattern (1)
      • Projects (0)
      • TIL (0)
      • Life (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • GitHub
  • 공지사항

  • 인기 글

  • 태그

    cpp
    Pipe
    싱글톤 패턴
    Vertex Shader
    그래픽스
    OpenGL
    C언어
    named pipe
    shader
    glfw
    3d
    graphics
    c++
    Game
    디자인 패턴
    공유 메모리
    graphicapi
    Process
    shared memory
    IPC
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
onepaperhoon
Scop 구현기 4편: Model 클래스
상단으로

티스토리툴바