Vulkanの話 第4回

ポリゴンを表示する その2 16/08/01 up

前回からの続きです。

前回は頂点バッファとインデックスバッファの作成まで行いました。

次にUniformバッファを作成します。

UniformバッファはDirectXでいうところの定数バッファです。

OpenGLでも同様の名称でしたのでわかるかと思います。

{ // UniformBufferを生成 vk::BufferCreateInfo bufferInfo; bufferInfo.size = sizeof(SceneData); bufferInfo.usage = vk::BufferUsageFlagBits::eUniformBuffer | vk::BufferUsageFlagBits::eTransferDst; g_VkUniformBuffer.buffer = g_VkDevice.createBuffer(bufferInfo); // UniformBuffer用のメモリを確保し、バインド vk::MemoryRequirements memReqs = g_VkDevice.getBufferMemoryRequirements(g_VkUniformBuffer.buffer); vk::MemoryAllocateInfo allocInfo; allocInfo.allocationSize = memReqs.size; allocInfo.memoryTypeIndex = GetMemoryTypeIndex(memReqs.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible); g_VkUniformBuffer.devMem = g_VkDevice.allocateMemory(allocInfo); g_VkDevice.bindBufferMemory(g_VkUniformBuffer.buffer, g_VkUniformBuffer.devMem, 0); // UniformBufferの情報を格納する g_VkUniformInfo.buffer = g_VkUniformBuffer.buffer; g_VkUniformInfo.offset = 0; g_VkUniformInfo.range = sizeof(SceneData); // 行列は単位行列を突っ込んでおく SceneData data; StoreIdentityMatrix(data.mtxWorld); StoreIdentityMatrix(data.mtxView); StoreIdentityMatrix(data.mtxProj); // バッファに情報をコピー void *pData = g_VkDevice.mapMemory(g_VkUniformBuffer.devMem, 0, sizeof(SceneData), vk::MemoryMapFlags()); memcpy(pData, &data, sizeof(data)); g_VkDevice.unmapMemory(g_VkUniformBuffer.devMem);}

バッファを生成、メモリを確保、それらをバインドという点は頂点バッファなどと同じです。

違いがあるとするとバッファ生成時のusageフラグに vk::BufferUsageFlagBits::eUniformBuffer とともに vk::BufferUsageFlagBits::eTransferDst を追加している部分。

これにより、このバッファはメモリ転送先として利用できることになります。

また、確保したメモリも頂点バッファは eDeviceLocal を指定してVRAM上に確保していましたが、今回は vk::MemoryPropertyFlagBits::eHostVisible を指定しています。

これはCPUからこのメモリに対してアクセスできることを示します。

Uniformバッファのように小さく、更新頻度も多いものはVRAMに配置してStagingバッファ経由でコピーするよりこの方が簡単でコストも安ことが多いです。

まあ、実際に安いかどうかはちゃんと計測してみないとわかりませんが。

SceneData 構造体はワールド、ビュー、プロジェクションの行列を持っているだけの構造体です。

一旦単位行列を vk::Device::mapMemory(), vk::Device::unmapMemory() 間でコピーしておきますが、描画時にはちゃんと変更します。

次にシェーダの読み込みですが、これは非常に簡単で、SPIR-Vにコンパイルされたシェーダバイナリを読み込み、vk::ShaderModule を作成するだけです。

この ShaderModule は~シェーダという括りを持っていません。何がどのシェーダかはユーザが管理する必要があります。

つまり、頂点シェーダだろうがピクセルシェーダ(フラグメントシェーダ)だろうが同じ vk::ShaderModule で管理されるわけです。

vk::ShaderModule CreateShaderModule(const std::string& filename){ // ファイルリード // 略 // Shaderモジュール作成 vk::ShaderModuleCreateInfo moduleCreateInfo; moduleCreateInfo.codeSize = size; moduleCreateInfo.pCode = reinterpret_cast<uint32_t*>(bin.data()); return g_VkDevice.createShaderModule(moduleCreateInfo);}

ShaderModule を作成してしまえばバイナリは不要です。

省略していますが、読み込んだファイルはローカルの std::vector に格納してあるので、関数外に出た段階でバイナリは解放されます。それでも問題ありません。

この関数を使って頂点シェーダもピクセルシェーダも作成します。

g_VkVShader = CreateShaderModule("data/test.vert.spv"); g_VkPShader = CreateShaderModule("data/test.frag.spv");

シェーダコードはGLSLで記述できます。

これをVulkanSDK内にある、glslangValidator.exe を利用してコンパイルします。

使い方についてはツールのヘルプをお読みください。

今回のシェーダは非常に単純で、以下のようになっています。

#version 450#extension GL_ARB_separate_shader_objects : enable#extension GL_ARB_shading_language_420pack : enable layout (location = 0) in vec3 inPos; layout (location = 1) in vec4 inColor; layout (binding = 0) uniform SceneData { mat4 mtxWorld; mat4 mtxView; mat4 mtxProj;} uScene; layout (location = 0) out vec4 outColor; out gl_PerVertex { vec4 gl_Position;};void main(){ outColor = inColor; gl_Position = uScene.mtxProj * uScene.mtxView * uScene.mtxWorld * vec4(inPos.xyz, 1.0);}

#version 450#extension GL_ARB_separate_shader_objects : enable#extension GL_ARB_shading_language_420pack : enable layout (location = 0) in vec4 inColor; layout (location = 0) out vec4 outFragColor;void main() { outFragColor = inColor;}

上が頂点シェーダ、下がピクセルシェーダですね。

なお、頂点シェーダは拡張子を .vert に、ピクセルシェーダは拡張子を .frag にしておくと自動的にその形式でコンパイルしてくれます。

どの形式でコンパイルするかはもちろん指定も可能ですが、ファイルの命名規則を前述のようにすることが出来るならコンパイルがちょっと簡単になります。

次はデスクリプタセットの作成です。

これはDX12にも存在したので詳しい説明は省きますが、シェーダで利用するリソースがどれが何個あり、どのステージで使用されるかを記述したオブジェクトです。

// デスクリプタプールを作成する{ // 今回はUniformBuffer1つのデスクリプタが必要なのでPoolSizeは1つでOK // UniformBufferが複数になってもPoolSizeは1つでいいが、タイプの違うデスクリプタが必要な場合はPoolSizeの数が増えるので注意 std::array<vk::DescriptorPoolSize, 1> typeCounts; typeCounts[0].type = vk::DescriptorType::eUniformBuffer; typeCounts[0].descriptorCount = 1; // デスクリプタプールを生成 vk::DescriptorPoolCreateInfo descriptorPoolInfo; descriptorPoolInfo.poolSizeCount = static_cast<uint32_t>(typeCounts.size()); descriptorPoolInfo.pPoolSizes = typeCounts.data(); descriptorPoolInfo.maxSets = 1; g_VkDescPool = g_VkDevice.createDescriptorPool(descriptorPoolInfo);}// デスクリプタセットレイアウトを作成する{ // 描画時のシェーダセットに対するデスクリプタセットのレイアウトを指定する // 頂点シェーダ用のUniformBuffer std::array<vk::DescriptorSetLayoutBinding, 1> layoutBindings; // UniformBuffer for VertexShader layoutBindings[0].descriptorType = vk::DescriptorType::eUniformBuffer; layoutBindings[0].descriptorCount = 1; layoutBindings[0].binding = 0; layoutBindings[0].stageFlags = vk::ShaderStageFlagBits::eVertex; layoutBindings[0].pImmutableSamplers = nullptr; // レイアウトを生成 vk::DescriptorSetLayoutCreateInfo descriptorLayout; descriptorLayout.bindingCount = static_cast<uint32_t>(layoutBindings.size()); descriptorLayout.pBindings = layoutBindings.data(); g_VkDescSetLayout = g_VkDevice.createDescriptorSetLayout(descriptorLayout, nullptr);}// デスクリプタセットを作成する{ // デスクリプタセットは作成済みのデスクリプタプールから確保する vk::DescriptorSetAllocateInfo allocInfo; allocInfo.descriptorPool = g_VkDescPool; allocInfo.descriptorSetCount = 1; allocInfo.pSetLayouts = &g_VkDescSetLayout; g_VkDescSets = g_VkDevice.allocateDescriptorSets(allocInfo); // デスクリプタセットの情報を更新する std::array<vk::WriteDescriptorSet, 1> descSetInfos; descSetInfos[0].dstSet = g_VkDescSets[0]; descSetInfos[0].descriptorCount = 1; descSetInfos[0].descriptorType = vk::DescriptorType::eUniformBuffer; descSetInfos[0].pBufferInfo = &g_VkUniformInfo; descSetInfos[0].dstBinding = 0; g_VkDevice.updateDescriptorSets(descSetInfos, nullptr);}

最初に作成するのはデスクリプタプールです。

デスクリプタプールはデスクリプタタイプ (vk::DescriptorType) ごとにいくつのデスクリプタが必要かを予め確保しておくオブジェクトです。

今回はUniformバッファが1つだけ必要なので、vk::DescriptorPoolSize も1つで済みます。

その他の種類のバッファやテクスチャなどが必要な場合はその種類に応じて増やす必要があります。

次に作成するのがデスクリプタセットレイアウトです。

これは作成するデスクリプタセットがどのようなレイアウトで構成されているかを指定します。

このサンプルでは Uniformバッファが1つ、頂点シェーダで使用するために必要で、0番にバインドする、という設定のみを行います。

使用するシェーダステージと型、数、バインドする番号はこの段階で決めておく必要があります。

DX12も同様ですが、DX11時代のように都度変更しても問題ない、という実装ではないので注意しましょう。

ここまで出来たらデスクリプタプールからデスクリプタセットレイアウトで指定した形のデスクリプタセットを生成します。

デスクリプタセットを確保する際、レイアウトで指定した種類や数がプール生成時に指定した数を上回るとエラーが出ますので注意してください。

また、プールからの確保はメモリ確保と同様なので、確保したデスクリプタセットの数、デスクリプタの数の総計がプールのそれより上回るとエラーが発生します。

このサンプルではプール作成時にセットの最大数が1、Uniformバッファのデスクリプタ数が1として指定されていますので、セットを2つ作ったり、Uniformバッファを2つレイアウトに設定したりするとエラーです。

とりあえず今回はここまで。

次回こそは描画まで進みます。