본문 바로가기

Graphics Note

OpenGL 강좌 - 5. 셰이더 프로그래밍 기초

셰이더는 GLSL(GL Shader Language) 문법으로 작성된 코드뭉치이다. 셰이더 또한 새로운 언어이므로 문법을 익혀야하지만, 다행히 C 문법과 굉장히 유사해 따로 공부해야하는 부담은 적다. 다만 정점 셰이더, 나아가 다른 형태의 셰이더를 이해하기 위한 기본 문법은 알아야하니 이번 강좌에서 간략히 설명하겠다.

(OpenGL 명세와 마찬가지로 GLSL 명세가 독립된 버전으로 존재한다. 다행히 현재 GLSL 명세는 OpenGL과 일치하는 4.5버전이 최신이다. 설명은 GLSL 4.5 명세를 기준으로 삼는다.)


* data type

* input/output variables

* uniform variables

* layout qualifier

* function



데이터 타입


셰이더 언어의 기본 타입은 C 언어의 기본 타입을 대부분 포함하며 정점을 보다 편리하게 다루기 위한 추가적인 타입을 제공한다.


스칼라 타입

void 엄밀히 말해 데이터 타입은 아니지만 마땅히 넣을곳이 없었다. 함수의 반환 타입으로만 사용가능하다.

bool / int / uint / float / double 당신이 생각하는 그 타입. uint는 unsigned int이다.


벡터 타입: 정점 속성등을 다루기 편하도록 벡터타입을 제공한다. n은 2,3 혹은 4가 될 수 있다.

vecn        각 컴포넌트가 single-precision인 n차원 벡터 타입이다.

dvecn        각 컴포넌트가 double-precision인 n차원 벡터 타입이다.

bvecn        각 컴포넌트가 bool 타입.

ivecn         각 컴포넌트가 int 타입.

uvecn        각 컴포넌트가 uint 타입.


매트릭스 타입: 그래픽스에서는 변환(transformation)등 행렬연산이 빈번하므로 행렬타입을 제공한다. n과 m은 2,3 혹은 4가 될 수 있다. column-major matrix이다.

matn    각 엘리먼트가 single-precision인 n차 정방행렬 타입이다.

matmxn 각 엘리먼트가 single-precision인 mxn 행렬 타입이다.

dmatn    각 엘리먼트가 double-precision인 n차 정방행렬 타입이다.

dmatmxn 각 엘리먼트가 double-precision인 mxn 행렬 타입이다.


벡터 및 매트릭스 타입은 잘 만들어진 구조체라 이해하면 편하다. 벡터 타입은 대략 아래와 같이 사용할 수 있다.

vec2 var_v2 = vec2(1.0, 2.0);
vec3 var_v3 = vec3(var_v2, 3.0);
vec4 var_v4;
var_v4.x = var_v3.x;
var_v4.yz = var_v2;
var_v4.w = 4.0;

벡터 타입의 각 컴포넌트에 접근하는 형태를 swizzling이라 부르는데, var_v4 = var_v2.xyxy; 와 같이 특이한 형태로도 사용 가능하다.

매트릭스 타입은 2차원 배열, 혹은 열벡터의 1차원 배열 형태로 다뤄질 수 있다.

mat2 var_m2;
var_m2[0] = vec2(1.0, 2.0); //set 1st column to (1.0, 2.0)
var_m2[1][0] = 3.0; //set 2-column, 1-row  element to 3.0

mat4 var_m4;
var_m4[0].xyzw = var_m2[0].xyxy;

이 외에도 텍스쳐 샘플링(texture sampling)이라는것을 위한 샘플러(sampler) 타입, 텍스쳐 본인인 이미지(image) 타입등이 있는데 여기에 모든걸 설명하기는 힘들고, 강좌를 진행하면서 필요에 따라 설명하겠다.



입력/출력 변수(input/output variables)


GLSL은 그래픽스 파이프라인의 각 셰이더를 정의하기 위한 언어인 만큼, 각 셰이더 사이의 입/출력에 관한 문법이 필요하다. 이를 위해 입력/출력 변수라는 개념이 존재하며, CPU로부터 데이터를 전달받고 각 셰이더의 연산 결과를 전달하여 최종 결과를 얻을 수 있다.

입력 변수는 이 전 셰이더 스테이지(shader stage)와 연결되고, 출력 변수는 다음 셰이더 스테이지와 연결된다. 연결되는 스테이지는 다음 순서를 따르며 중간에 옵션 셰이더가 없다면 제외하고 입출력 쌍이 연결된다.


host CPU program - vertex shader - tessellation control shader - tessellation evaluation shader - geometry shader - fragment shader


입력/출력 변수는 in/out 한정자와 함께 전역변수의 형태로 선언되며 연결된 스테이지의 입력-출력 쌍은 동일한 이름으로 짓는것이 일반적이다. (필수 조건인지는 확인해보지 못했다.)

/* vertex shader */
in vec3 position; //host CPU 코드로부터 받는 정점 속성. 다음 강좌에서 설명할 예정이다.
out vec4 to_frag; //정점 셰이더 -> 프래그먼트 셰이더로의 정보 전달. 정점 셰이더 입장에서 출력 변수이다.

void main() {
	gl_Position = vec4(position, 1.0);
	to_frag = vec4(1.0, 1.0, 1.0, 1.0);
}
/* fragment shader */
in vec4 to_frag; //정점 셰이더로부터 전달된 변수. 프래그먼트 셰이더 입장에선 입력 변수이다.
out vec4 pixel_color; //프래그먼트 셰이더의 vec4 타입 출력 변수는 '최종 색상'을 의미한다.

void main() {
	pixel_color = to_frag;
}

대략 위와 같은 형태로 사용되며, 다른 셰이더가 사이에 끼면 사용하는 방식이 조금씩 차이가난다. 강좌가 진행되면서 설명될 것이다.


위 예제에서 gl_Position은 미리 정의된 (built-in) 변수이다. 예전 GLSL 버전에서는 이런 변수가 굉장히 많았지만, 현재는 줄고 줄어 꼭 필요한 변수들만 남게되었다. 모두 gl_ 가 붙는 변수이며 입/출력 변수와 마찬가지로 셰이더에 따라 미리 정의된 변수의 종류와 쓰임새가 다르다. 자주 사용되는것들만 매우 대략적으로 살펴보고 넘어가자.


gl_Position: 정점 셰이더의 출력변수중 다음 스테이지에게 정점의 위치정보를 전달한다.

gl_FragCoord: 프래그먼트 셰이더의 입력변수중 현재 픽셀의 스크린 공간 좌표값이다.

gl_FragDepth: 프래그먼트 셰이더의 출력변수중 픽셀의 깊이값을 전달한다.



유니폼 변수(uniform variables)


앞서 입/출력 변수는 인접한 스테이지간 정보전달을 위한 변수였다면, 모든 셰이더에서 공통적으로 사용할 수 있는 변수도 정의할 수 있다. 이를 유니폼 변수라 부르는데, uniform qualifier를 통해 정의한다.


유니폼 변수는 필요한 셰이더에서 전역변수의 형태로 선언한 후 사용하면 되며, host CPU 코드에서 glUniform- 형태의 함수집단을 사용하여 값을 전달해준다.

uniform vec4 matWorld; //오브젝트-월드 공간 변환 매트릭스
uniform vec4 matViewProj; //월드-프로젝션 공간 변환 매트릭스

대표적으로 변환 매트릭스를 전달할때 사용된다. 실질적인 사용 예는 강좌가 진행되면서 많이 접하게 될 것이다.



레이아웃 한정자(layout qualifier)


GLSL은 스스로만 존재하는 프로그램이 아니다. 각 셰이더가 유기적으로 연결되고, CPU 코드와도 통신이 필요하다. 따라서 변수의 레이아웃을 명시하는것이 굉장히 중요한 부분인데, 이 기능을 GLSL 문법인 레이아웃 한정자를 통해 제공하며 다음과 같은 형태이다.


layout( quantifier1, quantifier2 = value, ...) variable definition;


레이아웃 한정자는 어떤 셰이더 코드인지, 어떤 형태의 변수를 위해 사용되는지에 따라 사용할 수 있는 한정자에 차이가 있다. 가장 대표적인 사용처는 셰이더 입력/출력 변수의 인덱스를 지정할 때이다. 이는 다음 강좌에서 볼 수 있을것이다.



함수


함수의 선언 및 정의는 C 문법과 거의 동일하지만 C++문법처럼 overloading이 가능하다. GPU 처리 특성상 재귀호출을 할 수 없으니 유의하자.

C 프로그래밍과 큰 차이점은, GLSL은 포인터 타입이 없기때문에 인자는 모두 call by value 형태로 전달된다는 점이다. 대신 함수와의 상호작용을 위해 in/out/inout 매개변수 한정자(parameter qualifier)를 제공한다


in    함수 내부에서 읽기만 가능. 한정자를 명시하지 않았을 때 기본 한정자이다.

out   함수 내부에서 호출자로의 쓰기만 가능

inout 함수 내부에서 값을 읽고, 호출자로의 쓰기 모두 가능하다.


함수는 직접 정의할 수 있지만, GLSL은 mul, sin, lerp등 자주 사용되는 함수 대부분을 제공해준다. 미리 정의된 함수들은 GLSL 명세서를 살펴보자.



그 외


조건문, 반복문, 배열, 구조체등은 C 문법과 거의 동일하므로 설명은 생략한다. 


이러한 문법 말고도 GLSL 언어만이 가지는 고유한 특징들이 굉장히 많지만, 일일이 설명하는것은 의미가 없으므로 강좌의 필요에 맞게 추가설명할 예정이다.


GLSL 프로그래밍은 디버깅도 힘들고 변칙적인 에러도 은근히 자주 발생해서 꽤 골머리 썩히는 부분이다. 무식하지만 많은 삽질을 통해 경험을 축적하는게 가장 좋은 공부 방법인것같다. 이것저것 많이 실험해보자.