今回扱うのはパイプラインの中の主役と言っても過言ではないシェーダです。
元々の語義としては「陰影処理を行うもの」ですが、実際としてはなんでもできます。前節でパイプラインとは「点の集まりで出来た図形を色のついたピクセルの集合に変換するもの」として紹介しましたが、シェーダはそのパイプラインの中において、あの点とこの点をあっちに動かしたりこっちに動かしたり、あのピクセルこのピクセルを赤に塗ったり青に塗ったりする役割を果たします。
昔はシェーダも簡素なことしかせず、文字通りただの「陰影を付けるもの」であり、その処理内容もほぼ固定だったらしいのですが、今は自由にシェーダプログラムを書いてGPUに実行させることができます。空間をねじ曲げ、複雑な模様を描き、物や何かをべらぼうに増やすことさえもできます。
何でもできるようになった代わり、お決まりのような普通の処理しかしたくない場合でもそれをちゃんと書かなければなりません。デフォルト動作に任せるという選択肢は存在しないのです。自由の刑か何か?
冗談はさておき、我々はシェーダプログラムを書く必要があります。シェーダに使う言語ですが、GLSLというC言語をベースとした専用言語を使います。GLSLで書いたプログラムは、専用のソフトでSPIR-Vという中間言語にコンパイルします。Vulkanからはそれを読み込んで実行することになります。ちなみにSPIR-Vの仕様はGLSLとは独立しており、実際にはGLSL以外の言語(DirectXで使われるHLSLとか)からコンパイルすることもできます。今回は普通にGLSLからコンパイルするのですが。
シェーダには種類があります。その中で最低限必要なものとして、今回は「頂点シェーダ(バーテックスシェーダ)」と「フラグメントシェーダ」を作ります。
まずは頂点シェーダのプログラムから見ていきましょう。頂点シェーダは頂点1つごとに呼ばれ、その頂点の座標を出力します。本来はメインの方のプログラムから点の座標データを与え、シェーダでそれを加工したりするものなのですが、ここでは簡単のためシェーダプログラムの中に座標値をハードコーディングします。
#version 450 #extension GL_ARB_separate_shader_objects : enable void main() { if(gl_VertexIndex == 0) { gl_Position = vec4(0.0, -0.5, 0.0, 1.0); } else if(gl_VertexIndex == 1) { gl_Position = vec4(0.5, 0.5, 0.0, 1.0); } else if(gl_VertexIndex == 2) { gl_Position = vec4(-0.5, 0.5, 0.0, 1.0); } }
出力先は2次元の画像なのになぜ点の位置が4次元座標なのかというのは一旦置いておいて、最初の2成分に注目しましょう。(0.0, -0.5)、(0.5, 0.5)、(-0.5, 0.5)の3点ですね。
Vulkanにおいては画像の一番左上が(-1.0, -1.0)、右下が(1.0, 1.0)になります。つまり、このシェーダに3点の座標を出力させれば以下のような座標データを出力します。
この連続した3点は1つの三角形として認識してくれますので、あとは色を付けたら三角形が描けそうです。
次はフラグメントシェーダです。フラグメントシェーダはピクセル1つごとに呼ばれ、そのピクセルの色を決定します。
#version 450 #extension GL_ARB_separate_shader_objects : enable layout(location = 0) out vec4 outColor; void main() { outColor = vec4(1.0, 0.0, 0.0, 1.0); }
色はRGBA(RGBとαチャンネル)の4次元ベクトルで表現されます。それぞれの値は0.0~1.0の実数で表現されます。ここではRが1.0、GBが0.0なので全て真っ赤になります。
それぞれのソースコードをshader.vert,shader.fragというファイル名で保存します。これをSPIR-Vにコンパイルします。Vulkan SDKに付属のglslcというツールを使います。VulkanインストールディレクトリのBinディレクトリ下にあるはずです。パスを通すなどして以下のコマンドを実行しましょう。
glslc shader.vert -o vert.spv glslc shader.frag -o frag.spv
-oオプションは出力ファイル名の指定です。これでvert.spvとfrag.spvというそれぞれのSPIR-V形式のシェーダファイルが出来ました。これをメインの方のプログラムから読み込みます。
ファイルのサイズと中身のデータさえ取れればいいので、その方法はC言語のファイルポインタだろうがWindowsAPIだろうがシステムコールだろうが何でも良いのですが、ここではC++の標準ライブラリを使用します。
インクルードするヘッダを追加しましょう。
#include <fstream> #include <filesystem>
そして読み込みます。まずは頂点シェーダから。
size_t vertSpvFileSz = std::filesystem::file_size("vert.spv"); std::ifstream vertSpvFile("vert.spv", std::ios_base::binary); std::vector<char> vertSpvFileData(vertSpvFileSz); vertSpvFile.read(vertSpvFileData.data(), vertSpvFileSz);
読み込んだら、そのデータからシェーダモジュールなるオブジェクトを作成します。シェーダモジュールはvk::Device のcreateShaderModule メソッドで作成できます。
vk::ShaderModuleCreateInfo vertShaderCreateInfo; vertShaderCreateInfo.codeSize = vertSpvFileSz; vertShaderCreateInfo.pCode = reinterpret_cast<const uint32_t*>(vertSpvFileData.data()); vk::UniqueShaderModule vertShader = device->createShaderModuleUnique(vertShaderCreateInfo);
これでSPIR-V形式の頂点シェーダのデータから、頂点シェーダを表すシェーダモジュールが作成できました。
フラグメントシェーダも同様にします。
size_t fragSpvFileSz = std::filesystem::file_size("frag.spv"); std::ifstream fragSpvFile("frag.spv", std::ios_base::binary); std::vector<char> fragSpvFileData(fragSpvFileSz); fragSpvFile.read(fragSpvFileData.data(), fragSpvFileSz); vk::ShaderModuleCreateInfo fragShaderCreateInfo; fragShaderCreateInfo.codeSize = fragSpvFileSz; fragShaderCreateInfo.pCode = reinterpret_cast<const uint32_t*>(fragSpvFileData.data()); vk::UniqueShaderModule fragShader = device->createShaderModuleUnique(fragShaderCreateInfo);
ここまでをとりあえず実行してみましょう。もしも実行時にエラーが出る場合は、ファイルの名前や場所が間違っていないかどうかを確認してください。エラーが出なければ成功です。
読み込んだシェーダをパイプラインに組み込みましょう。前節で書いたパイプラインの作成処理に追加します。
vk::PipelineShaderStageCreateInfo shaderStage[2]; shaderStage[0].stage = vk::ShaderStageFlagBits::eVertex; shaderStage[0].module = vertShader.get(); shaderStage[0].pName = "main"; shaderStage[1].stage = vk::ShaderStageFlagBits::eFragment; shaderStage[1].module = fragShader.get(); shaderStage[1].pName = "main"; vk::GraphicsPipelineCreateInfo pipelineCreateInfo; pipelineCreateInfo.pViewportState = &viewportState; pipelineCreateInfo.pVertexInputState = &vertexInputInfo; pipelineCreateInfo.pInputAssemblyState = &inputAssembly; pipelineCreateInfo.pRasterizationState = &rasterizer; pipelineCreateInfo.pMultisampleState = &multisample; pipelineCreateInfo.pColorBlendState = &blend; pipelineCreateInfo.layout = pipelineLayout.get(); pipelineCreateInfo.renderPass = renderpass.get(); pipelineCreateInfo.subpass = 0; pipelineCreateInfo.stageCount = 2; pipelineCreateInfo.pStages = shaderStage;
vk::PipelineShaderStageCreateInfo のpName に”main”という文字列を入れていますが、これは「このシェーダはmain関数から始める」という意味です。
これでパイプラインにシェーダが追加できました。おめでとうございます。
この節ではシェーダの追加をやりました。次節ではイメージビューの作成をやります。
// 環境に合わせて #define VK_USE_PLATFORM_WIN32_KHR #define VULKAN_HPP_TYPESAFE_CONVERSION #include <vulkan/vulkan.hpp> #include <fstream> #include <filesystem> #include <iostream> #include <vector> const uint32_t screenWidth = 640; const uint32_t screenHeight = 480; int main() { vk::InstanceCreateInfo createInfo; vk::UniqueInstance instance; instance = vk::createInstanceUnique(createInfo); std::vector<vk::PhysicalDevice> physicalDevices = instance->enumeratePhysicalDevices(); vk::PhysicalDevice physicalDevice; bool existsSuitablePhysicalDevice = false; uint32_t graphicsQueueFamilyIndex; for (size_t i = 0; i < physicalDevices.size(); i++) { std::vector<vk::QueueFamilyProperties> queueProps = physicalDevices[i].getQueueFamilyProperties(); bool existsGraphicsQueue = false; for (size_t j = 0; j < queueProps.size(); i++) { if (queueProps[j].queueFlags & vk::QueueFlagBits::eGraphics) { existsGraphicsQueue = true; graphicsQueueFamilyIndex = j; break; } } if (existsGraphicsQueue) { physicalDevice = physicalDevices[i]; existsSuitablePhysicalDevice = true; break; } } if (!existsSuitablePhysicalDevice) { std::cerr << "使用可能な物理デバイスがありません。" << std::endl; return -1; } vk::DeviceCreateInfo devCreateInfo; vk::DeviceQueueCreateInfo queueCreateInfo[1]; queueCreateInfo[0].queueFamilyIndex = graphicsQueueFamilyIndex; queueCreateInfo[0].queueCount = 1; float queuePriorities[1] = { 1.0 }; queueCreateInfo[0].pQueuePriorities = queuePriorities; devCreateInfo.pQueueCreateInfos = queueCreateInfo; devCreateInfo.queueCreateInfoCount = 1; vk::UniqueDevice device = physicalDevice.createDeviceUnique(devCreateInfo); vk::Queue graphicsQueue = device->getQueue(graphicsQueueFamilyIndex, 0); vk::CommandPoolCreateInfo cmdPoolCreateInfo; cmdPoolCreateInfo.queueFamilyIndex = graphicsQueueFamilyIndex; vk::UniqueCommandPool cmdPool = device->createCommandPoolUnique(cmdPoolCreateInfo); vk::CommandBufferAllocateInfo cmdBufAllocInfo; cmdBufAllocInfo.commandPool = cmdPool.get(); cmdBufAllocInfo.commandBufferCount = 1; cmdBufAllocInfo.level = vk::CommandBufferLevel::ePrimary; std::vector<vk::UniqueCommandBuffer> cmdBufs = device->allocateCommandBuffersUnique(cmdBufAllocInfo); vk::ImageCreateInfo imgCreateInfo; imgCreateInfo.imageType = vk::ImageType::e2D; imgCreateInfo.extent = vk::Extent3D(screenWidth, screenHeight, 1); imgCreateInfo.mipLevels = 1; imgCreateInfo.arrayLayers = 1; imgCreateInfo.format = vk::Format::eR8G8B8A8Unorm; imgCreateInfo.tiling = vk::ImageTiling::eLinear; imgCreateInfo.initialLayout = vk::ImageLayout::eUndefined; imgCreateInfo.usage = vk::ImageUsageFlagBits::eColorAttachment; imgCreateInfo.sharingMode = vk::SharingMode::eExclusive; imgCreateInfo.samples = vk::SampleCountFlagBits::e1; vk::UniqueImage image = device->createImageUnique(imgCreateInfo); vk::PhysicalDeviceMemoryProperties memProps = physicalDevice.getMemoryProperties(); vk::MemoryRequirements imgMemReq = device->getImageMemoryRequirements(image.get()); vk::MemoryAllocateInfo imgMemAllocInfo; imgMemAllocInfo.allocationSize = imgMemReq.size; bool suitableMemoryTypeFound = false; for (size_t i = 0; i < memProps.memoryTypeCount; i++) { if (imgMemReq.memoryTypeBits & (1 << i)) { imgMemAllocInfo.memoryTypeIndex = i; suitableMemoryTypeFound = true; break; } } if (!suitableMemoryTypeFound) { std::cerr << "使用可能なメモリタイプがありません。" << std::endl; return -1; } vk::UniqueDeviceMemory imgMem = device->allocateMemoryUnique(imgMemAllocInfo); device->bindImageMemory(image.get(), imgMem.get(), 0); vk::AttachmentDescription attachments[1]; attachments[0].format = vk::Format::eR8G8B8A8Unorm; attachments[0].samples = vk::SampleCountFlagBits::e1; attachments[0].loadOp = vk::AttachmentLoadOp::eDontCare; attachments[0].storeOp = vk::AttachmentStoreOp::eStore; attachments[0].stencilLoadOp = vk::AttachmentLoadOp::eDontCare; attachments[0].stencilStoreOp = vk::AttachmentStoreOp::eDontCare; attachments[0].initialLayout = vk::ImageLayout::eUndefined; attachments[0].finalLayout = vk::ImageLayout::eGeneral; vk::AttachmentReference subpass0_attachmentRefs[1]; subpass0_attachmentRefs[0].attachment = 0; subpass0_attachmentRefs[0].layout = vk::ImageLayout::eColorAttachmentOptimal; vk::SubpassDescription subpasses[1]; subpasses[0].pipelineBindPoint = vk::PipelineBindPoint::eGraphics; subpasses[0].colorAttachmentCount = 1; subpasses[0].pColorAttachments = subpass0_attachmentRefs; vk::RenderPassCreateInfo renderpassCreateInfo; renderpassCreateInfo.attachmentCount = 1; renderpassCreateInfo.pAttachments = attachments; renderpassCreateInfo.subpassCount = 1; renderpassCreateInfo.pSubpasses = subpasses; renderpassCreateInfo.dependencyCount = 0; renderpassCreateInfo.pDependencies = nullptr; vk::UniqueRenderPass renderpass = device->createRenderPassUnique(renderpassCreateInfo); vk::Viewport viewports[1]; viewports[0].x = 0.0; viewports[0].y = 0.0; viewports[0].minDepth = 0.0; viewports[0].maxDepth = 1.0; viewports[0].width = screenWidth; viewports[0].height = screenHeight; vk::Rect2D scissors[1]; scissors[0].offset = { 0, 0 }; scissors[0].extent = { screenWidth, screenHeight }; vk::PipelineViewportStateCreateInfo viewportState; viewportState.viewportCount = 1; viewportState.pViewports = viewports; viewportState.scissorCount = 1; viewportState.pScissors = scissors; vk::PipelineVertexInputStateCreateInfo vertexInputInfo; vertexInputInfo.vertexAttributeDescriptionCount = 0; vertexInputInfo.pVertexAttributeDescriptions = nullptr; vertexInputInfo.vertexBindingDescriptionCount = 0; vertexInputInfo.pVertexBindingDescriptions = nullptr; vk::PipelineInputAssemblyStateCreateInfo inputAssembly; inputAssembly.topology = vk::PrimitiveTopology::eTriangleList; inputAssembly.primitiveRestartEnable = false; vk::PipelineRasterizationStateCreateInfo rasterizer; rasterizer.depthClampEnable = false; rasterizer.rasterizerDiscardEnable = false; rasterizer.polygonMode = vk::PolygonMode::eFill; rasterizer.lineWidth = 1.0f; rasterizer.cullMode = vk::CullModeFlagBits::eBack; rasterizer.frontFace = vk::FrontFace::eClockwise; rasterizer.depthBiasEnable = false; vk::PipelineMultisampleStateCreateInfo multisample; multisample.sampleShadingEnable = false; multisample.rasterizationSamples = vk::SampleCountFlagBits::e1; vk::PipelineColorBlendAttachmentState blendattachment[1]; blendattachment[0].colorWriteMask = vk::ColorComponentFlagBits::eA | vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB; blendattachment[0].blendEnable = false; vk::PipelineColorBlendStateCreateInfo blend; blend.logicOpEnable = false; blend.attachmentCount = 1; blend.pAttachments = blendattachment; vk::PipelineLayoutCreateInfo layoutCreateInfo; layoutCreateInfo.setLayoutCount = 0; layoutCreateInfo.pSetLayouts = nullptr; vk::UniquePipelineLayout pipelineLayout = device->createPipelineLayoutUnique(layoutCreateInfo); size_t vertSpvFileSz = std::filesystem::file_size("vert.spv"); std::ifstream vertSpvFile("vert.spv", std::ios_base::binary); std::vector<char> vertSpvFileData(vertSpvFileSz); vertSpvFile.read(vertSpvFileData.data(), vertSpvFileSz); vk::ShaderModuleCreateInfo vertShaderCreateInfo; vertShaderCreateInfo.codeSize = vertSpvFileSz; vertShaderCreateInfo.pCode = reinterpret_cast<const uint32_t*>(vertSpvFileData.data()); vk::UniqueShaderModule vertShader = device->createShaderModuleUnique(vertShaderCreateInfo); size_t fragSpvFileSz = std::filesystem::file_size("frag.spv"); std::ifstream fragSpvFile("frag.spv", std::ios_base::binary); std::vector<char> fragSpvFileData(fragSpvFileSz); fragSpvFile.read(fragSpvFileData.data(), fragSpvFileSz); vk::ShaderModuleCreateInfo fragShaderCreateInfo; fragShaderCreateInfo.codeSize = fragSpvFileSz; fragShaderCreateInfo.pCode = reinterpret_cast<const uint32_t*>(fragSpvFileData.data()); vk::UniqueShaderModule fragShader = device->createShaderModuleUnique(fragShaderCreateInfo); vk::PipelineShaderStageCreateInfo shaderStage[2]; shaderStage[0].stage = vk::ShaderStageFlagBits::eVertex; shaderStage[0].module = vertShader.get(); shaderStage[0].pName = "main"; shaderStage[1].stage = vk::ShaderStageFlagBits::eFragment; shaderStage[1].module = fragShader.get(); shaderStage[1].pName = "main"; vk::GraphicsPipelineCreateInfo pipelineCreateInfo; pipelineCreateInfo.pViewportState = &viewportState; pipelineCreateInfo.pVertexInputState = &vertexInputInfo; pipelineCreateInfo.pInputAssemblyState = &inputAssembly; pipelineCreateInfo.pRasterizationState = &rasterizer; pipelineCreateInfo.pMultisampleState = &multisample; pipelineCreateInfo.pColorBlendState = &blend; pipelineCreateInfo.layout = pipelineLayout.get(); pipelineCreateInfo.renderPass = renderpass.get(); pipelineCreateInfo.subpass = 0; pipelineCreateInfo.stageCount = 2; pipelineCreateInfo.pStages = shaderStage; vk::UniquePipeline pipeline = device->createGraphicsPipelineUnique(nullptr, pipelineCreateInfo); vk::CommandBufferBeginInfo cmdBeginInfo; cmdBufs[0]->begin(cmdBeginInfo); // コマンドを記録 cmdBufs[0]->end(); vk::CommandBuffer submitCmdBuf[1] = { cmdBufs[0].get() }; vk::SubmitInfo submitInfo; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = submitCmdBuf; graphicsQueue.submit({ submitInfo }, nullptr); return 0; }
#version 450 #extension GL_ARB_separate_shader_objects : enable void main() { if(gl_VertexIndex == 0) { gl_Position = vec4(0.0, -0.5, 0.0, 1.0); } else if(gl_VertexIndex == 1) { gl_Position = vec4(0.5, 0.5, 0.0, 1.0); } else if(gl_VertexIndex == 2) { gl_Position = vec4(-0.5, 0.5, 0.0, 1.0); } }
#version 450 #extension GL_ARB_separate_shader_objects : enable layout(location = 0) out vec4 outColor; void main() { outColor = vec4(1.0, 0.0, 0.0, 1.0); }