[vk] Depth buffering

2021. 9. 4. 09:33그래픽스/vk

1. Introduction

- geometry는 3D로 투영되지만, 여전히 평면이다.

- 이번 챕터에서는 3D meshes을 위한 position에 z 좌표를 추가할것이다. 

- 이 3번째 좌표를 사용함으로써 현재 사각형 위에 사각형을 하나 더 배치하여

- geometry가 깊이별로 정렬되지 않을 때 발생하는 문제를 확인할 것이다.

 

 

 

2. 3D geometry

- Vertex 구조체에서 position을 3D vector를 사용하도록 수정할것이다.

- 그리고 VkVertexInputAttributeDescription 에 대응되는 format 또한 업데이트 시킬것이다.

struct Vertex {
    glm::vec3 pos;
    glm::vec3 color;
    glm::vec2 texCoord;

    ...

    static std::array<VkVertexInputAttributeDescription, 3> getAttributeDescriptions() {
        std::array<VkVertexInputAttributeDescription, 3> attributeDescriptions{};

        attributeDescriptions[0].binding = 0;
        attributeDescriptions[0].location = 0;
        attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
        attributeDescriptions[0].offset = offsetof(Vertex, pos);

        ...
    }
};

- 다음은 vertex shader 에서 3D 좌표를 입력 받을 수 있게 수정해야한다.

- 다시 컴파일 해주는것을 잊지 말자!

layout(location = 0) in vec3 inPosition;

...

void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
    fragColor = inColor;
    fragTexCoord = inTexCoord;
}

- 그리고 마지막으로 vertices 또한 Z축을 추가해주자

const std::vector<Vertex> vertices = {
    {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}},
    {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}},
    {{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
    {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}}
};

- 지금 응용프로그램을 실행해보면, 정확하게 이전과 같은 결과가 나올것이다.

- 이제 도형을 추가하여 scene을 좀더 흥미롭게 만들 것이며,

- 이 챕터에서 다룰 문제점을 보여줄것이다.

 

- vertices를 복사하고, z축을 -0.5로 설정하여 밑의 그림같이 바로 아래에 둘것이다.

const std::vector<Vertex> vertices = {
    {{-0.5f, -0.5f, 0.0f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}},
    {{0.5f, -0.5f, 0.0f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}},
    {{0.5f, 0.5f, 0.0f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
    {{-0.5f, 0.5f, 0.0f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}},

    {{-0.5f, -0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {0.0f, 0.0f}},
    {{0.5f, -0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {1.0f, 0.0f}},
    {{0.5f, 0.5f, -0.5f}, {0.0f, 0.0f, 1.0f}, {1.0f, 1.0f}},
    {{-0.5f, 0.5f, -0.5f}, {1.0f, 1.0f, 1.0f}, {0.0f, 1.0f}}
};

const std::vector<uint16_t> indices = {
    0, 1, 2, 2, 3, 0,
    4, 5, 6, 6, 7, 4
};

- 이제 프로그램을 실행해보면 아래와 같이 Escher 의 그림과 닮은 형태를 볼 수 있을것이다.

 

- 아래 그림의 frgments이 위의 그림의 fragments을 덮어쓰는 이러한 문제는

- 간단한 이유로 index array 순서에의해 나중에 그려지기 때문에 일어난다.

- 두가지 해결방법이 있다.

  • Sort all of the draw calls by depth from back to front
  • Use depth testing with a depth buffer

- 첫번째 접근방법은 transparent objects 를 그릴때 사용하는 일반적인 방식이다.

- - - - OIT(Order Independent Transparency, 주문 독립적 투명성)은 해결하기 힘든 문제이기 때문

- 하지만 깊이에의해 발생하는 ordering fragments는 depth buffer를 사용함으로써 일반적으로 해결할 수 있다. 

 

 

 

 

2.1 Depth buffer

 

- depth buffer

- - - - color attachment와 같은 attachment로, 모든 position에 대한 depth를 저장한다.

- - - - 매번 rasterizer가 fragment를 생성할때,

- - - - depth test는 새로운 fragment가 이전 fragment 보다 더 가까운지 체크한다.

- - - - 만일 새로운 fragment가 더 멀다면, 폐기(discard)한다.

- - - - depth test를 통과한 fragment는 자체 깊이를 깊이 버퍼에 덮어쓴다.

- - - - 색상 출력을 조작할 수 있는것처럼 fragment shader에서 이값을 조작할 수 있다.

#define GLM_FORCE_RADIANS
#define GLM_FORCE_DEPTH_ZERO_TO_ONE
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>

- GLM에의해 생성되어지는 perspective projection matrix는 OpenGL의 depth 범위인 -1.01.0 를 사용한다.

- 이러한 값들을 Vulkan의 범위에 맞게 0.0 ~ 1.0 으로 재설정해줘야한다.

- 이는 GLM_FORCE_DEPTH_ZERO_TO_ONE 를 glm 헤더파일위에 정의해줌으로써 설정해줄 수 있다.  

 

 

3. Depth image and view

- color attachment와 같게 depth attachment는 image를 베이스로 삼는다.

- 차이점은 swapchain이 자동으로 만들어주지 않는다는 것이다.

- 우리는 single depth image만 필요하므로 (한번에 하나의 draw 연산만 실행됨 only one draw operation)

- depth image는 또한 image, memory, image view의 삼박자(trifecta)를 맞출 필요가 있다.

VkImage depthImage;
VkDeviceMemory depthImageMemory;
VkImageView depthImageView;

- 이제 새로운 함수 createDepthResources 를 생성하고 리소스들을 셋업할것이다.

void initVulkan() {
    ...
    createCommandPool();
    createDepthResources();
    createTextureImage();
    ...
}

...

void createDepthResources() {

}

- depth image를 생성하는것은 아주 간단하다

- swapchain의 extent로 정의된 color attachment의 해상도와 같아야하며,

- depth attachment에 적합한 image usage를 사용해야한다.

 

 

3.1 format

- depth image를 위한 적절한 format이 무엇인가에 대한 질문만이 유일하다.

- format은 VK_FORMAT_ 애  _D??_ 와같은 depth component를 포함해야한다.

 

- texture image와 같지않게, format을 명시할 필요는없다.

- 우리는 이것에 대해 직접 접근할 필요가 없어 특정한 포맷이 필요하지 않기 때문이다.

- 이것은 그저 적절한 정확도만 있으면되며, 실제 응용프로그램에서는 최소 24비트가 일반적이다.

  • VK_FORMAT_D32_SFLOAT: 32-bit float for depth
  • VK_FORMAT_D32_SFLOAT_S8_UINT: 32-bit signed float for depth and 8 bit stencil component
  • VK_FORMAT_D24_UNORM_S8_UINT: 24-bit float for depth and 8 bit stencil component
- The stencil component는  stencil tests  목적으로 사용되어진다.
- 이것은 depth testing 과 결합되어질 수 있는 추가적인 테스트이다.
- 이것은 차후 챕터에서 다룬다.

- VK_FORMAT_D32_SFLOAT 포맷을 사용할것이다. (가장 일반적인 포맷)

- 유연성을 위해 응용프로그램에 가능한 것을 선택하도록하는것이 좋기 때문에 아래와 같은 함수를 작성한다.

- 그냥 우선순위 제일 높은 포맷을 배열 첫번째에 넣고 지원하는 포맷이면 선택하도록하는 함수이다.

- 찾지못하면 에러를 던져주자.

VkFormat findSupportedFormat(const std::vector<VkFormat>& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) {
    for (VkFormat format : candidates) {
        VkFormatProperties props;
        vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props);

        if (tiling == VK_IMAGE_TILING_LINEAR && (props.linearTilingFeatures & features) == features) {
            return format;
        } else if (tiling == VK_IMAGE_TILING_OPTIMAL && (props.optimalTilingFeatures & features) == features) {
            return format;
        }
    }

    throw std::runtime_error("failed to find supported format!");
}

- vkGetPhysicalDeviceFormatProperties 함수를 통해 쿼리한다.

- VkFormatProperties 는 다음 3가지 필드로 구성된다. 여기서는 첫번째와 두번째 포맷만 의미있다.

  • linearTilingFeatures: Use cases that are supported with linear tiling
  • optimalTilingFeatures: Use cases that are supported with optimal tiling
  • bufferFeatures: Use cases that are supported for buffers

- 이 함수는 다음과 같이 사용한다.

VkFormat findDepthFormat() {
    return findSupportedFormat(
        {VK_FORMAT_D32_SFLOAT, VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT},
        VK_IMAGE_TILING_OPTIMAL,
        VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT
    );
}

- VK_FORMAT_FEATURE_  플래그를 주의하자 (VK_IMAGE_USAGE_ 가 아님) 

- 모든 후보자 포맷들은 depth component 를 포함하고 있으며 뒤의 두개는 stencil component를 포함하고 있다.

- 지금 사용하지 않지만, 레이아웃 전환시 이 포맷들을 고려해야한다.

- 아래의 추가 헬퍼함수를 작성하여 stencil component인 것을 선택했는지 여부를 알 수 있게 하자.

 

bool hasStencilComponent(VkFormat format) {
    return format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT;
}

- 그리고 아래와 같이 이미지를 depth 용도로 만들자

createImage(swapChainExtent.width, swapChainExtent.height, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory);
depthImageView = createImageView(depthImage, depthFormat);

- 여기서 createImageView 함수를 수정해야한다 (하드코딩했기때문)

VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) {
    ...
    viewInfo.subresourceRange.aspectMask = aspectFlags;
    ...
}

- 이제 아래 와같이 view 도 depth 용으로 만들자

swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT);
...
depthImageView = createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT);
...
textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT);

- 이것들은 매핑하거나 복사할 필요가 없다. renderpass가 시작할때 color attachment와 같이 지울것이기 때문

 

( That's it for creating the depth image.

We don't need to map it or copy another image to it,

because we're going to clear it at the start of the render pass like the color attachment.)

 

4. Explicitly transitioning the depth image

- 렌더 패스에서 처리하기 때문에 depth 이미지에 대해 명시적인 레이아웃 전환을 할 필요가 없지만

- 그러나 완전성을 위하여, 프로세스를 설명할것이다. (스킵가능)

 

- transitionImageLayout 을 createDepthResources 끝부분에 추가하자

transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL);

- 여기서 이 파라미터에 대응하도록 transitionImageLayout  함수를 아래와 같이 수정해야한다.

if (newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;

    if (hasStencilComponent(format)) {
        barrier.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT;
    }
} else {
    barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
}

- 지금 stencil component를 사용하지 않지만, depth image의 layout transitions에 포함시킬 필요가 있다.

- Finally, add the correct access masks and pipeline stages:

if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
    barrier.srcAccessMask = 0;
    barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

    sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
    destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
    barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
    barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

    sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
} else if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL) {
    barrier.srcAccessMask = 0;
    barrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;

    sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
    destinationStage = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
} else {
    throw std::invalid_argument("unsupported layout transition!");
}

- 만일 fragment가 보이게되면, depth buffer는 depth tests 동안에 그것을 읽을것이다

- 그리고 새로운 fragment 가 그려지게될때 쓰여지게 될것이다.

- reading은 VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT  단계에서 수행되어진다.

- writing 은 VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT 단계에서 수행되어진다.

- 지정된 작업과 일치하는 가장 이른 파이프라인을 선택해야 필요할때 depth attachment로 연결하고, 사용가능하다.

 

 

 

5. Render pass

- 이제 createRenderPass 함수를 수정해야한다. (depth attachment 를 포함하도록)

- 첫번째 VkAttachmentDescription 는 아래와 같이 명시해야한다.

VkAttachmentDescription depthAttachment{};
depthAttachment.format = findDepthFormat();
depthAttachment.samples = VK_SAMPLE_COUNT_1_BIT;
depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
depthAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
depthAttachment.finalLayout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

- format 은 이전에 선택한것과 같아야한다.

- storeOp 를 신경쓰지 않는다고 설정한다. (drawing을 끝나고 신경쓰지 않기 때문)

- 이것은 조금더 하드웨어가 최적화를 수행할 수 있다.

- color attachment와 같게 이전 내용물에 대해선 신경쓰지 않는다.

- 그래서 initial 레이아웃을 VK_IMAGE_LAYOUT_UNDEFINED  로 설정한다.

- subpass 에 아래와 같이 기술해주자

VkSubpassDescription subpass{};
subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS;
subpass.colorAttachmentCount = 1;
subpass.pColorAttachments = &colorAttachmentRef;
subpass.pDepthStencilAttachment = &depthAttachmentRef;

- color attachments 과 같지않게, 한 서브패스는 오직 하나의 depth attachment 만 사용할 수 있다.(stencil 도 하나만)

- 여러가지 버퍼에서 깊이 테스트를 수행하는것은 의미가 없다.

std::array<VkAttachmentDescription, 2> attachments = {colorAttachment, depthAttachment};
VkRenderPassCreateInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
renderPassInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
renderPassInfo.pAttachments = attachments.data();
renderPassInfo.subpassCount = 1;
renderPassInfo.pSubpasses = &subpass;
renderPassInfo.dependencyCount = 1;
renderPassInfo.pDependencies = &dependency;

- 다음은  VkRenderPassCreateInfo 를 위와 같이 수정하는것이다.

dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT;

- 마지막으로 우리는 우리의 서브패스 의존성을 위와같이

- depth image를 전환할때 충돌이 일어나지 않도록 할 필요가 있다.

- depth image는 첫번째로 fragment test pipeline stage에 접근할것이다.

- 그리고 load 연산을 clear로 했기 때문에 우리는 access mask 를 write에 대한것으로명시해야한다.

 

6. Framebuffer

- 다음 단계는 프레임 버퍼를 수정하는것이다.

- depth image를 depth attachment에 바인딩하도록해야한다.

- createFramebuffers 로가서, depth image view를 두번째 attachment로 넣어주자.

std::array<VkImageView, 2> attachments = {
    swapChainImageViews[i],
    depthImageView
};

VkFramebufferCreateInfo framebufferInfo{};
framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO;
framebufferInfo.renderPass = renderPass;
framebufferInfo.attachmentCount = static_cast<uint32_t>(attachments.size());
framebufferInfo.pAttachments = attachments.data();
framebufferInfo.width = swapChainExtent.width;
framebufferInfo.height = swapChainExtent.height;
framebufferInfo.layers = 1;

- 모든 이미지에 대한 color attachment는 다른 view가 들어간다,

- 하지만 depth 이미지는 우리의 세마포어 덕분에 하나의 subpass만 실행되므로

- 하나의 이미지뷰만 사용할 수 있다.

 

- 또한 createFramebuffers  를 순서에 맞게 호출해주자.

void initVulkan() {
    ...
    createDepthResources();
    createFramebuffers();
    ...
}

 

 

7. Clear values

- 우리는 이제 VK_ATTACHMENT_LOAD_OP_CLEAR 로 설정된 다수의 attachments이 있으므로

- clear value를 수정할 필요가 있다. 아래와 같이 수정한다.  

std::array<VkClearValue, 2> clearValues{};
clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}};
clearValues[1].depthStencil = {1.0f, 0};

renderPassInfo.clearValueCount = static_cast<uint32_t>(clearValues.size());
renderPassInfo.pClearValues = clearValues.data();

- depth buffer의 depth 범위는 vulkan에서 0.0~1.0이다.

- 여기서 1.0 은 far view plan, 0.0은 near viw plan 이다.

- 각 포인트의 초기값은 1.0 으로 가능한 멀리 있어야한다.

- clearValues의 순서는 attachments 과 동일해야한다.

 

8. Depth and stencil state

- depth attachment는 이제 사용될 준비가 되었다.

- 이제 pipeline에서 활성화해줘야한다.

VkPipelineDepthStencilStateCreateInfo  구조체를 기술해야한다.

VkPipelineDepthStencilStateCreateInfo depthStencil{};
depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO;
depthStencil.depthTestEnable = VK_TRUE;
depthStencil.depthWriteEnable = VK_TRUE;

- depthTestEnable 필드는 새 fragments의 깊이를 깊이 버퍼와 비교하여 폐기해야하는지 여부를 지정한다.

- depthWriteEnable 필드는 depth test를 통과한 새로운 depth가 실제 버퍼에 저장해야하는지를 지정한다.

depthStencil.depthCompareOp = VK_COMPARE_OP_LESS;

- depthCompareOp 필드는 유지할것인지 폐기할것인지를 수행하는 비교 연산을 지정한다.

- - - - lower == closer 이어야하므로, 새로운 fragments는 less 해야한다.

depthStencil.depthBoundsTestEnable = VK_FALSE;
depthStencil.minDepthBounds = 0.0f; // Optional
depthStencil.maxDepthBounds = 1.0f; // Optional

- 위 세 필드는 선택적인 depth bound test 사용을 활성화 하는지에 대한 여부를 지정한다.

- 기본적으로 이런 기능을 사용하면 지정된 깊이 범위에 속하는 fragments만 유지할 수 있다.

depthStencil.stencilTestEnable = VK_FALSE;
depthStencil.front = {}; // Optional
depthStencil.back = {}; // Optional

- 마지막 3개의 필드는  stencil buffer  의 연산을 지정한다.

- 튜토리얼에서는 사용하지 않으므로 비워두자

- 이 기능을 사용하려면 포맷에 stencil component 이 포함되어야한다.

pipelineInfo.pDepthStencilState = &depthStencil;

- 마지막으로  VkGraphicsPipelineCreateInfo 구조체에 위에서 기술한 depth stencil state 를 지정하자

- 이 경우 파이프라인에서 지정한 render pass에서 depth stencil attachment가 포함되어있어야한다.

 

- 이제 프로그램을 실행해보면 다음과 같을 것이다.

9. Handling window resize

- 깊이버퍼 또한 해상도에 의존하므로 사이즈가 변경될 시 재생성해줘야한다.

void recreateSwapChain() {
    int width = 0, height = 0;
    while (width == 0 || height == 0) {
        glfwGetFramebufferSize(window, &width, &height);
        glfwWaitEvents();
    }

    vkDeviceWaitIdle(device);

    cleanupSwapChain();

    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createDepthResources();
    createFramebuffers();
    createUniformBuffers();
    createDescriptorPool();
    createDescriptorSets();
    createCommandBuffers();
}

 

void cleanupSwapChain() {
    vkDestroyImageView(device, depthImageView, nullptr);
    vkDestroyImage(device, depthImage, nullptr);
    vkFreeMemory(device, depthImageMemory, nullptr);

    ...
}

- Congratulations, your application is now finally ready to render arbitrary 3D geometry and have it look right.

- We're going to try this out in the next chapter by drawing a textured model!

 

C++ code / Vertex shader / Fragment shader

 

https://vulkan-tutorial.com/Depth_buffering