공부/컴퓨터 그래픽스

OpenGL ES를 이용한 3차원 컴퓨터 그래픽스 입문 Chapter 06 OpenGL ES와 쉐이더

bokob 2024. 7. 7. 16:25

정점 쉐이더는 정점 배열에 저장된 입력 정점들에 대해서 연산하고 또, 다양한 연산을 수행한다.

이 중에서 가장 중요한 것은 정점에 일련의 변환을 적용하는 것이다.

GPU 파이프라인에 입력되는 정점 배열은 정점 위치와 노멀 등의 데이터를 저장하는데, 정점 쉐이더는 한 번에 한 정점을 처리한다.

GPU는 수많은 코어(core)로 구성된 병렬 처리 구조를 가지고 있는데, 이들 코어는 다수의 정점을 동시에 처리하는 데 적합하다.

 

6.1 OpenGL ES와 쉐이딩 언어

OpenGL ES는 OpenGL의 부분집합이다. 

정점 쉐이더와 프래그먼트 쉐이더라는 두 가지 프로그램을 필요로 한다.

OpenGL ES 3.1에는 범용적 GPU 연산을 위해 컴퓨트 쉐이더(compute shader)가 추가되었고, OpenGL ES 3.2에는 기하 쉐이더(geometry shader)테썰레이션(tessellation) 기능이 추가되었다.

 

OpenGL ES의 쉐이더는 GPU에 특화된 언어로 작성하는데, 이를 OpenGL ES Shading Language라고 한다.

이 책에서는 OpenGL ES를 GL로, OpenGL ES Shading Language는 GLSL로 간단하 표기한다.

 

6.2 정점 쉐이더

GLSL은 C와 비슷한 언어이다. 추가로 GPU에서 동작하는 특징을 가진 것들이 있다.

GLSL은 float와 같은 기본 타입에 더불어 최대 4개 원소를 가지는 벡터 타입을 제공한다. ex) 4차원 실수 벡터인 vec4

정점 쉐이더의 입력과 출력

입력 데이터

  • 애트리뷰트(attribute): 정점 배열에 저장된 정점별 데이터, 각 정점은 위치(position), 노멀(normal), 텍스처좌표(texCoord) 애트리뷰트를 가진다.
  • 유니폼(uniform): 정점 쉐이더가 모두 공유하는 오브젝트별로 적용되는 데이터, 모든 정점에 공히 적용 ex) 월드/뷰/투영 변환

 

정점 쉐이더

1번 줄은 GLSL 3.0으로 작성된 것을 말해준다.

 

3번 줄은 월드, 뷰, 투영 변환이 모두 4 x 4 크기 행렬로 정의되고 모두 uniform 이라는 키워드로 수식된다.

 

5~7번 줄은 쉐이더에 입력되는 position, normal, texCoord 애트리뷰트가 in 이라는 키워드로 수식되었다.

애트리뷰트가 m개인 경우, 각각의 위치는 0, 1, 2 ..., m-1로 표기된다.

layout이라는 키워드를 사용해 position, normal, texCoord의 위치를 0, 1, 2로 지정하였다.

정점의 position과 normal은 모두 3차원 좌표를 가지므로, vec3로 정의되었다.

texCoord는 vec2로 정의되었는데, 텍스처가 2차원 이미지일 경우 여기에 접근하기 위한 좌표 역시 2차원으로 표현되기 때문

 

9~10번 줄은 쉐이더가 출력할 v_normal과 v_texCoord를 정의하는데, 이들은 모두 out이라는 키워드로 수식된다.

 

main() 함수에서는 오브젝트 공간에서 정의된 position을 클립 공간으로 변환한다. 이는 모든 정점 쉐이더가 반드시 수행해야 하는 것으로, 그 결과는 gl_Position이라는 내장 변수(built-in variable)에 저장해야 한다.

수행한 세 가지 변환 행렬으 크기는 모두 4 x 4인데, position은 3차원 벡터이므로, 이를 4차원 벡터로 변환하기 위하여 vec4를 생성자(constructor)로 사용했다.

 

그 다음으로 오브젝트 공간에서 정의된 normal을 월드 공간으로 변환하데, mat3(worldMat)은 월드 행렬의 왼쪽 위 3 x 3 부분 행렬을 말한다. [L|t]로 표기된 월드 행렬 중 L을 의미한다. L의 역전치행렬 $\left( \boldsymbol{L}^{-1} \right)^{T}$가 normal에 곱해지고 정규화되어 출력 변수인 v_normal에 저장된다.

 

마지막으로 texCoord를 v_texCoord에 그대로 복사하는데, 총 세 개의 출력 값(gl_Position, v_normal, v_texCoord)이 래스터라이저에게 전달될 것이다.

 

6.3 쉐이더를 위한 OpenGL ES 작업

GL Program

프래그먼트 쉐이더도 정점 쉐이더와 비슷한 방식으로 작성된다.

정점 및 프래그먼트 쉐이더가 렌더링을 위한 세부 작업을 수행한다면, GL 프로그램은 이들 쉐이더를 관리함과 동시에 쉐이더에 필요한 데이터를 공급하는 역할을 한다.

 

GL API

  • 프로그램 함수는 gl로 시작
  • 데이터는 GL로 시작

쉐이더 오브젝트를 위한 GL 프로그램

  • 쉐이더 소스코드를 저장하는 쉐이더 오브젝트(shader object)는 glCreateShader가 생성하고 GL_VERTEX_SHADER 혹은 GL_FRAGMENT_SHADER를 입력 받아서 쉐이더 오브젝트의 ID를 리턴한다.
  • GL 프로그램이 쉐이더 소스코드를 로드했고, char* source가 이 소스코드를 가리킨다고 가정하자. 이 소스코드는 glShaderSource에 의해 쉐이더 오브젝트에 저장된다.
  • 쉐이더 오브젝트는 glCompileShader에 의해 컴파일 된다.

 

프로그램 오브젝트를 위한 GL 프로그램

 

정점 및 프래그먼트 쉐이더 오브젝트는 모두 프로그램 오브젝트(program object)에 붙여(attach)져야 한다.

그 뒤에 프로그램 오브젝트는 실행 파일로 링크(link)된다.

  • 프로그램 오브젝트가 glCreateProgram에 의해 생성
  • shader(이전 예제 코드에서 나온 shader)를 ID로 가지는 정점 쉐이더 오브젝트가 glAttachShader에 의해 프로그램 오브젝트에 붙여졌다(프로그래먼트 쉐이더 오브젝트도 동일한 방식으로 프로그램 오브젝트에 붙여져야 한다).\
  • 프로그램 오브젝트가 glLinkProgram에 의해 링크되었다.
  • 프로그램 오브젝트를 렌더링에 사용하기 위해서 glUseProgram이 호출되었다.

 

6.4 애트리뷰트와 유니폼

GL에서 가장 중요한 역할 중 하나는 정점 쉐이더가 애트리뷰트와 유니폼을 사용할 수 있도록 하는 것이다.

즉, GL 프로그램은 이들을 정점 쉐이더에게 건네줘야 하고, 또한 이들이 어떤 구조를 가지는지 정점 쉐이더에게 알려줘야 한다.

 

6.4.1 애트리뷰트와 버퍼 오브젝트

정점 및 인덱스 배열 처리를 위한 GL 프로그램

Vertex 구조체는 정점의 위치(pos), 노멀(nor), 텍스처 좌표(tex)를 가진다.

glm은 OpenGL 수학 라이브러리를 말하는 것으로, GLSL과 동일한 이름과 기능을 가진 클래스와 함수를 제공한다.

.obj 파일 등에 저장된 폴리곤 메시 데이터가 GL 프로그램의 정점 및 인덱스 배열로 로드되었고, 이들을 각각 vertices와 indices가 갈리킨다고 가정하자. 이들은 위 코드에서 objData에 저장되었다.

 

CPU 메모리의 정점 및 인덱스 배열과 GPU 메모리의 버퍼 오브젝트

현재 CPU 메모리에 저장되어 있는 정점 및 인덱스 배열은 렌더링을 위해서 GPU 메모리의 버퍼 오브젝트(buffer object)로 옮겨지는데, 정점 배열은 배열 버퍼 오브젝트(array buffer objet)로, 인덱스 배열은 요소 배열 버퍼 오브젝트(element array buffer object)로 복사된다.

위 사진처럼 이들은 각각 GL_ARRAY_BUFFER와 GL_ELEMENT_ARRAY_BUFFER라고 부른다.

 

배열 버퍼 오브젝트를 위한 GL 프로그램

정점 배열을 위한 버퍼 오브젝트를 생성하고 채우는 과정을 보여준다.

  • glGenBuffers(GLsizei n, GLuint* buffers)를 호출하여 버퍼 오브젝트를 생성한다. glGenBuffers는 n개의 버퍼 오브젝트를 buffers에 저장
  • 이 버퍼 오브젝트를 정점 배열에 바인드(bind)하기 위하여 GL_ARRAY_BUFFER를 파라미터로 glBindBuffer 호출
  • 정점 및 인덱스 뱅열 처리를 위한 GL 프로그램 코드에서 정의한 objData.vertices로 버퍼 오브젝트를 채우기 위하여 glBufferData를 호출한다. glBufferData의 두 번째 파라미터는 정점 배열의 크기를 지정하고, 세 번째 파라미터는 정점 배열을 가리킨다.

 

요소 배열 버퍼 오브젝트를 위한 GL 프로그램

정점 배열을 위한 배열 버퍼 오브젝트를 생성하고 채우는 과정과 같이 인덱스 배열을 위한 요소 배열 버퍼 오브젝트를 채우고 생성하는 과정도  생성 -> 바인드 -> 데이터 제공 같은 순서로 진행된다.

glBindBuffer와 glBufferData를 호출할 때 GL_ELEMENT_ARRAY_BUFFER를 사용한다.

 

정점 애트리뷰트를 위한 GL 프로그램

GPU 버퍼 오브젝트에 저장된 정점 애트리뷰트, 정점 및 인덱스 배열 처리 코드에서 Vertex 구조체 크기가 32바이트이므로 스트라이드도 32바이트

정점 및 인덱스 배열이 모두 GPU 메모리로 옮겨지고, 위 그림은 정점 애트리뷰트들이 배열 버퍼 오브젝트에 어떤 방식으로 저장되었는지 보여준다.

정의한 Vertex 구조체의 크기각 32 바이트였으므로, 다음 정보를 얻기 위해서 32 바이트를 이동해야 한다. 이런 바이트 간격을 스트라이드(stride)라 한다.

 

GL 프로그램은 정점 쉐이더에게 위 그림 같은 구조와 스트라이드를 알려줘야 한다.

위 예제 코드에서 보면, 정점의 애트리뷰트들의 위치와 스트라이드를 알려주고 있다. 

glEnableVertexAttribArray를 이용하여 각 위치에 놓인 애트리뷰트를 활성화시킨다.

glVertexAttribPointer를 이용하여 각 애트리뷰트에 대한 자세한 정보를 제공한다.

첫 번째 파라미터는 정점 애트리뷰트의 인덱스, 두 번째는 애트리뷰트 요소의 벡터 크기, 세 번째는 데이터 타입, 네 번째는 정규화 여부, 다섯 번째는 스트라이드 크기, 여섯 번째는 애트리뷰트의 첫 시작이다.

 

6.4.2 유니폼

맨 처음의 예제 코드에서의 정점 쉐이더는 worlMat, viewMat, projMat 세 개의 유니폼을 사용하는데, 이 역시 GL 프로그램이 제공해야 한다.

예를 들어 가상 공간에서 물체가 움직일 때, 월드 행렬은 매 프레임 갱신되어야 한다. 위 예제 코드에서 worldMatrix라고 정의되어 있다. 이 worldMatrix를 정점 쉐이더 유니폼인 worldMat에 할당해야 하는데, 이를 위해서 GL 프로그램은 우선 worldMat의 위치를 알아내야 한다.

프로그램 오브젝트를 위한 GL 프로그램 예제 코드에서 오브젝트(해당 예제 코드에서 program을 가리킴)를 링크 했을 때 worldMat의 위치가 결정되었는데, 이 위치를 알아내기 위해 glGetUniformLocation을 호출한다.

 

정점 쉐이더의 유니폼 변수에 특정 값을 할당하기 위하여 GL은 glUniform으로 시작하는 함수들을 사용하는데, 4 X 4 행렬인 worldMat를 위해 glUniformMatrix4fv를 호출한다.

첫 번째 파라미터는 유니폼 위치이다. 예제 코드에서는 glGetUniformLocation을 사용해 얻은 loc, 즉 worldMat의 위치를 넘겨주고 있다.

두 번째 파라미터는 유니폼에 의해 수정될 매트릭스 수를 의미한다. 세 번째는 전치할 지 여부, 마지막은 설정할 행렬 데이터가 저장된 배열의 포인터이다.

 

6.5 드로우콜

드로우콜(drawcall)

  • 폴리곤 메시를 그리는 명령을 내리는 것

정점 쉐이더가 필요로 하는 오브젝트의 정보를 갖춘 다음 폴리곤 메시를 그려야 한다.

우측에 보이는 메시는 48개의 삼각형을 가졌다. 이 메시의 인덱스 배열은 144(48 x 3)개의 원소를 가지게 되는데, 이를 그리기 위해서 glDrawElements(GL_TRIANGLES, 144, GL_UNSIGNED_SHORT, 0)을 호출한다.

만약, 이 메시가 인덱스 없이 푷현되었다면, 즉 정점 배열로만 정의되었다면, glDrawArrays(GL_TRIANGLES, 0, 144)를 호출한다.

 

출처

[OpenGL ES를 이용한 3차원 컴퓨터 그래픽스 입문]을 보고 공부하고 정리한 내용