画面のクリア処理
前回 SharpVulkan をセットアップして、ウィンドウが出るところまでを確認できたと思います。
今回は前回作ったプロジェクトの中に自分で描画を行うプログラムコードを記述していきます。
VulkanControl の中身を自分で描画していきますが、まずはグラフィックスにおける Hello,world という位置付けの画面クリアを目指します。
イベントハンドラの接続
VulkanControl はそれ自身は Vulkan での描画結果を出力しています。
なにもしなくとも青みを帯びた黒い画面が出ていたと思います。
この部分を自分で描画したものにするために、VulkanControl の保持しているイベントハンドラと接続を行うことにします。
以下のように MainWindow.xaml を編集して、 VulkanControl に名前をつけておきます。
<vk:VulkanControl x:Name="vkctrl" EnableValidation="true" EnableDepthBuffer="True" DockPanel.Dock="Bottom" Background="Blue" >
この状態で、 MainWindow.xaml.cs を開きます。
以下のようにして vkctrl の 3つのイベントハンドラに自分のイベントハンドラ関数を接続します。
VisualStudio のエディタでプログラムを記述している場合、 ”+=” の部分までタイプした後、コード補完によりハンドラ関数を追加するのが楽です。
最後に、ウィンドウが閉じるときのハンドラ関数も以下のようにして追加しています。
これは VulkanControl のリソース破棄をここで行うために必要になります。
public MainWindow() { InitializeComponent(); vkctrl.VulkanInitialized += Vkctrl_VulkanInitialized; vkctrl.VulkanRendering += Vkctrl_VulkanRendering; vkctrl.VulkanClosing += Vkctrl_VulkanClosing; Closing += MainWindow_Closing; }
画面のクリア概要
各イベントハンドラを用意しましたが、そこでどのように処理を行っていくかについて説明します。
Vulkan の詳しい説明については、他の解説サイトに譲るとして、大まかにここでは説明していく方針でいきます。
Vulkan では描画処理の内容をコマンドバッファに追加していきます。
このコマンドバッファを Vulkan Device にキューを通じて送り込むことで描画関連の処理を実行します。また、今までの DirectX11 や OpenGL と比べて細かい制御をできるため、コードの記述量が多くなります。
今回の画面のクリア処理ではコマンドバッファのオブジェクトを本アプリの中で準備するところから始まります。
以下のようなプライベートメンバを MainWindow クラスに追加します。
private VkCommandPool m_commandPool; private VkCommandBuffer[] m_commandBuffers; private uint m_frameCount;
初期化関数
準備した初期化関数の中身を実装していきます。
以下のような初期化関数の中で、本アプリの中で必要とするオブジェクトを初期化します。
void Vkctrl_VulkanInitialized(object sender, SharpVulkanWpf.VulkanEventArgs args) { var device = args.Device; var commandPoolCreateInfo = new VkCommandPoolCreateInfo() { queueFamilyIndex = args.GraphicsQueueIndex, flags = VkCommandPoolCreateFlags.VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, }; VulkanAPI.vkCreateCommandPool(device, ref commandPoolCreateInfo, out m_commandPool); var allocateInfo = new VkCommandBufferAllocateInfo() { commandBufferCount = 1, commandPool = m_commandPool, }; VulkanAPI.vkAllocateCommandBuffers(device, ref allocateInfo, out m_commandBuffers); }
やっていることは多くありません。
Vulkan のコマンドバッファを扱うために VkCommandPool を用意して、そこからコマンドバッファを生成しています。
Vulkan ではオブジェクト生成のパラメータを保持したクラスを生成関数に渡すことによって、オブジェクトを生成します。
終了関数
初期化と対になる終了関数の実装を行います。
以下のようにして生成したオブジェクトを破棄していきます。
また、Windowの Closing では VulkanControl の Dispose を呼び出します。
これにより Vulkan の終了処理が行われます。
private void Vkctrl_VulkanClosing(object sender, SharpVulkanWpf.VulkanEventArgs args) { // 本クラスで作ったVulkanの各リソースを破棄する. var dev = args.Device; VulkanAPI.vkFreeCommandBuffers(dev, m_commandPool, m_commandBuffers); VulkanAPI.vkDestroyCommandPool(dev, m_commandPool); } private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e) { vkctrl.Dispose(); }
描画関数
描画関数の実装を説明していきます。
VulkanControl が Vulkan の Device を保持しているような雰囲気は先の関数達の実装でわかってもらえていると思います。
VulkanControl では描画先の管理もある程度行っています(具体的にはデフォルトのスワップチェインの管理をしています)。
描画処理では、 VulkanControl から描画先イメージの情報を受け取り、
そこに何かを描画して、SwapBuffers 関数を呼び出してコントロール内への描画を完了させる、といった処理になります。
VulkanControl から描画先イメージを取得する関数は AcquireNextImage です。
戻り値に VkImage となっています。今までの言葉で言うとここがプライマリバッファ相当になります。
ここで取得したイメージは、 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR というレイアウトになっています。
これでは描画、ここでは画面クリアを行えるレイアウトでは無いため変更を行います。
この変更を行う関数として以下のように setImageMemoryBarrier を用意しました。
void setImageMemoryBarrier(VkCommandBuffer command, VkAccessFlags srcAccessMask, VkAccessFlags dstAccessMask, VkImageLayout oldLayout, VkImageLayout newLayout, VkImage image, VkImageAspectFlags aspectFlags) { var imageMemoryBarrier = new VkImageMemoryBarrier() { srcAccessMask = srcAccessMask, dstAccessMask = dstAccessMask, oldLayout = oldLayout, newLayout = newLayout, srcQueueFamilyIndex = ~0u, dstQueueFamilyIndex = ~0u, subresourceRange = new VkImageSubresourceRange { aspectMask = aspectFlags, baseMipLevel = 0, levelCount = 1, baseArrayLayer = 0, layerCount = 1, }, image = image }; VulkanAPI.vkCmdPipelineBarrier( command, VkPipelineStageFlags.VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VkPipelineStageFlags.VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, null, // MemoryBarriers null, // BufferMemoryBarriers new VkImageMemoryBarrier[] { imageMemoryBarrier } ); }
Vkctrl_VulkanRendering 関数の中身全体は後方で出しています。
このコードを見ながら、順番に説明していきます。
各処理をコマンドバッファに積んでいくため、先にコマンドバッファへの記録を開始します(vkBeginCommandBuffer)。
AcquireNextImage で取得したイメージをのレイアウト情報を変更するため setImageMemoryBarrier 関数を呼び出します。
このとき、クリア処理の関数は vkCmdClearColorImage を使用します。
この関数の仕様として、 VK_ACCESS_TRANSFER_WRITE_BIT, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL という条件がイメージに対して必要なので、この設定で setImageMemoryBarrier のパラメータを決めます。
vkCmdClearColorImage のあとは、画面に表示させるために、再びレイアウトの変更が必要になります。要は AcquireNextImage でイメージ取得したときの逆変換になります。すなわち VK_ACCESS_MEMORY_READ_BIT, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR という条件になります。
ここまでで描画処理内容が終わりますので、コマンドバッファへの記録を終了します(vkEndCommandBuffer)
最後に、コマンドバッファを送り込んで(vkQueueSubmit)、VulkanControl の SwapBuffers を呼び出して画面に見えるようになります。
クリアの色情報を毎フレーム変化させるようにしてコードを記述したものが以下のようになっています。
private void Vkctrl_VulkanRendering(object sender, SharpVulkanWpf.VulkanEventArgs args) { var device = args.Device; var image = vkctrl.AcquireNextImage(); var command = m_commandBuffers[0]; VulkanAPI.vkBeginCommandBuffer(command); VkClearColorValue clearColor = new VkClearColorValue(); clearColor.valF32.R = 0.125f; clearColor.valF32.G = 0.25f; clearColor.valF32.B = (float)(0.5f * Math.Sin(m_frameCount++ * 0.1) + 0.5); clearColor.valF32.A = 1.0f; VkImageSubresourceRange range = new VkImageSubresourceRange() { baseMipLevel = 0, levelCount = 1, baseArrayLayer = 0, layerCount = 1, aspectMask = VkImageAspectFlags.VK_IMAGE_ASPECT_COLOR_BIT }; setImageMemoryBarrier(command, VkAccessFlags.VK_ACCESS_MEMORY_READ_BIT, VkAccessFlags.VK_ACCESS_TRANSFER_WRITE_BIT, VkImageLayout.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, VkImageLayout.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, image, VkImageAspectFlags.VK_IMAGE_ASPECT_COLOR_BIT); VulkanAPI.vkCmdClearColorImage(command, image, VkImageLayout.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, ref clearColor, new VkImageSubresourceRange[] { range }); // Present のためにレイアウト変更 setImageMemoryBarrier(command, VkAccessFlags.VK_ACCESS_TRANSFER_WRITE_BIT, VkAccessFlags.VK_ACCESS_MEMORY_READ_BIT, VkImageLayout.VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VkImageLayout.VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, image, VkImageAspectFlags.VK_IMAGE_ASPECT_COLOR_BIT); VulkanAPI.vkEndCommandBuffer(command); var submitInfo = new VkSubmitInfo() { commandBuffers = new[] { command } }; VulkanAPI.vkQueueSubmit(args.GraphicsQueue, new VkSubmitInfo[] { submitInfo }, null); vkctrl.SwapBuffers(); }
Vulkan ではこのようにレイアウトやアクセスフラグをこまめに制御していく必要があります。
まとめ
画面のクリアまでのコードを紹介しました。
ここまでおよそ 200行も到達していないと思います。これを、長いと感じたでしょうか、それとも短いと感じたでしょうか。
また、単なる画面のクリアなのにイメージレイアウトの変更とか・・・今まで感じたことのないものを感じてもらえたでしょうか。
SharpVulkan の Vulkan API クラスを使っていくと Vulkan の機能を操作できるということと、
SharpVulkanWpf の VulkanControl の中にアプリで記述した描画結果を出せる、という点をわかってもらえたところで今回はおしまいにしたいと思います。