Mesh
Assimp로 불러온 모델의 데이터를 OpenGL이 이해할 수 있는 포맷으로 변환시켜 오브젝트를 렌더링할 수 있도록 해야한다.
mesh는 그려질 수 있는 하나의 독립체
Mesh
가 최소한 어떠한 데이터들을 가지고 있어햐는지 정해야한다.위치벡터
,법선 벡터
,텍스처 좌표 벡터
를 포함하고 있는 vertex들의 모음이 필요하다.- 또한 mesh는 인덱스를 사용하여 그리기 위한
index들
을 포함할 수 있으며,텍스처 형태(diffuse/specular map)의 material 데이터
도 포함가능
아래와 같이 OpenGL에 vertex를 정의할 수 있다.
1
2
3
4
5
struct Vertex {
glm::vec3 Position;
glm::vec3 Normal;
glm::vec2 TexCoords;
};
vertex attribute들을 찾는데 사용할 수 있는 벡터들을 위의 구조체에 저장한다.
텍스처 데이터는 아래와 같은 구조체에 저장한다.
- 텍스처의 id와 타입을 저장
- ex. diffuse 텍스처, specular 텍스처
1
2
3
4
struct Texture {
unsigned int id;
string type;
};
- vertex와 텍스처에 대해 실제 이해했다면, 이제
mesh
클래스 의 구조를 정의할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Mesh {
public:
// mesh data
vector<Vertex> vertices;
vector<unsigned int> indices;
vector<Texture> textures;
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures);
void Draw(Shader &shader);
private:
// render data
unsigned int VAO, VBO, EBO;
void setupMesh();
};
mesh 클래스
- 생성자에게 mesh의 필수적인 모든 데이터를 준다.
setupMesh
함수: 버퍼들을 초기화하고, 마지막으로 Draw함수를 통해 mesh를 그린다.- Draw함수에 shader를 준다: shader를 전해줌으로써 그리기 전에 여러가지 uniform들을 설정할 수 있다.(sampler들을 텍스처 유닛에 연결하는 것과 같은)
생성자 함수는 다음과 같다.
- 내부에서
setupMesh
함수를 호출한다.
- 내부에서
1
2
3
4
5
6
7
8
Mesh(vector<Vertex> vertices, vector<unsigned int> indices, vector<Texture> textures)
{
this->vertices = vertices;
this->indices = indices;
this->textures = textures;
setupMesh();
}
Initialization
- 이
setupMesh()
덕분에 우리는 렌더링에 사용할 수 있는 mesh 데이터의 목록을 가질 수 있다.- 적절한 버퍼들을 설정하고,
- vertex attribute pointer를 통해
vertex shader layout
을 지정해주어야한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void setupMesh()
{
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int),
&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 struct의 도움을 받는다는 점만 빼면.
- C++에서의 Struct의 속성들은 메모리의 위치가 순차적으로 저장된다.
- 즉, struct 배열을 생성하면 순차적으로 변수들이 정렬되어,
array buffer
에 필요한 float(실제로는 byte)배열로 변환한다. - Vertex struct 를 채워넣으면 이 메모리 레이아웃은 다음과 같음.
- 즉, struct 배열을 생성하면 순차적으로 변수들이 정렬되어,
1
2
3
4
5
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 struct들을 buffer데이터로 전달할 수 있다.
- 이것들은
glBufferData
함수에 파라미터로 들어갈 값들로 완벽하게 변환될 수 있다.
- 이것들은
1
2
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), vertices[0], GL_STATIC_DRAW);
sizeof Vertex
: 적절한 바이트 크기 32바이트(8floats*4바이트)Struct의 또다른 사용법
offsetof(s, m)
: 전처리기 지시문- 파라미터1: struct 타입
- 파라미터2: 해당 struct 타입의 멤버 변수 이름
- 이 매크로는 struct에서 입력된 변수의 시작점 바이트 offset을 리턴한다
glVertexAttribPointer
함수의 offset 파라미터를 정의하기에 좋다.- 아래의 경우 법선 벡터의 바이트 offset은
12
바이트(3floats * 4 바이트)로 설정된다.stride 파라미터
는 Vertex struct의 크기로 설정하여 해당 속성만 알려줌
1
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, Normal));
Rendering
Mesh
클래스를 완성하기 위해Draw
함수를 정의해야한다.mesh
를 렌더링하기전,glDrawElements
함수를 호출하기 전에 적절한 텍스처를 바인딩해야한다.- 하지만, 텍스처이 개수를 모르며, 어떤 타입인지 모르기 때문에 어려움
- 이 문제를 해결하기 위해 특별한 네이밍 관습을 적용할 것이다.
- 각, diffuse 텍스처는
texture_diffuseN
이라 이름을 붙이고 - 각, specular 텍스처는
texture_specularN
이라 이름을 붙인다. N
은1
부터 텍스처 sampler에 허용되는 최댓값 사이의 어떠한 숫자이다.- 3개의 diffuse와 2개의 specular 텍스처를 가지고 있다고 해보자
- 이들
sampler
는 다음과 같다.
- 각, diffuse 텍스처는
1
2
3
4
5
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_diffuse2;
uniform sampler2D texture_diffuse3;
uniform sampler2D texture_specular1;
uniform sampler2D texture_specular2;
- 이 네이밍 관습으로 인해
shader
에서 텍스처sampler
를 있는만큼 모두 정의할 수 있다. - 최종 드로잉 코드는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void Draw(Shader &shader)
{
unsigned int diffuseNr = 1;
unsigned int specularNr = 1;
for(unsigned int i = 0; i < textures.size(); i++)
{
glActiveTexture(GL_TEXTURE0 + i); // activate proper texture unit before binding
// retrieve texture number (the N in diffuse_textureN)
string number;
string name = 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, textures[i].id);
}
glActiveTexture(GL_TEXTURE0);
// draw mesh
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
}
- 코드 설명
- 텍스처 타입마다 N 값을 계산, 적절한 uniform 이름을 얻기위해 텍스처 타입 문자열에 결합
- 적절한 sampler를 위치 시킴,
- 현재 활성화된 텍스처 유닛에 부합되는 위치값을 주고, 텍스처를 바인딩(Shader 를 파라미터로 받는 이유)