Vulkan で正しい 3D 遮蔽のためにデプスアタッチメントを追加する方法

Example: VulkanExamples/p11-depth

シリーズの Post 11(1:サーフェス、 2:三角形、 3:頂点バッファ、 4:プッシュ定数、 5:テクスチャ、 6:コンピュート+SSBO、 7:コンピュート→イメージ、 8:frames in flight、 9:インデックスバッファ、 10:ユニフォームバッファ+MVP)。 これまでに描いたものはすべてペインターズアルゴリズムだった: 描画順が可視性を決めていた。本物の 3D には デプス アタッチメント が必要——ハードウェアが比較するピクセル毎の 深度を保持する第2のレンダーターゲット。

証明するために、デモは近い quad を先に、遠い quad を後に 描く。デプステストがなければ遠い quad が重なり部分を上書き する——eLess テストでは近い quad のピクセルが生き残る。

最初に描かれた近い quad(赤)が遠い quad(青)との重なりで勝つ

仕組み

4つの部品が一致しなければならない——どれかを逃すと静かに デプステストが効かなくなる:

  1. イメージ。 eD32Sfloat、usage は eDepthStencilAttachment、一度 eDepthStencilAttachmentOptimal に遷移(eDepth アスペクト、dst ステージ eEarlyFragmentTests のバリア)。
  2. レンダリング情報。 動的レンダリングでは、デプス アタッチメントは pDepthAttachment として渡される2つ目の VkRenderingAttachmentInfo——1.0f にクリア(ファー プレーン;Vulkan の深度範囲は [0,1]、0 = ニア)。
  3. パイプライン。 VkPipelineDepthStencilStateCreateInfo で depthTestEnable + depthWriteEnable + eLess——さらに VkPipelineRenderingCreateInfo の setDepthAttachmentFormat() で、パイプラインがテスト対象のフォーマットを知る。
  4. シェーダが gl_Position.z を書く。 深度は頂点シェーダの 出力から来る——ここでは描画毎にプッシュ(z=0.3 近い、 z=0.7 遠い)。

比較演算子チートシート

op通過条件用途
eLessnew.z < stored.zclear=1.0 の後の標準
eLessOrEqual<=再描画での z-fighting 回避
eGreaternew.z > stored.z反転深度セットアップ(clear=0)

ビルド&実行

build.sh
g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc
./app                    # windowed
./app --headless out.ppm # offscreen -> PPM
python3 check.py out.ppm

チェック

重なり帯 x=[256..384] は両方の quad に属する。青い(遠い) quad は赤い(近い)quad の後に描かれる——つまりデプス テストが遠いフラグメントを拒否した場合に限り、重なりは赤い はず。それをスクリプトが正確にアサートする。

check.py
#!/usr/bin/env python3
# SPDX-License-Identifier: CC0-1.0
# check.py — デプスバッファが機能したことを証明。
# NEAR の赤い quad が x=[128..384] で最初に描かれ、FAR の青い
# quad が x=[256..512] で2番目に描かれる。重なり x=[256..384] は
# 赤のままでなければならない——デプステストがなければ後に描か
# れた青が表示される。
import sys

W, H = 640, 480
BG = (13, 13, 20)


def read_ppm(path):
    d = open(path, "rb").read()
    hdr, data = d.split(b"255\n", 1)
    toks = hdr.split()
    assert toks[0] == b"P6"
    w, h = int(toks[1]), int(toks[2])
    assert len(data) == w * h * 3
    return w, h, data


def px(d, x, y):
    i = (y * W + x) * 3
    return tuple(d[i:i + 3])


def main():
    w, h, d = read_ppm(sys.argv[1] if len(sys.argv) > 1 else "out.ppm")
    assert (w, h) == (W, H)

    def is_red(p): return p[0] > 150 and p[1] < 100
    def is_blue(p): return p[2] > 150 and p[0] < 100

    # 純粋ゾーン
    assert is_red(px(d, 160, 240)), "left quad missing (red)"
    assert is_blue(px(d, 460, 240)), "right quad missing (blue)"
    # デプスアサーション本体:FAR quad が最後に描いた重なりが
    # それでも NEAR quad の色を表示
    for x in (280, 320, 360):
        assert is_red(px(d, x, 240)), \
            f"overlap at x={x} shows {px(d, x, 240)} not near-red"
    # すべての外側:背景
    for x in (60, 580):
        assert px(d, x, 240) == BG, f"x={x} not background"
    print("OK: near quad wins the overlap although drawn FIRST")


if __name__ == "__main__":
    main()
output.txt
$ ./app --headless out.ppm && python3 check.py out.ppm
device: AMD Radeon RX 7800 XT (RADV NAVI32)
wrote out.ppm
OK: near quad wins the overlap although drawn FIRST

完全なソース

main.cpp
// SPDX-License-Identifier: CC0-1.0
//
// Post 11: デプステスト。2つの重なり合う quad——NEAR が最初に
// 描かれ、FAR が2番目。デプスアタッチメントがなければ遠い
// quad が誤って近い quad の上を塗る;eD32Sfloat +
// depthStencilState で近い quad が重なりに勝つ。
//
//   ./app                 windowed
//   ./app --headless o.ppm     1フレーム -> check.py 用 PPM
//
// Build:  g++ -std=c++23 -O2 main.cpp -o app -lvulkan -lglfw -lshaderc

#include "vkmini.hpp"

#include <GLFW/glfw3.h>

struct Vertex {
    float pos[2];
};
static const Vertex kQuad[] = {
    {{-0.4f, -0.4f}}, {{0.4f, -0.4f}}, {{0.4f, 0.4f}},
    {{0.4f, 0.4f}},  {{-0.4f, 0.4f}},  {{-0.4f, -0.4f}},
};

struct Push {
    float ofs[2];
    float z;
    float _pad;
    float col[3];
    float _pad2;
};
static_assert(sizeof(Push) == 32);

static const char* kVert = R"GLSL(
#version 460
layout(location = 0) in vec2 inPos;
layout(location = 0) out vec3 vColor;
layout(push_constant) uniform Push {
    vec2 ofs;
    float z;
    vec3 col;
} pc;
void main() {
    gl_Position = vec4(inPos + pc.ofs, pc.z, 1.0);
    vColor = pc.col;
}
)GLSL";

static const char* kFrag = R"GLSL(
#version 460
layout(location = 0) in vec3 vColor;
layout(location = 0) out vec4 outColor;
void main() { outColor = vec4(vColor, 1.0); }
)GLSL";

static vkmini::Vk::Buffer makeVertexBuffer(vkmini::Vk& vk) {
    vk::DeviceSize size = sizeof(kQuad);
    auto staging = vk.createBuffer(
        size, vk::BufferUsageFlagBits::eTransferSrc,
        vk::MemoryPropertyFlagBits::eHostVisible |
            vk::MemoryPropertyFlagBits::eHostCoherent,
        true);
    memcpy(staging.mapped, kQuad, size);
    auto buf = vk.createBuffer(
        size,
        vk::BufferUsageFlagBits::eVertexBuffer |
            vk::BufferUsageFlagBits::eTransferDst,
        vk::MemoryPropertyFlagBits::eDeviceLocal);
    vk.oneTime([&](vk::CommandBuffer c) {
        vk::BufferCopy r{0, 0, size};
        c.copyBuffer(staging.buf.get(), buf.buf.get(), r);
        vk::BufferMemoryBarrier b{};
        b.setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
            .setSrcAccessMask(vk::AccessFlagBits::eTransferWrite)
            .setDstAccessMask(
                vk::AccessFlagBits::eVertexAttributeRead)
            .setBuffer(buf.buf.get())
            .setOffset(0)
            .setSize(size);
        c.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
                          vk::PipelineStageFlagBits::eVertexInput,
                          {}, {}, b, {});
    });
    return buf;
}

/// デプスアタッチメント = カラーと並んで描かれる2つ目のイメージ。
/// 独自の usage、フォーマット、レイアウト
///(eDepthStencilAttachmentOptimal)と独自のバリアが必要——
/// さらにパイプラインのデプスステート。
struct Depth {
    vkmini::Vk::Image img;
};

static Depth makeDepth(vkmini::Vk& vk, uint32_t w, uint32_t h) {
    Depth o;
    o.img = vk.createImage(
        w, h, vk::Format::eD32Sfloat,
        vk::ImageUsageFlagBits::eDepthStencilAttachment);
    return o;
}

struct Pipe {
    vk::UniquePipelineLayout layout;
    vk::UniquePipeline pipeline;
};

/// パイプラインに depthStencil ステートを追加——レンダーターゲット
/// にデプスアタッチメントを含めるなら必須。
static Pipe makePipeline(vkmini::Vk& vk, vk::Format colorFmt,
                         vk::Format depthFmt) {
    Pipe o;
    auto vs = vk.shader(kVert, shaderc_vertex_shader, "q.vert");
    auto fs = vk.shader(kFrag, shaderc_fragment_shader, "q.frag");
    vk::PipelineShaderStageCreateInfo stages[2];
    stages[0].setStage(vk::ShaderStageFlagBits::eVertex)
        .setModule(vs.get())
        .setPName("main");
    stages[1].setStage(vk::ShaderStageFlagBits::eFragment)
        .setModule(fs.get())
        .setPName("main");
    vk::VertexInputBindingDescription binding{};
    binding.setBinding(0)
        .setStride(sizeof(Vertex))
        .setInputRate(vk::VertexInputRate::eVertex);
    std::array attrs{
        vk::VertexInputAttributeDescription{}
            .setLocation(0)
            .setBinding(0)
            .setFormat(vk::Format::eR32G32Sfloat)
            .setOffset(0),
    };
    vk::PipelineVertexInputStateCreateInfo vin{};
    vin.setVertexBindingDescriptions(binding)
        .setVertexAttributeDescriptions(attrs);
    vk::PipelineInputAssemblyStateCreateInfo ia{};
    ia.setTopology(vk::PrimitiveTopology::eTriangleList);
    vk::PipelineViewportStateCreateInfo vp{};
    vp.setViewportCount(1).setScissorCount(1);
    std::array dynStates{vk::DynamicState::eViewport,
                         vk::DynamicState::eScissor};
    vk::PipelineDynamicStateCreateInfo dyn{};
    dyn.setDynamicStates(dynStates);
    vk::PipelineRasterizationStateCreateInfo rs{};
    rs.setPolygonMode(vk::PolygonMode::eFill)
        .setCullMode(vk::CullModeFlagBits::eNone)
        .setFrontFace(vk::FrontFace::eCounterClockwise)
        .setLineWidth(1.0f);
    vk::PipelineMultisampleStateCreateInfo ms{};
    ms.setRasterizationSamples(vk::SampleCountFlagBits::e1);
    // 新しいステート本体:深度をテストして書き込む、最も近いもの
    // が勝つ(小さい z)
    vk::PipelineDepthStencilStateCreateInfo ds{};
    ds.setDepthTestEnable(true)
        .setDepthWriteEnable(true)
        .setDepthCompareOp(vk::CompareOp::eLess);
    vk::PipelineColorBlendAttachmentState cb{};
    cb.setColorWriteMask(
        vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG |
        vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA);
    vk::PipelineColorBlendStateCreateInfo blend{};
    blend.setAttachments(cb);
    vk::PushConstantRange pc{};
    pc.setStageFlags(vk::ShaderStageFlagBits::eVertex)
        .setOffset(0)
        .setSize(sizeof(Push));
    vk::PipelineLayoutCreateInfo li{};
    li.setPushConstantRanges(pc);
    o.layout = vk.device->createPipelineLayoutUnique(li);
    vk::PipelineRenderingCreateInfo rendering{};
    rendering.setColorAttachmentFormats(colorFmt)
        .setDepthAttachmentFormat(depthFmt); // 深度フォーマット宣言
    vk::GraphicsPipelineCreateInfo gi{};
    gi.setStages(stages)
        .setPVertexInputState(&vin)
        .setPInputAssemblyState(&ia)
        .setPViewportState(&vp)
        .setPDynamicState(&dyn)
        .setPRasterizationState(&rs)
        .setPMultisampleState(&ms)
        .setPDepthStencilState(&ds) // 接続
        .setPColorBlendState(&blend)
        .setLayout(o.layout.get())
        .setPNext(&rendering);
    auto res = vk.device->createGraphicsPipelineUnique({}, gi);
    if (res.result != vk::Result::eSuccess)
        throw std::runtime_error("pipeline failed");
    o.pipeline = std::move(res.value);
    return o;
}

/// シーン全体を記録:デプスバリア + デプスアタッチメント付き
/// レンダリング + 両方の quad(意図的に NEAR 先、FAR 後)。
static void record(vk::CommandBuffer c, vk::ImageView colorView,
                   vk::ImageView depthView, vk::Extent2D extent,
                   const Pipe& p, vk::Buffer vbuf) {
    vk::RenderingAttachmentInfo color{};
    color.setImageView(colorView)
        .setImageLayout(vk::ImageLayout::eColorAttachmentOptimal)
        .setLoadOp(vk::AttachmentLoadOp::eClear)
        .setStoreOp(vk::AttachmentStoreOp::eStore)
        .setClearValue(vk::ClearValue{
            vk::ClearColorValue{std::array{0.05f, 0.05f, 0.08f,
                                           1.f}}});
    // デプスアタッチメントがレンダリング情報に参加——独自の
    // レイアウト、独自のクリア値(1.0 = ファープレーン)。
    vk::RenderingAttachmentInfo depth{};
    depth.setImageView(depthView)
        .setImageLayout(
            vk::ImageLayout::eDepthStencilAttachmentOptimal)
        .setLoadOp(vk::AttachmentLoadOp::eClear)
        .setStoreOp(vk::AttachmentStoreOp::eDontCare)
        .setClearValue(
            vk::ClearValue{vk::ClearDepthStencilValue{1.0f, 0}});
    vk::RenderingInfo ri{};
    ri.setRenderArea({{0, 0}, extent})
        .setLayerCount(1)
        .setColorAttachments(color)
        .setPDepthAttachment(&depth);
    c.beginRendering(ri);
    c.bindPipeline(vk::PipelineBindPoint::eGraphics, p.pipeline.get());
    vk::Viewport vp{0, 0, float(extent.width), float(extent.height),
                    0.f, 1.f};
    c.setViewport(0, vp);
    vk::Rect2D sc{{0, 0}, extent};
    c.setScissor(0, sc);
    vk::DeviceSize off = 0;
    c.bindVertexBuffers(0, vbuf, off);
    // NEAR quad が最初(z=0.3、赤、左にシフト)
    Push near_{{-0.2f, 0.f}, 0.3f, 0.f, {0.9f, 0.15f, 0.15f}, 0.f};
    c.pushConstants(p.layout.get(), vk::ShaderStageFlagBits::eVertex,
                    0, sizeof(Push), &near_);
    c.draw(6, 1, 0, 0);
    // FAR quad が2番目(z=0.7、青、右にシフト)——デプステスト
    // なしでは重なりを塗りつぶす
    Push far{{0.2f, 0.f}, 0.7f, 0.f, {0.15f, 0.25f, 0.9f}, 0.f};
    c.pushConstants(p.layout.get(), vk::ShaderStageFlagBits::eVertex,
                    0, sizeof(Push), &far);
    c.draw(6, 1, 0, 0);
    c.endRendering();
}

static void imgBarrier(vk::CommandBuffer c, vk::Image img,
                       vk::ImageAspectFlags aspect, vk::ImageLayout ol,
                       vk::ImageLayout nl, vk::AccessFlags src,
                       vk::AccessFlags dst, vk::PipelineStageFlags ss,
                       vk::PipelineStageFlags ds) {
    vk::ImageMemoryBarrier b{};
    b.setOldLayout(ol)
        .setNewLayout(nl)
        .setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
        .setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
        .setImage(img)
        .setSubresourceRange({aspect, 0, 1, 0, 1})
        .setSrcAccessMask(src)
        .setDstAccessMask(dst);
    c.pipelineBarrier(ss, ds, {}, {}, {}, b);
}

int main(int argc, char** argv) {
    bool headless = argc > 1 && std::string(argv[1]) == "--headless";
    const char* ppmOut = argc > 2 ? argv[2] : "out.ppm";

    vkmini::Vk vk;
    GLFWwindow* win = nullptr;
    vk::UniqueSurfaceKHR surface;
    if (!headless) {
        glfwInit();
        glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
        win = glfwCreateWindow(640, 480, "vkmini p11", nullptr, nullptr);
    }
    std::vector<const char*> instExts;
    if (!headless) {
        uint32_t n = 0;
        const char** e = glfwGetRequiredInstanceExtensions(&n);
        instExts.assign(e, e + n);
    }
    vk.createInstance(instExts);
    if (!headless) {
        VkSurfaceKHR raw;
        glfwCreateWindowSurface(vk.instance.get(), win, nullptr, &raw);
        surface = vk::UniqueSurfaceKHR(raw, vk.instance.get());
    }
    vk.pickPhysicalDevice();
    vk.createDevice(headless ? std::vector<const char*>{}
                             : std::vector<const char*>{
                                   VK_KHR_SWAPCHAIN_EXTENSION_NAME});
    auto vbuf = makeVertexBuffer(vk);
    auto depth = makeDepth(vk, 640, 480);

    // デプスイメージを一度アタッチメントレイアウトに遷移——
    // フレーム毎にはアタッチメントの loadOp でクリアされる
    vk.oneTime([&](vk::CommandBuffer c) {
        imgBarrier(c, depth.img.img.get(),
                   vk::ImageAspectFlagBits::eDepth,
                   vk::ImageLayout::eUndefined,
                   vk::ImageLayout::eDepthStencilAttachmentOptimal,
                   {},
                   vk::AccessFlagBits::eDepthStencilAttachmentWrite,
                   vk::PipelineStageFlagBits::eTopOfPipe,
                   vk::PipelineStageFlagBits::eEarlyFragmentTests);
    });

    if (headless) {
        vkmini::Headless hl;
        hl.init(vk, 640, 480);
        Pipe p = makePipeline(vk, vk::Format::eR8G8B8A8Unorm,
                              vk::Format::eD32Sfloat);
        hl.render(vk::ImageLayout::eColorAttachmentOptimal,
                  [&](vk::CommandBuffer c) {
                      record(c, hl.color.view.get(),
                             depth.img.view.get(), hl.extent, p,
                             vbuf.buf.get());
                  });
        hl.savePpm(ppmOut);
        printf("wrote %s\n", ppmOut);
        return 0;
    }

    vkmini::Swapchain sc;
    sc.create(vk, vk.phys, surface.get(), 640, 480);
    Pipe p = makePipeline(vk, sc.format, vk::Format::eD32Sfloat);
    auto cmd = vk.device->allocateCommandBuffersUnique(
        {vk.pool.get(), vk::CommandBufferLevel::ePrimary, 1});
    auto acqSem = vk.device->createSemaphoreUnique({});
    auto doneSem = vk.device->createSemaphoreUnique({});
    auto fence = vk.device->createFenceUnique({});

    for (int f = 0; f < 240 && !glfwWindowShouldClose(win); ++f) {
        glfwPollEvents();
        vk.device->resetFences(fence.get());
        auto acq = vk.device->acquireNextImageKHR(
            sc.sc.get(), UINT64_MAX, acqSem.get(), {});
        vk::Image img = sc.images[acq.value];
        cmd[0]->reset();
        cmd[0]->begin(
            {vk::CommandBufferUsageFlagBits::eOneTimeSubmit});
        imgBarrier(cmd[0].get(), img, vk::ImageAspectFlagBits::eColor,
                   vk::ImageLayout::eUndefined,
                   vk::ImageLayout::eColorAttachmentOptimal,
                   vk::AccessFlags{},
                   vk::AccessFlagBits::eColorAttachmentWrite,
                   vk::PipelineStageFlagBits::eTopOfPipe,
                   vk::PipelineStageFlagBits::eColorAttachmentOutput);
        record(cmd[0].get(), sc.views[acq.value].get(),
               depth.img.view.get(), sc.extent, p, vbuf.buf.get());
        imgBarrier(cmd[0].get(), img, vk::ImageAspectFlagBits::eColor,
                   vk::ImageLayout::eColorAttachmentOptimal,
                   vk::ImageLayout::ePresentSrcKHR,
                   vk::AccessFlagBits::eColorAttachmentWrite,
                   vk::AccessFlags{},
                   vk::PipelineStageFlagBits::eColorAttachmentOutput,
                   vk::PipelineStageFlagBits::eBottomOfPipe);
        cmd[0]->end();
        vk::PipelineStageFlags waitStage =
            vk::PipelineStageFlagBits::eColorAttachmentOutput;
        vk::SubmitInfo si{};
        si.setWaitSemaphores(acqSem.get())
            .setWaitDstStageMask(waitStage)
            .setCommandBuffers(cmd[0].get())
            .setSignalSemaphores(doneSem.get());
        vk.queue.submit(si, fence.get());
        vk::PresentInfoKHR pi{};
        pi.setWaitSemaphores(doneSem.get())
            .setSwapchains(sc.sc.get())
            .setImageIndices(acq.value);
        (void)vk.queue.presentKHR(pi);
        (void)vk.device->waitForFences(fence.get(), VK_TRUE,
                                     UINT64_MAX);
    }
    vk.queue.waitIdle();
    sc.views.clear();
    sc.sc.reset();
    surface.reset();
    glfwDestroyWindow(win);
    glfwTerminate();
    return 0;
}

All source code in this series is released under CC0 1.0 Universal (CC0-1.0) — public domain dedication, no attribution required.

Next in the series: the window-management detail everyone hits — swapchain recreation on resize (VK_ERROR_OUT_OF_DATE_KHR).


同じカテゴリの類似記事をチェック: C++ Vulkan