Post Lists

2018년 7월 17일 화요일

Model Loading - Mesh (2)

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

Mesh
Assimp를 사용하여 우리는 프로그램에 많은 다른 모델들을 불러올 수 있다. 하지만, 일단 불러와지면, 그것들은 모두 Assimp의 데이터 구조로 저장된다. 우리가 실제로 원하는 것은 그 데이터를 우리가 오브젝트를 렌더링할 수 있도록 OpenGL이 이해하는 포맷으로 바꾸는 것이다. 우리는 이전 튜토리얼에서 한 mesh가 하나의 그릴 수 있는 entitry인 것을 배웠었다. 그래서 우리 자신의 mesh class를 정의하여 시작하자.

메쉬가 그것의 데이터로서 무엇을 최소한으로 가져야하는지 생각하기 위해, 지금까지 배웠던 것을 조금 다시 보자. 한 mesh는 적어도 각 정점이 position vector, normal vector 그리고 texture coordinate vector를 포함하는 정점들의 집합을 필요로 해야한다. 한 mesh는 또한 indexed drawing을 위한 인덱스들 그리고 텍스쳐(diffuse/specular maps)의 형태로 material data를 포함해야만 한다.

mesh class에 대한 최소 요구사항을 설정했으니까, 우리는 OpenGL에서 한 정점을 정의할 수 있다:


struct Vertex
{
    glm::vec3 Position;
    glm::vec3 Normal;
    glm::vec3 TexCoords;
};

우리는 vertex attributes의 각각에 인덱싱하기위해 사용할 수 있는 Vertex라고 불리는 구조체에 요구되는 벡터들 각각을 저장할 수 있다. Vertex struct를 제쳐두고서, 우리는 또한 Texture 구조체에서 texture data를 구성하기를 원한다:


struct Vertex
{
    unsigned int id;
    string type;
};

우리는 텍스쳐의 id와 그것의 유형을 저장한다. 예를들어 diffuse texture 또는 specular texture를

vertex와 texture의 실제 표기에 대해서 알았아늬, 우리는 mesh class의 구조를 정의할 수 있다.

class Mesh
{
public:
 /* Mesh Data*/
 std::vector<Vertex> m_vertices;
 std::vector<unsigned int> m_indices;
 std::vector<Texture> m_textures;

 /* Functions */
 Mesh(std::vector<Vertex>& vertices, std::vector<unsigned int>& indices, 
             std::vector<Texture>& textures);
 void Draw(Shader shader);
private:
 unsigned int VAO, VBO, EBO;
 void setupMesh();
};

너도 볼 수 있듯이, 클래스는 너무 복잡하지는 않다. 생성자에서, 우리는 그 메쉬에게 모든 필요한 데이터를 주고, 우리는 setupMesh function에서 버퍼를 초기화하고 그리고 마지막으로 Draw function으로 mesh를 그린다. 우리가 Draw function에 shader를 주는 것에 주목해라; shader를 mesh에게 넘김으로써, 우리는 그리기 전에 몇 가지 uniforms를 설정할 수 있다. (samplers를 texture unit으로 연결시키는 것 같은)

그 생성자의 함수 내용은 꽤 간단하다. 우리는 간단히 생성자에 상응하는 인자 변수들로 클래스의 public 변수들을 설정한다. 우리는 또한 생성자에서 setupMesh 함수를 호출한다.


Mesh::Mesh(std::vector<Vertex>& vertices, std::vector<unsigned int>& indices, 
           std::vector<Texture>& textures)
 :m_vertices(vertices), m_indices(indices), m_textures(textures)
{
 setupMesh();
}

여기에서 특별한 건 없다. setupMesh function을 이제 자세히 봐보자.

Initialization
생성자 덕분에, 우리는 이제 렌더링을 위해 우리가 사용할 수 있는 메쉬 데이터의 큰 리스트들을 가진다. 우리는 적절한 버퍼들을 설정할 필요가 있고, vertex attribute pointers들을 통해 vertex shader layout을 명ㅅ이해야할 필요가 있다. 이제까지 우리는 이러한 개념들과 어떠한 문제가 없었지만, 우리는 이번에 구조체에있는 vertex data의 도입부분으로 조금 가지고 놀아야 한다:


void Mesh::setupMesh()
{
 glGenVertexArrays(1, &VAO);
 glGenBuffers(1, &VBO);
 glGenBuffers(1, &EBO);

 glBindVertexArray(VAO);
 
 glBindBuffer(GL_ARRAY_BUFFER, VBO); 
 glBufferData(GL_ARRAY_BUFFER, m_vertices.size() * sizeof(Vertex), &m_vertices[0], GL_STATIC_DRAW);

 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
 glBufferData(GL_ELEMENT_ARRAY_BUFFER, m_indices.size() * sizeof(unsigned int), &m_indices[0], GL_STATIC_DRAW);

 // vertex positions
 glEnableVertexAttribArray(0);
 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0);

 // vertex normals
 glEnableVertexAttribArray(1);
 glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));

 // vertex texture coords
 glEnableVertexAttribArray(2);
 glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));

 glBindVertexArray(0);
}


코드는 너가 기대한 것보다 많이 다르지 않다. 그러나 몇 가지 함정들이 Vertex 구조체의 도움으로 사용되었다.

구조체는 C++에서 그것들의 메모리 layout이 순차적이라는 훌륭한 특징을 가진다. 즉, 만약 우리가 한 구조체를 데이터의 배열로서 나타내려한다면, 그것은 오직 직접적으로 우리가 array buffer를 위해 원하는 한 float (실제로 byte) array로 바꾸는 순차적인 순서의 구조체 변수를 포함할 것이다. 예를들어, 만약 우리가 한 채워진 Vertex struct를 가진다면, 그것의 메모리 layout은 다음과 동일할 것이다:


Vertex vertex;
vertex.Position = glm::vec3(0.2f, 0.4f, 0.6f);
vertex.Normal = glm::vec3(0.0f, 1.0f, 0.0f);
vertex.TexCoords = glm::vec2(1.0f, 0.0f);
// = [0.2f, 0.4f, 0.6f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f];

이 실용적인 특정 덕분에, 우리는 직접적으로 Vertex 구조체의 큰 리스트에 대한 포인터를 버퍼 데이터로서 넘길 수 있고, 그것들은 완벽하게 glBufferData가 그것의 인자로서 기대하는 것으로 바뀐다:

  glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);

자연적으로 sizeof 연산자는 적절한 크기의 바이트에 대한 구조체에 사용되어질 수 있다. 이것은 32 bytes가 되어야 한다 (8 floats * 4 bytes each)

구조체의 다른 훌륭한 사용은 offsetof(s,m)이라고 불리는 전처리 지시문인데, 이것은 그것의 첫 번째 인자로 구조체를 그리고 그것의 두 번째 인자로 구조체의 한 변수 이름을 취한다. 그 매크로는 그 구조체의 시작으로부터 그 변수의 bytes offset을 반환한다. 이것은 glVertexAttribPointer 함수의 offset parameter를 정의하는데 완벽하다:

  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Vertex::Normal));

그 offset은 이제 offset of macro를 사용하여 정의되는데, 이 경우에 그 매크로는 구조체의 normal vector의 byte offset에 동일한 normal vector 의 byte offset을 설정한다. 그 normal vector는 3개의 floats이고 따라서 12 bytes이다. 우리가 또한 stride parameter를 Vertex struct의 크기와 동일하게 설정하는것에 주목해라.

이것처럼 구조체를 사용하는 것은 좀 더 가독성있는 코드를 제공할 뿐만 아니라, 우리가 쉽게 그 구조를 확장할 수 있게 해준다. 만약 우리가 또 다른 vertex attribute를 원한다면, 우리는 간단히 그 구조체에 그것을 추가할 수 있고, 그것의 유연한 특성 때문에, 그 렌더링 코드는 부서지지 않을 것이다.

Rendering
완전해지려고 Mesh class를 위해 우리가 정의할 필요가 있는 마지막 함수는 그것의 Draw 함수이다. 실제로 메쉬를 렌더링하기 전에, 우리는 처음에 glDrawElements를 호출하기 전에 적절한 텍스쳐를 바인드하기 원한다. 그러나, 이것은 실제로 조금 어렵다. 왜냐하면 우리는 그 메쉬들이 얼마나 많은 텍스쳐들을 가지고 있는지 그리고 그것들이 무슨 타입을 가졌는지를 모르기 때문이다. 그래서 어떻게 우리는 shader에 있는 텍스쳐 units과 samplers들을 설정하는가?

그 문제를 해결하기 위해, 우리는 어떤 naming convention을 가정할 것이다: 각 diffuse texture는 texture_diffuseN으로 이름지어지고, 각 specular texture는 texture_specularN으로 이름지어져야 한다. 여기에서 N은 1부터 texture samplers의 허용되는 최대 숫자까지 범위의 어떤 수이다. 우리가 한 특정한 메쉬에 대해 3개의 diffuse textures과 2개의 specular textuers를 가진다고 해보자. 그것들의 텍스쳐 샘플러들은 그러고나서 호출되어야 한다:


uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;

이 convention으로, 우리는 쉐이더에 우리가 원하는 만큼의 많은 texture samplers들을 정의할 수 있다. 만약 한 메쉬가 실제로 (매우 많은) 텍스쳐들을 포함한다면, 우리는 그것들의 이름이 무엇이 될지 안다. 이 convention으로 우리는 단일 메쉬에 있는 어떤 양의 텍스쳐이든지 처리할 수 있고, 그 개발자는 적절한 samplers를 정의하여 그가 원하는 만큼의 많은 텍스쳐를 자유롭게 사용한다. (비록 덜 정의하는 것이 bind와 uniform calls의 낭비를 덜 할 것이다).

  Green Box
  이것과 같은 문제들에 대해 많은 솔루션들이 있다. 만약 너가 이 특정한 솔루션을 좋아하지 않는다면 창의적이게 되고 너 자신의 솔루션을 생각해내는 것은 너에게 달려있다.

그 최종 drawing code는 이렇게 된다:


void Mesh::Draw(Shader* shader)
{
 unsigned int diffuseNr = 1;
 unsigned int specularNr = 2;
 
 for (unsigned int i = 0; i < m_textures.size(); ++i)
 {
  glActiveTexture(GL_TEXTURE0 + i); // active proper texture unit before binding

  // retrieve texture number (the N in diffuse_textureN
  std::string number;
  std::string name = m_textures[i].type;
  if (name == "texture_diffuse")
   number = std::to_string(diffuseNr++);
  else if (name == "texture_specular")
   number = std::to_string(specularNr++);

  shader->setFloat(("material." + name + number).c_str(), i);
  glBindTexture(GL_TEXTURE_2D, m_textures[i].id);
 }
 glActiveTexture(GL_TEXTURE0);

 // draw mesh
 glBindVertexArray(VAO);
 glDrawElements(GL_TRIANGLES, m_indices.size(), GL_UNSIGNED_INT, 0);
 glBindVertexArray(0);
}

우리는 처음에 texture type마다 N-component 를 계산하고, 그것을 적절한 uniform name을 얻기위해 texture의 type string에 더한다. 우리는 그러고나서 적절한 sampler를 찾고, 그것에게 현재 active texture unit과 일치하는 location 값을 주고, 텍스쳐를 바인드 시킨다. 이것은 또한 우리가 Draw function에서 shader가 필요한 이유이다.

우리는 또한 "material."을 최종 uniform name에 더했다. 왜냐하면 우리는 보통 텍스쳐들을 material struct에 저장하기 때문에 (이것은 구현마다 다를지도 모른다).

  Green Box
  우리가 diffuse와 specular counters들을 우리가 그것들을 string으로 바꾸는 순간에 증가시킨것에 주목해라. C++에서 그 증가 호출 : variable++은 변수를 그 자체로 반환하고 그러고나서 그 변수를 증가시킨다. 반면에 ++variable는 처음에 그 변수를 증가시키고, 그리고 나서 그것을 반환한다. 우리의 경우에 std::string에 넘겨진 값은 원래의 카운터 값이다. 그 이후에 그 값은 다음 round를 위해 증가되어 진다.

너는 여기에서 Mesh class의 전체 소스코드를 볼 수 있다.

우리가 정의한 Mesh class는 이전 튜토리얼들에서 다룬 많은 토픽들에 대한 깔끔한 추상화이다. 다음 튜토리얼에서 우리는 컨테이너로서 몇 가지 메쉬 오브젝트들에 대해 작동하는 한 모델을 만들 것이고, 실제로 Assimp의 loading interface를 구현할 것이다.

댓글 없음:

댓글 쓰기