Vulkanの話 第6回

テクスチャマッピング 16/08/07 up

今回はVulkanでテクスチャを貼り付けてポリゴンを描画してみます。

解説の前にいくつかの紹介を。

まず、数学系のライブラリとしてglmを使用するようにしました。

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

https://github.com/g-truc/glm

ライセンスはMITなので使いやすいですね。

また、今回はテクスチャとして使用する画像データを読み込む必要があり、わかりやすさからTarga形式の画像データを読みこむようにしています。

そのために使用したライブラリがこちらです。

http://dmr.ath.cx/gfx/targa/

DDS形式とどちらにしようか迷ったのですが、Targaの方がとりあえずのプログラムを書きやすかったのでこちらにしました。

これらのライブラリの使用方法については解説しません。あしからず。

では Sample003 のコードを見ていきましょう。

基本的には Sample002 と同じような感じですが、いくらか追加・変更を行っています。

変更した部分すべてを提示しませんが、詳しくはサンプルを読んで頂く方向で。

まずはシェーダから見ていきましょう。頂点シェーダとピクセルシェーダが以下のようになっています。

#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 (location = 2) in vec2 inUV; layout (binding = 0) uniform SceneData { mat4 mtxView; mat4 mtxProj;} uScene; layout (push_constant) uniform MeshData { mat4 mtxWorld;} uMesh; layout (location = 0) out vec4 outColor; layout (location = 1) out vec2 outUV; out gl_PerVertex { vec4 gl_Position;};void main(){ outColor = inColor; outUV = inUV; gl_Position = uScene.mtxProj * uScene.mtxView * uMesh.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 = 1) in vec2 inUV; layout (location = 0) out vec4 outFragColor; layout (binding = 1) uniform sampler2D samColor;void main() { vec4 tc = texture(samColor, inUV, 0); outFragColor = tc * inColor;}

頂点シェーダには頂点入力としてUV座標が追加されています。

これはそのままピクセルシェーダにパススルーされます。

また、Uniformバッファが2つにわけられています。

カメラ周りの行列は SceneData にまとめ、ポリゴンのワールド行列は MeshData に移動しています。

ここでちょっと見慣れない push_constant という属性。

通常はバインドする番号を指定するのですが、それを指定せずに上記のような属性を指定しています。

これについては後で解説します。

ピクセルシェーダ側では sampler2D 形式のパラメータ samColor が追加されています。

これはテクスチャとテクスチャサンプラが一緒になったオブジェクトです。昔の OpenGL と同じ感じですね。

こちらは1番にバインドされていますが、特に理由はないです。

強いて理由をあげるなら、バインドする番号を0からの連番にしなくてもちゃんと機能するかを試したかったくらいの話です。

頂点バッファにUVを追加している部分は割愛して、テクスチャ読み込みの部分を見ていきましょう。

tga_image image;if (tga_read(&image, "data/icon.tga") != TGA_NOERR){ return false;}// Stagingバッファ作成 BufferResource staging;{ vk::BufferCreateInfo stagingCreateInfo; stagingCreateInfo.size = image.width * image.height * image.pixel_depth / 8; stagingCreateInfo.usage = vk::BufferUsageFlagBits::eTransferSrc; // このバッファはコピー元として利用する staging.buffer = g_VkDevice.createBuffer(stagingCreateInfo); // Stagingバッファ用メモリ作成 vk::MemoryRequirements memReqs = g_VkDevice.getBufferMemoryRequirements(staging.buffer); vk::MemoryAllocateInfo memAlloc; memAlloc.allocationSize = memReqs.size; memAlloc.memoryTypeIndex = GetMemoryTypeIndex(memReqs.memoryTypeBits, vk::MemoryPropertyFlagBits::eHostVisible); staging.devMem = g_VkDevice.allocateMemory(memAlloc); void* data = g_VkDevice.mapMemory(staging.devMem, 0, stagingCreateInfo.size, vk::MemoryMapFlags()); memcpy(data, image.image_data, stagingCreateInfo.size); g_VkDevice.unmapMemory(staging.devMem); g_VkDevice.bindBufferMemory(staging.buffer, staging.devMem, 0);}// イメージ生成{ g_VkTexture.format = vk::Format::eB8G8R8A8Unorm; vk::ImageCreateInfo imageCreateInfo; // 略 g_VkTexture.image = g_VkDevice.createImage(imageCreateInfo); assert(g_VkTexture.image); vk::MemoryRequirements memReqs = g_VkDevice.getImageMemoryRequirements(g_VkTexture.image); vk::MemoryAllocateInfo memAllocInfo; memAllocInfo.allocationSize = memReqs.size; memAllocInfo.memoryTypeIndex = GetMemoryTypeIndex(memReqs.memoryTypeBits, vk::MemoryPropertyFlagBits::eDeviceLocal); g_VkTexture.devMem = g_VkDevice.allocateMemory(memAllocInfo); g_VkDevice.bindImageMemory(g_VkTexture.image, g_VkTexture.devMem, 0);}// コピーコマンドを生成、実行{ vk::CommandBufferAllocateInfo cmdBufInfo; // 略 vk::CommandBuffer copyCommandBuffer = g_VkDevice.allocateCommandBuffers(cmdBufInfo)[0]; vk::CommandBufferBeginInfo cmdBufferBeginInfo; // コマンド積み込み開始 copyCommandBuffer.begin(cmdBufferBeginInfo); vk::ImageSubresourceRange subresourceRange; subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; subresourceRange.levelCount = 1; subresourceRange.layerCount = 1; // レイアウト設定 SetImageLayout( copyCommandBuffer, g_VkTexture.image, vk::ImageLayout::ePreinitialized, vk::ImageLayout::eTransferDstOptimal, subresourceRange); // コピーコマンド vk::BufferImageCopy bufferCopyRegion; bufferCopyRegion.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; bufferCopyRegion.imageSubresource.layerCount = 1; bufferCopyRegion.imageExtent.depth = 1; bufferCopyRegion.imageExtent.width = image.width; bufferCopyRegion.imageExtent.height = image.height; bufferCopyRegion.imageSubresource.mipLevel = 0; bufferCopyRegion.bufferOffset = 0; copyCommandBuffer.copyBufferToImage(staging.buffer, g_VkTexture.image, vk::ImageLayout::eTransferDstOptimal, 1, &bufferCopyRegion); // コピー完了後にレイアウト変更 // シェーダから読み込める状態にしておく SetImageLayout( copyCommandBuffer, g_VkTexture.image, vk::ImageLayout::eTransferDstOptimal, vk::ImageLayout::eShaderReadOnlyOptimal, subresourceRange); // コマンド積みこみ終了 copyCommandBuffer.end(); // 略} tga_free_buffers(&image);// サンプラの作成{ vk::SamplerCreateInfo samplerCreateInfo; samplerCreateInfo.magFilter = vk::Filter::eLinear; samplerCreateInfo.minFilter = vk::Filter::eLinear; // 略 g_VkTexture.sampler = g_VkDevice.createSampler(samplerCreateInfo);}// Viewの作成{ vk::ImageViewCreateInfo viewCreateInfo; viewCreateInfo.viewType = vk::ImageViewType::e2D; viewCreateInfo.format = g_VkTexture.format; // 略 viewCreateInfo.image = g_VkTexture.image; g_VkTexture.view = g_VkDevice.createImageView(viewCreateInfo);}

ちょっと長めですが、設定関係は省略しているものが多いです。コピペの際には注意してください。

最初にTargaイメージを前述したライブラリで読み込みます。

次にStagingバッファを作成し、そこにTargaイメージのコピーを行います。

Stagingバッファは頂点バッファやインデックスバッファでも解説しましたが、VRAM に直接アクセスが出来ないのでコピー命令を用いてDMA転送します。

そのためにはStagingバッファに一旦コピーしたい情報をコピーし、その後にコピーコマンドをコマンドバッファに対して発行します。

Stagingバッファは vk::Buffer を用います。

vk::Image を最初試したのですが、何故かうまくいきませんでした。

vk::Image 同士のコピーは命令として存在しているのですが、使用するのは主に描画バッファのコピーとかじゃないかという気がします。

Stagingバッファを作成したあとは vk::Image を作成します。

VRAM 上にメモリを確保する以外は特別なことは行っていません。

次にコピーを行うだけのコマンドバッファを作成し、このバッファでコピー命令を発行します。

ただしそのままコピーするのではなく、vk::Image の ImageLayout を vk::ImageLayout::eTransferDstOptimal に変更します。

テクスチャのレイアウト形式は GPU によって違いがあります。

この違いを考慮してレイアウトに合わせたバイナリをコピーするより、一旦リニアデータをコピーできるようにレイアウトを変更してからレイアウトを変更した方が簡単です。

コピーを行ったあとはシェーダリソースとして使用するため、vk::ImageLayout::eShaderReadOnlyOptimal に変更します。

コマンドの積み込みが終了したらコマンドバッファを実行、完了したところでStagingバッファを削除します。

最後にサンプラとViewを生成していますが、パラメータを適切に設定して作成するだけなので詳しくはソースコードをお読みください。

テクスチャをシェーダリソースとして使用するにはデスクリプタセットを適切に設定する必要があります。

// デスクリプタプールを作成する{ std::array<vk::DescriptorPoolSize, 2> typeCounts; // 略 typeCounts[1].type = vk::DescriptorType::eCombinedImageSampler; typeCounts[1].descriptorCount = 1; // デスクリプタプールを生成 vk::DescriptorPoolCreateInfo descriptorPoolInfo; // 略 g_VkDescPool = g_VkDevice.createDescriptorPool(descriptorPoolInfo);}// デスクリプタセットレイアウトを作成する{ // 描画時のシェーダセットに対するデスクリプタセットのレイアウトを指定する vk::DescriptorSetLayoutBinding layoutBindings[2]; // UniformBuffer for VertexShader // 略 // TextureSampler for PixelShader layoutBindings[1].descriptorType = vk::DescriptorType::eCombinedImageSampler; layoutBindings[1].descriptorCount = 1; layoutBindings[1].binding = 1; layoutBindings[1].stageFlags = vk::ShaderStageFlagBits::eFragment; layoutBindings[1].pImmutableSamplers = nullptr; // レイアウトを生成 vk::DescriptorSetLayoutCreateInfo descriptorLayout; // 略 g_VkDescSetLayout = g_VkDevice.createDescriptorSetLayout(descriptorLayout, nullptr);}// デスクリプタセットを作成する{ vk::DescriptorImageInfo texDescInfo; texDescInfo.sampler = g_VkTexture.sampler; texDescInfo.imageView = g_VkTexture.view; texDescInfo.imageLayout = vk::ImageLayout::eGeneral; // デスクリプタセットは作成済みのデスクリプタプールから確保する vk::DescriptorSetAllocateInfo allocInfo; allocInfo.descriptorPool = g_VkDescPool; allocInfo.descriptorSetCount = 1; allocInfo.pSetLayouts = &g_VkDescSetLayout; g_VkDescSets = g_VkDevice.allocateDescriptorSets(allocInfo); // デスクリプタセットの情報を更新する std::array<vk::WriteDescriptorSet, 2> descSetInfos; // 略 descSetInfos[1].dstSet = g_VkDescSets[0]; descSetInfos[1].descriptorCount = 1; descSetInfos[1].descriptorType = vk::DescriptorType::eCombinedImageSampler; descSetInfos[1].pImageInfo = &texDescInfo; descSetInfos[1].dstBinding = 1; g_VkDevice.updateDescriptorSets(descSetInfos, nullptr);}

頂点シェーダ用のUniformバッファについては省略しています。

省略していない部分が基本的にピクセルシェーダに設定するテクスチャリソース部分です。

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

追加されたデータでは、デスクリプタタイプは vk::DescriptorType::eCombinedImageSampler を指定しています。

こちらがイメージとサンプラを一緒にしたリソースです。

もちろん、別々に設定することも出来ます。

次にデスクリプタセットレイアウトを決定します。

デスクリプタセットの0番は前回と同じく頂点シェーダ用の Uniformバッファですが、1番にはテクスチャを設定するようにします。

この番号はバインドする番号とは別物です。

デスクリプタタイプをプールに指定したものと同じく eCombinedImageSampler を指定します。

バインドする番号は1番で、シェーダステージは vk::ShaderStageFlagBits::eFragment を指定してピクセルシェーダで使用できるようにします。

最後にデスクリプタセットの1番に vk::ImageView と vk::Sampler を指定した情報を設定して完了です。

デスクリプタセットに使用するイメージもサンプラも設定してありますので、描画時にはこのデスクリプタセットを設定するだけでOKです。

最後に PushConstant についてです。

頂点シェーダに見慣れない属性がありましたが、これは Uniformバッファ明示的に設定せずに定数バッファの情報をシェーダステージに送信する機能です。

通常は明示的にメモリを確保する必要がある Uniformバッファですが、PushConstant の場合はコマンドバッファ上にメモリを確保します。

主な使用用途としては、小さな定数情報(数個のベクトル、行列など)で、DrawCallのたびに変更される可能性が高いものです。

変更されることがほとんどないのであれば、普通に Uniformバッファにしてしまうほうが良いでしょう。

今回はメッシュごとのワールド行列に使用していますので用途としてはちょうどよいでしょう。

PushConstant を利用する場合はデスクリプタセットに何かする必要はないのですが、パイプラインレイアウトに PushConstant を使用する旨を通知する必要があります。

// 今回はPushConstantを利用する// 小さなサイズの定数バッファはコマンドバッファに乗せてシェーダに送ることが可能 vk::PushConstantRange pushConstantRange(vk::ShaderStageFlagBits::eVertex, 0, sizeof(MeshData)); vk::PipelineLayoutCreateInfo pPipelineLayoutCreateInfo; pPipelineLayoutCreateInfo.setLayoutCount = 1; pPipelineLayoutCreateInfo.pSetLayouts = &g_VkDescSetLayout; pPipelineLayoutCreateInfo.pushConstantRangeCount = 1; pPipelineLayoutCreateInfo.pPushConstantRanges = &pushConstantRange; g_VkPipeLayout = g_VkDevice.createPipelineLayout(pPipelineLayoutCreateInfo);

vk::PushConstantRange パラメータを利用してどのシェーダにどのサイズの PushConstant を使用するか通知します。

あとは実際に DrawCall を行う直前に PushConstant としての情報を送ってやるだけです。

cmdBuffer.pushConstants(g_VkPipeLayout, vk::ShaderStageFlagBits::eVertex, 0, sizeof(mesh1), &mesh1); cmdBuffer.drawIndexed(6, 1, 0, 0, 1);

こちらの方が Uniformバッファを使うより簡単そうではありますが、シーン情報のように1フレーム中は変更することがほとんどなく、その上様々なレンダーパスで使用されるものは Uniformバッファとして最初に作成した方が絶対有利です。

また、PushConstant は同一のシェーダステージに複数設定することは出来ません。

複数配置してシェーダのコンパイルを行うと、PushConstant はそれぞれのシェーダステージごとに1つまでしか使えません、というエラーが発生するはずです。

というわけで今回はここまで。

次回は何やるか未定。サンプルもまだ作ってないので。