Vulkan チュートリアルの次のステップとしてディスクリプタを理解するときに詰まったことを簡単にまとめました。
チュートリアルやサンプルでは小規模なシェーダが使われていることが多く、ディスクリプタと GLSL の layout 構文(特に set, binding)の対応について、なかなか参考にできるものが見つからないと思います。
そこで、いくつかの uniform の定義パターンに対応した、ディスクリプタの定義と更新方法についてメモします。
ディスクリプタとは
ディスクリプタはシェーダが必要となるリソース(UniformBuffer,Texture,Sampler)の情報を格納したオブジェクトです。
これを Pipeline にバインドすることで、シェーダは座標変換行列や、ポリゴンに張り付けるテクスチャを使えるようになります。
今回作るもの
例示した GLSL コードに対応する、
- VkDescriptorSet(の配列)の更新
- ↑ を作るための VkDescriptorSetLayout(の配列)作成
の処理を書いてみます。
VkDescriptorSetLayout
VkDescriptorSetLayout は、ディスクリプタにどのような構造で情報が格納されるのかを定義するためのオブジェクトです。
使いたいシェーダに書かれている uniform 変数(UniformBuffer,Texture,Sampler)すべてについて、正しく対応付けて定義する必要があります。
VkDescriptorSet
冒頭で説明したディスクリプタそのものです。リソースへの参照を格納します。
ちょうど、一般的なオブジェクト指向言語でいうところの、VkDescriptorSetLayout がクラス定義、VkDescriptorSet がインスタンスに相当します。
本題
GLSL
まずは例となる GLSL コードです。
例としてのパターンを増やすため、すこし大げさに変数名を付けたり Texture と Sampler を独立させたりしています。
ここでは変数の使用用途ではなく、layout と型に注目してください。
// vert
layout(set=0, binding=0) uniform SceneMatrices {
mat4 projection;
mat4 view;
};
layout(set=0, binding=1) uniform ModelMatrices {
mat4 model;
};
layout(set=1, binding=0) uniform texture2D LightMap;
layout(set=2, binding=0) uniform sampler LightMapSampler;
layout(set=3, binding=0) uniform sampler2D BoneMap;
// frag
layout(set=0, binding=1) uniform ModelMatrices {
mat4 model;
};
layout(set=0, binding=2) uniform Material {
vec4 colorScale;
};
layout(set=0, binding=3) uniform Camera {
vec3 cameraPos;
};
layout(set=1, binding=0) uniform texture2D LightMap;
layout(set=1, binding=1) uniform texture2D ColorTexture;
layout(set=2, binding=0) uniform sampler LightMapSampler;
layout(set=2, binding=1) uniform sampler ColorTextureSampler;
HLSL の register と比べて自由度が高すぎるので、少しルールを設けています。
- set=0 は UniformBuffer とする。
- set=1 は Texture とする。
- set=2 は SamplerState とする。
- set=3 は CombindSampler とする。
Texture はダミーの SamplerState を用意することで C++ 側は CombindSampler として扱えるため、UniformBuffer, Texture, SamplerState の3つにしてしまえば、HLSL のレジスタ b, t, s に丁度対応させることができます。
register(b1)
==layout(set=0, binding=1)
register(t0)
==layout(set=1, binding=0)
このようにルールを決めておくと、HLSL をコンバートするときにシンプルに考えることができるようになります。
注意点として、今回は頂点シェーダとフラグメントシェーダを同時にバインドするケースを想定しています。(というか、普通はそうやって使いますよね)
ディスクリプタはすべてのシェーダステージで共有されますので、個々のシェーダでは共有する uniform は同じレイアウトとするべきで、そうでない uniform は異なるレイアウトとしなければなりません。
例えば、
- 頂点シェーダで
layout(set=0, binding=0) uniform ModelMatrices {...}
- フラグメントシェーダで
layout(set=0, binding=0) uniform texture2D ColorTexture;
といったように、同じレイアウトに異なる uniform を定義することはできません。(型が違うなら検証レイヤーが報告してくれますが、型も同じにしてしまうと原因のわかりづらい問題の元となります)
VkDescriptorSetLayout を作るときのレイアウト定義
レイアウトを考えるときに必要なものは次の通りです。
- set, binding 番号
- 型 (UniformBuffer? Texture? SamplerState?)
- どのシェーダステージ (頂点シェーダ、フラグメントシェーダなど) から参照されるか
これらを使って、uniform ひとつずつ、構造体配列を作って定義していきます。
// UniformBuffer (set=0)
const VkDescriptorSetLayoutBinding UniformBufferDescriptorSetLayoutBinding[] =
{
// layout(set=0, binding=0) uniform SceneMatrices
{
0,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
1,
VK_SHADER_STAGE_VERTEX_BIT,
},
// layout(set=0, binding=1) uniform ModelMatrices
{
1,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
1,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
},
// layout(set=0, binding=2) uniform Material
{
2,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
1,
VK_SHADER_STAGE_FRAGMENT_BIT,
},
// layout(set=0, binding=3) uniform Camera
{
3,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER,
1,
VK_SHADER_STAGE_FRAGMENT_BIT,
},
};
// Texture (set=1)
const VkDescriptorSetLayoutBinding TextureDescriptorSetLayoutBinding[] =
{
// layout(set=1, binding=0) uniform texture2D LightMap
{
0,
VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
1,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
},
// layout(set=1, binding=1) uniform texture2D ColorTexture
{
1,
VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
1,
VK_SHADER_STAGE_FRAGMENT_BIT,
},
};
// SamplerState (set=2)
const VkDescriptorSetLayoutBinding SamplerStateDescriptorSetLayoutBinding[] =
{
// layout(set=2, binding=0) uniform sampler LightMapSampler
{
0,
VK_DESCRIPTOR_TYPE_SAMPLER,
1,
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
},
// layout(set=2, binding=1) uniform sampler ColorTextureSampler
{
1,
VK_DESCRIPTOR_TYPE_SAMPLER,
1,
VK_SHADER_STAGE_FRAGMENT_BIT,
},
};
// CombindSampler (set=3)
const VkDescriptorSetLayoutBinding CombindSamplerDescriptorSetLayoutBinding[] =
{
// layout(set=3, binding=0) uniform sampler2D BoneMap
{
0,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
1,
VK_SHADER_STAGE_VERTEX_BIT,
},
};
const VkDescriptorSetLayoutCreateInfo DescriptorSetLayoutCreateInfo[] =
{
// UniformBuffer (set=0)
{
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
NULL,
0,
4,
UniformBufferDescriptorSetLayoutBinding
},
// Texture (set=1)
{
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
NULL,
0,
2,
TextureDescriptorSetLayoutBinding
},
// SamplerState (set=2)
{
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
NULL,
0,
2,
SamplerStateDescriptorSetLayoutBinding
},
// CombindSampler (set=3)
{
VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
NULL,
0,
1,
CombindSamplerDescriptorSetLayoutBinding
},
};
VkDescriptorSetLayout descriptorSetLayout[4]; // Target!
for (int i = 0; i < 4; i++) {
vkCreateDescriptorSetLayout(
device,
&DescriptorSetLayoutCreateInfo[0],
NULL,
&descriptorSetLayout[0]);
}
DescriptorSet へ書き込むときの定義
更新に必要な情報は次の通りです。
- set, binding 番号
- 型 (UniformBuffer? Texture? SamplerState?)
- バインドする各種データ
- UniformBuffer : VkBuffer
- Texture : VkImageView
- SamplerState : VkSampler
VkDescriptorSet descriptorSets[4];
// まず descriptorSets を作る
VkDescriptorSetAllocateInfo allocInfo;
allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
allocInfo.pNext = nullptr;
allocInfo.descriptorPool = pool1;
allocInfo.descriptorSetCount = 4;
allocInfo.pSetLayouts = descriptorSetLayout; // さっき作ったもの
vkAllocateDescriptorSets(device, &allocInfo, descriptorSets));
// 以下で descriptorSets の設定を行う
// UniformBuffer (set=0)
const VkDescriptorBufferInfo UniformBufferInfo[] =
{
// layout(set=0, binding=0) uniform SceneMatrices
{
buffer1, // SceneMatrices のデータが格納された VkBuffer
0,
sizeof(mat4) * 2, // mat4 が 2つ分のサイズ
},
// layout(set=0, binding=1) uniform ModelMatrices
{
buffer2,
0,
sizeof(mat4),
},
// layout(set=0, binding=2) uniform Material
{
buffer3,
0,
sizeof(vec4),
},
// layout(set=0, binding=3) uniform Camera
{
buffer4,
0,
sizeof(vec3),
},
};
// Texture (set=1)
const VkDescriptorImageInfo TextureInfo[] =
{
// layout(set=1, binding=0) uniform texture2D LightMap
{
VK_NULL_HANDLE,
imageView1, // LightMap のテクスチャを参照する VkImageView
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
},
// layout(set=1, binding=1) uniform texture2D ColorTexture
{
VK_NULL_HANDLE,
imageView2, // ColorTexture のテクスチャを参照する VkImageView
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
},
};
// SamplerState (set=2)
const VkDescriptorImageInfo SamplerStateInfo[] =
{
// layout(set=2, binding=0) uniform sampler LightMapSampler
{
sampler1, // LightMapSampler の SamplerState を示す VkSampler
VK_NULL_HANDLE,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
},
// layout(set=2, binding=1) uniform sampler ColorTextureSampler
{
sampler2, // ColorTextureSampler の SamplerState を示す VkSampler
VK_NULL_HANDLE,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
},
};
// CombindSampler (set=3)
const VkDescriptorImageInfo CombindSamplerInfo[] =
{
// layout(set=3, binding=0) uniform sampler2D BoneMap
{
sampler3, // BoneMap の SamplerState を示す VkSampler
imageView3, // BoneMap のテクスチャを参照する VkImageView
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
},
};
const VkWriteDescriptorSet WriteInfo[] =
{
// layout(set=0, binding=0) uniform SceneMatrices
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr,
descriptorSets[0], // dstSet,
0, // dstBinding
0, 1,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, // descriptorType
nullptr, // pImageInfo
&UniformBufferInfo[0] // pBufferInfo
},
// layout(set=0, binding=1) uniform ModelMatrices
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr,
descriptorSets[0], // dstSet,
1, // dstBinding
0, 1,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, // descriptorType
nullptr, // pImageInfo
&UniformBufferInfo[1] // pBufferInfo
},
// layout(set=0, binding=2) uniform Material
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr,
descriptorSets[0], // dstSet,
2, // dstBinding
0, 1,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, // descriptorType
nullptr, // pImageInfo
&UniformBufferInfo[2] // pBufferInfo
},
// layout(set=0, binding=3) uniform Camera
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr,
descriptorSets[0], // dstSet,
3, // dstBinding
0, 1,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, // descriptorType
nullptr, // pImageInfo
&UniformBufferInfo[3] // pBufferInfo
},
// layout(set=1, binding=0) uniform texture2D LightMap
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr,
descriptorSets[1], // dstSet,
0, // dstBinding
0, 1,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, // descriptorType
&TextureInfo[0], // pImageInfo
nullptr
},
// layout(set=1, binding=1) uniform texture2D ColorTexture
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr,
descriptorSets[1], // dstSet,
1, // dstBinding
0, 1,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, // descriptorType
&TextureInfo[1], // pImageInfo
nullptr
},
// layout(set=2, binding=0) uniform sampler LightMapSampler
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr,
descriptorSets[2], // dstSet,
0, // dstBinding
0, 1,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, // descriptorType
&SamplerStateInfo[0], // pImageInfo
nullptr
},
// layout(set=2, binding=1) uniform sampler ColorTextureSampler
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr,
descriptorSets[2], // dstSet,
1, // dstBinding
0, 1,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, // descriptorType
&SamplerStateInfo[1], // pImageInfo
nullptr
},
// layout(set=3, binding=0) uniform sampler2D BoneMap
{
VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, nullptr,
descriptorSets[3], // dstSet,
0, // dstBinding
0, 1,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, // descriptorType
&CombindSamplerInfo[0], // pImageInfo
nullptr
},
};
vkUpdateDescriptorSets(device, 9, WriteInfo, 0, nullptr);
まとめ
人が書くもんじゃねぇ
たくさんの uniform を持つシェーダを使うための、ディスクリプタの定義を書いてみました。
後半のモチベ低下感がありますが、たったこれだけの GLSL コードに対して非常に多くのコードを書く必要があります。さらに、GLSL 側と C++ 側で layout 定義を完全に一致させなければならないため、後から uniform の追加削除が発生した場合はかなりのメンテナンスコストが発生します。また、シェーダファイルの部分的な使いまわし(フラグメントシェーダ共通、頂点シェーダ個別)も困難です。
とはいえ Vulkan 自体、プログラマフレンドリーなコンセプトで設計されたものではないため、そのあたりを求めても仕方がないという感じではあります。チュートリアルを離れて実際の運用となってきたときには set,binding 番号の自動マップは必須になってきます。
これについては glslang と spirv-cross を使うことで、自由に描かれた GLSL or HLSL から自動マップ済みの SPIR-V を生成する方法があります。(というか現実的にこれしかないと思います)
そのうち別に記事を書こうと思いますが、実際に自動マップしてみたコードは私が個人的に開発しているゲームエンジンの一部に入っていますので、興味がありましたらご覧ください。
https://github.com/lriki/Lumino/blob/master/src/LuminoEngine/src/Shader/ShaderTranspiler.cpp