Vulkanの話 第5回

ポリゴンを表示する その3 16/08/04 up

前回の続きです。

前回はデスクリプタセットを作成するところまで行いました。

初期化はあともう1つで終わりです。

今回初期化で作成するオブジェクトはパイプラインです。

パイプラインはDX12でいうところの Pipeline State Object (PSO) です。

深度テストやブレンド、使用するシェーダ、描画するトポロジなどを設定します。

DX12のPSOと違う点としてはレンダーターゲットの指定を行わないところです。

Vulkanではレンダーターゲットは 第3回 で作成したレンダーパスに設定されています。

以下にパイプラインの設定命令を提示しますが、長いので省略多めです。

bool InitializePipeline(){ { // デスクリプタセットレイアウトに対応したパイプラインレイアウトを生成する vk::PipelineLayoutCreateInfo pPipelineLayoutCreateInfo; pPipelineLayoutCreateInfo.setLayoutCount = 1; pPipelineLayoutCreateInfo.pSetLayouts = &g_VkDescSetLayout; g_VkPipeLayout = g_VkDevice.createPipelineLayout(pPipelineLayoutCreateInfo); } // 描画トポロジの設定 vk::PipelineInputAssemblyStateCreateInfo inputAssemblyState; inputAssemblyState.topology = vk::PrimitiveTopology::eTriangleList; // ラスタライズステートの設定 vk::PipelineRasterizationStateCreateInfo rasterizationState; // 略 // ブレンドモードの設定 vk::PipelineColorBlendStateCreateInfo colorBlendState; vk::PipelineColorBlendAttachmentState blendAttachmentState[1] = {}; // 略 // Viewportステートの設定 vk::PipelineViewportStateCreateInfo viewportState; viewportState.viewportCount = 1; viewportState.scissorCount = 1; // DynamicStateを利用してViewportとScissorBoxを変更できるようにしておく vk::PipelineDynamicStateCreateInfo dynamicState; std::vector<vk::DynamicState> dynamicStateEnables; dynamicStateEnables.push_back(vk::DynamicState::eViewport); dynamicStateEnables.push_back(vk::DynamicState::eScissor); dynamicState.pDynamicStates = dynamicStateEnables.data(); dynamicState.dynamicStateCount = static_cast<uint32_t>(dynamicStateEnables.size()); // DepthStensilステートの設定 vk::PipelineDepthStencilStateCreateInfo depthStencilState; // 略 // マルチサンプルステート vk::PipelineMultisampleStateCreateInfo multisampleState; // 略 // シェーダ設定 std::array<vk::PipelineShaderStageCreateInfo, 2> shaderStages; shaderStages[0].stage = vk::ShaderStageFlagBits::eVertex; shaderStages[0].module = g_VkVShader; shaderStages[0].pName = "main"; // 略 // パイプライン情報に各種ステートを設定して生成 vk::GraphicsPipelineCreateInfo pipelineCreateInfo; pipelineCreateInfo.layout = g_VkPipeLayout; pipelineCreateInfo.renderPass = g_VkRenderPass; pipelineCreateInfo.stageCount = static_cast<uint32_t>(shaderStages.size()); pipelineCreateInfo.pStages = shaderStages.data(); pipelineCreateInfo.pVertexInputState = &g_VkVertexInputState; pipelineCreateInfo.pInputAssemblyState = &inputAssemblyState; pipelineCreateInfo.pRasterizationState = &rasterizationState; pipelineCreateInfo.pColorBlendState = &colorBlendState; pipelineCreateInfo.pMultisampleState = &multisampleState; pipelineCreateInfo.pViewportState = &viewportState; pipelineCreateInfo.pDepthStencilState = &depthStencilState; pipelineCreateInfo.pDynamicState = &dynamicState; g_VkSamplePipeline = g_VkDevice.createGraphicsPipelines(g_VkPipelineCache, pipelineCreateInfo, nullptr)[0]; if (!g_VkSamplePipeline) { return false; } return true;}

省略されている部分で各種設定を行っていますが、詳細はサンプルコードをチェックしてみてください。

まずは vk::PipelineLayout を作成します。

パイプラインは vk::DescriptorSetLayout に対してこのオブジェクトを通してアクセスします。

vk::DescriptorSetLayout は vk::DescriptorSet がどのようなレイアウトでリソースを持っているかを定義するオブジェクトで、簡単に言ってしまえばどこの引き出しに何が入っているかを示す表のようなものです。

これに対して vk::DescriptorSet は引き出しそのものです。つまり、引き出しの中にはすでに対応するリソースが入っているというわけです。

この引き出し自体は複数作成することが出来ますし、それぞれでレイアウトを変更することももちろん可能です。

しかし、vk::Pipeline 君は引き出しのレイアウトを基本的には1つだけ覚えられます。彼は自分が覚えたレイアウトの通りに引き出しからリソースを取り出します。

vk::PipelineLayout は引き出しレイアウトの書かれている書類のようなものと覚えればいいのではないかと思います。

その後は各種情報を格納していきますが、ViewportScissor の部分だけ具体的なリソースを指定していません。

これは DynamicState と呼ばれる手法を用いて、実際に描画する際に動的にリソースを指定できるようにしています。

パイプラインに設定できるどのようなものでも DynamicState として後で設定できるわけではありません。例えばシェーダやパイプラインレイアウトは無理です。

基本的には動的に変化する可能性のあるものを設定すると良いでしょう。

Viewport や Scissor はカスケードシャドウマップの描画などで変更する可能性がありますが、それ以外ではあまり変更しない気もしますね。

シェーダについては前回も書きましたが、ShaderModule 自体はシェーダの種類を規定しませんので、ユーザがどのモジュールがどの主のシェーダかを管理する必要があります。

また、エントリポイントも自前で指定する必要があります。

これらの設定を行った vk::GraphicsPipelineCreateInfo を1つ以上指定することでパイプラインを生成できます。

CreateInfo を複数指定することで複数のパイプラインを1回で生成することも可能です。

作成すべきオブジェクトはここまでで全て作成しました。

では実際の描画を見ていきましょう。

最初にバッファをクリアしますが、Sample001ではカラーバッファのみクリアしていました。

今回は深度バッファもクリアしますが、以下に提示するコードは深度バッファのクリア部分のみです。

vk::ImageSubresourceRange depthSubRange; depthSubRange.aspectMask = vk::ImageAspectFlagBits::eDepth | vk::ImageAspectFlagBits::eStencil; depthSubRange.levelCount = 1; depthSubRange.layerCount = 1;// クリアするためにレイアウトを変更 SetImageLayout( cmdBuffer, g_VkDepthBuffer.image, vk::ImageLayout::eDepthStencilAttachmentOptimal, vk::ImageLayout::eTransferDstOptimal, depthSubRange);// クリア cmdBuffer.clearDepthStencilImage(g_VkDepthBuffer.image, vk::ImageLayout::eTransferDstOptimal, clearDepth, depthSubRange);// 描画のためにレイアウトを変更 SetImageLayout( cmdBuffer, g_VkDepthBuffer.image, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eDepthStencilAttachmentOptimal, depthSubRange);

基本的にはカラーバッファのクリアと同様です。

クリアする前に転送先レイアウトに変更し、クリアし、最後に深度・ステンシルバッファとしてのレイアウトに変更しています。

次にUniformバッファの内容を更新します。

SceneData scene1; StoreIdentityMatrix(scene1.mtxWorld); StoreIdentityMatrix(scene1.mtxView); StoreIdentityMatrix(scene1.mtxProj); PerspectiveFov(scene1.mtxProj, Deg2Rad(60.0f), 1920.0f / 1080.0f, 1.0f, 100.0f); RotY(scene1.mtxWorld, Deg2Rad(sRotY)); sRotY += 0.1f; if (sRotY > 360.0f) sRotY -= 360.0f; scene1.mtxView[14] = -10.0f; cmdBuffer.updateBuffer(g_VkUniformBuffer.buffer, 0, sizeof(scene1), reinterpret_cast<uint32_t*>(&scene1));

更新にはいくつかの方法がありますが、今回は vk::CommandBuffer::updateBuffer() 命令を用いています。

この命令を使用する場合には転送元バッファをユニークにしておく必要があるようです。バッファのコピー元が変化してしまっていたら困りますからね。

また、この命令はレンダーパスの開始~終了の処理範囲の外で行われなければならないという点に注意してください。

ポリゴンの描画命令は以下のようになります。

vk::RenderPassBeginInfo renderPassBeginInfo; renderPassBeginInfo.renderPass = g_VkRenderPass; renderPassBeginInfo.renderArea.extent = vk::Extent2D(kScreenWidth, kScreenHeight); renderPassBeginInfo.clearValueCount = 0; renderPassBeginInfo.pClearValues = nullptr; renderPassBeginInfo.framebuffer = g_VkFramebuffers[currentBuffer]; cmdBuffer.beginRenderPass(renderPassBeginInfo, vk::SubpassContents::eInline); vk::Viewport viewport = vk::Viewport(0.0f, 0.0f, static_cast<float>(kScreenWidth), static_cast<float>(kScreenHeight), 0.0f, 1.0f); cmdBuffer.setViewport(0, viewport); vk::Rect2D scissor = vk::Rect2D(vk::Offset2D(), vk::Extent2D(kScreenWidth, kScreenHeight)); cmdBuffer.setScissor(0, scissor); // 各リソース等のバインド vk::DeviceSize offsets = 0; cmdBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, g_VkPipeLayout, 0, g_VkDescSets, nullptr); cmdBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, g_VkSamplePipeline); cmdBuffer.bindVertexBuffers(0, g_VkVBuffer.buffer, offsets); cmdBuffer.bindIndexBuffer(g_VkIBuffer.buffer, 0, vk::IndexType::eUint32); cmdBuffer.drawIndexed(6, 1, 0, 0, 1); cmdBuffer.endRenderPass();

描画を行う場合はレンダーパスを開始する必要があります。

vk::RenderPassBeginInfo を用いてレンダーパス開始に必要な各種リソースを設定します。

DynamicState として設定することになっている Viewport と Scissor の設定もレンダーパス内で行っています。

描画単位ではデスクリプタセット、パイプライン、頂点バッファ、インデックスバッファを設定しており、最後の vk::CommandBuffer::drawIndexed() 命令で描画を行っています。

レンダーパスが終了したら1回の描画が完了です。

今回のサンプルではもう1回描画を行っていますが、基本的には1回めの描画と同じことを行っています。

同じことを行っているにもかかわらずレンダーパスを分けた理由は、Uniformバッファを1回めと2回めで変更しているためです。

先にも述べたとおり、vk::CommandBuffer::updateBuffer() はレンダーパス内部で実行できないので、1枚目を描画完了したら一旦レンダーパスを終了させています。

というわけで、ポリゴン描画まで完了しました。

解説長かった…まあ、DX12も長かったし、こんなもんでしょう。

次回はテクスチャを貼り付けて描画を行う方法を解説します。