Vulkanの話 第3回

ポリゴンを表示する その1 16/07/28 up

Vulkanの話 第3回目はポリゴン描画するまで行っているSample002の内容をやっていきます。

GitHubのリポジトリはこちら。

https://github.com/Monsho/VulkanSample/tree/master/Sample002

さて、その前に大きな修正の話。

初手から使用していたNVIDIAのC++ラッパーvk_cppですが、どうやらKhronos Groupに移管されたようです。

どのような経緯があったのかはわかりませんが、これによってほぼ公式のラッパーになりました。

リポジトリはこちら。

https://github.com/KhronosGroup/Vulkan-Hpp

ということで、これからはこのVulkan-Hppを使用していきます。

と言っても、今のところはサンプルコードの修正はインクルードするヘッダの名前を変更しただけです。

名前空間やクラス名に変更はありません。

また、現在私のリポジトリにアップされているvulkan.hppですが、こちらは私の方で1.0.21用に生成したものです。

Khronosのリポジトリに存在するvulkan.hppのバージョンはコミット時のコメントか、中身のバージョン番号で確認しましょう。

さて、長くなりそうなのでさくさく進めていきます。

今回はポリゴンを描画するので深度バッファを作成します。

描画するだけなら深度バッファはなくてもいいんですが、どうせ必要になるんだから作っておきましょう。

{ // 指定のフォーマットがサポートされているか調べる vk::Format depthFormat = vk::Format::eD32SfloatS8Uint; vk::FormatProperties formatProps = g_VkPhysicalDevice.getFormatProperties(depthFormat); assert(formatProps.optimalTilingFeatures & vk::FormatFeatureFlagBits::eDepthStencilAttachment); vk::ImageAspectFlags aspect = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; // 深度バッファのイメージを作成する vk::ImageCreateInfo imageCreateInfo; imageCreateInfo.imageType = vk::ImageType::e2D; imageCreateInfo.extent = vk::Extent3D(kScreenWidth, kScreenHeight, 1); imageCreateInfo.format = depthFormat; imageCreateInfo.mipLevels = 1; imageCreateInfo.arrayLayers = 1; imageCreateInfo.usage = vk::ImageUsageFlagBits::eDepthStencilAttachment; g_VkDepthBuffer.image = g_VkDevice.createImage(imageCreateInfo); // 深度バッファ用のメモリを確保 vk::MemoryRequirements memReqs = g_VkDevice.getImageMemoryRequirements(g_VkDepthBuffer.image); vk::MemoryAllocateInfo memAlloc; memAlloc.allocationSize = memReqs.size; memAlloc.memoryTypeIndex = GetMemoryTypeIndex(memReqs.memoryTypeBits, vk::MemoryPropertyFlagBits::eDeviceLocal); g_VkDepthBuffer.devMem = g_VkDevice.allocateMemory(memAlloc); g_VkDevice.bindImageMemory(g_VkDepthBuffer.image, g_VkDepthBuffer.devMem, 0); // Viewを作成 vk::ImageViewCreateInfo viewCreateInfo; viewCreateInfo.viewType = vk::ImageViewType::e2D; viewCreateInfo.format = depthFormat; viewCreateInfo.components = { vk::ComponentSwizzle::eR, vk::ComponentSwizzle::eG, vk::ComponentSwizzle::eB, vk::ComponentSwizzle::eA }; viewCreateInfo.subresourceRange.aspectMask = aspect; viewCreateInfo.subresourceRange.levelCount = 1; viewCreateInfo.subresourceRange.layerCount = 1; viewCreateInfo.image = g_VkDepthBuffer.image; g_VkDepthBuffer.view = g_VkDevice.createImageView(viewCreateInfo); // レイアウト変更のコマンドを発行する { vk::CommandBufferBeginInfo cmdBufferBeginInfo; vk::BufferCopy copyRegion; // コマンド積み込み開始 g_VkCmdBuffers[0].begin(cmdBufferBeginInfo); vk::ImageSubresourceRange subresourceRange; subresourceRange.aspectMask = aspect; subresourceRange.levelCount = 1; subresourceRange.layerCount = 1; SetImageLayout( g_VkCmdBuffers[0], g_VkDepthBuffer.image, vk::ImageLayout::eUndefined, vk::ImageLayout::eDepthStencilAttachmentOptimal, subresourceRange); // コマンド積みこみ終了 g_VkCmdBuffers[0].end(); // コマンドをSubmitして終了を待つ vk::SubmitInfo copySubmitInfo; copySubmitInfo.commandBufferCount = 1; copySubmitInfo.pCommandBuffers = &g_VkCmdBuffers[0]; g_VkQueue.submit(copySubmitInfo, VK_NULL_HANDLE); g_VkQueue.waitIdle(); }}

最初に今回使用する深度バッファのフォーマットがGPUで使用可能かチェックします。

まあ、PCのVulkan対応GPUならほぼ大丈夫でしょうけど、Androidとかだと必要かもしれないですね。

次に vk::ImageCreateInfo の必要項目を埋めて vk::Device::createImage() メソッドで vk::Image を作成します。

vk::Image はイメージ情報を格納するクラスですが、この段階では情報を格納しているだけでバッファは持っていません。

このイメージを使用するのに必要なバッファサイズを vk::Device::getImageMemoryRequirements() メソッドで取得します。

そして vk::Device::allocateMemory() で必要サイズのバッファを確保、vk::Device::bindImageMemory() で vk::Image と確保したメモリを関連付けます。

確保するメモリは vk::MemoryPropertyFlagBits::eDeviceLocal を指定して VRAM から確保します。

vk::Image の準備が整ったので View を作成します。

vk::ImageView は特定のイメージの View、シェーダからアクセスする場合のアクセサ的なものです。

まあ、この概念はDX11にもDX12にもあったので特に解説の必要はありませんね。

ここまで作成すれば深度バッファの準備は完了しているので、Swapchainのバックバッファと同様にイメージレイアウトを深度・ステンシルバッファとして使用できる形に遷移します。

次にフレームバッファを作成します。

Vulkan でのフレームバッファは1度の DrawCall で描画するバッファをまとめたオブジェクトです。

これは事前にどのイメージを描画バッファとするか、何枚の描画バッファを利用するかという情報を元に作成しておく必要があります。

描画バッファのサイズなども予め指定しておく必要があります。

{ std::array<vk::ImageView, 2> views; views[1] = g_VkDepthBuffer.view; vk::FramebufferCreateInfo framebufferCreateInfo; framebufferCreateInfo.renderPass = g_VkRenderPass; framebufferCreateInfo.attachmentCount = static_cast<uint32_t>(views.size()); framebufferCreateInfo.pAttachments = views.data(); framebufferCreateInfo.width = kScreenWidth; framebufferCreateInfo.height = kScreenHeight; framebufferCreateInfo.layers = 1; // Swapchainからフレームバッファを生成する g_VkFramebuffers = g_VkSwapchain.CreateFramebuffers(framebufferCreateInfo);}

vk::FramebufferCreateInfo 構造体に使用するバッファ数を View の数として与え、そのデータも追加します。

もちろん、バッファのサイズも忘れません。

使用するレンダーパスも指定する必要がありますが、レンダーパスについては後述します。

MySwapchain クラスにはCreateFramebuffers() メソッドが存在し、ここで Swapchain が持っているバッファの数分だけフレームバッファを作成します。

Swapchain は複数のバッファを交互に表示・描画を行うので、その分フレームバッファを作成しなければなりません。

このメソッドは以下のようになっています。

std::vector<vk::Framebuffer> CreateFramebuffers(vk::FramebufferCreateInfo framebufferCreateInfo){ std::vector<vk::ImageView> views; views.resize(framebufferCreateInfo.attachmentCount); for (size_t i = 0; i < framebufferCreateInfo.attachmentCount; i++) { views[i] = framebufferCreateInfo.pAttachments[i]; } framebufferCreateInfo.pAttachments = views.data(); std::vector<vk::Framebuffer> framebuffers; framebuffers.resize(m_imageCount); for (uint32_t i = 0; i < m_imageCount; i++) { views[0] = m_images[i].view; framebuffers[i] = g_VkDevice.createFramebuffer(framebufferCreateInfo); } return framebuffers;}

Swapchain が持っているバックバッファの vk::ImageView を用いてバックバッファ数分のフレームバッファを作成しています。

さて、次にレンダーパスを作成します。

このレンダーパス、私も正確に把握できていないので説明が怪しいのですが、Vulkan では描画を行う場合は必ずこのレンダーパスの開始/終了の間で行わなければなりません。

とりあえず作成コードを見ていきましょう。

// RenderPass設定std::array<vk::AttachmentDescription, 2> attachmentDescs;std::array<vk::AttachmentReference, 2> attachmentRefs;// カラーバッファのアタッチメント設定 attachmentDescs[0].format = g_VkSwapchain.GetFormat(); attachmentDescs[0].loadOp = vk::AttachmentLoadOp::eDontCare; attachmentDescs[0].storeOp = vk::AttachmentStoreOp::eStore; attachmentDescs[0].initialLayout = vk::ImageLayout::eColorAttachmentOptimal; attachmentDescs[0].finalLayout = vk::ImageLayout::eColorAttachmentOptimal;// 深度バッファのアタッチメント設定 attachmentDescs[1].format = vk::Format::eD32SfloatS8Uint; attachmentDescs[1].loadOp = vk::AttachmentLoadOp::eDontCare; attachmentDescs[1].storeOp = vk::AttachmentStoreOp::eDontCare; attachmentDescs[1].initialLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal; attachmentDescs[1].finalLayout = vk::ImageLayout::eDepthStencilAttachmentOptimal;// カラーバッファのリファレンス設定 vk::AttachmentReference& colorReference = attachmentRefs[0]; colorReference.attachment = 0; colorReference.layout = vk::ImageLayout::eColorAttachmentOptimal;// 深度バッファのリファレンス設定 vk::AttachmentReference& depthReference = attachmentRefs[1]; depthReference.attachment = 1; depthReference.layout = vk::ImageLayout::eDepthStencilAttachmentOptimal;std::array<vk::SubpassDescription, 1> subpasses;{ vk::SubpassDescription& subpass = subpasses[0]; subpass.pipelineBindPoint = vk::PipelineBindPoint::eGraphics; subpass.colorAttachmentCount = 1; subpass.pColorAttachments = &attachmentRefs[0]; subpass.pDepthStencilAttachment = &attachmentRefs[1];}std::array<vk::SubpassDependency, 1> subpassDepends;{ vk::SubpassDependency& dependency = subpassDepends[0]; dependency.srcSubpass = 0; dependency.dstSubpass = VK_SUBPASS_EXTERNAL; dependency.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; dependency.dstAccessMask = vk::AccessFlagBits::eColorAttachmentRead; dependency.dstStageMask = vk::PipelineStageFlagBits::eColorAttachmentOutput; dependency.srcStageMask = vk::PipelineStageFlagBits::eBottomOfPipe;}// RenderPass生成{ vk::RenderPassCreateInfo renderPassInfo; renderPassInfo.attachmentCount = (uint32_t)attachmentDescs.size(); renderPassInfo.pAttachments = attachmentDescs.data(); renderPassInfo.subpassCount = (uint32_t)subpasses.size(); renderPassInfo.pSubpasses = subpasses.data(); renderPassInfo.dependencyCount = (uint32_t)subpassDepends.size(); renderPassInfo.pDependencies = subpassDepends.data(); g_VkRenderPass = g_VkDevice.createRenderPass(renderPassInfo); if (!g_VkRenderPass) { return false; }}

レンダーパスが持つべき情報はいくつかあります。

まずはフレームバッファの View の数とフォーマット、その使用用途を情報として書き込んでおく必要があります。

この際、描画パス開始時のイメージレイアウトを指定し、終了時にどのレイアウトに変更するかを指定することが出来ます。

また、フレームバッファのクリアが必要であればここで設定しておくことも出来ます。

つまり、レンダーパス作成時にはどのようなフレームバッファが必要かを知っている必要があるわけですが、レンダリングパイプラインを構築するプログラマであればどの段階でどのようなフレームバッファが必要かは理解しているはずなので、事前に作成することが出来るでしょう。

ソースコードでは vk::AttachmentDescription が2つ作られていますが、これがその情報です。

このレンダーパスで使用するフレームバッファオブジェクトはこの配列と同じになっていなければなりません。

しかし面白いのは、フレームバッファオブジェクトがそのまま描画ターゲットになるというわけではない点です。

vk::AttachmentReference は描画対象となるカラーバッファと深度バッファとしてフレームバッファの何番を参照するかを指定します。

例えばフレームバッファに2枚のカラーバッファ、2枚の深度バッファを設定しておき、1枚のカラーバッファ、1枚の深度バッファを指定して利用することが可能・・・なはず。

いや、実は試してないのでほんとに出来るかどうかわかってないです。すみません。

この参照するバッファを設定するのがサブパスという存在です。

例えば Deferred Rendering を行う場合、通常は4枚のカラーバッファに描画するだけなのですが、ある特殊なシェーディングモデル(例えば肌とか)の場合に限り、通常の4枚に加えてもう1枚のカラーバッファに描画を行いたいとします。

この場合、レンダーパスを分けてしまうことも可能なはずですが、サブパスを使うとレンダーパスを終了させずに描画ターゲットの切り替えができるものと思われます。

ただし、サブパスは特定インデックスのサブパスを指定することが出来ないようで、次のサブパスに切り替えることしか出来ないようです。

なので、上記のような例では本来あまり使われないのではないかと思われます。

まあ、ポストプロセスで連続的に処理を繋ぎたい場合に使うのが基本でしょうか?

これらの情報を vk::RenderPassCreateInfo にまとめたら vk::Device::createRenderPass() メソッドで描画パスを作成します。

今回のレンダーパスはバッファのクリアは行っていないので、描画前に明示的にクリア処理を行う必要があります。

しかし、個人的にはこのようなやり方をした方がいいんじゃないかと思います。レンダーパスを使い回す場合にクリアを指定してしまうと面倒ですので。

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

これらのバッファは速度の観点から VRAM に配置したほうが良いのですが、VRAM は直接アクセスすることが出来ません。

つまり、特定の頂点・インデックスデータを直接 VRAM にコピーすることが出来ないのです。

そこで今回はCPUからアクセス可能なメモリに Staging Buffer を作成し、ここに頂点・インデックスデータをコピー、GPUにDMA転送のコマンドを送ってバッファコピーを行うようにします。

// バッファコピー用のコマンドバッファを生成する// 略 vk::CommandBuffer copyCommandBuffer = g_VkDevice.allocateCommandBuffers(cmdBufInfo)[0];// VRAM上に頂点バッファを作成し、そこにコピーするためのStaging Bufferにデータをコピーしておく BufferResource stagingVBuffer;{ // システムメモリ上にコピー元の頂点バッファを生成する vk::BufferCreateInfo vertexBufferInfo; vertexBufferInfo.size = sizeof(vertexData); vertexBufferInfo.usage = vk::BufferUsageFlagBits::eTransferSrc; // このバッファはコピー元として利用する stagingVBuffer.buffer = g_VkDevice.createBuffer(vertexBufferInfo); vk::MemoryRequirements memReqs = g_VkDevice.getBufferMemoryRequirements(stagingVBuffer.buffer); vk::MemoryAllocateInfo memAlloc; memAlloc.allocationSize = memReqs.size; memAlloc.memoryTypeIndex = GetMemoryTypeIndex(memReqs.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible); stagingVBuffer.devMem = g_VkDevice.allocateMemory(memAlloc); void* data = g_VkDevice.mapMemory(stagingVBuffer.devMem, 0, sizeof(vertexData), vk::MemoryMapFlags()); memcpy(data, vertexData, sizeof(vertexData)); g_VkDevice.unmapMemory(stagingVBuffer.devMem); g_VkDevice.bindBufferMemory(stagingVBuffer.buffer, stagingVBuffer.devMem, 0); // VRAM上にコピー先の頂点バッファを生成する vertexBufferInfo.usage = vk::BufferUsageFlagBits::eVertexBuffer | vk::BufferUsageFlagBits::eTransferDst; g_VkVBuffer.buffer = g_VkDevice.createBuffer(vertexBufferInfo); memReqs = g_VkDevice.getBufferMemoryRequirements(g_VkVBuffer.buffer); memAlloc.allocationSize = memReqs.size; memAlloc.memoryTypeIndex = GetMemoryTypeIndex(memReqs.memoryTypeBits, vk::MemoryPropertyFlagBits::eDeviceLocal); g_VkVBuffer.devMem = g_VkDevice.allocateMemory(memAlloc); g_VkDevice.bindBufferMemory(g_VkVBuffer.buffer, g_VkVBuffer.devMem, 0);}// インデックスバッファの処理も同様 BufferResource stagingIBuffer;// 略// メモリコピーのコマンドを発行し、終了を待つ{ vk::CommandBufferBeginInfo cmdBufferBeginInfo; vk::BufferCopy copyRegion; // コマンド積み込み開始 copyCommandBuffer.begin(cmdBufferBeginInfo); // 頂点バッファ copyRegion.size = sizeof(vertexData); copyCommandBuffer.copyBuffer(stagingVBuffer.buffer, g_VkVBuffer.buffer, copyRegion); // インデックスバッファ copyRegion.size = sizeof(indexData); copyCommandBuffer.copyBuffer(stagingIBuffer.buffer, g_VkIBuffer.buffer, copyRegion); // コマンド積みこみ終了 copyCommandBuffer.end(); // コマンドをSubmitして終了を待つ vk::SubmitInfo copySubmitInfo; copySubmitInfo.commandBufferCount = 1; copySubmitInfo.pCommandBuffers = &copyCommandBuffer; g_VkQueue.submit(copySubmitInfo, VK_NULL_HANDLE); g_VkQueue.waitIdle(); // 不要になったバッファを削除する // 略}

今回の頂点は float3 の位置と float4 のカラーをインターリーブして作成しています。

頂点データは vk::MemoryPropertyFlagBits::eHostVisible でしていた、CPUからアクセス可能な Staging Buffer のメモリに予めコピーしておきます。

その後、eDeviceLocal を指定して VRAM 上に配置したメモリを持った vk::Buffer に対してコピーコマンドを発行 (vk::CommandBuffer::copyBuffer() メソッドを使用) します。

上記のコードでは省略していますが、インデックスバッファも同様です。

頂点バッファを描画に使用する際にはバッファを何番のストリームにバインドし、属性のロケーションとバッファに対するオフセット、フォーマットなどを指定する必要があります。

これは頂点バッファ群に対してほぼ一律で設定できると思いますので、予め設定しておきます。

// 頂点バッファのバインドデスクリプションを設定 g_VkBindDescs.resize(1); g_VkBindDescs[0].binding = 0; g_VkBindDescs[0].stride = sizeof(Vertex); g_VkBindDescs[0].inputRate = vk::VertexInputRate::eVertex;// 頂点アトリビュートのデスクリプションを設定// 頂点のメモリレイアウトを指定する g_VkAttribDescs.resize(2);// Position g_VkAttribDescs[0].binding = 0; g_VkAttribDescs[0].location = 0; g_VkAttribDescs[0].format = vk::Format::eR32G32B32Sfloat; g_VkAttribDescs[0].offset = 0;// Color g_VkAttribDescs[1].binding = 0; g_VkAttribDescs[1].location = 1; g_VkAttribDescs[1].format = vk::Format::eR32G32B32A32Sfloat; g_VkAttribDescs[1].offset = sizeof(float) * 3;// 入力ステートに各情報を登録 g_VkVertexInputState.vertexBindingDescriptionCount = static_cast<uint32_t>(g_VkBindDescs.size()); g_VkVertexInputState.pVertexBindingDescriptions = g_VkBindDescs.data(); g_VkVertexInputState.vertexAttributeDescriptionCount = static_cast<uint32_t>(g_VkAttribDescs.size()); g_VkVertexInputState.pVertexAttributeDescriptions = g_VkAttribDescs.data();

見ての通りですね。

さて、ここまで見てきましたがまだまだ解説がたりません。

が、かなり長くなってしまったので一旦ここで切ります。

次回には終わらせたい…けど、どうかなぁ?