[MODEL]
1. 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);
};
- Model class의 코드
- Mesh 객체들의 vector를 가지고 있고 생성자에 파일의 위치를 지정해야한다
: 생성자에서 호출되는 loadModel 함수를 통해 파일을 로드한다
- 이후 texture를 로드할 때 필요한 파일 경로의 디렉터리도 저장한다
void Draw(Shader &shader)
{
for(unsigned int i = 0; i < meshes.size(); i++)
meshes[i].Draw(shader);
}
- Draw 함수는 기본적으로 반복문을 사용하여 각 mesh들의 Draw 함수를 호출시킨다
2. Importing a 3D model into OpenGL
#include <assimp/Importer.hpp>
#include <assimp/scene.h>
#include <assimp/postprocess.h>
- mode을 불러오고 구조를 변환하기 위해서 Assimp의 적절한 헤더파일을 include 한다
- loadModel 함수는 생성자로부터 직접적으로 호출되어 사용한다
: 함수 내부에서 사용자는 scene 객체라고 불리는 Assimp의 데이터 구조에 model을 불러오기 위해 Assimp를 사용한다
: scene 객체를 가지게되면 불러온 모델로부터 사용자가 원하는 모든 데이터를 얻을 수 있다
Assimp::Importer importer;
const aiScene *scene = importer.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs);
- Assimp는 각자 다른 모든 파일 포멧들을 불러오는 것에 대한 기술적인 상세사항을 깔끔하게 추상화하고 있다
- 먼저 Assimp namesapce의 실제 Importer 객체를 선언한 후, ReadFile 함수를 호출한다
ReadFile
첫 번째 parameter : 파일의 경로
두 번째 parameter : 여러 전처리 옵션
- Assimp는 간단히 파일을 불러오는 것 외에도 불러온 데이터에 추가적인 계산/연산을 하는 여러 옵션들을 지정할 수 있도록 해준다
: aiProcess-Triangulate를 설정함으로써 Assimp에게 model이 삼각형으로만 이루어지지 않았다면 model의 모든 primitive 도형들을 삼각형으로 변환하라고 전달한다
: aiProcess-FlipUVs를 설정함으로써 texture coordinate를 y축으로 뒤집는다 (OpenGL에서 대부분의 이미지들은 y축을 중심으로 거꾸로 되기 때문에, 전처리 옵션을 사용해서 이 문제를 해결할 수 있다)
+) 추가적인 옵션
aiProcess_GenNormals
: 모델이 법선 벡터들을 가지고 있지 않다면 각 vertex에 대한 법선을 실제로 생성한다
aiProcess_SplitLargeMeshes
: 큰 mesh들을 여러개의 작은 서브 mesh들로 나눈다
: 렌더링이 허용된 vertex 수의 최댓값을 가지고 있을 때 유용하고 오직 작은 mesh들만 처리할 수 있다
aiProcess_OptimizeMeshes
: aiProcess_SplitLargeMeshes와 반대로 여러 mesh들을 하나의 큰 mesh로 합친다
: 최적화를 위해 Drawing 호출을 줄일 수 있다
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);
}
- 전체적인 loadModel 함수의 코드
- model을 불러온 후에 scene과 scene의 루트 노드가 null인지 확인하고 데이터가 불완전하다는 플래그가 세워져있는지 확인한다
: 에러 조건 중 하나라도 만족하면 importer의 GetErrorString 함수를 통해 에러를 출력하고 return한다
- 주어진 파일 경로의 디렉터리 경로를 얻는다
- scene의 노드들을 처리하기 위해 루트 노드를 재귀적으로 동작하는 processNode 함수로 전달한다
: 각 노드들은 자식 노드를 가질 수 있기 때문에 먼저 노드를 처리하고 이후 노드의 모든 자식 노드를 처리한다
void processNode(aiNode *node, const aiScene *scene)
{
// process all the node's meshes (if any)
for(unsigned int i = 0; i < node->mNumMeshes; i++)
{
aiMesh *mesh = scene->mMeshes[node->mMeshes[i]];
meshes.push_back(processMesh(mesh, scene));
}
// then do the same for each of its children
for(unsigned int i = 0; i < node->mNumChildren; i++)
{
processNode(node->mChildren[i], scene);
}
}
- 전체적인 processNode 함수의 코드
- 해당 노드의 mesh index들을 확인하고 scene의 mMeshes 배열을 인덱싱하여 그에 따른 mesh들을 얻는다
- 반환된 mesh는 processMesh 함수로 전달되고, 이 함수는 meshes vector에 저장할 수 있는 Mesh 객체를 return한다
- 모든 mesh들이 처리되면 노드의 모든 자식들에게 반복하고, 동일한 processNode 함수를 호출한다
: 더이상의 자식이 없다면 함수의 실행을 종료한다
3. Assimp to Mesh
Mesh processMesh(aiMesh *mesh, const aiScene *scene)
{
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
for(unsigned int i = 0; i < mesh->mNumVertices; i++)
{
Vertex vertex;
// process vertex positions, normals and texture coordinates
[...]
vertices.push_back(vertex);
}
// process indices
[...]
// process material
if(mesh->mMaterialIndex >= 0)
{
[...]
}
return Mesh(vertices, indices, textures);
}
- processMesh 함수의 전체적인 구조
- Mesh를 처리하는 것은 기본적으로 세 부분으로 이루어진다
01] 모든 vertex 데이터를 얻는다
02] mesh의 indices를 얻는다
03] 연관된 material 데이터를 얻는다
: 처리된 데이터는 3개의 vector 중 하나에 저장되고 함수를 호출한 곳으로 vector가 return된다
- vertex data는 각 루프를 돌 때마다 vertices 배열에 삽입할 vertex struct를 정의하고, mesh에 존재하는 vertex의 개수(mesh->mNumVertices를 통해 얻는다)만큼 반복문을 수행하여 반복문 내부에서 모든 관련된 데이터로 이 struct를 채워넣는 형식으로 얻을 수 있다
glm::vec3 vector;
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;
- vertex의 위치를 계산하는 코드
- Assimp 데이터를 변환하기 위해 vec3의 자리표시자를 지정한다
: Assimp는 vector, matrix, string 등을 자신만의 데이터 타입으로 관리하고 GLM의 데이터 타입으로 정상적으로 변환되지 않기 떄문이다
- Assimp는 직관적이지 않은 vertex 위치 배열 mVertices를 호출한다
vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;
- 법선 벡터를 계산하는 코드
if(mesh->mTextureCoords[0]) // does the mesh contain texture coordinates?
{
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);
- texture coordinate는 거의 비슷하지만 Assimp는 각 vertex마다 최대 8개의 texture를 허용한다
: 우리는 하나의 texture만 사용하기 때문에 첫 번째 texture coordinate만 사용한다
- mesh가 실제로 texture coordinate를 가지고 있는지 확인한다
- 앞서 사용한 모든 함수와 코드는 mesh의 각 vertex마다 수행된다
4. Indices
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]);
}
- Assimp의 인터페이스는 각 mesh들이 face의 배열을 가지고 있도록 정의했고, 각 face는 하나의 primitive를 나타낸다
: 우리는 aiProcess_Triangulate 옵션 때문에 항상 삼각형이다
- face는 어떠한 순서로 vertex를 그려야하는지를 정의하는 indices를 가지고 있다
: 따라서 모든 face에 대해 반복문을 돌려 모든 face의 indices를 indices vector에 저장해야한다
- 외부 loop가 끝나면 glDrawElements 함수를 통해 mesh를 그리기 위한 vertex, index data가 완벽히 설정된 것이다
5. Material
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());
}
- 노드와 마찬가지로 mesh는 오직 mataerial 객체의 index만 가지고 있다
: 실제 mesh의 material을 얻기 위해서는 scene의 mMaterial 배열을 인덱싱해야한다
- mesh의 material index는 mMaterialIndex 속성에 설정되어 있다
: 이 속성을 통해 mesh가 실제로 material을 가지는지에 대한 유무를 확인할 수 있다
- 먼저 scene의 mMaterials 배열로부터 aiMaterial 객체를 얻고, mesh의 diffuse, specular texture를 불러온다
- material 객체는 내부적으로 각 texture type에 대한 texture 위치의 배열을 저장힌디
: 여러 texture type들은 aiTextureType_접두사로 분류된다
- loadMaterialTextures 함수를 통해 material에서 texture를 얻을 수 있다
- texture struct의 vector를 return하고 이것을 model의 textures 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);
Texture texture;
texture.id = TextureFromFile(str.C_Str(), directory);
texture.type = typeName;
texture.path = str;
textures.push_back(texture);
}
return textures;
}
- loadMaterialTexture 함수는 주어진 texture type의 모든 texture 위치들에 대해 반복문을 돌리고 texture file의 위치를 얻은 다음 불러오고 texture를 생성하며 이 정보를 vertex struct에 저장한다
- 먼저 GetTextureCount 함수를 통해 이 material에 저장된 texture의 갯수를 확인한다
: GetTextureCount 함수는 texture type 중 하나를 parameter로 받는다
- GetTextureCount의 결과를 aiString에 저장하는 GetTexture 함수를 통해 각 texture file의 위치를 얻는다
- 마지막으로 TextureFromFile 함수를 통해 (SOIL과 함께) texture를 불러오고 이 texture의 ID를 return한다
+) model 파일의 texture file 경로가 model file 경로와 동일하다고 가정하고 진행하므로, 간단히 loadModel 함수에서 얻은 texture 위치 문자열과 디렉터리 문자열을 결합하여 완전한 텍스처 경로를 얻을 수 있다 (GetTexture 함수도 디렉터리 문자열을 필요로 하는 이유)
[AN OPTIMIZATION]
- 대부분의 scene들은 여러 mesh들에 여러가지 texture를 재사용한다
struct Texture {
unsigned int id;
string type;
string path; // we store the path of the texture to compare with other textures
};
- 불러온 모든 texture를 전역으로 저장하여 model 코드를 수정한다
- texture를 로드할 곳은 어느 곳에서 먼저 로드되지는 않았는지 확인하고, 먼저 로드가 되었다면 texture를 가져오고 전체적인 texture의 로딩 과정을 건너뛴다
: processing power를 절약할 수 있다
- texture를 비교하기 위해 texture의 경로를 저장하여 사용한다
vector<Texture> textures_loaded;
- 불러온 모든 texture를 model class의 맨 위에 private으로 선언된 또다른 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)
{ // if texture hasn't been loaded already, load it
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); // add to loaded textures
}
}
return textures;
}
- loadMaterialTexture 함수에서 texture 경로를 textures_loaded vector에 있는 모든 texture의 경로와 비교하여 현재 texture 경로가 다른 것들과 같은지를 확인한다
: 같다면 texture를 불러오고 생성하는 과정을 모두 생략하고 간단히 texture struct에 존재하는 것을 사용한다
[NO MORE CONTAINERS]
- 이제 컨테이너가 아닌 실제 3D model을 사용할 수 있다
- 코드에서 model 객체를 선언하고 model file의 위치를 전달하면 model은 자동적으로 불러와지고, game loop에서 Draw 함수를 사용하여 object가 그려진다
- 최종 출력 결과
'OpenGL Study' 카테고리의 다른 글
OpenGL "Stencil testing" (0) | 2023.02.01 |
---|---|
OpenGL "Depth Testing" (0) | 2023.01.31 |
OpenGL "Mesh" (0) | 2023.01.24 |
OpenGL "Assimp" (0) | 2023.01.24 |
OpenGL "Multiple Lights" (0) | 2023.01.24 |