やっていくVulkan入門

3-5. シェーダ

今回扱うのはパイプラインの中の主役と言っても過言ではないシェーダです。

元々の語義としては「陰影処理を行うもの」ですが、実態としてはなんでもできます。前節でパイプラインとは「点の集まりで出来た図形を色のついたピクセルの集合に変換するもの」として紹介しましたが、シェーダはそのパイプラインの中において、あの点とこの点をあっちに動かしたりこっちに動かしたり、あのピクセルこのピクセルを赤に塗ったり青に塗ったりする役割を果たします。

昔はシェーダも簡素なことしかせず、文字通りただの「陰影を付けるもの」であり、その処理内容もほぼ固定だったらしいのですが、今は自由にシェーダプログラムを書いてGPUに実行させることができます。空間をねじ曲げ、複雑な模様を描き、物や何かをべらぼうに増やすことさえもできます。

何でもできるようになった代わり、お決まりのような普通の処理しかしたくない場合でもそれをちゃんと書かなければなりません。デフォルト動作に任せるという選択肢は存在しないのです。自由の刑か何か?

冗談はさておき、我々はシェーダプログラムを書く必要があります。シェーダに使う言語ですが、GLSLというC言語をベースとした専用言語を使います。GLSLで書いたプログラムは、専用のソフトでSPIR-Vという中間言語にコンパイルします。Vulkanからはそれを読み込んで実行することになります。ちなみにSPIR-Vの仕様はGLSLとは独立しており、実際にはGLSL以外の言語(DirectXで使われるHLSLとか)からコンパイルすることもできます。今回は普通にGLSLからコンパイルするのですが。

シェーダには種類があります。その中で最低限必要なものとして、今回は「頂点シェーダ(バーテックスシェーダ)」と「フラグメントシェーダ」を作ります。

フラグメントシェーダは「ピクセルシェーダ」とも呼ばれるようです。

まずは頂点シェーダのプログラムから見ていきましょう。頂点シェーダは頂点1つごとに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);
    }
}

基本はC言語なので読むのに不自由はしないでしょう。 gl_Position という変数に代入することで座標を出力できます。

出力先は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なので全て真っ赤になります。これを例えば(0.0, 1.0, 0.0, 1.0)などにすると全て緑になります。

それぞれのソースコードをshader.vert,shader.fragというファイル名で保存します。これをSPIR-Vにコンパイルします。コンパイルするには、Vulkan SDKに付属のglslcというツールを使います。VulkanインストールディレクトリのBinディレクトリ下にあるはずです。パスを通すなどして以下のコマンドを実行しましょう。

glslc shader.vert -o shader.vert.spv
glslc shader.frag -o shader.frag.spv

-oオプションは出力ファイル名の指定です。これでshader.vert.spvshader.frag.spvというそれぞれのSPIR-V形式のシェーダファイルが出来ました。では、これらの生成されたファイルをメインの方のプログラムから読み込みましょう。

ファイルのサイズと中身のバイナリデータさえ取れればいいので、その方法はC言語のファイルポインタだろうがWindowsAPIだろうがシステムコールだろうが何でも良いのですが、ここではC++の標準ライブラリを使用します。

インクルードするヘッダを追加しましょう。

#include <fstream>
#include <filesystem>

注: std::filesystemはC++17からの機能です。古いコンパイラではC++14などがデフォルトになっており、きちんと設定しないと使えないことがあるのでご注意ください。

シェーダファイルを読み込みます。まずは頂点シェーダから。

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::DevicecreateShaderModuleメソッドで作成できます。

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("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::PipelineShaderStageCreateInfopName"main"という文字列を入れていますが、これは「このシェーダはmain関数から始める」という意味です。

これでパイプラインにシェーダが追加できました。おめでとうございます。


この節ではシェーダの追加をやりました。次節ではイメージビューの作成をやります。

この節のコード
#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)) {
            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("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::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);
}
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})