この節では前節までで描画した(はず)の画像データを実際にファイルに書き出して見てみます。
画像ファイルを簡単に扱うために、stbと呼ばれる簡単に使えるヘッダライブラリを利用します。
まずはこちらからstb_image_write.h
というヘッダファイルをダウンロードし、ソースファイルと同じ場所に配置してください。配置出来たら、メインのソースファイルで以下のようにインクルードとシンボル定義を書きます。
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
これで利用する準備は完了です。
stbライブラリを使うと簡単に画像ファイルを書き出すことができます。今回はbmpファイルで書き出してみましょう。stbi_write_bmp
関数を使って行えます。
stbi_write_bmp("img.bmp", screenWidth, screenHeight, 4, /* 画像データへのポインタ */)
引数はそれぞれファイル名、幅、高さ、1画素当たりのバイト数、データです。第4引数が4のとき、それぞれの画素はRGBAをこの順でそれぞれ1バイト(符号なし8ビット)で表します。(ここはVulkanとは関係ない部分なので端折ります)
さて、画像のデータを取り出したいところですが、どこにあるのでしょう。3-2. メモリの確保で書いた内容を復習してください。データはメインメモリではなくGPUの中にあります。つまりこのままではアプリケーションからアクセスできません。そこでアプリケーション側のアドレス空間上にマップして、メインのプログラムから見れるようにする必要があります。そしてそれにはちょっとした段取りが必要です。
「GPU上のメモリには種類があり、適切に選択する必要がある」と書いたのを覚えていますでしょうか。実はGPU上のメモリには、GPUを操作しているPCの側(ホストと呼びます)からアクセスできるものとアクセスできないものがあります。そこで、メモリを確保するところで「目的のイメージにバインドできるメモリ」であるだけでなく「ホスト側から見えるメモリ」であるものを選択する必要があります。
具体的には以下のようにします。
bool suitableMemoryTypeFound = false;
for (size_t i = 0; i < memProps.memoryTypeCount; i++)
{
if (imgMemReq.memoryTypeBits & (1 << i) &&
(memProps.memoryTypes[i].propertyFlags & vk::MemoryPropertyFlagBits::eHostVisible))
{
imgMemAllocInfo.memoryTypeIndex = i;
suitableMemoryTypeFound = true;
break;
}
}
i番目の種類のメモリタイプのプロパティを見て、HostVisible(ホスト側から見える)であることを確認しています。
これで準備が出来ました。GPUの描画処理が終わった後のところに処理を書いていきましょう。
マッピングはvk::device
のmapMemory
メソッドで行います。
void* imgData = device->mapMemory(imgMem.get(), 0, imgMemReq.size);
第1引数はメモリオブジェクト、第2引数はデバイスメモリのマップしたい場所のオフセット、第3引数はマップするサイズです。第2引数と第3引数を使えば先頭から何バイト目の所から何バイト分の部分だけを扱う、ということもできますが、ここでは全部見たいのでこのようにしています。戻り値はマップされた場所のアドレスです。このimgData
にアクセスすれば画像の生データにアクセスできます。
早速書き出してしまいましょう。
stbi_write_bmp("img.bmp", screenWidth, screenHeight, 4, imgData);
マップしたメモリはきちんと後始末します。
device->unmapMemory(imgMem.get());
さて、無事img.bmp
というファイル名で画像は書き出されましたでしょうか。筆者の環境ではこのようになりました。
やりました!無事赤い三角形が描画出来ています!なんかちょっと背景が紫ですが。
描画した以外の部分は何も触っていないので、そこは透明の扱いになります。透明な場所はVulkanとは関係ないstb側の仕様で紫になるようです。これではちょっと気持ち悪いので、黒で塗ってみましょう。
レンダーパスの設定をすることで描画処理が始まる前に一度アタッチメント全体を特定の色でクリアすることができます。具体的には以下のようにします。
vk::AttachmentDescription attachments[1];
attachments[0].format = vk::Format::eR8G8B8A8Unorm;
attachments[0].samples = vk::SampleCountFlagBits::e1;
attachments[0].loadOp = vk::AttachmentLoadOp::eClear; // 変更
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;
アタッチメントの設定でloadOp
にeClear
を指定します。
vk::ClearValue clearVal[1];
clearVal[0].color.float32[0] = 0.0f;
clearVal[0].color.float32[1] = 0.0f;
clearVal[0].color.float32[2] = 0.0f;
clearVal[0].color.float32[3] = 1.0f;
vk::RenderPassBeginInfo renderpassBeginInfo;
renderpassBeginInfo.renderPass = renderpass.get();
renderpassBeginInfo.framebuffer = frameBuf.get();
renderpassBeginInfo.renderArea = vk::Rect2D({ 0,0 }, { screenWidth, screenHeight });
renderpassBeginInfo.clearValueCount = 1;
renderpassBeginInfo.pClearValues = clearVal;
cmdBufs[0]->beginRenderPass(renderpassBeginInfo, vk::SubpassContents::eInline);
どのような色でクリアするかの設定をレンダーパス開始コマンドの際に行います。ここでは(0.0, 0.0, 0.0, 1.0)、RGBが0でアルファだけ1ということは真っ黒でクリアすることになります。実行してみましょう。
黒と赤になりました。
この節では、前節までで三角形の描画がきちんと出来ていたことをファイルに書き出して確認しました。これにて3章の内容は終わりです。
次章ではウィンドウへの表示をやります。この節のコード
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#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(); j++) {
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) &&
(memProps.memoryTypes[i].propertyFlags & vk::MemoryPropertyFlagBits::eHostVisible)) {
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::eClear;
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("shader.vert.spv");
std::ifstream vertSpvFile("shader.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("shader.frag.spv");
std::ifstream fragSpvFile("shader.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).value;
vk::ImageViewCreateInfo imgViewCreateInfo;
imgViewCreateInfo.image = image.get();
imgViewCreateInfo.viewType = vk::ImageViewType::e2D;
imgViewCreateInfo.format = vk::Format::eR8G8B8A8Unorm;
imgViewCreateInfo.components.r = vk::ComponentSwizzle::eIdentity;
imgViewCreateInfo.components.g = vk::ComponentSwizzle::eIdentity;
imgViewCreateInfo.components.b = vk::ComponentSwizzle::eIdentity;
imgViewCreateInfo.components.a = vk::ComponentSwizzle::eIdentity;
imgViewCreateInfo.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
imgViewCreateInfo.subresourceRange.baseMipLevel = 0;
imgViewCreateInfo.subresourceRange.levelCount = 1;
imgViewCreateInfo.subresourceRange.baseArrayLayer = 0;
imgViewCreateInfo.subresourceRange.layerCount = 1;
vk::UniqueImageView imgView = device->createImageViewUnique(imgViewCreateInfo);
vk::ImageView frameBufAttachments[1];
frameBufAttachments[0] = imgView.get();
vk::FramebufferCreateInfo frameBufCreateInfo;
frameBufCreateInfo.width = screenWidth;
frameBufCreateInfo.height = screenHeight;
frameBufCreateInfo.layers = 1;
frameBufCreateInfo.renderPass = renderpass.get();
frameBufCreateInfo.attachmentCount = 1;
frameBufCreateInfo.pAttachments = frameBufAttachments;
vk::UniqueFramebuffer frameBuf = device->createFramebufferUnique(frameBufCreateInfo);
vk::CommandBufferBeginInfo cmdBeginInfo;
cmdBufs[0]->begin(cmdBeginInfo);
vk::ClearValue clearVal[1];
clearVal[0].color.float32[0] = 0.0f;
clearVal[0].color.float32[1] = 0.0f;
clearVal[0].color.float32[2] = 0.0f;
clearVal[0].color.float32[3] = 1.0f;
vk::RenderPassBeginInfo renderpassBeginInfo;
renderpassBeginInfo.renderPass = renderpass.get();
renderpassBeginInfo.framebuffer = frameBuf.get();
renderpassBeginInfo.renderArea = vk::Rect2D({ 0,0 }, { screenWidth, screenHeight });
renderpassBeginInfo.clearValueCount = 1;
renderpassBeginInfo.pClearValues = clearVal;
cmdBufs[0]->beginRenderPass(renderpassBeginInfo, vk::SubpassContents::eInline);
cmdBufs[0]->bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline.get());
cmdBufs[0]->draw(3, 1, 0, 0);
cmdBufs[0]->endRenderPass();
cmdBufs[0]->end();
vk::CommandBuffer submitCmdBuf[1] = { cmdBufs[0].get() };
vk::SubmitInfo submitInfo;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = submitCmdBuf;
graphicsQueue.submit({ submitInfo }, nullptr);
graphicsQueue.waitIdle();
void* imgData = device->mapMemory(imgMem.get(), 0, imgMemReq.size);
stbi_write_bmp("img.bmp", screenWidth, screenHeight, 4, imgData);
device->unmapMemory(imgMem.get());
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);
}
cmake_minimum_required(VERSION 3.22)
project(vulkan-test)
set(CMAKE_CXX_STANDARD 17)
add_executable(app main.cpp)
find_package(Vulkan REQUIRED)
target_include_directories(app PRIVATE ${Vulkan_INCLUDE_DIRS})
target_link_libraries(app PRIVATE ${Vulkan_LIBRARIES})