<aside> 💡

Vulkan Tutorial 영어 원문

</aside>

이전 API들과는 다르게 Vulkan의 셰이더 코드는 GLSL이나 HLSL같이 사람이 읽을 수 있는 구문이 아닌 바이트 코드 형식으로 지정해야 합니다. 이 바이트 코드 형식은 SPIR-V라고 부르며 Vulkan과 OpenCL(둘 다 Khronos의 API들)과 함께 사용하도록 설계되었습니다. 그래픽과 컴퓨트 셰이더를 작성할 수 있는 형식이지만, 이 튜토리얼에서는 Vulkan의 그래픽스 파이프라인에서 사용하는 셰이더에 중점을 두겠습니다.

바이트 코드 형식을 사용할 때 장점은 GPU 공급 업체가 셰이더 코드를 네이티브 코드로 바꾸기 위해 작성된 컴파일러가 덜 복잡하다는 것입니다. 과거 GLSL 문법과 같이 사람이 읽을 수 있는 구문을 사용하여, 몇 GPU 공급 업체들은 표준을 꽤 유연하게 해석했습니다. 공급 업체들 중 하나의 GPU를 사용하여 중요하지 않은 셰이더를 작성하는 경우, 구문 오류로 인해 다른 공급 업체의 드라이버가 코드를 거부하거나 컴파일러 버그로 인해 셰이더가 다르게 실행될 수 있습니다. SPIR-V 같은 간단한 바이트 코드 형식을 사용하면 이러한 문제를 피할 수 있습니다.

그러나 우리가 바이트 코드를 일일히 작성할 필요는 없습니다. Khronos는 GLSL을 SPIR-V로 컴파일하는 공급업체에 독릭된 자체적인 컴파일러를 출시하였습니다. 이 컴파일러는 셰이더 코드가 완전히 표준을 지키고, 프로그램과 함께 제공될 수 있는 하나의 SPIR-V 바이너리를 생산하도록 설계되었습니다. 이 컴파일러를 라이브러리로 포함하여 런타임 상에서 SPIR-V를 생성하도록 할 수 있지만, 이 튜토리얼에서는 그렇게 하지 않을 것입니다. Google의 glslangValidator.exe이나 glslc.exe 같은 컴파일러를 직접 사용할 수 있습니다. glslc의 장점은 GCC나 Clang과 같이 유명한 컴파일러들과 같은 매개변수 형식을 사용하고, include같은 몇가지 추가 기능을 포함한다는 것입니다. 둘 다 Vulkan SDK에 이미 포함되어 있어서 추가로 다운로드 받을 필요는 없습니다.

GLSL은 C-스타일 구문을 사용하는 셰이더 언어입니다. 여기에 작성된 프로그램은 모든 오브젝트들에 대해 호출되는 main이라는 함수를 가지고 있습니다. 입력에 매개변수를 사용하고 출력으로 반환 값을 사용하는 대신, GLSL은 전역 변수를 사용하여 입력과 출력을 처리합니다. 이 언어에는 내장된 vector나 matrix 요소와 같이 그래픽 프로그래밍을 지원하는 많은 기능들이 포함되어 있습니다. 외적, 행렬-벡터 곱, 반사 벡터와 같은 기능들이 포함되어 있습니다. 벡터 타입은 요소의 양을 나타내는 vec라는 것으로 불립니다. 예를 들어, 3D 위치는 vec3에 저장됩니다. .x와 같이 단일 개체에 접근할 수 있지만, 여러 구성 요소를 통해 새로운 벡터를 만드는 것도 가능합니다. vec3(1.0, 2.0, 3.0).xy의 결과는 vec2입니다. 벡터의 생성자는 백터 객체와 스칼라 값의 조합을 사용할 수 있습니다. vec3(vec2(1.0, 2.0), 3.0)으로 vec3를 생성할 수 있습니다.

이전 챕터에서 언급했듯이, 삼각형을 표시하려면 정점 셰이더와 조각 셰이더를 작성해야 합니다. 다음 두 섹션에서 각각의 GLSL 코드를 다루고, 두 개의 SPIR-V 바이너리를 생성하여 프로그램에 로드하는 방법을 보겠습니다.

정점 셰이더(Vertex shader)

정점 셰이더는 들어온 각 정점들을 처리합니다. 월드 위치, 색상, 법선과 텍스쳐 좌표 같은 속성들을 입력받습니다. 클립 좌표의 최종 위치와 색상이나 텍스쳐 좌표 같은 조각 셰이더에 전달할 속성들을 출력합니다. 이 값은 부드러운 그라디언트를 생성하기 위해 래스터라이저에 의해 보간된 후 조각 셰이더로 전달됩니다.

클립 좌표는 정점 셰이더의 4차원 벡터로, 전체 벡터를 마지막 컴포넌트로 나누어 정규화된 장치 좌표로 변환합니다. 이러한 정규화된 장치 좌표는 프레임버퍼를 다음과 같이 [-1, -1]에서 [1, 1] 좌표계로 매핑하는 동종 좌표계입니다.

normalized_device_coordinates.svg

이전에 컴퓨터 그래픽스를 건드려본 적이 있다면 친숙해야 합니다. OpenGL을 써봤다면 Y 좌표의 부호가 뒤집힌 것을 알아차릴 것입니다. 이제 Z 좌표는 0에서 1까지, Direct3D와 동일한 범위를 사용합니다.

첫번째 삼각형은 아무 변환도 적용하지 않고, 세 정점의 위치를 정규화된 장치 좌표로 직접 지정하여 다음과 같은 모습으로 만들 것입니다:

triangle_coordinates.svg

우리는 정점 셰이더에서 나온 클립 좌표의 마지막 요소를 1로 세팅하여 정규화된 장치 좌표를 직접 출력할 수 있습니다. 이렇게 하면 클립 좌표를 정규화된 장치 좌표로 변환하는 나눗셈은 아무 것도 바꾸지 않을 것입니다.

일반적으로 이러한 좌표들은 정점 버퍼에 저장되지만, Vulkan에서 정점 버퍼를 만들고 데이터를 채우는 것은 간단하지 않습니다. 그러므로 삼각형이 화면에 나타나는 것을 보고 만족할 때까지 그 작업을 미룰 것입니다. 그 동안에는 비정석적인 작업을 수행할 것입니다: 정점 셰이더에 좌표를 직접 포함하는 것입니다. 코드는 다음과 같습니다.

#version 450

vec2 positions[3] = vec2[](
    vec2(0.0, -0.5),
    vec2(0.5, 0.5),
    vec2(-0.5, 0.5)
);

void main() {
    gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}

main 함수는 모든 버텍스에 의해 호출됩니다. gl_VertexIndex 변수는 현재 정점의 인덱스를 포함합니다. 원래라면 정점 버퍼에 대한 인덱스이지만, 우리의 경우는 하드코딩된 배경에 대한 인덱스가 되겠죠. 각 정점의 위치는 상수 배열에서 접근 가능하고, 더미로 된 z와 w 요소를 결합하여 클립 좌표의 위치를 만듭니다. 내장된 변수인 gl_Position는 출력값으로 사용됩니다.

조각 셰이더(Fragment shader)

정점 셰이더에 의해 형성되는 삼각형은 화면 영역을 조각으로 채웁니다. 조각 셰이더는 이러한 조각들에게서 호출되어 프레임버퍼의 색상과 깊이를 생성합니다. 삼각형 전체를 빨간색으로 출력하는 간단한 조각 셰이더는 다음과 같습니다:

#version 450

layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(1.0, 0.0, 0.0, 1.0);
}