이전까지는 사각형에 텍스처를 입히는 작업을 했다. 사각형은 내가 직접 정점을 지정해 그릴 수 있었지만, 복잡한 3D 오브젝트는 이런 방식으로 그리기 어렵다. 그래서 보통 Blender 같은 모델링 프로그램으로 만든 .obj 파일을 이용해 3D 오브젝트를 렌더링한다.
.obj 파일은 렌더링에 필요한 다양한 정보를 포함하고 있다. 그 정보들은 다음과 같다:
보통 Assimp 라이브러리를 사용하면 .obj 파일의 모든 정보를 쉽게 가져올 수 있다. 하지만 이번 과제에서는 v, f, mtl 정도만 파싱하는 함수를 직접 구현했다.
다만, 유지보수와 확장성을 고려해 클래스 기반으로 구현했다. 이를 통해 나중에 vn이나 vt 같은 추가 정보도 쉽게 파싱할 수 있게 설계했다.
glload 클래스에서 .obj 파일을 읽고 파싱하는 함수를 구현했다. 간단한 흐름을 보면, 파일 확장자를 확인한 후 파일을 열어 한 줄씩 파싱을 진행했다. 이때 Line의 첫 번째 요소에 따라 다형성을 사용해 IObjLine 객체를 생성하고, 해당 객체에서 parsingLine() 함수를 호출하여 정보를 파싱한다. 이 방식 덕분에 유연한 구조를 만들어 나중에 추가적인 요소들을 쉽게 다룰 수 있게 되었다.
std::unique_ptr<glload::ObjInfo> glload::loadObjFile(const std::string& fileName) {
if (!checkFileExtension(fileName, ".obj")) {
std::cerr << "this file is not .obj file" << std::endl;
return nullptr;
}
std::ifstream fin(fileName);
if (!fin.is_open()) {
std::cerr << fileName << " is not exist" << std::endl;
return nullptr;
}
std::unique_ptr<ObjInfo> objInfo(new ObjInfo());
std::string line;
while (std::getline(fin, line)) {
std::stringstream ss(line);
std::unique_ptr<IObjLine> objLine = generateLine(ss, fileName);
if (!objLine) { continue ;}
if (!objLine->parsingLine(objInfo.get())) { return {}; }
}
return objInfo;
}
파싱한 정보는 ObjInfo라는 객체에 저장된다. 이 객체는 VertexInfo, IndexInfo, Material로 나뉘어 각 정보들을 관리한다.
struct ObjInfo {
VertexInfo vertexInfo;
IndexInfo indexInfo;
Material marterialInfo;
};
이렇게 파싱한 정보들을 ObjInfo 구조체에 저장한 후 Model 클래스에서 이 정보를 토대로 Mesh를 생성해 렌더링을 진행한다.
struct VertexInfo {
std::vector<Pos> vPosInfo;
// std::vector<Normal> vNormalInfo;
// std::vector<TexCoord> vTexInfo;
};
struct IndexInfo {
std::vector<Face> faces;
};
struct Material {
float Ka[3] { 0.2f, 0.2f, 0.2f };
float Kd[3] { 0.8f, 0.8f, 0.8f };
float Ks[3] { 0.5f, 0.5f, 0.5f };
float Ns { 50.0f };
float Ni { 1.0f };
float d { 1.0f };
uint32_t illum { 2 };
};
Model 클래스는 ObjInfo로부터 필요한 정보를 받아 Mesh를 생성하고, draw 함수를 통해 오브젝트를 화면에 그린다. 이 과정에서 정점과 인덱스 정보를 바탕으로 3D 오브젝트를 그리게 된다.
#include "../include/model.h"
std::unique_ptr<Model> Model::create(std::string fileName) {
std::unique_ptr<Model> model(new Model());
if (!model->createMeshes(fileName)) {
return nullptr;
}
return model;
}
bool Model::createMeshes(const std::string& fileName) {
std::unique_ptr<glload::ObjInfo> objInfo = glload::loadObjFile(fileName);
if (!objInfo) {
std::cerr << fileName << " is invalid .obj file" << std::endl;
return false;
}
std::vector<Vertex> vertices;
std::vector<uint32_t> indices;
for (int i = 0; i < objInfo->vertexInfo.vPosInfo.size(); i++) {
vertices.push_back(Vertex{glmath::vec3(objInfo->vertexInfo.vPosInfo[i].x,
objInfo->vertexInfo.vPosInfo[i].y,
objInfo->vertexInfo.vPosInfo[i].z),
glmath::vec2(0.0f, 0.0f)});
}
for (int j = 0; j < objInfo->indexInfo.faces.size(); j++) {
indices.push_back(objInfo->indexInfo.faces[j].index[0]);
indices.push_back(objInfo->indexInfo.faces[j].index[1]);
indices.push_back(objInfo->indexInfo.faces[j].index[2]);
}
m_meshes.push_back(Mesh::createMesh(vertices, indices, objInfo.get()));
return true;
}
void Model::draw() {
for (std::unique_ptr<Mesh>& mesh : m_meshes) {
mesh->draw();
}
}
드디어 사각형이 아닌 오브젝트를 그릴 수 있게 되었다! 과제에서 제공된 teapot2.obj 파일을 렌더링한 결과, 검은색으로 그린 티포트가 잘 나타났다. 비록 아직 단색으로 그려서 입체감은 부족하지만, 그래도 그림다운 그림이 나오는 첫걸음을 뗀 것이다.
이 계획을 바탕으로 앞으로 추가 기능을 구현해 나갈 예정이다.