Assimp는 Open Asset Import Library의 약자로, fbx, obj, gltf 등 다양한 3d 모델 포맷을 지원한다.
OpenGL 프로그램에서 모델링 불러올 때는 웬만하면 이걸 사용하는 것 같다.
학교 수업에서도 사용했고, learnopengl.com에서도 이걸 소개하고 있다.
우선 얘가 사용하는 구조를 보자면 다음과 같다.
이 다이어그램 전체가 하나의 모델이라고 생각하면 된다.
언뜻 보기에는 좀 복잡한데, 천천히 살펴보자면
- 우선 중요한 점을 하나 짚고 가자면, 모델은 여러개의 메시로 이루어져있다.
- 모델의 모든 데이터는 Scene 안에 담겨있다. (material, mesh 등)
- 좌측을 보면 Scene에 Root node가 달려있고, 그 아래로 트리처럼 Child node들이 달려있는 것을 볼 수 있다.
- 각 node들은 mMeshes[] 를 가지고 있는데, 여기 들어있는 것은 실제 메시 오브젝트는 아니고 메시의 ID이다.
- 실제 메시는 항상 Scene이 들고 있다.
- 따라서 프로그램이 node를 순회하면서, node가 가리키고 있는 메시들을 로드하도록 할 수 있다.
- 우측을 보면 Scene에 Mesh가 달려있는데, 여기에 있는 것들이 실제 메시 오브젝트들이다.
- Scene에 Material이 달려있는데, 여기서 메시에 맞는 텍스쳐를 뽑아올 수 있다.
- Scene에 Face가 달려있는데, 각 폴리곤의 버텍스의 인덱스를 담고 있다.
node가 트리 구조로 되어있어, 재귀 구조로 함수를 호출함으로써 모델을 쉽게 로드할 수 있다.
이걸 프로젝트에 추가해주는 방법은,
https://learnopengl.com/Model-Loading/Assimp
LearnOpenGL - Assimp
Assimp Model-Loading/Assimp In all the scenes so far we've been extensively playing with our little container friend, but over time, even our best friends can get a little boring. In bigger graphics applications, there are usually lots of complicated and i
learnopengl.com
이 글의 하단 Building Assimp에서 확인할 수 있다.
Assimp를 추가한 뒤 모델의 로드와 렌더링을 맡을 클래스 Model을 새로 만들어주었다.
Model.h :
#pragma once
#include <vector>
#include <string>
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
#include "Mesh.h"
#include "Texture.h"
class Model
{
public:
void LoadModel(const std::string& fileName); // 모델을 메모리에 올린다.
void RenderModel(); // 렌더링한다.
void ClearModel(); // 메모리에서 내린다.
~Model();
private:
// 재귀호출되며 노드를 순회하기 위한 함수
void LoadNode(aiNode* node, const aiScene* scene);
// 실제 메시 오브젝트를 참조하여 버텍스 정보를 로드한다.
void LoadMesh(aiMesh* mesh, const aiScene* scene);
// 텍스쳐를 로드한다.
void LoadMaterials(const aiScene* scene);
std::vector<Mesh*> meshList;
std::vector<Texture*> textureList;
std::vector<unsigned int> meshToTex; // 메시에 맞는 머티리얼의 인덱스를 저장한다.
std::string modelName;
};
Model.cpp:
#include <iostream>
#include "Model.h"
#include <GL/glew.h>
void Model::LoadModel(const std::string& fileName)
{
// 모델 이름 추출
int firstSlashIdx = fileName.find('/', 0);
modelName = fileName.substr(0, firstSlashIdx);
Assimp::Importer importer;
// 2번째 인자는 타입이 unsigned int인데, 비트 or 연산을 사용한다. 자세한 내용은 후술
const aiScene* scene = importer.ReadFile("Models/" + fileName,
aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_GenSmoothNormals | aiProcess_JoinIdenticalVertices);
if (!scene)
{
std::cout << fileName << " Model 로드 실패 : " << importer.GetErrorString() << std::endl;
return;
}
LoadNode(scene->mRootNode, scene);
LoadMaterials(scene);
}
void Model::RenderModel()
{
// LoadMesh 함수에서 채워놓은 meshList를 순회하며 메시들을 렌더링한다.
for (size_t i = 0; i < meshList.size(); i++)
{
// 메시에 해당하는 머티리얼을 통해 텍스쳐를 가져와 사용한다.
unsigned int materialIndex = meshToTex[i];
if (materialIndex < textureList.size() && textureList[materialIndex])
textureList[materialIndex]->UseTexture();
meshList[i]->RenderMesh();
}
}
void Model::ClearModel()
{
for (size_t i = 0; i < meshList.size(); i++)
{
if (meshList[i])
{
delete meshList[i];
meshList[i] = nullptr;
}
}
for (size_t i = 0; i < textureList.size(); i++)
{
if (textureList[i])
{
delete textureList[i];
textureList[i] = nullptr;
}
}
}
void Model::LoadNode(aiNode* node, const aiScene* scene)
{
for (size_t i = 0; i < node->mNumMeshes; i++)
{
// node->mMeshes[i] : 메시 자체가 아니고, 메시의 ID를 의미한다.
// 실제 메시는 scene에 저장되어있기 때문에 이렇게 참조하게 된다.
LoadMesh(scene->mMeshes[node->mMeshes[i]], scene);
}
// 자식 노드들을 재귀호출을 통해 순회하며 메시를 쭉 로드한다.
for (size_t i = 0; i < node->mNumChildren; i++)
{
LoadNode(node->mChildren[i], scene);
}
}
// 실제로 VBO, IBO로 쏴줄 정보들을 구성한다.
void Model::LoadMesh(aiMesh* mesh, const aiScene* scene)
{
std::vector<GLfloat> vertices;
std::vector<unsigned int> indices;
for (size_t i = 0; i < mesh->mNumVertices; i++)
{
// position
vertices.push_back(mesh->mVertices[i].x);
vertices.push_back(mesh->mVertices[i].y);
vertices.push_back(mesh->mVertices[i].z);
// texture
if (mesh->mTextureCoords[0])
{
vertices.push_back(mesh->mTextureCoords[0][i].x);
vertices.push_back(mesh->mTextureCoords[0][i].y);
}
else // 존재하지 않을 경우 그냥 0을 넣어주기
{
vertices.push_back(0.f);
vertices.push_back(0.f);
}
// normal (aiProcess_GenSmoothNormals를 적용했기 때문에 없을 수가 없다.)
vertices.push_back(mesh->mNormals[i].x);
vertices.push_back(mesh->mNormals[i].y);
vertices.push_back(mesh->mNormals[i].z);
}
// indices 채워주기
for (size_t i = 0; i < mesh->mNumFaces; i++)
{
aiFace face = mesh->mFaces[i];
for (size_t j = 0; j < face.mNumIndices; j++)
{
indices.push_back(face.mIndices[j]);
}
}
Mesh* newMesh = new Mesh();
newMesh->CreateMesh(vertices, indices); // GPU의 VBO, IBO로 버텍스 정보를 쏴준다.
meshList.push_back(newMesh);
// meshList에 mesh를 채워줌과 동시에, meshToTex에는 그 mesh의 materialIndex를 채워준다.
// 이렇게 meshList와 meshToTex를 나란히 채워줌으로써 mesh와 맞는 material을 손쉽게 찾을 수 있다.
meshToTex.push_back(mesh->mMaterialIndex);
}
void Model::LoadMaterials(const aiScene* scene)
{
textureList.resize(scene->mNumMaterials);
for (size_t i = 0; i < scene->mNumMaterials; i++)
{
aiMaterial* material = scene->mMaterials[i];
textureList[i] = nullptr;
// 텍스쳐가 존재하는 지 먼저 확인
if (material->GetTextureCount(aiTextureType_BASE_COLOR))
{
aiString texturePath;
// 텍스쳐 경로를 가져오는 데 성공했다면
if (material->GetTexture(aiTextureType_BASE_COLOR, 0, &texturePath) == aiReturn_SUCCESS)
{
// 혹시나 텍스쳐 경로가 절대 경로로 되어있다면 그에 대한 처리
int idx = std::string(texturePath.data).rfind("/");
std::string textureName = std::string(texturePath.data).substr(idx + 1);
std::string texPath = "Models/" + modelName + "/textures/" + textureName;
textureList[i] = new Texture(texPath.c_str());
// 텍스쳐를 디스크에서 메모리로 로드, GPU로 쏴준다.
if (!textureList[i]->LoadTexture())
{ // 실패 시
std::cout << "텍스쳐 로드 실패 : " << texPath << std::endl;
delete textureList[i];
textureList[i] = nullptr;
}
}
}
// textureList에 텍스쳐를 담는데 실패했다면
if (!textureList[i])
{
textureList[i] = new Texture("plain.png"); // 흰색 텍스쳐로 대체
}
}
}
Model::~Model()
{
ClearModel();
}
16번째 줄을 보면 2번째 인자가 특이한 모양으로 들어갔는데, 실제 타입은 unsigned int이다.
enum, 즉 정수끼리 비트 OR 연산을 한 결과값을 인자로 받도록 만들어져있다.
여기서 넣은 4가지의 의미를 하나씩 살펴보자면,
- Triangulate : 폴리곤의 모양을 triangle로 통일한다. quad 등이 나온다면 triangle로 쪼갠다.
- FlipUVs : 텍스쳐를 임포트할 때 위 아래를 뒤집는다. 일반적인 이미지 파일과 OpenGL 텍스쳐의 y축이 서로 반대 방향이기 때문.
- GenSmoothNormals : 모델링에 노멀 정보가 없을 경우, 자동으로 만들어준다.
- JoinIdenticalVertices : 같은 위치에 두 개 이상의 버텍스가 있는 경우, 하나로 통합시킨다.
LoadNode() 함수는 상술한 트리 구조의 node들을 재귀적으로 순회하기 위한 함수이다.
LoadMesh() 함수는 실제로 버텍스 정보를 std::vector에 담아주는 함수이다.
결과적으로 vertices는 이렇게 채워질 것이다.
{ position.x, position.y, position.z, texcoord.u, texcoord.v, normal.x, normal.y, normal.z, (1번 버텍스)
position.x, position.y, position.z, texcoord.u, texcoord.v, normal.x, normal.y, normal.z, (2번 버텍스)
position.x, position.y, position.z, texcoord.u, texcoord.v, normal.x, normal.y, normal.z, (3번 버텍스) ...}
함수 하단에 보이다시피, vertices뿐만 아니라 indices도 채워준다.
각 mesh는 여러개의 face를 가지고있다.
이 face들을 하나씩 순회하면서, 각 face를 구성하는 index들을 indices에 push_back 해주는 것이다.
LoadMaterial() 함수는 Scene에 들어있는 텍스쳐들을 몽땅 로드해준다.
로드한 텍스쳐들은 textureList에 담아놓아서,
상단의 RenderModel() 함수에서 그 textureList를 통해 맞는 텍스쳐를 사용하도록 하였다.
이렇게 Model 클래스를 완성한 뒤, 바로 모델링을 하나 다운받아 띄워보기로 결심했다.
요즘 재밌게 하고 있는 게임인 니어: 오토마타의 주인공 2B 모델링을 사용해보기로 했다.
https://sketchfab.com/3d-models/2b-nier-automata-8b00fabb9d364c2da3c0c49ed79217ef
2b Nier automata - Download Free 3D model by kain - Sketchfab
hi here wip of my new 3d model i have been working on .2b from nier automata
sketchfab.com
main.cpp:
#define STB_IMAGE_IMPLEMENTATION
#include <iostream>
#include <vector>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm\gtc\matrix_transform.hpp>
#include <glm\gtc\type_ptr.hpp>
#include <assimp/Importer.hpp>
#include "Camera.h"
#include "Mesh.h"
#include "Shader.h"
#include "Window.h"
#include "Model.h"
#define WIDTH 1280
#define HEIGHT 720
Window* mainWindow;
Camera* camera;
GLfloat deltaTime = 0.f;
GLfloat lastTime = 0.f;
// Vertex Shader
static const char* vShaderPath = "Shaders/VertexShader.glsl";
// Fragment Shader
static const char* fShaderPath = "Shaders/FragmentShader.glsl";
std::vector<Mesh*> meshList;
std::vector<Shader*> shaderList;
Model* model_2B;
// 쉐이더 변수 핸들
GLuint loc_modelMat = 0;
GLuint loc_PVM = 0;
GLuint loc_sampler = 0;
// 쉐이더 컴파일
void CreateShader()
{
Shader* shader = new Shader;
shader->CreateFromFiles(vShaderPath, fShaderPath);
shaderList.push_back(shader);
}
void GetShaderHandles()
{
// 핸들 얻어오기
loc_modelMat = shaderList[0]->GetModelMatLoc();
loc_PVM = shaderList[0]->GetPVMLoc();
loc_sampler = shaderList[0]->GetSamplerLoc();
}
int main()
{
// GLFW 초기화
if (!glfwInit())
{
printf("GLFW 초기화 실패\n");
glfwTerminate();
return 1;
}
mainWindow = new Window(WIDTH, HEIGHT);
mainWindow->Initialise();
CreateShader();
GLfloat initialPitch = 0.f;
GLfloat initialYaw = -90.f; // 카메라가 -z축을 보고 있도록
camera = new Camera(glm::vec3(0.f,0.f,0.f), glm::vec3(0.f, 1.f, 0.f), initialYaw, initialPitch, 5.f, 0.3f);
model_2B = new Model();
std::string modelPath = "2b_nier_automata/scene.gltf";
model_2B->LoadModel(modelPath);
// 창이 닫힐 때까지 반복
while (!mainWindow->getShouldClose())
{
shaderList[0]->UseShader();
// 쉐이더 내부 변수 위치들 가지고오기
GetShaderHandles();
glUniform1i(loc_sampler, 0); // sampler를 0번 텍스쳐 유닛과 연결
GLfloat now = glfwGetTime();
deltaTime = now - lastTime;
lastTime = now;
// Get + Handle User Input
glfwPollEvents();
camera->keyControl(mainWindow->getsKeys(), deltaTime);
camera->mouseControl(mainWindow->getXChange(), mainWindow->getYChange());
// Clear the window
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glm::mat4 modelMat(1.0f);
modelMat = glm::translate(modelMat, glm::vec3(0.0f, -2.f, -5.f));
glUniformMatrix4fv(loc_modelMat, 1, GL_FALSE, glm::value_ptr(modelMat));
glm::mat4 view = camera->calculateViewMatrix();
glm::mat4 projection = glm::perspective(glm::radians(45.0f),
(GLfloat)mainWindow->getBufferWidth() / mainWindow->getBufferHeight(), 0.1f, 100.0f);
glm::mat4 PVM = projection * view * modelMat;
glUniformMatrix4fv(loc_PVM, 1, GL_FALSE, glm::value_ptr(PVM));
model_2B->RenderModel();
std::cout << "rendered model" << std::endl;
glUseProgram(0);
mainWindow->swapBuffers();
}
return 0;
};
이렇게해서 실행을 해봤는데.. 뭔가 이상했다. 흰색이어야 할 일부분이 빨간색으로 나왔다.
텍스쳐에서 뭔가 문제가 생긴 것 같아 뒤져보던 중, 이 모델링의 텍스쳐 중 single channel 텍스쳐가 있다는 것을 발견했다.
텍스쳐를 로드하는 코드를 짤 때 싱글 채널은 RGBA 중 R만 쓰는 것으로 처리해놓았는데, 이게 실제로는 빨간색을 의미하는 것이 아니고 grayscale을 의미하는 것이기 때문에 생긴 문제이다.
// 텍스쳐의 채널 수에 따라 맞는 포맷으로 쏴주기
GLenum const textureFormat[] = { GL_RED, GL_RG, GL_RGB, GL_RGBA };
glTexImage2D(
GL_TEXTURE_2D, 0, textureFormat[bitDepth - 1],
width, height, 0,
textureFormat[bitDepth - 1], GL_UNSIGNED_BYTE, texData);
문제가 되는 코드이다. bitDepth가 1인 경우, 즉 single channel인 경우 GL_RED 포맷으로 렌더링하게 되고, 쉐이더는 그걸 RGBA 중 R만 0이 아닌 것으로 판단하여 빨간색으로 렌더링하고 있던 것이다.
그래서 이 부분을 일종의 예외처리를 해주는 코드를 추가해주었다.
// 1채널 텍스처인 경우 3채널로 다시 로드
if (bitDepth == 1)
{
stbi_image_free(texData); // 기존 데이터 해제
texData = stbi_load(fileLocation, &width, &height, &bitDepth, STBI_rgb);
if (!texData)
{
std::cerr << "Failed to reload texture as 3-channel: " << fileLocation << std::endl;
return false;
}
bitDepth = 3; // 채널 수를 3로 설정
}
텍스쳐가 싱글 채널인 경우 강제로 3채널로 바꿔 다시 로드하도록 하였다.
Texture 클래스 전문:
Texture.h
#pragma once
#include <GL\glew.h>
#include "stb_image.h"
class Texture
{
public:
Texture(const char* fileLoc);
/// <summary>
/// 텍스쳐 메모리로 로드, GPU로 쏴주기
/// </summary>
bool LoadTexture();
/// <summary>
/// 0번 텍스쳐 유닛에 텍스쳐 물려주기
/// </summary>
void UseTexture();
void ClearTexture(); // 메모리에서 텍스쳐 내리기
~Texture();
private:
GLuint textureID;
int width, height, bitDepth;
const char* fileLocation;
};
Texture.cpp
#include "Texture.h"
#include "stb_image.h"
#include <stdexcept>
#include <iostream>
Texture::Texture(const char* fileLoc)
{
textureID = 0;
width = 0;
height = 0;
bitDepth = 0;
fileLocation = fileLoc;
}
bool Texture::LoadTexture()
{
unsigned char* texData = stbi_load(fileLocation, &width, &height, &bitDepth, 0);
if (!texData)
{
printf("Failed to find: %s\n", fileLocation);
return false;
}
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 1채널 텍스처인 경우 3채널로 다시 로드
if (bitDepth == 1)
{
stbi_image_free(texData); // 기존 데이터 해제
texData = stbi_load(fileLocation, &width, &height, &bitDepth, STBI_rgb);
if (!texData)
{
std::cerr << "Failed to reload texture as 3-channel: " << fileLocation << std::endl;
return false;
}
bitDepth = 3; // 채널 수를 3로 설정
}
// 텍스쳐의 채널 수에 따라 맞는 포맷으로 쏴주기
GLenum const textureFormat[] = { GL_RED, GL_RG, GL_RGB, GL_RGBA };
glTexImage2D(
GL_TEXTURE_2D, 0, textureFormat[bitDepth - 1],
width, height, 0,
textureFormat[bitDepth - 1], GL_UNSIGNED_BYTE, texData);
//glGenerateMipmap(GL_TEXTURE_2D);
glBindTexture(GL_TEXTURE_2D, 0);
stbi_image_free(texData);
return true;
}
void Texture::UseTexture()
{
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID);
}
void Texture::ClearTexture()
{
glDeleteTextures(1, &textureID);
textureID = 0;
width = 0;
height = 0;
bitDepth = 0;
fileLocation = "";
}
Texture::~Texture()
{
ClearTexture();
}
수정 결과:
색깔이 제대로 나와주는 것을 볼 수 있었다.
'OpenGL > 공부' 카테고리의 다른 글
[OpenGL] FrameBuffer 생성, 사용 (0) | 2024.07.16 |
---|---|
[OpenGL] ImGuizmo 설치, 사용법 (0) | 2024.07.16 |
[OpenGL] Phong Reflection Model - 구현 (0) | 2024.06.19 |
[OpenGL] Normal Transformation (Normal Matrix) (0) | 2024.06.15 |
[OpenGL] Phong Reflection Model - 개요 (1) | 2024.06.15 |