OpenGL
[LearnOpenGL] 21. Model
coco_ball
2022. 8. 22. 22:50
Model class
class Model
{
public:
Model(char *path)
{
loadModel(path);
}
void Draw(Shader shader);
private:
// model data
vector<Mesh> meshes;
string directory;
void loadModel(string path); // 파일을 불러옴
void processNode(aiNode *node, const aiScene *scene);
Mesh processMesh(aiMesh *mesh, const aiScene *scene);
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type,
string typeName);
};
private 함수들은 Assimp의 import 루틴 일부분을 처리
Draw function
void Draw(Shader shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
Importing a 3D model into OpenGL
헤더 파일
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
Assimp namespace의 실제 Importer 객체 선언
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
ReadFile 함수
- 첫 번째 파라미터 : 파일의 경로
- 두 번째 파라미터 : post-processing(전처리)옵션
- aiProcess-Triangulate : 모델이 삼각형으로만 이루어지지 않았다면 모델의 모든 primitive 도형들을 삼각형으로 변환
- aiProcess-FlipUVs : 텍스처 좌표를 y축으로 뒤집음 ( OpenGL에서 대부분의 이미지들은 y축을 중심으로 거꾸로 되어있는데, 전처리 옵션으로 해결)
- aiProcess_GenNormals : 모델이 법선 벡터를 가지고 있지 않으면 각 vertex에 대한 법선 벡터 생성
- aiProcess_SplitLargeMeshes : 큰 mesh들을 여러개의 작은 서브 mesh로 나눔. 렌더링이 허용된 vertex 수의 최댓값을 가지고 있을 때 유용하고 오직 작은 mesh만 처리 가능
- aiProcess_OptimizeMeshes : 여러 mesh들을 하나의 큰 mesh로 합침. 최적화를 위해 드로잉 호출을 줄일 수 있음
- http://assimp.sourceforge.net/lib_html/postprocess_8h.html
loadModel function
void loadModel(string path)
{
Assimp::Importer import;
const aiScene *scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
if(!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
{
cout << "ERROR::ASSIMP::" << import.GetErrorString() << endl;
return;
}
directory = path.substr(0, path.find_last_of('/'));
processNode(scene->mRootNode, scene);
}
AI_SCENE_FLAGS_INCOMPLETE = 데이터가 불완전하다는 플래그?
processNode function(scene의 모든 노드를 처리하기 위해 재귀적으로 동작 ( 종료 조건 : 모든 노드가 처리 되었을 경우))
void processNode(aiNode *node, const aiScene *scene)
{
// 노드의 모든 mesh들을 처리(만약 있다면)
for(unsigned int i = 0; i < node->mNumMeshes; i++)
{
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// 그런 다음 각 자식들에게도 동일하게 적용
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
processMesh function(meshes vector에 저장할 수 있는 Mesh 객체를 리턴)
Assimp to Mesh
Mesh의 처리는 3-part process임
- vertex data를 얻음
- mesh의 indice를 얻음
- 연관된 material data를 얻음
처리된 데이터는 3개의 벡터 중 하나에 저장되고, Mesh가 만들어져서 리턴됨
- vertices
- 각 loop를 돌 때마다 vertices 배열에 삽입할 vertex struct 정의
- mesh→mNumVertices만큼 반복문 실행
- Assimp는 자신만의 데이터 타입으로 데이터를 관리하기 때문에 vec3를 정의하여 변환함
//vertex의 위치 glm::vec3 vector; vector.x = mesh->mVertices[i].x; vector.y = mesh->mVertices[i].y; vector.z = mesh->mVertices[i].z; vertex.Position = vector;
//normal vector vector.x = mesh->mNormals[i].x; vector.y = mesh->mNormals[i].y; vector.z = mesh->mNormals[i].z; vertex.Normal = vector;
- Assimp는 각 vertex마다 최대 8개의 텍스처를 허용하는데, 우리는 하나의 텍스처만 사용하기 때문에 첫 번째 텍스처 좌표에만 집중함
- mesh가 실제로 텍스처 좌표를 가지고 있는지도 확인해야함(항상 있는 것이 아님)
여기까지 하면 vertex struct는 모두 채워졌고, vertices vector에 삽입한다. (이 처리는 mesh의 각 vertex마다 수행)if(mesh->mTextureCoords[0]) // mesh가 텍스처 좌표를 가지고 있는가? { glm::vec2 vec; vec.x = mesh->mTextureCoords[0][i].x; vec.y = mesh->mTextureCoords[0][i].y; vertex.TexCoords = vec; } else vertex.TexCoords = glm::vec2(0.0f, 0.0f);
- Indices
- 각각의 face는 하나의 primitive를 나타내고, 우리는 aiProcess_Triangulate 때문에 항상 삼각형임
- face는 우리가 어떤 순서로 vertex를 그려야 하는지 정의하는 indices를 포함
- 모든 face에 대해 반복문을 돌려 각 face의 indices를 indices vector에 저장
여기까지 하면 mesh를 그리기 위한 vertex, index data가 설정됨for(unsigned int i = 0; i < mesh->mNumFaces; i++) { aiFace face = mesh->mFaces[i]; for(unsigned int j = 0; j < face.mNumIndices; j++) indices.push_back(face.mIndices[j]); }
- Material
- mesh의 material을 얻기 위해서는 scene의 mMaterial 배열을 인덱싱 해야함
material 객체는 텍스처 타입에 대한 텍스처 위치 배열을 저장 ( 텍스처 타입은 aiTextureType_ 접두사로 분류)if(mesh->mMaterialIndex >= 0) { aiMaterial *material = scene->mMaterials[mesh->mMaterialIndex]; vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse"); textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end()); vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular"); textures.insert(textures.end(), specularMaps.begin(), specularMaps.end()); }
TextureFromFile : 텍스처를 불러오고 ID를 리턴vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName) { vector<Texture> textures; for(unsigned int i = 0; i < mat->GetTextureCount(type); i++) { aiString str; mat->GetTexture(type, i, &str); Texture texture; texture.id = TextureFromFile(str.C_Str(), directory); texture.type = typeName; texture.path = str; textures.push_back(texture); } return textures; }
- loadMaterialTextures (material에서 텍스처를 얻고 texture struct의 벡터를 리턴)
An optimization
각각의 mesh마다 같은 텍스처가 사용됨에도 불구하고 mesh마다 새로운 텍스처가 불리고, 생성됨
→ 병목현상 발생 가능
불러온 텍스처들을 전역으로 저장하고 텍스처를 불러오고 싶을 때 이미 불러와졌는지 확인
Texture 수정
struct Texture {
unsigned int id;
string type;
string path; // 다른 텍스처와 비교하기 위해 텍스처의 경로를 저장
};
model class 맨 위에 private 벡터로 모든 텍스처 저장
vector<Texture> textures_loaded;
loadMaterialTextures (텍스처 경로를 textures_loaded vector에 있는 모든 텍스처 경로와 비교하고, 현재 텍스처 경로가 다른 것들과 같으면 텍스처를 불러오고 생성하는 부분은 생략)
vector<Texture> loadMaterialTextures(aiMaterial *mat, aiTextureType type, string typeName)
{
vector<Texture> textures;
for(unsigned int i = 0; i < mat->GetTextureCount(type); i++)
{
aiString str;
mat->GetTexture(type, i, &str);
bool skip = false;
for(unsigned int j = 0; j < textures_loaded.size(); j++)
{
if(std::strcmp(textures_loaded[j].path.data(), str.C_Str()) == 0)
{
textures.push_back(textures_loaded[j]);
skip = true;
break;
}
}
if(!skip)
{ // 텍스처가 이미 불러와져있지 않다면 불러옵니다.
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);
texture.type = typeName;
texture.path = str.C_Str();
textures.push_back(texture);
textures_loaded.push_back(texture); // 불러온 텍스처를 삽입합니다.
}
}
return textures;
}