728x90
반응형
- GPU 파이프 라인에 입력되는 정점 배열은 정점 위치와 노멀 등의 데이터를 저장하는데, 정점 쉐이더는 한 번에 한 정점을 처리한다.
- GPU는 수많은 코어(core)로 구성된 병렬 처리 구조를 가지고 있다. (다수의 정점을 동시에 처리하는데 적합하다.)
6.1 OpenGL ES와 쉐이딩 언어
- OpenmGL ES는 정점 쉐이더(vertex shader) 와 프래그먼트 쉐이더라는 두 가지 프로그램을 필요로 한다.
- OpenGL ES의 쉐이더는 GPU에 특화된 언어로 작성하는데, 이를 OpenGL ES Shading Language라고 한다.
6.2 정점 쉐이더
- GLSL은 C와 비슷한 언어이다.
- 하지만, GPU에서 작동하는 GLSL은 CPU에서 작동하는 C와는 분명하게 구분되는 자신만의 특징을 가진다.
- GLSL은 float와 같은 기본 타입에 더불어 최대 4개 원소를 가지는 벡터 타입을 제공한다.
- 예를 들어, mat3와 mat4는 각각 3X3과 4X4 크기의 정사각행렬을, mat3X4는 3X4 크기의 행렬을 의미한다.
- 정점 쉐이더는 두 가지 종류의 입력을 받아들인다. 하나는 정점 배열에 저장된 정점별 애트리뷰트(attribute)들이고, 각 정점들은 위치(position), 노멀(normal), 텍스처 좌표(texCoord) 애트리뷰트를 가지고 있다. (텟트처 좌표에 관해서는 8장에서 자세히 기술)
정점 배열 1000개의 정점을 저장하고 있다면, 정점 쉐이더는 1000번 실행되는데, 1000번 실행되는 정점 쉐이더가 모두 공유하는 또 다른 종류의 입력 데이터가 있다.
- 대표적인 예가 월드/뷰/투영 변환인데, 이러한 입력 데이터들을 유니폼(uniform)이라 부른다.
gl_Position
- 정점은 결국 clip space로 들어가게 되며, vertex shader가 출력으로 반드시 해줘야 한다. (의무)
- gl_Position은 clip space position이고, 내장변수(built in variable)를 통해 나타내고 있다.
[예제 코드]
#version 300 es
uniform mat4 worldMat, viewMat, projMat; // uniform (입력)
layout(location = 0) in vec3 position; // attribute (입력)
layout(location = 1) in vec3 normal; // layout은 위치를 표시하는 역할을 한다. (없어도 됨)
layout(location = 2) in vec2 texCoord;
out vec3 v_normal; // 출력
out vec2 v_texCoord;
void main()
{
gl_Position = projMat * viewMat * worldMat * vec4(position, 1.0); // clip space가 나올 것. 맨 끝에 1.0을 해준 이유는 4X4 행렬로 맞춰주기 위해서
v_normal = normalize(transpose(inverse(mat3(worldMat))) * normal); // world space에 정의될 것
v_texCoord = texCoord;
}
- 메인 문을 보면 우선 오브젝트 공간에서 정의된 position을 클립 공간으로 변환한다. (모든 정점 쉐이더가 반드시 수행해야 하는 것)
- 그 결과는 gl_Position이라는 내장 변수 (built in variable)에 저장해야 한다.
- v_normal에서의 worldMat은 월드 행렬의 왼쪽 위 3X3 부분 행렬을 말한다. 이는 [L | t]로 표기된 월드 행렬 중 L을 의미한다.
- L의 역전치 행렬이 normal에 곱해지고 정규화되어 출력 변수인 v_normal에 저장된다.
- 함수의 마지막 줄은 texCoord를 v_texCoord에 그대로 복사하는데, 총 세 개의 출력 값(v_normal, v_texCoord, gl_Position)이 래스터라이저에게 전달될 것이다.
6.3 쉐이더를 위한 OpenGL ES 작업
- GL 프로그램의 함수는 gl로, 데이터는 GL로 시작한다.
[예제 코드]
GLuint shader = glCreateShader(GL_VERTEX_SHADER); // 쉐이더 오브젝트 생성
glShaderSource(shader, 1, &source, NULL); // 만들어진 쉐이더 오브젝트를 저장
glCompileShader(shader); // 쉐이더 오브젝트는 컴파일된다.
- 쉐이더 오브젝트(shader object)는 glCreateShader가 생성
- glShaderSource에 의해 쉐이더 오브젝트가 저장된다.
- 쉐이더 오브젝트는 glCompileShader에 의해 컴파일된다.
[예제 코드]
GLuint program = glCreateProgram(); // program Object 생성
glAttachShader(program, shader); // 아까 만든 shader를 붙이기
glLinkProgram(program); // 링크
glUseProgram(program); // 렌더링에 사용하기 위해서 호출 !
- 정점 및 프래그먼트 쉐이더 오브젝트는 모두 프로그램 오브젝트(program object)에 붙여져야 한다.
6.4 애트리뷰터와 유니폼
- GL 프로그램의 중요한 역할 중 하나는 정점 쉐이더가 애트리뷰트와 유니폼을 사용할 수 있도록 하는 것이다.
- 이들을 정점 쉐이더에게 건네줘야 하고, 또한 이들이 어떤 구조를 가지는지 정점 쉐이더에게 알려줘야 한다.
[예제 코드]
struct Vertex
{
glm::vec3 pos; // position
glm::vec3 nor; // normal
glm::vec2 tex; // texture coordinates
};
typedef GLshort Index;
struct ObjData
{
std::vector<Vertex> vertices;
std::vector<Index> indices;
};
ObjData objData;
- 여기까지 하면 메모리에 vertex array와 index array는 Load되어 있다.
- 현재 CPU 메모리에 저장되어 있는 정점 및 인덱스 배열은 렌더링을 위해서 GPU 메모리의 버퍼 오브젝트(buffer object)로 옮겨진다. (실제 렌더링은 GPU에서 하니까, GPU메모리에서 buffer object를 만든다 !)
- 정점 배열은 배열 버퍼 오브젝트(array buffer obect)로, 인덱스 배열은 요소 배열 버퍼 오브젝트(element array buffer object)로 복사된다.
[예제 코드]
GLuint abo;
glGenBuffers(1, &abo);
glBindBuffer(GL_ARRAY_BUFFER, abo);
glBufferData(GL_ARRAY_BUFFER, (GLsizedi) objData.vertices.size() * sizeof(Vertex),
objData.vertices.data(), GL_STATIC_DRAW);
- 실제 옮기는 과정을 설명하는 부분
- glGenBuffers를 통해 n개의 버퍼 오브젝트를 buffers에 저장한다.
- 이 버퍼 오브젝트를 정점 배열에 바인드(bind)하기 위하여 GL_ARRAY_BUFFER를 파라미터로 glBindBuffer를 호출한다.
- objData.vertices로 버퍼 오브젝트를 채우기 위하여 glBufferData를 호출한다.
→ GPU메모리에 vertex array가 복사가 된 것이다.
[예제 코드]
glEnableVertexAttribArray(0); // position = attribute 0
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE,
sizeof(Vertex), (const GLvoid*) offsetof(Vertex, pos));
glEnableVertexAttribArray(1); // normal = attribute 1
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE,
sizeof(Vertex), (const GLvoid*) offsetof(Vertex, nor));
glEnableVertexAttribArray(2); // texture coordinates = attribute 2
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE,
sizeof(Vertex), (const GLvoid*) offsetof(Vertex, tex));
- 인덱스 배열도 마찬가지로 처리된다.
- 위 그림은 pos, nor, tex라는 정점 애트리뷰트들이 배열 버퍼 오브젝트에 어떤 방식으로 저장되어 있는지 보여준다.
- GL 프로그램은 정점 쉐이더에게 다음과 같은 구조와 스트라이드를 알려줘야 한다. (어디서부터 어디까지 가지고 와야하는지)
- glEnableVertexAttribArray를 이용하여 n번에 놓인 애트리뷰트를 활성화시킨다.
- glVertexAttribPointer를 통해 해당 애트리뷰트에 대한 자세한 정보를 제공한다.
→ 이 과정에서 얼만큼 들고와야하는지에 대해 Vertex의 크기와 처음 위치를 매개변수로 전달해준다.
유니폼
[예제 코드]
glmLLmat4 worldMatrix; // 매 프레임 갱신되어야하는 월드행렬
GLint loc = glGetUniformLocation(program, "worldMat"); // program object에서 "worldMat"의 위치를 찾기 위해
glUniformMatrix4fv(loc, 1, GL_FALSE, glm::value _ptr(worldMatrix);
- 정점 쉐이더는 worldMat, viewMat, projMat 세 개의 유니폼을 사용하는데, 이 역시 GL프로그램이 제공해야 한다.
- 가상 공간에서 물체가 움직이는 경우 월드 행렬은 매 프레임 갱신되어야 하는데, 이 행렬을 worldMatrix라고 할 때, worldMatrix를 정점 쉐이더 유니폼인 worldMat에 할당해야 하는데, 이를 위해서 GL 프로그램은 우선 worldMat의 위치를 알아내야 한다.
- 프로그램 오브젝트(program)를 링크했을 때 worldMat의 위치가 결정되었는데, 이 위치를 알아내기 위해 GL프로그램은 glGetUniformLocation을 호출한다.
- 정점 쉐이더의 유니폼 변수에 특정 값을 할당하기 위하여 4X4 행렬인 worldMat를 위해 glUniformMatrix4fv를 호출한다.
6.5 드로우콜
- 다양한 함수를 이용해 정점 쉐이더가 필요로 하는 애트리뷰트와 유니폼을 GL 프로그램이 모두 제공했다면, 이제 해당 폴리곤 메시를 그리는 명령을 내려야 한다. 그걸 드로우콜(drawcall)이라고 한다.
- 이를 그리기 위해서는 glDrawElements(GL_TRIANGLES, 정점의 개수, GL_USIGNED_SHORT, 0)을 호출한다. (인덱스 배열일 경우)
- 인덱스 없이 표현되었다면, 즉 정점 배열로만 정의되었다면, glDrawArray(GL_TRIANGLES, 0, 정점의 개수)를 호출한다. (정점 배열일 경우)
출처
[OpenGL ES를 이용한 3차원 컴퓨터 그래픽스 입문]을 보고 공부하고 정리한 내용입니다.
728x90
'공부 > Open GL ES를 이용한 3차원 컴퓨터 그래픽스 입문' 카테고리의 다른 글
[Open GL ES를 이용한 3차원 컴퓨터 그래픽스 입문] 챕터 8 - 이미지 텍스처링 [1/2] (0) | 2022.02.17 |
---|---|
[OpenGL ES를 이용한 3차원 컴퓨터 그래픽스 입문] 챕터 7 - 래스터라이저 (0) | 2022.02.15 |
[Open GL ES를 이용한 3차원 컴퓨터 그래픽스 입문] 챕터 5 - 정점 처리 (0) | 2022.02.13 |
[OpenGL ES를 이용한 3차원 컴퓨터 그래픽스 입문] 챕터 4 - 좌표계와 변환 (0) | 2022.02.12 |
[OpenGL ES를 이용한 3차원 컴퓨터 그래픽스 입문] 챕터 3 - 모델링 (0) | 2022.02.11 |