Post

Vulkan 튜토리얼: 그래픽스 파이프라인 생성하기

이번 튜토리얼에서는 그래픽스 파이프라인 생성을 위한 버텍스 및 프라그멘트 셰이더 코드를 작성하고, 그래픽스 파이프라인에 어떤 상태(state)가 있는지를 알아보겠습니다.

그래픽스 파이프라인 개요

A brief diagram of graphics pipeline 간략히 나타낸 그래픽스 파이프라인, 버텍스 버퍼와 인덱스 버퍼는 위 파이프라인을 거쳐 이미지의 픽셀로 대응합니다. (Vulkan Tutorial)

프리미티브 토폴로지

그래픽스 파이프라인은 입력으로 버텍스 버퍼와 (선택적으로) 인덱스 버퍼가 주어집니다. 버텍스 버퍼는 우리가 렌더링할 도형의 정점(vertex)의 정보(이는 정점의 좌표 뿐만 아니라 추가적인 애트리뷰트(attribute)를 포함할 수 있습니다: 색/텍스쳐 좌표 등)를 배열 형태로 담고 있습니다.

만일 인덱스 버퍼가 주어지지 않을 경우, 위 파이프라인의 Input assembler(입력 조합기)가 정점을 순서대로 조합하여 프리미티브(primitive)를 구성합니다. 이 프리미티브를 구성하는 방법은 여러가지가 있습니다. 예를 들어, 버텍스 버퍼의 각 원소를 v1, v2, v3, ...로 나열하면

  • Lines: v1, v2, v3, v4, v5, v6, …
  • Line strips: v1, v2, v2, v3, v3, v4, …
  • Triangle list: v1, v2, v3, v4, v5, v6, …
  • Triangle strips: v1, v2, v3, v2, v3, v4, v3, v4, v5, …
  • Triangle fans: v1, v2, v3, v1, v3, v4, v1, v4, v5, …

등의 프리미티브 구성을 가질 수 있습니다. Vulkan은 이러한 프리미티브 구성 방법을 토폴로지(topology)라고 합니다.

인덱스 버퍼가 주어졌을 경우, 입력 조합기는 해당 인덱스에 대응하는 정점으로 위 조합을 대신합니다. 예를 들어, 인덱스 버퍼의 각 원소를 i1, i2, i3, ...로 나열하면

  • Lines: v[i1], v[i2], v[i3], v[i4], v[i5], v[i6], …
  • Line strips: v[i1], v[i2], v[i2], v[i3], v[i3], v[i4], …
  • Triangle list: v[i1], v[i2], v[i3], v[i4], v[i5], v[i6], …
  • Triangle strips: v[i1], v[i2], v[i3], v[i2], v[i3], v[i4], v[i3], v[i4], v[i5], …
  • Triangle fans: v[i1], v[i2], v[i3], v[i1], v[i3], v[i4], v[i1], v[i4], v[i5], …

과 같은 식입니다.

Primitive topology 프리미티브 토폴로지를 도표로 나타낸 모습. (Microsoft learn)

Polyhedron 우리가 정이십면체를 렌더링한다고 가정해보겠습니다. 위 두 방법 중 어느 것이 더 효율적일까요? 정이십면체는 스무 개의 정삼각형으로 이루어진 정다면체이고, 한 꼭짓점이 5개의 면에 의해 공유됩니다. 따라서 정이십면체는 20×3/5=12개의 꼭짓점으로 이루어져 있게 됩니다.

위 Triangle list 토폴로지를 이용한다 가정했을 떄, 버텍스 버퍼만을 이용하는 경우 20개 삼각형에 필요한 총 60개의 정점을 저장해야 합니다. 반면 버텍스 버퍼와 인덱스 버퍼를 같이 이용하는 경우 버텍스 버퍼에는 20개의 정점을, 인덱스 버퍼에는 60개의 인덱스를 저장하면 됩니다. 대개 버텍스 버퍼는 정점의 좌표 외에도 여러 애트리뷰트를 포함하고 있으므로, 하나의 원소의 용량이 인덱스보다 크고, 따라서 이 경우 정점 정보를 줄이고 인덱스 버퍼에서 정점을 공유하는 것이 더 효율적인 방법이 될 것입니다.

TODO: colored cube image. 반면, 이번에는 각 면 별로 다른 색을 가지는 정육면체를 렌더링한다고 가정합시다. 버텍스/인덱스 버퍼를 이용한 방법으로는 면에 따른 애트리뷰트를 정할 수는 없기 때문에, 한 면에 사용되는 색 애트리뷰트는 삼각형의 세 정점이 같은 값을 가져야 합니다. 따라서 설사 정육면체의 한 정점의 좌표가 세 개에서 최대 여섯개의 삼각형에 공유되더라도, 이들은 서로 다른 컬러 애트리뷰트를 가져야 하기 때 다른 정점을 사용해야 합니다. 즉, 이런 경우에는 인덱스 버퍼가 의미가 없기 때문에 (이미 버텍스 버퍼가 총 6×2×3=36개의 정점을 가집니다) 버텍스 버퍼만을 사용하는 것이 좋습니다.

버텍스 셰이더

이렇게 입력 버텍스 (및 인덱스) 버퍼로부터 조합된 프리미티브는 그 정점이 순차적으로 버텍스 셰이더의 입력됩니다. 버텍스 셰이더는 다음 두 가지의 기능을 할 수 있습니다:

  • 입력된 정점을 NDC(normalized device coordinates)로 변환합니다. 이는 정점을 실제 2차원 화면에 렌더링하기 위한 과정으로, 정점을 $[-1, 1]$의 좌표과 $[0, 1]$의 깊이로 변환하는 작업입니다.1. Vulkan NDC는 이미지의 좌상단을 (-1, -1), 우하단을 (1, 1), 화면을 수직으로 뚫고 들어가는 방향을 +z 방향으로 정의하여 좌표계를 설정합니다2.
  • 각 정점의 어떤 정보를 프라그멘트 셰이더에 보간된(interpolated) 형태로 전송합니다3. 예를 들어, 어떤 삼각형 프리미티브의 세 정점의 정보가 각각 (1, 0, 0), (0, 1, 0), (0, 0, 1)이라면 이 정점의 좌표로부터 래스터라이징된 각 픽셀은 위 세 3차원 벡터를 각 정점으로부터 거리를 기반으로 보간한 어떤 값(예를 들어, 픽셀이 정점 좌표의 무게 중심일 경우, (1/3, 1/3, 1/3))을 가질 것입니다.

위 도표에서 보듯이, 버텍스 셰이더에서 정점의 좌표를 바꿀 수 있습니다. 대개 정점을 4차원 벡터로 변환하고, 4x4 행렬을 사용하여 시점과 투영 행렬(projection matrix)에 맞게 NDC를 결정하게 됩니다.

결국 정점의 NDC를 생성하는 것은 버텍스 셰이더의 역할입니다. 따라서 버텍스 버퍼에는 반드시 정점의 좌표에 관련된 정보를 담을 필요가 없습니다. 버텍스 버퍼에 1차원 좌표를 넣고, 버텍스 셰이더에서 y·z성분을 추가할 수 있습니다. 심지어는 버텍스 버퍼에 아무것도 넣지 않은 채 단순히 몇 개의 정점만을 생성할 것이라는 정보만 버텍스 셰이더에 넘길 수도 있습니다. 이 튜토리얼에서는 먼저 후자의 방법을 이용하여 삼각형을 렌더링할 것입니다.

테셀레이션 셰이더와 지오메트리 셰이더

테셀레이션 셰이더는 버텍스 셰이더로부터 받은 프리미티브를 더 작은 삼각형으로 분할하며, 지오메트리 셰이더는 테셀레이션 셰이더로부터 입력받은 프리미티브에 추가적으로 프리미티브를 생성하거나 파기할 수 있습니다. 이 튜토리얼에서는 두 셰이더는 사용하지 않습니다.

지오메트리 셰이더는 GPU의 각 코어별로 다른 작업을 할당하게 하고, 따라서 병렬 처리의 이점이 감소하는 성능 문제를 갖고 있어 요즘에는 잘 쓰이지 않는 추세입니다. 현대 GPU는 지오메트리 셰이더보다 더 범용적으로 사용할 수 있는 메시 셰이더가 있으므로, 관심이 있는 경우 확인해 보세요.

래스터라이제이션

래스터라이제이션은 프리미티브의 각 정점의 NDC에 기반하여, 프리미티브가 덮는 픽셀을 계산합니다. 또 이때부터 프리미티브의 개념을 상실하고 프라그멘트가 됩니다. 화면을 벗어나는 프라그멘트는 삭제되며, 위에서 설명한 바와 같이 버텍스 셰이더에서 출력된 정보는 각 프라그멘트에 보간되어 입력됩니다. 더불어, 만일 Early Depth Test가 활성화된 경우 깊이 테스트(depth test)에 통과하지 못한 프라그멘트는 파기(discarded)됩니다 (아직은 이에 대해 이해하지 못해도 괜찮습니다).

프라그멘트 셰이더

해당 셰이더는 래스터라이제이션을 거친 모든 프라그멘트에 대해 실행되며, 해당 프라그멘트가 대응하는 컬러 어태치먼트(color attachment)에 쓸 값을 결정하는 단계입니다. 컬러 어태치먼트란 프라그멘트 셰이더의 출력 값을 저장할 이미지를 의미합니다. 대개 이는 프라그멘트의 색(즉, 픽셀의 색)을 의미하나, 다른 어떤 값이든 상관없습니다. 또한 그래픽스 파이프라인을 생성하며 여러 개의 컬러 어태치먼트를 명시할 수도 있습니다 (이 경우 여러 개의 컬러 어태치먼트의 값을 명시해야 합니다).

프라그멘트 셰이더에서 프라그멘트의 깊이를 임의로 정하여 깊이 어태치먼트(depth attachment)에 저장하거나, 프라그멘트를 파기할 수도 있으나, 이는 자칫 셰이더 성능을 저해할 수 있고 Early depth testing과 같은 성능 향상 기능을 사용할 수 없게 되는 제약 사항이 있습니다.

컬러 블렌딩

프라그멘트 셰이더에서 컬러 어태치먼트에 쓸 값을 해당 어태치먼트에 바로 저장하는 것이 아닌, 기존에 저장된 값을 기반으로 하여 결정할 수 있습니다. 이러한 과정을 컬러 블렌딩(color blending)이라고 합니다. 예를 들어, (새 값)=0.5*(기존 값) + 0.5*(출력 값)과 같은 수식을 사용해 기존 어태치먼트와 자연스럽게 병합되는 새 값을 생성할 수 있습니다.


휴, 확실히 무언가 설정해야 할 게 많습니다! 다행이 이 모든 과정을 셰이더로 나타내야 하는 것은 아닙니다. 위 도표의 초록색 사각형에 해당하는 스테이지는 파이프라인의 고정 함수(fixed function)으로써 파이프라인을 생성할 때 결정되는 단계이며4, 나머지 네 개의 노란 사각형에 해당하는 스테이지의 테셀레이션·지오메트리·프라그멘트 셰이더는 반드시 필요한 것은 아닙니다. 이제, 각 스테이지가 어떤 기능을 하는지를 알았으므로, 이제 GLSL을 이용해 버텍스 셰이더와 프라그멘트 셰이더를 작성해봅시다.

셰이더 작성하기

Subject: [PATCH] Create shaders for render colored triangle.
---
Index: src/03_hello-triangle/CMakeLists.txt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/CMakeLists.txt b/src/03_hello-triangle/CMakeLists.txt
--- a/src/03_hello-triangle/CMakeLists.txt
+++ b/src/03_hello-triangle/CMakeLists.txt
@@ -10,4 +10,7 @@
 # Compile shaders.
 # --------------------
 
-compile_shaders(03_hello-triangle)
\ No newline at end of file
+compile_shaders(03_hello-triangle
+    shaders/triangle.vert
+    shaders/triangle.frag
+)
\ No newline at end of file
Index: src/03_hello-triangle/shaders/triangle.frag
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/shaders/triangle.frag b/src/03_hello-triangle/shaders/triangle.frag
new file mode 100644
--- /dev/null
+++ b/src/03_hello-triangle/shaders/triangle.frag
@@ -0,0 +1,9 @@
+#version 450
+
+layout (location = 0) in vec3 fragColor;
+
+layout (location = 0) out vec4 outColor;
+
+void main(){
+    outColor = vec4(fragColor, 1.0);
+}
\ No newline at end of file
Index: src/03_hello-triangle/shaders/triangle.vert
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/shaders/triangle.vert b/src/03_hello-triangle/shaders/triangle.vert
new file mode 100644
--- /dev/null
+++ b/src/03_hello-triangle/shaders/triangle.vert
@@ -0,0 +1,20 @@
+#version 450
+
+const vec2 positions[] = vec2[3](
+    vec2(-0.5, 0.5),
+    vec2(0.5, 0.5),
+    vec2(0, -0.5)
+);
+
+const vec3 colors[] = vec3[3](
+    vec3(1, 0, 0),
+    vec3(0, 1, 0),
+    vec3(0, 0, 1)
+);
+
+layout (location = 0) out vec3 fragColor;
+
+void main() {
+    gl_Position = vec4(positions[gl_VertexIndex], 0, 1);
+    fragColor = colors[gl_VertexIndex];
+}
\ No newline at end of file

triangles.verttriangles.frag 파일을 생성했습니다. 이전에 언급한 바와 같이, .vert.frag 확장자는 해당 파일이 각각 버텍스/프라그멘트 셰이더임을 나타내는 관습적 표현입니다.

버텍스 셰이더에는 vec2의 배열 positionsgl_VertexIndex 인덱스로 접근하여 xy 좌표를 가져오고, 이를 vec4로 바꾸어 gl_Position에 할당했습니다.

gl_VertexIndex는 버텍스 셰이더가 입력 조합기의 몇 번째 정점에서 실행되는지를 의미하는 변수입니다 (예를 들어, 입력 조합기로부터 총 4개의 삼각형 프리미티브가 나오는 경우, 두번째 프리미티브의 첫 번째 정점이 실행되는 버텍스 셰이더의 gl_VertexIndex는 6이 됩니다).

gl_Position은 버텍스 셰이더의 출력 변수로, 이 정점의 NDC는 해당 gl_Position의 xyz 좌표를 w성분으로 나눈 값이 됩니다 (NDC = gl_Position.xyz / gl_Position.w).

따라서 우리는 세 좌표((-0.5, 0.5), (0.5, 0.5), (0, -0.5))를 기반으로 세 NDC((-0.5, 0.5, 0), (0.5, 0.5, 0), (0, -0.5, 0))를 생성했습니다. 따라서 이 정점은 각각 이미지의 다음 좌표에 배치될 것입니다.

Vertices in NDC 프리미티브 정점 배치

더불어, location = 0 애트리뷰트에 out 지정자로 vec3을 넘겼습니다. 이 값은 앞서 말했듯이 래스터라이제이션 후 각 프라그멘트에서 보간되는 컬러 값으로, 마찬가지로 빨강((1, 0, 0)), 초록((0, 1, 0)), 파랑((0, 0, 1))을 갖습니다.

프라그멘트 셰이더에서는 버텍스 셰이더에서 출력한 값을 동일한 location = 0 애트리뷰트에 in 지정자로 받았습니다. 이 값은 보간된 값으로, 각 프라그멘트별로 다른 값을 가질 것입니다 (모든 프라그멘트가 세 정점까지의 다른 거리를 가지므로). 이 값을 vec4로 변환하여 outColor에 할당했습니다. 이 값은 프라그멘트 셰이더의 출력으로, 이 값이 location = 0 애트리뷰트에 out 지정자로 정의된 0번째 컬러 어태치먼트에 출력됩니다.

우리가 삼각형을 렌더링하는데 필요한 모든 정보가 버텍스 셰이더에 포함돼있음을 확인하세요. 이에 미루어보아 버텍스 버퍼와 인덱스 버퍼가 필요하지 않음을 예상할 수 있습니다. 또한, 0번째 컬러 어태치먼트에 vec4 타입을 저장하므로, 그래픽스 파이프라인에 한 개의 4채널 컬러 어태치먼트를, 그 형식으로 R8G8B8A8_UNORM을 사용할 것도 알 수 있습니다.

만일 컬러 어태치먼트의 형식으로 R8G8B8A8_UINT를 사용한다고 해서 프라그멘트 셰이더에서 uvec4 타입을 사용할 필요는 없습니다. 셰이더에서 부동소수점 및 그 벡터 타입을 사용하면 0.0부터 1.0까지의 값이 해당 컬러 어태치먼트 타입의 최소값(0)부터 최대값(255)까지의 범위로 변환됩니다.

또한 이번 두 셰이더에는 디스크립터 셋이 사용되지 않았습니다. (set 지정자가 없습니다!) 따라서 코드에서 디스크립터 관련 내용은 지워도 됩니다.

Subject: [PATCH] Remove unnecessary descriptor set related stuffs.
---
Index: src/03_hello-triangle/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/main.cpp b/src/03_hello-triangle/main.cpp
--- a/src/03_hello-triangle/main.cpp
+++ b/src/03_hello-triangle/main.cpp
@@ -248,25 +248,8 @@
     } };
     linearImage.bindMemory(*linearImageMemory, 0);
 
-    // Create descriptor set layout.
-    const vk::raii::DescriptorSetLayout descriptorSetLayout = [&] {
-        constexpr vk::DescriptorSetLayoutBinding layoutBinding {
-            0,
-            vk::DescriptorType::eStorageImage,
-            1,
-            vk::ShaderStageFlagBits::eCompute,
-        };
-        return vk::raii::DescriptorSetLayout { device, vk::DescriptorSetLayoutCreateInfo {
-            {},
-            layoutBinding,
-        } };
-    }();
-
     // Create pipeline layout.
-    const vk::raii::PipelineLayout pipelineLayout { device, vk::PipelineLayoutCreateInfo {
-        {},
-        *descriptorSetLayout,
-    } };
+    const vk::raii::PipelineLayout pipelineLayout { device, vk::PipelineLayoutCreateInfo{} };
 
     // Create compute shader module.
     const vk::raii::ShaderModule computeShaderModule = [&] {
@@ -289,39 +272,6 @@
         *pipelineLayout,
     } };
 
-    // Create descriptor pool.
-    const vk::raii::DescriptorPool descriptorPool = [&] {
-        constexpr vk::DescriptorPoolSize poolSize {
-            vk::DescriptorType::eStorageImage,
-            1,
-        };
-        return vk::raii::DescriptorPool { device, vk::DescriptorPoolCreateInfo {
-            {},
-            1,
-            poolSize,
-        } };
-    }();
-
-    // Allocate descriptor set from descriptor pool.
-    const vk::DescriptorSet descriptorSet = (*device).allocateDescriptorSets(vk::DescriptorSetAllocateInfo {
-        *descriptorPool,
-        *descriptorSetLayout,
-    }).front();
-
-    // Write image info to descriptorSet.
-    const vk::DescriptorImageInfo imageInfo {
-        {},
-        *imageView,
-        vk::ImageLayout::eGeneral,
-    };
-    (*device).updateDescriptorSets(vk::WriteDescriptorSet {
-        descriptorSet,
-        0,
-        0,
-        vk::DescriptorType::eStorageImage,
-        imageInfo,
-    }, {});
-
     // Create command pool.
     const vk::raii::CommandPool commandPool { device, vk::CommandPoolCreateInfo {
         {},

그래픽스 파이프라인 생성하기

그래픽스 파이프라인을 생성하는 것은 컴퓨트 파이프라인과 마찬가지로 셰이더 모듈을 불러오고 (이 경우 버텍스 및 프라그멘트 셰이더로 2개를 불러와야 합니다) 스테이지를 만든 후, 앞서 설명했던 고정 함수를 결정해야 합니다.

vk::GraphicsPipelineCreateInfo 개요

본 내용은 Vulkan 명세에 보다 자세히 설명돼 있으니, 확인해보세요.

다음은 그래픽스 파이프라인 생성 정보 구조체 vk::GraphicsPipelineCreateInfo 생성자 인수입니다.

`vk::GraphicsPipelineCreateInfo` 생성자 인수 vk::GraphicsPipelineCreateInfo 생성자 인수

음… 모든 것을 명시할 필요는 없습니다! 이 인수는 4개의 범주로 나눌 수 있는데, 앞서 설명한 파이프라인 스테이지와 연계해서 생각해보겠습니다.

  • 정점 입력 상태(vertex input state)
    • vk::PipelineVertexInputStateCreateInfo: 버텍스 버퍼로부터 어떻게 정점을 가져올지를 결정합니다. 이에 대한 내용은 앞으로 버텍스 버퍼를 사용할 때 설명하겠습니다.
    • vk::PipelineInputAssemblyStateCreateInfo: 위 스테이지의 입력 조합기에 해당합니다. 어떻게 프리미티브 토폴로지를 만들지 결정합니다.
  • 래스터라이제이션 이전 셰이더 상태(pre-rasterization shader state)
    • vk::PipelineShaderStageCreateInfo 중 버텍스 셰이더: 버텍스 셰이더 스테이지입니다.
    • vk::PipelineTessellationStateCreateInfo: 프리미티브 테셀레이션에 대한 단계입니다. 이 튜토리얼에서는 사용하지 않습니다.
    • vk::PipelineViewportStateCreateInfo: 뷰포트(viewport)에 대한 단계입니다. 뷰포트는 우리가 이미지의 어느 영역을 NDC의 기준으로 할지 정하는 뷰포트(vk::Viewport)와, 어느 영역을 벗어났을 때 프라그멘트를 삭제할 지 정하는 가위(scissor; vk::Rect2D)로 구성됩니다 (대개 이 둘은 같은 값을 가집니다).
    • vk::PipelineRasterizationStateCreateInfo: 래스터라이제이션에 대한 단계입니다. 여기에서는 프리미티브를 채울지/비울지(변만 렌더링), 프리미티브 변의 두께를 결정합니다.
  • 프라그멘트 셰이더 상태(fragment shader state)
    • vk::PipelineShaderStageCreateInfo 중 프라그멘트 셰이더: 프라그멘트 셰이더 스테이지입니다.
    • vk::PipelineMultisampleStateCreateInfo: 멀티 샘플링에 대한 단계로, 지금은 다루지 않습니다.
    • vk::PipelineDepthStencilStateCreateInfo: 깊이 및 스텐실(stencil) 검사에 대한 단계로, 지금은 다루지 않습니다.
  • 프라그멘트 출력 상태(fragment output state)
    • vk::PipelineColorBlendStateCreateInfo: 컬러 블렌딩에 대한 단계로, 각 컬러 어태치먼트 별 어떤 블렌딩을 사용할 지 결정합니다.

셰이더 모듈 로드

Subject: [PATCH] Create shader modules.
---
Index: src/03_hello-triangle/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/main.cpp b/src/03_hello-triangle/main.cpp
--- a/src/03_hello-triangle/main.cpp
+++ b/src/03_hello-triangle/main.cpp
@@ -251,26 +251,21 @@
     // Create pipeline layout.
     const vk::raii::PipelineLayout pipelineLayout { device, vk::PipelineLayoutCreateInfo{} };
 
-    // Create compute shader module.
-    const vk::raii::ShaderModule computeShaderModule = [&] {
-        const std::vector shaderCode = readFile("shaders/grid.comp.spv");
-        return vk::raii::ShaderModule { device, vk::ShaderModuleCreateInfo {
-            {},
-            shaderCode,
-        } };
-    }();
-
     // Create compute pipeline.
-    const vk::raii::Pipeline computePipeline { device, nullptr, vk::ComputePipelineCreateInfo {
-        {},
-        vk::PipelineShaderStageCreateInfo {
+    const vk::raii::Pipeline graphicsPipeline = [&] {
+        // Create shader modules.
+        const std::vector vertexShaderCode = readFile("shaders/triangle.vert.spv");
+        const vk::raii::ShaderModule vertexShaderModule { device, vk::ShaderModuleCreateInfo {
+            {},
+            vertexShaderCode,
+        } };
+
+        const std::vector fragmentShaderCode = readFile("shaders/triangle.frag.spv");
+        const vk::raii::ShaderModule fragmentShaderModule { device, vk::ShaderModuleCreateInfo {
             {},
-            vk::ShaderStageFlagBits::eCompute,
-            *computeShaderModule,
-            "main",
-        },
-        *pipelineLayout,
-    } };
+            fragmentShaderCode,
+        } };
+    }();
 
     // Create command pool.
     const vk::raii::CommandPool commandPool { device, vk::CommandPoolCreateInfo {

이전에 만든 readFile 함수를 이용해 코드를 불러오고, 셰이더 모듈을 만들었습니다.

Subject: [PATCH] Add pipeline shader stages.
---
Index: src/03_hello-triangle/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/main.cpp b/src/03_hello-triangle/main.cpp
--- a/src/03_hello-triangle/main.cpp
+++ b/src/03_hello-triangle/main.cpp
@@ -254,17 +254,32 @@
             {},
             fragmentShaderCode,
         } };
+
+        const std::array stages {
+            vk::PipelineShaderStageCreateInfo {
+                {},
+                vk::ShaderStageFlagBits::eVertex,
+                *vertexShaderModule,
+                "main",
+            },
+            vk::PipelineShaderStageCreateInfo {
+                {},
+                vk::ShaderStageFlagBits::eFragment,
+                *fragmentShaderModule,
+                "main",
+            },
+        };
     }();
 
     // Create command pool.

마찬가지로 셰이더 스테이지 또한 두 셰이더 모듈을 명시하였습니다.

정점 입력 상태

Subject: [PATCH] Add pipeline vertex input state.
---
Index: src/03_hello-triangle/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/main.cpp b/src/03_hello-triangle/main.cpp
--- a/src/03_hello-triangle/main.cpp
+++ b/src/03_hello-triangle/main.cpp
@@ -280,6 +280,8 @@
                 "main",
             },
         };
+
+        constexpr vk::PipelineVertexInputStateCreateInfo vertexInputState{};
     }();
 
     // Create command pool.

현재로썬 버텍스 버퍼를 사용하지 않으므로 기본 정점 입력 상태를 사용합니다.

입력 조합기 상태

Subject: [PATCH] Add pipeline input assembly state.
---
Index: src/03_hello-triangle/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/main.cpp b/src/03_hello-triangle/main.cpp
--- a/src/03_hello-triangle/main.cpp
+++ b/src/03_hello-triangle/main.cpp
@@ -282,6 +282,11 @@
         };
 
         constexpr vk::PipelineVertexInputStateCreateInfo vertexInputState{};
+
+        constexpr vk::PipelineInputAssemblyStateCreateInfo inputAssemblyState {
+            {},
+            vk::PrimitiveTopology::eTriangleList,
+        };
     }();
 
     // Create command pool.

삼각형 리스트 토폴로지를 사용하여 입력 조합기 상태를 설정했습니다.

뷰포트 상태

Subject: [PATCH] Add pipeline viewport state.
---
Index: src/03_hello-triangle/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/main.cpp b/src/03_hello-triangle/main.cpp
--- a/src/03_hello-triangle/main.cpp
+++ b/src/03_hello-triangle/main.cpp
@@ -287,6 +287,14 @@
             {},
             vk::PrimitiveTopology::eTriangleList,
         };
+
+        constexpr vk::Viewport viewport { 0.f, 0.f, 512.f, 512.f, 0.f, 1.f };
+        constexpr vk::Rect2D scissor { { 0, 0 }, { 512, 512 } };
+        const vk::PipelineViewportStateCreateInfo viewportState {
+            {},
+            viewport,
+            scissor,
+        };
     }();
 
     // Create command pool.

vk::Viewport 구조체 생성자 인수는 다음과 같습니다:

  • x, y: 뷰포트의 좌상단 좌표
  • width, height: 뷰포트의 너비와 높이
  • minDepth, maxDepth: 뷰포트의 깊이 범위. 이 두 값은 대개 0과 1로 설정합니다.

VK_KHR_maintenance1 extension을 이용하면 뷰포트의 높이를 음수로 설정할 수 있습니다. 이는 특히 OpenGL과 호환성을 유지하기 위해 사용됩니다. 자세한 내용은 Sascha Willems의 글을 참고하세요.

우리는 512x512 이미지에 삼각형을 렌더링할 것이므로, xy는 0, widthheight는 512로 설정했습니다.

또한 (비록 지금은 프라그멘트가 NDC를 벗어나지 않지만) 프라그멘트가 이미지를 벗어난 영역에서는 처리되지 않길 원하므로, 가위 또한 이미지의 크기와 같게 설정했습니다.

래스터라이제이션 상태

Subject: [PATCH] Add pipeline rasterization state.
---
Index: src/03_hello-triangle/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/main.cpp b/src/03_hello-triangle/main.cpp
--- a/src/03_hello-triangle/main.cpp
+++ b/src/03_hello-triangle/main.cpp
@@ -295,6 +295,15 @@
             viewport,
             scissor,
         };
+
+        constexpr vk::PipelineRasterizationStateCreateInfo rasterizationState {
+            {},
+            {}, {},
+            vk::PolygonMode::eFill,
+            vk::CullModeFlagBits::eNone, {},
+            {}, {}, {}, {},
+            1.f,
+        };
     }();
 
     // Create command pool.

코드의 두 번째 줄은 각각 depthClampEnable (정점 NDC의 z 좌표가 $[0, 1]$을 벗어난 경우 이를 벗어난 쪽 구간 끝 값으로 설정), rasterizerDiscardEnable (래스터라이저 비활성화; 참일 경우 래스터라이제이션이 발생하지 않음)로 둘다 vk::False로 설정합니다.

세 번째 polygonMode는 삼각형을 채우고 싶으므로 vk::PolygonMode::eFill로 전달하며, 만일 변만 그리거나 정점만 그리고 싶을 경우 각각 eLine 또는 ePoint를 설정할 수 있습니다.

네 번째 cullMode는 이 프리미티브의 winding order에 따라 래스터라이제이션 여부를 정할 수 있습니다. 우리는 winding order에 관계없이 래스터라이제이션 할 것이므로 vk::CullModeFlagBits::eNone으로 설정하고, 그 뒤의 winding order의 기준으로 할 frontFace 또한 기본값으로 설정합니다.

그 다음 줄은 깊이 테스트 관련 인수로, 지금은 깊이 테스트를 사용하지 않으므로 기본값으로 전달합니다.

마지막 lineWidth는 변을 렌더링할 때 사용할 선의 두께로, 대개 1.0을 사용합니다 (만일 디바이스가 wideLines 기능을 지원한다면 그보다 두꺼운 선을 그릴 수 있습니다).

멀티샘플링 상태

Subject: [PATCH] Add pipeline multisample state.
---
Index: src/03_hello-triangle/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/main.cpp b/src/03_hello-triangle/main.cpp
--- a/src/03_hello-triangle/main.cpp
+++ b/src/03_hello-triangle/main.cpp
@@ -304,6 +304,11 @@
             {}, {}, {}, {},
             1.f,
         };
+
+        constexpr vk::PipelineMultisampleStateCreateInfo multisampleState {
+            {},
+            vk::SampleCountFlagBits::e1,
+        };
     }();
 
     // Create command pool.

본 튜토리얼에서는 멀티샘플링을 사용하지 않으므로 기본 샘플 개수(vk::SampleCountFlagBits::e1)로 설정합니다.

컬러 블렌딩 상태

Subject: [PATCH] Add pipeline color blend state.
---
Index: src/03_hello-triangle/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/main.cpp b/src/03_hello-triangle/main.cpp
--- a/src/03_hello-triangle/main.cpp
+++ b/src/03_hello-triangle/main.cpp
@@ -309,6 +309,18 @@
             {},
             vk::SampleCountFlagBits::e1,
         };
+
+        constexpr vk::PipelineColorBlendAttachmentState colorBlendAttachmentState {
+            {},
+            {}, {}, {},
+            {}, {}, {},
+            vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA,
+        };
+        const vk::PipelineColorBlendStateCreateInfo colorBlendState {
+            {},
+            {}, {},
+            colorBlendAttachmentState,
+        };
     }();
 
     // Create command pool.

한 개의 어태치먼트만 사용하므로 해당 어태치먼트에 대한 컬러 블렌딩 상태만 정의합니다. 지금으로써는 우리가 프라그멘트 셰이더에서 출력한 값이 그대로 이미지 픽셀에 저장되길 원하므로, 두 번째 인자 blendEnablevk::False로 설정하여 블렌딩을 비활성화하였습니다. 마지막 인자 colorWriteMask는 프라그멘트 셰이더의 어떤 채널을 어태치먼트에 쓸지 결정하는데, 우리는 네 채널(R, G, B, A) 모두 쓰기를 원하므로 모든 채널을 활성화하였습니다.

그래픽스 파이프라인 생성 마무리

모든 고정 함수 및 셰이더 스테이지를 결정했으므로, 이들 및 앞서 만든 파이프라인 레이아웃을 vk::GraphicsPipelineCreateInfo 생성자의 인수로 넘겨 파이프라인 생성을 마무리합니다.

Subject: [PATCH] Finish graphics pipeline creation.
---
Index: src/03_hello-triangle/main.cpp
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/03_hello-triangle/main.cpp b/src/03_hello-triangle/main.cpp
--- a/src/03_hello-triangle/main.cpp
+++ b/src/03_hello-triangle/main.cpp
@@ -321,6 +321,21 @@
             {}, {},
             colorBlendAttachmentState,
         };
+
+        return vk::raii::Pipeline { device, nullptr, vk::GraphicsPipelineCreateInfo {
+            {},
+            stages,
+            &vertexInputState,
+            &inputAssemblyState,
+            {},
+            &viewportState,
+            &rasterizationState,
+            &multisampleState,
+            {},
+            &colorBlendState,
+            {},
+            *pipelineLayout,
+        } };
     }();
 
     // Create command pool.

이렇게 기나긴 그래픽스 파이프라인 생성을 마무리…했을까요?

앞서 프라그멘트 셰이더에서 설명했듯 그래픽스 파이프라인이 컬러 어태치먼트에 값을 기록하기 위해서는 부동소수점 형식으로 나온 값 (또는 벡터)를 실제 어태치먼트의 형식에 맞게 변환하는 과정이 필요합니다. 그런데 우리는 아직 우리가 사용할 컬러 어태치먼트의 형식 정보를 파이프라인 생성 정보에 입력하지 않았습니다.

이는 vk::GraphicsPipelineCreateInfo 생성자의 renderPass 인수에 해당하는 정보입니다 (하지만 우리는 그 대신 다른 방법을 택할 것입니다). 이에 대해서는 설명할게 조금 기니, 다음 튜토리얼에서 다루어보도록 하겠습니다.

이번 튜토리얼의 전체 코드 변경 사항은 다음과 같습니다.

  1. 이는 DirectX 및 Metal과 마찬가지이며, OpenGL과 다른 관습을 따릅니다 (OpenGL은 깊이를 $[-1, 1]$ 사이의 값으로 계산합니다). 

  2. 이는 OpenGL, DirectX 및 Metal과 반대의 관습을 따릅니다 (둘은 좌단을 (-1, -1), 우상단을 (1, 1)로 하는 NDC를 사용합니다). Vulkan은 오른손 좌표계(right-handedness coordinate system)을, 나머지 셋은 왼손 좌표계(left-handedness coordinate system)을 사용합니다. 

  3. GLSL에 flat 지정자를 적용하여 보간하지 않은 값을 전송할 수도 있습니다. 

  4. 동적 상태(dynamic state)로 설정되지 않은 한. 

This post is licensed under CC BY 4.0 by the author.