Vulkan Tutorial (Drawing a triangle/Graphics pipeline basics/Shader modules) 日本語訳

この記事は [Shader modules - Vulkan Tutorial](https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipeline_basics/Shader_modules) の日本語訳です。

# Shader modules

それまでのAPIと違い、Vulkanでのシェーダコードは、[`GLSL`](https://en.wikipedia.org/wiki/OpenGL_Shading_Language)や[`HLSL`](https://en.wikipedia.org/wiki/High-Level_Shading_Language)のような人間が読める文法とは対象的な、バイトコードフォーマットで指定する必要があります。このバイトコードフォーマットは[`SPIR-V`](https://www.khronos.org/spir)と呼ばれ、VulkanとOpenCL(どちらもKhronosのAPIです)両方から使われるように設計されています。グラフィックスシェーダとコンピュートシェーダを書くのに使うことができるフォーマットですが、このチュートリアルではVulkanのグラフィックスパイプラインで使われるシェーダにフォーカスします。

バイトコードフォーマットを使うことの利点は、シェーダコードをネイティブコードに変換するためにGPUベンダが書くコンパイラが、大幅に単純になることです。GLSLのような人間が読める構文では、いくつかのGPUベンダが規格をより柔軟に解釈するということが過去にありました。これらのベンダのうちの１つのGPU上で単純でないシェーダを書いたとき、他のベンダのドライバでは文法エラーで拒否されたり、もっと悪い場合はコンパイラのバグによって異なる動作をするという危険性がありました。SPIR-Vのような単純なバイトコードフォーマットでは、そのようなことは回避されているはずです。

しかし、このバイトコードを手動で書く必要があるというわけではありません。Khronosは、GLSLからSPIR-Vにコンパイルする、ベンダ独立な独自のコンパイラをリリースしています。このコンパイラは、あなたのシェーダコードが完全に規格に準拠していることを検証し、プログラムに同梱できるSPIR-Vバイナリを生成するように設計されています。また、このコンパイラをライブラリとしてインクルードし、実行時にSPIR-Vを生成することもできますが、このチュートリアルでは行いません。このコンパイラを`glslangValidator.exe`として直接使うこともできますが、私たちはGoogleの`glslc.exe`を代わりに使います。`glslc`の利点は、GCCやClangのような有名なコンパイラと同じパラメータフォーマットを使えることと、*includes*のような追加の機能を含んでいることです。これらは両方ともVulkan SDKに既に含まれているので、追加で何かダウンロードする必要はありません。

GLSLはCスタイルの文法のシェーダ言語です。GLSLで書かれたプログラムには、全てのオブジェクトから呼び出される`main`関数があります。入力に引数、出力に戻り値を使う代わりに、GLSLでは入力と出力を処理するためにグローバル変数を使います。この言語には、組み込みのベクトルと行列のような、グラフィックスプログラミングを手助けするたくさんの機能が含まれています。外積、行列とベクトルの乗算、ベクトルの反射などの操作のための関数が含まれています。ベクトル型は`vec`と要素数を表す数字と共に呼ばれます。例えば、３次元の位置は`vec3`に格納されます。`.x`のようにメンバを通して単一の要素にアクセスすることもできますし、複数の要素から新しいベクトルを作成することもできます。例えば、`vec3(1.0, 2.0, 3.0).xy`という式の結果は`vec2`になります。ベクトルのコンストラクタは、ベクトルとスカラー値の組み合わせを取ることもできます。例えば、`vec3`は`vec3(vec2(1.0, 2.0), 3.0)`のようにして生成することができます。

前章で述べたように、画面に三角形を描画するためには頂点シェーダとフラグメントシェーダを書く必要があります。次の２節ではそれらのGLSLコードを説明し、その後、２つのSPIR-Vバイナリを生成し、それらをプログラムにロードする方法について見ていきます。

## Vertex shader

頂点シェーダは入ってくる頂点それぞれを処理します。ワールド位置、色、法線、テクスチャ座標などのようなアトリビュートを入力として受け取ります。出力はクリップ座標系における位置と、色やテクスチャ座標のようなフラグメントシェーダに渡す必要があるアトリビュートです。これらの値は、スムーズなグラデーションを生成するために、ラスタライザーによってフラグメント間で補間されます。

クリップ座標(*clip coordinate*)とは頂点シェーダからの４次元ベクトルで、その後、ベクトル全体を最後の要素で割ることによって正規化デバイス座標(*normalized device coordinate*)に変換されます。正規化デバイス座標は、フレームバッファを[-1, 1] × [-1, 1]座標系にマッピングする[同次座標](https://en.wikipedia.org/wiki/Homogeneous_coordinates)で、次のようなものです。
![](https://vulkan-tutorial.com/images/normalized_device_coordinates.svg)

コンピュータグラフィックスに手を出したことがあるなら、これらには既に精通していることでしょう。もしOpenGLを使ったことがあるなら、Y座標の符号が反転していることに気がついたかもしれません。Z座標はDirect3Dと同じ、0から1までの範囲を使います。

私たちの最初の三角形は、いかなる変換も適用しないつもりなので、３つの頂点の位置を正規化デバイス座標で直接指定して、以下の形を作成します。
![](https://vulkan-tutorial.com/images/triangle_coordinates.svg)

頂点シェーダで最後の要素を`1`に設定してクリップ座標として出力することにより、正規化デバイス座標系で直接出力することができます。この方法だと、クリップ座標系から正規化デバイス座標系へと変換するための除算で何も変化しません。

通常、これらの座標は頂点バッファに格納されますが、Vulkanで頂点バッファを作成し、データを設定する方法は自明ではありません。そのため、三角形の画面への表示を達成するまで、それを後回しにすることにしました。それまでの間、少しオーソドックスでないことをします。頂点シェーダの中に直接、座標を含んでしまいます。コードは次のようになります。

```glsl
#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

頂点シェーダからの位置で形作られた三角形は画面中の領域をフラグメントで満たします。フラグメントシェーダはこれらのフラグメントに対して呼び出され、フレームバッファ用の色とデプス値を生成します。三角形全体に対して赤色を出力する単純なフラグメントシェーダは以下のようになります。

```glsl
#version 450

layout(location = 0) out vec4 outColor;

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

頂点シェーダの`main`関数が頂点ごとに呼び出されるように、フラグメントシェーダの`main`関数はフラグメントごとに呼び出されます。GLSLでの色は４要素のベクトルで、[0, 1]の範囲のR、G、B、アルファのチャンネルを持ちます。頂点シェーダの`gl_Position`とは異なり、現在のフラグメントの色を出力するための組み込み変数はありません。自身の出力変数を、フレームバッファごとに指定しなければいけません。`layout(location = 0)`修飾子でフレームバッファのインデックスを指定します。赤色はこの`outColor`変数に書き込まれ、それはインデックス`0`にある最初の(そして唯一の)フレームバッファとリンクされます。

## Per-vertex colors

三角形全体を赤色にするのはそれほど面白くないので、次のような見た目のほうがもっと素敵になるのではないでしょうか？
![](https://vulkan-tutorial.com/images/triangle_coordinates_colors.png)

これを実現するためには、両方のシェーダにいくつかの変更をする必要があります。最初に、３つの頂点それぞれに個別の色を指定する必要があります。頂点シェーダは、位置のときと同じように、色の配列を含むようになります。

```glsl
vec3 colors[3] = vec3[](
    vec3(1.0, 0.0, 0.0),
    vec3(0.0, 1.0, 0.0),
    vec3(0.0, 0.0, 1.0)
);
```

あとは、これらの頂点ごとの色をフラグメントシェーダにそのまま渡すだけでよく、フレームバッファにはそれらの補間された値が出力されます。頂点シェーダに色の出力を追加し、`main`関数でそれに書き込みます。

```glsl
layout(location = 0) out vec3 fragColor;

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

次に、マッチする入力をフラグメントシェーダに追加する必要があります。

```glsl
layout(location = 0) in vec3 fragColor;

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

入力変数は必ずしも同じ名前を使う必要はなく、これらは`location`ディレクティブで指定されるインデックスを使ってリンクされます。`main`関数はアルファ値と共に色を出力するように変更されました。上の画像で見た通り、`fragColor`の値は３つの頂点間のフラグメントに対して自動的に補間され、結果的になめらかなグラデーションとなります。

## Compiling the shaders

あなたのプロジェクトのルートディレクトリに`shaders`という名前のディレクトリを作成し、このディレクトリ内に`shader.vert`というファイル名で頂点シェーダを、`shader.frag`というファイル名でフラグメントシェーダを格納してください。GLSLのシェーダに公式な拡張子はありませんが、これら２つは識別するためによく使われる拡張子です。

`shader.vert`の中身は次のようになります。

```glsl
#version 450

layout(location = 0) out vec3 fragColor;

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

vec3 colors[3] = vec3[](
    vec3(1.0, 0.0, 0.0),
    vec3(0.0, 1.0, 0.0),
    vec3(0.0, 0.0, 1.0)
);

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

そして`shader.frag`の中身は次のようになります。

```glsl
#version 450

layout(location = 0) in vec3 fragColor;

layout(location = 0) out vec4 outColor;

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

では、これらを`glslc`プログラムを使って、SPIR-Vバイトコードにコンパイルしていきます。

### Windows

次のような中身の`compile.bat`ファイルを作成してください。

```bat
C:/VulkanSDK/x.x.x.x/Bin32/glslc.exe shader.vert -o vert.spv
C:/VulkanSDK/x.x.x.x/Bin32/glslc.exe shader.frag -o frag.spv
pause
```

`glslc.exe`へのパスはVulkan SDKをインストールしたパスに置き換えてください。実行するにはファイルをダブルクリックします。

### Linux

次のような中身の`compile.sh`ファイルを作成してください。

```sh
/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.vert -o vert.spv
/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.frag -o frag.spv
```

`glslc`へのパスはVulkan SDKをインストールしたパスに置き換えてください。`chmod +x compile.sh`でスクリプトを実行可能にしてから実行してください。

### End of platform-specific instructions

これら２つのコマンドは、GLSLソースファイルを読み込んで、SPIR-Vバイトコードを出力せよ、ということを`-o`(output)フラグを使ってコンパイラに伝えます。

もしシェーダに文法エラーが含まれていたら、あなたの期待通り、コンパイラは行番号と問題点を教えてくれます。試しにセミコロンを削除してコンパイルスクリプトを再度実行してみてください。また、どんな種類のフラグがサポートされているか見るために、何も引数を付けずにコンパイラを実行してみてください。例えば、バイトコードを人間が読めるフォーマットで出力することもでき、あなたのシェーダが実際に何をやっているのか、どのような最適化がこのステージで適用されているのかを見ることができます。

コマンドラインでシェーダをコンパイルするのは、最も単純な方法のうちの一つで、私たちもこのチュートリアルでそれを使いますが、あなた自身のコードから直接シェーダをコンパイルすることもできます。Vulkan SDKは、あなたのプログラム中からGLSLコードをSPIR-Vにコンパイルするためのライブラリである、[`libshaderc`](https://github.com/google/shaderc)を含んでいます。

## Loading a shader

これでSPIR-Vシェーダを生成する方法を手に入れましたので、それらをプログラムにロードして、ある時点でグラフィックスパイプラインに接続する時が来ました。最初に、ファイルからバイナリデータをロードするための簡単なヘルパー関数を作成します。

```cpp
#include <fstream>

...

static std::vector<char> readFile(const std::string& filename) {
    std::ifstream file(filename, std::ios::ate | std::ios::binary);

    if (!file.is_open()) {
        throw std::runtime_error("failed to open file!");
    }
}
```

`readFile`関数は指定されたファイルから全てのバイトを読み込み、それを`std::vector`で管理されるバイト配列で返します。はじめに、2つのフラグを指定してファイルを開きます。

* `ate`：ファイルの末尾から読み始める
* `binary`：ファイルをバイナリファイルとして読み込む(テキスト変換を避ける)

ファイルの末尾から読み始めることの利点は、読み込み位置を使ってファイルのサイズを決定し、バッファを確保できることです。

```cpp
size_t fileSize = (size_t) file.tellg();
std::vector<char> buffer(fileSize);
```

その後、ファイルの先頭にシークして全てのバイトを一度に読み込むことができます。

```cpp
file.seekg(0);
file.read(buffer.data(), fileSize);
```

そして最後に、ファイルを閉じてバイト配列を返します。

```cpp
file.close();

return buffer;
```

`createGraphicsPipeline`からこの関数を呼び出して、2つのシェーダのバイトコードをロードします。

```cpp
void createGraphicsPipeline() {
    auto vertShaderCode = readFile("shaders/vert.spv");
    auto fragShaderCode = readFile("shaders/frag.spv");
}
```

バッファのサイズを出力して、実際のファイルのバイトサイズと一致しているかチェックし、シェーダが正しくロードされたことを確認してください。バイナリコードなのでNULL終端されている必要がなく、後でそのサイズについて明示することになります。

## Creating shader modules

コードをパイプラインに渡す前に、それを[`VkShaderModule`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkShaderModule.html)オブジェクトにラップする必要があります。これを行うためのヘルパー関数、`createShaderModule`を作成しましょう。

```cpp
VkShaderModule createShaderModule(const std::vector<char>& code) {

}
```

この関数は引数にバイトコードのバッファを取り、そこから[`VkShaderModule`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkShaderModule.html)を作成します。

シェーダモジュールの作成はシンプルで、バイトコードのバッファへのポインタとその長さを指定するだけです。この情報は[`VkShaderModuleCreateInfo`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkShaderModuleCreateInfo.html)構造体の中で指定されます。一つの落とし穴は、バイトコードのサイズはバイト単位で指定されるのに、バイトコードのポインタは`char`ポインタではなく`uint32_t`ポインタであることです。そのため、下に示すように`reinterpret_cast`でポインタをキャストする必要があります。このようなキャストを行う時、データが`uint32_t`のアライメント要求を満たしていることを確認する必要があります。ラッキーなことに、データは`std::vector`に格納されており、既にデフォルトアロケータが最悪のケースのアライメント要求を満たすことを保証してくれています。

```cpp
VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());
```

[`VkShaderModule`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkShaderModule.html)は[`vkCreateShaderModule`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkCreateShaderModule.html)を呼び出すことで作成することができます。

```cpp
VkShaderModule shaderModule;
if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
    throw std::runtime_error("failed to create shader module!");
}
```

引数はこれまでのオブジェクト作成関数と同じで、論理デバイス、作成情報構造体へのポインタ、カスタムアロケータへのポインタ(オプション)、そして出力変数のハンドルです。コードのバッファはシェーダモジュール作成後、直ちに解放することができます。作成したシェーダモジュールを返すのを忘れないでください。

```cpp
return shaderModule;
```

シェーダモジュールは、前節でファイルから読み込んだシェーダバイトコードと、その中で定義されている関数の薄いラッパーに過ぎません。GPUによるSPIR-Vバイトコードからマシンコードへのコンパイルとリンクは、グラフィックスパイプラインが作成されるまで行われません。これは、パイプラインの作成が完了したらすぐにシェーダモジュールを破棄しても良いということを意味します。そのため、これらをクラスメンバではなく、`createGraphicsPipeline`関数内のローカル変数にしています。

```cpp
void createGraphicsPipeline() {
    auto vertShaderCode = readFile("shaders/vert.spv");
    auto fragShaderCode = readFile("shaders/frag.spv");

    VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
    VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);
```

[`vkDestroyShaderModule`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/vkDestroyShaderModule.html)呼び出しを２つ追加して、関数の最後にクリーンアップが行われるようにします。この章の残りのコードは全て、これらの行の前に挿入されます。

```cpp
    ...
    vkDestroyShaderModule(device, fragShaderModule, nullptr);
    vkDestroyShaderModule(device, vertShaderModule, nullptr);
}
```

## Shader stage creation

実際にシェーダを使うにはパイプライン作成手順の一部として、[`VkPipelineShaderStageCreateInfo`](https://www.khronos.org/registry/vulkan/specs/1.0/man/html/VkPipelineShaderStageCreateInfo.html)構造体を通して、シェーダを特定のパイプラインステージに割り当てる必要があります。
再び`createGraphicsPipeline`関数の中で、頂点シェーダ用に構造体を埋めるところから始めます。

```cpp
VkPipelineShaderStageCreateInfo vertShaderStageInfo{};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
```

まず最初のステップは、必須の`sType`メンバに加えて、どのパイプラインステージでシェーダが使われることになるのかをVulkanに伝えることです。前の章で説明した、プログラマブルステージそれぞれに対応したenum値があります。

```cpp
vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";
```

次の2つのメンバーは、コードを含んでいるシェーダモジュールと、エントリーポイントとして知られる、呼び出す関数を指定します。これが意味するのは、複数のフラグメントシェーダを一つのシェーダモジュールに結合し、振る舞いを変化させるために違うエントリーポイントを使うことが可能だということです。しかし今回の場合は、標準の`main`のままにします。

もう一つ、ここでは使わないですが議論する価値のある(オプショナルな)メンバ、`pSpecializationInfo`があります。それはシェーダ定数の値を指定することができるようになります。あなたは一つのシェーダモジュールで、そこで使われている定数にパイプライン作成時、違う値を指定することによってそれの振る舞いを設定できます。これはレンダリング時に変数を使って設定するよりも効率的です。なぜなら、それらの値に依存している`if`文を取り除くような最適化を、コンパイラができるからです。もし、あなたがこのような定数を持っていないならば、我々の構造体初期化が自動的におこなっているように、このメンバに`nullptr`を設定することができます。

フラグメントシェーダ用に構造体を変更するのは簡単です。

```cpp
VkPipelineShaderStageCreateInfo fragShaderStageInfo{};
fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";
```

最後にこれら2つの構造体を含む配列を定義し、後で実際のパイプライン作成時に参照します。

```cpp
VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};
```

パイプラインのプログラマブルステージについての説明は以上です。次の章では、固定機能ステージについて見ていきます。

[C++ code](https://vulkan-tutorial.com/code/09_shader_modules.cpp) / [Vertex shader](https://vulkan-tutorial.com/code/09_shader_base.vert) / [Fragment shader](https://vulkan-tutorial.com/code/09_shader_base.frag)

前の記事
次の記事