Object Loading (Assimp)
카테고리: OpenGL
태그: Graphics
Main Reference
- Learn OpenGL
- Rinthel Kwon - OpenGL Lecture
Assimp Library
Assimp(Asset Import) Library는 3D 모델의 다양한 포맷을 로드할 수 있게 지원하는 오픈 소스 라이브러리이다. C/C++ 인터페이스로 구성되어 있으며 임포트된 에셋은 모델 파일 포맷과 관계 없이 유지되기 때문에 cross-platform을 지원한다. Assimp 라이브러리의 구조는 대략 위 그림과 같다. Scene 객체에 Root Node, Meshes, Materials가 존재하고, Root Node는 자식 노드들을 가지고, 자식 노드 역시 자식 노드를 갖는 트리 구조로 이루어져 잇다.
Model | Model Architecture |
---|---|
Assimp 라이브러리가 트리 구조로 이루어진 이유는 대부분의 모델들이 부모/자식 관계로 이루이져 있기 때문이다. 위 예시를 보면 캐릭터의 각 파트의 transform 정보는 부모 파트의 local coordinate 기준으로 기술된다.
Model Import
// mesh.h
#ifndef __MESH_H__
#define __MESH_H__
struct Vertex {
glm::vec3 position;
glm::vec3 normal;
glm::vec2 texCoord;
};
CLASS_PTR(Material);
class Material {
public:
static MaterialUPtr Create() {
return MaterialUPtr(new Material());
}
TexturePtr diffuse;
TexturePtr specular;
float shininess { 32.0f };
void SetToProgram(const Program* program) const;
private:
Material() {}
};
CLASS_PTR(Mesh);
class Mesh {
public:
static MeshUPtr Create(
const std::vector<Vertex>& vertices,
const std::vector<uint32_t>& indices,
uint32_t primitiveType);
const VertexLayout* GetVertexLayout() const { return m_vertexLayout.get(); }
BufferPtr GetVertexBuffer() const { return m_vertexBuffer; }
BufferPtr GetIndexBuffer() const { return m_indexBuffer; }
void SetMaterial(MaterialPtr material) { m_material = material; }
MaterialPtr GetMaterial() const { return m_material; }
void Draw(const Program* program) const;
private:
Mesh() {}
void Init(
const std::vector<Vertex>& vertices,
const std::vector<uint32_t>& indices,
uint32_t primitiveType);
uint32_t m_primitiveType { GL_TRIANGLES };
VertexLayoutUPtr m_vertexLayout;
BufferPtr m_vertexBuffer;
BufferPtr m_indexBuffer;
MaterialPtr m_material;
};
#endif // __MESH_H__
- 하나의 모델은 트리 구조로 이루어진 여러 mesh들로 이루어져 있음
- VBO, VAO, 바인딩, Draw 등 메시를 그리는 과정에 필요한 작업들 전부 mesh 클래스 내부로 이동
- VAO는 unique pointer, VBO, IBO는 shared pointer 사용한 이유?
- VBO, IBO는 다른 VAO와 바인딩해 재사용 할 수 있지만, VAO는 해당 메시에서만 사용
.obj 파일을 뜯어보면..
- mtl(material) file name
- v : vertex position
- vt : texture coord
- vn : normal vector
- f : face(한 면)을 이루는 삼각형 vertex 3개 (line of position/texcoord/normal)
// model.h
#ifndef __MODEL_H__
#define __MODEL_H__
CLASS_PTR(Model);
class Model {
public:
static ModelUPtr Load(const std::string& filename);
int GetMeshCount() const { return (int)m_meshes.size(); }
MeshPtr GetMesh(int index) const { return m_meshes[index]; }
void Draw(const Program* program) const;
private:
Model() {}
bool LoadByAssimp(const std::string& filename);
void ProcessMesh(aiMesh* mesh, const aiScene* scene);
void ProcessNode(aiNode* node, const aiScene* scene);
std::vector<MeshPtr> m_meshes;
std::vector<MaterialPtr> m_materials;
};
- Assimp library를 이용해 로드한 모델은 ai(asset importer) - Node, Mesh, Material 변수 타입으로 저장
- Draw 함수의 경우 단순하게 각 메시들의 draw 함수 호출
bool Model::LoadByAssimp(const std::string& filename) {
Assimp::Importer importer;
auto scene = importer.ReadFile(filename, aiProcess_Triangulate | aiProcess_FlipUVs);
if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode) {
SPDLOG_ERROR("failed to load model: {}", filename);
return false;
}
auto dirname = filename.substr(0, filename.find_last_of("/"));
auto LoadTexture = [&](aiMaterial* material, aiTextureType type) -> TexturePtr {
if (material->GetTextureCount(type) <= 0)
return nullptr;
aiString filepath;
material->GetTexture(aiTextureType_DIFFUSE, 0, &filepath);
auto image = Image::Load(fmt::format("{}/{}", dirname, filepath.C_Str()));
if (!image)
return nullptr;
return Texture::CreateFromImage(image.get());
};
for (uint32_t i = 0; i < scene->mNumMaterials; i++) {
auto material = scene->mMaterials[i];
auto glMaterial = Material::Create();
glMaterial->diffuse = LoadTexture(material, aiTextureType_DIFFUSE);
glMaterial->specular = LoadTexture(material, aiTextureType_SPECULAR);
m_materials.push_back(std::move(glMaterial));
}
ProcessNode(scene->mRootNode, scene);
return true;
}
- LoadByAssimp 함수는 기본 Scene 객체 로드.
- m_materials에 material(texture) 추가
- LoadTexture 함수는 lamda를 이용해 함수 내부에 구현
- 루트 노드를 이용해 ProcessNode 함수 호출
void Model::ProcessNode(aiNode* node, const aiScene* scene) {
for (uint32_t i = 0; i < node->mNumMeshes; i++) {
auto meshIndex = node->mMeshes[i];
auto mesh = scene->mMeshes[meshIndex];
ProcessMesh(mesh, scene);
}
for (uint32_t i = 0; i < node->mNumChildren; i++) {
ProcessNode(node->mChildren[i], scene);
}
}
void Model::ProcessMesh(aiMesh* mesh, const aiScene* scene) {
SPDLOG_INFO("process mesh: {}, #vert: {}, #face: {}",
mesh->mName.C_Str(), mesh->mNumVertices, mesh->mNumFaces);
std::vector<Vertex> vertices;
vertices.resize(mesh->mNumVertices);
for (uint32_t i = 0; i < mesh->mNumVertices; i++) {
auto& v = vertices[i];
v.position = glm::vec3(mesh->mVertices[i].x, mesh->mVertices[i].y, mesh->mVertices[i].z);
v.normal = glm::vec3(mesh->mNormals[i].x, mesh->mNormals[i].y, mesh->mNormals[i]. z);
v.texCoord = glm::vec2(mesh->mTextureCoords[0][i].x, mesh->mTextureCoords[0][i].y);
}
std::vector<uint32_t> indices;
indices.resize(mesh->mNumFaces * 3);
for (uint32_t i = 0; i < mesh->mNumFaces; i++) {
indices[3*i ] = mesh->mFaces[i].mIndices[0];
indices[3*i+1] = mesh->mFaces[i].mIndices[1];
indices[3*i+2] = mesh->mFaces[i].mIndices[2];
}
auto glMesh = Mesh::Create(vertices, indices, GL_TRIANGLES);
if (mesh->mMaterialIndex >= 0)
glMesh->SetMaterial(m_materials[mesh->mMaterialIndex]);
m_meshes.push_back(std::move(glMesh));
}
- 각 노드별로 노드에 포함되는 메시 생성 후 자식 노드로 ProcessNode 재귀 호출
- assimp로 로드한 aiMesh 객체는 이미 vertices position/normal/texCoord 심지어 indices까지 구분해 놓음
- 위 데이터를 바탕으로 우리 프로젝트에서 사용하는 mesh 객체 생성
Result
댓글 남기기