셰이더는 그래픽스 파이프라인에서 특정 단계를 수행하기 위해 GPU에서 실행되는 작은 프로그램이다. 파이프라인 상에 여러 셰이더가 존재할 수 있고 대표적으로 사용되는 것은 버텍스 셰이더, 프레그먼트 셰이더가 있다.
하지만 이 밖에도 다양한 셰이더들이 존재하고, 이러한 셰이더를 활용하는 것은 GPU의 병렬 처리 성능을 통해 많은 양의 데이터를 동시에 빠르게 처리할 수 있다는 장점이 있다.
그래픽스 파이프라인을 따라가면서 마주치는 각 스테이지와 셰이더들에 대해 정리해보는 시간을 가져보고자 한다.
버텍스 쉐이더 전에는 버텍스 쉐이더에 입력을 제공하는 버텍스 페칭 또는 버텍스 풀링 고정함수 스테이지가 존재한다.
버텍스 쉐이더에 입력을 제공하는 방식은 특별하지 않다. GLSL에서는 in/out 키워드를 통해 쉐이더의 입력과 출력 변수를 다룬다.
layout (location = 0) in vec4 pos;
버텍스 쉐이더를 작성하다보면 위 구조의 코드를 볼 수 있다.
이는 버텍스 속성(location)이 0번으로 선언된 데이터를 pos 라는 입력으로 받는다는 의미이다.
버텍스의 속성 세팅은 앞서 glVertexAttrib*() 계열 함수를 활용하여 VAO에 버텍스 버퍼의 속성을 입력해준 바 있다.
버텍스 속성의 위치가 0번이고, 이 속성을 가리키는 pos가 버텍스 쉐이더의 입력으로 쓰이게 되는 것이다.
버텍스 쉐이더에서 작업 완료된 변수들은 out 키워드로 포장하여 다음 스테이지 쉐이더의 입력으로 (보통 프레그먼트 쉐이더) 보내진다.
out vec4 vs_color;
이렇게 선언된 변수는 쉐이더 내에서 작업을 거친 후 값을 대입하여 다음 스테이지의 입력으로 넘겨진다.
여러 변수를 하나의 인터페이스 블록으로 그룹화하여 in/out을 관리해보자.
기본적인 형태는 구조체와 유사하나, 입력인지 출력인지를 나타낼 in/out 키워드가 붙는다.
// in vs
out VS_OUT
{
vec4 color;
} vs_out;
// in fs
in VS_OUT
{
vec4 color;
} fs_in;
인터페이스 블록 명은 어떤 쉐이더에서든 동일하게 사용해야하나, 인스턴스 이름은 쉐이더마다 다르게 사용 가능하다.
조각화 라고 번역되는 테셀레이션은 프리미티브를 좀 더 작고 단순한 여러개의 렌더링 가능한 프리미티브로 분할하는 작업이다. openGL의 고정함수로, 들어온 버텍스들을 바탕으로 저 작은 도형으로 쪼개준다. 그렇게 분할된 프리미티브는 일반적인 rasterization HW에 의해 그래픽스 파이프라인을 따라 흐르게 된다.
테셀레이션은 테셀레이션 컨트롤 쉐이더, 고정함수 테셀레이션 엔진, 테셀레이션 이벨류에이션 쉐이더로 구성된다.
(+ 테셀레이션을 직접 구현해보려 하였으나, openGL 4.0 이상부터 지원하기 때문에 3.3 환경을 사용중인 나는 다음을 기약하기로 함...ㅠㅠ)
테셀레이션 컨트롤 쉐이더라고 불리우는 이 녀석은, 버텍스 쉐이더로부터 입력을 받는다.
두 가지 일을 수행한다.
패치라고 불리는 큰 도형을 점, 선, 삼각형 등으로 분할하기 위해서 각 패치를 여러 제어점(Control Point)로 만든다.
openGL 파이프라인의 고정함수.
분할의 역할을 수행한다.
분할된 프리미티브들을 생성해서 해당 버텍스들을 TES로 전달한다. 즉, 테셀레이션 엔젠은 TES의 호출에 사용될 인자를 생성한다.
테셀레이터에 의해 생성된 각 버텍스에 대해 한 번씩 TES가 호출된다. TES는 각 버텍스의 위치나 속성들을 계산하는 역할을 한다.
테셀레이션을 활용하면, 수 많은 버텍스를 생성하여 좀 더 부드럽고 자연스러운 곡면을 만들기에 유리하다. 또한 일일히 버텍스를 입력해줘야하는 수고를 덜 수 있다.
지오메트리 쉐이더(Geometry Shader)는 OpenGL 셰이더 파이프라인의 한 부분으로, 버텍스 셰이더와 프래그먼트 셰이더 사이에 위치합니다. 이 쉐이더의 주요 역할은, 버텍스 셰이더에서 출력된 기본 도형(프리미티브)을 받아 이를 변형하거나, 더 많은 정점이나 도형을 생성하는 것입니다. 이를 통해 점(Point), 선(Line), 삼각형(Triangle) 등의 기본 도형을 확대하거나 세밀하게 표현할 수 있습니다.
셰이더 파일 작성
지오메트리 쉐이더는 GLSL로 작성되며, #version 지시문과 함께 layout을 사용하여 입력과 출력을 정의합니다.
아래는 삼각형을 받아서 각각의 삼각형을 확장하여 더 많은 삼각형을 생성하는 예시입니다.
#version 450 core
layout (triangles) in; // 입력: 삼각형
layout (triangle_strip, max_vertices = 3) out; // 출력: 삼각형 스트립, 최대 3개의 정점 생성 가능
void main() {
for (int i = 0; i < 3; i++) {
gl_Position = gl_in[i].gl_Position; // 입력된 삼각형 정점 그대로 출력
EmitVertex(); // 정점 출력
}
EndPrimitive(); // 프리미티브(삼각형) 종료
}
지오메트리 쉐이더 컴파일
지오메트리 쉐이더는 버텍스 및 프래그먼트 셰이더와 마찬가지로 OpenGL에서 컴파일하고 프로그램에 링크해야 합니다. glCreateShader(GL_GEOMETRY_SHADER)를 사용하여 지오메트리 쉐이더를 생성합니다.
지오메트리 쉐이더 활성화
지오메트리 쉐이더를 사용하려면 다른 셰이더와 함께 프로그램에 포함되어 있어야 합니다. 프로그램에 링크하고 glUseProgram으로 활성화합니다.
layout(triangles) in;은 삼각형 입력을 의미하고, layout(triangle_strip, max_vertices = 3) out;은 삼각형 스트립을 출력하며 최대 3개의 정점을 생성할 수 있다는 뜻입니다.버텍스에 대한 정보가 확정되고 난 뒤, 해당 버텍스들을 그룹화하는 프리미티브 어셈블리 과정을 진행한다. 이후 구성된 오브젝트에 대해 카메라 시야에 들어오는 영역에 대해서만 계산을 진행하기 위한 클리핑을 수행한다. View frustum 영역에 들어오는 프리미티브들에 대해 래스터라이제이션을 진행하여 각 픽셀들의 색을 결정할 수 있게끔 해준다.
그래픽스 파이프라인에서 마지막으로 programmable한 스테이지.
각 프레그먼트(픽셀)의 색상을 결정하여 프레임버퍼로 보내고 그려질 수 있게한다. 래스터라이져에 의해 색상이 입혀질 프레그먼트들의 목록이 fs로 전달된다.
일반적으로 해당 단계에서 lighting, material, depth 등을 결정하게 되어 계산량이 많다. 프레그먼트들은 버텍스의 값을 가지고 보간하여 결정된다.
이 외에도 컴퓨트 쉐이더라는 범용적인 쉐이더가 존재하기도 한다.
간단하게 그래픽스 파이프라인과 함께 사용되는 쉐이더들에 대해 알아보았다.
버텍스 쉐이더에서 이용할 버텍스들에 대한 계산을 진행하고, 이후 테셀레이션 과정을 거쳐 이 버텍스들을 세분화하며 지오메트리 쉐이더를 통해 발전시킬 수 있었다. 이렇게 만들어진 버텍스들을 가지고 레스터라이제이션을 거쳐 각 프레그먼트에 대한 세팅을 진행해주는 프레그먼트 쉐이더까지 어떤 녀석인지 보았다. 이후 한 단계씩 자세하게 들여다보며 공부해보도록 하겠다.