Post Lists

2018년 7월 17일 화요일

Model Loading - Model (3)

https://learnopengl.com/Model-Loading/Model

Model
이제 Assimp로 우리의 손을 더럽힐 시간이고, 실제 loading과 traslation code를 만들기 시작할 시간이다. 이 튜토리얼의 목표는 완전한 한 모델을 나타내는 다른 클래스를 만드는 것이다. 즉, 다양한 메쉬들을 포함하는 한 모델을, 가능하게 다양한 오브젝트들과 함께. 나무 발코니를 포함하는 한 집, 타워, 그리고 아마 수영장은 여전히 단일의 모델로서 불러와 질 수 있다. 우리는 Assimp로 그 모델을 불러오고, 그것을 우리가 지난 튜토리얼에서 만든 다양한 Mesh objects로 바꿀 것이다.

더 야단법석 떨 필요 없이, 나는 너에게 Model class의 클래스 구조를 보여줄 것이다:


class Model
{
public:
 Model(char* path)
 {
  loadModel(path);
 }

 void Draw(Shader* shader);
private:
 /* Model Data */
 std::vector<Mesh> m_meshes;
 std::string directory;

 /* Functions */
 void loadModel(std::string path);
 void processNode(aiNode* node, const aiScene* scene);
 Mesh processMesh(aiMesh* mesh, const aiScene* scene);
 std::vector<Texture> loadMaterialTextures(aiMaterial* mat, 
                                                  aiTextureType type, std::string typeName);
};

Model 클래스는 Mesh objects의 한 벡터를 포함하고, 우리에게 그것에게 그것의 생성자에 파일 위치를 주는 것을 요구한다. 그것은 그러고나서 file을 생성자에서 호출되는 loadModel 함수를 통해 불러온다. private functions은 모두 Assimp의 import routine의 일부를 처리하기 위해 디자인되었고, 우리는 그것들을 곧 다룰 것이다. 우리는 또한 나중에 텍스쳐를 불러올 때 필요한 파일 경로의 directory를 저장한다.

Draw 함수는 특별한 것이 없다 그리고 기본적으로 그것들의 개별 Draw function을 호출하기 위해 메쉬 각각에 대해 루프를 돈다:


void Model::Draw(Shader* shader)
{
 for (unsigned int i = 0; i < m_meshes.size(); ++i)
  m_meshes[i].Draw(shader);
}

Importing a 3D model into OpenGL
한 모델을 불러오고, 그것의 우리의 구조로 바꾸기 위해서, 우리는 처음에 Assimp의 적절한 헤더들을 include 할 필요가 있다. 그래야 그 컴파일러가 우리에게 불평하지 않을 것이다:

  #include <assimp/impoter.hpp>
  #include <assimp/scene.h>
  #include <assimp/postprocess.h>

우리가 호출한 첫 번째 함수는 생성자로부터 직접 호출되는 loadModel이다. loadModel 내에서 우리는 모델을 scene object라고 불리는 Assimp의 자료구조로 불러오기위해 Assimp를 사용한다. 너는 model loading series의 첫 번째 튜토리얼로부터 이것이 Assimp의 자료구조의 root object라는 것을 기억할지도 모른다. 우리가 scene object를 가지기만 한다면, 우리는 불러와진 모델로부터 우리가 필요한 모든 데이터에 접근할 수 있따.

Assimp에 대한 훌륭한 것은 그것의 깔끔히 모든 다른 파일 포맷을 불러오는 기술적 세부사항으로부터 추상화되어있고, 단 한줄로 이것을 한다는 것이다:

  Assimp::Importer importer;
  const aiScene* scene = importer.ReadFile(path, ai_Process_Triangulate | aiProcess_FlipUVs);

우리는 처음에 Assimp의 namespace로부터 실제 Importer object를 선언하고, 그러고나서 그것의 ReadFile function을 호출한다. 그 함수는 파일 경로를 기대하고, 그것의 두 번째 인자로 몇 가지 후처리 옵션들을 기대한다. 간단히 파일을 불러오는 것을 제쳐두고, Assimp는 우리가 몇가지 옵션들을 명시하는 것을 허용한다. 그 옵션들은 Assimp가 불러와진 데이터에 대핸 몇 가지 추가 계산/연산을 하도록 한다. ai_Process_Triangulate를 설정하여, 우리는 Assimp에게 만약 그 모델이 (전적으로) 삼각형으로 구성되어있지 않으면, 그것은 모든 그 모델의 primitive shapes를 triangles로 바꿔야한다고 말한다. aiProcess_FlipUVs는 텍스쳐 좌표를 y축에 대하여 뒤집는다. 이것은 처리하는 동안에 필요하다 (너는 Texture 튜토리얼로부터 OpenGL에 있는 대부분의 이미지들은 y축이 반전되어있는 것을 기억할지도 모른다. 그래서 이 작은 postprocessing option은 우리를 위해 이것은 고쳐준다). 몇 가지 다른 유용한 옵션들이 있다:


  • aiProcess_GenNormals : 만약 그 모델이 normal vectors를 안가지고 있다면 각 vertex에 대해 normals들을 만든다.
  • aiProcess_SplitLargeMeshes : 큰 메쉬들을 더 작은 하위 메쉬들로 분해한다. 이것은 만약 너가 허용되는 최대 정점 개수를 가지고, 오직 더 작은 메쉬들만 처리할 수 있다면 유용하다.
  • aiProcess_OptimizeMeshes : 몇 가지 메쉬들을 하나의 큰 메쉬들로 합쳐서 실제로 반대의 것을 한다. 이것은 최적화를 위해 drawing calls을 줄여준다.
Assimp는 훌륭한 후처리 명령어 집합을 제공한다. 그래서 너는 여기에서 그것들 모두를 볼 수 있다. 실제로 Assimp를 통해 model를 불러오는 것은 놀라도록 쉽다 (너도 보듯이). 힘든 일은 불러온 데이터를 Mesh 객체의 한 배열로 이동시키기 위해 반환된 오브젝트를 사용할 때에 있다.

완전한 loadModel 함수는 여기에 기재되어있다.


void Model::loadModel(std::string path)
{
 Assimp::Importer import;
 const aiScene* scene = import.ReadFile(path, aiProcess_Triangulate | aiProcess_FlipUVs | aiProcess_GenNormals);

 if (!scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode)
 {
  std::cout << "ERROR::ASSIMP::" << import.GetErrorString() << '\n';
  return;
 }

 directory = path.substr(0, path.find_last_of('/'));
 
 processNode(scene->mRootNode, scene);
}

우리가 model을 불러온 후에, 우리는 scene과 scene의 root node가 null이 아닌지를 체크하고, 반환된 데이터가 불완전한지를 보기위해 그것의 플래그들 중 하나를 체크한다. 이러한 에러 조건 중 어느것이라도 충족된다면, 우리는 importer의 GetErrorString 함수를 통해 에러를 보고하고 return한다. 우리는 또한 주어진 파일 경로의 directory path를 얻는다.

만약 어떤 것도 잘못된게 없으면, 우리는 scene의 모든 노드들을 처리하고 싶다. 그래서 우리는 재귀 processNode 함수에 첫 번째 노드 (root node)를 넘겨준다. 각 노드는 (가능하게도) 자식들의 집합을 포함하고 있기 때문에,  문제가 되는 노드를 처음에 처리하고 싶어하고, 그러고나서 모든 노드의 자식들을 계속해서 처리한다. 등등. 이것은 재귀 구조에 맞는다. 그래서 우리는 재귀 함수를 정의할 것이다. 재귀 함수는 어떤 것을 하고 반복적으로 같은 함수를 다른 인자를 가지고 호출하는 함수이다. 이것은 어떤 특정 조건이 만족될 때 까지 한다. 우리의 경우에 exit condition은 모든 노드들이 처리되었을 때 만족된다.

너가 Assimp의 구조로부터 기억하듯이 각 노드는 mesh 인덱스들의 집합을 포함한다. 거기에서 각 인덱스는 scene object에 위치한 특정한 mesh를 가리킨다. 우리는 따라서 이러한 mesh 인덱스들을 얻어오고 싶고, 각 메쉬를 얻어오고, 각 메쉬를 처리하고 그러고나서 그 노드의 자식 노드들 각각에 또 다시 이 모든 것을 한다. processNode 함수의 내용은 아래처럼 보여진다:


void Model::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]];
  m_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);
}

우리는 처음에 노드의 메쉬 인덱스들의 각각을 체크하고, scene의 mMeshes 배열을 인덱싱하여 그에 상응하는 메쉬를 얻어온다. 그 반환된 메쉬는 그러고나서 processMesh 함수로 넘겨진다. 이 함수는 m_meshes  list/vector에 우리가 저장할 수 있는 Mesh object를 반환한다.

모든 메쉬들이 처리되기만 ㅎ나다면, 우리는 그 노드들의 모든 자식들에 대해 반복하고, 그 노드의 자식들에 대해 같은 processNode를 호출한다. 한 노드가 더 이상 자식이 없다면 그 함수는 실행을 멈춘다.

  Green Box
  세심한 독자는 우리가 기본적으로 어떤 노드들을 처리하는지에 잊었을 것이고 간단히 인덱스를 가지고 이 모든 복잡한 것을 하지 않고 직접적으로 scene의 모든 메쉬들을 통해 반복할 수 있다는 것을 눈치챘을지도 모른다. 우리가 이것을 하는 이유는 이 처럼 노드들을 사용하는 초기 아이디어가, 그것이 메쉬들 사이에 부모 자식들 관계를 정의한다는 것이다. 재귀적으로 이러한 관계들을 반복하여, 우리는 실제로 어떤 메쉬들이 다른 메쉬들의 부모가 되도록 정의할 수 있다.
그러한 시스템의 usecase는 너가 자동차 메쉬를 옮기고 싶고, 모든 그것의 자식들(엔진 메쉬, steering wheel mesh, 그리고 그것의 타이어 메쉬)들이 이동하도록 또한 하고 싶을 때; 그러한 시스템은 parent-child 관계를 사용하여 만들어진다.
지금당장 그러나, 우리는 그러한 시스템을 사용하지 않지만, 너의 메쉬 데이터에 대해 추가 통제력을 원할 때 마다, 이러한 접근법을 고수하는 것이 일반적으로 추천되어진다. 이러한 노드같은 관계는 결국 그 모델들을 만든 아티스트에 의해서 정의된다.

다음 단계는 실제로 Assimp data를 우리가 지난 튜토리얼에서 만든 Mesh class로 처리하는 것이다.

Assimp to Mesh
한 aiMesh 오브젝트를 우리의 mesh object로 바꾸는 것은 너무 어렵지는 않다. 우리가 해야할 모든 것은 그 메쉬의 관계된 properties 각각에 접근하고, 그것들을 우리 자신의 오브젝트에 담는 것이다. processMesh 함수의 일반적 구조는 그래서 이렇게 된다:


Mesh Model::processMesh(aiMesh* mesh, const aiScene* scene)
{
 std::vector<Vertex> vertices;
 std::vector<unsigned int> indices;
 std::vector<Texture> textures;

 for (unsigned int i = 0; i < mesh->mNumVertices; ++i)
 {
  Vertex vertex;
  // process vertex positions, normals and texture coordintaes

  vertices.push_back(vertex);
 }

 if (mesh->mMaterialIndex >= 0)
 {

 }

 return Mesh(vertices, indices, textures);
}

한 메쉬를 처리하는 것은 3 가지 부분으로 구성된다 : 모든 vertex data를 가져오고, 메쉬의 인덱스들을 가져오고, 마지막으로 관계된 material 데이터를 가져오는 것. 처리된 데이터는 3개의 벡터중의 하나에 저장되고, 그러한 것들로 부터 하나의 Mesh가 만들어지고 함수 호출자에게 반환된다.

vertex data를 받아오는 것은 꽤 간단하다: 우리는 매 반복마다 vertices array에 추가할 Vertex 구조체를 선언한다. 우리는 메쉬 내에서 (mesh->mNumVertices를 통해 얻어진) 많은 정점들이 존재하는 만큼 반복한다. 그 반복 내에서, 우리는 그러고나서 이러한 구조체를 모든 관련된 데이터로 채우기를 원한다. vertex position에 대해서 이것은 다음과 같이 된다:


glm::vec3 vector;
vector.x = mesh->mVertices[i].x;
vector.y = mesh->mVertices[i].y;
vector.z = mesh->mVertices[i].z;
vertex.Position = vector;

우리가 Assimp의 데이터를 옮기기 위해 vec3 placeholder를 정의한 것에 주목해라. 우리는 Assimp가 vector, matrices, strings 등등에 대해 그것 자신의 데이터 타입을 유지하기 떄문에 placeholder가 필요하다. 그리고 그것들은 glm의 데이터 타입과 잘 변환되지 않는다.

  Green Box
  Assimp는 정말 직관적이지 않은 그것의 vertex position array를 mVertices이라고 부른다.

normals에 대한 절차는 놀랍지않게 다가온다:


vector.x = mesh->mNormals[i].x;
vector.y = mesh->mNormals[i].y;
vector.z = mesh->mNormals[i].z;
vertex.Normal = vector;

텍스쳐 좌표는 대충 같지만, Assimp는 한 모델이 정점마다 8개의 다른 텍스쳐 좌표까지 갖도록 한다. 그리고 우리는 이것을 사용하지 않을 것이다. 그래서 우리는 오직 텍스쳐 좌표의 첫 번째 집합만을 신경쓴다. 우리는 또한 메쉬가 실제로 텍스쳐 좌표를 가지고 있는지를 확인하길 원할 것이다. (항상 텍스쳐 좌표가 있는 것은 아니다.)


if (mesh->mTextureCoords[0]) // does the mesh contain texture coordinate?
{
 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);

vertex 구조체는 이제 완전히 요구된 vertex attribute로 채워져있고, 우리는 그것을 반복의 끝에서 vertices vector의 마지막에 넣을 수 있다. 이 프로세스는 메쉬의 정점 각각에 대해 반복된다.

Indices
Assimp의 인터페이스는 faces의 한 배열을 갖는 각 메쉬를 정의했다. 거기에서 각 face는 단일의 primitive를 나타낸다. 우리의 경우에 (aiProcess_Triangulate 옵션 때문에) 그 단일 primitive는 항상 triangles이다. 한 face는 어떤 정점이 각 primitive에 대해 무슨 순서로 그려질 필요가 있는지를 정의하는 인덱스들을 포함한다. 그래서 만약 우리가 모든 페이스들에 대해 반복하고 모든 face의 인덱스들을 indices vector에 저장한다면, 우리는 모두 준비가 된 것이다:


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]);
}

outer loop가 끝난 후에, 우리는 이제 glDrawElements로 mesh를 그리기 위한 정점과 인덱스 데이터의 하나의 완전한 집합을 가진다. 그러나, 이야기를 끝내고, 그 메쉬에 세부사항을 더하기 위해, 우리는 메쉬의 material을 또한 처리하고 싶다.

Material
노드들 처럼, 한 메쉬는 오직 한 material object에 대한 한 인덱스들을 포함하고, 한 메쉬의 실제 material을 가져오기 위해, 우리는 scene의 mMaterials array에 인덱싱 할필요가 있다. 그 메쉬의 material index는 그것의 mMaterialIndex property에 정해져있고, 우리는 그 메쉬가 실제로 material을 가졌는지 체크하기 위해 또한 그 property를 query할 수 있다:


if (mesh->mMaterialIndex >= 0)
{
aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex];
std::vector<Texture> diffuseMaps = loadMaterialTextures(material, aiTextureType_DIFFUSE, "texture_diffuse");
textures.insert(textures.end(), diffuseMaps.begin(), diffuseMaps.end());

std::vector<Texture> specularMaps = loadMaterialTextures(material, aiTextureType_SPECULAR, "texture_specular");
textures.insert(textures.end(), specularMaps.begin(), specularMaps.end());
}

우리는 처음에 scene의 mMaterials array로부터 aiMaterial object를 얻어온다. 그러고나서 우리는 mesh의 diffuse and/or specular textures를 불러오고 싶다. 한 material object는 내부적으로 각 텍스쳐 타입에 대한 texture locations의 한 배열을 저장한다. 그 다른 텍스쳐 타입은 모두 aiTextureType_ 의 접두사가 붙는다. 우리는 그 material로부터 textures를 얻기위해 loadMaterialTextures라고 불리는 helper function을 사용한다. 그 함수는 우리는 그러고나서 그 model의 끝에 저장할 Texture 구조체의 한 벡터를 반환한다.

loadMaterialTextures 함수는 주어진 texture type의 모든 texture locations에 대해 반복하고, texture의 파일 위치를 불러오고, 그러고나서 텍스쳐를 불러오고 생성하고, Vertex 구조체에 그 정보를 저장한다. 그것은 이것처럼 보인다:


std::vector<Texture> Model::loadMaterialTextures(aiMaterial* mat, aiTextureType type, std::string typeName)
{
 std::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;
}

우리는 처음에 우리가 준 texture tpyes들 중 하나를 요구하는 GetTextureCount 함수로 material에 저장된 텍스쳐의 양을 체크한다. 우리는 그러고나서 aiString에 그 결과를 저장하는 GetTexture를 통해 텍스쳐의 파일 위치 각각을 얻어온다. 우리는 그러고나서 texture를 불러오는 (SOIL로) TextureFromFile이라고 불리는 또 다른 helper function을 사용하고, texture의 ID를 반환한다. 만약 너가 그 함수를 어떻게 쓸지 확신이 안든다면, 그것의 내용에 대해 마지막에 열거된 완전한 코드를 확인할 수 있다.

  Green Box
  우리가 모델 파일에 있는 텍스쳐 파일 경로가 실제 모델 오브젝트에 대해 local이라고 가정했다는 것에 주목해라. 예를들어, 그 모델 그자체의 이ㅜ치로서 같은 디렉토리에서. 우리는 그러고나서 그 텍스쳐 위치 string을 우리가 이전에 얻어온 directory string과 연결한다. (loadModel function에서) 이것은 완전한 텍스쳥 경로를 얻기 위해서이다. (GetTexture 함수는 또한 directory string을 필요로 한다.)

인터넷에서 볼 수 있는 몇몇 모델들은 여전히 그들의 텍스쳐 로케이션에 대해 absoulte paths를 사용한다. 그러나 그것은 각 machine에 대해 작용하지 않을 것이다. 그러한 경우에, 너는 텍스쳐에 대한 local path를 사용하기 위해 아마도 직접 그 파일을 수정하길 원할 것이다.

그리고 Assimp를 사용하여 한 모델을 불러오는 것에 거의 왔다.

A large optimization
우리는 완전히 끝나지는 않았다. 왜냐하면 우리가 하고싶은 큰 (완전히 필수적이지 않지만) 최적화가 있기 때문이다. 대부분 scene들은 그들이ㅡ 몇 가지 텍스쳐들을 몇 가지 메쉬들에게 재 사용한다; 그것의 벽에 화강암 텍스쳐를 가진 집을 생각해보아라. 이 텍스쳐는 또한 마루, 천장, 계단, 아마 탁자 그리고 아마도 심지어 가까이에 있는 작은 우물에도 적용될 수 있다. 텍스쳐를 불러오는 것은 값 싼 연산이 아니고, 우리의 현재 구현에서, 새로운 텍스쳐가 불러와지고 각 메쉬에 대해 생성된다. 비록 정확히 같은 텍스쳐가 이전에 몇번 불러와졌을 지라도. 이것은 빠르게 너의  model loading implementation의 bottleneck이다.

그래서 우리는 불러와진 모든 텍스쳐들을 저장하여 모델 코드에 대해 작은 약간의 변형을 더할 것이고, 우리가 한 텍스쳐를 로드하기 원할 때 마다, 우리는 처음에 그것이 이미 불러와졌는지를 체크할 것이다. 만약 그렇다면, 우리는 그 텍스쳐를 가져오고 많은 처리 전력을 절약하여 전체 loading routine을 넘길 것이다. 실제로 텍스쳐를 비교하기 위해 우리는 그들의 경로를 또한 비교할 필요가 있따:


struct Texture
{
 unsigned int id;
 std::string type;
 std::string path;
};

그러고나서 우리는 model의 private 변수로서 class file의 위에 선언된 다른 벡터에 모든 불러와진 texture를 저장한다:

  vector<Texture> m_textures_loaded;

그러고나서 loadMaterialTextures 함수에서, 우리는 텍스쳐 경로와 textures_loaded에 있는 모든 텍스쳐와 비교하고 싶다. 현재 텍스쳐 경로가 그러한 것들 중 같은게 있는지 확인하기 위해. 만약 그렇다면 우리는 텍스쳐 loading/generation part를 넘기고, 간단히 메쉬의 텍스쳐로서 위치한 텍스쳐 구조체를 사용한다. (업데이트된) 함수는 아래처럼 보인다:


std::vector<Texture> Model::loadMaterialTextures(aiMaterial* mat, aiTextureType type, std::string typeName)
{
 std::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 < m_texture_loaded.size(); ++j)
  {
   if (std::strcmp(m_texture_loaded[j].path.data(), str.C_Str()) == 0)
   {
    textures.push_back(m_texture_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);
  }
 }

 return textures;
}

그래서 우리는 이제 다용도 모델 로딩 시스템을 가졌을 뿐만 아니라, 꽤 빠르게 오브젝트들을 불러오는 최적화된 것을 가지게 되었다.

 RED BOX
 Assimp의 몇몇 버전은 너의 IDE의 디버그 버전 또는 모드를 사용할 때 꽤 느리게 모델들을 불러오는 경향이 있다. 그래서  만약 너가 느린 LOADING TIME에 돌아가면 또한 release version으로 테스트해보아라.

너는 최적화된 Model class의 완전한 소스코드를 여기에서 찾을 수 있다.

No more containers!
그래서 진짜 예술가에 의해서 만들어진 한 모델을 실제로 불러와서 우리의 구현을 이용해보자.  ~~ 이번에 우리는 Crytek의 게임 Crysis에 의해 사용된 original nanosuit를 불러올 것이다. (이 예제는 tf3dm.com에서 다운로드 된다. 어떤 다른 모델도 사용되어질 수 있다) 그 모델은 model의 diffuse, specular 그리고 normal maps(나중에 사용할) .mtl 파일이 함께 있는 .obj file로서 exported된다. 너는 여기에서 (다소 변형된) 그 모델을 다운로드 할 수 있다. 이것은 불러오기에 더 쉽다. 모든 텍스쳐와 모델 파일들은 불러올 텍스쳐에 대해 같은 디렉토리에 있어야 한다는 것에 주목해라.

  Green Box
  이 웹사이트에서 너가 다운로드 한 버전은 각 텍스쳐 파일 경로가 원래 source에서 다운로드 받는다면 absoulte path였던 것 대신에 local relative path로 수정된 것이다.

이제 코드에서 Model object를 선언하고, 그 모델 파일의 위치를 넘겨주자. 그 모델은 그러고나서 자동적으로 (에러가 없다면) 그 게임 루프에서 그것의 Draw 함수를 사용해서 오브젝트를 그려보자. 어떤 buffer allocation도 ㅇ필요없고, attribute pointers 그리고 render commands도 필요없다. 오직 간단한 1개의 라인이다. 그러고나서 만약 너가 fragment shader가 그 오브젝트의 diffuse texture color를 만들어내는 간단한 쉐이더를 추가한다면, 결과는 이렇게 보일 것이다:

맨위 green light box로 상부가 초록색, 그리고 light attenuation이 크지 않아 발까지 초록색
밑의 red light box로 하체 다리가 빨간색
그리고 view에서 파란색의 flash light로 인해, 복부가 파랗게 보인다.
너는 여기에서 완전한 소스코드를 볼 수 있다.

우리는 또한 창의적게 될 수 있다. 우리가 Lighting tutorials에서 배웠던 것처럼 render equation에 두 개의 point lights를 도입할 수 있고 specular map과 함께 놀랄만한 결과를 얻을 수 있다

심지어 나는 이것이 우리가 이제까지 사용했던 containers들 보다 좀 더 멋지다고 인정해야만 한다. Assimp를 사용하여 너는 인터넷에서 찾을 수 있는 수 많은 모델들을 불러 올 수 있다. 너가 몇 가지 파일 포맷으로 다운로드하기 위해 3D 모델들을 무료로 제공하는 몇 가지 리소스 웹사이트들이 있다. 몇 몇 모델들은 여전히 적절히 불러와지지 않을 것을 주목해라. 그러한 것들은 작동하지 않을 texture path를 가지고 있거나 또는 간단히 Assimp가 읽을 수 없는 포멧으로 exported되었을 지도 모른다.

=================================================
이제 Advanced OpenGL 가기전에,
이대로 쭉 가면서 이론을 공부할지. 아니면
지금 코드를 좀 정리할지 고민이 든다.

왜냐하면, 실험하기에는 나쁘지는 않지만, 약간의 불편함이 있기 때문이다.
그래서, 하나의 Object class를 만들어서, Model data, Lighting Data, Shader등
한 번에 담겨져있는 클래스를 만들어서 쉽게 사용하고 싶은데,
Advanced OpenGL과 Advanced Lighting을 하면서 그러한 것들을 많이 바꿀지도 몰라서 고민이다.

이제 내린 결론은, 현재 까지 한 것중에서, 좀 더 편하게 공부할 수 있도록
클래스를 수정하도록 하자. 이러한 클래스를 만들어보는 것은 나중에
게임을 만들 때, 어떤 식으로 클래스를 구성하고 연결지을지에 도움이 될 것이다.



댓글 없음:

댓글 쓰기