상세 컨텐츠

본문 제목

SCOP : .obj 파일을 이용한 3D 오브젝트 렌더링

카테고리 없음

by Banjosh 2024. 9. 25. 17:04

본문

이전까지는 사각형에 텍스처를 입히는 작업을 했다. 사각형은 내가 직접 정점을 지정해 그릴 수 있었지만, 복잡한 3D 오브젝트는 이런 방식으로 그리기 어렵다. 그래서 보통 Blender 같은 모델링 프로그램으로 만든 .obj 파일을 이용해 3D 오브젝트를 렌더링한다.


.obj 파일의 구조

.obj 파일은 렌더링에 필요한 다양한 정보를 포함하고 있다. 그 정보들은 다음과 같다:

  • o: 오브젝트 이름
  • v: 정점의 위치
  • vn: 정점의 법선 벡터
  • vf: 정점의 텍스처 좌표
  • f: face를 이루는 정보의 모음 (정점의 위치 / 법선 벡터 / 텍스처 좌표)
  • mtl: 오브젝트의 재질 정보
  • s: 매끄러운 정도

Assimp 라이브러리와의 차별점

보통 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 구조체

파싱한 정보는 ObjInfo라는 객체에 저장된다. 이 객체는 VertexInfo, IndexInfo, Material로 나뉘어 각 정보들을 관리한다.

struct ObjInfo {
	VertexInfo vertexInfo;
	IndexInfo indexInfo;
	Material marterialInfo;
};
  • VertexInfo: 정점의 위치 정보 Pos를 가지고 있다.
  • IndexInfo: Face 구조체로 이루어진 정점의 인덱스 정보를 담고 있다.
  • Material: 오브젝트의 재질 정보를 담고 있으며, Ka, Kd, Ks, Ns 같은 정보가 포함되어 있다.

이렇게 파싱한 정보들을 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 클래스

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 파일을 렌더링한 결과, 검은색으로 그린 티포트가 잘 나타났다. 비록 아직 단색으로 그려서 입체감은 부족하지만, 그래도 그림다운 그림이 나오는 첫걸음을 뗀 것이다.

검은색으로 그린 teapot2.obj


앞으로의 작업 계획

  1. 오브젝트를 화면 정중앙에 배치한 후 회전이동 구현
  2. face가 구분되도록 각 면에 색 적용
  3. 특정 키를 눌렀을 때 텍스처 전환 (부드럽게 전환)

이 계획을 바탕으로 앞으로 추가 기능을 구현해 나갈 예정이다.