本章では、グラフィックスパイプラインを使用する際に、アプリケーションがどのようにデータを頂点シェーダにマッピングするかを理解するために、仕様書の固定機能頂点処理の章の概要を説明します。
また、Vulkan はさまざまな使い方ができるツールであることも忘れてはいけません。以下は、教育目的のための、頂点データをどのようにレイアウトできるかの例です。
binding
は、頂点シェーダが vkCmdDraw*
呼び出し中にデータの読み取りを開始する頂点バッファ内の位置に関連付けられています。binding
を変更しても、アプリの頂点シェーダのソースコードを変更する必要は ありません 。
以下のコード例は、bindings
の挙動の図と一致しています。
// この例では、両方のバインディングに同じバッファを使用する
VkBuffer buffers[] = { vertex_buffer, vertex_buffer };
VkDeviceSize offsets[] = { 8, 0 };
vkCmdBindVertexBuffers(
my_command_buffer, // commandBuffer
0, // firstBinding
2, // bindingCount
buffers, // pBuffers
offsets, // pOffsets
);
以下の例では、入力データに応じて、binding
と location
の値を設定するさまざまな方法を示しています。
最初の例では、頂点ごとの属性データは次のようになります。
struct Vertex {
float x, y, z;
uint8_t u, v;
};
パイプラインの作成情報は、おおよそ次のようになります。
const VkVertexInputBindingDescription binding = {
0, // binding
sizeof(Vertex), // stride
VK_VERTEX_INPUT_RATE_VERTEX // inputRate
};
const VkVertexInputAttributeDescription attributes[] = {
{
0, // location
binding.binding, // binding
VK_FORMAT_R32G32B32_SFLOAT, // format
0 // offset
},
{
1, // location
binding.binding, // binding
VK_FORMAT_R8G8_UNORM, // format
3 * sizeof(float) // offset
}
};
const VkPipelineVertexInputStateCreateInfo info = {
1, // vertexBindingDescriptionCount
&binding, // pVertexBindingDescriptions
2, // vertexAttributeDescriptionCount
&attributes[0] // pVertexAttributeDescriptions
};
これを利用した GLSL コードは以下のようになります。
layout(location = 0) in vec3 inPos;
layout(location = 1) in uvec2 inUV;
この例では、頂点データが密に詰まっておらず、余分なパディングがある場合を検討します。
struct Vertex {
float x, y, z, pad;
uint8_t u, v;
};
唯一の変更点は、パイプライン作成時のオフセットを調整することです。
1, // location
binding.binding, // binding
VK_FORMAT_R8G8_UNORM, // format
- 3 * sizeof(float) // offset
+ 4 * sizeof(float) // offset
これで、u
と v
が読み込まれる場所に正しいオフセットが設定されます。
データがインターリーブされていない場合は以下のようになります。
float position_data[] = { /*....*/ };
uint8_t uv_data[] = { /*....*/ };
この場合、バインディングは2つになりますが、ロケーションは2つのままです。
const VkVertexInputBindingDescription bindings[] = {
{
0, // binding
3 * sizeof(float), // stride
VK_VERTEX_INPUT_RATE_VERTEX // inputRate
},
{
1, // binding
2 * sizeof(uint8_t), // stride
VK_VERTEX_INPUT_RATE_VERTEX // inputRate
}
};
const VkVertexInputAttributeDescription attributes[] = {
{
0, // location
bindings[0].binding, // binding
VK_FORMAT_R32G32B32_SFLOAT, // format
0 // offset
},
{
1, // location
bindings[1].binding, // binding
VK_FORMAT_R8G8_UNORM, // format
0 // offset
}
};
const VkPipelineVertexInputStateCreateInfo info = {
2, // vertexBindingDescriptionCount
&bindings[0], // pVertexBindingDescriptions
2, // vertexAttributeDescriptionCount
&attributes[0] // pVertexAttributeDescriptions
};
GLSL コードは例 A と変わりません。
layout(location = 0) in vec3 inPos;
layout(location = 1) in uvec2 inUV;
この例は、binding
と location
が互いに独立していることを説明するものです。
この例では、頂点データは次のようなフォーマットで提供される2つのバッファにレイアウトされています。
struct typeA {
float x, y, z; // position
uint8_t u, v; // UV
};
struct typeB {
float x, y, z; // normal
};
typeA a[] = { /*....*/ };
typeB b[] = { /*....*/ };
シェーダのインターフェイスは次のようになります。
layout(location = 0) in vec3 inPos;
layout(location = 1) in vec3 inNormal;
layout(location = 2) in uvec2 inUV;
VkVertexInputBindingDescription
と VkVertexInputAttributeDescription
を適宜設定することで、以下のように適切にマッピングされます。
const VkVertexInputBindingDescription bindings[] = {
{
0, // binding
sizeof(typeA), // stride
VK_VERTEX_INPUT_RATE_VERTEX // inputRate
},
{
1, // binding
sizeof(typeB), // stride
VK_VERTEX_INPUT_RATE_VERTEX // inputRate
}
};
const VkVertexInputAttributeDescription attributes[] = {
{
0, // location
bindings[0].binding, // binding
VK_FORMAT_R32G32B32_SFLOAT, // format
0 // offset
},
{
1, // location
bindings[1].binding, // binding
VK_FORMAT_R32G32B32_SFLOAT, // format
0 // offset
},
{
2, // location
bindings[0].binding, // binding
VK_FORMAT_R8G8_UNORM, // format
3 * sizeof(float) // offset
}
};
VkVertxInputAttributeDescription::format
が混乱の原因になることがあります。format
は、シェーダが読み込むべきデータのサイズとタイプを記述するだけです。
VkFormat
の値を使う理由は、明確に定義されていて、頂点シェーダの入力レイアウトと一致するためです。
この例では、頂点データは4つの浮動小数点数だけです。
struct Vertex {
float a, b, c, d;
};
format
と offset
の設定により、読み込まれるデータが重なります。
const VkVertexInputBindingDescription binding = {
0, // binding
sizeof(Vertex), // stride
VK_VERTEX_INPUT_RATE_VERTEX // inputRate
};
const VkVertexInputAttributeDescription attributes[] = {
{
0, // location
binding.binding, // binding
VK_FORMAT_R32G32_SFLOAT, // format - Reads in two 32-bit signed floats ('a' and 'b')
0 // offset
},
{
1, // location
binding.binding, // binding
VK_FORMAT_R32G32B32_SFLOAT, // format - Reads in three 32-bit signed floats ('b', 'c', and 'd')
1 * sizeof(float) // offset
}
};
シェーダでデータを読み込むと、重なっている部分の値は同じになります。
layout(location = 0) in vec2 in0;
layout(location = 1) in vec2 in1;
// in0.y == in1.x
重要なのは、in1
が vec2
であるのに対して、入力属性は VK_FORMAT_R32G32B32_SFLOAT
であり、完全には一致していないことです。仕様書によると
頂点シェーダのコンポーネントが少ない場合、余分なコンポーネントは破棄されます。
つまりこの場合、ロケーション1の最後の成分(d
)は破棄され、シェーダに読み込まれることはありません。
仕様書では、Component
割り当てについてさらに詳しく説明されています。以下にその概要をご紹介します。
VkVertxInputAttributeDescription
の各 location
は4つのコンポーネントを持っています。上の例では、シェーダ入力のコンポーネント数が少ない場合、format
からの余分なコンポーネントが破棄されることを示しました。
VK_FORMAT_R32G32B32_SFLOAT
は3つのコンポーネントを持ちますが、vec2
は2つしかありません。
その逆のケースでは、仕様書には、欠落しているコンポーネントをどのように拡張するかを示す表がある。
例としてはこのようになります。
layout(location = 0) in vec3 inPos;
layout(location = 1) in uvec2 inUV;
この場合、次のように埋められます。
layout(location = 0) in vec4 inPos;
layout(location = 1) in uvec4 inUV;