diff --git a/kr/00_Introduction.md b/kr/00_Introduction.md new file mode 100644 index 00000000..a885b72c --- /dev/null +++ b/kr/00_Introduction.md @@ -0,0 +1,77 @@ +## 소개 + +이 튜토리얼은 [Vulkan](https://www.khronos.org/vulkan/) 그래픽스와 계산 API를 사용하는 기본적인 내용을 알려 드립니다. Vulkan은 (OpenGL로 잘 알려진) [Khronos group](https://www.khronos.org/)에서 만든 새로운 API로 최신 그래픽카드에 대한 훨씬 잘 추상화된 API를 제공합니다. 이 새로운 인터페이스는 여러분의 응용 프로그램이 무엇을 하는 것인지를 더 잘 설명하게 해 주고, 이를 통해 기존 [OpenGL](https://en.wikipedia.org/wiki/OpenGL) +및 [Direct3D](https://en.wikipedia.org/wiki/Direct3D)보다 높은 성능과 더 투명한 드라이버의 동작을 보장합니다. Vulkan의 배경이 되는 아이디어는 [Direct3D 12](https://en.wikipedia.org/wiki/Direct3D#Direct3D_12) +나 [Metal]()과 비슷하지만, Vulkan은 완전한 크로스 플랫폼을 보장하여 윈도우즈, 리눅스, 안드로이드에서 모두 동작하는 응용 프로그램을 개발할 수 있게 합니다. + +하지만, 이러한 이점을 활용하기 위해 여러분이 지불해야 할 비용은 훨씬 장황한 API를 다루어야 한다는 것입니다. 응용 프로그램에서 모든 그래픽스 API와 관련된 상세 사항들을 처음부터 설정해야 하는데, 초기 프레임 버퍼 생성이나 버퍼나 텍스처 이미지 객체들을 위한 메모리 관리 시스템을 만드는 것 등입니다. 그래픽 드라이버가 해 주는 일이 적어서 여러분의 응용 프로그램이 제대로 동작하기 위해서는 직접 더 많은 작업을 해 주어야 합니다. + +여기서 말하고자 하는 것은 Vulkan이 모든 사람들을 위해 만들어진 것은 아니라는 점입니다. Vulkan은 고성능 컴퓨터 그래픽스에 관심이 있고, 여기에 시간을 투자할 의지가 있는 프로그래머를 그 대상으로 하고 있습니다. 여러분이 컴퓨터 그래픽스보다는 게임 개발에 더 관심이 있다면, 그냥 OpenGL이나 Direct3D를 계속 사용하는 것이 더 나을 것입니다. Vulkan이 짧은 시간 내에 그 자리를 대체하지는 않을 겁니다. 다른 대안으로는 [Unreal Engine](https://en.wikipedia.org/wiki/Unreal_Engine#Unreal_Engine_4) +이나 [Unity]() 같은 게임 엔진을 사용하는 것입니다. 게임 엔진을 사용하면 훨씬 고수준의 API를 통해 Vulkan을 사용 가능합니다. + +그럼 각설하고, 이 튜토리얼을 위해 준비해야 할 사항들은 다음과 같습니다: + +- Vulkan에 호환되는 그래픽 카드와 드라이버 ([NVIDIA](https://developer.nvidia.com/vulkan-driver), [AMD](http://www.amd.com/en-us/innovations/software-technologies/technologies-gaming/vulkan), [Intel](https://software.intel.com/en-us/blogs/2016/03/14/new-intel-vulkan-beta-1540204404-graphics-driver-for-windows-78110-1540), [Apple Silicon (Or the Apple M1)](https://www.phoronix.com/scan.php?page=news_item&px=Apple-Silicon-Vulkan-MoltenVK)) +- C++ 경험(RAII, initializer lists에 익숙해야 합니다.) +- C++17 기능을 지원하는 컴파일러 (Visual Studio 2017+, GCC 7+, 또는 Clang 5+) +- 3D 컴퓨터 그래픽스 경험 + +이 튜토리얼은 OpenGL이나 Direct3D의 개념에 대한 사전지식을 가정하고 있지는 않지만 3D 컴퓨터 그래픽스에 대한 기본 지식은 필요합니다. 예를 들자면 원근 투영(Perspective projection)에 관한 수학적인 배경 등은 설명하지 않습니다. 컴퓨터 그래픽스 개념에 대한 개념서로 [이 책](https://paroj.github.io/gltut/)을 참고 하십시오. 다른 컴퓨터 그래픽스 관련 자료들은 다음과 같습니다: + +- [Ray tracing in one weekend](https://github.com/RayTracing/raytracing.github.io) +- [Physically Based Rendering book](http://www.pbr-book.org/) +- Vulkan은 오픈 소스 [Quake](https://github.com/Novum/vkQuake)와 [DOOM 3](https://github.com/DustinHLand/vkDOOM3)의 엔진에서도 사용되었습니다. + +원한다면 C++ 대신 C를 사용할 수도 있지만, 그러려면 다른 선형대수 라이브러리를 사용해야 하고 코드 구조를 스스로 설계하셔야 합니다. 우리는 클래스나 RAII 같은 C++ 기능을 로직(logic)과 리소스의 생애주기 관리를 위해 사용할 것입니다. Rust 개발자를 위한 대안으로 이 튜토리얼의 다음과 같은 버전들이 있습니다: [Vulkano based](https://github.com/bwasty/vulkan-tutorial-rs), [Vulkanalia based](https://kylemayes.github.io/vulkanalia). + +다른 프로그래밍 언어를 사용하는 개발자들이 따라오시기 쉽고, 기본 API에 대한 이해를 돕기 위해 Vulkan이 동작하도록 하는 데는 원본 C API를 사용할 것입니다. 하지만 C++ 개발자라면, 새로운 [Vulkan-Hpp](https://github.com/KhronosGroup/Vulkan-Hpp) 바인딩을 사용하시면 특정 종류의 오류를 방지할 수 있고, 몇 가지 지저분한 작업들을 하지 않아도 됩니다. + +## E-book + +이 튜토리얼을 e-book으로 보고 싶으시면 EPUB나 PDF 버전을 받으시면 됩니다: + +- [EPUB](https://vulkan-tutorial.com/resources/vulkan_tutorial_en.epub) +- [PDF](https://vulkan-tutorial.com/resources/vulkan_tutorial_en.pdf) + +## 튜토리얼 구조 + +우리는 Vulkan이 어떻게 동작하는지에 대한 개요와 삼각형을 화면에 그리기 위해 해야 하는 일들을 설명하는 것으로 튜토리얼을 시작할 것입니다. 모든 각각의 상세한 단계들의 목적은 전체적인 그림을 이해하고 나면 좀 더 쉽게 이해가 될 것입니다. 다음으로 [Vulkan SDK](https://lunarg.com/vulkan-sdk/), 선형대수 연산을 위한 [GLM library](http://glm.g-truc.net/), 윈도우 생성을 위한 [GLFW](http://www.glfw.org/)를 포함한 개발 환경을 설정할 것입니다. 이 튜토리얼에서는 Visual Studio에 기반한 윈도우즈에서의 개발 환경, GCC를 활용한 우분투 리눅스에서의 개발 환경 설정을 설명할 것입니다. + +그 이후에는 삼각형을 화면에 그리기 위해 해야 하는 Vulkan 프로그램 기본 구성요소들을 구현해 볼 것입니다. 각 챕터들은 대략적으로 아래와 같은 구조를 따릅니다: + +- 새로운 개념과 그 목적에 대한 소개 +- 그러한 내용을 프로그램으로 작성하기 위해 필요한 API 호출 방법들 +- 해당 기능들을 헬퍼 함수로 만드는 추상화 작업 + +각각의 챕터는 이전 챕터에 이어지는 것으로 쓰여졌지만, 각 챕터는 Vulkan의 특정 기능을 소개하는 개별적인 소개글이라 생각하고 읽으셔도 됩니다. 따라서 이 사이트를 유용한 참조 문서로 생각하셔도 됩니다. Vulkan 함수와 타입에 대한 모든 것들이 명세(specification)와 링크되어 있으니 더 알고 싶으시면 클릭하시면 됩니다. Vulkan은 새로운 API라서 명세 자체에 한계점이 있을 수 있습니다. [이 Khronos repository](https://github.com/KhronosGroup/Vulkan-Docs)에 적극적으로 피드백을 남겨 주세요. + +앞서 이야기 한 것처럼 Vulkan API는 여러분들에게 그래픽스 하드웨어에 대한 최대한의 제어권을 제공하는 장황한 API입니다. 이로 인해 텍스처를 생성하는 기본적인 연산도 여러 단계를 거쳐야만 하고 이러한 작업들을 여러 번 반복해야만 합니다. 따라서 우리는 자체적으로 헬퍼 함수들을 만들어 볼 것입니다. + +각 챕터마다 해당 단계에 해당하는 전체 코드에 대한 링크를 제공할 것입니다. 코드 구조가 잘 이해되지 않거나, 버그가 있거나, 비교를 해 보고 싶다면 참고 하십시오. 모든 코드는 다양한 제조사의 그래픽 카드에서 올바로 동작하는 것을 테스트 한 상태입니다. 각 챕터에는 또한 코멘트 섹션이 있어서 해당 주제에 대한 질문을 남기실 수 있습니다. 여러분의 플랫폼, 드라이버 버전, 소스 코드, 기대하는 동작과 실제 동작을 남겨서 우리가 여러분을 도울 수 있도록 도와 주세요. + +이 튜토리얼은 커뮤니티 활성화를 위한 목적도 있습니다. Vulkan은 아직 신생 API이고 모범 사례들이 아직 확립되지 않았습니다. 튜토리얼이나 사이트 자체에 대한 피드백이 있다면 [GitHub repository](https://github.com/Overv/VulkanTutorial)에 이슈나 풀 리퀘스트(pull request)를 남겨 주세요. 레포지토리를 *watch*하시면 튜토리얼이 업데이트 되면 알림을 받을 수 있습니다. + +여러분의 첫 삼각형을 Vulkan을 사용해서 화면에 그리는 의식을 치르고 나면, 선형 변환, 텍스처, 3D 모델 등을 포함할 수 있도록 프로그램을 확장할 것입니다. + +전에 그래픽스 API를 사용해 본 적이 있다면, 삼각형 하나를 그리기 위해서 여러 단계의 작업이 필요하다는 것을 아실 겁니다. Vulkan에서도 마찬가지인데, 이러한 개별적인 단계들이 이해하기 어렵지 않고 꼭 필요한 작업임을 알게 되실겁니다. 또 명심하여야 할 것은 단순한 삼각형을 한 번 그리기만 하면, 텍스처링된 3D 모델을 그리는 것은 그리 많은 추가 작업이 필요하지는 않다는 것입니다. + +이후 튜토리얼을 따라가다 문제가 있다면, 먼저 FAQ에 동일한 문제가 이미 해결된 적이 있는지부터 확인해 보세요. 그러고 나서도 문제를 해결하지 못했다면, 관련된 챕터의 코멘트 섹션에 편하게 질문을 남겨 주세요. + +미래의 고성능 그래픽스 API에 뛰어들 준비가 되셨나요? [출발해 봅시다!](!kr/Overview) + +## License + +Copyright (C) 2015-2023, Alexander Overvoorde + +The contents are licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/), +unless stated otherwise. By contributing, you agree to license +your contributions to the public under that same license. + +The code listings in the `code` directory in the source repository are licensed +under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/). +By contributing to that directory, you agree to license your contributions to +the public under that same public domain-like license. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. \ No newline at end of file diff --git a/kr/01_Overview.md b/kr/01_Overview.md new file mode 100644 index 00000000..e8984509 --- /dev/null +++ b/kr/01_Overview.md @@ -0,0 +1,121 @@ +이 챕터에서는 Vulkan에 대한 개요와 Vulkan이 해결하고자 하는 문제에 대한 설명부터 시작해 보겠습니다. 그 이후엔 첫 삼각형을 그리기 위해 필요한 재료들을 살펴볼 것입니다. 이를 통해 이후 챕터에서 다루는 내용들에 대한 큰 그림을 이해할 수 있을 것입니다. 마지막에는 Vulkan API의 구조와 일반적인 사용 패턴을 다루는 것으로 마무리 할 것입니다. + +## Vulkan의 탄생 + +기존의 그래픽스 API와 마찬가지로, Vulkan은 [GPUs](https://en.wikipedia.org/wiki/Graphics_processing_unit)에 대한 크로스 플랫폼 추상화를 위해 설계되었습니다. 이러한 API들의 문제점은 그들이 설계된 시기에는 그래픽 하드웨어들이 대부분 고정된 기능 구성으로 제한되어 있었다는 것입니다. 프로그래머는 표준 포맷으로 정점(Vertex) 데이터를 넘겨주어야 했고 조명과 셰이딩 옵션은 API 제조사에서 제공하는 기능을 사용해야 했습니다. + +그래픽 카드의 아키텍처가 발전하면서 점점 더 많은 프로그램가능한(programmable) 기능을 제공하기 시작했습니다. 이러한 새로운 기능은 어떻게든 기존 API와 통합되어야 했습니다. 이에 따라 이상적인 추상화와는 멀어지기 시작했고, 프로그래머의 의도와 현대적인 그래픽스 아키텍처간의 맵핑을 위해 그래픽 드라이버 내부에 대한 어림짐작들이 포함되기 시작했습니다. 이것이 게임의 성능 향상을 위해 많은 드라이버 업데이트가 필요한 이유고, 때때로 인로 인해 많은 성능 향상이 일어나기도 합니다. 드라이버의 복잡성 때문에 응용 프로그램 개발자들은 벤더(vendor) 사이의 불일치를 다루어야 하기도 하는데, 예를 들어 어떤 [셰이더](https://en.wikipedia.org/wiki/Shader) 문법이 받아들여지는지 아닌지와 같은 것입니다. 이러한 새로운 기능 이외에도, 지난 십 년 동안 새로 시작에 편입된 강력한 그래픽 하드웨어를 가진 모바일 기기가 있습니다. 이 모바일 GPU는 전력과 공간 요구사항으로 인해 또다른 아키텍처를 가지게 되었습니다. 한 예로 [tiled rendering](https://en.wikipedia.org/wiki/Tiled_rendering)이 있는데, 이 기능에 대해 프로그래머에게 보다 많은 제어 권한을 부여함으로써 성능이 향상될 수 있었습니다. 이러한 API의 설계 시기에 기인한 또 다른 문제로 멀티쓰레딩의 제약이 있습니다. 이로 인해 CPU 단에서의 병목이 발생하기도 합니다. + +Vulkan은 처음부터 현대적인 그래픽스 아키텍처에 기반한 설계를 통해 이러한 문제를 해결합니다. 보다 장황한 API를 통해 프로그래머가 의도를 명확하게 기술할 수 있게 함으로써 드라이버의 오버헤드(overhead)를 줄이고, 멀티쓰레드로 명령(command)을 병렬적으로 생성 및 제출(submit)할 수 있습니다. 단일 컴파일러, 표준화된 바이트 코드로 변경함으로써 셰이더 컴파일의 불일치 문제도 해결합니다. 마지막으로 그래픽스와 계산 기능을 단일 API로 병합하여 현대 그래픽 카드의 범용 계산(general purpose) 기능을 공식적으로 지원합니다. + +## 삼각형을 그리기 위해 필요한 것들 + +이제 잘 만들어진 Vulkan 프로그램이 삼각형 하나를 그리기 위해 어떤 단계들을 거치는지 알아볼 것입니다. 여기 소개하는 모든 개념들은 다음 챕터들에서 보다 상세히 다룰 것입니다. 지금은 모든 각각의 컴포넌트들의 관계에 대한 큰 그림을 이해하면 됩니다. + +### 1단계 - 인스턴스와 물리 장치(physical device) 선택 + +Vulkan 응용 프로그램은 `VkInstance`를 통해 Vulkan API를 설정함으로써 시작합니다. 인스턴스는 여러분의 응용 프로그램과 사용할 API 확장(extension)을 기술(describe)함으로써 생성됩니다. 인스턴스 생성 이후에는, Vulkan을 지원하는 하드웨어들을 쿼리하고 사용할 하나 이상의 `VkPhysicalDevice`를 선택합니다. 적절한 장치(예를 들어 특정 그래픽 카드)를 선택하기 위해 VRAM 크기라던데 장치의 기능들도 쿼리할 수 있습니다. + +### 2단계 - 논리적 장치(logical device)와 큐 패밀리(queue family) + +사용할 적절한 하드웨어를 선택한 뒤에는, 논리적 장치인 VkDevice를 만들어야 합니다. 이를 통해 좀 더 상세하게 다중 뷰포트 렌더링을 할 것인지, 64 bit float을 사용할지와 같은 상세 VkPhysicalDeviceFeatures를 기술합니다. 또한 어떤 큐 패밀리를 사용할지 명시해야 합니다. 그리기 명령이나 메모리 연산과 같은 대부분의 연산들은 Vulkan을 통해 수행되는데 이는 이러한 작업들을 VkQueue에 제출한 후 비동기적으로 실행됩니다. 큐는 큐 패밀리에 할당되는데, 각 큐 패밀리는 큐에 있는 특정 연산의 집합을 지원합니다. 예를 들어, 그래픽스를 위한 큐 패밀리, 계산을 위한 큐 패밀리, 메모리 전송을 위한 큐 패밀리가 있을 수 있습니다. 큐 패밀리의 가용 여부는 물리적 장치 선택의 구분 기준으로도 활용될 수 있습니다. Vulkan을 지원하지만 그래픽스 연산 기능을 전혀 지원하지 않는 장치가 있을 수도 있습니다. 하지만 오늘날 Vulkan을 지원하는 모든 그래픽 카드들은 일반적으로 우리가 필요로 하는 모든 큐 연산을 지원하고 있습니다. + +### 3단계 - 윈도우 표면(window surface)과 스왑 체인(swap chain) + +오프스크린 렌더링만을 하지 않는 한, 렌더링된 이미지를 표시할 윈도우가 필요합니다. 윈도우는 [GLFW](http://www.glfw.org/)와 [SDL](https://www.libsdl.org/) 같은 네이티브 플랫폼 API나 라이브러리를 사용해 만들 수 있습니다. 이 튜토리얼에서는 GLFW를 사용할 것이고, 자세한 내용은 다음 챕터에서 다루겠습니다. + +실제 윈도우에 렌더링을 하기 위해서는 두 가지 컴포넌트가 더 필요합니다. 윈도우 표면(VkSurfaceKHR)과 스왑 체인(VkSwapchainKHR)입니다. `KHR` 접미어는 이것들이 Vulkan 확장의 일부임을 의미합니다. Vulkan API는 완벽히 플랫폼 독립적이며, 이로 인해 우리는 윈도우 매니저와 상호작용하기 위해서 표준화된 WSI (Window System Interface)를 사용해야만 합니다. 표면(surface)은 렌더링을 수행할 윈도우에 대한 크로스 플랫폼적 추상화를 의미하고 일반적으로 네이티브 윈도우 핸들(예를 들어 윈도우의 경우에는 `HWND`)에 대한 참조를 제공하여 인스턴스화됩니다. 다행히 GLFW 라이브러리는 여러 플랫폼들에 대한 이러한 상세 사항을 처리해 주는 내장 함수를 제공하고 있습니다. + +스왑 체인은 렌더 타겟(render target)의 집합입니다. 스왑 체인의 기본 목적은 우리가 현재 렌더링을 수행하고 있는 이미지와 화면에 그려진 이미지를 달리하는 데 있습니다. 완전히 다 그려진 이미지만을 화면에 표시하기 위한 중요한 기능입니다. 프레임(frame)을 그리기 위해서는 먼저 스왑 체인에 렌더링을 수행할 대상 이미지를 요청해야 합니다. 프레임 그리기가 끝나면, 이미지는 스왑 체인에 반환되어 어느 시점에 화면에 그려지게 됩니다. 렌더 타겟의 개수와 화면에 표시되는 상태는 표시 모드(present mode)를 통해 기술됩니다. 일반적인 표시 모드로는 이중 버퍼링(vsync)과 삼중 버퍼링입니다. 스왑 체인 생성 챕터에서 자세히 살펴볼 것입니다. + +어떤 플랫폼들은 `VK_KHR_display`와 `VK_KHR_display_swapchain` 확장을 통해 윈도우 관리자(manager)와 상호작용하지 않고 곧바로 화면에 그릴 수 있도록 허용하기도 합니다. 이 확장들은 전체 스크린을 표시하는 표면을 만들 수 있고, 이를 이용하여 예를 들자면 여러분 스스로 윈도우 관리자를 구현하는 데 사용될 수 있습니다. + +### 4단계 - 이미지 뷰와 프레임버퍼(framebuffer) + +스왑 체인으로부터 얻은 이미지에 그리기(draw)를 하기 위해서는 이미지를 VkImageView와 VkFramebuffer로 래핑(wrap)해야 합니다. 이미지 뷰는 사용될 이미지의 특정 부분을 참조하고, 프레임버퍼는 색상, 깊이, 스텐실(stensil) 그리기의 대상이 되는 이미지 뷰를 참조합니다. 스왑 체인에 여러 이미지들이 있을 수 있기 떄문에, 그 각각의 이미지에 대해 미리 이미지 뷰와 프레임버퍼를 생성해 두고 그리기 시점에 적절한 것들을 선택해야 합니다. + +### 5단계 - 렌더 패스(pass) + +Vulkan에서의 렌더 패스는 렌더링 연상 과정에 사용될 이미지의 타입을 기술합니다. 그것이 어떻게 사용될지, 그 내용이 어떻게 취급될지와 같은 것들 말입니다. 삼각형 그리기 응용 프로그램에서는 우리는 Vulkan에게 단일 이미지를 색상 타겟으로 사용할 것이고, 그리기 연산이 수행되기 직전에 단일 색상으로 지울(clear)것을 명시할 것입니다. 렌더 패스는 이미지의 타입만 지정하고, VkFramebuffer가 실제로 특정 이미지를 그 슬롯(slot)에 바인딩(bind)합니다. + +### 6단계 - 그래픽스 파이프라인 + +Vulkan에서의 그래픽스 파이프라인은 VkPipeline 객체를 만들어서 설정됩니다. 뷰포트 크기와 깊이 버퍼 연산과 같은 그래픽 카드의 상태 구성 방법과 VkShaderModule 객체를 통한 프로그램 가능한 상태와 같은 것들을 기술합니다. VkShaderModule은 셰이더의 바이트 코드로부터 생성됩니다. 드라이버는 파이프라인에서 어떤 렌더 타겟이 사용될지 알아야 하는데 이는 렌더 패스 참조를 통해 명시할 수 있습니다. + +Vulkan과 기존 API간의 가장 큰 차이점은 거의 모든 그래픽스 파이프라인의 구성이 미리 설정되어야 한다는 것입니다. 즉, 여러분이 사른 셰이더를 사용하거나 정점의 레이아웃(layout)을 살짝 바꾸려고 할 때에도 그래픽스 파이프라인 전체를 다시 생성해야 한다는 것입니다. 그 말은 렌더링 연산을 위한 여러 조합들에 대해 VkPipeline들을 미리 만들어놔야 한다는 뜻입니다. 몇 가지 기본적인 구성, 예를 들어 뷰포트 크기나 지움(clear) 색상과 같은 것들만 동적으로 바꿀 수 있습니다. 또한 모든 상태는 명시적으로 기술되어야 합니다. 예를 들어 색상 혼합(blend) 상태의 기본값(default) 같은 것은 제공되지 않습니다. + +좋은 소식은 이러한 작업들이 ahead-of-time 컴파일과 just-in-time 컴파일의 차이 같은 것이라, 드라이버가 미리 최적화 할 수 있는 여지가 많고, 런타임 성능이 보다 예측하기 쉽다는 것입니다. 왜냐하면 다른 그래픽스 파이프라인으로의 변경과 같은 큰 상태 변화가 매우 명시적으로 표현되기 때문이지요. + +### 7단계 - 명령 풀(pool)과 명령 버퍼 + +앞서 이야기한 것처럼, 우리가 실행하고자 하는, 예를 들자면 그리기와 같은 Vulkan의 많은 연산들은 큐에 제출되어야 합니다. 이러한 연산들은 제출되기 전에 VkCommandBuffer에 먼저 기록되어야 합니다. 이러한 명령 버퍼는 `VkCommandPool`로부터 할당되고 이는 특정한 큐 패밀리와 연관(associate)되어 있습니다. 간단한 삼각형을 그리기 위해서는 아래와 같은 연산들을 명령 버퍼에 기록해야 합니다. + +- 렌더 패스 시작 +- 그래픽스 파이프라인 바인딩 +- 3개 정점 그리기 +- 렌더 패스 종료 + +프레임버퍼의 이미지는 스왑 체인이 우리에게 전달해준 이미지에 의존하기 때문에, 명령 버퍼에 기록할 명령어는 모든 가능한 이미지에 대해 기록되어야 하고, 그리기 시점에 올바른 것이 선택되어야 합니다. 다른 방법으로는 매 프레임마다 명령 버퍼를 기록하는 것인데, 그리 효율적이지 않습니다. + +### 8단계 - 메인 루프(main loop) + +그리기 명령이 명령 버퍼에 기록되었으므로 메인 루프는 직관적입니다. 먼저 스왑 체인으로부터 vkAcquireNextImageKHR를 통해 이미지를 얻습니다. 그리고 해당 이미지에 적합한 명령 버퍼를 선택하고 vkQueueSubmit를 통해 실행합니다. 마지막으로 vkQueuePresentKHR를 통해 이미지를 스왑 체인에 반환하여 화면에 표시되게 합니다. + +큐에 제출된 연산들은 비동기적으로 실행됩니다. 따라서 세마포어와 같은 동기화 객체를 사용하여 실행이 올바른 순서로 이루어지도록 해야 합니다. 그리기 명령 버퍼의 실행은 이미지 획득 이후에 되도록 해야 합니다. 그렇지 않으면 화면에 표시하기 위해 읽고 있는 이미지에 렌더링이 수행될 수도 있습니다. vkQueuePresentKHR의 호출은 렌더링이 끝날 때까지 기다려야 하는데 이를 위해서는 렌더링이 끝나면 신호를 보내는 두 번째 세마포어를 사용해야 할 것입니다. + +### 요약 + +이 정신없는 소개를 통해 삼각형을 그리기 앞서 알아야 할 것들에 대한 기본적인 이해가 되었길 바랍니다. 실제 프로그램은 몇 가지 단계가 더 필요한데, 정점 버퍼를 할당한다던지, 유니폼(uniform) 버퍼를 만들고 텍스처 이미지를 업로드한다던다 하는 것이고, 이어지는 챕터에서 소개할 것입니다. 우선은 간단히 시작할 것인데 Vulkan의 학습 곡선이 이미 충분히 가파르기 때문입니다. 주의하실 것은 초기에 우리는 정점의 좌표를 정점 버퍼를 사용하는 대신 정점 셰이더에 직접 하드코딩 하는 편법을 쓸 것입니다. 이는 정점 버퍼를 사용하기 위해서는 우선 명령 버퍼에 익숙해져야 하기 때문입니다. + +요약하자면, 삼각형을 그리기 위해서 우리는: + +- VkInstance 생성 +- 지원하는 그래픽 카드 선택 (VkPhysicalDevice) +- 그리기와 표시를 위해 VkDevice와 VkQueue 생성 +- 윈도우, 윈도우 표면 및 스왑 체인 생성 +- VkImageView로 스왑 체인 이미지 래핑 +- 렌더 타겟과 사용법을 명시하는 렌더 패스 생성 +- 렌더 패스를 위한 프레임버퍼 생성 +- 그래픽스 파이프라인 설정 +- 모든 후보 스왑 체인 이미지에 대해 명령 버퍼를 할당하고 그리기 명령을 기록 +- 이미지를 획득하고, 올바른 그리기 명령을 제출하고, 이미지를 스왑 체인에 반환하여 프레임 그리기 + +단계가 많지만 개별 단계의 목적에 대해서는 이후 챕터에서 명확하고 단순하게 설명할 것입니다. 개별 단계와 전체 프로그램의 관계에 대해 헷갈리시면 이 챕터로 다시 돌아 오십시오. + +## API 컨셉 + +이 챕터는 Vulkan API가 저수준에서 어떻게 구조화 되어있는지를 간략히 살펴보는 것으로 마치겠습니다. + +### 코드 작성 규칙(convention) + +모든 Vulkan 함수, 열거형과 구조체들은 `vulkan.h` 헤더에 정의되어 있고, 이는 LunarG가 개발한 [Vulkan SDK](https://lunarg.com/vulkan-sdk/)에 포함되어 있습니다. 다음 챕터에서 이 SDK를 설치할 것입니다. + +함수는 소문자 `vk` 접두어를 갖고, 열거형 및 구조체와 같은 타입은 `Vk` 접두어, 열거자 값은 `VK_` 접두어를 갖습니다. API는 함수에 매개변수 전달을 위해 구조체를 아주 많이 사용합니다. 예를 들어, 객체의 생성은 대개 아래와 같은 패턴을 따릅니다: + +```c++ +VkXXXCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_XXX_CREATE_INFO; +createInfo.pNext = nullptr; +createInfo.foo = ...; +createInfo.bar = ...; + +VkXXX object; +if (vkCreateXXX(&createInfo, nullptr, &object) != VK_SUCCESS) { + std::cerr << "failed to create object" << std::endl; + return false; +} +``` + +Vulkan의 많은 구조체들은 해당 구조체의 타입을 `sType` 멤버를 통해 명시적으로 명시하도록 되어 있습니다. `pNext` 멤버는 확장 구조를 가리킬 수 있도록 되어 있는데 이 튜토리얼에서는 항상 `nullptr`입니다. 객체를 생성하서나 소멸(destroy)하는 함수는 VkAllocationCallbacks 매개변수를 가지고 있어서 드라이버 메모리에 대한 커스텀 할당자(allocator)를 사용할 수 있도록 하는데 이 역시 이 튜토리얼에서는 항상 `nullptr`로 둘 것입니다. + +대부분의 함수는 VkResult를 반환하고 이는 `VK_SUCCESS`이거나 오류 코드입니다. 명세를 보면 각 함수가 반환할 수 있는 오류 코드와 그 의미가 적혀 있습니다. + +### 검증 레이어(validation layer) + +앞서 이야기한 것처럼, Vulkan은 고성능과 적은 드라이버 오버헤드를 위해 설계되었습니다. 따라서 기본적으로는 아주 제한적인 오류 체킹과 디버깅 기능만을 포함하고 있습니다. 코드를 잘못 작성하는 드라이버는 오류 코드를 반환하는 대신 그냥 크래쉬(crash)가 발생하거나, 더 나쁜 경우에는 여러분의 그래픽 카드에서는 제대로 동작하는 것처럼 보이지만 다른 그래픽 카드에서는 전혀 동작하지 않을겁니다. + +Vulkan은 꼼꼼한 오류 체크를 *검증 레이어(validation layer)* 기능을 통해 제공합니다. 검증 레이어는 API와 그래픽 드라이버 사이에 삽입되는 코드로 함수 매개변수에 대한 추가적인 검증이나 메모리 관리 문제를 추적하는 데 사용됩니다. 좋은 점은 이러한 기능을 개발 과정에서 사용하고 릴리즈 할 때에는 완전히 사용하지 않도록 하여 오버헤드를 없앨 수 있다는 것입니다. 스스로 검증 레이어를 작성할 수도 있지만, LunarG가 만든 Vulkan SDK는 표준적인 검증 레이어를 제공하고, 이 튜토리얼에서는 그것을 사용할 것입니다. 여러분은 레이어에서 날아온 디버깅 메시지를 처기하기 위한 콜백 함수를 등록해야 합니다. + +Vulkan의 각 연산은 아주 명시적이고 검증 레이어는 꼼꼼하기 떄문에 화면이 검은 색 밖에 안나오는 경우에 OpenGL이나 Direct3D보다 그 원인을 찾기가 훨씬 쉽습니다! + +코드를 작성하기 전에 시작해야 할 남은 한 단계는 [개발 환경을 설정](!kr/Development_environment)하는 것입니다. diff --git a/kr/02_Development_environment.md b/kr/02_Development_environment.md new file mode 100644 index 00000000..e4923675 --- /dev/null +++ b/kr/02_Development_environment.md @@ -0,0 +1,476 @@ +이 챕터에서는 Vulkan 응용 프로그램 개발을 위한 환경을 설정하고 몇 가지 유용한 라이브러리를 설치할 것입니다. 우리가 사용할 툴들은 윈도우즈, 리눅스와 MacOS에서 호환되지만 설치 방법은 약간씩 다르기 때문에 개별적으로 설명합니다. + +## 윈도우즈 + +윈도우에서 개발하시는 경우엔 코드 컴파일에는 비주얼 스튜디오를 사용한다고 가정하겠습니다. C++17 지원을 위해서는 비주얼 스튜디오 2017이나 2019가 필요합니다. 아래 설명하는 단계들은 2017을 기준으로 작성되었습니다. + +### Vulkan SDK + +Vulkan 응용 프로그램 개발을 위해 가장 중요한 구성요소는 SDK입니다. SDK는 헤더, 표준 검증 레이어, 디버깅 도구와 Vulkan 함수의 로더(loader)가 포함되어 있습니다. 로더는 런타임에 드라이버의 함수를 탐색하는 OpenGL에서의 GLEW와 유사한 도구입니다. + +SDK는 [LunarG 웹사이트](https://vulkan.lunarg.com/) 페이지 하단의 버튼을 통해 다운로드 할 수 있습니다. 계정을 만드실 필요는 없지만 계정을 만들면 유용하게 활용할 수 있는 추가적인 문서에 접근할 수 있습니다. + +![](/images/vulkan_sdk_download_buttons.png) + +설치 과정을 거치시고, SDK의 설치 경로를 주의깊게 확인하십시오. 처음으로 할 것은 여러분의 그래픽 카드와 드라이버가 Vulkan을 제대로 지원하는지 확인하는 것입니다. SDK를 설치한 폴더로 가서 `Bin` 디렉터리 안의 `vkcube.exe` 데모를 실행해 보세요. 아래와 같은 화면이 나타나야 합니다: + +![](/images/cube_demo.png) + +오류 메시지가 나타난다면 드라이버가 최신 버전인지 확인하고, 그래픽 카드가 Vulkan을 지원하고 Vulkan 런타임이 드라이브에 포함되어 있는지 확인하세요. 주요 벤더들의 드라이버 링크는 [introduction](!kr/Introduction) 챕터를 확인하세요. + +이 폴더에는 개발에 유용한 다른 프로그램들도 있습니다. `glslangValidator.exe`와 `glslc.exe`는 사람이 읽을 수 있는 [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language) 코드를 바이트 코드로 변환하기 위해 사용됩니다. [shader modules](!kr/Drawing_a_triangle/Graphics_pipeline_basics/Shader_modules) 챕터에서 이 내용을 자세히 살펴볼 것입니다. `Bin` 디렉터리에는 또한 Vulkan 로더와 검증 레이어의 바이너리들을 포함하고 있으며, `Lib` 디렉터리에는 라이브러리들이 들어 있습니다. + +마지막으로 `Include` 디렉터리에는 Vulkan 헤더들이 있습니다. 다른 파일들도 자유롭게 살펴보시길 바라지만 이 튜토리얼에서는 필요하지 않습니다. + +### GLFW + +앞서 언급한 것처럼 Vulkan은 플랫폼 독립적인 API여서 렌더링 결과를 표시할 윈도우 생성을 위한 도구 같은것은 포함되어 있지 않습니다. Vulkan의 크로스 플랫폼 이점을 살리면서도 Win32의 어려움을 회피하는 방법으로 우리는 [GLFW library](http://www.glfw.org/)를 사용하여 윈도우를 만들 것입니다. GLFW는 윈도우, 리눅스와 MacOS를 모두 지원합니다. 비슷한 목적으로 사용 가능한 [SDL](https://www.libsdl.org/)과 같은 라이브러리도 있지만, GLFW는 윈도우 생성뿐만 아니라 Vulkan의 다른 추가적인 플랫폼 의존적인 작업들에 대한 추상화도 제공해 준다는 것입니다. + +GLFW의 최신 버전을 [공식 웹사이트](http://www.glfw.org/download.html)에서 찾을 수 있습니다. 이 튜토리얼에서는 64비트 바이너리를 사용할 것인데 32비트 모드로 빌드하셔도 됩니다. 그 경우에는 Vulkan SDK의 `Lib` 디렉터리 대신 `Lib32` 디렉터리의 라이브러리들을 링크하셔야 합니다. 다운로드 하시고 나서 편한 곳에 압축을 푸십시오. 저는 내 문서 아래의 비주얼 스튜디오 디렉터리 아래 `Libraries` 폴더를 만들어 그 곳에 넣었습니다. + +![](/images/glfw_directory.png) + +### GLM + +DirectX 12와는 다르게, Vulkan은 선형대수 연산을 위한 라이브러리가 포함되어 있지 않아서 다운로드 해야 합니다. [GLM](http://glm.g-truc.net/)은 그래픽스 API를 위해 설계된 좋은 라이브러리로 OpenGL에서도 자주 사용됩니다. + +GLM은 헤더만으로 구성된 라이브러리로, [최신 버전](https://github.com/g-truc/glm/releases)을 다운로드하고 적절한 위치에 가져다 놓으세요. 그러면 아래와 같은 디렉터리 구조가 될 겁니다: + +![](/images/library_directory.png) + +### 비주얼 스튜디오 설정 + +필요한 의존성(dependencies)를 설치했으므로 Vulkan 개발을 위한 비주얼 스튜디오 프로젝트를 설정하고 모든 것들이 올바로 동작하는지 확인하기 위한 짧은 코드를 작성해 보겠습니다. + +비주얼 스튜디오를 실행하고 `Windows Desktop Wizard` 프로젝트를 선택한 뒤 이름을 설정하고 `OK`를 누르세요. + +![](/images/vs_new_cpp_project.png) + +`Console Application (.exe)`를 선택해서 우리의 응용 프로그램이 디버깅 메시지를 표시할 수 있도록 하고 `Empty Project`로 비주얼 스튜디오가 보일러플레이트(boilerplate) 코드를 생성하지 않도록 하세요. + +![](/images/vs_application_settings.png) + +`OK`를 눌러 프로젝트를 만들고 C++ 소스 파일을 추가 하세요. 어떻게 하는지 알고 계실 테지만, 하는 법을 알려 드리겠습니다. + +![](/images/vs_new_item.png) + +![](/images/vs_new_source_file.png) + +이제 아래 코드를 파일에 추가 하세요. 지금은 이해하려 하실 필요 없습니다. 그냥 Vulkan 응용 프로그램을 컴파일하고 실행할 수 있는지 확인하세요. 다음 챕터에서 다시 처음부터 시작할 것입니다. + +```c++ +#define GLFW_INCLUDE_VULKAN +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include + +#include + +int main() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window", nullptr, nullptr); + + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); + + std::cout << extensionCount << " extensions supported\n"; + + glm::mat4 matrix; + glm::vec4 vec; + auto test = matrix * vec; + + while(!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } + + glfwDestroyWindow(window); + + glfwTerminate(); + + return 0; +} +``` + +이제 프로젝트 설정을 통해 오류를 해결해 봅시다. 프로젝트 설정 창을 열고 `All Configurations`이 선택되어 있는지 확인하세요. 대부분의 세팅이 `Debug`와 `Release` 모드에 공통적으로 해당됩니다: + +![](/images/vs_open_project_properties.png) + +![](/images/vs_all_configs.png) + +`C++ -> General -> Additional Include Directories`로 가셔서 드롭다운 메뉴에서 ``을 누르세요: + +![](/images/vs_cpp_general.png) + +Vulkan, GLFW, GLM을 위한 헤더 디렉터리를 추가하세요: + +![](/images/vs_include_dirs.png) + +다음으로 `Linker -> General`에서 추가 라이브러리 디렉터리 설정창을 여세요: + +![](/images/vs_link_settings.png) + +그리고 Vulkan과 GLFW를 위한 오브젝트 파일의 위치를 추가하세요: + +![](/images/vs_link_dirs.png) + +`Linker -> Input`에서 `Additional Dependencies`의 ``의 드롭다운 메뉴를 누르세요: + +![](/images/vs_link_input.png) + +Vulkan과 GLFW의 오브젝트 파일 이름을 추가하세요: + +![](/images/vs_dependencies.png) + +마지막으로 컴파일러가 C++17 기능을 지원하도록 설정하세요: + +![](/images/vs_cpp17.png) + +이제 프로젝트 설정 창을 닫아도 됩니다. 모두 제대로 설정되었으면 더이상 에러 메시지가 나타나지 않을 겁니다. + +마지막으로 64비트 모드에서 컴파일을 하는지 확인하시고: + +![](/images/vs_build_mode.png) + +`F5`를 눌러 컴파일 후 실행을 해 보면 명령 창(command prompt)과 윈도우가 아래처럼 나타나는 것을 볼 수 있을 겁니다: + +![](/images/vs_test_window.png) + +extention의 숫자는 0이 아니어야 합니다. 축하합니다. [Vulkan을 즐기기 위한](!kr/Drawing_a_triangle/Setup/Base_code)! 모든 준비가 완료되었습니다. + +## 리눅스 + +이 가이드는 우분투, 페도라와 Arch 리눅스 유저를 대상으로 하지만, 패키지 매니저별로 다른 명령어만 사용하시면 그대로 따라하시면 됩니다. C++17을 지원하는 컴파일러 (GCC 7+ 또는 Clang 5+)를 사용하셔야 합니다. `make`도 필요합니다. + +### Vulkan 패키지 + +리눅스에서의 Vulkan 응용 프로그램 개발을 위해 가장 중요한 구성요소는 Vulkan 로더, 검증 레이어와 여러분의 기기가 Vulkan을 지원하는지 테스트하기 위한 몇 개의 명령줄 유틸리티들입니다. + +- `sudo apt install vulkan-tools` 또는 `sudo dnf install vulkan-tools`: 명령줄 유틸리티로, 가장 중요한 것은 `vulkaninfo`와 `vkcube`입니다. 기기가 Vulkan을 지원하는지 확인하기 위해 실행해 보십시오. +- `sudo apt install libvulkan-dev` 또는 `sudo dnf install vulkan-loader-devel` : Vulkan 로더를 설치합니다. 로더는 OpenGL에서의 GLEW처럼, 런타임에 드라이버의 함수들을 탐색합니다. +- `sudo apt install vulkan-validationlayers-dev spirv-tools` 또는 `sudo dnf install mesa-vulkan-devel vulkan-validation-layers-devel`: 표준 검증 레이어와 필요한 SPIR-V 도구들을 설치합니다. Vulkan 응용 프로그램을 디버깅하기 위해 필수적이고, 이어지는 챕터에서 자세히 다룰 것입니다. + +Arch 리눅스에서는 위 도구들을 설치하기 위해서는 `sudo pacman -S vulkan-devel`를 실행하면 됩니다. + +성공적으로 설치가 되었다면, Vulkan 관련 부분은 완료된 것입니다. `vkcube`를 실행해서 아래와 같은 윈도우가 나타나는 것을 확인하십시오. + +![](/images/cube_demo_nowindow.png) + +오류 메시지가 나타난다면 드라이버가 최신 버전인지 확인하고, 그래픽 카드가 Vulkan을 지원하고 Vulkan 런타임이 드라이브에 포함되어 있는지 확인하세요. 주요 벤더들의 드라이버 링크는 [introduction](!kr/Introduction) 챕터를 확인하세요. + +### X Window System and XFree86-VidModeExtension + +이 라이브러리들이 시스템에 없을 수도 있습니다. 그런 경우엔 다음 명령어를 사용해 설치할 수 있습니다. + +- `sudo apt install libxxf86vm-dev` 또는 `dnf install libXxf86vm-devel`: XFree86-VidModeExtension에 대한 인터페이스를 제공합니다. +- `sudo apt install libxi-dev` or `dnf install libXi-devel`: X Window System에서 XINPUT 확장에 대한 클라이언트 인터페이스를 제공합니다. + +### GLFW + +앞서 언급한 것처럼 Vulkan은 플랫폼 독립적인 API여서 렌더링 결과를 표시할 윈도우 생성을 위한 도구 같은것은 포함되어 있지 않습니다. Vulkan의 크로스 플랫폼 이점을 살리면서도 Win32의 어려움을 회피하는 방법으로 우리는 [GLFW library](http://www.glfw.org/)를 사용하여 윈도우를 만들 것입니다. GLFW는 윈도우, 리눅스와 MacOS를 모두 지원합니다. 비슷한 목적으로 사용 가능한 [SDL](https://www.libsdl.org/)과 같은 라이브러리도 있지만, GLFW는 윈도우 생성뿐만 아니라 Vulkan의 다른 추가적인 플랫폼 의존적인 작업들에 대한 추상화도 제공해 준다는 것입니다. + +다음 명령문을 통해 GLFW를 설치할 것입니다.: + +```bash +sudo apt install libglfw3-dev +``` + +or + +```bash +sudo dnf install glfw-devel +``` + +or + +```bash +sudo pacman -S glfw-wayland # glfw-x11 for X11 users +``` + +### GLM + +DirectX 12와는 다르게, Vulkan은 선형대수 연산을 위한 라이브러리가 포함되어 있지 않아서 다운로드 해야 합니다. [GLM](http://glm.g-truc.net/)은 그래픽스 API를 위해 설계된 좋은 라이브러리로 OpenGL에서도 자주 사용됩니다. + +GLM은 헤더만으로 구성된 라이브러리로, `libglm-dev` 또는 `glm-devel` 패키지를 통해 설치 가능합니다: + +```bash +sudo apt install libglm-dev +``` + +또는 + +```bash +sudo dnf install glm-devel +``` + +또는 + +```bash +sudo pacman -S glm +``` + +### 셰이더 컴파일러 + +필요한 것들이 거의 다 준비되었는데, 사람이 읽을 수 있는 [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language)를 바이트 코드로 컴파일해 주는 프로그램은 아직 설치되지 않았습니다. + +두 가지 유명한 셰이더 컴파일러는 크로노스 그룹의 `glslangValidator`와 구글의 `glslc`입니다. 후자는 우리에게 익숙한 GCC와 Clang과 유사한 사용법을 제공하기 때문에 그것을 사용할 것입니다. 우분투에서, 구글의 [공식 바이너리](https://github.com/google/shaderc/blob/main/downloads.md)를 다운로드 하고 `glslc`를 `/usr/local/bin`에 복사하십시오. 권한에 따라 `sudo`를 사용해야 할 수 있습니다. 페도라에서는 `sudo dnf install glslc`, Arch 리눅스에서는 `sudo pacman -S shaderc`를 사용하십시오. 테스트를 위해 `glslc`를 실행하면 컴파일할 셰이더를 올바로 전달하지 않았다는 메시지가 나타날 겁니다. + +`glslc: error: no input files` + +[shader modules](!kr/Drawing_a_triangle/Graphics_pipeline_basics/Shader_modules) 챕터에서 `glslc`를 자세히 살펴볼 것입니다. + +### makefile 프로젝트 구성 + +필요한 의존성(dependencies)들을 모두 설치하였으니 Vulkan을 위한 기본 makefile 프로젝트를 만들고 약간의 코드 작성을 통해 모든 것들이 올바로 동작하는지 확인해 봅시다. + +`VulkanTest`와 같은 새 디렉터리를 편한 위치에 만들고 `main.cpp` 소스 파일을 생성한 뒤, 다음 코드를 삽입하세요. 지금은 이해하려 하지 마시고, Vulkan 응용 프로그램을 컴파일하고 실행할 수 있는지만 확인하시면 됩니다. 다음 챕터에서 처음부터 다시 시작할 것입니다. + +```c++ +#define GLFW_INCLUDE_VULKAN +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include + +#include + +int main() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window", nullptr, nullptr); + + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); + + std::cout << extensionCount << " extensions supported\n"; + + glm::mat4 matrix; + glm::vec4 vec; + auto test = matrix * vec; + + while(!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } + + glfwDestroyWindow(window); + + glfwTerminate(); + + return 0; +} +``` + +다음으로 기본 Vulkan 코드를 컴파일하고 실행하기 위한 makefile을 작성할 것입니다. `Makefile`이라는 이름으로 새 파일을 만드십시오. 저는 여러분들이 기본적인 makefile 사용 경험이 있다고 가정할 것입니다. 예를 들어 변수(variable)와 규칙(rule)이 어떻게 동작하는지 등을 이야기하는 것입니다. 그렇지 않으면, [이 튜토리얼](https://makefiletutorial.com/)을 통해 빠르게 살펴 보십시오. + +우선 나머지 부분을 간략하 하기 위해 몇 가지 변수를 정의할 것입니다. 기본 컴파일러 플래그를 명시하기 위해 `CFLAGS` 변수를 정의합니다. + +```make +CFLAGS = -std=c++17 -O2 +``` + +모던(modern) C++ (`-std=c++17`)를 사용할 것이고, 최적화 수준(optimization level)을 O2로 설정할 것입니다. 빠른 컴파일을 위해 -O2를 제거할 수도 있지만, 릴리즈(release) 빌드에서는 포함해야 한다는 것을 잊으면 안됩니다. + +비슷하게 `LDFLAGS` 변수로 링커 플래그를 정의합니다. + +```make +LDFLAGS = -lglfw -lvulkan -ldl -lpthread -lX11 -lXxf86vm -lXrandr -lXi +``` + +GLFW를 위해 `-lglfw` 플래그를, Vulkan 함수 로더를 위해 `-lvulkan`를 사용하고, 나머지는 GLFW가 필요로 하는 저수준(low-level) 시스템 라이브러리들입니다. 나머지 플래그들은 GLFW의 의존성들은 쓰레딩과 윈도우 관리와 관련한 플래그들입니다. + +`Xxf68vm`와 `Xi` 라이브러리가 여러분의 시스템에 아직 설치되어 있지 않을 수 있습니다. 다음 패키지로부터 찾을 수 있습니다: + +```bash +sudo apt install libxxf86vm-dev libxi-dev +``` + +또는 + +```bash +sudo dnf install libXi-devel libXxf86vm-devel +``` + +또는 + +```bash +sudo pacman -S libxi libxxf86vm +``` + +이제 `VulkanTest`를 위한 컴파일 규칙을 명시하는 것은 쉽습니다. 들여쓰기(indentation)에 스페이스 대신 탭을 사용하는 것을 잊지 마세요. + +```make +VulkanTest: main.cpp + g++ $(CFLAGS) -o VulkanTest main.cpp $(LDFLAGS) +``` + +위와 같은 규칙이 제대로 동작하는 것을, makefile을 저장한 뒤 `make`를 `main.cpp`와 `Makefile`이 있는 디렉터리에서 실행하여 확인하십시오. 그 결과 `VulkanTest` 실행 파일이 생성될 것입니다. + +`test`와 `clean` 두 가지 규칙을 더 정의할 것인데, 앞의 것은 실행 파일을 실행하는 것이고 뒤의 것은 생성된 실행 파일을 삭제하는 것입니다. + +```make +.PHONY: test clean + +test: VulkanTest + ./VulkanTest + +clean: + rm -f VulkanTest +``` + +`make test`를 실행하면 프로그램이 성공적으로 실행될 것이고 Vulkan 확장 숫자를 보여줄 것입니다. 창을 닫으면 성공(`0`) 반환 코드를 반환하면서 응용 프로그램이 종료될 것입니다. 결과적으로 아래와 같은 makefile이 존재하게 됩니다: + +```make +CFLAGS = -std=c++17 -O2 +LDFLAGS = -lglfw -lvulkan -ldl -lpthread -lX11 -lXxf86vm -lXrandr -lXi + +VulkanTest: main.cpp + g++ $(CFLAGS) -o VulkanTest main.cpp $(LDFLAGS) + +.PHONY: test clean + +test: VulkanTest + ./VulkanTest + +clean: + rm -f VulkanTest +``` + +이제 이 디렉터리를 여러분의 Vulkan 프로젝트를 위한 템플릿으로 사용하시면 됩니다. 복사하고 이름을 `HelloTriangle`과 같은 것으로 바꾸고, `main.cpp`의 모든 내용을 지우면 됩니다. + +이제 [진정한 탐험](!kr/Drawing_a_triangle/Setup/Base_code)을 위한 준비가 끝났습니다. + +## MacOS + +These instructions will assume you are using Xcode and the [Homebrew package manager](https://brew.sh/). Also, keep in mind that you will need at least MacOS version 10.11, and your device needs to support the [Metal API](). + +### Vulkan SDK + +The most important component you'll need for developing Vulkan applications is the SDK. It includes the headers, standard validation layers, debugging tools and a loader for the Vulkan functions. The loader looks up the functions in the driver at runtime, similarly to GLEW for OpenGL - if you're familiar with that. + +The SDK can be downloaded from [the LunarG website](https://vulkan.lunarg.com/) using the buttons at the bottom of the page. You don't have to create an account, but it will give you access to some additional documentation that may be useful to you. + +![](/images/vulkan_sdk_download_buttons.png) + +The SDK version for MacOS internally uses [MoltenVK](https://moltengl.com/). There is no native support for Vulkan on MacOS, so what MoltenVK does is actually act as a layer that translates Vulkan API calls to Apple's Metal graphics framework. With this you can take advantage of debugging and performance benefits of Apple's Metal framework. + +After downloading it, simply extract the contents to a folder of your choice (keep in mind you will need to reference it when creating your projects on Xcode). Inside the extracted folder, in the `Applications` folder you should have some executable files that will run a few demos using the SDK. Run the `vkcube` executable and you will see the following: + +![](/images/cube_demo_mac.png) + +### GLFW + +As mentioned before, Vulkan by itself is a platform agnostic API and does not include tools for creation a window to display the rendered results. We'll use the [GLFW library](http://www.glfw.org/) to create a window, which supports Windows, Linux and MacOS. There are other libraries available for this purpose, like [SDL](https://www.libsdl.org/), but the advantage of GLFW is that it also abstracts away some of the other platform-specific things in Vulkan besides just window creation. + +To install GLFW on MacOS we will use the Homebrew package manager to get the `glfw` package: + +```bash +brew install glfw +``` + +### GLM + +Vulkan does not include a library for linear algebra operations, so we'll have to download one. [GLM](http://glm.g-truc.net/) is a nice library that is designed for use with graphics APIs and is also commonly used with OpenGL. + +It is a header-only library that can be installed from the `glm` package: + +```bash +brew install glm +``` + +### Setting up Xcode + +Now that all the dependencies are installed we can set up a basic Xcode project for Vulkan. Most of the instructions here are essentially a lot of "plumbing" so we can get all the dependencies linked to the project. Also, keep in mind that during the following instructions whenever we mention the folder `vulkansdk` we are refering to the folder where you extracted the Vulkan SDK. + +Start Xcode and create a new Xcode project. On the window that will open select Application > Command Line Tool. + +![](/images/xcode_new_project.png) + +Select `Next`, write a name for the project and for `Language` select `C++`. + +![](/images/xcode_new_project_2.png) + +Press `Next` and the project should have been created. Now, let's change the code in the generated `main.cpp` file to the following code: + +```c++ +#define GLFW_INCLUDE_VULKAN +#include + +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include + +#include + +int main() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + GLFWwindow* window = glfwCreateWindow(800, 600, "Vulkan window", nullptr, nullptr); + + uint32_t extensionCount = 0; + vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); + + std::cout << extensionCount << " extensions supported\n"; + + glm::mat4 matrix; + glm::vec4 vec; + auto test = matrix * vec; + + while(!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } + + glfwDestroyWindow(window); + + glfwTerminate(); + + return 0; +} +``` + +Keep in mind you are not required to understand all this code is doing yet, we are just setting up some API calls to make sure everything is working. + +Xcode should already be showing some errors such as libraries it cannot find. We will now start configuring the project to get rid of those errors. On the *Project Navigator* panel select your project. Open the *Build Settings* tab and then: + +- Find the **Header Search Paths** field and add a link to `/usr/local/include` (this is where Homebrew installs headers, so the glm and glfw3 header files should be there) and a link to `vulkansdk/macOS/include` for the Vulkan headers. +- Find the **Library Search Paths** field and add a link to `/usr/local/lib` (again, this is where Homebrew installs libraries, so the glm and glfw3 lib files should be there) and a link to `vulkansdk/macOS/lib`. + +It should look like so (obviously, paths will be different depending on where you placed on your files): + +![](/images/xcode_paths.png) + +Now, in the *Build Phases* tab, on **Link Binary With Libraries** we will add both the `glfw3` and the `vulkan` frameworks. To make things easier we will be adding the dynamic libraries in the project (you can check the documentation of these libraries if you want to use the static frameworks). + +- For glfw open the folder `/usr/local/lib` and there you will find a file name like `libglfw.3.x.dylib` ("x" is the library's version number, it might be different depending on when you downloaded the package from Homebrew). Simply drag that file to the Linked Frameworks and Libraries tab on Xcode. +- For vulkan, go to `vulkansdk/macOS/lib`. Do the same for the both files `libvulkan.1.dylib` and `libvulkan.1.x.xx.dylib` (where "x" will be the version number of the the SDK you downloaded). + +After adding those libraries, in the same tab on **Copy Files** change `Destination` to "Frameworks", clear the subpath and deselect "Copy only when installing". Click on the "+" sign and add all those three frameworks here aswell. + +Your Xcode configuration should look like: + +![](/images/xcode_frameworks.png) + +The last thing you need to setup are a couple of environment variables. On Xcode toolbar go to `Product` > `Scheme` > `Edit Scheme...`, and in the `Arguments` tab add the two following environment variables: + +- VK_ICD_FILENAMES = `vulkansdk/macOS/share/vulkan/icd.d/MoltenVK_icd.json` +- VK_LAYER_PATH = `vulkansdk/macOS/share/vulkan/explicit_layer.d` + +It should look like so: + +![](/images/xcode_variables.png) + +Finally, you should be all set! Now if you run the project (remembering to setting the build configuration to Debug or Release depending on the configuration you chose) you should see the following: + +![](/images/xcode_output.png) + +The number of extensions should be non-zero. The other logs are from the libraries, you might get different messages from those depending on your configuration. + +You are now all set for [the real thing](!kr/Drawing_a_triangle/Setup/Base_code). diff --git a/kr/03_Drawing_a_triangle/00_Setup/00_Base_code.md b/kr/03_Drawing_a_triangle/00_Setup/00_Base_code.md new file mode 100644 index 00000000..d1e2c22d --- /dev/null +++ b/kr/03_Drawing_a_triangle/00_Setup/00_Base_code.md @@ -0,0 +1,159 @@ +## 일반적인 구조 + +이전 챕터에서 Vulkan 프로젝트를 만들고 필요한 설정들을 하고 샘플 코드를 통해 테스트 해 봤습니다. 이 챕터에서는 아래 코드로부터 처음부터 시작해 보겠습니다: + +```c++ +#include + +#include +#include +#include + +class HelloTriangleApplication { +public: + void run() { + initVulkan(); + mainLoop(); + cleanup(); + } + +private: + void initVulkan() { + + } + + void mainLoop() { + + } + + void cleanup() { + + } +}; + +int main() { + HelloTriangleApplication app; + + try { + app.run(); + } catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} +``` + +먼저 LunarG SDK의 Vulkan 헤더를 include합니다. 이 헤더는 함수, 구조체와 열거자들을 제공해 줍니다. `stdexcept`와 `iostream` 헤더는 오류를 보고하고 전파하기 위해 include하였습니다. `cstdlib`은 `EXIT_SUCCESS`와 `EXIT_FAILURE` 매크로를 제공합니다. + +프로그램은 클래스로 래핑되어 있는데 Vulkan 객체들을 프라이빗 클래스 멤버로 저장할 것이며, 그 각각을 초기화하는 함수를 추가할 것입니다. 초기화 함수는 `initVulkan` 함수 안에서 호출할 것입니다. 모든 것들이 준비가 되고 나면 메인 루프로 들어가 프레임을 렌더링하기 시작합니다. `mainLoop` 함수 본문을 작성해서 루프를 추가하고 윈도우가 닫히기 전까지 반복하도록 할 것입니다. 윈도우가 닫히고 `mainLoop`가 반환되면, 사용한 리소스들을 `cleanup` 함수를 통해 해제(deallocate)할 것입니다. + +실행 도중 치명적인 오류가 발생하면 `std::runtime_error` 예외를 메지시와 함께 throw할 것인데, 이는 `main`함수로 전파되어 명령 창에 출력될 것입니다. 여러 가지 표준 예외 타입들을 다루기 위해 좀 더 일반적인 `std::exception`을 catch하도록 했습니다. 곧 마주하게 될 오류의 한 예는 필요한 특정 확장이 지원되지 않는 경우가 있습니다. + +다음 챕터부터는 대부분 `initVulkan` 함수 안에서 호출할 하나의 새로운 함수를 추가하고 하나 이상의 Vulkan 객체를 프라이빗 클래스 멤버로 추가할 것입니다. 해당 객체는 마지막에 `cleanup`을 통해 해제되어야 합니다. + +## 리소스 관리 + +`malloc`을 통해 할당된 메모리는 `free`되어야 하듯이, 모든 우리가 만든 Vulkan 객체는 더 이상 필요하지 않은 시점에서는 명시적으로 소멸되어야 합니다. C++에서는 [RAII](https://en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization)를 통해 자동으로 리소스를 관리하거나 `` 헤더를 통해 제공되는 스마트 포인터를 사용할 수 있습니다. 하지만 이 튜토리얼에서는 Vulkan 객체의 할당과 해제를 직접 하는 방식을 선택했습니다. 결국 Vulkan의 특별한 점은 모든 실수를 피하기 위해 모든 작업들이 명시되어야 한다는 점이고, API의 동작 방식을 배우기 위해 객체의 생애주기도 명시적으로 나타내는 것이 좋다고 생각했습니다. + +이 튜토리얼을 따라한 뒤에, 여러분은 Vulkan 객체를 생성자에서 획득하고 소멸자에서 해제하는 C++ 클래스를 만들어 자동적으로 리소스 관리를 하도록 할 수 있습니다. 또는 `std::unique_ptr`나 `std::shared_ptr`에 사용자 정의 deleter를 명시하여 사용할 수도 있습니다. 큰 규모의 Vulkan 프로그램에는 RAII 모델의 사용을 추천하지만, 학습 목적으로는 어떤 일이 발생하는지 알고 있는것이 더 좋습니다. + +Vulkan 객체는 `vkCreateXXX`같은 함수를 사용해 직접 만들어지거나 `vkAllocateXXX`와 같은 함수로 다른 객체를 통해 할당될 수 있습니다. 객체가 더 이상 사용되지 않는 게 확실하면, 이와 대응되는 `vkDestroyXXX`와 `vkFreeXXX`를 사용해 소멸시켜야 합니다. 이 함수들의 매개변수는 객체의 타입에 따라 다른데, 모든 함수들이 공유하는 매개변수가 하나 있습니다. `pAllocator`입니다. 이는 사용자 정의 메모리 할당자를 위한 콜백을 명시할 수 있도록 하는 선택적인 매개변수입니다. 튜토리얼에서 이 매개변수는 무시할 것이고, 항상 `nullptr`을 인자로 넘길 것입니다. + +## GLFW 통합하기 + +오프스크린 렌더링을 하려는 목적이면 윈도우 없이도 Vulkan은 완벽하게 동작하지만, 실제로 무언가를 보여주는 것이 훨씬 재미있겠죠! 먼저 `#include `를 아래 코드로 대체하십시오. + +```c++ +#define GLFW_INCLUDE_VULKAN +#include +``` + +이렇게 하면 GLFW는 자신에게 필요한 definition들과 Vulkan 헤더를 자동으로 include할 것입니다. `initWindow` 함수를 추가하고 `run`함수에 이 함수를 호출하는 라인을 다른 함수 호출에 앞서 삽입하세요. 이 함수를 사용해 GLFW를 초기화하고 윈도우를 생성할 것입니다. + +```c++ +void run() { + initWindow(); + initVulkan(); + mainLoop(); + cleanup(); +} + +private: + void initWindow() { + + } +``` + +`initWindow`에서 가장 먼저 호출하는 것은 `glfwInit()`이어야 합니다. 이 함수는 GLFW 라이브러리를 초기화합니다. GLFW는 원래 OpenGL 컨텍스트를 생성하게 되어있기 때문에, 이어지는 코드를 통해 OpenGL 컨텍스트를 생성하지 않도록 알려주어야 합니다: + +```c++ +glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); +``` + +나중에 살펴볼 것이지만 윈도우 크기가 변하면 몇 가지 특수한 처리를 해 주어야 하기 때문에 지금은 윈도우 힌트를 추가적으로 호출하여 그 기능을 꺼 둡니다: + +```c++ +glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); +``` + +이제 남은 것은 실제 윈도우를 만드는 것입니다. `GLFWwindow* window;` 프라이빗 클래스 멤버를 추가하여 윈도우의 참조를 저장하도록 하고 윈도우를 아래와 같이 초기화합니다: + +```c++ +window = glfwCreateWindow(800, 600, "Vulkan", nullptr, nullptr); +``` + +앞의 세 매개변수는 윈도우의 가로, 세로, 타이틀을 명시합니다. 네 번째 매개변수를 통해 윈도우를 열 모니터를 명시할 수 있고, 다섯 번째 매개변수는 OpenGL을 사용할 때만 필요합니다. + +가로와 세로 크기를 하드코딩하는 대신 상수를 사용하는 것이 좋겠네요. 나중에 몇 번 해당 값을 참조하는 일이 있을 겁니다. 아래 코드를 `HelloTriangleApplication` 클래스 정의 앞쪽에 추가하였습니다: + +```c++ +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; +``` + +그리고 윈도우 생성 호출을 아래와 같이 바꾸었습니다: + +```c++ +window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +``` + +이제 `initWindow` 함수는 아래와 같은 상태입니다: + +```c++ +void initWindow() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + glfwWindowHint(GLFW_RESIZABLE, GLFW_FALSE); + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +} +``` + +오류가 발생하거나 윈도우가 닫힐 때까지 응용 프로그램이 계속 실행되게 하려면 `mainLoop` 함수에 이벤트 루프를 아래와 같이 추가해야 합니다: + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + } +} +``` + +보기만 해도 이해가 되실 겁니다. 반복문을 돌면서 사용자가 윈도우를 닫기 위해 X 버튼을 눌렀는지와 같은 이벤트를 체크합니다. 이 루프가 나중에 프레임을 렌더링하기 위한 함수들을 호출하는 부분이 될겁니다. + +윈도우가 닫히면, 리소스를 소멸시켜 정리하고 GLFW도 종료해야 합니다. 아래는 `cleanup` 함수의 첫 단계 코드입니다. + +```c++ +void cleanup() { + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +프로그램을 실행하면 `Vulkan`이라는 이름의 윈도우가 보이고 윈도우를 닫기 전까지 응용 프로그램의 실행 상태가 유지될 것입니다. Vulkan 응용 프로그램의 뼈대를 만들었으니, 이제 [첫 번째 Vulkan 객체를 만들어 봅시다](!kr/Drawing_a_triangle/Setup/Instance)! + +[C++ code](/code/00_base_code.cpp) diff --git a/kr/03_Drawing_a_triangle/00_Setup/01_Instance.md b/kr/03_Drawing_a_triangle/00_Setup/01_Instance.md new file mode 100644 index 00000000..b8d97acf --- /dev/null +++ b/kr/03_Drawing_a_triangle/00_Setup/01_Instance.md @@ -0,0 +1,170 @@ +## 인스턴스 생성 + +가장 먼저 할 일은 *인스턴스*를 생성하여 Vulkan 라이브러리를 초기화하는 것입니다. 인스턴스는 여러분의 응용 프로그램과 Vulkan 라이브러리를 연결해 주는 매개체이고 인스턴스를 생성하는 것은 여러분의 응용 프로그램에 대해 드라이버에게 상세한 사항들을 알려주는 것과 같습니다. + +먼저 `createInstance` 함수를 `initVulkan` 함수 내에서 호출합시다. + +```c++ +void initVulkan() { + createInstance(); +} +``` + +또한 인스턴스의 핸들을 저장하기 위한 멤버를 추가합니다: + +```c++ +private: +VkInstance instance; +``` + +이제, 인스턴스를 만드려면 우리 응용 프로그램에 대한 몇 가지 정보를 구조체에 채워넣어야 합니다. 엄밀히 말하면 선택적인 과정이지만, 이를 통해 드라이버에게 유용한 정보를 전달하고 특정 응용프로그램을 최적화 할 수 있습니다 (e.g. because it uses a well-known graphics engine with certain special behavior). 이 구조체는 `VkApplicationInfo`입니다: + +```c++ +void createInstance() { + VkApplicationInfo appInfo{}; + appInfo.sType = VK_STRUCTURE_TYPE_APPLICATION_INFO; + appInfo.pApplicationName = "Hello Triangle"; + appInfo.applicationVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.pEngineName = "No Engine"; + appInfo.engineVersion = VK_MAKE_VERSION(1, 0, 0); + appInfo.apiVersion = VK_API_VERSION_1_0; +} +``` + +앞서 언급한 것처럼 Vulkan의 많은 구조체는 `sType` 멤버를 통해 명시적으로 타입을 명시하도록 합니다. 또한 이 구조체는 `pNext` 멤버를 가지는 많은 구조체 중 하나인데, 나중을 위한 확장을 가리킬 수 있도록 합니다. 여기서는 초기화를 통해 그냥 `nullptr`로 두었습니다. + +Vulkan에서는 많은 정보가 함수의 매개변수 대신 구조체로 전달되고 인스턴스 생성을 위해서는 하나 이상의 구조체를 전달해야 하는 경우가 있습니다. 지금 보시는 두 번째 구조체는 반드시 전달되어야 하는데 Vulkan 드라이버에게 어떤 전역(global) 확장과 검증 레이어를 사용하려고 하는지 알려주는 것입니다. 여기서 전역의 의미는 특정 장치가 아닌 전체 프로그램에 적용된다는 의미인데, 다음 몇 챕터를 보게 되면 확실히 이해될 것입니다. + +```c++ +VkInstanceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; +createInfo.pApplicationInfo = &appInfo; +``` + +이 두 개는 직관적으로 이해가 될 겁니다. 뒤에 나올 두 개는 의도하는 전역 확장을 명시합니다. overview 챕터에서 이야기한 것처럼 Vulkan은 플랫폼 독립적인 API기 때문에 우리는 윈도우 시스템과 상호작용하기위한 확장이 필요합니다. GLFW에는 여기에 필요한 확장을 반환해주는 편리한 함수가 있어서 이 것을 구조체에 전달해 줍니다: + +```c++ +uint32_t glfwExtensionCount = 0; +const char** glfwExtensions; + +glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + +createInfo.enabledExtensionCount = glfwExtensionCount; +createInfo.ppEnabledExtensionNames = glfwExtensions; +``` + +구조체의 마지막 두 멤버가 활성화할 전역 검증 레이어를 결정합니다. 다음 챕터에서 자세히 설명할 것이니 지금은 그냥 비워 둡시다. + +```c++ +createInfo.enabledLayerCount = 0; +``` + +이제 Vulkan이 인스턴스를 생성하기 위한 모든 것들을 명시했으니 `vkCreateInstance`를 호출할 수 있습니다: + +```c++ +VkResult result = vkCreateInstance(&createInfo, nullptr, &instance); +``` + +앞으로도 보시겠지만 Vulkan의 객체 생성 함수의 파라메터들의 일반적인 패턴은 아래와 같습니다: + +- 생성 정보에 관한 구조체를 가리키는 포인터 +- 생성자에 대한 사용자 정의 콜백을 가리키는 포인터. 튜토리얼에서는 항상 `nullptr` +- 새로운 객체의 핸들을 저장하기 위한 변수의 포인터 + +문제 없이 잘 동작했다면 `VkInstance` 클래스 멤버에 인스턴스의 핸들이 저장될 것입니다. 거의 대부분 Vulkan의 함수는 `VkResult` 타입의 값을 반환하는데 그 값은 `VK_SUCCESS`이거나 오류 코드입니다. 인스턴스가 성공적으로 생성되었다면, 결과값을 저장할 필요는 없고 그냥 성공 여부만 체크하면 됩니다. + +```c++ +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` + +이제 프로그램을 실행해 인스턴스가 잘 생성되었는지 확인하십시오. + +## VK_ERROR_INCOMPATIBLE_DRIVER 오류에 맞닥뜨린다면: + +최신 MoltenVK SDK를 MacOS에서 사용중이하면 `vkCreateInstance`로부터 `VK_ERROR_INCOMPATIBLE_DRIVER`가 반환될 수 있습니다. [Getting Start Notes](https://vulkan.lunarg.com/doc/sdk/1.3.216.0/mac/getting_started.html)를 살펴보십시오. 1.3.216 Vulkan SDK부터는 `VK_KHR_PORTABILITY_subset` 확장이 필수적입니다. + +오류를 해결하기 위해서는 먼저 `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` 비트를 `VkInstanceCreateInfo` 구조체 플래그에 추가하고, `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME`를 인스턴스의 확장 리스트에 추가하십시오. + +코드는 보통 아래와 같이 될 겁니다: + +```c++ +... + +std::vector requiredExtensions; + +for(uint32_t i = 0; i < glfwExtensionCount; i++) { + requiredExtensions.emplace_back(glfwExtensions[i]); +} + +requiredExtensions.emplace_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + +createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + +createInfo.enabledExtensionCount = (uint32_t) requiredExtensions.size(); +createInfo.ppEnabledExtensionNames = requiredExtensions.data(); + +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` + +## 확장 지원 체크하기 + +`vkCreateInstance`문서를 보면 `VK_ERROR_EXTENSION_NOT_PRESENT` 오류 코드가 반환될 수 있다는 것을 알 수 있습니다. +필요로 하는 확장을 명시하고 오류가 반환되면 그냥 프로그램을 종료할 수도 있습니다. +이는 윈도우 시스템 인터페이스와 같은 필수적인 확장에 대해서는 적절한 방법이지만, 추가적인 기능을 체크만 하려고 할 때에는 어떻게 해야 할까요? + +인스턴스를 생성하기 전에 지원하는 확장들의 리스트를 얻고 싶으면 `vkEnumerateInstanceExtensionProperties`를 사용하면 됩니다. 확장의 개수를 저장할 변수의 포인터와 확장의 상세 사항을 저장할 `VkExtensionProperties` 배열을 매개변수로 받습니다. 선택적으로 첫 번째 파라메터로 특정한 검증 레이어로 필터링하도록 할 수 있는데, 지금은 무시해도 됩니다. + +확장의 세부 사항을 저장할 배열을 할당하려면 먼저 몇 개나 있는지 알아야 합니다. 그냥 마지막 매개변수를 빈 채로 먼저 확장의 개수만 요청합니다: + +```c++ +uint32_t extensionCount = 0; +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, nullptr); +``` + +(`include `를 추가하고) 확장의 세부 사항들을 저장할 배열을 생성합니다. + +```c++ +std::vector extensions(extensionCount); +``` + +마지막으로 확장의 세부 사항들을 요청합니다: + +```c++ +vkEnumerateInstanceExtensionProperties(nullptr, &extensionCount, extensions.data()); +``` + +`VkExtensionProperties`구조체 각각은 확장의 이름과 버전을 담고 있습니다. 간단한 for문을 사용해 목록을 순회할 수 있습니다. (`\t`는 들여쓰기를 위한 탭 문자입니다) + +```c++ +std::cout << "available extensions:\n"; + +for (const auto& extension : extensions) { + std::cout << '\t' << extension.extensionName << '\n'; +} +``` + +이 코드를 `createInstance`에 추가하면 Vulkan 지원에 대한 세부 사항을 알 수 있습니다. 문제를 하나 내 드리면, `glfwGetRequiredInstanceExtensions`가 반환한 확장들이 모두 지원되는지를 확인해 보세요. + +## 정리하기 + +`VkInstance`는 프로그램 종료 전에 제거되어야 합니다. `cleanup`에서 `vkDestroyInstance` 함수를 통해 제거할 수 있습니다. + +```c++ +void cleanup() { + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +`vkDestroyInstance` 함수의 매개변수는 명확히 이해가 되실겁니다. 이전 장에서 이야기한 것처럼, Vulkan의 할당과 해제 함수에 추가적으로 콜백을 전달할 수 있는데 `nullptr`로 둔 상태입니다. 앞으로 모든 챕터에서 우리가 생성할 Vulkan 리소스들은 인스턴스가 해제되기 전에 정리되어야 합니다. + +인스턴스 생성 이후 보다 복잡한 과정을 살펴보기 전에, [검증 레이어](!kr/Drawing_a_triangle/Setup/Validation_layers)를 통해 디버깅 옵션을 살펴보겠습니다. + +[C++ code](/code/01_instance_creation.cpp) diff --git a/kr/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md b/kr/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md new file mode 100644 index 00000000..443d3518 --- /dev/null +++ b/kr/03_Drawing_a_triangle/00_Setup/02_Validation_layers.md @@ -0,0 +1,374 @@ +## 검증 레이어란? + +Vulkan API는 최소한의 드라이버 오버헤드를 기반으로 설계되었고 그를 위해서 기본적으로 API에서는 최소한의 오류 체크 기능만을 포함하고 있습니다. 열거자를 잘못된 값으로 설정한다거나 필요한 매개변수에 널 포인터를 넘긴다거나 하는 간단한 오류도 일반적으로는 명시적으로 처리되지 않아서 크래시나 정의되지 않은 동작을 일으키게 됩니다. Vulkan에서 여러분이 하는 작업은 매우 명시적이어야 하기 때문에, 새로운 GPU의 기능을 사용한다거나, 논리적 장치 생성 때 필요한 것들을 깜빡한다거나 하는 작은 실수를 저지르기 쉽습니다. + +하지만, 그렇다고 그러한 체크 기능이 API에 포함될 수 없는것은 아닙니다. Vulkan은 *검증 레이어*라고 알려진 우아한 해결책을 만들었습니다. 검증 레이어는 선택적인 구성요소로 Vulkan 함수 호출에 후킹(hook)할 수 있는 추가적인 연산입니다. 일반적으로 검증 레이어에서 수행하는 연산은: + +- 명세를 기반으로 매개변수의 값을 체크하여 잘못된 사용을 탐지 +- 리소스의 누수를 탐지하기 위해 객체의 생성과 소멸을 추적 +- 호출 지점으로부터 쓰레드를 추적하여 쓰레드 세이프티(safety)를 확인 +- 표준 출력에 모든 호출과 매개변수를 로깅(logging) +- 프로파일링(profiling)과 리플레이(replaying)를 위한 Vulkan 호출 추적 + +진단(diagnostics) 검증 레이어의 함수 구현 예시는 아래와 같습니다: + +```c++ +VkResult vkCreateInstance( + const VkInstanceCreateInfo* pCreateInfo, + const VkAllocationCallbacks* pAllocator, + VkInstance* instance) { + + if (pCreateInfo == nullptr || instance == nullptr) { + log("Null pointer passed to required parameter!"); + return VK_ERROR_INITIALIZATION_FAILED; + } + + return real_vkCreateInstance(pCreateInfo, pAllocator, instance); +} +``` + +이러한 검증 레이어들은 여러분이 의도하는 모든 디버깅 기능들을 얼마든지 누적(stack)할 수 있도록 되어 있습니다. 디버깅 빌드에서 검증 레이어를 활성화 하고 릴리즈 빌드에서는 비활성화 하면 양 쪽 상황에서 모두 문제가 없을 것입니다. + +Vulkan은 내장 검증 레이어를 제공하지는 않지만 LunarG Vulkan SDK에서는 흔히 발생하는 오류 검출을 위한 레이어들을 제공하고 있습니다. 완전히 [오픈 소스](https://github.com/KhronosGroup/Vulkan-ValidationLayers)이니, 어떤 종류의 실수를 탐지해 주는지 알 수 있고, 여러분이 기여도 할 수 있습니다. 검증 레이어를 사용하는 것이 여러분의 응용 프로그램이 다른 드라이버에서 정의되지 않은 동작으로 인해 올바로 동작하지 않는 것을 방지하는 가장 좋은 방법입니다. + +검증 레이어는 시스템에 설치되어 있어야 사용 가능합니다. 예를 들어 LunarG 검증 레이어는 Vulkan SDK가 설치된 PC에서만 사용 가능합니다. + +Vulkan에는 두 가지 종류의 검증 레이어가 존재하는데 인스턴스와 장치(device) 레이어입니다. 인스턴스 레이어는 인스턴스와 같은 전역 Vulkan 객체들만을 체크하고 장치 레이어는 특정 GPU에 관련된 호출만을 체크합니다. 현재 장치 레이어는 더 이상 사용되지 않으며(deprecated), 인스턴스 검증 레이어가 모든 Vulkan 호출에 적용됩니다. 명세 문서에는 여전히 호환성을 위해 장치 수준에서 검증 레이어를 활성화 할 것을 권장하고 있습니다. [나중에](!kr/Drawing_a_triangle/Setup/Logical_device_and_queues) 보게 될 것인데, 우리는 같은 레이어를 인스턴스와 논리 장치 수준에서 사용할 것입니다. + +## 검증 레이어 사용하기 + +이 장에서 우리는 Vulkan SDK에서 제공하는 표준 진단 레이어를 활성화 하는 법을 알아볼 것입니다. 확장과 마찬가지로, 검증 레이어는 그 이름을 명시하여 활성화해야 합니다. 모든 유용한 표준 검증들은 SDK에 포함되어 있는 `VK_LAYER_KHRONOS_validation`이라는 레이어에 포함되어 있습니다. + +먼저 프로그램에 두 개의 구성 변수를 추가하여 사용할 레이어를 명시하고 그들을 활성화 할것인지를 알려 줍니다. 저는 디버깅 모드인지 아닌지에 따라 값을 설정하도록 했습니다. `NDEBUG` 매크로는 C++ 표준에 포함된 매크로로 "디버그가 아님"을 의미합니다. + +```c++ +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; + +const std::vector validationLayers = { + "VK_LAYER_KHRONOS_validation" +}; + +#ifdef NDEBUG + const bool enableValidationLayers = false; +#else + const bool enableValidationLayers = true; +#endif +``` + +`checkValidationLayerSupport` 함수를 추가하여 요청한 레이어들이 모두 사용 가능한지를 체크합니다. 먼저 가용한 모든 레이어 목록을 `vkEnumerateInstanceLayerProperties`를 사용해 만듭니다. 사용법은 인스턴스 생성 챕터에서 봤던 `vkEnumerateInstanceExtensionProperties`와 동일합니다. + +```c++ +bool checkValidationLayerSupport() { + uint32_t layerCount; + vkEnumerateInstanceLayerProperties(&layerCount, nullptr); + + std::vector availableLayers(layerCount); + vkEnumerateInstanceLayerProperties(&layerCount, availableLayers.data()); + + return false; +} +``` + +다음으로, `validationLayers` 내의 모든 레이어가 `availableLayers` 내에 존재하는지를 체크합니다. `strcmp` 사용을 위해 ``의 include가 필요합니다. + +```c++ +for (const char* layerName : validationLayers) { + bool layerFound = false; + + for (const auto& layerProperties : availableLayers) { + if (strcmp(layerName, layerProperties.layerName) == 0) { + layerFound = true; + break; + } + } + + if (!layerFound) { + return false; + } +} + +return true; +``` + +이제 이 함수를 `createInstance`에서 사용할 수 있습니다. + +```c++ +void createInstance() { + if (enableValidationLayers && !checkValidationLayerSupport()) { + throw std::runtime_error("validation layers requested, but not available!"); + } + + ... +} +``` + +이제 프로그램을 디버그 모드에서 실행해 오류가 발생하지 않는지 확인하세요. 오류가 발생하면, FAQ를 살펴보세요. + +마지막으로, `VkInstanceCreateInfo` 구조체 초기화를 수정해서 사용이 가능한 경우 검증 레이어의 이름을 포함하도록 하세요. + +```c++ +if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); +} else { + createInfo.enabledLayerCount = 0; +} +``` + +체크 과정이 성공적이라면 `vkCreateInstance`가 `VK_ERROR_LAYER_NOT_PRESENT` 오류를 반환하지 않을 것이지만, 확인을 위해 실행해 보시기 바랍니다. + +## 메시지 콜백 + +검증 레이어는 기본적으로 디버그 메시지를 표준 출력창에 표시하지만, 프로그램에서 명시적으로 콜밸을 제공하여 우리가 원하는 방식대로 처리할 수도 있습니다. 이렇게 하면 어떤 종류의 메시지를 보기 원하는지 선택할 수 있는데 모든 메시지들이 (치명적인) 오류에 관한 것은 아니기 때문입니다. 지금은 그냥 그대로 두고 싶다면 이 챕터의 마지막으로 넘어가셔도 됩니다. + +메시지 처리를 위한 콜백을 설정하고 관련한 설정들을 조정하고 싶다면, `VK_EXT_debug_utils` 확장을 사용해 디버그 메신저(messenger) 콜백을 설정해야 합니다. + +먼저 검증 레이어가 활성화 되었는지 여부에 따라 필요한 확장 목록을 반환하는 `getRequiredExtensions` 함수를 만들겠습니다. + +```c++ +std::vector getRequiredExtensions() { + uint32_t glfwExtensionCount = 0; + const char** glfwExtensions; + glfwExtensions = glfwGetRequiredInstanceExtensions(&glfwExtensionCount); + + std::vector extensions(glfwExtensions, glfwExtensions + glfwExtensionCount); + + if (enableValidationLayers) { + extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME); + } + + return extensions; +} +``` + +GLFW가 명시한 확장은 항상 필요하지만, 디버그 메시지를 위한 확장은 조건에 따라 추가하였습니다. 여기서 `VK_EXT_DEBUG_UTILS_EXTENSION_NAME` 매크로를 사용하였는데 이는 문자열 리터럴 "VK_EXT_debug_utils"와 동일하는 것에 유의하세요. 이러한 매크로를 사용하면 오타로 인한 오류를 방지할 수 있습니다. + +이제 이 함수를 `createInstance`에서 사용합니다: + +```c++ +auto extensions = getRequiredExtensions(); +createInfo.enabledExtensionCount = static_cast(extensions.size()); +createInfo.ppEnabledExtensionNames = extensions.data(); +``` + +`VK_ERROR_EXTENSION_NOT_PRESENT` 오류가 발생하지 않는지 프로그램을 실행해 확인하세요. 확장들이 존재하는지는 확인할 필요 없습니다. 왜냐하면 검증 레이어가 가용하다면 당연히 해당 확장들도 사용 가능하기 떄문입니다. + +이제 디버그 콜백의 생김새를 한 번 봅시다. `PFN_vkDebugUtilsMessengerCallbackEXT` 프로토타입을 갖는 `debugCallback`이라는 새 스태틱 멤버 함수를 추가합시다. `VKAPI_ATTR`과 `VKAPI_CALL`를 통해 Vulkan에서 호출하기 위해 적절한 시그니처를 갖고 있는지 확인합니다. + +```c++ +static VKAPI_ATTR VkBool32 VKAPI_CALL debugCallback( + VkDebugUtilsMessageSeverityFlagBitsEXT messageSeverity, + VkDebugUtilsMessageTypeFlagsEXT messageType, + const VkDebugUtilsMessengerCallbackDataEXT* pCallbackData, + void* pUserData) { + + std::cerr << "validation layer: " << pCallbackData->pMessage << std::endl; + + return VK_FALSE; +} +``` + +첫 번쨰 파라메터는 메시지의 심각도를 나타내고, 아래 플래그 중 하나입니다. + +- `VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT`: 진단 메시지 +- `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT`: 리소스의 생성과 같은 정보 메시지 +- `VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT`: 오류는 아니지만 응용 프로그램의 버그일 수 있는 메시지 +- `VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT`: 프로그램이 중단될 수 있는 허용되지 않는 동작에 대한 메시지 + +이러한 열거자들의 값은 비교 연산자를 통해 메시지가 특정 심각도와 같거나 더 심각한지를 설정할 수 있습니다. 예를 들어 아래와 같습니다: + +```c++ +if (messageSeverity >= VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT) { + // Message is important enough to show +} +``` + +`messageType` 매개변수는 아래와 같은 값을 가질 수 있습니다: + +- `VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT`: 명세 또는 성능과 관계없는 이벤트가 발생함 +- `VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT`: 명세를 위반했거나 실수일 수 있는 경우 +- `VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT`: Vulkan에서 효율적이지 않을 수 있음 + +`pCallbackData` 매개변수는 메세지의 상세 사항을 포함하여 아래와 같은 아주 중요한 매개변수를 갖는 `VkDebugUtilsMessengerCallbackDataEXT` 구조체를 참조합니다: + +- `pMessage`: 널문자로 끝나는 문자열(string) 디버그 메시지 +- `pObjects`: 메시지와 관련 있는 Vulkan 객체 핸들의 배열 +- `objectCount`: 배열 내 객체의 숫자 + +마지막으로, `pUserData` 매개변수는 콜백 설정 시 명시한 포인터를 포함하며 사용자가 원하는 데이터를 전달할 수 있도록 합니다. + +콜백은 불리언(boolean)을 반환하는데 해당 검증 레이어를 촉발한(triggered) Vulkan 호출이 중단(aborted)되어야 하는지 여부를 의미합니다. true가 반환되었다면 `VK_ERROR_VALIDATION_FAILED_EXT` 오류와 함께 호출이 중단됩니다. 보통 true 반환은 검증레이어 자체를 테스트 할 때에만 사용되므로 항상 `VK_FALSE`를 반환하는 것이 맞습니다. + +이제 남은 것은 Vulkan에 콜백 함수를 알려주는 것 뿐입니다. 놀랍게도 Vulkan에서는 디버그 콜백조차 명시적으로 생성되고 소멸되는 핸들을 가지고 관리해야 합니다. 이러한 콜백은 *debug messenger*의 일부분으로 원하는 개수만큼 가질 수 있습니다. `instance` 바로 아래에 이 핸들을 위한 멤버를 추가 합시다: + +```c++ +VkDebugUtilsMessengerEXT debugMessenger; +``` + +이제 `initVulkan`에서 호출할 `setupDebugMessenger` 함수를 `createInstance` 바로 다음에 추가합니다: + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); +} + +void setupDebugMessenger() { + if (!enableValidationLayers) return; + +} +``` + +메신저에 대한 세부 사항과 콜백에 대한 내용을 구조체에 채웁니다: + +```c++ +VkDebugUtilsMessengerCreateInfoEXT createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; +createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; +createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; +createInfo.pfnUserCallback = debugCallback; +createInfo.pUserData = nullptr; // Optional +``` + +`messageSeverity` 필드는 여러분의 콜백이 호출될 심각도 타입을 모두 명시할 수 있도록 되어 있습니다. 저는 `VK_DEBUG_UTILS_MESSAGE_SEVERITY_INFO_BIT_EXT`를 제외하고 모두 명시해서, 잠재적 문제는 모두 받고, 일반적인 디버깅 정보는 포함하지 않도록 했습니다. + +`messageType` 필드는 콜백이 알림을 받을 메시지 타입을 필터링 할 수 있도록 합니다. 여기서는 모든 타입을 활성화 하였습니다. 필요하지 않으면 몇 개를 비활성화 해도 됩니다. + +마지막으로 `pfnUserCallback`필드는 콜백 함수에 대한 포인터를 명시합니다. 선택적으로 `pUserData` 필드에 포인터를 전달하여 콜백 함수와 함께 전달될 파라메터를 명시할 수 있습니다. 예를 들어 `HelloTriangleApplication` 클래스의 포인터를 전달하는 데 사용할 수 있습니다. + +검증 레이어 메시지와 디버그 콜백을 구성하는 다양한 방법이 있지만, 이 튜토리얼을 위한 시작으로는 이 정도가 좋습니다. [확장 명세](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap50.html#VK_EXT_debug_utils)에서 보다 다양한 정보를 볼 수 있습니다. + +`VkDebugUtilsMessengerEXT` 객체를 생성하기 위해 `vkCreateDebugUtilsMessengerEXT` 함수에 이 구조체를 전달해야 합니다. 안타깝게도 이 함수는 확장 함수이기 때문에 자동적으로 로드되지 않습니다. `vkGetInstanceProcAddr`를 사용해 주소값을 얻어야 합니다. 이를 처리하기 위한 방안으로 프록시 함수를 만들 것입니다. 저는 `HelloTriangleApplication` 클래스 정의 바로 다음에 추가 하였습니다. + +```c++ +VkResult CreateDebugUtilsMessengerEXT(VkInstance instance, const VkDebugUtilsMessengerCreateInfoEXT* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkDebugUtilsMessengerEXT* pDebugMessenger) { + auto func = (PFN_vkCreateDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkCreateDebugUtilsMessengerEXT"); + if (func != nullptr) { + return func(instance, pCreateInfo, pAllocator, pDebugMessenger); + } else { + return VK_ERROR_EXTENSION_NOT_PRESENT; + } +} +``` + +함수가 로드될 수 없으면 `vkGetInstanceProcAddr`함수는 `nullptr`을 반환합니다. 이제 이 함수를 호출하여 가능한 경우에 확장 객체를 생성할 수 있습니다. + +```c++ +if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { + throw std::runtime_error("failed to set up debug messenger!"); +} +``` + +두 번째부터 마지막까지의 매개변수는 선택적인 할당 콜백으로 `nullptr`로 설정할 것이고, 이를 제외하면 나머지는 직관적입니다. 디버그 메신저는 우리의 Vulkan 인스턴스와 레이어에 한정되어 있으므로 첫 인자에서 명시해야만 합니다. 이러한 패턴과 다른 _child_ 객체들은 나중에 다시 보게 될 것입니다. + +`VkDebugUtilsMessengerEXT` 객체는 `vkDestroyDebugUtilsMessengerEXT`호출로 정리되어야 합니다. `vkCreateDebugUtilsMessengerEXT`와 마찬가지로 이 함수는 명시적으로 로드되어야 합니다. + +`CreateDebugUtilsMessengerEXT` 바로 밑에 또 다른 프록시 함수를 만듭시다. + +```c++ +void DestroyDebugUtilsMessengerEXT(VkInstance instance, VkDebugUtilsMessengerEXT debugMessenger, const VkAllocationCallbacks* pAllocator) { + auto func = (PFN_vkDestroyDebugUtilsMessengerEXT) vkGetInstanceProcAddr(instance, "vkDestroyDebugUtilsMessengerEXT"); + if (func != nullptr) { + func(instance, debugMessenger, pAllocator); + } +} +``` + +이 함수는 정적(static) 멤버 함수 또는 클래스 밖의 함수로 만드는 것을 잊지 마십시오. 이 함수를 `cleanup` 함수에서 호출합니다. + +```c++ +void cleanup() { + if (enableValidationLayers) { + DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); + } + + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +## 인스턴스의 생성과 소멸 디버깅 + +검증 레이어와 디버깅을 프로그램에 추가하였지만 아직 모든 것이 완료된 것은 아닙니다. `vkCreateDebugUtilsMessengerEXT` 호출을 위해서는 그 전에 유효한 인스턴스가 생성되어야 하고, `vkDestroyDebugUtilsMessengerEXT`은 인스턴스가 소멸되기 전에 호출되어야 합니다. 따라서 `vkCreateInstance`와 `vkDestroyInstance` 호출에 있어서 발생하는 문제는 디버깅이 불가능합니다. + +[확장 문서](https://github.com/KhronosGroup/Vulkan-Docs/blob/main/appendices/VK_EXT_debug_utils.adoc#examples)를 자세히 읽어보면 위 두 함수 호출에 대해 별도의 디버깅 메신서를 생성하는 방법이 있다는 것을 알 수 있으실 겁니다. 이를 위해서는 `VkDebugUtilsMessengerCreateInfoEXT` 구조체에 대한 포인터를 `VkInstanceCreateInfo`의 `pNext` 확장 필드에 넘겨주기만 하면 됩니다. 먼저 메신저 생성 정보만 추출해 벌도의 함수로 만듭니다. + +```c++ +void populateDebugMessengerCreateInfo(VkDebugUtilsMessengerCreateInfoEXT& createInfo) { + createInfo = {}; + createInfo.sType = VK_STRUCTURE_TYPE_DEBUG_UTILS_MESSENGER_CREATE_INFO_EXT; + createInfo.messageSeverity = VK_DEBUG_UTILS_MESSAGE_SEVERITY_VERBOSE_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_WARNING_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_SEVERITY_ERROR_BIT_EXT; + createInfo.messageType = VK_DEBUG_UTILS_MESSAGE_TYPE_GENERAL_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_VALIDATION_BIT_EXT | VK_DEBUG_UTILS_MESSAGE_TYPE_PERFORMANCE_BIT_EXT; + createInfo.pfnUserCallback = debugCallback; +} + +... + +void setupDebugMessenger() { + if (!enableValidationLayers) return; + + VkDebugUtilsMessengerCreateInfoEXT createInfo; + populateDebugMessengerCreateInfo(createInfo); + + if (CreateDebugUtilsMessengerEXT(instance, &createInfo, nullptr, &debugMessenger) != VK_SUCCESS) { + throw std::runtime_error("failed to set up debug messenger!"); + } +} +``` + +이제 이를 `createInstance` 함수에서 재사용 할 수 있습니다: + +```c++ +void createInstance() { + ... + + VkInstanceCreateInfo createInfo{}; + createInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO; + createInfo.pApplicationInfo = &appInfo; + + ... + + VkDebugUtilsMessengerCreateInfoEXT debugCreateInfo{}; + if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); + + populateDebugMessengerCreateInfo(debugCreateInfo); + createInfo.pNext = (VkDebugUtilsMessengerCreateInfoEXT*) &debugCreateInfo; + } else { + createInfo.enabledLayerCount = 0; + + createInfo.pNext = nullptr; + } + + if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); + } +} +``` + +`debugCreateInfo` 변수를 if문 밖에 작성하여 `vkCreateInstance` 호출 전에 소멸되지 않도록 하였습니다. 추가적인 디버깅 메신저를 이런 식으로 만들면 `vkCreateInstance` 호출 동안에, 그리고 `vkDestroyInstance` 호출과 이후 정리 과정에 자동적으로 사용됩니다. + +## 테스팅 + +이제 일부러 실수를 만들어서 검증 레이어가 동작하는지 봅시다. `DestroyDebugUtilsMessengerEXT`를 `cleanup`에서 잠시 제거하고 프로그램을 실행해 봅시다. 종료되면 이런 화면을 보시게 될겁니다. + +![](/images/validation_layer_test.png) + +> 아무런 메시지가 보이지 않으면 [설치를 확인해 보세요](https://vulkan.lunarg.com/doc/view/1.2.131.1/windows/getting_started.html#user-content-verify-the-installation). + +어떤 호출이 메시지를 트리거하였는지 보려면 메시지 콜백에 중단점을 걸고 호출 스택을 확인하면 됩니다. + +## 구성(Configuration) + +`VkDebugUtilsMessengerCreateInfoEXT` 구조체에 명시한 플래그 이외에도 더 많은 검증 레이어의 동작에 관한 세팅을 할 수 있습니다. Vulkan SDK로 가서 `Config` 디렉터리를 보세요. `vk_layer_settings.txt` 파일을 찾을 수 있을텐데 레이어를 설정하기 위한 설명이 적혀 있습니다. + +여러분의 응용 프로그램을 위한 레이어 설정을 하기 위해서는 그 파일을 프로젝트의 `Debug` 와 `Release` 디렉터리에 복사하고 원하는 동작을 하도록 안내를 따라 하면 됩니다. 하지만, 이 튜토리얼에서는 기본 세팅을 사용하는 것으로 가정할 것입니다. + +앞으로 튜토리얼에서 검증 레이어의 유용함을 보여드리기 위해서, 그리고 Vulkan이 정확히 어떤 일을 하는지를 아는 것이 얼마나 중요한지 알려드리기 위해 일부러 실수를 하는 경우들이 있을 겁니다. 이제 [시스템의 Vulkan 장치](!kr/Drawing_a_triangle/Setup/Physical_devices_and_queue_families)에 대해 알아봅시다. + +[C++ code](/code/02_validation_layers.cpp) diff --git a/kr/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md b/kr/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md new file mode 100644 index 00000000..952e5db7 --- /dev/null +++ b/kr/03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md @@ -0,0 +1,309 @@ +## 물리적 장치 선택 + +VkInstance를 통해 Vulkan 라이브러리를 초기화 한 이후에는 우리가 필요로 하는 기능을 지원하는 시스템의 그래픽 카드를 찾고 선택해야 합니다. 여러 대의 그래픽 카드를 선택하고 동시에 사용할 수도 있습니다. 하지만 이 튜토리얼에서는 우리의 요구에 맞는 첫 번째 그래픽 카드만을 사용하도록 할 것입니다. + +`pickPhysicalDevice` 함수를 추가하고 `initVulkan` 함수에서 이 함수를 호출하도록 합시다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + pickPhysicalDevice(); +} + +void pickPhysicalDevice() { + +} +``` + +우리가 선택할 그래픽 카드는 새롭게 클래스 멤버로 추가된 VkPhysicalDevice 핸들에 저장됩니다. 이 객체는 VkInstance가 소멸될 때 암시적(implicitly)으로 소멸되므로, `cleanup`에 무언가를 추가할 필요는 없습니다. + +```c++ +VkPhysicalDevice physicalDevice = VK_NULL_HANDLE; +``` + +그래픽 카드의 목록을 불러오는 것은 확장의 목록을 불러오는 것과 비슷하며 그 개수를 질의(query)하는 것으로 시작됩니다. + +```c++ +uint32_t deviceCount = 0; +vkEnumeratePhysicalDevices(instance, &deviceCount, nullptr); +``` + +Vulkan을 지원하는 장치가 없으면 더 진행할 이유가 없겠죠. + +```c++ +if (deviceCount == 0) { + throw std::runtime_error("failed to find GPUs with Vulkan support!"); +} +``` + +그렇지 않으면 모든 VkPhysicalDevice 핸들을 저장할 배열을 할당합니다. + +```c++ +std::vector devices(deviceCount); +vkEnumeratePhysicalDevices(instance, &deviceCount, devices.data()); +``` + +이제 각 장치를 순회하면서 우리가 하고자 하는 작업에 적합한지 확인합니다. 모든 그래픽 카드가 같지는 않기 때문입니다. 이를 위해 아래와 같은 새 함수를 만듭니다: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + return true; +} +``` + +그리고 어떤 물리적 장치든 요구사항에 맞는 것이 있는지를 확인합니다. + +```c++ +for (const auto& device : devices) { + if (isDeviceSuitable(device)) { + physicalDevice = device; + break; + } +} + +if (physicalDevice == VK_NULL_HANDLE) { + throw std::runtime_error("failed to find a suitable GPU!"); +} +``` + +다음 섹션에서는 우리가 `isDeviceSuitable`에서 확인할 첫 번째 요구사항을 소개할 것입니다. 이후 챕터에서 보다 많은 Vulkan 기능을 사용할 것이기 때문에 보다 많은 요구사항을 확인하도록 확장해 나갈 것입니다. + +## 기본 장치 적합성(suitability) 확인 + +장치의 적합성을 확인하기 위해 몇 가지 세부사항을 질의할 것입니다. 장치의 기본적인 속성인 이름, 타입, 지원하는 Vulkan 버전 등은 vkGetPhysicalDeviceProperties를 사용해 질의할 수 있습니다. + +```c++ +VkPhysicalDeviceProperties deviceProperties; +vkGetPhysicalDeviceProperties(device, &deviceProperties); +``` + +텍스처 압축, 64비트 float, 다중 뷰포트 렌더링(VR에서 유용합니다) 등과 같은 추가적인 기능을 지원하는지 여부는 vkGetPhysicalDeviceFeatures를 사용해 질의할 수 있습니다. + +```c++ +VkPhysicalDeviceFeatures deviceFeatures; +vkGetPhysicalDeviceFeatures(device, &deviceFeatures); +``` + +장치 메모리라던가, 큐 패밀리(queue family)와 같은 더 세부적인 사항에 대한 질의도 가능하며, 이에 대해서는 이후에 논의할 것입니다(다음 섹션 참고). + +예를 들어, 우리 응용 프로그램이 지오메트리(geometry) 셰이더를 지원하는 장치에서만 사용할 수 있도록 하고 싶습니다. 그러면 `isDeviceSuitable` 함수는 아래와 같이 구현할 수 있습니다. + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + VkPhysicalDeviceProperties deviceProperties; + VkPhysicalDeviceFeatures deviceFeatures; + vkGetPhysicalDeviceProperties(device, &deviceProperties); + vkGetPhysicalDeviceFeatures(device, &deviceFeatures); + + return deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU && + deviceFeatures.geometryShader; +} +``` + +장치가 적합한지 아닌지만 체크해서 첫 번째 장치를 선택하는 대신, 각 장치에 점수를 부여하고 가장 높은 점수의 장치를 선택하게 할 수도 있습니다. 이렇게 하면 적합한 장치에 더 많은 점수를 부여할 수 있지만 그러한 경우 적합한 장치가 내장 그래픽(integrated GPU) 카드일 경우 그 장치가 선택될 수도 있습니다. 이러한 방식은 다음과 같이 구현할 수 있습니다. + +```c++ +#include + +... + +void pickPhysicalDevice() { + ... + + // Use an ordered map to automatically sort candidates by increasing score + std::multimap candidates; + + for (const auto& device : devices) { + int score = rateDeviceSuitability(device); + candidates.insert(std::make_pair(score, device)); + } + + // Check if the best candidate is suitable at all + if (candidates.rbegin()->first > 0) { + physicalDevice = candidates.rbegin()->second; + } else { + throw std::runtime_error("failed to find a suitable GPU!"); + } +} + +int rateDeviceSuitability(VkPhysicalDevice device) { + ... + + int score = 0; + + // Discrete GPUs have a significant performance advantage + if (deviceProperties.deviceType == VK_PHYSICAL_DEVICE_TYPE_DISCRETE_GPU) { + score += 1000; + } + + // Maximum possible size of textures affects graphics quality + score += deviceProperties.limits.maxImageDimension2D; + + // Application can't function without geometry shaders + if (!deviceFeatures.geometryShader) { + return 0; + } + + return score; +} +``` + +이 튜토리얼에서 이런 모든 기능을 구현할 필요는 없지만 여러분의 장치 선택 과정을 어떻게 설계할 수 있을지에 대한 아이디어는 얻게 되셨을겁니다. 물론 후보 장치들의 이름을 보여주고 유저가 선택하도록 할 수도 있습니다. + +지금은 시작하는 단계이므로 Vulkan 지원 여부만 있으면 되고 그러니 그냥 아무 GPU나 선택하도록 하겠습니다. + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + return true; +} +``` + +다음 섹션에서는 첫 번째 실제 필요로 하는 기능을 체크해 보겠습니다. + +## 큐 패밀리(Queue families) + +그리기부터 텍스처 업로드까지 거의 대부분의 Vulkan 명령 실행 전에, 명령(command)들이 큐에 제출되어야만 합니다. 다양한 종류의 *큐 패밀리*로부터 도출된 다양한 종류의 큐가 있으며, 각 큐 패밀리는 처리할 수 있는 명령이 제한되어 있습니다. 예를 들어 계산(compute) 명령만을 처리할 수 있는 큐 패밀리가 있고, 메모리 전송 관련 명령만을 처리할 수 있는 큐 패밀리도 있습니다. + +장치가 어떤 큐 패밀리를 지원하는지와 이들 중 어떤 것이 우리가 사용하고자 하는 명령을 지원하는지를 체크해야만 합니다. 이를 위해 `findQueueFamilies` 함수를 추가하고 우리가 필요로하는 큐 패밀리들을 찾도록 해 봅시다. + +지금은 그래픽스 명령을 지원하는 큐만 확인할 것이므로 함수는 아래와 같습니다: + +```c++ +uint32_t findQueueFamilies(VkPhysicalDevice device) { + // Logic to find graphics queue family +} +``` + +하지만, 이후 챕터부터 또다른 큐가 필요하기 때문에 이를 대비해 인덱스를 구조체로 만드는 것이 낫습니다: + +```c++ +struct QueueFamilyIndices { + uint32_t graphicsFamily; +}; + +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + // Logic to find queue family indices to populate struct with + return indices; +} +``` + +큐 패밀리를 지원하지 않으면 어떻게 될까요? `findQueueFamilies`에서 예외를 throw할 수도 있지만, 이 함수는 장치 적합성을 확인하기 위한 목적으로는 적합하지 않습니다. 예를 들어 전송(transfer) 큐 패밀리가 있는 장치를 *선호*하긴 하지만 필수 요구사항은 아닐수도 있습니다. 따라서 특정한 큐 패밀리가 있는지 알려주는 방법이 필요합니다. + +큐 패밀리가 존재하지 않는것에 대한 마법같은 인덱스를 사용하는 방법은 없습니다. `0`을 포함해서 모든 `uint32_t` 값이 사실상 유효한 큐 패밀리의 인덱스일 수 있기 때문입니다. 다행히 C++17에서는 값이 존재하는지 아닌지를 구분할 수 있는 자료 구조를 지원합니다. + +```c++ +#include + +... + +std::optional graphicsFamily; + +std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // false + +graphicsFamily = 0; + +std::cout << std::boolalpha << graphicsFamily.has_value() << std::endl; // true +``` + +`std::optional`은 무언가 값을 할당하기 전에는 값이 없는 상태를 나타낼 수 있는 래퍼(wrapper)입니다. 어느 시점에 여러분은 그 안에 값이 있는지 없는지를 `has_value()` 멤버 함수를 통해 확인할 수 있습니다. 따라서 로직을 아래와 같이 바꿀 수 있습니다: + +```c++ +#include + +... + +struct QueueFamilyIndices { + std::optional graphicsFamily; +}; + +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + // Assign index to queue families that could be found + return indices; +} +``` + +이제 실제로 `findQueueFamilies`를 구현할 수 있습니다: + +```c++ +QueueFamilyIndices findQueueFamilies(VkPhysicalDevice device) { + QueueFamilyIndices indices; + + ... + + return indices; +} +``` + +큐 패밀리의 목록을 가져오는 과정은 예상하실 수 있듯이 `vkGetPhysicalDeviceQueueFamilyProperties`를 사용하는 것입니다. + +```c++ +uint32_t queueFamilyCount = 0; +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); + +std::vector queueFamilies(queueFamilyCount); +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); +``` + +VkQueueFamilyProperties 구조체는 지원하는 연산의 종류와 해당 패밀리로부터 생성될 수 있는 큐의 개수 등의 큐 패밀리 세부 사항을 포함하고 있습니다. 우리는 `VK_QUEUE_GRAPHICS_BIT`을 지원하는 최소한 하나의 큐 패밀리를 찾아야만 합니다. + +```c++ +int i = 0; +for (const auto& queueFamily : queueFamilies) { + if (queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) { + indices.graphicsFamily = i; + } + + i++; +} +``` + +이제 큐 패밀리 룩업(lookup) 함수가 있으니 `isDeviceSuitable` 함수에서 이를 사용해 장치가 우리가 사용하고자 하는 명령을 처리할 수 있는지 확인합니다: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + return indices.graphicsFamily.has_value(); +} +``` + +좀 더 편리하게 사용하기 위해, 구조체 안에도 확인 기능을 추가합니다: + +```c++ +struct QueueFamilyIndices { + std::optional graphicsFamily; + + bool isComplete() { + return graphicsFamily.has_value(); + } +}; + +... + +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + return indices.isComplete(); +} +``` + +`findQueueFamilies` 에서 빠른 종료를 위해서도 사용합니다: + +```c++ +for (const auto& queueFamily : queueFamilies) { + ... + + if (indices.isComplete()) { + break; + } + + i++; +} +``` + +좋습니다. 우선은 적절한 물리적 장치를 찾는 것은 이것으로 끝입니다! 다음 단계는 [논리적 장치](!kr/Drawing_a_triangle/Setup/Logical_device_and_queues)와의 인터페이스를 만드는 것입니다. + +[C++ code](/code/03_physical_device_selection.cpp) diff --git a/kr/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md b/kr/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md new file mode 100644 index 00000000..ecc58bc4 --- /dev/null +++ b/kr/03_Drawing_a_triangle/00_Setup/04_Logical_device_and_queues.md @@ -0,0 +1,132 @@ +## 개요 + +사용할 물리적 장치를 선택한 뒤에는 이와 상호작용할 *논리적 장치*를 설정해야 합니다. 논리적 장치의 생성 과정은 인스턴스 생성 과정과 비슷하고 우리가 사용할 기능들을 기술해야 합니다. 또한 이제는 어떤 큐 패밀리가 가용한지를 알아냈기 때문에 어떤 큐를 생성할지도 명시해야 합니다. 만일 요구사항이 다양하다면, 하나의 물리적 장치로부터 여러 개의 논리적 장치를 만들 수도 있습니다. + +논리적 장치에 대한 핸들을 저장한 멤버를 클래스에 생성하는 것부터 시작합니다. + +```c++ +VkDevice device; +``` + +다음으로 `initVulkan`에서 호출할 `createLogicalDevice` 함수를 추가합니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + pickPhysicalDevice(); + createLogicalDevice(); +} + +void createLogicalDevice() { + +} +``` + +## 생성할 큐 명시하기 + +논리적 장치를 생성하는 것은 이전처럼 여러 세부사항을 구조체에 명시하는 과정을 포함하며, 그 첫번째가 `VkDeviceQueueCreateInfo`입니다. 이 구조체는 하나의 큐 패밀리에 대한 큐의 개수를 명시합니다. 현재 우리는 그래픽스 기능 관련 큐에만 관심이 있습니다. + +```c++ +QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + +VkDeviceQueueCreateInfo queueCreateInfo{}; +queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; +queueCreateInfo.queueFamilyIndex = indices.graphicsFamily.value(); +queueCreateInfo.queueCount = 1; +``` + +현재의 드라이버들은 큐 패밀리 하나당 적은 수의 큐만을 생성할 수 있도록 제한되어 있고, 여러분도 하나 이상 필요하지는 않을겁니다. 왜냐하면 여러 쓰레드(thread)에 필요한 커맨드 버퍼들을 모두 생성해 두고 메인 쓰레드에서 적은 오버헤드의 호출로 이들을 한꺼번에 제출(submit)할 수 있기 떄문입니다. + +Vulkan에서는 커맨드 버퍼의 실행 스케줄에 영향을 주는 큐의 우선순위를 `0.0`과 `1.0` 사이의 부동소수점 값으로 명시할 수 있게 되어 있습니다. 큐가 하나밖에 없더라도 이를 명시해 주어야만 합니다: + +```c++ +float queuePriority = 1.0f; +queueCreateInfo.pQueuePriorities = &queuePriority; +``` + +## 사용할 장치 기능 명시하기 + +다음으로는 우리가 사용할 장치의 기능을 명시해야 합니다. 이는 이전 챕터의 지오메트리 셰이더를 `vkGetPhysicalDeviceFeatures`로 질의했던 것과 비슷합니다. 지금은 특별한 기능이 필요 없으니 그냥 정의만 해 두고 모든 값을 `VK_FALSE`로 둡시다. 나중에 Vulkan을 사용해 좀 더 흥미로운 것들을 할 때 다시 이 구조체를 사용할 것입니다. + +```c++ +VkPhysicalDeviceFeatures deviceFeatures{}; +``` + +## 논리적 장치 생성하기 + +이전 두 개의 구조체가 준비되었으니 `VkDeviceCreateInfo` 구조체를 채워 봅시다. + +```c++ +VkDeviceCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; +``` + +먼저 큐 생성 정보와 장치 기능 구조체에 대한 포인터를 추가합니다. + +```c++ +createInfo.pQueueCreateInfos = &queueCreateInfo; +createInfo.queueCreateInfoCount = 1; + +createInfo.pEnabledFeatures = &deviceFeatures; +``` + +나머지 정보는 `VkInstanceCreateInfo` 구조체와 비슷해서 확장이나 검증 레이어를 명시할 수 있습니다. 차이점은 이것들이 이번에는 장치에 종속적(device specific)이라는 것입니다. + +장치에 종속적인 확장 중 하나의 예시로는 `VK_KHR_swapchain`가 있는데, 렌더링된 이미지를 장치로부터 윈도우로 전달하는 기능입니다. 시스템의 Vulkan 장치가 이 기능을 지원하지 않을 수 있습니다. 예를 들어 계산 명령만 수행하는 장치일 경우에 그렇습니다. 이 확장에 대한 설명은 나중에 스왑 체인 챕터에서 다시 살펴볼 것입니다. + +Vulkan의 예전 구현에서는 인스턴스와 장치 종속적인 검증 레이어가 구분되어 있었으나, [지금은 아닙니다](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap40.html#extendingvulkan-layers-devicelayerdeprecation). 즉, `VkDeviceCreateInfo`의 `enabledLayerCount` 와 `ppEnabledLayerNames` 필드가 최신 구현에서는 무시됩니다. 하지만, 이전 버전과의 호환성을 위해 어쨌든 설정해 주는 것이 좋습니다. + +```c++ +createInfo.enabledExtensionCount = 0; + +if (enableValidationLayers) { + createInfo.enabledLayerCount = static_cast(validationLayers.size()); + createInfo.ppEnabledLayerNames = validationLayers.data(); +} else { + createInfo.enabledLayerCount = 0; +} +``` + +지금은 장치 종속적인 확장은 필요하지 않습니다. + +이제 `vkCreateDevice` 함수를 사용해 논리적 장치를 생성할 준비가 되었습니다. + +```c++ +if (vkCreateDevice(physicalDevice, &createInfo, nullptr, &device) != VK_SUCCESS) { + throw std::runtime_error("failed to create logical device!"); +} +``` + +매개변수들은 상호작용할 물리적 장치, 큐와 방금 명시한 사용 정보, 선택적으로 명시할 수 있는 콜백에 대한 포인터, 마지막으로 논리적 장치를 저장할 핸들에 대한 포인터입니다. 인스턴스 생성 함수와 유사하게 이 호출은 존재하지 않는 확장을 활성화 한다거나, 지원하지 않는 기능을 명시하는 경우 오류를 반환합니다. + +장치는 `cleanup`에서 `vkDestroyDevice`함수를 통해 소멸되어야 합니다. + +```c++ +void cleanup() { + vkDestroyDevice(device, nullptr); + ... +} +``` + +논리적 장치는 인스턴스와 직접적으로 상호작용하지 않으므로 인스턴스는 매개변수에 포함되지 않습니다. + +## 큐 핸들 얻기(Retrieving) + +큐는 논리적 장치와 함께 생성되지만 아직 이들과 상호작용하기 위한 핸들은 얻지 못했습니다. 먼저 그래픽스 큐에 대한 핸들을 클래스 멤버에 추가해 줍시다. + +```c++ +VkQueue graphicsQueue; +``` + +장치 큐는 장치가 소멸될 때 자동으로 정리되므로 `cleanup`에서 해 주어야 할 일은 따로 없습니다. + +`vkGetDeviceQueue`함수를 사용해 각 큐 패밀리에 대한 핸들을 얻어올 수 있습니다. 매개변수는 논리적 장치, 큐 패밀리, 큐 인덱스, 큐 핸들을 저장할 변수의 포인터 입니다. 이 패밀리에서 하나의 큐만 생성하고 있으므로 인덱스는 간단히 `0`으로 설정하면 됩니다. + +```c++ +vkGetDeviceQueue(device, indices.graphicsFamily.value(), 0, &graphicsQueue); +``` + +논리적 장치와 큐의 핸들이 확보 되었으니 이제 실제로 그래픽 카드를 사용해 무언가를 할 수 있습니다! 다음 몇 개 챕터에서는 윈도우 시스템에 결과를 표시하기 위한 리소스들을 설정해 보겠습니다. + +[C++ code](/code/04_logical_device.cpp) diff --git a/kr/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md b/kr/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md new file mode 100644 index 00000000..2e961b41 --- /dev/null +++ b/kr/03_Drawing_a_triangle/01_Presentation/00_Window_surface.md @@ -0,0 +1,169 @@ +Vulkan은 플랫폼 독립적인 API이기 때문에, 윈도우 시스템과 직접적으로 소통할 수는 없습니다. Vulkan과 윈도우 시스템간의 연결을 만들어 결과물을 화면에 보이도록 하기 위해서 우리는 WSI (Window System Integration)을 사용해야만 합니다. 이 챕터에서는 먼저 `VK_KHR_surface`를 살펴볼 것입니다. `VK_KHR_surface` 객체는 렌더링된 이미지를 표현할 표면(surface)의 추상화된 객체입니다. 우리 프로그램에서 표면은 GLFW를 사용해 열어놓은 윈도우가 뒷받침할 것입니다. + +`VK_KHR_surface` 확장은 인스턴스 수준의 확장이고, 우리는 이미 활성화 시켜 놓았습니다. 왜냐하면 `glfwGetRequiredInstanceExtensions`를 통해 반환된 리스트에 포함되어 있거든요. 이 리스트는 다음 몇 챕터에서 사용할 다른 WSI 확장도 포함하고 있습니다. + +윈도우 표면은 인스턴스 생성 이후에 곧바로 생성해야 하는데 이는 윈도우 표면이 물리적 장치의 선택에 영향을 주기 때문입니다. 이 내용을 여기까지 미룬 이유는 윈도우 표면이 렌더 타겟과 표현과 관련된 큰 주제이고, 이러한 내용으로 인해 기본적인 세팅 설명을 복잡하게 만들고 싶지 않았기 때문입니다. 또한 윈도우 표면은 Vulkan에서 선택적인 구성요소로, 오프 스크린(off-screen) 렌더링을 할 경우에는 필요하지 않습니다. Vulkan은 보이지 않는 윈도우를 생성하는 등의 편법을 동원하지 않고서도 이런 기능을 사용 가능합니다 (OpenGL에서는 편법으로 구현해야만 합니다). + +## 윈도우 표면 생성 + +`surface` 클래스 멤버를 디버그 콜백 바로 뒤에 추가하는 것 부터 시작합니다. + +```c++ +VkSurfaceKHR surface; +``` + +`VkSurfaceKHR`객체와 그 활용은 플랫폼 독립적이지만, 생성에 있어서는 윈도우 시스템의 세부 사항에 의존적입니다. 예를 들어, 윈도우에서는 `HWND` 와 `HMODULE` 핸들이 필요합니다. 따라서 플랫폼 의존적인 확장들이 존재하고 윈도우의 경우 이 확장은 `VK_KHR_win32_surface`입니다. 이 확장은 `glfwGetRequiredInstanceExtensions`에 자동적으로 포함되어 있습니다. + +윈도우즈에서 표면을 생성하기 위해 이러한 플랫폼별 확장을 사용하는 예시를 보여드리겠습니다. 하지만 이 튜토리얼에서 이를 실제 사용하진 않을 것입니다. GLFW와 같은 라이브러리를 사용하면서도 플랫폼별 코드를 사용하는 것은 적절하지 않습니다. GLFW에서는 `glfwCreateWindowSurface`를 통해 플랫폼별 차이에 따른 코드를 처리해 줍니다. 그래도, 사용하기 전에 뒤쪽에서 어떤 일이 벌어지는지는 알아두는 것이 좋겠죠. + +네이티브 플랫폼 기능에 접근하기 위해서는 위쪽에 include를 바꿔줘야 합니다. + +```c++ +#define VK_USE_PLATFORM_WIN32_KHR +#define GLFW_INCLUDE_VULKAN +#include +#define GLFW_EXPOSE_NATIVE_WIN32 +#include +``` + +윈도우 표면은 Vulkan 객체기 때문에 우리가 값을 채워야 하는 `VkWin32SurfaceCreateInfoKHR` 구조체를 사용해야 합니다. 여기에는 두 개의 중요한 매개변수가 있는데 `hwnd` 와 `hinstance`입니다. 이는 윈도우와 프로세스에 대한 핸들입니다. + +```c++ +VkWin32SurfaceCreateInfoKHR createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; +createInfo.hwnd = glfwGetWin32Window(window); +createInfo.hinstance = GetModuleHandle(nullptr); +``` + +`glfwGetWin32Window`함수는 GLFW 윈도우 객체로부터 `HWND`를 얻기위해 사용됩니다. `GetModuleHandle` 호출은 현재 프로세스의 `HINSTANCE` 핸들을 반환해줍니다. + +이후에는 `vkCreateWin32SurfaceKHR`를 통해 표면을 생성할 수 있는데 매개변수는 인스턴스, 표면 생성 세부사항, 사용자 정의 할당자와 표면 핸들 저장을 위한 변수입니다. 정확히 하자면 이는 WSI 확장 함수이지만, 자주 사용되는 관계로 표준 Vulkan 로더에 포함되어 있고, 그렇기 때문에 명시적으로 로드할 필요가 없습니다. + +```c++ +if (vkCreateWin32SurfaceKHR(instance, &createInfo, nullptr, &surface) != VK_SUCCESS) { + throw std::runtime_error("failed to create window surface!"); +} +``` + +이 과정은 리눅스 등 다른 플랫폼에서도 유사한데, 이 경우 `vkCreateXcbSurfaceKHR`는 XCB 커넥션과 윈도우, X11 등 세부사항을 생성 시에 명시해 주어야 합니다. + +`glfwCreateWindowSurface` 함수는 이러한 과정을 각 플랫폼별로 구현해 두었습니다. 우리는 이를 우리의 프로그램에 통합해서 사용하기만 하면 됩니다. `initVulkan`에서 호출할 `createSurface` 함수를 인스턴스 생성과 `setupDebugMessenger` 뒤에 추가하겠습니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); +} + +void createSurface() { + +} +``` + +GLFW 호출은 구조체 대신 간단한 매개변수들을 받기 때문에 구현이 직관적입니다. + +```c++ +void createSurface() { + if (glfwCreateWindowSurface(instance, window, nullptr, &surface) != VK_SUCCESS) { + throw std::runtime_error("failed to create window surface!"); + } +} +``` + +매개변수는 `VkInstance`, GLFW 윈도우에 대한 포인터, 사용자 정의 할당자와 `VkSurfaceKHR` 변수에 대한 포인터입니다. 내부적으로는 플랫폼 관련 호출을 한 뒤에 `VkResult` 값을 전달하여 반환해 줍니다. GLFW는 표면 소멸을 위한 특별한 함수를 제공하지는 않으므로, 기본(original) API를 통해 구현해야 합니다: + +```c++ +void cleanup() { + ... + vkDestroySurfaceKHR(instance, surface, nullptr); + vkDestroyInstance(instance, nullptr); + ... + } +``` + +표면이 인스턴스보다 먼저 소멸되도록 해야 합니다. + +## 표현 지원에 대한 질의(Querying for presentation support) + +Vulkan 구현이 윈도우 시스템 통합을 지원하지만, 그렇다고 모든 시스템의 장치가 이를 지원한다는 의미는 아닙니다. 따라서 `isDeviceSuitable`를 확장하여 장치가 우리가 만든 표면에 이미지를 표현할 수 있는지를 확인해야 합니다. 표현(presentation)은 큐의 기능이므로 이는 우리가 만든 표면에 표현 기능을 지원하는 큐 패밀리를 찾는 문제로 귀결됩니다. + +그리기 명령(drawing command)을 지원하는 큐 패밀리와 표현 기능을 지원하는 큐 패밀리가 동일하지 않을 수 있습니다. 따라서 `QueueFamilyIndices` 구조체를 수정해 별도의 표현 큐 상황을 고려하겠습니다. + +```c++ +struct QueueFamilyIndices { + std::optional graphicsFamily; + std::optional presentFamily; + + bool isComplete() { + return graphicsFamily.has_value() && presentFamily.has_value(); + } +}; +``` + +다음으로, `findQueueFamilies`함수를 수정해서 윈도우 표면에 표현 기능이 있는 큐 패밀리를 찾아 봅시다. 이를 체크하기 위한 함수는 `vkGetPhysicalDeviceSurfaceSupportKHR` 함수이고, 물리적 장치, 큐 패밀리 인덱스와 표면을 매개변수로 받습니다. `VK_QUEUE_GRAPHICS_BIT`와 동일한 루프에 이 함수의 호출을 추가합니다: + +```c++ +VkBool32 presentSupport = false; +vkGetPhysicalDeviceSurfaceSupportKHR(device, i, surface, &presentSupport); +``` + +불리언 값을 확인하고 표현 패밀리의 큐 인덱스를 저장합니다: + +```c++ +if (presentSupport) { + indices.presentFamily = i; +} +``` + +알아두셔야 할 것은 결국 이 두 큐 패밀리는 동일할 가능성이 아주 높다는 것입니다. 하지만 이 프로그램에서는 접근법의 일관성을 위해 이 둘을 별개의 큐인 것처럼 처리할 것입니다. 그리기와 표현을 동일한 큐에서 지원하는 물리적 장치를 선호하도록 로직을 구현하면 성능이 개선될 수 있습니다. + +## 표현 큐 생성하기 + +이제 남은 것은 논리적 장치 생성 과정을 수정하여 표현 큐를 생성하고 `VkQueue` 핸들을 찾는 과정입니다. 핸들을 위한 멤버 변수를 추가합니다: + +```c++ +VkQueue presentQueue; +``` + +다음으로 여러 개의 `VkDeviceQueueCreateInfo` 구조체를 추가하여 두 개의 패밀리로부터 큐를 생성해야 합니다. 깔끔한 방법은 요구되는 모든 큐에 대한 고유한(unique) 큐 패밀리 집합을 생성하는 방법입니다: + +```c++ +#include + +... + +QueueFamilyIndices indices = findQueueFamilies(physicalDevice); + +std::vector queueCreateInfos; +std::set uniqueQueueFamilies = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + +float queuePriority = 1.0f; +for (uint32_t queueFamily : uniqueQueueFamilies) { + VkDeviceQueueCreateInfo queueCreateInfo{}; + queueCreateInfo.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queueCreateInfo.queueFamilyIndex = queueFamily; + queueCreateInfo.queueCount = 1; + queueCreateInfo.pQueuePriorities = &queuePriority; + queueCreateInfos.push_back(queueCreateInfo); +} +``` + +그리고 `VkDeviceCreateInfo`에서 해당 벡터를 가리키도록 수정합니다: + +```c++ +createInfo.queueCreateInfoCount = static_cast(queueCreateInfos.size()); +createInfo.pQueueCreateInfos = queueCreateInfos.data(); +``` + +큐 패밀리가 같다면 인덱스를 한 번만 넘겨주면 됩니다. 마지막으로 큐 핸들을 얻기 위한 호출을 추가합니다. + +```c++ +vkGetDeviceQueue(device, indices.presentFamily.value(), 0, &presentQueue); +``` + +큐 패밀리가 같은 경우 두 개의 핸들은 이제 같은 값을 가질 것입니다. 다음 챕터에서는 스왑 체인을 살펴보고 어떻게 스왑 체인이 표면에 이미지를 표현하는 기능을 제공하는지 알아볼 것입니다. + +[C++ code](/code/05_window_surface.cpp) diff --git a/kr/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md b/kr/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md new file mode 100644 index 00000000..ee56817d --- /dev/null +++ b/kr/03_Drawing_a_triangle/01_Presentation/01_Swap_chain.md @@ -0,0 +1,444 @@ +Vulkan은 "기본 프레임버퍼(default framebuffer)"의 개념이 없습니다. 따라서 렌더링을 수행할 버퍼를 소유한 하부 구조(infrastructure)를 만들어서 화면에 그려지게 해야 합니다. 이 하부 구조는 *스왑 체인(swap chain)*이라고 하며 Vulkan의 경우 명시적으로 생성되어야 합니다. 스왑 체인은 기본적으로 화면에 표시되길 기다리는 이미지들의 큐입니다. 우리 응용 프로그램에서는 이러한 이미지를 만들고 큐에 반환할 것입니다. 큐가 어떻게 동작하고 어떤 조건에서 큐의 이미지가 표시될 것인지와 같은 사항들은 스왑 체인의 설정에 따라 달라집니다. 하지만 일반적으로 스왑 체인의 역할은 화면의 주사율(refresh rate)과 이미지의 표시를 동기화(synchronize)하는 것입니다. + +## 스왑 체인 지원 확인 + +모든 그래픽 카드가 이미지를 곧바로 화면에 표시하는 기능을 지원하는 것은 아닙니다. 예를 들어 서버를 위해 설계된 그래픽 카드는 디스플레이 출력이 없을 수 있습니다. 또한, 이미지의 표현은 윈도우 시스템, 그 윈도우와 연관된 표면(surface)과 밀접하게 관련되어 있기 때문에 Vulkan 코어(core)에는 포함되어 있지 않습니다. 지원하는지를 확인한 후에 `VK_KHR_swapchain` 장치 확장을 활성화시켜줘야만 합니다. + +이러한 목적으로 우리는 먼저 `isDeviceSuitable` 함수를 수정해 이러한 확장을 지원하는지 확인할 것입니다. `VkPhysicalDevice`를 사용해 지원하는 확장의 목록을 얻는 법을 이미 봤기 때문에 어렵지 않을 겁니다. Vulkan 헤더 파일은 `VK_KHR_swapchain`로 정의된 `VK_KHR_SWAPCHAIN_EXTENSION_NAME` 매크로를 지원합니다. 매크로를 사용하면 컴파일러가 타이핑 오류를 탐지할 수 있습니다. + +먼저 필요로 하는 장치 확장의 목록을 정의합니다. 이는 검증 레이어 목록을 얻은 것과 유사합니다. + +```c++ +const std::vector deviceExtensions = { + VK_KHR_SWAPCHAIN_EXTENSION_NAME +}; +``` + +다음으로 `checkDeviceExtensionSupport` 함수를 새로 만듭니다. 이는 `isDeviceSuitable`에서 추가적인 체크를 위해 호출됩니다: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + QueueFamilyIndices indices = findQueueFamilies(device); + + bool extensionsSupported = checkDeviceExtensionSupport(device); + + return indices.isComplete() && extensionsSupported; +} + +bool checkDeviceExtensionSupport(VkPhysicalDevice device) { + return true; +} +``` + +함수의 본문을 수정해 확장들을 열거하고 요구되는 확장들이 있는지를 확인합니다. + +```c++ +bool checkDeviceExtensionSupport(VkPhysicalDevice device) { + uint32_t extensionCount; + vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr); + + std::vector availableExtensions(extensionCount); + vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data()); + + std::set requiredExtensions(deviceExtensions.begin(), deviceExtensions.end()); + + for (const auto& extension : availableExtensions) { + requiredExtensions.erase(extension.extensionName); + } + + return requiredExtensions.empty(); +} +``` + +요구사항에 있지만 확인되지 않은 확장들을 표현하기 위해 문자열(string) 집합(set)을 사용했습니다. 이렇게 하면 가용한 확장들을 열거하면서 쉽게 제외할 수 있습니다. 물론 `checkValidationLayerSupport`에서처럼 중첩된 루프(nested loop)를 사용해도 됩니다. 성능에 영향은 없습니다. 이제 코드를 실행해 여러분의 그래픽 카드가 스왑 체인을 생성할 수 있는지 확인하세요. 사실 이전 장에서 확인한 표현 큐가 사용 가능하다면 스왑 체인 확장도 반드시 지원해야만 합니다. 하지만 이러한 사항들을 명시적으로 확인하고, 확장 또한 명시적으로 활성화 하는 것이 더 좋습니다. + +## 장치 확장 활성화 + +스왑 체인을 사용하려면 먼저 `VK_KHR_swapchain` 확장을 활성화 해야 합니다. 확장을 활성화하기 위해서는 논리적 장치 생성 구조체에 약간의 변경만 해 주면 됩니다: + +```c++ +createInfo.enabledExtensionCount = static_cast(deviceExtensions.size()); +createInfo.ppEnabledExtensionNames = deviceExtensions.data(); +``` + +기존의 `createInfo.enabledExtensionCount = 0;` 명령문을 대체하도록 해야 합니다. + +## 스왑 체인 지원 세부사항 질의 + +스왑 체인이 사용 가능한지만 확인하는 것으로 끝이 아닙니다. 왜냐하면 우리의 윈도우 표면과 실제로 호환이 되지 않을 수도 있기 떄문입니다. 스왑 체인을 생성하는 것 또한 인스턴스나 장치 생성보다 복잡한 설정이 필요하므로 더 진행하기 전에 필요한 세부 사항들을 질의하는 것 부터 시작해야 합니다. + +확인해야 할 속성은 기본적으로 세 종류입니다: + +* 기본 표면 기능 (스왑 체인의 최대/최소 이미지 개수, 이미지의 최대/최소 너비와 높이) +* 표면 포맷 (픽셀 포맷, 컬러 공간) +* 사용 가능한 표시 모드 + +`findQueueFamilies`와 유사하게, 구조체를 사용하여 이러한 세부 사항들을 질의 과정에서 사용할 것입니다. 위에 이야기한 세 종류의 속성들은 다음과 같은 구조체와 구조체 리스트로 만듭립니다. + +```c++ +struct SwapChainSupportDetails { + VkSurfaceCapabilitiesKHR capabilities; + std::vector formats; + std::vector presentModes; +}; +``` + +다음으로 `querySwapChainSupport` 라는 이름의 새 함수를 만들고 이 구조체를 생성합니다. + +```c++ +SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) { + SwapChainSupportDetails details; + + return details; +} +``` + +이 장에서는 이러한 정보를 포함한 구조체를 질의하는 방법을 설명합니다. 이 구조체의 의미와 정확히 어떤 데이터들을 가지고 있는지는 다음 장에서 설명할 것입니다. + +기본 표면 기능으로 시작해 봅시다. 이러한 속성들은 질의하기 쉽고, `VkSurfaceCapabilitiesKHR` 타입의 단일 구조체로 반환됩니다. + +```c++ +vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities); +``` + +이 함수는 지원되는 기능을 판단할 때, 명시된 `VkPhysicalDevice`와 `VkSurfaceKHR` 윈도우 표면을 고려하도록 구현되어 있습니다. 모든 질의 지원 함수들은 이러한 두 매개변수를 받도록 되어 있는데 이들이 스왑 체인의 핵심 구성요소이기 때문입니다. + +다음 단계는 지원하는 표면 포맷을 질의하는 것입니다. 이는 구조체의 리스트로, 두 개의 함수 호출과 유사한 방식입니다. + +```c++ +uint32_t formatCount; +vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr); + +if (formatCount != 0) { + details.formats.resize(formatCount); + vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data()); +} +``` + +모든 가능한 포맷을 저장할 수 있도록 벡터의 크기가 변해야 합니다. 마지막으로, 지원하는 표현 모드를 질의하는 것도 `vkGetPhysicalDeviceSurfacePresentModesKHR`를 사용해 동일한 방식으로 이루어집니다: + +```c++ +uint32_t presentModeCount; +vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr); + +if (presentModeCount != 0) { + details.presentModes.resize(presentModeCount); + vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data()); +} +``` + +이제 모든 세부 사항이 구조체 안에 있으니 `isDeviceSuitable`를 한번 더 수정하여 적절하게 스왑 체인이 지원되고 있는지 확인하도록 해 봅시다. 이 튜토리얼에서의 스왑 체인 지원은 우리가 가진 윈도우 표면에 대해 최소 하나의 이미지 포맷과 표면 모드를 지원하는 것이면 충분합니다. + +```c++ +bool swapChainAdequate = false; +if (extensionsSupported) { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device); + swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty(); +} +``` + +확장이 사용 가능한지 확인 후에 스왑 체인 지원 여부를 얻은 것일 뿐입니다. 함수의 마지막 라인은 아래와 같이 변경되어야 합니다. + +```c++ +return indices.isComplete() && extensionsSupported && swapChainAdequate; +``` + +## 스왑 체인에 대한 적절한 설정 선택 + +우리가 얻은 `swapChainAdequate` 조건이 만족되었다면 충분하지만, 서로 다른 최적화 요구사항에 대한 모드들이 여전히 존재합니다. 이제는 최적의 스왑 체인 설정을 찾기 위한 몇 가지 함수를 작성해 볼 것입니다. 결정해야 할 설정은 세 가지입니다: + +* 표면 포맷 (색상 깊이(depth)) +* 표시 모드 (이미지를 화면에 "스왑"하는 조건) +* 스왑 크기 (스왑 체인 이미지의 해상도) + +이러한 각각의 설정에 대해 생각하고 있는 이상적인 값이 있을 것이고, 가능하면 이러한 값을 사용하도록 합니다. 그렇지 않다면 그 다음으로 괜찮은 값을 사용하도록 하는 로직을 만들 것입니다. + +### 표면 포맷 + +이 설정에 대한 함수는 아래와 같이 시작합니다. 뒤에서 `SwapChainSupportDetails` 구조체의 `formats` 멤버를 인자로 넘겨줄 것입니다. + +```c++ +VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + +} +``` + +각 `VkSurfaceFormatKHR`는 `format` 과 `colorSpace` 멤버를 가지고 있습니다. `format`은 컬러 채널과 타입을 명시합니다. 예를 들어 `VK_FORMAT_B8G8R8A8_SRGB`는 B,G,R과 알파 채널을 그 순서대로 8비트 부호없는(unsigned) 정수로 저장하여 픽셀당 32비트를 사용합니다. `colorSpace` 멤버는 `VK_COLOR_SPACE_SRGB_NONLINEAR_KHR`를 사용해 SRGB 컬러 공간을 지원하는지 여부를 표시합니다. 참고로 이 플래그는 이전 버전 명세에서는 `VK_COLORSPACE_SRGB_NONLINEAR_KHR`였습니다. + +컬러 공간에 대해서 우리는 가능하면 SRGB를 사용할 것인데, 이것이 [보다 정확한 색상 인지가 가능하기 때문입니다](http://stackoverflow.com/questions/12524623/). 또한 이는 나중에 살펴볼 (예를들면 텍스처와 같은) 이미지에 대한 표준 컬러 공간입니다. 이러한 이유로 컬러 포맷도 SRGB 컬러 포맷을 사용하는 것이고 가장 흔히 사용되는 것이 `VK_FORMAT_B8G8R8A8_SRGB`입니다. + +리스트를 순회하며 이러한 선호하는 조합이 사용 가능한지 확인합니다. + +```c++ +for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return availableFormat; + } +} +``` + +실패한다면 사용 가능한 포맷들에 얼마나 "좋은지" 여부를 바탕으로 순위를 매길 수 있습니다. 하지만 대부분 명시된 첫 번째 포맷을 그냥 사용하는 것으로 충분합니다. + +```c++ +VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector& availableFormats) { + for (const auto& availableFormat : availableFormats) { + if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { + return availableFormat; + } + } + + return availableFormats[0]; +} +``` + +### 표시 모드(Presentation mode) + +표시 모드는 스왑 체인에서 가장 중요한 설정인데, 이미지를 화면에 표시하는 실제 조건을 나타내는 부분이기 때문입니다. Vulkan에서는 네 가지 모드가 가능합니다: + +* `VK_PRESENT_MODE_IMMEDIATE_KHR`: 여러분 응용 프로그램에서 제출(submit)된 이미지가 곧바로 화면에 전송되어 테어링(tearing) 현상이 발생할 수 있습니다. +* `VK_PRESENT_MODE_FIFO_KHR`: 스왑 체인은 큐가 되어, 디스플레이는 화면이 갱신될 때 큐의 앞에서 이미지를 가져오고, 프로그램은 렌더링된 이미지를 큐의 뒤에 삽입합니다. 큐가 꽉 차면 프로그램은 대기해야 합니다. 현대 게임에서 볼 수 있는 수직 동기화(vertical sync)와 유사합니다. 화면이 갱신되는 순간은 "수직 공백(vertical blank)"라 불립니다. +* `VK_PRESENT_MODE_FIFO_RELAXED_KHR`: 이 모드는 이전 모드와 프로그램이 지연되어서 마지막 수직 공백때 큐가 비는 경우에만 다르게 동작합니다. 다음 수직 공백을 기다리는 대신, 그 다음 이미지가 도착하는 즉시 전송됩니다. 이러한 경우 눈에 띄는 테어링이 발생하게 됩니다. +* `VK_PRESENT_MODE_MAILBOX_KHR`: 이것도 두 번째 모드의 또다른 버전입니다. 큐가 꽉 찼을 때 응용 프로그램을 대기시키는 대신, 큐에 있는 이미지가 새로운 이미지로 대체됩니다. 이 모드는 가능한 빠르게 렌더링을 수행하면서 테어링을 방지할 수 있고, 표준적인 수직 동기화보다 지연시간(latency) 문제를 줄일 수 있습니다. This is commonly known as "triple buffering", although the existence of three buffers alone does not necessarily mean that the framerate is unlocked. + +`VK_PRESENT_MODE_FIFO_KHR` 모드만 사용 가능한 것이 보장되기 때문에 사용 가능한 최선의 모드를 찾는 함수를 작성합니다: + +```c++ +VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { + return VK_PRESENT_MODE_FIFO_KHR; +} +``` + +에너지 사용량 문제가 없는 경우라면, 개인적으로 `VK_PRESENT_MODE_MAILBOX_KHR`가 좋은 대체 모드라고 생각합니다. 테어링을 방지하면서도 새로운 이미지를 가급적 최신 이미지로 수직 공백 전까지 유지하기 때문에 꽤 낮은 지연시간을 갖습니다. 모바일 장치와 같이 에너지 사용량 문제가 중요한 경우에는 대신 `VK_PRESENT_MODE_FIFO_KHR`를 사용하는 것이 좋을 것입니다. 이제 `VK_PRESENT_MODE_MAILBOX_KHR` 가 사용 가능한지 리스트 내에서 찾아 봅니다: + +```c++ +VkPresentModeKHR chooseSwapPresentMode(const std::vector& availablePresentModes) { + for (const auto& availablePresentMode : availablePresentModes) { + if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) { + return availablePresentMode; + } + } + + return VK_PRESENT_MODE_FIFO_KHR; +} +``` + +### 스왑 범위(extent) + +이제 주요 속성 하나만 남았고, 마지막 함수로 추가할 것입니다: + +```c++ +VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { + +} +``` + +스왑 범위는 스왑 체인 이미지의 해상도이고 거의 대부분의 경우에 *픽셀 단위에서* 이미지를 그리고자 하는 윈도의 해상도와 동일한 값을 가집니다(보다 상세한 내용은 곧 살펴볼 것입니다). 가능한 해상도의 범위는 `VkSurfaceCapabilitiesKHR` 구조체에 정의되어 있습니다. Vulkan은 `currentExtent` 멤버의 너비와 높이를 설정하여 윈도우의 해상도와 맞추도록 하고 있습니다. 하지만 어떤 윈도우 매니저의 경우 `currentExtent`의 너비와 높이 값을 특수한 값(`uint32_t`의 최대값)으로 설정하여 이 두 값을 다르게 할 수 있습니다. 이러한 경우 윈도우에 가장 적절한 해상도를 `minImageExtent`와 `maxImageExtent` 사이 범위에서 선택하게 됩니다. 하지만 올바른 단위(unit)로 해상도를 명시해야 합니다. + +GLFW는 크기를 측정하는 두 단위가 있고 이는 픽셀과 [스크린 좌표계](https://www.glfw.org/docs/latest/intro_guide.html#coordinate_systems) 입니다. 예를 들어 우리가 이전에 윈도우를 생성할 때 명시한 `{WIDTH, HEIGHT}` 해상도는 스크린 좌표계 기준으로 측정한 값입니다. 하지만 Vulkan은 픽셀 단위로 동작하기 때문에, 스왑 체인의 크기도 픽셀 단위로 명시해 주어야만 합니다. 안타깝게도 여러분이 (애플의 레티나 디스플레이와 같은) 고DPI 디스플레이를 사용하는 경우, 스크린 좌표계가 픽셀 단위와 달라집니다. 높은 픽셀 밀도로 인해 픽셀 단위의 윈도우 해상도는 스크린 좌표계 단위의 윈도우 해상도보다 커집니다. Vulkan이 스왑 크기에 관한 것을 수정해 주지 않는 한, 그냥 `{WIDTH, HEIGHT}`를 사용할 수는 없습니다. 대신에 `glfwGetFramebufferSize`를 사용해서 윈도우의 해상도를 최대 및 최소 이미지 크기와 맞추기 전에 픽셀 단위로 받아와야만 합니다. + +```c++ +#include // Necessary for uint32_t +#include // Necessary for std::numeric_limits +#include // Necessary for std::clamp + +... + +VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) { + if (capabilities.currentExtent.width != std::numeric_limits::max()) { + return capabilities.currentExtent; + } else { + int width, height; + glfwGetFramebufferSize(window, &width, &height); + + VkExtent2D actualExtent = { + static_cast(width), + static_cast(height) + }; + + actualExtent.width = std::clamp(actualExtent.width, capabilities.minImageExtent.width, capabilities.maxImageExtent.width); + actualExtent.height = std::clamp(actualExtent.height, capabilities.minImageExtent.height, capabilities.maxImageExtent.height); + + return actualExtent; + } +} +``` + +여기서 `clamp` 함수는 `width`와 `height` 값을 허용 가능한 최대와 최소 크기로 제한하기 위해 사용되었습니다. + +## 스왑 체인 생성 + +이제 런타임 선택을 위해 필요한 모든 헬퍼 함수들이 준비되었으니 동작하는 스왑 체인을 만들기 위한 모든 정보를 얻을 수 있습니다. + +`createSwapChain`함수는 이러한 함수 호출의 결과를 받는 함수이고 `initVulkan`에서 논리적 장치 생성 이후에 호출됩니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); +} + +void createSwapChain() { + SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice); + + VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats); + VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes); + VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities); +} +``` + +이러한 속성들 이외에도 스왑 체인에 몇 개의 이미지를 사용할 것인지 결정해야 합니다. 아래 구현은 동작하기 위한 최소 개수를 명시합니다: + +```c++ +uint32_t imageCount = swapChainSupport.capabilities.minImageCount; +``` + +하지만 이러한 최소 개수를 사용하면, 렌더링을 수행할 또다른 이미지를 얻기위해 드라이버의 내부 연산을 기다리는 결과를 낳을 수 있습니다. 따라서 최소로 요구되는 것보다 하나 더 많은 이미지를 요구하는 것이 권장됩니다. + +```c++ +uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1; +``` + +또한 이 과정에서 최대 이미지 개수를 넘지 않도록 해야 하며 여기서 `0`은 최대 개수의 제한이 없다는 것을 의미하는 특별한 값입니다. + +```c++ +if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) { + imageCount = swapChainSupport.capabilities.maxImageCount; +} +``` + +Vulkan 객체들이 그렇듯이, 스왑 체인 객체를 생성하는 것도 커다란 구조체에 값을 채우는 과정이 필요합니다. 익숙한 코드로 시작합니다: + +```c++ +VkSwapchainCreateInfoKHR createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; +createInfo.surface = surface; +``` + +어떤 표면에 스왑 체인이 연결되어야 하는지를 명시한 뒤에, 스왑 체인 이미지의 세부 사항들을 명시합니다: + +```c++ +createInfo.minImageCount = imageCount; +createInfo.imageFormat = surfaceFormat.format; +createInfo.imageColorSpace = surfaceFormat.colorSpace; +createInfo.imageExtent = extent; +createInfo.imageArrayLayers = 1; +createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT; +``` + +`imageArrayLayers`는 각 이미지가 구성하는 레이어의 개수를 명시합니다. 여러분이 스테레오 3D(stereoscopic 3D) 응용 프로그램을 개발하는 것이 아니라면 이 값은 항상 `1`입니다. `imageUsage` 비트 필드는 스왑 체인의 이미지에 어떤 연산을 적용할 것인지를 명시합니다. 이 튜토리얼에서 우리는 여기에 직접 렌더링을 수행할 것이므로 색상 어태치먼트(color attachment)로 사용될 것입니다. 먼저 별도의 이미지에 렌더링한 뒤 후처리(post-processing)를 적용하는 것도 가능합니다. 이러한 경우 `VK_IMAGE_USAGE_TRANSFER_DST_BIT`과 같은 값을 사용하고 렌더링된 이미지를 스왑 체인 이미지로 전송하기 위한 메모리 연산을 사용해야 합니다. + +```c++ +QueueFamilyIndices indices = findQueueFamilies(physicalDevice); +uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()}; + +if (indices.graphicsFamily != indices.presentFamily) { + createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT; + createInfo.queueFamilyIndexCount = 2; + createInfo.pQueueFamilyIndices = queueFamilyIndices; +} else { + createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; + createInfo.queueFamilyIndexCount = 0; // Optional + createInfo.pQueueFamilyIndices = nullptr; // Optional +} +``` + +다음으로 여러 큐 패밀리에 걸쳐 사용될 스왑 체인의 이미지들이 어떻게 처리될 것인지를 명시해 주어야 합니다. 우리 응용 프로그램에서는 그래픽스 큐 패밀리와 표시 큐가 다른 경우가 이에 해당됩니다. 그래픽스 큐로부터 스왑 체인 이미지에 그리기가 수행될 것이고, 이를 표시 큐에 제출할 것입니다. 여러 큐에서 접근 가능한 이미지를 다루는 두 가지 방법이 있습니다: + +* `VK_SHARING_MODE_EXCLUSIVE`: 하나의 이미지가 한 번에 하나의 큐 패밀리에 의해 소유(own)되고 다른 큐에서 사용되기 전에 명시적으로 전송되어야 합니다. 이 옵션이 성능이 가장 좋습니다. +* `VK_SHARING_MODE_CONCURRENT`: 소유권의 명시적 이동 없이 이미지가 여러 큐에서 동시에 접근 가능합니다. + +큐 패밀리가 다르다면 이 튜토리얼에서는 소유권 챕터로 넘어가기 전에는 동시성 모드(concurrent mode)를 사용할 것입니다. 동시성에 대한 설명은 몇몇 개념 때문에 나중에 설명하는 것이 낫기 때문입니다. 동시성 모드는 어떤 큐 패밀리의 소유권이 공유될 것인지 `queueFamilyIndexCount`와 `pQueueFamilyIndices` 매개변수를 사용해 미리 명시하게 되어 있습니다. 그래픽스 큐 패밀리와 표시 큐 패밀리가 동일하다면 (대부분의 하드웨어에서는 동일함) 독점(exclusive) 모드를 사용할 것입니다. 동시성 모드에서는 최소한 두 개의 서로다른 큐 패밀리를 명시해야만 하기 떄문입니다. + +```c++ +createInfo.preTransform = swapChainSupport.capabilities.currentTransform; +``` + +이제 스왑 체인의 이미지에 적용할 특정 변환(transform)을 명시할 수 있습니다. 이는 기능이 지원될 때(`capabilities`의 `supportedTransforms`)에만 가능한데 예를 들면 시계방향으로 90도 회전이라던가, 수평 뒤집기(flip) 등이 있습니다. 이러한 변환을 적용하지 않을 것이면, 현재 변환(current transformation)으로 명시하면 됩니다. + +```c++ +createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; +``` + +`compositeAlpha` 필드는 윈도우 시스템의 다른 윈도우와의 블렌딩(blending)을 위해 알파 채널이 사용될 것인지를 명시합니다. 거의 대부분의 경우 알파 채널은 무시하는 것이 좋으므로 `VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR`를 사용합니다. + +```c++ +createInfo.presentMode = presentMode; +createInfo.clipped = VK_TRUE; +``` + +`presentMode`는 이름만 봐도 아실 수 있겠죠. `clipped`가 `VK_TRUE` 면 가려진 픽셀의 색상에 대해서는 신경쓰지 않겠다는 의미인데, 예를 들면 다른 윈도우가 그 픽셀 위에 있는 경우입니다. 뒤쪽의 픽셀 값을 읽어와 의도하는 결과를 얻을 것이 아니라면 그냥 클리핑(clipping)을 활성화 하는게 성능에 좋습니다. + +```c++ +createInfo.oldSwapchain = VK_NULL_HANDLE; +``` + +이제 마지막 필드인 `oldSwapChain` 입니다. Vulkan을 사용하면 응용 프로그램이 실행되는 동안 스왑 체인이 사용 불가능해지거나, 최적화되지 않을 수 있습니다. 예를 들어 윈도우가 리사이즈(resize)되는 경우에 그렇습니다. 이러한 경우 스왑 체인이 처음부터 다시 만들어져야만 하고 이전 스왑 체인에 대한 참조가 여기에 명시되어야만 합니다. 이 주제는 복잡하기 때문에 [이후 챕터](!kr/Drawing_a_triangle/Swap_chain_recreation)에서 보다 자세히 배워볼 것입니다. 지금은 그냥 하나의 스왑 체인만 만드는 것으로 가정합시다. + +이제 `VkSwapchainKHR` 객체를 저장할 클래스 멤버를 추가합니다: + +```c++ +VkSwapchainKHR swapChain; +``` + +이제 스왑 체인을 만드는 것은 단순히 `vkCreateSwapchainKHR`를 호출하는 것으로 간단해졌습니다. + +```c++ +if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) { + throw std::runtime_error("failed to create swap chain!"); +} +``` + +매개변수는 논리적 장치, 스왑 체인 생성 정보, 선택적인 사용자 정의 할당자와 핸들을 저장할 변수에 대한 포인터입니다. 새로울 것 없죠. 소멸은 장치 소멸에 앞서 `vkDestroySwapchainKHR`를 사용해 이루어져야 합니다: + +```c++ +void cleanup() { + vkDestroySwapchainKHR(device, swapChain, nullptr); + ... +} +``` + +이제 프로그램을 실행해 스왑 체인이 올바로 생성되었는지 확인하세요! `vkCreateSwapchainKHR`에서 접근 위반 오류가 발생하거나 `Failed to find 'vkGetInstanceProcAddress' in layer SteamOverlayVulkanLayer.dll`와 같은 메시지를 마주치게 되면, [FAQ](!en/FAQ)에서 Steam 오버레이 레이어 내용을 살펴보세요. + +검증 레이어가 활성화 된 상태에서 `createInfo.imageExtent = extent;` 명령문을 지워 보세요. 검증 레이어가 바로 실수를 탐지하고 도움이 되는 메시지를 출력하는 것을 볼 수 있을 겁니다. + +![](/images/swap_chain_validation_layer.png) + +## 스왑 체인 이미지의 획득(Retrieving) + +이제 스왑 체인이 생성되었으니, 그 안의 `VkImage`들에 대한 핸들을 획득하는 과정이 남았습니다. 나중 챕터에서 렌더링 연산을 위해 이를 사용하게 됩니다. 핸들을 저장하기 위한 클래스 멤버를 추가합니다: + +```c++ +std::vector swapChainImages; +``` + +스왑 체인 구현에 의해 이미지들이 만들어지고 스왑 체인이 소멸될 때 자동으로 정리되므로 `cleanup` 코드에 뭔가를 추가할 필요는 없습니다. + +`createSwapChain` 함수의 마지막 부분 `vkCreateSwapchainKHR` 뒤에 획득을 위한 코드를 추가할 것입니다. 획득 과정은 Vulkan으로부터 객체의 배열을 획득하는 일반적인 과정입니다. 기억하셔야 할 것은 우리는 스왑 체인의 이미지 최소 개수만 명시하였으므로 구현에 따라 더 많은 이미지가 생성되었을 수 있습니다. 그래서 `vkGetSwapchainImagesKHR`로 최종 생성된 이미지 개수를 먼저 얻고 컨테이너(container) 크기를 조정한 뒤 핸들을 얻어오도록 구현하였습니다. + +```c++ +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr); +swapChainImages.resize(imageCount); +vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data()); +``` + +마지막으로 멤버 변수에 스왑 체인 이미지를 위해 우리가 명시한 포맷과 크기를 저장합니다. 나중 챕터에서 이 값들을 사용할 것입니다. + +```c++ +VkSwapchainKHR swapChain; +std::vector swapChainImages; +VkFormat swapChainImageFormat; +VkExtent2D swapChainExtent; + +... + +swapChainImageFormat = surfaceFormat.format; +swapChainExtent = extent; +``` + +이제 그림을 그리고 화면에 표시될 이미지가 준비되었습니다. 다음 챕터에서부터는 이미지를 렌더 타겟(render target)으로 설정하는 법, 실제 그래픽스 파이프라인과 그리기 명령에 대해 살펴볼 것입니다! + +[C++ code](/code/06_swap_chain_creation.cpp) diff --git a/kr/03_Drawing_a_triangle/01_Presentation/02_Image_views.md b/kr/03_Drawing_a_triangle/01_Presentation/02_Image_views.md new file mode 100644 index 00000000..e3c3490f --- /dev/null +++ b/kr/03_Drawing_a_triangle/01_Presentation/02_Image_views.md @@ -0,0 +1,104 @@ +스왑 체인에 포함된 `VkImage`를 사용하기 위해서는 렌더링 파이프라인에서 `VkImageView` 객체를 생성해야 합니다. 이미지 뷰(image view)는 말 그대로 이미지에 대한 뷰 입니다. 이를 통해 이미지에 어떻게 접근하는지와 이미지의 어느 부분에 접근할 것인지를 명시하는데, 예를 들어 2D 텍스처로 취급될 것인지, 밉맵(mipmap) 수준이 없는 깊이 텍스처(depth texture)로 취급될 것인지와 같은 사항입니다. + +이 장에서 우리는 `createImageViews` 함수를 작성하여 스왑 체인에 있는 모든 이미지에 대한 이미지 뷰를 생성하고 이는 나중에 컬러 타겟으로 사용될 것입니다. + +먼저 이미지 뷰를 저장할 클래스 멤버를 추가합니다: + +```c++ +std::vector swapChainImageViews; +``` + +`createImageViews` 함수를 만들고 스왑 체인 생성 후에 호출합니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); +} + +void createImageViews() { + +} +``` + +우선적으로 해애 할 일은 리스트의 크기를 조정해 우리가 생성할 이미지 뷰가 모두 들어갈 수 있도록 하는 것입니다. + +```c++ +void createImageViews() { + swapChainImageViews.resize(swapChainImages.size()); + +} +``` + +다음으로 모든 스왑 체인 이미지에 대한 반복문을 만듭니다. + +```c++ +for (size_t i = 0; i < swapChainImages.size(); i++) { + +} +``` + +이미지 뷰 생성에 대한 매개변수는 `VkImageViewCreateInfo` 구조체에 명시됩니다. 처음 몇 개의 매개변수는 직관적입니다. + +```c++ +VkImageViewCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; +createInfo.image = swapChainImages[i]; +``` + +`viewType`과 `format` 필드는 이미지 데이터가 어떻게 해석되어야 할지를 명시합니다. `viewType` 매개변수는 이미지를 1차원, 2차원, 3차원 혹은 큐브 맵(cube map)으로 취급할 수 있도록 합니다. + +```c++ +createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; +createInfo.format = swapChainImageFormat; +``` + +`components` 필드는 컬러 채널을 뒤섞을 수 있도록 합니다. 예를 들어 흑백(monochrome) 텍스처를 위해서는 모든 채널을 빨간색 채널로 맵핑할 수 있습니다. 또한 `0`이나 `1`과 같은 상수를 채널에 맵핑할 수도 있습니다. 우리의 경우 기본(default) 맵핑을 사용할 것입니다. + +```c++ +createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY; +createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY; +createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY; +createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY; +``` + +`subresourceRange` 필드는 이미지의 목적이 무엇인지와 어떤 부분이 접근 가능할지를 기술합니다. 우리 이미지는 컬러 타겟이고 밉맵핑이나 다중 레이어는 사용하지 않습니다. + +```c++ +createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +createInfo.subresourceRange.baseMipLevel = 0; +createInfo.subresourceRange.levelCount = 1; +createInfo.subresourceRange.baseArrayLayer = 0; +createInfo.subresourceRange.layerCount = 1; +``` + +스테레오 3D 응용 프로그램을 만든다면, 스왑 체인을 다중 레이어로 만들 것입니다. 그런 경우 각 이미지에 대한 다중 이미지 뷰를 만들 수 있고, 이는 왼쪽과 오른쪽 눈에 대한 이미지 표현을 서로 다른 레이어를 통해 접근할 수 있도록 합니다. + +이제 이미지 뷰를 만드는 것은 `vkCreateImageView`를 호출하면 됩니다. + +```c++ +if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create image views!"); +} +``` + +이미지와는 다르게 이미지 뷰는 우리가 명시적으로 만든 것이기 때문에 소멸을 위해서는 프로그램 종료 시점에 반복문을 추가해야 합니다. + +```c++ +void cleanup() { + for (auto imageView : swapChainImageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + + ... +} +``` + +이미지를 텍스처로 사용하기 위한 목적으로는 이미지 뷰를 만드는 것으로 충분하지만 렌더 타겟으로 만들기 위해서는 아직 할 일이 남아 있습니다. 이를 위해서는 프레임버퍼(framebuffer)와 관련된 추가적인 작업이 필요합니다. 하지만 우선 그래픽스 파이프라인부터 설정하도록 하겠습니다. + +[C++ code](/code/07_image_views.cpp) diff --git a/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md b/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md new file mode 100644 index 00000000..f065b6d5 --- /dev/null +++ b/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/00_Introduction.md @@ -0,0 +1,50 @@ +다음 몇 챕터에서 우리는 첫 번째 삼각형을 그리기 위한 그래픽스 파이프라인을 설정할 것입니다. 그래픽스 파이프라인이란 메쉬(mesh)의 정점(vertex)들과 텍스처들을 받아서 렌더 타겟의 픽셀로 출력하기 위한 일련의 연산들을 말합니다. 간단한 개요가 아래 그림에 표현되어 있습니다: + +![](/images/vulkan_simplified_pipeline.svg) + +*입력 조립기(input assembler)*는 명시한 버퍼로부터 정점 데이터를 수집합니다. 또한 인덱스 버퍼를 사용하여 특정 요소들을 정점 데이터의 중복 없이 반복 사용할 수 있도록 할 수도 있습니다. + +*정점 셰이더(vertex shader)*는 각 정점에 대해 실행되며, 일반적으로 정점의 위치를 모델 공간으로부터 스크린 공간으로 변환하는 작업을 합니다. 또한 정점별(per-vertex) 데이터를 파이프라인의 다음 단계로 전달합니다. + +*테셀레이션 셰이더(tessellation shaders)*는 특정한 규칙에 따라 기하(geometry)를 분할하여 메쉬의 품질을 향상시킬 수 있는 과정입니다. 이는 벽돌로 된 벽이라던가, 계단과 같은 표면에 적용되어 가까이서 봤을 때 덜 평평하게(flat) 보이도록 하는 데 자주 사용됩니다. + +*기하 셰이더(geometry shader)*는 모든 프리미티브(삼각형, 선, 점)에 대해 실행되며 그 정보를 제외(discard)시키거나 입력된 것보다 더 많은 프리미티브를 생성할 수 있습니다. 테셀레이션 셰이더와 비슷하지만 훨씬 유연합니다. 하지만 요즘 응용 프로그램에서는 자주 사용되지 않는데 인텔의 내장 그래픽 카드를 제외하고 대부분 그래픽 카드에서는 성능이 그리 좋지 않기 때문입니다. + +*래스터화(rasterization)* 단계는 프리미티브를 *프래그먼트*로 이산화하는 단계입니다. 프래그먼트는 픽셀 요로소 프레임버퍼에 채워집니다. 화면 밖에 놓여 있는 프래그먼트는 버려지고 정점 셰이더의 출력 어트리뷰트(attribute)는 프래그먼트들에 걸쳐 그림에 보이는 것과 같이 보간됩니다. 다른 프리미티브 프래그먼트 뒤에 놓여있는 프래그먼트도 깊이 테스트(depth test)에 의해 버려집니다. + +*프래그먼트 셰이더(fragment shader)*는 모든 살아남은 프래그먼트에 대해 실행되며 어떤 프레임버퍼에 프래그먼트가 쓰여질지, 어떤 색상과 깊이값이 쓰여질지를 결정합니다. 이는 정점 셰이더에서 보간된 데이터를 바탕으로 이루어지며 데이터는 텍스처 좌표계와 라이팅(lighting)을 위한 법선(normal) 정보 같은 것들이 포함됩니다. + +*컬러 블렌딩(color blending)* 단계는 프레임버퍼의 같은 픽셀에 맵핑되는 다른 프래그먼트들을 섞는 연산을 적용합니다. 프래그먼트 값들이 다른 값들을 대체할 수도 있고, 투명도에 따라 더해지거나 섞일 수 있습니다. + +녹색으로 표현된 단계는 *고정 함수(fixed-function)* 단계로 알려져 있습니다. 이 단계들은 매개변수를 사용해 연산을 약간 변경할 수 있지만, 동작 방식 자체는 미리 정의되어 있습니다. + +주황색으로 표시된 단계는 `programmable`한 단계인데, 여러분이 작성한 코드를 그래픽 카드에 업로드할 수 있어서 원하는 대로 연산을 할 수 있다는 뜻입니다. 이렇게 되면 예를 들어 프래그먼트 셰이더에서 텍스처링이라던지 레이 트레이싱(ray tracing)을 위한 라이팅 등을 구현할 수 있게 됩니다. 이러한 프로그램은 여러 객체(예를들어 정점 또는 프래그먼트)들을 처리하기 위해 여러 GPU 코어에서 동시에 병렬적으로 실행됩니다. + +OpenGL이나 Direct3D같은 예전 API를 사용해 봤다면, 파이프라인의 설정을 바꾸는 `glBlendFunc`와 `OMSetBlendState` 같은 함수의 사용에 익숙할 것입니다. Vulkan의 그래픽스 파이프라인은 거의 완전히 불변적(immutable)이라서, 셰이더를 바꾸거나 다른 프레임버퍼를 바인딩(bind)한다거나 블렌딩 함수를 바꾸거나 할 떄에는 파이프라인을 처음부터 다시 만들어야 합니다. 이에 대한 단점으로는 우리가 사용하고자 하는 렌더링 연산을 위한 다양한 상태를 표현하는 모든 조합에 대해 파이프라인들을 만들어야 한다는 것입니다. 하지만, 파이프라인에서 수행하는 모든 연산에 대해 미리 알 수 있기 때문에, 드라이버가 훨씬 최적화를 잘 할 수 있는 장점도 있습니다. + +여러분이 하려는 작업에 따라서 몇 개의 프로그램가능한(programmable) 단계는 선택적으로 사용해도 됩니다. 예를 들어 테셀레이션과 기하 단계는 간단한 형상을 그릴 떄에는 활성화하지 않아도 됩니다. 깊이 값에만 관심이 있다면 프래그먼트 셰이더 단계를 비활성화 할수도 있는데 [그림자 맵](https://en.wikipedia.org/wiki/Shadow_mapping) 생성을 할 때에 유용할 것입니다. + +다음 장에서는 삼각형을 화면에 표시하기 위한 두 개의 프로그램 가능한 단계(정점 셰이더와 프래그먼트 셰이더)를 만들어 볼 것입니다. 고정된 함수 구성인 블렌딩 모드, 뷰포트(viewport), 래스터화 같은 단계는 그 다음 챕터에서 설정할 것입니다. Vulkan에서의 그래픽스 파이프라인을 위한 마지막 설정 단계는 입력과 출력 프레임버퍼와 관련되어 있습니다. + +`initVulkan`의 `createImageViews` 바로 뒤에 호출할 `createGraphicsPipeline` 함수를 만들겠습니다. 이후 챕터에서 이 함수를 만들어 나갈 것입니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createGraphicsPipeline(); +} + +... + +void createGraphicsPipeline() { + +} +``` + +[C++ code](/code/08_graphics_pipeline.cpp) diff --git a/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md b/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md new file mode 100644 index 00000000..b9baa66b --- /dev/null +++ b/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/01_Shader_modules.md @@ -0,0 +1,330 @@ +기존 API와는 다르게, Vulkan의 셰이더 코드는 [GLSL](https://en.wikipedia.org/wiki/OpenGL_Shading_Language)이나 [HLSL](https://en.wikipedia.org/wiki/High-Level_Shading_Language)과 같은 사람이 읽을 수 있는(human-readable) 문법이 아닌 바이트코드(bytecode) 포맷으로 명시되어야 합니다. 이 바이트코드 포맷은 [SPIR-V](https://www.khronos.org/spir)라 불리며 Vulkan과 OpenCL에서의 사용을 위해 설계되었습니다(둘 다 크로노스(Khronos)의 API). 이를 사용해 그래픽스 및 계산 셰이더 작성이 가능하지만 이 튜토리얼에서는 Vulkan의 그래픽스 파이프라인에 사용되는 셰이더에 포커스를 맞추도록 하겠습니다. + +바이트코드를 사용함으로써 얻을 수 있는 장점은 GPU 벤더가 작성하는, 셰이더 코드를 네이티브 코드로 변환하는 컴파일러가 훨씬 간단해진다는 것입니다. 과거의 사례를 봤을 때 사람이 읽을 수 있는 GLSL과 같은 문법에서, 어떤 GPU 벤더들은 표준을 유연하게 해석하는 경우가 있었습니다. 이러한 벤더의 GPU에서 여러분이 일반적이지 않은(non-trivial) 셰이더를 작성하는 경우에, 다른 벤더의 드라이버에서는 여러분의 코드가 문법 오류로 판단된다던지, 더 안좋은 상황에서는 다른 방식으로 동작한다던지 하는 문제가 있을 수 있습니다. SPIR-V와 같은 직관적인 바이트코드 포맷을 사용하면 이러한 문제가 해결될 것으로 바라고 있습니다. + +그렇다고 우리가 손으로 바이트코드를 작성해야 한다는 뜻은 아닙니다. 크로노스 자체적으로 GLSL을 SPIR-V로 변환하는 벤더 독립적인 컴파일러를 릴리즈하였습니다. 이 컴파일러는 여러분의 셰이더 코드가 표준에 맞는지를 검증하고 프로그램에 사용할 수 있는 SPIR-V 바이너리를 생성합니다. 또한 이 컴파일러를 라이브러리의 형태로 추가하여 런타임에 SPIR-V를 생성하도록 할 수도 있지만, 이 튜토리얼에서 이 기능을 사용하지는 않을 것입니다. 컴파일러는 `glslangValidator.exe`를 통해 직접 사용할수도 있지만 우리는 구글에서 만든 `glslc.exe`를 사용할 것입니다. `glslc`의 장점은 GCC와 Clang과 같은 유명한 컴파일러와 같은 매개변수 포맷을 사용한다는 점, 그리고 *include*와 같은 부가 기능을 제공하는 점입니다. 둘 다 Vulkan SDK에 포함되어 있으므로 추가적으로 다운로드 할 필요는 없습니다. + +GLSL은 C 스타일 문법을 가진 셰이더 언어입니다. GLSL로 작성된 프로그램은 `main`함수가 있어서 모든 객체에 대해 실행됩니다. 입력에 매개변수를 사용하고 출력에 반환값을 사용하는 대신, GLSL은 입력과 출력을 관리하는 전역 변수를 가지고 있습니다. 이 언어는 그래픽스 프로그램을 위한 다양한 기능을 포함하고 있는데 내장 벡터(vector)와 행렬(matrix) 타입이 그 예시입니다. 외적(cross product)이나 행렬-벡터 곱, 벡터를 기준으로 한 반사(reflection) 연산을 위한 함수 또한 포함되어 있습니다. 벡터 타입은 `vec`이라고 물리며 요소의 개수를 명시하는 숫자가 뒤에 붙습니다. 예를 들어 3차원 위치는 `vec3`에 저장됩니다. 개별 요소에 대한 접근은 멤버 접근 연산자처러 `.x`로 접근 가능하지만 여러 요소를 갖는 벡터를 새로 만들수도 있습니다. 예를 들어 `vec3(1.0, 2.0, 3.0).xy`는 결과적으로 `vec2` 입니다. 벡터의 생성자(constructor)는 벡터 객체와 스칼라(scalar)값의 조합을 받을 수 있습니다. 예를 들어 `vec3`가 `vec3(vec2(1.0, 2.0), 3.0)`를 통해 만들어질 수 있습니다. + +이전 챕터에서 이야기한 것처럼 삼각형을 화면에 그리기 위해 우리는 정점 셰이더와 프래그먼트 셰이더를 작성해야 합니다. 다음 두 섹션에서 각각의 GLSL 코드를 설명할 것이고 그 이후에는 SPIR-V 바이너리를 만드는 방법과 이를 프로그램에 로드(load)하는 법을 보여드리겠습니다. + +## 정점 셰이더 + +정점 셰이더는 각 입력 정점을 처리합니다. 정점 셰이더는 모델 공간 좌표, 색상, 법선과 텍스처 좌표같은 입력 데이터를 어트리뷰트로 받습니다. 출력은 클립 좌표(clip coordinate) 위치와 프래그먼트 셰이더로 전달할 색상과 텍스처 좌표와 같은 어트리뷰트 들입니다. 이 값들은 래스터화 단계에서 여러 프래그먼트에 걸쳐 부드럽게 변하도록(smooth gradient) 보간됩니다. + +*클립 좌표*는 정점 셰이더에서 도출된 4차원 벡터로 벡터를 마지막 구성요소의 값으로 나눔으로써 *정규화된 장치 좌표(normalized device coordinate)*로 변환됩니다. 정규화된 장치 좌표계는 [동차 좌표(homogeneous coordinates)](https://en.wikipedia.org/wiki/Homogeneous_coordinates)로, 아래 그림과 같이 프레임버퍼와 맵핑되는 [-1, 1]x[-1, 1] 좌표계입니다: + +![](/images/normalized_device_coordinates.svg) + +컴퓨터 그래픽스를 좀 공부하셨다면 이런 것들이 익숙하실 겁니다. OpenGL을 사용해보셨다면 Y좌표가 뒤집혀 있는 것을 눈치채실 겁니다. Z좌표도 이제 Direct3D와 동일하게 0에서 1 사이의 값을 사용합니다. + +첫 번째 삼각형 그릴 때, 우리는 아무 변환도 적용하지 않을 것입니다. 그냥 3개 정점의 위치를 정규화된 장치 좌표에서 직접 명시하여 아래와 같은 모양을 만들 것입니다: +![](/images/triangle_coordinates.svg) + +정점 셰이더의 클립 좌표에서 마지막 요소를 `1`로 설정하여 정규화된 장치 좌표를 바로 출력되도록 할 수 있습니다. 이렇게 하면 클립 좌표를 정규화된 장치 좌표로 변환해도 아무런 값의 변화가 없을것입니다. + +일반적으로 이러한 좌표는 정점 버퍼에서 저장되겠지만 Vulkan에서 정점 버퍼를 생성하고 값을 집어넣는 것은 쉽지 않습니다. 그래서 이러한 작업은 화면에 삼각형을 띄우는 만족스러운 결과 이후로 미루도록 하겠습니다. 정석적인 방법은 아니지만 정점 셰이더에 좌표값을 직접 추가하겠습니다. 코드는 아래와 같습니다: + +```glsl +#version 450 + +vec2 positions[3] = vec2[]( + vec2(0.0, -0.5), + vec2(0.5, 0.5), + vec2(-0.5, 0.5) +); + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); +} +``` + +`main` 함수는 모든 정점에 대해 호출됩니다. `gl_VertexIndex` 내장 변수가 현재 정점의 인덱스를 가지고 있습니다. 이는 보통 정점 버퍼의 인덱스이지만 우리의 경우 하드코딩된 정점 데이터의 인덱스를 의미합니다. 각 정점의 위치는 셰이더에 있는 상수 배열로부터 얻어지고, `z`와 `w`값이 합쳐져 클립 좌표값이 됩니다. `gl_Position` 내장 변수가 출력처럼 활용됩니다. + +## 프래그먼트 셰이더 + +정점 셰이더의 위치들로 구성된 삼각형은 화면상의 일정 영역을 프래그먼트로 채우게 됩니다. 프래그먼트 셰이더는 이 프래그먼트들에 대해 실행되어 프레임버퍼(들)의 색상과 깊이 값을 생성합니다. 전체 삼각형에 대해 빨간색을 출력하는 간단한 프래그먼트 셰이더는 아래와 같습니다: + +```glsl +#version 450 + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(1.0, 0.0, 0.0, 1.0); +} +``` + +정점 셰이더가 모든 정점에 대해 `main` 함수를 호출하는 것처럼, 모든 프래그먼트에 대해 `main`함수가 호출됩니다. GLSL에서 색상은 [0, 1] 범위의 R,G,B와 알파 채널의 4차원 벡터로 표현됩니다. 정점 셰이더의 `gl_Position`과는 다르게, 현재 프래그먼트 출력을 위한 내장 변수는 없습니다. 각 프레임버퍼를 위한 출력 변수는 스스로 명시해야 하며 `layout(location = 0)` 수식어가 프레임버퍼의 인덱스를 명시합니다. 빨간색이 이러한 `outColor` 변수에 쓰여졌고, 이는 첫 번째인(그리고 유일한) `0`번 인덱스 프레임버퍼와 연결되어 있습니다. + +## 정점별 색상 + +삼각형 전체를 빨간색으로 만드는것 재미가 없네요. 아래와 같이 그린다면 훨씬 재미있지 않을까요? + +![](/images/triangle_coordinates_colors.png) + +이를 위해 두 셰이더 모두에 약간의 수정을 하겠습니다. 먼저 세 개의 정점에 각각 다른 색상을 명시해 주어야 합니다. 이제 정점 셰이더는 위치와 함께 색상을 위한 배열도 가집니다: + +```glsl +vec3 colors[3] = vec3[]( + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0) +); +``` + +이제 프래그먼트 셰이더에 정점별 색상을 전달해줘서 프레임버퍼에 보간된 색상을 출력하게 하면 됩니다. 정점 셰이더에 색상 출력을 위한 변수를 추가하고 `main`함수에서 값을 쓰면 됩니다: + +```glsl +layout(location = 0) out vec3 fragColor; + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + fragColor = colors[gl_VertexIndex]; +} +``` + +다음으로, 프래그먼트 셰이더에는 매칭되는 입력을 추가해야 합니다: + +```glsl +layout(location = 0) in vec3 fragColor; + +void main() { + outColor = vec4(fragColor, 1.0); +} +``` + +입력 변수의 이름이 (정점 셰이더의 출력과) 같은 이름일 필요는 없습니다. 이들은 `location` 지시어에 의해 명시된 인덱스를 기반으로 연결됩니다. `main`함수는 알파값과 함께 색상을 출력하도록 수정되었습니다. 위쪽 이미지에서 본 것처럼, `fragColor`의 값은 자동으로 세 정점 사이에서 보간되어 연속적인 값을 보여줍니다. + +## 셰이더 컴파일 + +프로젝트의 루트(root) 디렉토리에 `shaders`라는 이름의 디렉토리를 만들고 정점 셰이더는 `shader.vert` 파일에, 프래그먼트 셰이더는 `shader.frag`파일에 작성하고 해당 디렉토리에 넣으세요. GLSL 셰이더를 위한 공식적인 확장자는 없지만, 이러한 방식이 그 둘을 구분하기 위해 일반적으로 사용하는 방법입니다. + +`shader.vert`의 내용은 아래와 같습니다: + +```glsl +#version 450 + +layout(location = 0) out vec3 fragColor; + +vec2 positions[3] = vec2[]( + vec2(0.0, -0.5), + vec2(0.5, 0.5), + vec2(-0.5, 0.5) +); + +vec3 colors[3] = vec3[]( + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, 1.0) +); + +void main() { + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); + fragColor = colors[gl_VertexIndex]; +} +``` + +`shader.frag`의 내용은 아래와 같습니다: + +```glsl +#version 450 + +layout(location = 0) in vec3 fragColor; + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(fragColor, 1.0); +} +``` + +이제 `glslc` 프로그램을 사용해 이들을 SPIR-V 바이트코드로 만들겁니다. + +**윈도우즈** + +아래와 같은 내용을 담은 `compile.bat` 파일을 만듭니다: + +```bash +C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.vert -o vert.spv +C:/VulkanSDK/x.x.x.x/Bin/glslc.exe shader.frag -o frag.spv +pause +``` + +`glslc.exe`의 경로는 여러분이 Vulkan SDK를 설치한 경로로 설정해 주세요. 그리고 더블클릭하여 실행합니다. + +**리눅스** + +아래와 같은 내용을 담은 `compile.sh` 파일을 만듭니다: + +```bash +/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.vert -o vert.spv +/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslc shader.frag -o frag.spv +``` + +`glslc.exe`의 경로는 여러분이 Vulkan SDK를 설치한 경로로 설정해 주세요. 그리고 `chmod +x compile.sh`로 실행 파일을 만든 뒤 실행합니다. + +**플랫폼별 안내는 여기까지** + +위 두 명령어는 `-o` (output) 플래그로 컴파일러에게 GLSL 소스 파일을 읽어서 SPIR-V 바이트코드 파일을 출력하도록 합니다. + +여러분의 셰이더에 문법적 오류가 있다면 컴파일러가 해당하는 라인과 문제가 뭔지를 알려줍니다. 예를 들어 세미콜론을 지우고 다시 컴파일 해 보세요. 또한 아무런 인자 없이 컴파일러를 실행해 어떤 플래그들이 지원되는지 살펴 보세요. 예를 들어 바이트코드를 사람이 읽을 수 있는 포맷으로 출력하여 셰이더가 정확히 어떤 일을 하는지도 볼 수 있고, 이 단계에서 어떤 최적화가 적용되는지도 볼 수 있습니다. + +셰이더를 명령줄(commandline)로 컴파일하는 것은 가장 직관적인 방법이고 이 튜토리얼에서는 이 방식을 사용할 것이지만, 코드 내에서 셰이더를 컴파일하도록 할 수도 있습니다. Vulkan SDK에는 [libshaderc](https://github.com/google/shaderc)가 포함되어 있는데, 프로그램 내에서 GLSL 코드를 SPIR-V로 컴파일하기 위한 라이브러리입니다. + +## 셰이더 로딩 + +SPIR-V 셰이더를 생성할 방법을 알아봤으니 이제는 그 결과물을 프로그램에 로드하고 이를 그래픽스 파이프라인 어딘가에 꽂아넣을 시간입니다. 먼저 간단한 헬퍼 함수를 만들어 바이너리 데이터를 파일로부터 로드할 수 있도록 합니다. + +```c++ +#include + +... + +static std::vector readFile(const std::string& filename) { + std::ifstream file(filename, std::ios::ate | std::ios::binary); + + if (!file.is_open()) { + throw std::runtime_error("failed to open file!"); + } +} +``` + +`readFile` 함수는 명시한 파일에서 모든 바이트를 읽어와서 `std::vector`로 저장된 바이트 배열을 반환하도록 할 것입니다. 먼저 두 개의 플래그로 파일을 엽니다. + +- `ate`: 파일의 끝에서부터 읽습니다. +- `binary`: 파일을 바이너리로 읽습니다 (텍스트로 변환 방지) + +끝에서부터 읽는 경우의 장점은 읽기 위치를 사용해 파일의 크기를 파악하여 버퍼를 할당할 수 있다는 점입니다. + +```c++ +size_t fileSize = (size_t) file.tellg(); +std::vector buffer(fileSize); +``` + +그러고 나서 파일의 맨 앞까지 탐색하여 모든 바이트를 한 번에 읽어옵니다: + +```c++ +file.seekg(0); +file.read(buffer.data(), fileSize); +``` + +마지막으로 파일을 닫고 바이트를 반환합니다: + +```c++ +file.close(); + +return buffer; +``` + +이제 이 함수를 `createGraphicsPipeline`에서 호출하여 두 셰이더의 바이트코드를 로드합니다: + +```c++ +void createGraphicsPipeline() { + auto vertShaderCode = readFile("shaders/vert.spv"); + auto fragShaderCode = readFile("shaders/frag.spv"); +} +``` + +셰이더가 제대로 로드 되었는지를 버퍼의 크기를 출력하여 파일의 실제 바이트 사이와 일치하는를 통해 확인하세요. 바이너리 코드이기 때문에 널 종료(null terminate)여야 할 필요가 없고 나중에는 이러한 크기를 명시적으로 확인할 것입니다. + +## 셰이더 모듈 생성 + +코드를 파이프라인에 넘기기 전에, `VkShaderModule` 객체로 이들을 감싸야 합니다. 이를 위한 `createShaderModule` 헬퍼 함수를 만듭시다. + +```c++ +VkShaderModule createShaderModule(const std::vector& code) { + +} +``` + +이 함수는 바이트코드 버퍼를 매개변수로 받아서 `VkShaderModule`를 만들 것입니다. + +셰이더 모듈을 만드는 것은 간단합니다. 바이트코드가 있는 버퍼에 대한 포인터와 그 길이를 명시해 주기만 하면 됩니다. 이 정보들은 `VkShaderModuleCreateInfo` 구조체에 명시할 것입니다. 하나 주의할 점은 바이트코드의 크기는 바이트 단위이지만, 바이트코드의 포인터는 `char` 포인터가 아닌 `uint32_t` 포인터라는 것입니다. 따라서 아래 보이는 것처럼 `reinterpret_cast`를 활용해 캐스팅(cast)을 해 주어야 합니다. 이런 식으로 캐스팅을 하게 되면 데이터가 `uint32_t`의 정렬(alignment) 요구사항에 맞는지 확인해 주어야 합니다. 다행히 데이터는 기본 할당자가 정렬 요구사항을 만족하도록 보장되어 있는 `std::vector`에 저장되어 있으니 문제 없습니다. + +```c++ +VkShaderModuleCreateInfo createInfo{}; +createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO; +createInfo.codeSize = code.size(); +createInfo.pCode = reinterpret_cast(code.data()); +``` + +`VkShaderModule`은 `vkCreateShaderModule`를 호출하여 생성됩니다: + +```c++ +VkShaderModule shaderModule; +if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) { + throw std::runtime_error("failed to create shader module!"); +} +``` + +매개변수는 이전 객체 생성 함수와 동일합니다. 논리적 장치, 생성 정보를 담은 구조체에 대한 포인터, 사용자 정의 할당자를 위한 선택적 포인터, 그리고 출력 변수에 대한 핸들입니다. 셰이더 모듈을 생성하고 나면 코드가 담긴 버퍼는 해제되어도 됩니다. 만들어진 셰이더 모듈을 반환하는 것도 잊지 마시고요: + +```c++ +return shaderModule; +``` + +셰이더 모듈은 단지 파일로부터 로드한 셰이더 바이트코드를 감싸는 작은 래퍼입니다. GPU에서 실행을 위해 수행하는 SPIR-V 바이트코드의 컴파일과 링킹을 통한 기계 코드로의 변환은 그래픽스 파이프라인이 생성되기 전에는 수행되지 않습니다. 즉, 파이프라인 생성이 완료되면 셰이더 모듈은 소멸되어도 문제가 없고, 그러한 이유로 우리는 이들을 클래스 멤버가 아닌 `createGraphicsPipeline`함수의 지역 변수로 선언할 것입니다: + +```c++ +void createGraphicsPipeline() { + auto vertShaderCode = readFile("shaders/vert.spv"); + auto fragShaderCode = readFile("shaders/frag.spv"); + + VkShaderModule vertShaderModule = createShaderModule(vertShaderCode); + VkShaderModule fragShaderModule = createShaderModule(fragShaderCode); +``` + +정리 과정은 함수의 마지막 부분에 `vkDestroyShaderModule` 함수를 두 번 호출함으로써 이루어집니다. 이 챕터의 나머지 모든 코드는 이 둘 사이에 작성될 것입니다. + +```c++ + ... + vkDestroyShaderModule(device, fragShaderModule, nullptr); + vkDestroyShaderModule(device, vertShaderModule, nullptr); +} +``` + +## 셰이더 단계(stage) 생성 + +셰이더를 실제로 사용하기 위해서는 이들을 `VkPipelineShaderStageCreateInfo` 구조체를 활용하여 파이프라인의 특정 단계에 할당해야 하고, 이 역시 파이프라인 생성 과정의 한 부분입니다. + +먼저 정점 셰이더를 위한 구조체를 채우는 것부터 시작할 것인데, 마찬가지로 `createGraphicsPipeline` 함수 안에서 이루어집니다. + +```c++ +VkPipelineShaderStageCreateInfo vertShaderStageInfo{}; +vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; +``` + +첫 단계로 당연히 필요한 `sType` 외에, Vulkan에게 셰이더가 사용될 파이프라인 단계를 알려줍니다. 이전 챕터에서 설명한 각 프로그램가능한 단계를 위한 열거형 값들이 있습니다. + +```c++ +vertShaderStageInfo.module = vertShaderModule; +vertShaderStageInfo.pName = "main"; +``` + +다음 두 멤버는 코드를 담은 셰이더 모듈과 *진입점(entrypoint)*인 호출할 함수를 명시합니다. 즉 여러 프래그먼트 셰이더들을 하나의 셰이더 모듈로 만들고 서로 다른 진입점을 사용해 다른 동작을 하도록 만들 수도 있습니다. 지금은 그냥 표준적인 `main`을 사용할 것입니다. + +마지막 하나의 (선택적인) 멤버는 `pSpecializationInfo`이고, 여기서 사용할 것은 아니지만 언급할 필요는 있습니다. 이 멤버는 셰이더 상수(constant)의 값을 명시할 수 있도록 합니다. 하나의 셰이더 모듈을 만들고 파이프라인 생성 단계에서 사용되는 상수의 값을 다르게 명시하여 다르게 동작하도록 할 수 있습니다. 이렇게 하는 것이 변수를 사용하여 렌더링 시점에 셰이더의 동작을 바꾸는 것보다 효율적인데, 이렇게 하면 컴파일러가 이 값에 의존하는 `if` 분기를 제거하는 등의 최적화를 할 수 있습니다. 이에 해당하는 상수가 없다면 이 값은 `nullptr`로 두면 되고, 지금 우리 코드에서는 자동으로 이렇게 됩니다. + +프래그먼트 셰이더를 위해 구조체를 수정하는 것은 쉽습니다: + +```c++ +VkPipelineShaderStageCreateInfo fragShaderStageInfo{}; +fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT; +fragShaderStageInfo.module = fragShaderModule; +fragShaderStageInfo.pName = "main"; +``` + +이 두 구조체를 포함하는 배열을 정의하는 것이 마지막 단계이고, 실제 파이프라인 생성 단계에서는 이 배열을 사용해 셰이더 모듈들을 참조하도록 할 것입니다. + +```c++ +VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo}; +``` + +파이프라인의 프로그램 가능한 단계에 대한 설정은 여기까지입니다. 다음 챕터에서는 고정 함수 단계를 살펴볼 것입니다. + +[C++ code](/code/09_shader_modules.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md b/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md new file mode 100644 index 00000000..dc9be547 --- /dev/null +++ b/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/02_Fixed_functions.md @@ -0,0 +1,326 @@ +예전 그래픽스 API는 그래픽스 파이프라인 대부분의 단계에서 기본 상태(default state)를 제공했습니다. Vulkan에서는 파이프라인 상태 대부분을 명시적으로 설정해야 하고 이는 불변하는 파이프라인 상태 객체로 만들어집니다(baked). 이 챕터에서는 이러한 고정 함수 연산들에 대한 모든 구조체를 만들 것입니다. + +## 동적 상태(Dynamic state) + +*대부분*의 파이프라인 상태가 파이프라인 상태 객체로 만들어져야만 하지만 몇몇 상태들은 그리기 시점에 파이프라인을 *재생성하지 않고도 변경될 수 있습니다*. 예시로는 뷰포트의 크기라던지, 선의 두께라던지 블렌딩 상수 등이 있습니다. 동적 상태를 사용하고 싶고, 이런 상태들을 계속 제외된 상태로 두고 싶다면, `VkPipelineDynamicStateCreateInfo` 구조체를 아래와 같이 만들어야 합니다. + +```c++ +std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR +}; + +VkPipelineDynamicStateCreateInfo dynamicState{}; +dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; +dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); +dynamicState.pDynamicStates = dynamicStates.data(); +``` + +이렇게 하면 해당하는 값들의 설정은 무시되고 그리기 시점에 이들을 변경 가능(그리고 변경해야만) 합니다. 이렇게 하면 보다 유연한 설정이 가능하고 뷰포트나 시저(scissor) 상태에 대해서는 그렇게 하는 것이 일반적이지만, 파이프라인 상태 객체를 만드는 것이 보다 복잡해집니다. + +## 정점 입력 + +`VkPipelineVertexInputStateCreateInfo` 구조체는 정점 데이터의 포맷을 기술하고, 이는 정점 셰이더로 넘겨집니다. 크게 두 가지 방법으로 기술됩니다: + +* 바인딩: 데이터 사이의 간격과 데이터가 정점별 데이터인지 인스턴스별(per-instance) 데이터인지 여부 ([인스턴싱](https://en.wikipedia.org/wiki/Geometry_instancing) 참고) +* 어트리뷰트 기술: 정점 셰이더에 전달된 어트리뷰트의 타입, 어떤 바인딩에 이들을 로드할 것인지와 오프셋이 얼마인지 + +우리는 정점 셰이더에 정점 데이터를 하드 코딩하고 있기 때문에 지금은 이 구조체에 로드할 정점 데이터가 없다고 명시할 것입니다. 정점 버퍼 챕터에서 다시 살펴볼 것입니다. + +```c++ +VkPipelineVertexInputStateCreateInfo vertexInputInfo{}; +vertexInputInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO; +vertexInputInfo.vertexBindingDescriptionCount = 0; +vertexInputInfo.pVertexBindingDescriptions = nullptr; // Optional +vertexInputInfo.vertexAttributeDescriptionCount = 0; +vertexInputInfo.pVertexAttributeDescriptions = nullptr; // Optional +``` + +`pVertexBindingDescriptions`와 `pVertexAttributeDescriptions` 멤버는 앞서 언급한 정점 데이터를 로드하기 위한 세부 사항들을 기술하는 구조체의 배열에 대한 포인터입니다. 이 구조체를 `createGraphicsPipeline`함수의 `shaderStages` 배열 뒤에 추가합니다. + +## 입력 조립 + +`VkPipelineInputAssemblyStateCreateInfo`구조체는 두 가지를 기술합니다: 정점으로부터 어떤 기하 형상이 그려질지와 프리미티브 재시작(restart)을 활성화할지 여부입니다. 앞의 내용은 `topology` 멤버에 명시되고 가능한 값들은 아래와 같습니다: + +* `VK_PRIMITIVE_TOPOLOGY_POINT_LIST`: 정점으로부터 점을 그림 +* `VK_PRIMITIVE_TOPOLOGY_LINE_LIST`: 재사용 없이 두 정점마다 선을 그림 +* `VK_PRIMITIVE_TOPOLOGY_LINE_STRIP`: 선의 마지막 정점이 다음 선의 시작점으로 사용됨 +* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST`: 재사용 없이 3개의 정점마다 삼각형을 그림 +* `VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP `: 삼각형의 두 번쨰와 세 번째 정점이 다음 삼각형의 첫 두 개의 정점으로 사용됨 + +일반적으로 정점은 정점 버퍼로부터 인덱스 순서대로 로드되지만, *요소 버퍼(element buffer)*를 사용해 인덱스를 직접 명시할 수 있습니다. 이렇게 하면 정점의 재사용을 통해 성능을 최적화 할 수 있습니다. `primitiveRestartEnable` 멤버를 `VK_TRUE`로 설정했다면, `_STRIP` 토폴로지 모드의 선과 삼각형을 `0xFFFF` 또는 `0xFFFFFFFF`와 같은 특별한 인덱스를 사용해 분할할 수 있습니다. + +이 예제에서는 삼각형들을 그릴 것이므로 구조체는 아래와 같은 값들로 설정할 것입니다: + +```c++ +VkPipelineInputAssemblyStateCreateInfo inputAssembly{}; +inputAssembly.sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO; +inputAssembly.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; +inputAssembly.primitiveRestartEnable = VK_FALSE; +``` + +## 뷰포트와 시저 + +뷰포트는 출력으로 그려질 프레임버퍼의 영역을 설정합니다. 튜토리얼에서는 거의 항상 `(0, 0)`에서 `(width, height)`까지고, 지금도 그렇게 설정합니다. + +```c++ +VkViewport viewport{}; +viewport.x = 0.0f; +viewport.y = 0.0f; +viewport.width = (float) swapChainExtent.width; +viewport.height = (float) swapChainExtent.height; +viewport.minDepth = 0.0f; +viewport.maxDepth = 1.0f; +``` + +스왑 체인의 크기와 그 이미지가 윈도우의 `WIDTH`와 `HEIGHT`와는 다르다는 것을 기억하십시오. 스왑 체인 이미지는 나중에 프레임버퍼로 사용될 것이므로 그 크기를 사용해야 합니다. + +`minDepth`와 `maxDepth` 값은 프레임버퍼에서 사용할 깊이 값의 범위입니다. 이 값들은 `[0.0f, 1.0f]` 범위여야 하지만 `minDepth`가 `maxDepth`보다 클 수 있습니다. 특수한 작업을 하는 것이 아니라면 `0.0f`와 `1.0f`의 일반적인 값을 사용하면 됩니다. + +뷰포트가 이미지로부터 프레임버퍼로의 변환을 정의하는 반면, 시저 사각형은 픽셀이 저장될 실제 영역을 정의합니다. 시저 사각형 밖의 픽셀은 래스터화 단계에서 버려집니다. 이는 변환이 아닌 필터라고 보면 됩니다. 차이점이 아래에 나타나 있습니다. 왼쪽 시저 사각형은 위와 같은 결과가 도출되는 수 많은 가능성 중 하나일 뿐임에 주의하십시오. 뷰포트보다 큰 시저 사각형이면 모두 결과가 왼쪽 위와 같이 나타나게 됩니다. + +![](/images/viewports_scissors.png) + +따라서 전체 프레임버퍼에 그리고 싶다면 시저 사각형은 전체 범위를 커버하도록 명시하면 됩니다: + +```c++ +VkRect2D scissor{}; +scissor.offset = {0, 0}; +scissor.extent = swapChainExtent; +``` + +뷰포트와 시저 사각형은 파이프라인의 정적인 부분으로 명시할 수도 있고 [동적 상태](#dynamic-state)로 명시할 수도 있습니다. 정적으로 하는 경우 다른 상태들과 비슷하게 유지되지만 이들은 동적 상태로 명시하는 것이 유연성을 위해 더 편리한 방법입니다. 이런 방식이 더 일반적이고 동적 상태는 성능 저하 없도록 구현되어 있습니다. + +동적 뷰포트와 시저 사각형을 위해서는 해당하는 동적 상태를 파이프라인에서 활성화해야 합니다: + +```c++ +std::vector dynamicStates = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR +}; + +VkPipelineDynamicStateCreateInfo dynamicState{}; +dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; +dynamicState.dynamicStateCount = static_cast(dynamicStates.size()); +dynamicState.pDynamicStates = dynamicStates.data(); +``` + +그리고 그 개수를 파이프라인 생성 시에 명시해주면 됩니다. + +```c++ +VkPipelineViewportStateCreateInfo viewportState{}; +viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; +viewportState.viewportCount = 1; +viewportState.scissorCount = 1; +``` + +실제 뷰포트와 시저 사각형은 그리기 시점에 설정하면 됩니다. + +동적 상태를 사용하면 하나의 명령 버퍼로부터 여러 뷰포트와 시저 사각형을 명시하는 것도 가능합니다. + +동적 상태를 사용하지 않으면, 뷰포트와 시저 사각형은 `VkPipelineViewportStateCreateInfo` 구조체를 사용해 파이프라인에 설정되어야 합니다. 이렇게 생성된 뷰포트와 시저 사각형은 해당 파이프라인에서 불변성을 가집니다. 이 값들을 변경하고자 하면 새로운 값으로 새로운 파이프라인을 생성해야 합니다. + +```c++ +VkPipelineViewportStateCreateInfo viewportState{}; +viewportState.sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO; +viewportState.viewportCount = 1; +viewportState.pViewports = &viewport; +viewportState.scissorCount = 1; +viewportState.pScissors = &scissor; +``` + +어떻게 설정하셨건, 어떤 그래픽 카드에서는 다중 뷰포트와 시저 사각형이 사용 가능하므로 구조체의 멤버는 이들의 배열을 참조하도록 되어 있습니다. 다중 뷰포트 또는 시저 사각형을 사용하려면 GPU 기능을 활성화 하는 것도 필요합니다 (논리적 장치 생성 부분을 참고하세요). + +## 래스터화 + +래스터화는 정점 셰이더로부터 만들어진 정점을 받아서 프래그먼트 셰이더에서 색상을 결정할 프래그먼트로 변환합니다. 또한 [깊이 테스트](https://en.wikipedia.org/wiki/Z-buffering), +[face culling](https://en.wikipedia.org/wiki/Back-face_culling)과 시저 테스트를 수행하고 출력 프래그먼트가 다각형 내부를 채우는지, 모서리만 그리는지(와이어프레임 렌더링)도 설정할 수 있습니다. 이 모든 것들은 `VkPipelineRasterizationStateCreateInfo` 구조체를 통해 설정합니다. + +```c++ +VkPipelineRasterizationStateCreateInfo rasterizer{}; +rasterizer.sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO; +rasterizer.depthClampEnable = VK_FALSE; +``` + +`depthClampEnable`가 `VK_TRUE`면, near plane과 far plane 밖의 프래그먼트는 값이 버려지는 대신 clamp됩니다. 이는 그림자 맵과 같은 특수한 경우에 유용한 기능입니다. 이는 GPU 기능을 활성화함으로써 사용 가능합니다. + +```c++ +rasterizer.rasterizerDiscardEnable = VK_FALSE; +``` + +`rasterizerDiscardEnable`가 `VK_TRUE`면 기하 요소는 래스터화 다음 단계로 넘어가지 않습니다. 이렇게 되면 기본적으로 프레임버퍼에 아무것도 출력되지 않습니다. + +```c++ +rasterizer.polygonMode = VK_POLYGON_MODE_FILL; +``` + +`polygonMode`는 프래그먼트가 기하 요소를 생성하는 방식을 결정합니다. 아래와 같은 모드들이 사용 가능합니다: + +* `VK_POLYGON_MODE_FILL`: 폴리곤 영역을 프래그먼트로 채움 +* `VK_POLYGON_MODE_LINE`: 폴리곤의 모서리가 선으로 그려짐 +* `VK_POLYGON_MODE_POINT`: 폴리곤의 정점이 점으로 그려짐 + +채우기 모드 이외에는 GPU 기능을 활성화해야 사용 가능합니다. + +```c++ +rasterizer.lineWidth = 1.0f; +``` + +`lineWidth` 멤버는 이해하기 쉽습니다. 프래그먼트 개수 단위로 선의 두께를 명시합니다. 지원하는 선의 최대 두께는 하드웨어에 따라 다르며, `1.0f` 이상의 값은 `wideLines` GPU 기능을 활성화해야만 사용 가능합니다. + +```c++ +rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; +rasterizer.frontFace = VK_FRONT_FACE_CLOCKWISE; +``` + +`cullMode` 변수는 사용할 face culling 타입을 결정합니다. culling을 하지 않거나, 앞면(front face)를 culling하거나, 뒷면(back fack)를 culling하거나, 양면 모두를 culling할 수 있습니다. `frontFace` 변수는 앞면으로 간주할 면의 정점 순서를 명시하며 시계방향 또는 반시계방향일 수 있습니다. + +```c++ +rasterizer.depthBiasEnable = VK_FALSE; +rasterizer.depthBiasConstantFactor = 0.0f; // Optional +rasterizer.depthBiasClamp = 0.0f; // Optional +rasterizer.depthBiasSlopeFactor = 0.0f; // Optional +``` + +래스터화 단계에서 상수를 더하거나 프래그먼트의 기울기를 기반으로 깊이값을 편향(bias)시킬 수 있습니다. 이 기능은 그림자 맵에서 종종 사용되지만, 우리는 사용하지 않을 것입니다. `depthBiasEnable`를 `VK_FALSE`로 설정합니다. + +## 멀티샘플링(Multisampling) + +`VkPipelineMultisampleStateCreateInfo`구조체는 멀티샘플링을 설정하는데, 이는 [안티앨리어싱(anti-aliasing)](https://en.wikipedia.org/wiki/Multisample_anti-aliasing)을 하는 방법 중 하나입니다. 이는 동일한 픽셀로 래스터화되는 여러 다각형의 프래그먼트 셰이더 결과를 결합하여 수행됩니다. 주로 모서리에서 수행되며, 모서리가 앨리어싱에 따른 문제가 가장 눈에 띄게 발생하는 부분입니다. 단일 다각형이 픽셀에 맵핑되는 경우에는 프래그먼트 셰이더를 여러 번 실행할 필요가 없기 때문에, 단순히 고해상도로 렌더링한 후에 다운샘플링(downsampling)하는 것 보다 훨씬 계산 비용이 낮습니다. 이를 사용하려면 GPU 기능을 활성화해야 합니다. + +```c++ +VkPipelineMultisampleStateCreateInfo multisampling{}; +multisampling.sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO; +multisampling.sampleShadingEnable = VK_FALSE; +multisampling.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; +multisampling.minSampleShading = 1.0f; // Optional +multisampling.pSampleMask = nullptr; // Optional +multisampling.alphaToCoverageEnable = VK_FALSE; // Optional +multisampling.alphaToOneEnable = VK_FALSE; // Optional +``` + +멀티샘플링은 나중 챕터에서 다시 살펴볼 것이고, 지금은 활성화 하지 않은 상태로 두겠습니다. + +## 깊이와 스텐실(stencil) 테스트 + +깊이 또는 스텐실 버퍼를 사용하는 경우 `VkPipelineDepthStencilStateCreateInfo`를 사용해 깊이와 스텐실 테스트를 설정해야 합니다. 지금은 그렇지 않으니 구조체에 대한 포인터 대신 `nullptr`를 전달합니다. 깊이 버퍼링 챕터에서 다시 사용할 것입니다. + +## 컬러 블렌딩 + +프래그먼트 셰이더가 값을 반환한 후에는 이미 프레임버퍼에 쓰여진 색상값과 결합되어야 합니다. 이러한 변환 과정은 컬러 블렌딩이라 하며 두 가지 방법이 있습니다: + +* 쓰여진 값과 새 값을 섞어 새로운 색상을 만듬 +* 쓰여진 값과 새 값을 비트 연산(bitwise operation)하여 결합 + +컬러 블렌딩을 구성하는 두 종류의 구조체가 있습니다. 먼저 `VkPipelineColorBlendAttachmentState`는 어태치(attach)된 프레임버퍼별 설정을 담은 구조체이며 `VkPipelineColorBlendStateCreateInfo`는 *전역(global)* 컬러 블렌딩 설정을 담고 있습니다. 우리는 하나의 프레임버퍼만 사용합니다: + +```c++ +VkPipelineColorBlendAttachmentState colorBlendAttachment{}; +colorBlendAttachment.colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; +colorBlendAttachment.blendEnable = VK_FALSE; +colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_ONE; // Optional +colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional +colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; // Optional +colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; // Optional +colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; // Optional +colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; // Optional +``` + +이 프레임버퍼별 구조체는 컬러 블렌딩의 첫 번째 방법을 설정할 수 있도록 합니다. 수행되는 연산은 다음과 같은 의사 코드(pseudocode)로 잘 설명됩니다: + +```c++ +if (blendEnable) { + finalColor.rgb = (srcColorBlendFactor * newColor.rgb) (dstColorBlendFactor * oldColor.rgb); + finalColor.a = (srcAlphaBlendFactor * newColor.a) (dstAlphaBlendFactor * oldColor.a); +} else { + finalColor = newColor; +} + +finalColor = finalColor & colorWriteMask; +``` + +`blendEnable`가 `VK_FALSE`면, 프래그먼트 셰이더에서 계산한 새로운 색상이 수정되지 않고 전달됩니다. 그렇지 않으면 새로운 색상을 위해 두 개의 결합(mix) 연산이 수행됩니다. 결과 색상은 `colorWriteMask`를 통해 명시된 채널들과 AND연산이 수행되어 전달될 채널이 결정됩니다. + +컬러 블렌딩을 하는 가장 보편적인 방법은 알파 블렌딩(alpha blending)입니다. 이는 새로운 색상이 이미 쓰여진 색상과 불투명도(opacity)를 기반으로 섞이는 것입니다. `finalColor`는 다음과 같이 계산됩니다: + +```c++ +finalColor.rgb = newAlpha * newColor + (1 - newAlpha) * oldColor; +finalColor.a = newAlpha.a; +``` + +이는 아래와 같은 매개변수를 사용하면 수행됩니다: + +```c++ +colorBlendAttachment.blendEnable = VK_TRUE; +colorBlendAttachment.srcColorBlendFactor = VK_BLEND_FACTOR_SRC_ALPHA; +colorBlendAttachment.dstColorBlendFactor = VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; +colorBlendAttachment.colorBlendOp = VK_BLEND_OP_ADD; +colorBlendAttachment.srcAlphaBlendFactor = VK_BLEND_FACTOR_ONE; +colorBlendAttachment.dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO; +colorBlendAttachment.alphaBlendOp = VK_BLEND_OP_ADD; +``` + +명세에 있는 `VkBlendFactor`와 `VkBlendOp` 열거형을 통해 모든 가능한 연산을 찾아볼 수 있습니다. + +두 번째 구조체는 모든 프레임버퍼를 위한 구조체 배열의 참조이고, 앞서 언급한 계산들에 사용할 블렌드 팩터(blend factor)들로 사용될 블렌드 상수들을 설정할 수 있습니다. + +```c++ +VkPipelineColorBlendStateCreateInfo colorBlending{}; +colorBlending.sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO; +colorBlending.logicOpEnable = VK_FALSE; +colorBlending.logicOp = VK_LOGIC_OP_COPY; // Optional +colorBlending.attachmentCount = 1; +colorBlending.pAttachments = &colorBlendAttachment; +colorBlending.blendConstants[0] = 0.0f; // Optional +colorBlending.blendConstants[1] = 0.0f; // Optional +colorBlending.blendConstants[2] = 0.0f; // Optional +colorBlending.blendConstants[3] = 0.0f; // Optional +``` + +블렌딩의 두 번째 방법(비트 연산)을 하려면 `logicOpEnable`를 `VK_TRUE`로 설정해야 합니다. 그러면 비트 연산은 `logicOp` 필드에 명시됩니다. 주의하실 점은 이러한 경우 첫 번째 방법은 자동으로 비활성화 됩니다. 마치 여러분이 모든 프레임버퍼에 대해 `blendEnable`를 `VK_FALSE`로 설정한 것과 같이 말이죠! `colorWriteMask` 또한 이 모드에서 프레임버퍼의 어떤 채널이 영향을 받을지를 결정하기 위해 사용됩니다. 지금 우리가 한 것처럼 두 모드를 모두 비활성화 하는 것도 가능합니다. 이러한 경우 프래그먼트 색상은 변경되지 않고 그대로 프레임버퍼에 쓰여집니다. + +## 파이프라인 레이아웃(layout) + +셰이더에서 사용하는 `uniform`은 동적 상태 변수처럼 전역적인 값으로 셰이더를 재생성하지 않고 그리기 시점에 값을 변경하여 다른 동작을 하도록 할 수 있습니다. 이는 주로 변환 행렬을 정점 셰이더에 전달하거나, 프래그먼트 셰이더에 텍스처 샘플러(sampler)를 생성하기 위해 사용됩니다. + +이러한 uniform 값은 `VkPipelineLayout` 객체를 생성하여 파이프라인 생성 단계에서 명시되어야 합니다. 나중 챕터로 넘어가기 전까지는 사용하지 않을 것이지만 빈 파이프라인 레이아웃이라도 생성해 두어야만 합니다. + +이 객체를 저장할 클래스 멤버를 만들 것인데, 나중에 다른 함수에서 참조할 것이기 때문입니다. + +```c++ +VkPipelineLayout pipelineLayout; +``` + +그리고 `createGraphicsPipeline` 함수에서 객체를 만듭니다. + +```c++ +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 0; // Optional +pipelineLayoutInfo.pSetLayouts = nullptr; // Optional +pipelineLayoutInfo.pushConstantRangeCount = 0; // Optional +pipelineLayoutInfo.pPushConstantRanges = nullptr; // Optional + +if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create pipeline layout!"); +} +``` + +구조체는 또한 *push 상수*를 명시하는데, 나중에 알아보겠지만 동적인 값을 셰이더에 전달하는 또 다른 방법입니다. 파이프라인 레이아웃은 프로그램의 실행 주기동안 참조되므로 마지막에는 소멸되어야 합니다: + +```c++ +void cleanup() { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + ... +} +``` + +## 결론 + +이로써 고정 함수 상태는 끝입니다! 이 모든 것들을 처음부터 만들어가는 과정은 힘들었지만, 그로 인해 그래픽스 파이프라인에서 일어나는 거의 모든 일들을 알게 되었습니다! 이러한 과정으로 인해 뜻밖의 오류가 발생할 가능성이 줄어들 것인데 특성 구성요소의 기본 상태를 제공한다면 그렇지 않았을 것입니다. + +그래픽스 파이프라인 생성을 위해서는 아직도 하나 더 객체를 만들어야 하고, 이는 [렌더 패스(render pass)](!kr/Drawing_a_triangle/Graphics_pipeline_basics/Render_passes)입니다. + +[C++ code](/code/10_fixed_functions.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md b/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md new file mode 100644 index 00000000..7655b8c0 --- /dev/null +++ b/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/03_Render_passes.md @@ -0,0 +1,157 @@ +## 설정 + +파이프라인을 마무리 하기 전에, Vulkan에서 렌더링을 할 때는 사용할 프레임버퍼에 대해 알려줄 필요가 있습니다. 얼마나 많은 색상과 깊이 버퍼가 있을 것인지, 각각에 대해 얼마나 많은 샘플을 사용할 것인지, 렌더링 연산 과정에서 각각의 내용들이 어떻게 처리될 것인지 등을 명시해야 합니다. 이런 모든 정보가 *렌더 패스(render pass)* 객체에 포함되는데, 이를 위해 새롭게 `createRenderPass` 함수를 만들 겁니다. 이 함수를 `initVulkan` 함수에서 `createGraphicsPipeline` 전에 호출합니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); +} + +... + +void createRenderPass() { + +} +``` + +## 어태치먼트(attachment) 기술 + +우리의 경우 스왑 체인 안에 있는 이미지들 중 하나로써 하나의 색상 버퍼 어태치먼트만 있을 예정입니다. + +```c++ +void createRenderPass() { + VkAttachmentDescription colorAttachment{}; + colorAttachment.format = swapChainImageFormat; + colorAttachment.samples = VK_SAMPLE_COUNT_1_BIT; +} +``` + +색상 어태치먼트의 `format`과 스왑 체인의 이미지 포맷은 동일해야 하며, 지금은 멀티샘플링을 하지 않으니 1개의 샘플을 사용합니다. + +```c++ +colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; +colorAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE; +``` + +`loadOp` 과 `storeOp`은 렌더링 전과 후에 데이터로 어떤 작업을 할 것인지를 결정합니다. `loadOp`과 관련해서는 다음과 같은 선택지가 있습니다: + +* `VK_ATTACHMENT_LOAD_OP_LOAD`: 어태치먼트에 존재하는 내용을 유지 +* `VK_ATTACHMENT_LOAD_OP_CLEAR`: 시작 시 상수값으로 내용을 채움 +* `VK_ATTACHMENT_LOAD_OP_DONT_CARE`: 어떤 내용이 존재하는지 정의할 수 없음. 신경쓰지 않음 + +우리의 경우 새로운 프레임을 그리기 전에 clear 연산을 통해 검은 색으로 프레임버퍼를 지울 것입니다. `storeOp`의 선택지는 두 가지입니다: + +* `VK_ATTACHMENT_STORE_OP_STORE`: 렌더링된 내용이 메모리에 저장되고 나중에 읽을 수 있음 +* `VK_ATTACHMENT_STORE_OP_DONT_CARE`: 프레임버퍼의 내용이 렌더링 이후에도 정의되지 않음으로 남아 있음 + +우리는 화면에 삼각형을 그리고 그걸 확인하는 데 관심이 있으니 store 연산을 사용할 것입니다. + +```c++ +colorAttachment.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; +colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; +``` + +`loadOp`과 `storeOp`는 색상과 깊이 데이터에 대해 적용되고 `stencilLoadOp` / +`stencilStoreOp`은 스텐실 데이터에 적용됩니다. 우리 프로그램은 스텐실 버퍼에 대해 아무런 작업을 하지 않기 때문에 이에 대한 로드와 저장은 신경쓰지 않습니다. + +```c++ +colorAttachment.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +colorAttachment.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; +``` + +Vulkan의 텍스처와 프레임버퍼는 특정 픽셀 포맷의 `VkImage` 객체로 표현됩니다. 하지만 픽셀의 메모리 레이아웃은 이미지를 어디에 사용하느냐에 따라 달라질 수 있습니다. + +흔히 사용되는 레이아웃은 아래와 같습니다: + +* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: 이미지가 색상 어태치먼트로 사용됨 +* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: 스왑 체인을 통해 표시될 이미지 +* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: 메모리 복사의 대상으로 사용되는 이미지 + +텍스처링(texturing) 챕터에서 이 주제에 대해 보다 자세히 논의할 것입니다. 지금 알아두셔야 할 중요한 사항은 이미지는 그 이후에 사용될 작업과 관련한 연산에 어울리는 특정한 레이아웃으로 정의해야 한다는 것입니다. + +`initLayout`은 렌더 패스가 시작되기 전 이미지가 어떤 레이아웃일지 명시합니다. `finalLayout`은 렌더 패스가 끝나면 자동적으로 어떤 레이아웃으로 사용될지를 명시합니다. `initLayout`에 `VK_IMAGE_LAYOUT_UNDEFINED`를 사용하면 우리는 그 이미지가 전에 어떤 레이아웃이었는지 신경쓰지 않겠다는 뜻입니다. 이 경우의 단점은 이미지의 내용이 보존된다는 보장이 없다는 것이지만 지금 우리는 어차피 내용을 지우고 시작할 것이니 관계 없습니다. 렌더링 이후에 스왑 체인을 사용해 이미지를 표시할 것이니 `finalLayout`으로는 `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`를 사용합니다. + +## 서브패스(subpass)와 어태치먼트 참조 + +하나의 렌더 패스는 여러 서브패스로 구성될 수 있습니다. 서브패스는 이전 패스에서 저장된 프레임버퍼의 내용을 가지고 렌더링 연산을 수행하는 일련의 패스입니다. 예를 들어 여러 후처리(post-processing) 과정을 연속적으로 적용하는 경우를 들 수 있습니다. 이러한 렌더링 연산들의 집합을 하나의 렌더 패스에 넣으면, Vulkan이 그 순서를 조정해 메모리 대역폭을 아껴서 보다 나은 성능을 얻을 수 있습니다. 하지만 우리의 삼각형 같은 경우 하나의 서브패스만 있으면 됩니다. + +모든 서브패스는 우리가 이전 장에서 만든 하나 이상의 어태치먼트 구조체를 참조합니다. 이 참조는 `VkAttachmentReference` 구조체로 아래와 같습니다. + +```c++ +VkAttachmentReference colorAttachmentRef{}; +colorAttachmentRef.attachment = 0; +colorAttachmentRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; +``` + +`attachment` 매개변수는 인덱스를 사용해 어태치먼트 배열로부터 참조할 어태치먼트를 명시합니다. 우리 배열은 하나의 `VkAttachmentDescription`만 가지고 있으므로 인덱스는 `0`입니다. `layout`은 서브패스 실행 동안 이 참조를 사용하고자 하는 어태치먼트의 레이아웃을 명시합니다. Vulkan은 서브패스가 시작되면 자동적으로 어태치먼트를 이 레이아웃으로 전환합니다. 우리는 어태치먼트가 색상 버퍼의 기능을 하길 원하고 `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`가 이름 그대로 가장 좋은 성능을 내줄 겁니다. + +서브패스는 `VkSubpassDescription` 구조체를 사용해 기술됩니다: + +```c++ +VkSubpassDescription subpass{}; +subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; +``` + +Vulkan이 나중에는 계산을 위한 서브패스도 지원할 지 모르므로, 그래픽스 서브패스를 위한 것이면 이를 명시해야 합니다. 다음으로 색상 어태치먼트의 참조를 명시합니다: + +```c++ +subpass.colorAttachmentCount = 1; +subpass.pColorAttachments = &colorAttachmentRef; +``` + +이 배열의 어태치먼트의 인덱스는 프래그먼트 셰이더에서 직접 `layout(location = 0) out vec4 outColor` 지시자를 통해 참조됩니다! + +다른 어태치먼트 타입들이 서브패스로부터 참조될 수 있습니다: + +* `pInputAttachments`: 셰이서에서 읽어온 어태치먼트 +* `pResolveAttachments`: 색상 어태치먼트의 멀티샘플링을 위한 어태치먼트 +* `pDepthStencilAttachment`: 깊이와 스텐실 데이터를 위한 어태치먼트 +* `pPreserveAttachments`: 이 서브패스에는 사용되지 않지만 데이터가 보존되어야 하는 어태치먼트 + +## 렌더 패스 + +이제 어태치먼트와 기본적인 서브패스 참조가 명시되었으므로 렌더 패스를 만들 수 있습니다. `VkRenderPass` 객체를 저장하기 위한 새로운 클래스 멤버를 `pipelineLayout` 위에 정의합니다. + +```c++ +VkRenderPass renderPass; +VkPipelineLayout pipelineLayout; +``` + +렌더 패스 객체는 `VkRenderPassCreateInfo`구조체에 어태치먼트 배열과 서브패스들을 채워서 생성합니다. `VkRenderPassCreateInfo`객체는 그 배열의 인덱스를 명시함으로써 어태치먼트를 참조합니다. + +```c++ +VkRenderPassCreateInfo renderPassInfo{}; +renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; +renderPassInfo.attachmentCount = 1; +renderPassInfo.pAttachments = &colorAttachment; +renderPassInfo.subpassCount = 1; +renderPassInfo.pSubpasses = &subpass; + +if (vkCreateRenderPass(device, &renderPassInfo, nullptr, &renderPass) != VK_SUCCESS) { + throw std::runtime_error("failed to create render pass!"); +} +``` + +파이프라인 레이아웃처럼, 렌더 패스도 프로그램 실행 중 계속 참조되므로 프로그램 종료 시점에 해제되어야 합니다: + +```c++ +void cleanup() { + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + vkDestroyRenderPass(device, renderPass, nullptr); + ... +} +``` + +많은 작업을 했는데요, 이제 다음 챕터에서 모든 것들을 합쳐서 드디어 그래픽스 파이프라인 객체를 만들 것입니다! + +[C++ code](/code/11_render_passes.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md b/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md new file mode 100644 index 00000000..5e1494cb --- /dev/null +++ b/kr/03_Drawing_a_triangle/02_Graphics_pipeline_basics/04_Conclusion.md @@ -0,0 +1,85 @@ +이제 이전 장에서 만든 모든 구조체와 객체들을 사용해 그래픽스 파이프라인을 만들 것입니다! 복습으로 우리가 가진 객체들의 종류를 되돌아봅시다: + +* 셰이더 단계: 그래픽스 파이프라인 내의 프로그램 가능한 단계들의 기능을 정의하는 셰이더 모듈 +* 고정 함수 상태: 파이프라인의 고정함수 단계들을 정의하는 구조체들. 여기에는 입력 조립기, 래스터화, 뷰포트와 컬러 블렌딩이 포함됨 +* 파이프라인 레이아웃: 셰이더가 참조하는, 그리기 시점에 갱신될 수 있는 유니폼과 push 값들 +* 렌더 패스: 파이프라인에서 참조하는 어태치먼트들과 그 사용 용도 + +이 것들이 모여 그래픽스 파이프라인의 기능을 완전히 명시합니다. 이제 우리는 `createGraphicsPipeline` 함수의 마지막 부분에 `VkGraphicsPipelineCreateInfo` 구조체를 만들 수 있습니다. `vkDestroyShaderModule` 보다는 전이어야 하는데 이것들이 생성 과정에서 사용되기 때문입니다. + +```c++ +VkGraphicsPipelineCreateInfo pipelineInfo{}; +pipelineInfo.sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO; +pipelineInfo.stageCount = 2; +pipelineInfo.pStages = shaderStages; +``` + +`VkPipelineShaderStageCreateInfo`구조체의 배열을 참조하는 것으로 시작합니다. + +```c++ +pipelineInfo.pVertexInputState = &vertexInputInfo; +pipelineInfo.pInputAssemblyState = &inputAssembly; +pipelineInfo.pViewportState = &viewportState; +pipelineInfo.pRasterizationState = &rasterizer; +pipelineInfo.pMultisampleState = &multisampling; +pipelineInfo.pDepthStencilState = nullptr; // Optional +pipelineInfo.pColorBlendState = &colorBlending; +pipelineInfo.pDynamicState = &dynamicState; +``` + +그리고 고정함수 단계를 기술하는 구조체들을 참조합니다. + +```c++ +pipelineInfo.layout = pipelineLayout; +``` + +다음으로 파이프라인 레이아웃이 오는데, 여기에는 구조체에 대한 포인터가 아닌 Vulkan 핸들을 사용합니다. + +```c++ +pipelineInfo.renderPass = renderPass; +pipelineInfo.subpass = 0; +``` + +마지막으로 그래픽스 파이프라인이 사용할 렌더 패스에 대한 참조와 서브패스 인덱스가 있습니다. 이 특정 인스턴스가 아닌 다른 렌더 패스를 사용할 수도 있지만 그러한 경우 그것들이 `renderPass`와 *호환되어야 합니다*. 호환성에 대해서는 [여기](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap8.html#renderpass-compatibility)에 설명되어 있지만 그러한 기능은 이 튜토리얼에서는 사용하지 않을 겁니다. + +```c++ +pipelineInfo.basePipelineHandle = VK_NULL_HANDLE; // Optional +pipelineInfo.basePipelineIndex = -1; // Optional +``` + +두 개의 매개변수가 사실 더 있습니다. `basePipelineHandle`와 +`basePipelineIndex` 입니다. Vulkan에서는 기존 파이프라인으로부터 새로운 그래픽스 파이프라인을 만들 수도 있습니다. 이러한 파이프라인 유도(derivative)는 대부분의 기능이 비슷한 파이프라인을 설정하는 데 성능적인 이점이 있고, 같은 부모로부터 유도된 파이프라인으로 교체하는 것은 더 빠르게 수행될 수 있습니다. 기존 파이프라인의 핸들을 `basePipelineHandle`에 명시하거나 곧 생성할 파리프라인의 인덱스를 `basePipelineIndex`를 사용해 참조할 수 있습니다. 지금은 하나의 파이프라인만 있으므로 널 핸들과 유효하지 않은 인덱스로 설정해 둡니다. 이러한 기능은 `VkGraphicsPipelineCreateInfo`의 `flag` 필드에 `VK_PIPELINE_CREATE_DERIVATIVE_BIT`가 명시되어 있어야만 사용할 수 있습니다. + +마지막으로 `VkPipeline` 객체를 저장할 클래스 멤버를 준비해 둡시다: + +```c++ +VkPipeline graphicsPipeline; +``` + +그리고 최종적으로 그래픽스 파이프라인을 만듭니다: + +```c++ +if (vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &graphicsPipeline) != VK_SUCCESS) { + throw std::runtime_error("failed to create graphics pipeline!"); +} +``` + +`vkCreateGraphicsPipelines`함수는 Vulkan의 다른 객체들을 만들 떄보다 더 많은 매개변수를 받습니다. 한 번의 호출로 여러 개의 `VkGraphicsPipelineCreateInfo`를 받아 여러 개의 `VkPipeline`객체를 만들 수 있게 되어있습니다. + +`VK_NULL_HANDLE`를 넘겨준 두 번째 매개변수는 선택적으로 `VkPipelineCache`객체에 대한 참조를 넘겨줄 수 있습니다. 파이프라인 캐시(cache)는 여러 `vkCreateGraphicsPipelines` 호출을 위해, 파이프라인 생성을 위한 데이터를 저장하고 재사용하는데 사용될 수 있습니다. 만일 캐시가 파일로 저장되어 있다면 다른 프로그램에서도 사용될 수 있습니다. 이렇게 하면 나중에 파이프라인 생성을 위해 소요되는 시간을 눈에 띄게 줄일 수 있습니다. 이에 대해서는 파이프라인 캐시 챕터에서 살펴보겠습니다. + +모든 그리기 연산 과정에서는 그래픽스 파이프라인이 필요하므로 프로그램이 종료될 때에만 해제되어야 합니다. + +```c++ +void cleanup() { + vkDestroyPipeline(device, graphicsPipeline, nullptr); + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + ... +} +``` + +이제 프로그램을 실행하고 작업에 대한 보상으로 성공적으로 파이프라인이 만들어졌는지 확인하세요! 이제 무언가 화면에 나오기까지 얼마 남지 않았습니다. 다음 몇 개 챕터에서는 스왑 체인으로부터 실제 프레임버퍼를 설정하고 그리기 명령을 준비해 보겠습니다. + +[C++ code](/code/12_graphics_pipeline_complete.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/kr/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md b/kr/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md new file mode 100644 index 00000000..c8b44e16 --- /dev/null +++ b/kr/03_Drawing_a_triangle/03_Drawing/00_Framebuffers.md @@ -0,0 +1,97 @@ +지난 몇 개의 챕터에서 프레임버퍼와 관련한 많은 것들을 이야기 했고 스왑 체인 이미지와 같은 포맷인, 단일 프레임버퍼를 사용할 렌더 패스를 설정했습니다. 하지만 아직 실제 생성은 하지 않았습니다. + +렌더 패스 생성 과정에서 명시한 어태치먼트는 `VkFramebuffer` 객체로 래핑하여 바인딩됩니다. 프레임버퍼 객체는 어태치먼트를 표현하는 모든 `VkImageView` 객체를 참조합니다. 우리의 경우 어태치먼트는 색상 어태치먼트 하나입니다. 하지만 어태치먼트로 사용해야 하는 이미지는 우리가 화면에 표시하기 위한 이미지를 요청했을 때 스왑 체인이 어떠한 이미지를 반환하냐에 달려 있습니다. 즉, 우리는 스왑 체인에 있는 모든 이미지에 대해 프레임버퍼를 만들어야 하고, 그리기 시점에는 그 중 하나를 선택해서 사용해야 합니다. + +이를 위해 프레임버퍼를 저장할 `std::vector` 클래스 멤버를 하나 더 만듭니다: + + +```c++ +std::vector swapChainFramebuffers; +``` + +이 배열을 위한 객체는 `initVulkan`에서 호출할 새로운 `createFramebuffers`함수에서 만들 것입니다. 그래픽스 파이프라인 생성 이후에 호출합니다: + + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); +} + +... + +void createFramebuffers() { + +} +``` + + +모든 프레임버퍼를 저장할 수 있도록 컨테이너 크기부터 조정합니다: + + +```c++ +void createFramebuffers() { + swapChainFramebuffers.resize(swapChainImageViews.size()); +} +``` + + +그리고 이미지 뷰를 순회하면서 이를 기반으로 프레임버퍼를 만듭니다: + + +```c++ +for (size_t i = 0; i < swapChainImageViews.size(); i++) { + VkImageView attachments[] = { + swapChainImageViews[i] + }; + + VkFramebufferCreateInfo framebufferInfo{}; + framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; + framebufferInfo.renderPass = renderPass; + framebufferInfo.attachmentCount = 1; + framebufferInfo.pAttachments = attachments; + framebufferInfo.width = swapChainExtent.width; + framebufferInfo.height = swapChainExtent.height; + framebufferInfo.layers = 1; + + if (vkCreateFramebuffer(device, &framebufferInfo, nullptr, &swapChainFramebuffers[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create framebuffer!"); + } +} +``` + + +보이시는 것처럼 프레임버퍼의 생성은 매우 직관적입니다. 먼저 어떤 `renderPass`에 프레임버퍼가 호환되어야 하는지 명시합니다. 호환되는 경우에만 렌더 패스에 프레임버퍼를 사용할 수 있는데, 숫자와 타입이 같아야 합니다. + +`attachmentCount`와 `pAttachments` 매개변수는 렌더 패스의 `pAttachment` 배열에 해당하는 어태치먼트 기술자와 바인딩될 `VkImageView` 객체를 명시합니다. + +`width` 와 `height` 매개변수는 설명하지 않아도 될 것 같고, `layers`는 이미지 배열의 레이어 수를 의미합니다. 우리 스왑 체인 이미지는 하나이므로, 레이어 수도 `1`입니다. + +프레임버퍼는 렌더링이 모두 완료된 후에, 이를 사용하는 이미지 뷰와 렌더 패스보다 먼저 해제되어야 합니다: + + +```c++ +void cleanup() { + for (auto framebuffer : swapChainFramebuffers) { + vkDestroyFramebuffer(device, framebuffer, nullptr); + } + + ... +} +``` + + +이제 렌더링을 위해 필요한 모든 객체를 만들었습니다. 이제 다음 챕터에서는 실제 그리기 명령을 작성해 보도록 하겠습니다. + + +[C++ code](/code/13_framebuffers.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/kr/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md b/kr/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md new file mode 100644 index 00000000..26a2585a --- /dev/null +++ b/kr/03_Drawing_a_triangle/03_Drawing/01_Command_buffers.md @@ -0,0 +1,266 @@ +그리기 명령이나 메모리 전송과 같은 Vulkan의 명령(command)은 함수 호출을 통해 직접 수행되는 것이 아닙니다. 수행하고자 하는 연산들을 모두 명령 버퍼 객체에 먼저 기록해야 합니다. 이로 인해 Vulkan에게 우리가 하고자 하는 것들을 알려줄 준비가 완료되었다면, 모든 명령이 한꺼번에 Vulkan으로 제출(submit)되어 동시에 실행 가능한 상태가 됩니다. 또한 원한다면 여러 쓰레드에서 명령을 기록할 수 있다는 장점도 있습니다. + +## 명령 풀(Command pools) + +명령 버퍼를 만드려면 먼저 명령 풀부터 만들어야 합니다. 명령 풀은 명령 버퍼로 할당될 버퍼의 메모리를 관리합니다. `VkCommandPool`을 저장할 새 클래스 멤버를 추가합니다: + +```c++ +VkCommandPool commandPool; +``` + +그리고 `initVulkan`의 프레임버퍼 생성 이후에 호출할 `createCommandPool` 함수를 새로 만듭니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); +} + +... + +void createCommandPool() { + +} +``` + +명렬 풀 생성에는 두 개의 매개변수만 필요합니다: + +```c++ +QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice); + +VkCommandPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; +poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT; +poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value(); +``` + +명령 풀에는 두가지 가능한 플래그가 존재합니다: + +* `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT`: 명령 버퍼가 새로운 명령을 자주 기록할 것을 알려주는 힌트 (이에 따라 메모리 할당 방식이 바뀔 수 있음) +* `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT`: 명령 버퍼가 독립적으로 재기록(rerecord)될 수 있음. 이 플래그가 없으면 모두 함께 리셋(reset)되어야 함 + +우리는 명령 버퍼를 매 프레임 기록할 것이기 때문에 리셋하고 재기록 하게 하려고 합니다. 따라서 커맨드 풀은 `VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT` 플래그 비트로 설정합니다. + +명령 버퍼는 이를 장치 큐 중 하나에 제출함으로써 실행됩니다. 장치 큐는 예를들어 우리가 획득한 그래픽스 또는 표시 큐와 같은 것입니다. 각 명령 풀은 한 종류의 큐에 제출할 명령 버퍼만 할당 가능합니다. 우리는 그리기를 위한 명령을 기록할 것이라서 그래픽스 큐 패밀리를 선택한 것입니다. + +```c++ +if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) { + throw std::runtime_error("failed to create command pool!"); +} +``` + +마지막으로 `vkCreateCommandPool` 함수를 호출해 명령 풀을 만듭니다. 특별한 매개변수는 없습니다. 명령은 화면에 무언가를 그리기 위해 프로그램 내내 사용할 것이니, 마지막에 가서야 해제하게 됩니다: + +```c++ +void cleanup() { + vkDestroyCommandPool(device, commandPool, nullptr); + + ... +} +``` + +## 명령 버퍼 할당 + +이제 명령 버퍼 할당을 시작해 봅시다. + +`VkCommandBuffer` 객체를 클래스 멤버로 추가합니다. 명령 버퍼는 명령 풀이 소멸되면 자동으로 해제되므로 따로 정리 과정은 필요 없습니다. + +```c++ +VkCommandBuffer commandBuffer; +``` + +이제 명령 풀에서 하나의 명령 버퍼를 만들기 위해 `createCommandBuffer` 함수를 만들어 봅시다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createCommandBuffer(); +} + +... + +void createCommandBuffer() { + +} +``` + +명령 버퍼는 `vkAllocateCommandBuffers` 함수를 사용해 할당되는데, 명령 풀을 명시하는 `VkCommandBufferAllocateInfo` 매개변수와 할당할 버퍼 개수를 매개변수로 받습니다: + +```c++ +VkCommandBufferAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; +allocInfo.commandPool = commandPool; +allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; +allocInfo.commandBufferCount = 1; + +if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate command buffers!"); +} +``` + +`level` 매개변수는 할당된 명령 버퍼가 주(primary) 명령 버퍼인지, 보조(secondary) 명령 버퍼인지를 명시합니다. + +* `VK_COMMAND_BUFFER_LEVEL_PRIMARY`: 실행을 위해 큐에 제출될 수 있지만, 다른 명령 버퍼에서 호출은 불가능. +* `VK_COMMAND_BUFFER_LEVEL_SECONDARY`: 직접 제출은 불가능하지만 주 명령 버퍼로부터 호출될 수 있음. + +보조 명령 버퍼의 기능은 여기에서 사용하진 않을 것이지만, 주 명령 버퍼에서 자주 사용되는 연산을 재사용하기 위해 유용하게 사용될 수 있다는 것은 눈치 채실 수 있을 겁니다. + +우리는 하나의 명령 버퍼만을 할당하므로 `commandBufferCount`는 1입니다. + +## 명령 버퍼 기록 + +이제 실행하고자 하는 명령을 명령 버퍼에 채우는 `recordCommandBuffer` 함수를 만들어 봅시다. `VkCommandBuffer`와 값을 쓰고자 하는 현재 스왑체인 이미지의 인덱스를 매개변수로 넘겨줍니다. + +```c++ +void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) { + +} +``` + +명령 버퍼의 기록은 항상 `vkBeginCommandBuffer`에 간단한 `VkCommandBufferBeginInfo` 구조체를 넘겨주는 것으로 시작합니다. 이 구조체는 해당 명령 버퍼의 사용 방식에 대한 세부 사항을 명시합니다. + +```c++ +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +beginInfo.flags = 0; // Optional +beginInfo.pInheritanceInfo = nullptr; // Optional + +if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { + throw std::runtime_error("failed to begin recording command buffer!"); +} +``` + +`flags` 매개변수는 명령 버퍼를 어떻게 사용할 것인지를 명시합니다. 아래와 같은 값들이 될 수 있습니다: + +* `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`: 명령 버퍼는 한 번 실행 후 즉시 재기록됨. +* `VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT`: 이 버퍼는 보조 명령 버퍼로써 하나의 렌더링 패스에 완전하게 속해 있음. +* `VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT`: 명령이 지연될 경우에 명령 버퍼가 재제출(resubmit) 될 수 있음 + +연재는 어떤 플래그도 해당하지 않습니다. + +`pInheritanceInfo` 매개변수는 보조 명령 버퍼에만 해당됩니다. 주 명령 버퍼에서 호출될 떄 어떤 상태를 상속(inherit)하는지 명시합니다. + +명령 버퍼가 이미 기록된 상태에서 `vkBeginCommandBuffer`를 호출하면 암시적으로 버퍼가 리셋됩니다. 명령을 버퍼에 나중에 추가(append)하는 것은 불가능합니다. + +## 렌더 패스 시작하기 + +그리기는 `vkCmdBeginRenderPass`를 사용해 렌더 패스를 시작함으로써 시작됩니다. 렌더 패스는 `VkRenderPassBeginInfo` 구조체의 매개변수를 기반으로 설정됩니다. + +```c++ +VkRenderPassBeginInfo renderPassInfo{}; +renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; +renderPassInfo.renderPass = renderPass; +renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex]; +``` + +첫 매개변수는 렌더패스 그 자체, 그리고 바인딩할 어태치먼트입니다. 각 스왑 체인 이미지에 대해 프레임버퍼를 만들었고, 색상 어태치먼트로 명시된 상태입니다. 따라서 그 프레임버퍼를 우리가 그리고자 하는 스왑체인 이미지로 바인딩해야 합니다. 전달된 imageIndex를 사용해 현재 스왑체인 이미지 중 적절한 프레임버퍼를 선택할 수 있습니다. + +```c++ +renderPassInfo.renderArea.offset = {0, 0}; +renderPassInfo.renderArea.extent = swapChainExtent; +``` + +다음 두 매개변수는 렌더 영역(area)의 크기를 명시합니다. 렌더 영역은 셰이더가 값을 읽고 쓰는 영역을 정의합니다. 이 영역 밖의 픽셀은 정의되지 않은 값을 가지게 됩니다. 어태치먼트와 같은 크기여야 성능이 높아집니다. + +```c++ +VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; +renderPassInfo.clearValueCount = 1; +renderPassInfo.pClearValues = &clearColor; +``` + +마지막 두 매개변수는 `VK_ATTACHMENT_LOAD_OP_CLEAR`에 사용될 지우기(clear) 값이고, 색상 어태치먼트의 로드 연산에 사용한 바 있습니다. 여기서는 간단히 검은색의 100% 불투명도로 지우도록 하겠습니다. + +```c++ +vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); +``` + +이제 렌더 패스가 시작됩니다. 명령을 기록하는 함수는 `vkCmd` 접두어로 구분할 수 있습니다. 이들은 모두 `void` 반환이므로 기록을 끝낼 때 까지는 오류 처리가 불가능합니다. + +모든 명령의 첫 매개변수는 명령을 기록할 명령 버퍼입니다. 두 번째 매개변수는 방금 만든, 렌더 패스 세부사항을 명시합니다. 마지막 매개변수는 렌더 패스 안의 그리기 명령이 어떻게 제공될지를 제어합니다. 두 개의 값 중 하나입니다: + +* `VK_SUBPASS_CONTENTS_INLINE`: 렌더 패스 명령이 주 명령 버퍼에 포함되어 있고 보조 명령 버퍼는 실행되지 않음 +* `VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS`: 렌더 패스 명령이 보조 명령 버퍼에서 실행됨 + +보조 명령 버퍼는 사용하지 않을 것이므로, 첫 번째 값을 선택합니다. + +## 기본 그리기 명령 + +이제 그래픽스 파이프라인을 바인딩합니다: + +```c++ +vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); +``` + +두 번째 매개변수는 파이프라인 객체가 그래픽스 파이프라인인지 계산(compute) 파이프라인인지를 명시합니다. 이제 Vulkan에게 그래픽스 파이프라인에서 어떤 명령을 실행하고 프래그먼트 셰이더에서 어떤 어태치먼트를 사용할 것인지를 알려 주었습니다. + +[고정 함수 챕터](../02_Graphics_pipeline_basics/02_Fixed_functions.md#dynamic-state)에서 이야기 한 것처럼, 우리는 파이프라인에게 뷰포트와 시저 상태가 동적일 것이라고 명시해 둔 상태입니다. 따라서 이들을 명령 버퍼에서 그리기 명령을 수행하기 이전에 설정해 주어야 합니다: + +```c++ +VkViewport viewport{}; +viewport.x = 0.0f; +viewport.y = 0.0f; +viewport.width = static_cast(swapChainExtent.width); +viewport.height = static_cast(swapChainExtent.height); +viewport.minDepth = 0.0f; +viewport.maxDepth = 1.0f; +vkCmdSetViewport(commandBuffer, 0, 1, &viewport); + +VkRect2D scissor{}; +scissor.offset = {0, 0}; +scissor.extent = swapChainExtent; +vkCmdSetScissor(commandBuffer, 0, 1, &scissor); +``` + +이제 삼각형을 그리기 위한 그리기 명령을 추가합니다: + +```c++ +vkCmdDraw(commandBuffer, 3, 1, 0, 0); +``` + +실제 `vkCmdDraw` 명령은 아주 어렵지 않은데 미리 모든 정보를 설정해 두었기 때문입니다. 이 명령은 명령 버퍼 이외에 다음과 같은 매개변수를 갖습니다: + +* `vertexCount`: 현재 정점 버퍼는 없을지라도, 드로우(draw)를 위해서는 3개의 정점이 필요합니다. +* `instanceCount`: 인스턴스(instanced) 렌더링을 위해 사용되는데, 그 기능을 사용하지 않는경우 `1`로 설정합니다. +* `firstVertex`: 정점 버퍼의 오프셋을 설정하는 데 사용되며, `gl_VertexIndex`의 가장 작은 값을 정의합니다. +* `firstInstance`: 인스턴스 렌더링의 오프셋을 설정하는 데 사용되며, `gl_InstanceIndex`의 가장 작은 값을 정의합니다. + +## 마무리 + +이제 렌더 패스를 끝냅니다: + +```c++ +vkCmdEndRenderPass(commandBuffer); +``` + +그리고 명령 버퍼의 기록도 끝냅니다: + +```c++ +if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to record command buffer!"); +} +``` + +다음 장에서는 메인 루프를 위한 코드를 작성할 것이고, 그 과정에서 스왑 체인 이미지를 얻고, 명령 버퍼를 기록하고 실행하며, 결과 이미지를 스왑 체인에 반환할 것입니다. + +[C++ code](/code/14_command_buffers.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/kr/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md b/kr/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md new file mode 100644 index 00000000..0b065f10 --- /dev/null +++ b/kr/03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md @@ -0,0 +1,405 @@ + +이제 모든 것이 결합되는 챕터입니다. 메인 루프에서 실행되어 삼각형을 화면에 표시하는 `drawFrame`함수를 작성할 것입니다. 이 함수를 만들고 `mainLoop`에서 호출하도록 합시다: + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } +} + +... + +void drawFrame() { + +} +``` + +## 프레임 개요 + +큰 그림에서 보자면, Vulkan에서 프레임을 하나 렌더링하는 것은 다음 단계들로 이루어집니다: + +* 이전 프레임이 끝나기까지 대기 +* 스왑 체인에서 이미지 얻어오기 +* 그 이미지에 장면을 드로우하기 위한 명령 버퍼 기록 +* 기록된 명령 버퍼 제출 +* 스왑 체인 이미지 표시 + +챕터를 진행하면서 드로우 함수를 확장할 것이지만, 지금은 이 정도가 렌더링 루프의 핵심이라 보시면 됩니다. + + + +## 동기화(Synchronization) + + + +(*역주: 여기서 '동기화'는 동시에 실행한다는 의미가 아닌 올바른 실행 스케줄(순서)를 유지한다는 의미*) Vulkan의 핵심 설계 철학은 GPU에서의 실행 동기화가 명시적이라는 겁니다. 연산의 순서는 우리가 다양한 동기화 요소들을 가지고 정의하는 것이고, 이를 통해 드라이버는 원하는 실행 순서를 파악합니다. 이 말은 많은 Vulkan API 호출이 GPU에서 실제 동작하는 시기는 비동기적이고, 실제 연산이 끝나기 전에 함수가 종료된다는 뜻입니다. + +이 챕터에서는 여러 이벤트들에 대해 순서를 명시적으로 지정해야 할 필요가 있습니다. 이러한 이벤트들이 GPU에서 일어나기 때문인데 그 예로: + +* 스왑 체인에서 이미지 얻어오기 +* 그 이미지를 드로우하기 위한 명령 실행 +* 표시를 위해 이미지를 화면에 나타내고 스왑체임으로 다시 반환 + +이러한 이벤트들은 단일 함수 호출로 동작하지만 실제 실행은 비동기적으로 이루어집니다. +실제 연산이 끝나기 전에 함수가 반환되고 실행 순서도 정의되어 있지 않습니다. 각 연산이 이전 연산에 종속적이기 때문에 이렇게 되면 안됩니다. 따라서 원하는 순서대로 실행이 될 수 있도록 하게 해 주는 요소들을 살펴볼 것입니다. + +### 세마포어(Semaphores) + +큐 연산들 사이에 순서를 추가하기 위해 세마포어를 사용할 수 있습니다. 여기서 큐 연산은 우리가 큐에 제출한 작업들이며 명령 버퍼 내의 연산이거나 나중에 볼 함수의 연산들입니다. +큐의 예시로는 그래픽스 큐와 표시 큐가 있습니다. 세마포어는 동일 큐 내에서, 그리고 서로 다른 큐 사이에서 순서를 정하기 위해 사용됩니다. + +Vulkan에는 바이너리와 타임라인(timeline) 세마포어가 있습니다. 이 튜토리얼에서는 바이너리 세마포어만 사용할 것이고 타임라인 세마포어에 대해서는 논의하지 않을 것입니다. 이후에 세마포어라고 한다면 바이너리 세마포어를 의미하는 것입니다. + +세마포어는 시그널 상태(signaled)이거나 시그널이 아닌 상태(unsignaled)일 수 있습니다. 일단 시그널이 아닌 상태로 시작됩니다. 우리가 세마포어를 사용하는 방식은 우선 동일한 세마포어를 한 쪽 큐 연산에는 '시그널(signal)' 세마포어로, 다른 쪽에는 '대기(wait)' 세마포어로 사용하는 것입니다. 예를 들어 세마포어 S가 있고 큐 연산 A와 B가 있다고 합시다. Vulkan에게 우리가 알려주는 것은 실행이 끝나면 연산 A 세마포어 S를 '시그널'하도록 하고, 연산 B는 실행 전에 세마포어 S를 '대기'하도록 하는 것입니다. 연산 A가 끝나면 세마포어 S가 시그널 상태가 될 것인데, 연산 B는 S가 시그널 상태가 될때까지는 실행되지 않습니다. 연산 B가 실행이 시작되면 세마포어 S는 자동적으로 시그널이 아닌 상태로 돌아오고 다시 사용 가능한 상태가 됩니다. + +방금 설명한 내용을 의사 코드로 표현하자면 다음과 같습니다: +``` +VkCommandBuffer A, B = ... // 명령 버퍼 기록 +VkSemaphore S = ... // 세마포어 생성 + +// A를 큐에 등록하고 끝나면 S를 '시그널'하도록 함 - 즉시 실행 시작 +vkQueueSubmit(work: A, signal: S, wait: None) + +// B를 큐에 등록하고 시작하기 위해 S를 기다림 +vkQueueSubmit(work: B, signal: None, wait: S) +``` + +주의하셔야 할것은 위 코드에서 두 번의 `vkQueueSubmit()` 호출은 즉시 반환된다는 것입니다. 대기 과정은 GPU에서만 일어납니다. CPU는 블러킹(blocking)없이 계속 실행합니다. CPU가 대기하도록 하려면 다른 동기화 요소가 필요합니다. + +### 펜스(Fences) + +펜스도 비슷한 목적을 가지고 있습니다. 동일하게 동기화 실행을 위해 사용되며 CPU(호스트라고도 함)에서의 순차적 실행이 목적이라는 것만 다릅니다. 간단히 말해 호스트가 GPU가 어떤 작업을 끝냈다는 것을 알아야만 하는 상황에서 펜스를 사용합니다. + +세마포어와 유사하게 펜스도 시그널 상태와 시그널이 아닌 상태를 가집니다. 작업 실행을 제출할 때 해당 작업에 펜스를 부착할 수 있습니다. 작업이 끝나면 펜스는 시그널 상태가 됩니다. 그리고 호스트는 펜스가 시그널 상태가 될때까지 기다리게 하면 호스트가 작업이 끝난 뒤에야만 진행되도록 보장할 수 있습니다. + +구체적인 예시로 스크린샷을 찍는 예시가 있습니다. 예를들어 GPU에서 필요한 작업을 이미 수행했다고 합시다. 이제 이미지를 GPU에서부터 호스트로 전송하고 메모리를 파일로 저장해야 합니다. 전송을 위한 명령 버퍼 A와 펜스 F가 있다고 합시다. 명령 버퍼 A를 펜스 F와 함께 제출하고 호스트에게 F가 시그널 상태가 될때까지 기다리게 합니다. 이렇게 하면 호스트는 명령 버퍼가 실행을 끝낼 때까지 블러킹 상태가 됩니다. 따라서 안전하게 메모리 전송이 끝난 뒤 디스크에 파일을 저장할 수 있습니다. + +방금 설명한 내용을 의사 코드로 표현하자면 다음과 같습니다: +``` +VkCommandBuffer A = ... // 전송을 위한 명령 버퍼 기록 +VkFence F = ... // 펜스 생성 + +// A를 큐에 등록하고 즉시 실행을 시작하며, 끝나면 F를 시그널 상태로 만듬 +vkQueueSubmit(work: A, fence: F) + +vkWaitForFence(F) // A의 실행이 끝날때 까지 실행 중단(블럭) + +save_screenshot_to_disk() // 전송이 끝날 때까지 싱행이 불가능 +``` + +세마포어 예시와는 달리 이 예시는 호스트의 실행을 *블럭*합니다. 즉 모든 연산이 끝날때까지 호스트는 아무것도 하지 않는다는 뜻입니다. 이 경우에는 스크린샷을 디스크에 저장하기 전까지 전송이 끝나야만 한다는 것을 보장해야 하기 때문입니다. + +일반적으로 꼭 필요한 경우가 아니라면 호스트를 블럭하지 않는것이 좋습니다. GPU에 전달하고 난 뒤 호스트는 다른 유용한 작업을 하는 것이 좋습니다. 펜스가 시그널 상태가 되는 것을 기다리는 것은 유용한 작업이 아니죠. 따라서 작업의 동기화를 위해서는 세마포어를 사용하거나, 아직 다루지 않은 다른 동기화 방법을 사용해야 합니다. + +펜스의 경우 시그널이 아닌 상태로 되돌리는 작업은 매뉴얼하게 수행해 주어야 합니다. 펜스는 호스트의 실행을 제어하기 위해 사용되는 것이고 호스트가 펜스를 되돌리는 시점을 결정하게 됩니다. 이와는 달리 세마포어는 호스트와 상관없이 GPU 내의 작업 순서를 결정하기 위해 사용됩니다. + +정리하자면, 세마포어는 GPU에서의 실행 순서를 명시하기 위해 사용하고 펜스는 CPU와 GPU간의 동기화를 위해 사용한다는 것입니다. + +### 어떻게 결정해야 하나요? + +사용할 수 있는 두 종류의 동기화 요소가 있고 마침 동기화가 필요한 두 과정이 존재합니다. +스왑체인 연산과 이전 프레임이 끝나기를 기다리는 과정입니다. 스왑체인 연산에 대해서는 세마포어를 사용할 것인데 이는 GPU에서 수행되는 작업이고, 호스트가 그 동안 기다리는 것을 원치 않기 때문입니다. 이전 프레임이 끝나기를 기다리는 것은 반대의 이유로 펜스를 사용할 것인데 호스트가 기다려야 하기 때문입니다. 그리고 기다려야 하는 이유는 한번에 하나 이상의 프레임이 그려지는 것을 원치 않기 때문입니다. 매 프레임마다 명령 버퍼를 다시 기록하기 때문에 다음 프레임을 위한 작업을 현재 프레임의 실행이 끝나기 전에 기록할 수 없습니다. 만일 그렇게 하게 되면 GPU가 명령을 실행하는 동안 명령 버퍼가 덮어쓰여지게 됩니다. + +## 동기화 객체의 생성 + +스왑체인으로부터 이미지가 획득되어 렌더링할 준비가 되었는지에 대한 세마포어 하나와 렌더링이 끝나서 표시할 준비가 되었다는 세마포어 하나가 필요합니다. 또한 한 번에 하나의 프레임만 렌더링하기 위한 펜스 하나가 필요합니다. + +이 세마포어와 펜스 객체를 저장하기 위한 클래스 멤버를 만듭니다: + +```c++ +VkSemaphore imageAvailableSemaphore; +VkSemaphore renderFinishedSemaphore; +VkFence inFlightFence; +``` + +세마포어를 만들기 위해 이 장의 마지막 `create`함수인 `createSyncObjects`를 추가합니다: + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createCommandBuffer(); + createSyncObjects(); +} + +... + +void createSyncObjects() { + +} +``` + +세마포어 생성을 위해서는 `VkSemaphoreCreateInfo`를 채워야 하는데 현재 API 버전에서는 `sType` 이외의 다른 필드는 요구하지 않습니다: + +```c++ +void createSyncObjects() { + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; +} +``` + +나중 버전의 Vulkan API나 확장에서는 다른 구조체처럼 `flags`와 `pNext` 기능이 추가될 수 있습니다. + +펜스를 만들기 위해서는 `VkFenceCreateInfo`를 채워야 합니다: + +```c++ +VkFenceCreateInfo fenceInfo{}; +fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; +``` + +세마포어와 펜스를 만드는 것은 `vkCreateSemaphore` & `vkCreateFence`를 사용하는 기존과 비슷한 패턴입니다: + +```c++ +if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphore) != VK_SUCCESS || + vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphore) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &inFlightFence) != VK_SUCCESS) { + throw std::runtime_error("failed to create semaphores!"); +} +``` + +프로그램의 종료 시점에 세마포어와 펜스는 정리되어야 하며 이는 모든 명령이 끝나고 더이상 동기화가 필요하지 않은 시점입니다: + +```c++ +void cleanup() { + vkDestroySemaphore(device, imageAvailableSemaphore, nullptr); + vkDestroySemaphore(device, renderFinishedSemaphore, nullptr); + vkDestroyFence(device, inFlightFence, nullptr); +``` + +메인 그리기 함수로 가 봅시다! + +## 이전 프레임 대기 + +프레임 시작 시점에 이전 프레임이 끝나기를 기다려야 하므로 명령 버퍼와 세마포어(*역주: 펜스일 듯*)가 사용 가능해야 합니다. 이를 위해 `vkWaitForFences`를 호출합니다. + +```c++ +void drawFrame() { + vkWaitForFences(device, 1, &inFlightFence, VK_TRUE, UINT64_MAX); +} +``` + +`vkWaitForFences` 함수는 펜스 배열을 받아서, 몇 개 또는 전체 펜스들이 시그널인 상태를 반환할 때까지 호스트를 대기하도록 합니다. 인자로 넘긴 `VK_TRUE`는 모든 펜스를 기다리겠다는 의미인데 지금은 하나의 펜스만 있으므로 크게 의미는 없습니다. 이 함수는 또한 타임아웃(timeout) 매개변수를 갖는데 `UINT64_MAX`를 통해 64비트 부호없는 정수의 최대값으로 설정했습니다. 즉 타임아웃을 사용하지 않겠다는 의미입니다. + +대기 후에 `vkResetFences` 호출을 통해 펜스를 시그널이 아닌 상태로 리셋해 주어야 합니다: + +```c++ + vkResetFences(device, 1, &inFlightFence); +``` + +더 진행하기 전에 현재 설계에 약간의 문제가 있습니다. 첫 프레임에 `drawFrame()`를 호출하기 때문에 바로 `inFlightFence`가 시그널 상태가 되도록 기다립니다. `inFlightFence`는 프레임 렌더링이 끝나야 시그널 상태가 되는데 지금은 첫 번째 프레임이므로 펜스를 시그널 상태로 만들어줄 이전 프레임이 없습니다! 따라서 `vkWaitForFences()`가 프로세스를 무한정 블럭해서 아무 일도 일어나지 않을 것입니다. + +이 문제를 해결하는 많은 방법 중 API에 들어있는 똑똑한 해결책이 하나 있습니다. 펜스를 시그널인 상태로 생성해서 `vkWaitForFences()`의 첫 호출이 바로 반환되도록 하는 것입니다. + +이렇게 하기 위해서 `VK_FENCE_CREATE_SIGNALED_BIT` 플래그를 `VkFenceCreateInfo`에 추가합니다: + +```c++ +void createSyncObjects() { + ... + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + ... +} +``` + +## 스왑 체인에서 이미지 얻어오기 + +다음으로 `drawFrame` 함수에서 할 일은 스왑 체인으로부터 이미지를 얻어오는 것입니다. 스왑 체인은 확장 기능이므로 `vk*KHR` 네이밍으로 되어 있는 함수를 사용해야 하는 것을 잊지 마세요. + +```c++ +void drawFrame() { + ... + + uint32_t imageIndex; + vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); +} +``` + +`vkAcquireNextImageKHR`의 첫 두 매개변수는 논리적 장치와 이미지를 얻어오려고 하는 스왑 체인입니다. 세 번째 매개변수는 이미지가 가용할때까지의 나노초 단위 타임아웃 시간입니다. 64비트의 부호없는 정주의 최대값을 사용했고, 그 의미는 타임아웃을 적용하지 않겠다는 의미입니다. + +다음 두 매개변수는 표시 엔진이 이미지 사용이 끝나면 시그널 상태로 변환될 동기화 객체들을 명시합니다. 그 시점이 바로 우리가 새로운 프레임을 드로우 할 시점입니다. +세마포어나 펜스, 또는 그 둘 다를 명시할 수 있습니다. 여기서는 `imageAvailableSemaphore`를 사용할 것입니다. + +마지막 매개변수는 사용이 가능해진 스왑 체인 이미지의 인덱스를 출력할 변수입니다. 이 인덱스는 `swapChainImages` 배열의 `VkImage`의 인덱스입니다. 이 인덱스를 사용해 `VkFrameBuffer`를 선택할 것입니다. + +## 명령 버퍼 기록 + +사용할 스왑 체인 이미지의 imageIndex를 얻었으면 이제 명령 버퍼를 기록할 수 있습니다. 먼저, `vkResetCommandBuffer`를 호출해 명령 버퍼가 기록이 가능한 상태가 되도록 합니다. + +```c++ +vkResetCommandBuffer(commandBuffer, 0); +``` + +`vkResetCommandBuffer`의 두 번째 매개변수는 `VkCommandBufferResetFlagBits` 플래그입니다. 특별히 무언가 작업을 하지는 않을 것이므로 0으로 두겠습니다. + +이제 `recordCommandBuffer`를 호출하여 원하는 명령을 기록합니다. + +```c++ +recordCommandBuffer(commandBuffer, imageIndex); +``` + +기록이 완료된 명령 버퍼가 있으니 이제 제출할 수 있습니다. + +## 명령 버퍼 제출 + +큐 제출과 동기화는 `VkSubmitInfo` 구조체의 매개변수들로 설정합니다. + +```c++ +VkSubmitInfo submitInfo{}; +submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + +VkSemaphore waitSemaphores[] = {imageAvailableSemaphore}; +VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT}; +submitInfo.waitSemaphoreCount = 1; +submitInfo.pWaitSemaphores = waitSemaphores; +submitInfo.pWaitDstStageMask = waitStages; +``` + +첫 세 개의 매개변수는 실행이 시작되기 전 대기할 세마포어, 그리고 파이프라인의 어떤 스테이지(stage)에서 대기할지를 명시합니다. 우리의 경우 색상 값을 이미지에 기록하는 동안 대기할 것이므로 색상 어태치먼트에 쓰기를 수행하는 스테이지를 명시하였습니다. 즉 이론적으로는 우리의 구현이 정점 셰이더 등등을 이미지가 가용하지 않은 상태에서 실행될 수 있다는 뜻입니다. `waitStages` 배열의 각 요소가 `pWaitSemaphores`의 동일한 인덱스와 대응됩니다. + +```c++ +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandBuffer; +``` + +다음 두 매개변수는 실제로 실행을 위해 어떤 명령 버퍼를 제출할 것인지를 명시합니다. +만들어둔 유일한 명령 버퍼를 제출합니다. + +```c++ +VkSemaphore signalSemaphores[] = {renderFinishedSemaphore}; +submitInfo.signalSemaphoreCount = 1; +submitInfo.pSignalSemaphores = signalSemaphores; +``` + +`signalSemaphoreCount`와 `pSignalSemaphores` 매개변수는 명령 버퍼 실행이 끝나면 시그널 상태가 될 세마포어를 명시합니다. 우리의 경우 `renderFinishedSemaphore`를 사용합니다. + +```c++ +if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence) != VK_SUCCESS) { + throw std::runtime_error("failed to submit draw command buffer!"); +} +``` + +이제 `vkQueueSubmit`를 사용해 명령 버퍼를 그래픽스 큐에 제출합니다. 이 함수는 `VkSubmitInfo` 구조체 배열을 받을 수 있는데 작업량이 많을 때는 이 방식이 효율적입니다. 마지막 매개변수는 펜스로 명령 버퍼 실행이 끝나면 시그널 상태가 됩니다. 이렇게 하면 언제 안전하게 명령 버퍼를 다시 사용할 수 있는 상태가 되는지를 알 수 있으므로 `inFlightFence`를 사용합니다. 이제 다음 프레임이 되면, CPU는 이번 명령 버퍼의 실행이 끝날때까지 대기하다가 새로룽 명령들을 기록하게 됩니다. + +## 서브패스 종속성(dependencies) + +렌더패스의 서브패스는 이미지 레이아웃의 전환을 자동적으로 처리해 준다는 사실을 기억하십시오. 이러한 전환은 *서브패스 종속성*에 의해 제어되는데, 서브패스간의 메모리와 실행 종속성을 명시합니다. 지금은 하나의 서브패스만 있는 상태지만 이 서브패스의 바로 이전과 이후의 연산 또한 암시적으로 "서브패스"로 간주됩니다. + +렌더패스의 시작 시점과 끝 시점에 전환을 처리해주는 내장된 종속성이 있긴 합니다만, 시작 시점의 경우 올바른 시점에 제어되지 않습니다. 이는 전환이 파이프라인의 시작 시점에 일어난다고 가정하고 설계되었는데 우리의 경우 파이프라인 시작 시점에 이미지를 획득하지는 않은 상태입니다. 이러한 문제를 해결하기 위한 방법이 두 가지가 있습니다. `imageAvailableSemaphore`의 to `waitStages`를 `VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT`으로 바꾸어 이미지가 가용할 때까지 렌더패스를 시작하지 않는 방법이 있고, 렌더패스가 `VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT` 스테이지를 대기하도록 하는 방법이 있습니다. 저는 여기서 두 번째 방법을 선택했는데, 서브패스 종속성과 그 동작 방식을 살펴보는 데 좋기 때문입니다. + +서브패스 종속성은 `VkSubpassDependency` 구조체에 명시됩니다. `createRenderPass` 함수에 라애 코드를 추가합니다: + +```c++ +VkSubpassDependency dependency{}; +dependency.srcSubpass = VK_SUBPASS_EXTERNAL; +dependency.dstSubpass = 0; +``` + +첫 두 필드는 의존(dependency)하는 서브패스와 종속(dependent)되는 서브패스를 명시합니다.(*역주: 의존=선행되어야 하는 서브패스, 종속=후행해야 하는 서브패스*) 특수한 값인 `VK_SUBPASS_EXTERNAL`은 `srcSubpass` 또는 `dstSubpass` 중 어디에 설정되었느냐에 따라 서브패스의 앞과 뒤에 오는 암시적인 서브패스를 명시하는 값입니다. `0` 인덱스는 우리의 첫 번째(그리고 유일한) 서브패스를 의미하는 인덱스입니다. 종속성 그래프의 사이클을 방지하기 위해 `dstSubpass`는 항상 `srcSubpass`보다 커야 합니다(둘 중 하나가 `VK_SUBPASS_EXTERNAL`가 아닌 경우에 해당). + +```c++ +dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; +dependency.srcAccessMask = 0; +``` + +다음 두 개의 필드는 대기할 연산과 그 연산이 일어날 스테이지를 명시합니다. 접근하기 전에, 스왑 체인이 이미지를 읽기를 마칠 때까지 기다려야 합니다. 따라서 색상 어태치먼트 출력 자체를 기다리도록 하면 됩니다. + +```c++ +dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; +dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; +``` + +이 서브패스를 기다려야 하는 연산은 컬러 어태치먼트 스테이지에 있고 컬러 어태치먼트에 값을 쓰는 연산과 관련되어 있습니다. 이렇게 설정하면 실제로 필요하고 허용되기 전까지는 이미지의 전환이 발생하지 않습니다.: when we want to start writing colors to it. + +```c++ +renderPassInfo.dependencyCount = 1; +renderPassInfo.pDependencies = &dependency; +``` + +`VkRenderPassCreateInfo` 구조체는 종속성의 배열을 명시하기 위한 두 개의 필드를 가지고 있습니다. + +## 표시 + +프레임을 그리는 마지막 단계는 결과를 스왑체인에 다시 제출해서 화면에 표시되도록 하는 단계입니다. 표시는 `drawFrame` 함수 마지막에 `VkPresentInfoKHR` 구조체를 통해 설정됩니다. + +```c++ +VkPresentInfoKHR presentInfo{}; +presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR; + +presentInfo.waitSemaphoreCount = 1; +presentInfo.pWaitSemaphores = signalSemaphores; +``` + +첫 두 매개변수는 `VkSubmitInfo`처럼, 표시를 수행하기 전 어떤 세마포어를 기다릴지를 명시합니다. 우리는 명령 버퍼 실행이 끝나서 삼각형이 그려질 때까지 대기해야 하므로 그 때 시그널 상태가 되는 `signalSemaphores`를 사용합니다. + +```c++ +VkSwapchainKHR swapChains[] = {swapChain}; +presentInfo.swapchainCount = 1; +presentInfo.pSwapchains = swapChains; +presentInfo.pImageIndices = &imageIndex; +``` + +다음 두 매개변수는 이미지를 표시할 스왑 체인과 각 스왑 체인의 이미지 인덱스를 명시합니다. 이는 거의 항상 한 개만 사용합니다. + +```c++ +presentInfo.pResults = nullptr; // Optional +``` + +마지막으로 추가적인 매개변수로 `pResults`가 있습니다. 여기에는 `VkResult`의 배열을 명시해서 각각의 스왑 체인에서 표시가 성공적으로 이루어졌는지를 체크합니다. 하나의 스왑 체인만 사용하는 경우 그냥 표시 함수의 반환값으로 확인하면 되기 때문에 현재는 사용하지 않습니다. + +```c++ +vkQueuePresentKHR(presentQueue, &presentInfo); +``` + +`vkQueuePresentKHR` 함수는 이미지를 표시하라는 요청을 스왑 체인에 제출합니다. 다음 챕터에서 `vkAcquireNextImageKHR`와 `vkQueuePresentKHR`에 대한 오류 처리를 추가할 것입니다. 왜냐하면 지금까지와는 다르게 이 떄의 오류는 프로그램을 종료할 정도의 오류는 아닐 수 있기 때문입니다. + +여기까지 모든 단계를 제대로 수행했다면 이제 프로그램을 실행하면 아래와 비슷한 장면을 보시게 될겁니다: + +![](/images/triangle.png) + +>이 삼각형은 여러분들이 그래픽스 관련한 튜토리얼에서 보던 삼각형과 다를 수 있습니다. 왜냐하면 이 튜토리얼에서는 셰이더가 선형 색상 공간(linear color space)에서 보간을 수행한 뒤 sRGB 색상 공간으로 변환을 수행하기 때문입니다. 이러한 차이점에 대해서는 [이 블로그](https://medium.com/@heypete/hello-triangle-meet-swift-and-wide-color-6f9e246616d9)를 참고하세요. + +짝짝짝! 하지만 안타깝게도 검증 레이어가 활성화 된 상태라면, 프로그램이 종료될 때 오류가 발생하는 것을 보실 수 있습니다. `debugCallback`에 의해 출력되는 메시지가 그 이유를 알려줍니다: + +![](/images/semaphore_in_use.png) + +`drawFrame`의 모든 연산이 비동기적이라는 것을 기억하십시오. 즉 `mainLoop`에서 루프를 종료했을 때도 그리기와 표시 연산이 계속 진행되고 있다는 뜻입니다. 연산이 진행되는 도중에 리소스를 정리하는 것은 좋지 않습니다. + +이 문제를 해결하기 위해 `mainLoop`를 끝내고 윈도우를 소멸하기 이전에 논리적 장치가 연산을 끝내기를 기다려야 합니다. + +```c++ +void mainLoop() { + while (!glfwWindowShouldClose(window)) { + glfwPollEvents(); + drawFrame(); + } + + vkDeviceWaitIdle(device); +} +``` + +`vkQueueWaitIdle`를 통해 특정 명령 큐의 연산이 끝나기를 기다리도록 할 수도 있습니다. +이 함수는 동기화를 위한 아주 기초적인 방법으로 사용될 수도 있습니다. 이제 윈도우를 닫아도 문제 없이 프로그램이 종료되는 것을 보실 수 있습니다. + +## 결론 + +900줄이 좀 넘는 코드로 드디어 화면에 뭔가를 표시할 수 있었습니다. Vulkan 프로그램을 부트스트래핑(bootstrapping)하는 것은 많은 노력이 필요하지만, 명시성으로 인해 우리에게 엄청난 양의 제어권을 제공한다는 것을 알 수 있었습니다. 제가 권장하는 것은 이제 시간을 갖고 코드를 다시 읽어보면서 모든 Vulkan 객체들의 목적과 그들이 각각 어떻게 관련되어 있는지에 대한 개념을 복습해 보시라는 것입니다. 이러한 지식을 가진 상태에서 이제 프로그램의 기능을 확장해 나가 볼 것입니다. + +다음 챕터에서는 렌더링 루프를 확장하여 여러 프레임을 사용하도록 할 것입니다. + +[C++ code](/code/15_hello_triangle.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/kr/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md b/kr/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md new file mode 100644 index 00000000..74ebb07c --- /dev/null +++ b/kr/03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md @@ -0,0 +1,147 @@ +## 여러 프레임의 사용(frames in-flight) + +지금 우리 렌더링 루프에 눈에 띄는 문제가 하나 있습니다. 다음 프레임을 렌더링 하기 전에 이전 프레임을 기다려야만 하고 이로 인해 호스트는 불필요한 아이들링(ideling) 시간을 갖게 됩니다. + + + +이를 수정하는 방법은 여러 프레임을 동시에 사용하는 것입니다. 즉, 하나의 프레임에 렌더링을 수행하는 것과 다음 프레임의 기록 과정을 서로 간섭이 없도록 할 것입니다. 어떻게 해야 할까요? 일단 렌더링에 필요한 모든 접근과 수정이 필요한 자원들이 모두 복제되어야만 합니다. 즉, 여러 개의 명령 버퍼, 세마포어와 펜스들이 있어야 합니다. 나중 챕터에서는 다른 리소스들에 대한 다중 인스턴스를 추가할 것이고, 그 챕터에서 이러한 개념을 다시 보게될 것입니다. + +프로그램 상단에 얼마나 많은 프레임을 동시에 처리할 것인지 정의하는 상수를 먼저 추가합니다: + +```c++ +const int MAX_FRAMES_IN_FLIGHT = 2; +``` + +우리는 CPU가 GPU보다 *너무* 앞서나가는 것을 원하지는 않으므로 2로 설정하였습니다. 두 개의 프레임을 동시에 사용하면 CPU와 GPU는 각자의 작업을 동시에 수행할 수 있습니다. CPU가 먼저 끝나면, 작업을 더 제출할기 전에 GPU가 렌더링을 끝내길 기다릴 것입니다. 세 개 이상의 프레임을 동시에 사용하면 CPU가 GPU보다 앞서나가 지연되는 프레임이 발생할 수 있습니다. 일반적으로, 이러한 지연은 좋지 않습니다. 하지만 이렇게 사용되는 프레임의 개수를 조정하는 것 또한 Vulkan의 명시성의 한 예가 될 것입니다. + +각 프레임은 각자의 명령 버퍼, 세마포어 집합과 펜스를 가져야 합니다. 이들을 `std::vector`들로 바꾸고 이름도 변경합니다. + +```c++ +std::vector commandBuffers; + +... + +std::vector imageAvailableSemaphores; +std::vector renderFinishedSemaphores; +std::vector inFlightFences; +``` + +이제 여러 개의 명령 버퍼를 생성해야 합니다. `createCommandBuffer`를 `createCommandBuffers`로 이름을 변경하고 명령 버퍼 벡터의 크기를 `MAX_FRAMES_IN_FLIGHT`로 수정합니다. `VkCommandBufferAllocateInfo`를 수정하여 명령 버퍼의 숫자를 받고록 하고 새로운 명령 버퍼 벡터의 위치를 넘겨줍니다: + +```c++ +void createCommandBuffers() { + commandBuffers.resize(MAX_FRAMES_IN_FLIGHT); + ... + allocInfo.commandBufferCount = (uint32_t) commandBuffers.size(); + + if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate command buffers!"); + } +} +``` + +`createSyncObjects` 함수는 모든 객체를 생성하도록 수정되어야 합니다: + +```c++ +void createSyncObjects() { + imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + inFlightFences.resize(MAX_FRAMES_IN_FLIGHT); + + VkSemaphoreCreateInfo semaphoreInfo{}; + semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + + VkFenceCreateInfo fenceInfo{}; + fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; + fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS || + vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) { + + throw std::runtime_error("failed to create synchronization objects for a frame!"); + } + } +} +``` + +모두 정리되어야 하는 것도 마찬가지입니다: + +```c++ +void cleanup() { + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr); + vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr); + vkDestroyFence(device, inFlightFences[i], nullptr); + } + + ... +} +``` + +명령 풀을 해제하면 명령 버퍼가 자동으로 해제되기 때문에 명령 버퍼 정리에는 별도의 작업이 필요 없다는 사실을 기억하세요. + +각 프레임에서 올바른 객체를 사용하기 위해서는 현재 프레임이 무엇인지를 알고 있어야 합니다. 이를 위해 프레임 인덱스를 사용할 것입니다: + + +```c++ +uint32_t currentFrame = 0; +``` + +`drawFrame` 함수에서는 적절한 객체를 사용하도록 수정합니다: + +```c++ +void drawFrame() { + vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + vkResetFences(device, 1, &inFlightFences[currentFrame]); + + vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); + + ... + + vkResetCommandBuffer(commandBuffers[currentFrame], 0); + recordCommandBuffer(commandBuffers[currentFrame], imageIndex); + + ... + + submitInfo.pCommandBuffers = &commandBuffers[currentFrame]; + + ... + + VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]}; + + ... + + VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]}; + + ... + + if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { +} +``` + +당연히 다음 프레임에는 프레임을 증가시켜주어야 합니다: + +```c++ +void drawFrame() { + ... + + currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +} +``` + +모듈로 연산(%)을 사용해 프레임 인덱스가 `MAX_FRAMES_IN_FLIGHT`개의 프레임 뒤에는 다시 0이 되도록 합니다. + + + +이제 동기화화 관련한 모든 구현을 마쳐서 `MAX_FRAMES_IN_FLIGHT`개 이상의 프레임 작업이 동시에 큐에 들어가지 않도록 하였으며 이러한 프레임들이 서로 겹치지도 않게 되었습니다. 정리 부분과 같은 나머지 부분은 `vkDeviceWaitIdle` 처럼 보다 기초적인 동기화 방법에 의존하고 있습니다. 어떠한 접근법을 사용할지는 성능 요구사항에 따라서 여러분이 선택하셔야 합니다. + +동기화에 대해 예를 통해 더 알고 싶으시면 Khronos에서 제공하는 [이 개요 문서](https://github.com/KhronosGroup/Vulkan-Docs/wiki/Synchronization-Examples#swapchain-image-acquire-and-present)를 살펴보세요. + +다음 챕터에서는 잘 동작하는 Vulkan 프로그램에 필요한 또다른 요구사항을 살펴보겠습니다. + +[C++ code](/code/16_frames_in_flight.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/kr/03_Drawing_a_triangle/04_Swap_chain_recreation.md b/kr/03_Drawing_a_triangle/04_Swap_chain_recreation.md new file mode 100644 index 00000000..4f14ddd6 --- /dev/null +++ b/kr/03_Drawing_a_triangle/04_Swap_chain_recreation.md @@ -0,0 +1,236 @@ +## 서론 + +지금까지 만든 프로그램으로 성공적으로 삼각형을 그렸지만 아직 잘 처리하지 못하는 상황이 있습니다. 윈도우 표면이 변경되어 스왑 체인이 더이상 호환되지 않을 때 입니다. 이러한 상황이 발생하는 이유 중 하나로 윈도우의 크기가 변하는 경우가 있습니다. 이러한 이벤트를 탐지하여 스왑 체인을 새로 만들어야만 합니다. + +## 스왑 체인 재생성 + +`recreateSwapChain` 함수를 새로 만드는데 이 함수는 `createSwapChain` 함수 및 스왑 체인, 그리고 윈도우 크기와 관련한 모든 객체를 만드는 함수를 호출하도록 할 것입니다. + +```c++ +void recreateSwapChain() { + vkDeviceWaitIdle(device); + + createSwapChain(); + createImageViews(); + createFramebuffers(); +} +``` + +먼저 `vkDeviceWaitIdle`를 호출하는데 이전 장에서처럼 이미 사용 중인 자원을 건드리면 안되기 때문입니다. 그리고 당연히 스왑 체인은 새로 만들어야 하고요. 이미지 뷰는 스왑 체인의 이미지와 직접적으로 관련되어 있기 때문에 다시 만들어야 합니다. 하지막으로 프레임버퍼도 스왑 체인 이미지와 직접적으로 관련되어 있으니 역시나 마찬가지로 다시 만들어 주어야 합니다. + +이러한 객체들의 이전 버전은 모두 재생성 되기 전에 정리되어야 하는데, 이를 확실히 하기 위해 정리 코드의 몇 부분을 변도의 함수로 만들어 `recreateSwapChain` 함수에서 호출 가능하도록 할 것입니다. 이 함수는 `cleanupSwapChain`로 명명합시다: + +```c++ +void cleanupSwapChain() { + +} + +void recreateSwapChain() { + vkDeviceWaitIdle(device); + + cleanupSwapChain(); + + createSwapChain(); + createImageViews(); + createFramebuffers(); +} +``` + +여기서는 간략화 해서 렌더패스는 재생성하지 않았습니다. 이론적으로는 응용 프로그램의 실행 동안 스왑 체인 이미지의 포맷도 바뀔 수 있습니다. 예를 들어 윈도우를 일반적인 모니터에서 HDR 모니터로 이동한다거나 하는 등을 생각해 볼 수 있습니다. 이러한 경우 응용 프로램에서 HDR로의 변경이 적절히 적용되도록 렌더패스 재생성도 필요할 수 있습니다. + +새로 만들어진 객체들의 정리 코드는 `cleanup`에서 `cleanupSwapChain`로 옮깁니다: + +```c++ +void cleanupSwapChain() { + for (auto framebuffer : swapChainFramebuffers) { + vkDestroyFramebuffer(device, framebuffer, nullptr); + } + + for (auto imageView : swapChainImageViews) { + vkDestroyImageView(device, imageView, nullptr); + } + + vkDestroySwapchainKHR(device, swapChain, nullptr); +} + +void cleanup() { + cleanupSwapChain(); + + vkDestroyPipeline(device, graphicsPipeline, nullptr); + vkDestroyPipelineLayout(device, pipelineLayout, nullptr); + + vkDestroyRenderPass(device, renderPass, nullptr); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr); + vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr); + vkDestroyFence(device, inFlightFences[i], nullptr); + } + + vkDestroyCommandPool(device, commandPool, nullptr); + + vkDestroyDevice(device, nullptr); + + if (enableValidationLayers) { + DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr); + } + + vkDestroySurfaceKHR(instance, surface, nullptr); + vkDestroyInstance(instance, nullptr); + + glfwDestroyWindow(window); + + glfwTerminate(); +} +``` + +`chooseSwapExtent`에서 이미 새로운 윈도우의 해상도를 질의해서 스왑 체인 이미지가 (새로운) 윈도우에 적합한 크기가 되도록 했다는 것에 주목하십시오. 따라서 `chooseSwapExtent`를 수정할 필요는 없습니다(`glfwGetFramebufferSize`를 사용해서 스왑 체인 생성 시점에 픽셀 단위의 표면 해상도를 얻어왔다는 것을 기억하세요). + +이로써 스왑 체인을 재생성하는 부분은 끝입니다! 하지만 이러한 접근법의 단점은 새로운 스왑 체인이 생성될 때까지 모든 렌더링이 중단된다는 것입니다. 이전 스왑 체인이 사용되는 동안에 그리기가 수행되는 동안에 대 스왑 체인을 만드는 것도 가능합니다. 그러려면 `VkSwapchainCreateInfoKHR` 구조체의 `oldSwapChain` 필드에 이전 스왑 체인을 전달하고 사용이 끝난 뒤 소멸시키면 됩니다. + +## 최적화되지 않았거나 부적합한 스왑 체인 + +이제 언제 스왑 체인 재생성이 필요한지 알아내서 `recreateSwapChain` 함수를 호출하면 됩니다. 다행히 Vulkan은 대개 표시 단계에서 현재 스왑 체인이 적합하지 않게 된 시점에 이러한 것을 알려 줍니다. `vkAcquireNextImageKHR`와 `vkQueuePresentKHR` 함수는 아래와 같은 특정한 값으로 이러한 상황을 알려줍니다. + +* `VK_ERROR_OUT_OF_DATE_KHR`: 스왑 체인이 표면과 호환이 불가능하여 렌더링이 불가능하게 되었음. 일반적으로 윈도우의 크기가 변했을 때 발생 +* `VK_SUBOPTIMAL_KHR`: 스왑 체인이 표면을 표현하는 데 여전히 사용 가능하지만 표면 속성이 정확히 일치하지는 않음 + +```c++ +VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); + +if (result == VK_ERROR_OUT_OF_DATE_KHR) { + recreateSwapChain(); + return; +} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + throw std::runtime_error("failed to acquire swap chain image!"); +} +``` + +이미지를 획득하려 할 때 스왑체인이 부적합하다고 판단되면 그 이미지는 표현에 활용할 수 없습니다. 따라서 즉시 스왑 체인을 재생성하고 다음 `drawFrame`을 다시 호출해야 합니다. + +스왑 체인이 최적화되지 않은 경우에도 이렇게 하도록 할 수도 있지만 저는 이 경우에는 어쨌든 이미지를 이미 획득했기 때문에 그냥 진행하기로 했습니다. `VK_SUCCESS`와 `VK_SUBOPTIMAL_KHR`는 모두 "성공" 반환 코드로 취급합니다. + +```c++ +result = vkQueuePresentKHR(presentQueue, &presentInfo); + +if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { + recreateSwapChain(); +} else if (result != VK_SUCCESS) { + throw std::runtime_error("failed to present swap chain image!"); +} + +currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT; +``` + +`vkQueuePresentKHR` 함수는 위와 같은 의미를 가진 같은 값들을 반환합니다. 이 경우에는 최적화되지 않은 경우에도 스왑 체인을 재생성하는데 가능한 좋은 결과를 얻고 싶기 때문입니다. + +## 데드락(deadlock) 해소 + +지금 시점에서 코드를 실행하면 데드락이 발생할 수 있습니다. 코드를 디버깅해보면 `vkWaitForFences`에는 도달하지만 여기에서 더 이상 진행하지 못하는 것을 볼 수 있습니다. 이는 `vkAcquireNextImageKHR`이 `VK_ERROR_OUT_OF_DATE_KHR`을 반환하면 스왑체인을 재생성하고 `drawFrame`로 돌아가게 했기 때문입니다. 하지만 그러한 처리는 현재 프레임의 펜스가 기다리는 상태에서 일어날 수 있습니다. 바로 반환되는 바람에 아무런 작업도 제출되지 않았고 펜스는 시그널 상태가 될 수 없어서 `vkWaitForFences`에서 멈춘 상태가 됩니다. + +다행히 손쉬운 해결법이 있습니다. 작업을 다시 제출할 것이 확실한 시점까지 펜스를 리셋하는 것을 미루는 것입니다. 이렇게 되면 빠른 반환이 일어났을 때 펜스는 여전히 시그널 상태이고 `vkWaitForFences`는 다음 프레임에서 데드락이 발생하지 않을 것입니다. + +`drawFrame`의 시작 부분으 다음과 같이 되어야 합니다 +: +```c++ +vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + +uint32_t imageIndex; +VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); + +if (result == VK_ERROR_OUT_OF_DATE_KHR) { + recreateSwapChain(); + return; +} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) { + throw std::runtime_error("failed to acquire swap chain image!"); +} + +// 작업을 제출하는 경우에만 펜스를 리셋 +vkResetFences(device, 1, &inFlightFences[currentFrame]); +``` + +## 크기 변환의 명시적 처리 + +윈도우 크기 변환에 대해 많은 드라이버와 플랫폼이 `VK_ERROR_OUT_OF_DATE_KHR`를 자동으로 반환해주지만, 이러한 동작이 보장된 것은 아닙니다. 따라서 추가적인 코드를 통해 크기 변환을 명시적으로 처리해 주도록 하겠습니다. 먼저 크기 변환이 일어났을 때를 위한 플래그를 멤버 변수로 추가합니다: + +```c++ +std::vector inFlightFences; + +bool framebufferResized = false; +``` + +`drawFrame`함수에서도 이 플래그를 체크하도록 수정합니다: + +```c++ +if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) { + framebufferResized = false; + recreateSwapChain(); +} else if (result != VK_SUCCESS) { + ... +} +``` + +이러한 작업을 `vkQueuePresentKHR` 뒤에 진행해서 세마포어가 적합상 상태에 있도록 하는 것이 중요합니다. 그렇지 않으면 시그널 상태인 세마포어가 제대로 대기를 하지 못할 수 있습니다. 이제 실제 크기 변경을 탐지하기 위해 GLFW 프레임워크의 `glfwSetFramebufferSizeCallback` 함수를 사용하여 콜백을 설정합니다: + +```c++ +void initWindow() { + glfwInit(); + + glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API); + + window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); + glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); +} + +static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { + +} +``` + +콜백을 `static` 함수로 만드는 이유는 GLFW가 `HelloTriangleApplication` 인스턴스를 가리키는 `this` 포인터로부터 올바른 멤버 함수를 호출하는 법을 알 수 없기 때문입니다. + +하지만 콜백 내에서 `GLFWwindow`에 대한 참조에 접근할 수 있고, 임의의 포인터를 그 안에 저장할 수 있는 `glfwSetWindowUserPointer`라는 GLFW 함수가 있습니다: + +```c++ +window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr); +glfwSetWindowUserPointer(window, this); +glfwSetFramebufferSizeCallback(window, framebufferResizeCallback); +``` + +이 값은 이제 콜백 내에서 `glfwGetWindowUserPointer`를 사용해 적절히 변환된 뒤 올바른 플래그 설정을 위해 사용됩니다: + +```c++ +static void framebufferResizeCallback(GLFWwindow* window, int width, int height) { + auto app = reinterpret_cast(glfwGetWindowUserPointer(window)); + app->framebufferResized = true; +} +``` + +이제 프로그램을 실행하고 윈도우 크기를 조정하여 프레임버퍼가 윈도우 크기에 맞게 조정되는지 살펴 보세요. + +## 최소화 처리 + +스왑 체인이 부적합하게 되는 다른 또다른 경우로 특수한 윈도우 크기 변경 사례가 있습니다. 바로 윈도우 최소화 입니다. 이 경우가 특수한 이유는 프레임버퍼 크기가 `0`이 되기 떄문입니다. 이 튜토리얼에서는 이러한 경우에 대해 윈도우가 다시 활성화가 될때까지 정지하는 방식으로 처리할 것입니다. `recreateSwapChain` 함수를 사용합니다: + +```c++ +void recreateSwapChain() { + int width = 0, height = 0; + glfwGetFramebufferSize(window, &width, &height); + while (width == 0 || height == 0) { + glfwGetFramebufferSize(window, &width, &height); + glfwWaitEvents(); + } + + vkDeviceWaitIdle(device); + + ... +} +``` + +처음의 `glfwGetFramebufferSize` 호출은 올바른 크기일 경우에 대한 것으로 이 경우 `glfwWaitEvents`는 기다릴 것이 없습니다. + +축하합니다! 이제 올바로 동작하는 것 Vulkan 프로그램을 완성했습니다! 다음 챕터에서는 정점 셰이더에 하드코딩된 정점을 제거하고 정점 버퍼(vertex buffer)를 사용해 볼 것입니다. + +[C++ code](/code/17_swap_chain_recreation.cpp) / +[Vertex shader](/code/09_shader_base.vert) / +[Fragment shader](/code/09_shader_base.frag) diff --git a/kr/04_Vertex_buffers/00_Vertex_input_description.md b/kr/04_Vertex_buffers/00_Vertex_input_description.md new file mode 100644 index 00000000..c24311e4 --- /dev/null +++ b/kr/04_Vertex_buffers/00_Vertex_input_description.md @@ -0,0 +1,167 @@ +## 서론 + +다음 몇 챕터동안 정점 셰이더에 하드코딩된 정점 데이터를 메모리의 정점 버퍼(vertex buffer)로 바꾸어 보겠습니다. 먼저 가장 손쉬운 방법인 CPU에서 보이는(visible) 버퍼를 만든 뒤 `memcpy`를 통해 정점 데이터를 직접 복사하는 방법을 알아볼 것이고, 이후에 스테이징 버퍼(staging buffer)를 사용해 정점 데이터를 고성능 메모리에 복사하는 방법을 알아볼 것입니다. + +## 정점 셰이더 + +먼저 정점 셰이더가 정점 데이터를 코드로 포함하지 않도록 수정할 것입니다. 정점 셰이더는 `in` 키워드로 정점 버퍼에서 입력을 받을 것입니다. + +```glsl +#version 450 + +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec3 inColor; + +layout(location = 0) out vec3 fragColor; + +void main() { + gl_Position = vec4(inPosition, 0.0, 1.0); + fragColor = inColor; +} +``` + +`inPosition`와 `inColor` 변수는 *정점 어트리뷰트(vertex attribute)*입니다. 이는 정점 버퍼에 명시된 정점별 속성이며, 기존처럼 위치와 속성 데이터 입니다. 정점 셰이더를 수정한 뒤 다시 컴파일하는 것을 잊지 마세요! + +`fragColor`처럼, `layout(location = x)`는 입력에 대해 나중에 참조하기 위한 인덱스를 할당하는 것입니다. 예를들어 `dvec3`와 같은 64비트 벡터는 여러 *슬롯(slot)*을 사용한다는 사실을 중요하게 알아두셔야 합니다. 이러한 경우 그 다음으로 오는 인덱스는 2 이상 큰 인덱스여야 합니다: + +```glsl +layout(location = 0) in dvec3 inPosition; +layout(location = 2) in vec3 inColor; +``` + +레이아웃 한정자(qualifier)에 대해서는 [OpenGL wiki](https://www.khronos.org/opengl/wiki/Layout_Qualifier_(GLSL))에서 자세한 정보를 찾아 볼 수 있습니다. + +## 정점 데이터 + +정점 데이터를 셰이더 코드에서 우리 프로그램의 배열로 옮길 예정입니다. 먼저 벡터와 행렬 같은 선형대수 관련 자료형을 제공해 주는 GLM 라이브러를 include 하는 것 부터 시작합니다. 이 자료형들을 사용해 위치와 색상 벡터를 명시할 것입니다. + +```c++ +#include +``` + +우리가 정점 셰이더에서 사용할 두 어트리뷰트를 포함하는 `Vertex` 구조체를 만듭니다: + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; +}; +``` + +GLM은 셰이더 언어에서 사용되는 벡터 자료형과 정확히 매치되는 C++ 자료형을 제공해 줍니다: + +```c++ +const std::vector vertices = { + {{0.0f, -0.5f}, {1.0f, 0.0f, 0.0f}}, + {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, + {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} +}; +``` + +이제 `Vertex` 구조체를 사용해 정점 데이터를 명시합니다. 이전과 완전히 동일한 위치와 색상값을 사용하지만 이제는 정점에 대한 배열 하나에 모두 포함해 두었습니다. 이러한 방식을 정점 어트리뷰트의 *interleving*이라고 합니다. + +## 바인딩 기술자(Binding descriptions) + +다음 단계는 GPU 메모리에 업로드된 데이터를 정점 셰이더로 어떻게 전달할지를 Vulkan에 알려주는 것입니다. 이러한 정보를 전달하기 위한 두 종류의 구조체가 필요합니다. + +첫 구조체는 `VkVertexInputBindingDescription`이고 `Vertex` 구조체에 멤버 함수를 추가하여 적절한 데이터를 생성할 수 있도록 합니다. + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; + + static VkVertexInputBindingDescription getBindingDescription() { + VkVertexInputBindingDescription bindingDescription{}; + + return bindingDescription; + } +}; +``` + +정점 바인딩은 정점에 대해 얼만큼의 데이터를 메모리로부터 로드할 것인지를 명시합니다. 각 데이터별 바이트의 크기, 그리고 각 정점에 대해 다음 데이터로 넘어갈지, 아니면 다음 인스턴스에서 널어갈지를 포함합니다. + +```c++ +VkVertexInputBindingDescription bindingDescription{}; +bindingDescription.binding = 0; +bindingDescription.stride = sizeof(Vertex); +bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; +``` + +우리의 정점별 데이터는 하나의 배열에 포장되어(packed) 있으니 바인딩은 하나만 있으면 됩니다. `binding` 매개변수는 바인딩 배열의 바인딩할 인덱스를 명시합니다. `stride` 매개변수는 한 요소와 다음 요소 사이의 바이트 크기입니다. `inputRate` 매개변수는 아래와 같은 값 중 하나를 가집니다: + +* `VK_VERTEX_INPUT_RATE_VERTEX`: 각 정점에 대해 다음 데이터 요소로 이동함 +* `VK_VERTEX_INPUT_RATE_INSTANCE`: 각 인스턴스에 대해 다음 데이터 요소로 넘어감 + +인스턴스 렌더링을 한 것은 아니므로 정점별 데이터로 해 두겠습니다. + +## 어트리뷰트 기술자 + +정점 입력을 처리하는 방법을 설명하기 위한 두 번째 구조체는 `VkVertexInputAttributeDescription`입니다. 이 구조체를 채우기 위해 또 다른 헬퍼 함수를 `Vertex`에 추가하겠습니다. + +```c++ +#include + +... + +static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + return attributeDescriptions; +} +``` + +함수 프로토타입에서 알 수 있듯, 두 개의 구조체를 사용할 것입니다. 어트리뷰트 기술자를 위한 구조체는 바인딩 기술자를 활용해 얻어진 정점 데이터 덩어리로부터 정점 어트리뷰트를 어떻게 추출할지를 알려줍니다. 우리는 위치와 색상 두 개의 어트리뷰트가 있으니 두 개의 어트리뷰트 기술자 구조체가 필요합니다. + +```c++ +attributeDescriptions[0].binding = 0; +attributeDescriptions[0].location = 0; +attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; +attributeDescriptions[0].offset = offsetof(Vertex, pos); +``` + +`binding` 매개변수는 어떤 바인딩에서 정점별 데이터를 얻어올 것인지를 Vulkan에 알려줍니다. `location` 매개변수는 정점 셰이더의 `location` 지시자에 대한 참조입니다.정점 셰이더의 location `0`에 대한 입력이 위치값에 해당하고, 이는 두 개의 32비트 부동소수점으로 이루어져 있습니다. + +`format` 매개변수는 어트리뷰트의 데이터 자료형을 알려줍니다. 약간 헷갈리는 점은 이러한 포맷이 색상 포맷과 동일한 열거자로 명시된다는 점입니다. 아래와 같은 셰이더 자료형에 따르는 포맷이 사용됩니다: + +* `float`: `VK_FORMAT_R32_SFLOAT` +* `vec2`: `VK_FORMAT_R32G32_SFLOAT` +* `vec3`: `VK_FORMAT_R32G32B32_SFLOAT` +* `vec4`: `VK_FORMAT_R32G32B32A32_SFLOAT` + +보다시피 색상 채널의 수와 일치하는 요소 숫자를 갖는 셰이더 자료형의 포맷을 사용해야 합니다. 셰이더의 요소 숫자보다 더 많은 채널을 사용하는 것도 허용되지만 남는 값은 무시됩니다. 요소 숫자보다 채널 수가 적으면 BGA 요소의 기본값인 `(0, 0, 1)`가 사용됩니다. 색상 타입인 (`SFLOAT`, `UINT`, `SINT`)와 비트 너비 또한 셰이더 입력의 자료형과 일치해야 합니다. 예시는 다음과 같습니다: + +* `ivec2`: `VK_FORMAT_R32G32_SINT`, 32비트 부호 있는 정수 2개 요소를 갖는 벡터 +* `uvec4`: `VK_FORMAT_R32G32B32A32_UINT`, 32비트 부호 없는 정수 4개의 요소를 갖는 벡터 +* `double`: `VK_FORMAT_R64_SFLOAT`, 64비트 double 부동소수점 + +`format` 매개변수는 어트리뷰트 데이터의 바이트 크기를 암시적으로 정의하며 `offset` 매개변수는 정점별 데이터를 읽어올 시작 바이트를 명시합니다. 바인딩은 한 번에 하나의 `Vertex`를 읽어오며 위치 어트리뷰트(`pos`)는 `0` 바이트, 즉 처음부터 읽어옵니다. `offsetof` 매크로를 사용하면 자동으로 계산됩니다. + +```c++ +attributeDescriptions[1].binding = 0; +attributeDescriptions[1].location = 1; +attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; +attributeDescriptions[1].offset = offsetof(Vertex, color); +``` + +색상 어트리뷰트도 동일한 방식으로 기술됩니다. + +## 파이프라인 정점 입력 + +이제 `createGraphicsPipeline` 안의 구조체를 참조하여 정점 데이터를 위와 같은 포맷으로 받아들이도록 그래픽스 파이프라인을 설정해야 합니다. `vertexInputInfo` 구조체를 찾아 두 기술자를 참조하도록 수정합니다: + +```c++ +auto bindingDescription = Vertex::getBindingDescription(); +auto attributeDescriptions = Vertex::getAttributeDescriptions(); + +vertexInputInfo.vertexBindingDescriptionCount = 1; +vertexInputInfo.vertexAttributeDescriptionCount = static_cast(attributeDescriptions.size()); +vertexInputInfo.pVertexBindingDescriptions = &bindingDescription; +vertexInputInfo.pVertexAttributeDescriptions = attributeDescriptions.data(); +``` + +이제 이 파이프라인은 `vertices` 컨테이너의 정점 데이터를 받아들이고 정점 셰이더로 넘길 준비가 되었습니다. 검증 레이어를 활성화 한 상태에서 프로그램을 실행하면 바인딩된 정점 버퍼가 없다는 오류 메시지를 보시게 될겁니다. 다음 단계는 정점 버퍼를 만들고 정점 데이터를 버퍼에 넘겨 GPU가 접근할 수 있도록 하는 것입니다. + +[C++ code](/code/18_vertex_input.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/kr/04_Vertex_buffers/01_Vertex_buffer_creation.md b/kr/04_Vertex_buffers/01_Vertex_buffer_creation.md new file mode 100644 index 00000000..574b436d --- /dev/null +++ b/kr/04_Vertex_buffers/01_Vertex_buffer_creation.md @@ -0,0 +1,259 @@ +## 서론 + +Vulkan에서 버퍼는 그래픽 카드가 읽을 수 있는, 임의의 데이터를 저장하는 메모리 영역을 의미합니다. 이 챕터에서의 예시처럼 정점 데이터를 저장하는 데 사용될 수도 있지만 나중 챕터에서 살펴볼 것인데 다른 용도로도 자주 사용됩니다. 지금까지 살펴본 Vulkan 객체와는 다르게 버퍼는 스스로 메모리를 할당하지 않습니다. 지금까지 살펴본 것처럼 Vulkan API는 프로그래머에게 거의 모든 제어권을 주는데, 메모리 관리 또한 이에 포함됩니다. + +## 버퍼 생성 + +`createVertexBuffer` 함수를 새로 만들고 `initVulkan`의 `createCommandBuffers` 바로 직전에 호출하도록 합니다. + +```c++ +void initVulkan() { + createInstance(); + setupDebugMessenger(); + createSurface(); + pickPhysicalDevice(); + createLogicalDevice(); + createSwapChain(); + createImageViews(); + createRenderPass(); + createGraphicsPipeline(); + createFramebuffers(); + createCommandPool(); + createVertexBuffer(); + createCommandBuffers(); + createSyncObjects(); +} + +... + +void createVertexBuffer() { + +} +``` + +버퍼 생성을 위해서는 `VkBufferCreateInfo` 구조체를 채워야 합니다. + +```c++ +VkBufferCreateInfo bufferInfo{}; +bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; +bufferInfo.size = sizeof(vertices[0]) * vertices.size(); +``` + +첫 번째 필드는 `size`이고, 버퍼의 바이트 단위 크기를 명시합니다. 정점 데이터의 바이트 단위 크기를 계산하는 것은 `sizeof`를 사용하면 됩니다. + +```c++ +bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; +``` + +두 번째 필드는 `usage`인데 버퍼의 데이터가 어떤 목적으로 사용될지를 알려줍니다. bitwise OR를 사용해 목적을 여러개 명기하는것도 가능합니다. 우리의 사용 목적은 정점 버퍼이며 다른 타입에 대해서는 다른 챕터에서 알아보겠습니다. + +```c++ +bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +``` + +스왑 체인의 이미지처럼 버퍼는 특정 큐 패밀리에 의해 소유되거나 여러 큐 패밀리에서 공유될 수 있습니다. 버퍼는 그래픽스 큐에서만 활용될 예정이므로 독점(exclusive) 접근으로 두겠습니다. + +`flag` 매개변수는 sparse한 버퍼 메모리를 설정하기 위해 사용되는데, 지금은 사용하지 않습니다. 기본값인 `0`으로 둘 것입니다. + +이제 `vkCreateBuffer`로 버퍼를 만들 수 있습니다. 버퍼 핸들을 저장할 `vertexBuffer`를 클래스의 멤버로 정의합니다. + +```c++ +VkBuffer vertexBuffer; + +... + +void createVertexBuffer() { + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = sizeof(vertices[0]) * vertices.size(); + bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(device, &bufferInfo, nullptr, &vertexBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to create vertex buffer!"); + } +} +``` + +버퍼는 프로그램이 끝날 때까지 렌더링 명령에서 활용되기 위해 유효한 상태로 남아있어야 하고, 스왑 체인에는 종속적이지 않으므로 `cleanup` 함수에서 정리합니다: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + + ... +} +``` + +## 메모리 요구사항 + +버퍼가 생성되었지만 아직 실제로 메모리가 할당된 것은 아닙니다. 버퍼의 메모리 할당을 위한 첫 단계는 `vkGetBufferMemoryRequirements`라는 이름의 함수로 메모리 요구사항을 질의하는 것입니다. + +```c++ +VkMemoryRequirements memRequirements; +vkGetBufferMemoryRequirements(device, vertexBuffer, &memRequirements); +``` + +`VkMemoryRequirements` 구조체에는 세 개의 필드가 있습니다: + +* `size`: 요구되는 메모리의 바이트 단위 크기로, `bufferInfo.size`와는 다를 수 있음 +* `alignment`: `bufferInfo.usage`와 `bufferInfo.flags`에 의해 결정되는, 메모리 영역에서 버퍼가 시작되는 바이트 오프셋(offset) +* `memoryTypeBits`: 버퍼에 적합한 메모리 타입의 비트 필드 + +그래픽 카드는 할당할 수 있는 서로 다른 종류의 메모리가 있습니다. 각 메모리 타입은 허용 가능한 연산과 성능 특성이 다릅니다. 버퍼의 요구사항과 우리 응용 프로그램의 요구사항을 결합하여 적합한 메모리 타입을 결정해야 합니다. 이러한 목적을 위해서 `findMemoryType` 함수를 새로 만듭시다. + +```c++ +uint32_t findMemoryType(uint32_t typeFilter, VkMemoryPropertyFlags properties) { + +} +``` + +먼저 사용 가능한 메모리 타입의 정보를 `vkGetPhysicalDeviceMemoryProperties`를 사용해 질의해야 합니다. + +```c++ +VkPhysicalDeviceMemoryProperties memProperties; +vkGetPhysicalDeviceMemoryProperties(physicalDevice, &memProperties); +``` + +`VkPhysicalDeviceMemoryProperties` 구조체는 `memoryTypes`와 `memoryHeaps` 배열을 가지고 있습니다. 메모리 힙은 VRAM, 그리고 VRAM이 부족할 때 사용하는 RAM의 스왑 공간 같은 메모리 자원입니다. 이 힙 안에 여러 메모리 타입이 존재하게 됩니다. 지금은 메모리 타입만 사용하고 그 메모리가 어떤 힙에 존재하는 것인지 신경쓰지 않을 것이지만 그에 따라 성능에 영향이 있을 수 있다는 것은 예상하실 수 있을겁니다. + +먼저 버퍼에 적합한 메모리 타입을 찾습니다: + +```c++ +for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if (typeFilter & (1 << i)) { + return i; + } +} + +throw std::runtime_error("failed to find suitable memory type!"); +``` + +`typeFilter` 매개변수는 적합한 메모리 타입을 명시하기 위한 비트 필드입니다. 즉 적합한 메모리 타입에 대한 인덱스는 그냥 반복문을 돌면서 해당 비트가 1인지를 확인하여 얻을 수 있습니다. + +하지만 우리는 정점 버퍼를 위한 적합한 메모리 타입에만 관심이 있는 것이 아닙니다. 정점 데이터를 해당 메모리에 쓸 수 있어야 합니다. `memoryTypes` 배열은 힙과 각 메모리 타입의 속성을 명시하는 `VkMemoryType` 구조체의 배열입니다. 속성은 메모리의 특수 기능을 정의하는데 예를 들자면 CPU에서 값을 쓸 수 있도록 맵핑(map)할 수 있는지 여부와 같은 것입니다. 이 속성은 `VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT`로 명시되는데, 추가적으로 `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT` 속성도 필요합니다. 그 이유에 대해서는 메모리 맵핑을 할 때 알게 될 겁니다. + +이제 반복문을 수정해 이러한 속성에 대한 지원 여부를 확인합니다: + +```c++ +for (uint32_t i = 0; i < memProperties.memoryTypeCount; i++) { + if ((typeFilter & (1 << i)) && (memProperties.memoryTypes[i].propertyFlags & properties) == properties) { + return i; + } +} +``` + +필요한 속성이 하나 이상일 수 있으므로 bitwise AND의 결과가 0이 아닌 것만을 확인하면 안되고 해당하는 비트 필드가 필요한 속성과 동일한지 확인해야 합니다. 버퍼에 적합한 메모리 타입이 있고 이러한 속성들을 가지고 있으면 해당 인덱스를 반환하고, 아니면 예외를 발생시키도록 합니다. + +## 메모리 할당 + +이제 올바른 메모리 타입을 정하는 법이 마련되었으니 `VkMemoryAllocateInfo` 구조체를 채워 실제로 메모리를 할당해 보겠습니다. + +```c++ +VkMemoryAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; +allocInfo.allocationSize = memRequirements.size; +allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT); +``` + +이제 단지 크기와 타입을 명시하면 되는데, 모두 정점 버퍼의 메모리 요구사항과 원하는 속성으로부터 도출되는 값입니다. 메모리에 대한 핸들을 저장하기 위한 클래스 멤버를 만들고 `vkAllocateMemory`를 통해 메모리를 할당받습니다. + +```c++ +VkBuffer vertexBuffer; +VkDeviceMemory vertexBufferMemory; + +... + +if (vkAllocateMemory(device, &allocInfo, nullptr, &vertexBufferMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate vertex buffer memory!"); +} +``` + +메모리 할당이 성공했으면 `vkBindBufferMemory`로 그 메모리를 버퍼와 연결(associate)시킵니다: + +```c++ +vkBindBufferMemory(device, vertexBuffer, vertexBufferMemory, 0); +``` + +첫 세 매개변수는 특별히 설명할 것이 없고, 네 번째 매개변수는 메모리 영역에서의 오프셋입니다. 지금 메모리는 정점 버퍼만을 위해 할당받은 것이므로 오프셋은 `0`입니다. +오프셋이 0이 아니면, `memRequirements.alignment`를 사용해 분할 가능해야만 합니다. + +물론 C++에서의 동적 메모리 할당처럼 이 메모리는 어떤 시점에 해제되어야만 합니다. +버퍼 객체와 바인딩된 메모리는 버퍼가 더이상 사용되지 않을 때 해제되면 되기 때문에 버퍼의 소멸 이후에 해제하도록 합시다: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + vkFreeMemory(device, vertexBufferMemory, nullptr); +``` + +## 정점 버퍼 채우기 + +이제 정점 데이터를 버퍼에 복사할 시간입니다. 이는 CPU가 접근 가능한 메모리에 `vkMapMemory`로 [버퍼 메모리 맵핑](https://en.wikipedia.org/wiki/Memory-mapped_I/O)을 함으로써 수행합니다. + +```c++ +void* data; +vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); +``` + +이 함수는 오프셋과 크기로 명시된 특정 메모리 리소스 영역에 접근이 가능하도록 해 줍니다. 여기서 오프셋과 크기는 각각 `0`과 `bufferInfo.size`입니다. `VK_WHOLE_SIZE`와 같은 특수한 값으로 전체 메모리를 맵핑하는 것도 가능합니다. 끝에서 두 번째 매개변수는 플래그를 명시하기 위해 사용될 수도 있지만 현재 API에는 아직 사용 가능한 것이 없습니다. 따라서 값은 `0`이어야만 합니다. 마지막 매개변수는 맵핑된 메모리에 대한 포인터 출력입니다. + +```c++ +void* data; +vkMapMemory(device, vertexBufferMemory, 0, bufferInfo.size, 0, &data); + memcpy(data, vertices.data(), (size_t) bufferInfo.size); +vkUnmapMemory(device, vertexBufferMemory); +``` + +이제 `memcpy`로 정점 데이터를 맵핑된 메모리에 복사하고 `vkUnmapMemory`를 사용해 다시 언맵핑합니다. 안타깝게도 드라이버가 즉시 버퍼 메모리에 복사를 수행하지 못할 수도 있습니다. 예를 들어 캐싱(chching) 때문에요. 또한 버퍼에의 쓰기 작업이 아직 맵핑된 메모리에 보이지 않을 수도 있습니다. 이러한 문제를 처리하기 위한 두 가지 방법이 있습니다: + +* `VK_MEMORY_PROPERTY_HOST_COHERENT_BIT`로 명시된 호스트에 일관성(coherent) 메모리 힙을 사용함 +* 맵핑된 메모리에 쓰기를 수행한 후 `vkFlushMappedMemoryRanges`를 호출하고, 맵핑된 메모리를 읽기 전에 `vkInvalidateMappedMemoryRanges` 를 호출함 + +우리는 첫 번째 방법을 사용했는데 이렇게 하면 맵핑된 메모리의 내용이 할당된 메모리와 항상 동일한 것이 보장됩니다. 이러한 방식은 명시적인 플러싱(flushing)에 비해 약간의 성능 손해가 있다는 것을 아셔야 하지만, 크게 상관 없습니다. 왜 그런지는 다음 챕터에서 살펴보도록 하겠습니다. + +메모리 영역을 플러싱하거나 일관성 메모리 힙을 사용한다는 이야기는 드라이버가 버퍼에 대한 쓰기 의도를 파악하게 된다는 것이지만 아직 실제로 GPU가 그 메모리 영역을 볼 수 있다는 이야기는 아닙니다. 실제로 데이터가 GPU로 전송되는 것은 백그라운드에서 진행되며 [명세](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-submission-host-writes)에서는 단순히 다음 `vkQueueSubmit` 호출 이전에 완료가 보장된다는 것만 정의하고 있습니다. + +## 정점 버퍼 바인딩 + +이제 남은 것은 렌더링 연산에서 정점 버퍼를 바인딩 하는 것입니다. `recordCommandBuffer` 함수를 확장하여 이러한 작업을 수행하도록 합니다. + +```c++ +vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); + +VkBuffer vertexBuffers[] = {vertexBuffer}; +VkDeviceSize offsets[] = {0}; +vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); + +vkCmdDraw(commandBuffer, static_cast(vertices.size()), 1, 0, 0); +``` + +`vkCmdBindVertexBuffers` 함수는 정점 버퍼를 이전 챕터에서 설정한 바인딩에 바인딩합니다. 명령버퍼를 제외한 첫 두 매개변수는 오프셋과 정점 버퍼의 바인딩 숫자를 명시합니다. 마지막 두 매개변수는 바인딩할 정점 버퍼의 배열과 정점 데이터를 읽기 시작할 바이트 오프셋을 명시합니다. 또한 `vkCmdDraw` 호출에서도 하드코딩된 숫자 `3`을 사용하는 대신 버퍼의 정점 개수를 넘겨주도록 수정합니다. + +이제 프로그램을 실행하면 익숙한 삼각형을 다시 볼 수 있습니다: + +![](/images/triangle.png) + +`vertices` 배열을 수정하여 위쪽 정점의 색상을 바꿔봅시다: + +```c++ +const std::vector vertices = { + {{0.0f, -0.5f}, {1.0f, 1.0f, 1.0f}}, + {{0.5f, 0.5f}, {0.0f, 1.0f, 0.0f}}, + {{-0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}} +}; +``` + +이제 프로그램을 다시 실행하면 아래와 같은 화면이 보입니다: + +![](/images/triangle_white.png) + +다음 장에서는 정점 데이터를 정점 버퍼에 복사하는 데 있어 더 좋은 성능을 가지지만, 작업이 좀 더 필요한 방법들을 살펴볼 것입니다. + +[C++ code](/code/19_vertex_buffer.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/kr/04_Vertex_buffers/02_Staging_buffer.md b/kr/04_Vertex_buffers/02_Staging_buffer.md new file mode 100644 index 00000000..b2aa05a2 --- /dev/null +++ b/kr/04_Vertex_buffers/02_Staging_buffer.md @@ -0,0 +1,196 @@ +## 서론 + +지금 만든 정점 버퍼는 잘 동작하지만 CPU에서 접근이 가능하도록 선택한 메모리 타입이 그래픽 카드에서 읽기에는 최적화된 메모리 타입은 아닐 수 있습니다. 가장 적합한 메모리는 `VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT` 플래그를 가지고 있고 대개는 대상 그래픽 카드에 대해 CPU에서는 접근이 불가능합니다. 이 챕터에서는 두 정점 버퍼를 만들 것입니다. 하나는 *스테이징 버퍼(staging buffer)*로 CPU에서 접근 가능하여 정점 배열을 넣을 수 있으며 다른 하나는 장치의 로컬 메모리에 있는 정점 버퍼입니다. 그러고 나서 버퍼 복사 명령을 사용해 스테이징 버퍼에서 실제 정점 버퍼로 데이터를 옮길 것입니다. + +## 전송 큐(Transfer queue) + +버퍼 복사 맹령은 전송 연산을 지원하는 큐 패밀리가 필요하고 이는 `VK_QUEUE_TRANSFER_BIT`로 표기됩니다. 좋은 소식은 `VK_QUEUE_GRAPHICS_BIT`이나 `VK_QUEUE_COMPUTE_BIT` 기능이 있는 큐 패밀리는 암시적으로 `VK_QUEUE_TRANSFER_BIT` 연산을 지원한다는 것입니다. 이러한 경우 `queueFlags`에 이를 명시적으로 표시하도록 구현되는 것이 강제되지는 않습니다. + +도전을 원하신다면 전송 연산을 위해 또 다른 큐 패밀리를 사용하도록 해 보십시오. 이렇게 하려면 다음과 같은 추가적인 수정이 필요합니다: + +* `QueueFamilyIndices`와 `findQueueFamilies`를 수정하여 `VK_QUEUE_TRANSFER_BIT` 비트를 갖지만 `VK_QUEUE_GRAPHICS_BIT`는 갖지 않는 큐 패밀리를 명시적으로 탐색합니다. +* `createLogicalDevice`를 수정하여 전송 큐에 대한 핸들을 요청하도록 합니다. +* 명령 버퍼를 위한 두 번째 명령 풀을 만들어 전송 큐 패밀리에 제출할 것입니다. +* 리소스의 `sharingMode`를 `VK_SHARING_MODE_CONCURRENT`로 하고 그래픽스와 전송 큐 패밀리를 명시합니다. +* (이 챕터에서 사용할 예정인) `vkCmdCopyBuffer`와 같은 전송 명령을 그래픽스 큐 대신 전송 큐에 전송합니다. + +작업이 좀 필요하지만 이를 통해 큐 패밀리간에 리소스가 어떻게 공유되는지를 배우실 수 있을겁니다. + +## 버퍼 생성 추상화 + +이 장에서는 여러 버퍼를 생성할 것이므로, 버퍼 생성에 관한 헬퍼 함수를 만드는 것이 좋을 것 같습니다. 새로운 함수인 `createBuffer`를 만들고 `createVertexBuffer`의 (맵핑을 제외한) 코드를 옮겨옵니다. + +```c++ +void createBuffer(VkDeviceSize size, VkBufferUsageFlags usage, VkMemoryPropertyFlags properties, VkBuffer& buffer, VkDeviceMemory& bufferMemory) { + VkBufferCreateInfo bufferInfo{}; + bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + bufferInfo.size = size; + bufferInfo.usage = usage; + bufferInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateBuffer(device, &bufferInfo, nullptr, &buffer) != VK_SUCCESS) { + throw std::runtime_error("failed to create buffer!"); + } + + VkMemoryRequirements memRequirements; + vkGetBufferMemoryRequirements(device, buffer, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + if (vkAllocateMemory(device, &allocInfo, nullptr, &bufferMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate buffer memory!"); + } + + vkBindBufferMemory(device, buffer, bufferMemory, 0); +} +``` + +버퍼의 크기와 메모리 속성, 사용 목적에 대한 매개변수를 만들어서 다른 종류의 버퍼를 만들 때도 사용 가능하도록 함수를 정의하는 것을 잊지 마세요. 마지막 두 매개변수는 핸들을 저장할 출력 변수입니다. + +이제 버퍼 생성과 메모리 할당 코드를 `createVertexBuffer`에서 제거하고 대신 `createBuffer`를 호출하면 됩니다: + +```c++ +void createVertexBuffer() { + VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + createBuffer(bufferSize, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, vertexBuffer, vertexBufferMemory); + + void* data; + vkMapMemory(device, vertexBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, vertices.data(), (size_t) bufferSize); + vkUnmapMemory(device, vertexBufferMemory); +} +``` + +프로그램을 실행해 정점 버퍼가 여전히 제대로 동작하는지 확인해보세요. + +## 스테이징 버퍼 사용 + +이제 `createVertexBuffer`를 수정해 호스트에서 보이는 버퍼는 임시 버퍼로, 장치 로컬을 실제 정점 버퍼로 사용하도록 할것입니다. + +```c++ +void createVertexBuffer() { + VkDeviceSize bufferSize = sizeof(vertices[0]) * vertices.size(); + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, vertices.data(), (size_t) bufferSize); + vkUnmapMemory(device, stagingBufferMemory); + + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); +} +``` + +새로운 `stagingBuffer`와 `stagingBufferMemory`를 사용해 정점 데이터를 맵핑하고 복사할 것입니다. 이 챕터에서 두 개의 새로운 버퍼 사용법(usage) 플래그를 사용합니다: + +* `VK_BUFFER_USAGE_TRANSFER_SRC_BIT`: 메모리 전송 연산에서 소스(source)로 사용되는 버퍼 +* `VK_BUFFER_USAGE_TRANSFER_DST_BIT`: 메모리 전송 연산에서 목적지(destination)로 사용되는 버퍼 + +`vertexBuffer`는 이제 장치 로컬인 메모리 타입에서 할당되고, 그로 인해 일반적으로 `vkMapMemory`는 사용할 수 없게 됩니다. 하지만 `stagingBuffer`에서 `vertexBuffer`로 데이터를 복사할 수는 있습니다. 우리가 이렇게 하려고 한다는 것을 `stagingBuffer`에는 전송 소스 플래그를, `vertexBuffer`에는 전송 목적지 플래그와 정점 버퍼 플래그를 사용해 알려주어야 합니다. + +이제 한 버퍼에서 다른 버퍼로 복사를 하는 `copyBuffer` 함수를 만듭니다. + +```c++ +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { + +} +``` + +메모리 전송 연산은 그리기와 마찬가지로 명령 버퍼를 통해 실행됩니다. 그러므로 먼저 임시 명령 버퍼를 할당해야 합니다. 이렇게 임시 사용되는 버퍼에 대한 별도의 명령 풀을 만들면 메모리 할당 최적화가 수행될 수 있습니다. 지금의 경우 명령 풀 생성시에 `VK_COMMAND_POOL_CREATE_TRANSIENT_BIT` 플래그를 사용해야 합니다. + +```c++ +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandPool = commandPool; + allocInfo.commandBufferCount = 1; + + VkCommandBuffer commandBuffer; + vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); +} +``` + +그리고 바로 명령 버퍼에 기록을 시작합니다: + +```c++ +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; +beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + +vkBeginCommandBuffer(commandBuffer, &beginInfo); +``` + +명령 버퍼를 한 번만 사용하고 복사 연산 실행이 끝나서 함수가 반환될 때까지 대기하도록 할 것입니다. 이러한 의도를 `VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT`를 사용해 드라이버에게 알려주는 것이 좋습니다. + +```c++ +VkBufferCopy copyRegion{}; +copyRegion.srcOffset = 0; // Optional +copyRegion.dstOffset = 0; // Optional +copyRegion.size = size; +vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region); +``` + +버퍼 내용은 `vkCmdCopyBuffer` 명령을 통해 전송됩니다. 소스와 목적지 버퍼, 그리고 복사할 영역에 대한 배열을 인자로 받습니다. 영역은 `VkBufferCopy`로 정의되며 소스 버퍼의 오프셋, 목적지 버퍼의 오프셋, 크기로 구성됩니다. `vkMapMemory` 명령과는 달리 여기서는 `VK_WHOLE_SIZE`로 명시하는 것은 불가능합니다. + +```c++ +vkEndCommandBuffer(commandBuffer); +``` + +이 명령 버퍼는 복사 명령만을 포함하므로 바로 기록을 중단하면 됩니다. 이제 명령 버퍼를 실행하여 전송을 완료합니다: + +```c++ +VkSubmitInfo submitInfo{}; +submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandBuffer; + +vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); +vkQueueWaitIdle(graphicsQueue); +``` + +그리기 명령과는 다르게 여기서를 기다려야 할 이벤트가 없습니다. 그냥 버퍼에 대한 전송 명령을 바로 실행하기를 원합니다. 여기서도 마찬가지로 이러한 전송이 완료되는 것을 기다리는 두 가지 방법이 있습니다. `vkWaitForFences`로 펜스를 사용해 대기하거나, 전송 큐가 아이들(idle) 상태가 될때까지 대기하도록 `vkQueueWaitIdle`를 사용하는 것입니다. 하나씩 실행하는 것이 아니라 여러 개의 전송 명령을 동시에 계획하고 전체가 끝날때까지 대기하는 경우에 펜스를 사용하면 됩니다. 이렇게 하면 드라이버가 최적화 하기 더 좋습니다. + +```c++ +vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); +``` + +전송 연산을 위해 사용한 명령 버퍼를 정리하는 것을 잊지 마세요. + +이제 `createVertexBuffer` 함수에서 `copyBuffer`를 호출하여 정점 데이터를 장치의 로컬 버퍼로 옮깁니다: + +```c++ +createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, vertexBuffer, vertexBufferMemory); + +copyBuffer(stagingBuffer, vertexBuffer, bufferSize); +``` + +스테이징 버퍼에서 장치 버퍼로 데이터를 복사한 뒤에는 정리해 주어야 합니다: + +```c++ + ... + + copyBuffer(stagingBuffer, vertexBuffer, bufferSize); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +프로그램을 실행하여 삼각형이 잘 보이는지 확인하세요. 개선점이 바로 눈에 보이지는 않지만 이제 정점 데이터는 고성능 메모리로부터 로드(load)됩니다. 보다 복잡한 형상을 렌더링 할 때에는 이러한 사실이 중요해집니다. + +## 결론 + +실제 응용 프로그램에서는 개별 버퍼마다 `vkAllocateMemory`를 호출하지 않는것이 좋다는 점을 주의하십시오. 동시에 수행 가능한 메모리 할당은 물리적 장치의 `maxMemoryAllocationCount`에 의해 제한되며 NVIDIA GTX 1080와 같은 고성능 장치에서도 `4096`정도밖에 안됩니다. 많은 객체를 위한 메모리 할당을 한꺼번에 수행하는 적정한 방법은 여러 객체들에 대해 `offset` 매개변수를 사용해 한 번에 할당을 수행하는 별도의 할당자(allocator)를 만드는 것입니다. + +이러한 할당자를 직접 구현해도 되고, GPUOpen 이니셔티브에서 제공하는 [VulkanMemoryAllocator](https://github.com/GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator) 라이브러리를 사용해도 됩니다. 하지만 이 튜토리얼에서는 각 리소스에 대해 별도의 할당을 수행해도 상관없는데 지금은 위와 같은 제한에 걸릴 만큼 복잡한 작업은 하지 않기 떄문입니다. + +[C++ code](/code/20_staging_buffer.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/kr/04_Vertex_buffers/03_Index_buffer.md b/kr/04_Vertex_buffers/03_Index_buffer.md new file mode 100644 index 00000000..cb889b77 --- /dev/null +++ b/kr/04_Vertex_buffers/03_Index_buffer.md @@ -0,0 +1,122 @@ +## 서론 + +실제 응용 프로그램에서 렌더링할 3D 메쉬는 여러 삼각형에서 정점을 공유하는 경우가 많습니다. 이는 아래와 같은 간단한 사각형을 렌더링 할 때도 적용됩니다: + +![](/images/vertex_vs_index.svg) + +사각형을 렌더링하려면 삼각형 두 개가 필요하기 때문에 정점 6개를 갖는 정점 버퍼가 필요합니다. 문제는 두 개의 정점은 동일한 데이터이기 때문에 50%의 중복이 발생한다는 것입니다. 메쉬가 복잡해지면 평균적으로 정점 한개당 3개의 삼각형에서 재사용되어 상황은 더 나빠집니다. 이 문제를 해결하는 방법은 *인덱스 버퍼(index buffer)*를 사용하는 것입니다. + +인덱스 버퍼는 정점 버퍼에 대한 포인터 배열과 같습니다. 정점 데이터의 순서를 바꾸고 이미 존재하는 데이터는 여러 정점으로 활용할 수 있게 해줍니다. 위 그림에서는 정점 버퍼가 네 개의 정점을 가지고 있을 때 인덱스 버퍼를 사용해 사각형을 표현하는 예시를 보여줍니다. 첫 세 개의 인덱스가 위 오른쪽 삼각형을 정의하며 마지막 세 개의 인덱스가 왼쪽 아래 삼각형을 정의합니다. + +## 인덱스 버퍼 생성 + +이 챕터에서 우리는 정점 데이터를 수정하고 인덱스 데이터를 추가하여 위 그림과 같은 사각형을 그려 볼 것입니다. 네 개의 꼭지점을 표현하도록 정점 데이터를 수정합니다: + +```c++ +const std::vector vertices = { + {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}}, + {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}}, + {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}}, + {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}} +}; +``` + +왼쪽 위 꼭지점은 빨간색, 오른쪽 위는 초록색, 오른쪽 아래는 파란색, 왼쪽 아래는 흰색입니다. 인덱스 버퍼의 내용은 새로운 `indices`를 추가하여 정의합니다. 오른쪽 위 삼각형과 왼쪽 아래 삼각형을 위해 그림에서와 같이 인덱스를 정의합니다. + +```c++ +const std::vector indices = { + 0, 1, 2, 2, 3, 0 +}; +``` + +`vertices`요소 개수에 따라 인덱스 버퍼로 `uint16_t`나 `uint32_t`를 사용하는 것이 모두 가능합니다. 지금은 65535개보다는 정점이 적으므로 `uint16_t`를 사용합니다. + +정점 데이터와 마찬가지로 인덱스도 `VkBuffer`를 통해 GPU로 전달되어 접근 가능하게 만들어야만 합니다. 인덱스 버퍼 리소스를 저장할 두 개의 새로운 클래스 멤버를 정의합니다: + +```c++ +VkBuffer vertexBuffer; +VkDeviceMemory vertexBufferMemory; +VkBuffer indexBuffer; +VkDeviceMemory indexBufferMemory; +``` + +새로 추가하는 `createIndexBuffer` 함수는 `createVertexBuffer`와 거의 동일합니다: + +```c++ +void initVulkan() { + ... + createVertexBuffer(); + createIndexBuffer(); + ... +} + +void createIndexBuffer() { + VkDeviceSize bufferSize = sizeof(indices[0]) * indices.size(); + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, indices.data(), (size_t) bufferSize); + vkUnmapMemory(device, stagingBufferMemory); + + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_DST_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, indexBuffer, indexBufferMemory); + + copyBuffer(stagingBuffer, indexBuffer, bufferSize); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +눈에 띄는 차이점은 두 가지입니다. `bufferSize`는 인덱스 자료형인 `uint16_t` 또는 `uint32_t`의 크기 곱하기 인덱스의 개수입니다. `indexBuffer`의 사용법은 당연히 `VK_BUFFER_USAGE_VERTEX_BUFFER_BIT`가 아닌 `VK_BUFFER_USAGE_INDEX_BUFFER_BIT` 입니다. 이 외에는 모든 것이 같습니다. `indices`의 내용을 장치의 로컬 인덱스 버퍼에 복사하기 위해 스테이징 버퍼를 만들어 사용합니다. + +인덱스 버퍼 정점 버퍼처럼 프로그램 종료 시점에 정리되어야 합니다: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyBuffer(device, indexBuffer, nullptr); + vkFreeMemory(device, indexBufferMemory, nullptr); + + vkDestroyBuffer(device, vertexBuffer, nullptr); + vkFreeMemory(device, vertexBufferMemory, nullptr); + + ... +} +``` + +## 인덱스 버퍼 사용 + +그리기를 위해 인덱스 버퍼를 사용하기 위해서는 `recordCommandBuffer`에 두 가지 변화가 필요합니다. 우선 정범 버퍼터럼 인덱스 버퍼도 바인딩 해야 합니다. 차이점은 하나의 인덱스 버퍼만 가질 수 있다는 것입니다. 각 정점 어트리뷰트에 대해 안타깝게도 여러 개의 인덱스를 사용할 수는 없으니 하나의 어트리뷰트만 바뀌어도 전체 정점 데이터를 복사해야 합니다. + +```c++ +vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); + +vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT16); +``` + +인덱스 버퍼의 바인딩은 `vkCmdBindIndexBuffer`로 수행되며 이 함수는 인덱스 버퍼, 바이트 오프셋, 인덱스 데이터의 타입을 매개변수로 받습니다. 앞서 이야기한 것처럼 타입은 `VK_INDEX_TYPE_UINT16` 또는 `VK_INDEX_TYPE_UINT32` 입니다. + +인덱스 버퍼를 바인딩한 것만으로 아직 바뀐 것은 없습니다. Vulkan에 인덱스 버퍼를 사용하도록 그리기 명령 또한 수정해야 합니다. `vkCmdDraw` 라인을 `vkCmdDrawIndexed`로 바꿉니다: + +```c++ +vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); +``` + +`vkCmdDraw` 호출과 매우 유사합니다. 첫 두 매개변수는 인덱스의 개수와 인스턴스 개수를 명시합니다. 인스턴싱을 하지 않으므로 `1`로 두었습니다. 인덱스의 개수는 정점 셰이더에 넘겨질 정점의 개수를 의미합니다. 다음 매개변수는 인덱스 버터의 오프셋이고 `1`을 사용하면 두 번째 인덱스부터 렌더링을 시작합니다. 마지막에서 두 번째 매개변수는 인덱스 버퍼에 추가할 인덱스의 오프셋을 의미합니다. 마지막 매개변수는 인스턴싱을 위한 오프셋이고, 여기서는 사용하지 않습니다. + +이제 프로그램을 실행하면 아래와 같은 화면이 보입니다: + +![](/images/indexed_rectangle.png) + +이제 인덱스 버퍼를 사용해 정점을 재사용하여 메모리를 아끼는 법을 배웠습니다. 나중에 복잡한 3D 모델을 로딩할 챕터에서는 이러한 기능이 특히 중요합니다. + +이전 챕터에서 버퍼와 같은 다중 리소스들을 한 번의 메모리 할당으로 진행해야 한다고 언급했지만 사실 그 이상으로 해야 할 일들이 있습니다. [드라이버 개발자가 추천하길](https://developer.nvidia.com/vulkan-memory-management) 정점과 인덱스 버퍼와 같은 다중 버퍼를 하나의 `VkBuffer`에 저장하고 `vkCmdBindVertexBuffers`와 같은 명령에서 오프셋을 사용하라고 합니다. 이렇게 하면 데이터가 함께 존재하기 때문에 더 캐시 친화적입니다. 또한 같은 렌더링 연산에 사용되는 것이 아니라면, 메모리 덩어리(chunk)를 여러 리소스에서 재사용 하는 것도 가능합니다. 이는 *앨리어싱(aliasing)*이라고 불리며 몇몇 Vulkan 함수는 이러한 동작을 수행하려 한다는 것을 알려주기 위한 명시적 플래그도 존재합니다. + +[C++ code](/code/21_index_buffer.cpp) / +[Vertex shader](/code/18_shader_vertexbuffer.vert) / +[Fragment shader](/code/18_shader_vertexbuffer.frag) diff --git a/kr/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md b/kr/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md new file mode 100644 index 00000000..a3524ac5 --- /dev/null +++ b/kr/05_Uniform_buffers/00_Descriptor_set_layout_and_buffer.md @@ -0,0 +1,319 @@ +## 서론 + +이제 각 정점에 대한 어트리뷰트를 정점 셰이더에 넘길 수 있게 되었습니다만, 전역 변수는 어떤가요? 이 챕터에서부터 3D 그래픽스로 넘어갈 것인데 그러려면 모델-뷰-투영(projection) 행렬이 있어야 합니다. 이를 정점 데이터로 포함할 수도 있지만 그건 메모리도 낭비될 뿐만 아니라 변환(transformation)이 바뀌게 되면 정점 버퍼가 업데이트되어야 하는 문제가 있습니다. 변환 정보는 매 프레임마다 변화합니다. + +Vulkan에서 이를 처리하는 올바른 방법은 *리소스 기술자(resource descriptor)*를 사용하는 것입니다. 기술자는 셰이더가 버퍼나 이미지와 같은 리소스에 자유롭게 접근하게 해 주는 방법입니다. 우리는 변환 행렬을 가지고 있는 버퍼를 설정하고 정점 셰이더가 기술자를 통해 이에 접근할 수 있도록 할 것입니다. 기술자의 사용은 세 부분으로 이루어져 있습니다: + +* 파이프라인 생성 시점에 기술자 집합 레이아웃 명시 +* 기술자 풀로부터 기술자 집합(set) 할당 +* 렌더링 시점에 기술자 바인딩 + +*기술자 집합 레이아웃*은 파이프라인에서 접근할 리소스의 타입을 명시하고, 이는 렌더 패스에서 접근할 어태치먼트의 타입을 명시하는 것과 비슷합니다. *기술자 집합*은 기술자에 바인딩될 버퍼나 이미지를 명시하는데, 이는 프레임버퍼가 렌더 패스 어태치먼트에 바인딩될 실제 이미지 뷰를 명시하는 것과 비슷합니다. 이후에 기술자 집합은 정점 버퍼나 프레임버퍼와 유사하게 그리기 명령에 바인딩됩니다. + +기술자에는 다양한 종류가 있는데 이 챕터에서는 유니폼 버퍼 객체(uniform buffer object, UBO)를 사용할 것입니다. 다른 타입의 기술자에 대해서는 나중 챕터에서 알아볼 것이고, 사용 방법은 동일합니다. 정점 셰이더에서 사용하려고 하는 데이터가 아래와 같은 C 구조체 형식의 데이터라고 해 봅시다: + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +이 데이터를 `VkBuffer`를 통해 복사하고 UBO 기술자를 사용하여 정점 셰이더에서 아래와 같이 접근할 수 있습니다: + +```glsl +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0); + fragColor = inColor; +} +``` + +매 프레임마다 모델, 뷰, 투영 행렬을 갱신하여 이전 챕터에서 만든 사각형이 3D 회전하도록 만들어 보겠습니다. + +## 정점 셰이더 + +정점 셰이더가 UBO를 사용하도록 수정하는 것은 위에서 본 것과 같습니다. 또한 여러분들이 MVP 변환에는 익숙하다고 가정하고 있습니다. 잘 모르시면 첫 번째 챕터에서 언급한 [관련 자료](https://www.opengl-tutorial.org/beginners-tutorials/tutorial-3-matrices/)를 살펴보십시오. + +```glsl +#version 450 + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; + +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec3 inColor; + +layout(location = 0) out vec3 fragColor; + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0); + fragColor = inColor; +} +``` + +`uniform`, `in`, `out` 선언의 순서는 상관 없다는 것에 유의하십시오. `binding` 지시자는 어트리뷰트의 `location` 지시자와 유사합니다. 이 바인딩을 기술자 집합 레이아웃 에서 참조할 것입니다. `gl_Potision`이 있는 라인은 클립 공간에서의 위치를 계산하기 위해 변환 행렬들을 사용하는 것으로 수정되었습니다. 2D 삼각형의 경우와는 다르게, 클립 공간 좌표의 마지막 요소는 `1`이 아닐 수 있습고, 이는 최종적으로 정규화된 장치 좌표계(normalized device coordinate)로 변환될 때 나눠지게 됩니다. 원근 투영을 위해 이러한 과정이 수행되고, 이를 *perspective division*이라고 합니다. 이로 인해 가까운 물체가 멀리 있는 물체보다 크게 표현됩니다. + +## 기술자 집합 레이아웃 + +다음 단계는 C++ 쪽에서 UBO를 정의하고 Vulkan에 이 기술자를 알려주는 것입니다. + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +GLM을 사용하면 셰이더에서 정의한 데이터 타입과 정확히 일치됩니다. 행렬 내의 데이터는 셰이더에 전달되어야 하는 형식과 바이너리 수준에서 호환되기 때문에 단순히 `UniformBufferObject`를 `VkBuffer`로 `memcpy`하기만 하면 됩니다. + +셰이더에서 사용하는 기술자 바인딩에 대한 세부사항을 파이프라인 생성시에 알려주어야 하며, 이는 정점 어트리뷰트와 `location` 인덱스에 대해 했던 작업과 비슷합니다. 이러한 정보들을 정의하기 위한 `createDescriptorSetLayout` 함수를 새로 만듭니다. 파이프라인 생성 시점에서 이러한 정보를 사용해야 하기 때문에, 파이프라인 생성 직전에 호출하도록 합니다. + +```c++ +void initVulkan() { + ... + createDescriptorSetLayout(); + createGraphicsPipeline(); + ... +} + +... + +void createDescriptorSetLayout() { + +} +``` + +바인딩은 `VkDescriptorSetLayoutBinding` 구조체를 통해 기술됩니다. + +```c++ +void createDescriptorSetLayout() { + VkDescriptorSetLayoutBinding uboLayoutBinding{}; + uboLayoutBinding.binding = 0; + uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + uboLayoutBinding.descriptorCount = 1; +} +``` + +첫 두 필드는 셰이더에서 사용하는 `binding`과 기술자의 타입인 UBO를 명시합니다. 셰이더 변수가 UBO의 배열을 표현하는 것도 가능하며 `descriptorCount`는 그 배열의 개수를 명시합니다. 애니메이션을 위해 스켈레톤(skeleton) 관절들의 회전을 명시하는 등의 목적으로 사용이 가능합니다. MVP 변환은 하나의 UBO이므로 `descriptorCount`는 `1`을 사용합니다. + +```c++ +uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT; +``` + +또한 어떤 셰이더 단계를 기술자가 참조할지를 명시해야 합니다. `stageFlags` 필드는 `VkShaderStageFlagBits` 값의 조합, 또는 `VK_SHADER_STAGE_ALL_GRAPHICS` 일 수 있습니다. 우리의 경우 정점 셰이더에서만 기술자를 참조합니다. + +```c++ +uboLayoutBinding.pImmutableSamplers = nullptr; // Optional +``` + +`pImmutableSamplers` 필드는 이미지 샘플링과 관련된 기술자에서만 사용되며 나중에 살펴볼 것입니다. 지금은 그냥 기본값으로 둡니다. + +모든 기술자의 바인딩은 하나의 `VkDescriptorSetLayout` 객체로 표현됩니다. `pipelineLayout` 위에 새로운 클래스 멤버를 정의합니다: + +```c++ +VkDescriptorSetLayout descriptorSetLayout; +VkPipelineLayout pipelineLayout; +``` + +`vkCreateDescriptorSetLayout`를 사용해 레이아웃을 생성합니다. 이 함수는 `VkDescriptorSetLayoutCreateInfo`와 바인딩 배열을 받습니다: + +```c++ +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = 1; +layoutInfo.pBindings = &uboLayoutBinding; + +if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create descriptor set layout!"); +} +``` + +파이프라인 생성 시점에 기술자 집합 레이아웃을 명시해서 Vulkan에게 셰이더가 어떤 기술자를 사용할 것인지를 알려 주어야 합니다. 기술자 집합 레이아웃은 파이프라인 레이아웃 객체에 명시됩니다. `VkPipelineLayoutCreateInfo`를 수정하여 레이아웃 객체를 참조하도록 합니다: + +```c++ +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 1; +pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout; +``` + +이 부분에 왜 여러개의 기술자 집합 레이아웃을 명시할 수 있게 되어있는지 궁금하실겁니다. 하나의 기술자 집합 레이아웃에 이미 모든 바인딩 정보가 들어있는데도 말이죠. 다음 챕터에서 기술자 풀과 기술자 집합을 살펴보면서 이에 대한 이야기를 해 보도록 하겠습니다. + +기술자 집합 레이아웃은 새로운 그래픽스 파이프라인이 생성될 동안, 즉 프로그램 종료 시까지 유지되어야 합니다: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + + ... +} +``` + +## 유니폼 버퍼(Uniform buffer) + +다음 챕터에서 우리는 셰이더에서 사용할 UBO 데이터를 가진 버퍼를 명시할 예정입니다. 그러려면 먼저 버퍼를 만들어야겠죠. 매 프레임 새로운 데이터를 유니폼 버퍼에 복사할 예정이므로 스테이징 버퍼를 사용하는 것은 적절하지 않습니다. 단지 추가적인 작업으로 인해 성능이 낮아질 뿐입니다. + +여러 프레임이 동시에 작업되기 때문에 버퍼가 여러 개 필요합니다. 버퍼에서 값을 읽는 동안 다름 프레임을 위한 데이터를 덮어 쓰면 안되기 때문이죠. 따라서 사용하고 있는 프레임의 개수만큼 유니폼 버퍼가 필요하며, GPU가 읽고 있는 버퍼가 아닌 다른 버퍼에 값을 기록해야 합니다. + +`uniformBuffers`와 `uniformBuffersMemory`를 새로운 클래스 멤버로 추가합니다: + +```c++ +VkBuffer indexBuffer; +VkDeviceMemory indexBufferMemory; + +std::vector uniformBuffers; +std::vector uniformBuffersMemory; +std::vector uniformBuffersMapped; +``` + +비슷하게, 버퍼를 할당하는 `createUniformBuffers` 함수를 새로 만들고 `createIndexBuffer` 뒤에 호출하도록 합니다: + +```c++ +void initVulkan() { + ... + createVertexBuffer(); + createIndexBuffer(); + createUniformBuffers(); + ... +} + +... + +void createUniformBuffers() { + VkDeviceSize bufferSize = sizeof(UniformBufferObject); + + uniformBuffers.resize(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT); + uniformBuffersMapped.resize(MAX_FRAMES_IN_FLIGHT); + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, uniformBuffers[i], uniformBuffersMemory[i]); + + vkMapMemory(device, uniformBuffersMemory[i], 0, bufferSize, 0, &uniformBuffersMapped[i]); + } +} +``` + +`vkMapMemory`를 사용해 버퍼를 생성한 뒤에 곧바로 맵핑하여 데이터를 쓰기 위한 포인터를 얻습니다. 프로그램의 실행 내내 버퍼는 이 포인터에 맵핑된 상태가 됩니다. 이러한 기술은 **"지속적 맵핑(persistent mapping)"**이라고 하며 모든 Vulkan 구현에서 동작합니다. 매번 데이터를 갱신할 때마다 버퍼를 맵핑하지 않아도 되기 때문에 성능이 증가합니다. + +유니폼 데이터는 모든 드로우 콜(draw call)에서 활용되기 때문에 렌더링이 끝났을 때 해제되어야 합니다. + +```c++ +void cleanup() { + ... + + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + vkDestroyBuffer(device, uniformBuffers[i], nullptr); + vkFreeMemory(device, uniformBuffersMemory[i], nullptr); + } + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + + ... + +} +``` + +## 유니폼 데이터 갱신 + +`updateUniformBuffer` 함수를 새로 만들고 `drawFrame` 함수에서 다음 프레임의 제출 전에 호출하도록 합니다: + +```c++ +void drawFrame() { + ... + + updateUniformBuffer(currentFrame); + + ... + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + + ... +} + +... + +void updateUniformBuffer(uint32_t currentImage) { + +} +``` + +이 함수에서 매 프레임 새로운 변환을 생성하여 물체가 회전하도록 합니다. 이러한 기능을 구현하기 위해 두 개의 새로운 헤더를 include합니다: + +```c++ +#define GLM_FORCE_RADIANS +#include +#include + +#include +``` + +`glm/gtc/matrix_transform.hpp` 헤더는 `glm::rotate`와 같은 모델 변환, `glm::lookAt`과 같은 뷰 변환, `glm::perspective`와 같은 투영 변환을 생성하기 위한 함수를 제공합니다. `glm::rotate`과 같은 함수가 라디안(radian)을 받도록 `GLM_FORCE_RADIANS`를 정의하여 혼동이 없도록 해야 합니다. + +`chrono` 표준 라이브러리 헤더는 정확한 시간과 관련된 함수를 제공합니다. 이를 사용하여 프레임 레이트와 상관없이 물체가 1초에 90도 회전하도록 할 것입니다. + +```c++ +void updateUniformBuffer(uint32_t currentImage) { + static auto startTime = std::chrono::high_resolution_clock::now(); + + auto currentTime = std::chrono::high_resolution_clock::now(); + float time = std::chrono::duration(currentTime - startTime).count(); +} +``` + +`updateUniformBuffer` 함수는 렌더링 시작부터 현재까지의 시간을 부동소수점 정밀도로 계산하기 위한 로직을 작성하는 것부터 시작합니다. + +이제 UBO에 모델, 뷰, 투영 변환을 정의합니다. 모델 회전은 Z축에 대한 회전이며, `time` 변수를 사용합니다: + +```c++ +UniformBufferObject ubo{}; +ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f)); +``` + +`glm::rotate` 함수는 기존 변환과 회전 각도, 회전축을 매개변수로 받습니다. `glm::mat4(1.0f)` 생성자는 단위 행렬(identity matrix)를 반환합니다. `time * glm::radians(90.0f)`를 회전 각도로 사용함으로써 1초에 90도 회전을 하게 할 수 있습니다. + +```c++ +ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f)); +``` + +뷰 변환에 대해서는 45도 위에서 물체를 바라보도록 했습니다. `glm::lookAt`은 눈의 위치, 바라보는 지점과 업(up) 벡터를 매개변수로 받습니다. + +```c++ +ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f); +``` + +45도의 수직 시야각(field-of-view)를 갖도록 원근 투영을 정의했습니다. 나머지 매개변수는 종횡비(aspect ratio), 근면(near plane)과 원면(far plane)입니다. 현재 스왑 체인의 범위(extent)를 기반으로 종횡비를 계산하여 윈도우 크기가 변해도 새로운 너비와 높이를 반영할 수 있도록 하는 것이 중요합니다. + +```c++ +ubo.proj[1][1] *= -1; +``` + +GLM은 원래 OpenGL을 기반으로 설계되었기 때문에 클립 좌표계의 Y축이 뒤집혀 있습니다. 이를 보정하기 위한 가장 간단한 방법은 투영행렬의 Y축 크기변환(scaling) 요소의 부호를 바꿔주는 것입니다. 이렇게 하지 않으면 위아래가 뒤집혀서 렌더링됩니다. + +모든 변환이 정의되었으니 UBO의 데이터를 현재 유니폼 버퍼로 복사할 수 있습니다. 이 과정은 정점 버퍼에서와 완전히 동일하며, 스테이징 버퍼가 없다는 점만 다릅니다. 전에 이야기한 것처럼 유니폼 버퍼는 한 번만 맵핑하므로 다시 맵핑할 필요 없이 쓰기만 수행하면 됩니다: + +```c++ +memcpy(uniformBuffersMapped[currentImage], &ubo, sizeof(ubo)); +``` + +UBO를 이런 방식으로 사용하는 것은 자주 바뀌는 값을 셰이더에 전달하기 위한 효율적인 방법은 아닙니다. 작은 버퍼의 데이터를 셰이더에 전달하기 위한 더 효율적인 방법은 *Push 상수(push constant)*입니다. 이에 대해서는 나중 챕터에서 알아보도록 하겠습니다. + +다음 챕터에서는 `VkBuffer`들을 유니폼 버퍼 기술자에 바인딩하는 기술자 집합에 대해 알아볼 것입니다. 이를 통해 셰이더가 이러한 변환 데이터에 접근할 수 있게 될 것입니다. + +[C++ code](/code/22_descriptor_set_layout.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/kr/05_Uniform_buffers/01_Descriptor_pool_and_sets.md b/kr/05_Uniform_buffers/01_Descriptor_pool_and_sets.md new file mode 100644 index 00000000..efcb2de2 --- /dev/null +++ b/kr/05_Uniform_buffers/01_Descriptor_pool_and_sets.md @@ -0,0 +1,327 @@ +## 서론 + +이전 장에서의 기술자 집합 레이아웃은 바인딩 될 수 있는 기술자의 타입을 명시합니다. 이 장에서는 각 `VkBuffer` 리소스를 위한 기술자 집합을 만들어서 유니폼 버퍼 기술자에 바인딩할 것입니다. + +## 기술자 풀 + +기술자 집합은 직접 만들 수 없고 명령 버퍼처럼 풀로부터 할당되어야 합니다. 기술자 집합에서 이에 대응하는 것은 당연하게도 *기술자 풀*입니다. 이를 설정하기 위해 `createDescriptorPool` 함수를 새로 작성합니다. + +```c++ +void initVulkan() { + ... + createUniformBuffers(); + createDescriptorPool(); + ... +} + +... + +void createDescriptorPool() { + +} +``` + +먼저 기술자 집합이 어떤 기술자 타입을 포함할 것인지, 몇 개를 포함할 것인지를 `VkDescriptorPoolSize` 구조체를 통해 명시합니다. + +```c++ +VkDescriptorPoolSize poolSize{}; +poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +poolSize.descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +이 기술자 중 하나를 매 프레임 할당할 것입니다. 이 풀 크기에 대한 구조체는 `VkDescriptorPoolCreateInfo`에서 참조됩니다: + +```c++ +VkDescriptorPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +poolInfo.poolSizeCount = 1; +poolInfo.pPoolSizes = &poolSize; +``` + +가용한 개별 기술자의 최대 숫자와는 별개로 할당될 기술자 집합의 최대 숫자도 명시해야 합니다: + +```c++ +poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +명령 풀과 유사하게 이 구조체도 개별 기술자 집합이 해제가 될 수 있을지에 대한 선택적인 플래그로 `VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT`가 존재합니다. 기술자 세트는 생성한 이후에 건들지 않을 것이므로 이 플래그를 사용하지는 않을 것입니다. 따라서 `flags`는 기본값인 `0`으로 두면 됩니다. + +```c++ +VkDescriptorPool descriptorPool; + +... + +if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { + throw std::runtime_error("failed to create descriptor pool!"); +} +``` + +기술자 풀의 핸들을 저장하기 위한 클래스 멤버를 추가하고 `vkCreateDescriptorPool`를 호출하여 생성합니다. + +## 기술자 집합 + +이제 기술자 집합을 할당할 수 있습니다. 이를 위해 `createDescriptorSets` 함수를 추가합니다: + +```c++ +void initVulkan() { + ... + createDescriptorPool(); + createDescriptorSets(); + ... +} + +... + +void createDescriptorSets() { + +} +``` + +기술자 집합의 할당은 `VkDescriptorSetAllocateInfo` 구조체를 사용합니다. 어떤 기술자 풀에서 할당할 것인지, 기술자 집합을 몇 개나 할당할 것인지, 기반이 되는 기술자 집합 레이아웃이 무엇인지 등을 명시합니다: + +```c++ +std::vector layouts(MAX_FRAMES_IN_FLIGHT, descriptorSetLayout); +VkDescriptorSetAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; +allocInfo.descriptorPool = descriptorPool; +allocInfo.descriptorSetCount = static_cast(MAX_FRAMES_IN_FLIGHT); +allocInfo.pSetLayouts = layouts.data(); +``` + +우리의 경우 사용 중인 각 프레임마다 하나의 기술자 집합을 생성할 것이고, 레이아웃은 모두 동일합니다. 안타깝게도 이 레이아웃들을 모두 복사해야만 하는데 이 다음 함수에서 집합의 개수와 배열의 개수가 일치되어야 하기 떄문입니다. + +기술자 집합 핸들을 저장할 클래스 멤버를 추가하고 `vkAllocateDescriptorSets`를 사용해 할당합니다: + +```c++ +VkDescriptorPool descriptorPool; +std::vector descriptorSets; + +... + +descriptorSets.resize(MAX_FRAMES_IN_FLIGHT); +if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate descriptor sets!"); +} +``` + +기술자 집합은 기술자 풀이 소멸될 때 자동으로 해제되므로 명시적으로 정리해 줄 필요는 없습니다. `vkAllocateDescriptorSets` 호출은 기술자 집합을 할당하고 각각은 하나의 유니폼 버퍼 기술자를 갖고 있습니다. + +```c++ +void cleanup() { + ... + vkDestroyDescriptorPool(device, descriptorPool, nullptr); + + vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); + ... +} +``` + +이제 기술자 집합은 할당되었으나 그 안의 기술자에 대한 구성이 남아 있습니다. 이제 각 기술자를 생성하기 위한 반복문을 추가합니다. + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + +} +``` + +우리 유니폼 버퍼 기술자와 같이, 버퍼를 참조하는 기술자는 `VkDescriptorBufferInfo` 구조체로 설정할 수 있습니다. 이 구조체는 버퍼와 데이터가 들어있는 버퍼의 영역을 명시합니다. + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + VkDescriptorBufferInfo bufferInfo{}; + bufferInfo.buffer = uniformBuffers[i]; + bufferInfo.offset = 0; + bufferInfo.range = sizeof(UniformBufferObject); +} +``` + +지금 우리가 하는 것처럼 전체 버퍼를 덮어쓰는 상황이라면 range에 `VK_WHOLE_SIZE`를 사용해도 됩니다. 기술자의 구성은 `vkUpdateDescriptorSets` 함수를 사용해 갱신되는데 `VkWriteDescriptorSet` 구조체의 배열을 매개변수로 받습니다. + +```c++ +VkWriteDescriptorSet descriptorWrite{}; +descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrite.dstSet = descriptorSets[i]; +descriptorWrite.dstBinding = 0; +descriptorWrite.dstArrayElement = 0; +``` + +첫 두 필드는 갱신하고 바인딩할 기술자 집합을 명시합니다. 우리는 유니폼 버퍼 바인딩 인덱스로 `0`을 부여했습니다. 기술자는 배열일 수도 있으므로 갱신하고자 하는 첫 인덱스를 명시해 주어야 합니다. 지금은 배열이 아니므로 인덱스로는 `0`을 사용합니다. + +```c++ +descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +descriptorWrite.descriptorCount = 1; +``` + +기술자의 타입을 다시 명시해 주어야 합니다. `dstArrayElement` 인덱스부터 시작해서 배열의 여러 기술자를 한꺼번에 갱신하는 것이 가능합니다. `descriptorCount` 필드가 갱신할 배열의 요소 개수를 명시하게 됩니다. + +```c++ +descriptorWrite.pBufferInfo = &bufferInfo; +descriptorWrite.pImageInfo = nullptr; // Optional +descriptorWrite.pTexelBufferView = nullptr; // Optional +``` + +마지막 필드는 실제 기술자를 구성할 `descriptorCount`개의 구조체 배열을 참조합니다. 셋 중에 실제로 사용할 것이 무엇인지에 따라 달라집니다. `pBufferInfo` 필드는 버퍼 데이터를 참조하는 경우 사용되고, `pImageInfo`는 이미지 데이터를 참조하는 경우, `pTexelBufferView`는 버퍼 뷰를 참조하는 기술자에 대해 사용됩니다. 우리의 경우 버퍼를 참조하므로 `pBufferInfo`를 사용합니다. + +```c++ +vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr); +``` + +갱신은 `vkUpdateDescriptorSets`를 사용해 이루어집니다. 두 종류의 배열을 매개변수로 받는데 `VkWriteDescriptorSet`과 `VkCopyDescriptorSet` 입니다. 후자는 이름 그대로 기술자들끼리 복사할 때 사용됩니다. + +## 기술자 집합 사용 + +이제 `recordCommandBuffer` 함수를 갱신해서 실제로 각 프레임에 대한 올바른 기술자 세트를 셰이더의 기술자와 `vkCmdBindDescriptorSets`를 통해 바인딩해야 합니다. 이는 `vkCmdDrawIndexed` 호출 전에 이루어져야 합니다: + +```c++ +vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[currentFrame], 0, nullptr); +vkCmdDrawIndexed(commandBuffer, static_cast(indices.size()), 1, 0, 0, 0); +``` + +정점 버퍼나 인덱스 버퍼와는 다르게, 기술자 집합은 그래픽스 파이프라인에서만 사용되는 것은 아닙니다. 따라서 기술자 집합을 그래픽스 또는 컴퓨트 파이프라인 중 어디에 사용할 것인지를 명시해야 합니다. 다음 매개변수는 기술자가 기반으로 하는 레이아웃입니다. 그 다음 세 개의 매개변수는 첫 기술자 집합의 인덱스와 바인딩할 집합의 개수, 그리고 바인딩할 집합의 배열입니다. 이에 대해선 잠시 뒤에 다시 실펴볼 것입니다. 마지막 두 개의 매개변수는 동적(dynamic) 기술자를 사용할 때를 위한 오프셋의 배열을 명시합니다. 이에 대해서는 나중 챕터에서 알아보겠습니다. + +지금 시점에 프로그램을 실행하면 아무것도 보이지 않을 겁니다. 문제는 우리가 투영 행렬에 Y 뒤집기를 수행했기 때문에 정점이 시계방향 순서가 아닌 반시계 방향 순서로 그려진다는 것입니다. 이로 인해 후면 컬링(backface culling)이 동작하여 아무것도 그려지지 않게 됩니다. `createGraphicsPipeline` 함수로 가서 `VkPipelineRasterizationStateCreateInfo`의 `frontFace`를 바로잡아줍니다: + +```c++ +rasterizer.cullMode = VK_CULL_MODE_BACK_BIT; +rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE; +``` + +이제 다시 실행해보면 아래와 같이 보일겁니다: + +![](/images/spinning_quad.png) + +이제 투영 행렬이 종횡비에 맞게 투영하므로 사각형이 정사각형으로 보입니다. `updateUniformBuffer`가 화면 크기 변경을 처리하므로 `recreateSwapChain`에서 기술자 집합을 다시 생성할 필요는 없습니다. + +## 정렬 요구조건(Alignment requirements) + +지금까지 대충 넘어갔던 것 중의 하나는 셰이더에서의 유니폼 정의와 C++ 구조체가 어떻게 일치해야 하는가에 관한 것입니다. 양 쪽에 동일한 타입을 사용하는 것이 당연해 보입니다: + +```c++ +struct UniformBufferObject { + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +layout(binding = 0) uniform UniformBufferObject { + mat4 model; + mat4 view; + mat4 proj; +} ubo; +``` + +하지만 그냥 이것으로 끝은 아닙니다. 예를 들어 구조체와 셰이더를 아래와 같이 수정해 봅시다: + +```c++ +struct UniformBufferObject { + glm::vec2 foo; + glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; + +layout(binding = 0) uniform UniformBufferObject { + vec2 foo; + mat4 model; + mat4 view; + mat4 proj; +} ubo; +``` + +셰이더를 다시 컴파일하고 프로그램을 실행하면 지금까지 보였던 사각형이 사라진 것을 볼 수 있습니다! 왜냐하면 *정렬 요구조건*을 고려하지 않았기 때문입니다. + +Vulkan은 구조체의 데이터가 메모리에 특정한 방식으로 정렬되어 있을 것이라고 예상합니다. 예를 들어: + +* 스칼라 값은 N으로 정렬 (= 32비트 float의 경우 4바이트) +* `vec2`는 2N으로 정렬 (= 8바이트) +* `vec3` 또는 `vec4`는 4N으로 정렬 (= 16바이트) +* 중접된 구조체는 멤버의 기본 정렬을 16의 배수로 반올림한 것으로 정렬 +* `mat4` 행렬은 `vec4`와 동일한 정렬이어야 함 + +정렬 요구조건에 대한 전체 내용은 [해당하는 명세](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap15.html#interfaces-resources-layout)를 보시면 됩니다. + +원래 우리의 셰이더는 세 개의 `mat4` 필드를 사용하였으므로 항상 정렬 요구조건을 만족하였습니다. 각 `mat4`는 4 x 4 x 4 = 64바이트이고, `model`은 오프셋 `0`, `view`는 오프셋 `64`, `proj`는 오프셋 `128`입니다. 각각이 16의 배수이므로 문제가 없었습니다. + +8바이트 크기인 `vec2`가 추가된 새 구조체로 인해 모든 오프셋이 맞지 않게 됩니다. 이제 `model`은 오프셋 `8`, `view`는 오프셋 `72`, `proj`는 오프셋 `136`이므로 16의 배수가 아닙니다. 이 문제를 해결하기 위해서는 C++11에서 추가된 [`alignas`](https://en.cppreference.com/w/cpp/language/alignas) 지정자를 사용하게 됩니다. + +The new structure starts with a `vec2` which is only 8 bytes in size and therefore throws off all of the offsets. Now `model` has an offset of `8`, `view` an offset of `72` and `proj` an offset of `136`, none of which are multiples of 16. To fix this problem we can use the [`alignas`](https://en.cppreference.com/w/cpp/language/alignas) specifier introduced in C++11: + +```c++ +struct UniformBufferObject { + glm::vec2 foo; + alignas(16) glm::mat4 model; + glm::mat4 view; + glm::mat4 proj; +}; +``` + +이제 컴파일하고 다시 실행해 보면 셰이더가 올바를 행렬값을 얻어오는 것을 볼 수 있습니다. GLM을 include하기 직전에 `GLM_FORCE_DEFAULT_ALIGNED_GENTYPES`를 정의할 수 있습니다: + +```c++ +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES +#include +``` + +이렇게 하면 GLM이 정렬 요구사항이 이미 만족된 `vec2`와 `mat4`를 사용하게 됩니다. 이 정의를 추가하면 `alignas` 지정자를 없애도 제대로 동작합니다. + +안타깝게도 이 방법은 중첩된 구조체를 사용하면 통하지 않게 됩니다. C++에서 아래와 같은 정의를 생각해 보세요: + +```c++ +struct Foo { + glm::vec2 v; +}; + +struct UniformBufferObject { + Foo f1; + Foo f2; +}; +``` + +그리고 셰이더에서는 다음과 같이 정의했습니다: + +```c++ +struct Foo { + vec2 v; +}; + +layout(binding = 0) uniform UniformBufferObject { + Foo f1; + Foo f2; +} ubo; +``` + +이 경우 `f2`는 오프셋 `8`을 갖게 되는데 실제로는 중첩된 구조체이기 때문에 `16`을 가져야만 합니다. 이러한 경우엔 정렬을 직접 명시해 주어야 합니다: + +```c++ +struct UniformBufferObject { + Foo f1; + alignas(16) Foo f2; +}; +``` + +교훈은, 정렬을 언제나 명시해 주는 것이 좋다는 겁니다. 그렇게 하면 정렬 오류로 인해 생기는 이상한 문제들을 방지할 수 있습니다. + +```c++ +struct UniformBufferObject { + alignas(16) glm::mat4 model; + alignas(16) glm::mat4 view; + alignas(16) glm::mat4 proj; +}; +``` + +`foo`를 삭제한 뒤 셰이더를 다시 컴파일하는 것을 잊지 마세요. + +## 다중 기술자 집합 + +몇몇 구조체와 함수 호출에서 눈치 채실 수 있듯이, 다중 기술자 집합을 동시에 바인딩 하는 것이 가능합니다. 이 경우 각 기술자 집합에 대해 파이프라인 레이아웃 생성시에 기술자 집합 레이아웃을 생성해야 합니다. 셰이더에서는 특정 기술자 집합을 아래와 같이 참조해야 합니다: + +```c++ +layout(set = 0, binding = 0) uniform UniformBufferObject { ... } +``` + +객체별로 다른 기술자를 사용하거나 별도의 기술자 집합에서 공유하는 기술자를 사용할 때 이러안 기능을 활용할 수 있습니다. 이 경우 드로우 콜마다 대부분의 기술자를 다시 바인딩하지 않아도 되어서 더 효율적일 수 있습니다. + +[C++ code](/code/23_descriptor_sets.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/kr/06_Texture_mapping/00_Images.md b/kr/06_Texture_mapping/00_Images.md new file mode 100644 index 00000000..7f0ae75b --- /dev/null +++ b/kr/06_Texture_mapping/00_Images.md @@ -0,0 +1,568 @@ +## 서론 + +지금까지 물체는 정점별로 할당된 색깔로 표현이 되었지만 이러한 방법으로는 한계가 있습니다. 이 챕터에서는 좀 더 흥미로운 표현을 위해 텍스처 맵핑을 구현해 보도록 하겠습니다. 이를 통해 나중에는 3D 모델을 로딩하고 그리는 것도 가능하게 될겁니다. + +텍스처를 프로그램에 추가기 위해서는 아래와 같은 과정이 필요합니다: + +* 장치 메모리가 베이크(baked)한 이미지 객체 생성 +* 이미지 객체를 이미지 파일의 픽셀로 채움 +* 이미지 샘플러(sampler) 생성 +* 텍스처로부터 색상을 샘플링할 결합된(combined) 이미지 샘플러 기술자 추가 + +전에 이미 이미지 객체를 다뤄본 적 있지만 그 경우는 스왑 체인 확장이 자동적으로 만들어준 경우였습니다. 이번에는 직접 만들어야 합니다. 이미지를 만들고 여기에 데이터를 채우는 것은 정점 버퍼 생성과 비슷합니다. 스테이징 리소스를 먼저 만들고 여기에 픽셀 데이터를 채운 뒤 이를 렌더링에 사용할 최종 이미지 객체에 복사할 것입니다. 이러한 목적으로 스테이징 이미지를 만드는 것도 가능하지만 Vulkan에서는 `VkBuffer`로부터 이미지로 픽셀을 복사하는 것이 가능하고 이러한 목적으로 제공되는 API가 실제로 [어떤 하드웨어에서는 더 빠릅니다](https://developer.nvidia.com/vulkan-memory-management). + +먼저 이 버퍼를 만들고 픽셀 값으로 채운 뒤 그 픽셀값을 복사할 이미지를 만듭니다. 이미지를 만드는 것은 버퍼를 만드는 것과 크게 다르지 않습니다. 전과 같이 메모리 요구조건을 질의하고, 장치 메모리를 할당하고 바인딩하면 됩니다. + +하지만 이미지를 다룰 때 추가적으로 해 주어야 하는 작업이 있습니다. 이미지마다 다른 *레이아웃*을 가질 수 있는데 이는 픽셀들이 메모리에 어떻게 존재하는지에 영향을 미칩니다. 그래픽 하드웨어가 동작하는 방식 때문에 예를들어 픽셀값을 그냥 행별로 나열하는 것은 성능에 좋지 않을 수 있습니다. 이미지에 대해 어떤 연산을 수행할 때 해당 연산에 최적화된 형태의 레이아웃을 가지고 있는지를 확인해야 합니다. 전에 렌더 패스를 명시할 때 이러한 레이아웃을 이미 살펴본 바 있습니다: + +* `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`: 표시 목적으로 최적 +* `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`: 프래그먼트 셰이더에서 색상값을 쓰기 위한 어태치먼트로써 최적 +* `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`: `vkCmdCopyImageToBuffer`에서처럼 전송의 소스로써 최적 +* `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`: `vkCmdCopyBufferToImage`에서처럼 전송의 목적지로써 최적 +* `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`: 셰이더에서 샘플링하는 목적으로 최적 + +이미지 레이아웃을 전송하는 가장 흔한 방법은 *파이프라인 배리어(barrier)* 입니다. 파이프라인 배리어는 리소스로의 동기화된 접근을 위해 주로 사용되는데, 예를 들자면 이미지에 대한 쓰기 이후 읽기를 보장하기 위해서와 같은 목적입니다. 하지만 이 방법은 레이아웃을 전송하기 위해서도 사용될 수 있습니다. 이 챕터에서 파이프라인 배리어를 이러한 목적으로 사용하는 방법을 살펴볼 것입니다. 배리어는 `VK_SHARING_MODE_EXCLUSIVE`일 때 큐 패밀리의 소유권(ownership) 이전을 위해서도 사용됩니다. + +## 이미지 라이브러리 + +이미지를 로드하기 위한 다양한 라이브러리가 있고, BMP나 PPM과 같은 간단한 포맷은 직접 코드를 작성해도 됩니다. 이 튜토리얼에서 우리는 [stb collection](https://github.com/nothings/stb)의 stb_image를 사용할 예정입니다. 이 라이브러리의 장점은 모든 코드가 파일 하나에 있어서 빌드 구성이 간단해진다는 것입니다. `stb_image.h`를 다운로드하여 편리한 위치, 예를들자면 GLFW와 GLM이 있는 위치에 두십시오. 그리고 그 위치를 include 경로에 추가하십시오. + +**Visual Studio** + +`stb_image.h`가 있는 디렉토리를 `추가 포함 디렉토리` 경로에 추가하십시오. + +![](/images/include_dirs_stb.png) + +**Makefile** + +`stb_image.h`가 있는 디텍초리는 GCC의 include 디렉토리에 추가하십시오: + +```text +VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64 +STB_INCLUDE_PATH = /home/user/libraries/stb + +... + +CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_INCLUDE_PATH) +``` + +## 이미지 로딩 + +이미지 라이브러리를 아래와 같이 include합니다: + +```c++ +#define STB_IMAGE_IMPLEMENTATION +#include +``` + +The header only defines the prototypes of the functions by default. One code +file needs to include the header with the `STB_IMAGE_IMPLEMENTATION` definition +to include the function bodies, otherwise we'll get linking errors. + +```c++ +void initVulkan() { + ... + createCommandPool(); + createTextureImage(); + createVertexBuffer(); + ... +} + +... + +void createTextureImage() { + +} +``` + +이미지를 로드하고 Vulkan 이미지 객체에 업로드하기 위한 `createTextureImage` 함수를 새로 만듭니다. 명령 버퍼를 사용할 것이기 때문에 `createCommandPool` 뒤에 호출해야 합니다. + +`shaders` 디렉토리 옆에 이미지를 저장할 `textures` 디렉토리를 새로 만듭니다. `texture.jpg`라는 이미지를 로딩할 예정입니다. 저는 [CC0 라이센스 이미지](https://pixabay.com/en/statue-sculpture-fig-historically-1275469/)를 512 x 512 픽셀로 리사이징하여 사용하기로 했는데 여러분은 원하는 아무 이미지나 사용하십시오. 라이브러리는 JPEG, PNG, BMP, GIF같은 일반적인 이미지 파일 포맷을 지원합니다. + +![](/images/texture.jpg) + +라이브러리를 사용해 이미지를 로딩하는 것은 아주 쉽습니다: + +```c++ +void createTextureImage() { + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + VkDeviceSize imageSize = texWidth * texHeight * 4; + + if (!pixels) { + throw std::runtime_error("failed to load texture image!"); + } +} +``` + +`stbi_load` 함수는 파일 경로와 로드할 채널 개수를 인자로 받습니다. `STBI_rgb_alpha`값은 이미지에 알파 채널이 없어도 알파 채널을 포함하여 로드하도록 되어 있으며 추후 다른 텍스처 포맷을 사용할 때도 일관적인 코드를 사용 가능하므로 좋습니다. 중간의 세 매개변수는 너비, 높이와 이미지의 실제 채널 수의 출력입니다. 반환되는 포인터는 픽셀값 배열의 첫 요소의 포인터입니다. 픽셀들은 각 픽셀별 4바이트로 각 행이 배치되어 있으며 `STBI_rgb_alpha`의 경우 총 `texWidth * texHeight * 4`개의 값이 존재합니다. + +## 스테이징 버퍼 + +이제 호스트에서 관찰 가능한 버퍼를 만들어 `vkMapMemory`를 사용해 픽셀 데이터를 복사할 수 있도록 하겠습니다. 임시 버퍼에 대한 변수를 `createTextureImage` 함수에 만듭니다: + +```c++ +VkBuffer stagingBuffer; +VkDeviceMemory stagingBufferMemory; +``` + +버퍼는 맵핑이 가능하고 전송의 소스로 활용할 수 있도록 호스트에서 관찰 가능한 곳에 있어야 하며, 그래야 나중에 이미지로 복사할 수 있습니다: + +```c++ +createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); +``` + +이제 이미지 로딩 라이브러리부터 얻은 픽셀값을 버퍼로 복사합니다: + +```c++ +void* data; +vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); + memcpy(data, pixels, static_cast(imageSize)); +vkUnmapMemory(device, stagingBufferMemory); +``` + +이 시점에서 원본 픽셀 배열을 정리하는 것을 잊지 마세요: + +```c++ +stbi_image_free(pixels); +``` + +## 텍스처 이미지 + +셰이더에서 버퍼의 픽셀 값을 접근하도록 설정할 수도 있지만 이러한 목적으로는 Vulkan의 이미지 객체를 사용하는 것이 더 좋습니다. 이미지 객체를 사용하는 장점 중 하나는 2D 좌표로 색상값을 얻을 수 있어서 더 빠르고 편리하다는 것입니다. 이미지 객체가 갖고있는 픽셀은 텍셀(texel)이라고 하며 여기서부터는 그렇게 지칭하겠습니다. 아래와 같은 클래스 멤버를 추가합니다: + +```c++ +VkImage textureImage; +VkDeviceMemory textureImageMemory; +``` + +이미지에 대한 매개변수는 `VkImageCreateInfo`에 명시됩니다: + +```c++ +VkImageCreateInfo imageInfo{}; +imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; +imageInfo.imageType = VK_IMAGE_TYPE_2D; +imageInfo.extent.width = static_cast(texWidth); +imageInfo.extent.height = static_cast(texHeight); +imageInfo.extent.depth = 1; +imageInfo.mipLevels = 1; +imageInfo.arrayLayers = 1; +``` + +`imageType` 필드에 명시된 이미지 타입은 Vulkan에게 이미지의 텍셀이 어떤 좌표계를 사용하는지를 알려줍니다. 1D, 2D, 3D 이미지가 있습니다. 1차원 이미지는 데이터의 배열이나 그라디언트를 저장하기 위해 사용되고, 2차원 이미지는 주로 텍스처 용도로, 3차원 이미지는 복셀(voxel) 볼륨을 저장하기 위해 사용됩니다. `extent` 필드는 이미지의 크기를 명시하고, 이는 곧 각 축에 몇 개의 텍셀을 가지고 있는지를 의미합니다. 따라서 `depth`는 `0`이 아닌 `1`이어야 합니다. 우리 텍스처는 배열이 아니며 현재는 밉맵핑도 하지 않을 겁니다. + +```c++ +imageInfo.format = VK_FORMAT_R8G8B8A8_SRGB; +``` + +Vulkan은 다양한 이미지 포맷을 지원하지만 텍셀과 버퍼의 픽셀 포맷으로 같은 포맷을 사용해야 합니다. 그렇지 않으면 복사 연산이 실패하게 됩니다. + +```c++ +imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; +``` + +`tiling` 필드는 다음 두 값중 하나를 가집니다: + +* `VK_IMAGE_TILING_LINEAR`: 우리 `pixels` 배열의 경우처럼 텍셀이 행 우선 순서(row-major order)로 저장됨 +* `VK_IMAGE_TILING_OPTIMAL`: 텍셀이 최적 접근을 위해 구현에서 정의한 순서대로 저장됨 + +이미지의 레이아웃과는 달리 타일링 모드는 나중에 바꿀 수 없습니다. 이미지 메모리의 텍셀에 직접 접근하고 싶다면 `VK_IMAGE_TILING_LINEAR`를 사용해야 합니다. 우리의 경우 스테이징 이미지가 아닌 스테이징 버퍼를 사용하고 있으므로 이렇게 할 필요는 없습니다. 셰이더에서 효율적인 접근이 가능하도록 `VK_IMAGE_TILING_OPTIMAL`를 사용할겁니다. + +```c++ +imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; +``` + +이미지의 `initialLayout`는 두 가지 값이 가능합니다: + +* `VK_IMAGE_LAYOUT_UNDEFINED`: GPU에서 사용이 불가능하고 첫 전환(transition) 이후 텍셀이 버려짐 +* `VK_IMAGE_LAYOUT_PREINITIALIZED`: GPU에서 사용이 불가능하고 첫 전환 이후 텍셀이 유지됨 + +첫 전환 이후에 텍셀이 유지되어야 하는 경우는 별로 없습니다. 유지되어야 하는 한 예로 `VK_IMAGE_TILING_LINEAR`와 함께 이미지를 스테이징 이미지로 활용하는 경우가 있습니다. 이 경우 텍셀 데이터를 업로드한 이후에 이미지를 전송의 소스로 전환하며, 데이터를 버리지 않습니다. 하지만 우리의 경우 먼저 이미지를 전송의 목적지로 전환한 후 버퍼 객체로부터 텍셀 데이터를 복사하기 때문에 이러한 속성이 필요없고 따라서 `VK_IMAGE_LAYOUT_UNDEFINED`를 사용해도 됩니다. + +```c++ +imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; +``` + +`usage` 필드는 버퍼 생성과 동일한 의미를 갖습니다. 이미지는 버퍼 복사의 목적지로 활용될 것입니다. 또한 이미지는 셰이더에서 메쉬의 색상을 결정하기 위해 활용될 예정이므로 사용법에는 `VK_IMAGE_USAGE_SAMPLED_BIT`가 포함되어야 합니다. + +```c++ +imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; +``` + +이미지는 하나의 큐 패밀리에서만 활용될 예정입니다. 전송 연산이 가능한 그래픽스 큐 패밀리입니다. + +```c++ +imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; +imageInfo.flags = 0; // Optional +``` + +`samples` 플래그는 멀티샘플링과 관련되어 있습니다. 이는 이미지가 어태치먼트로 활용될때만 의미가 있으므로 샘플은 1로 둡니다. 희박한(sparse) 이미지의 경우에 대한 선택적인 플래그들이 몇 가지 있습니다. 희박한 이미지란 특정 영역만이 베이킹되는 이미지입니다. 예를 들어 복셀 지형을 위해 3D 텍스처를 사용한다고 하면 아무 것도 없는 영역에 대한 메모리 할당을 피하기 위해서 이러한 이미지를 사용할 수 있습니다. 이 튜토리얼에서는 이러한 사용 사례가 없으므로 기본값인 `0`으로 두겠습니다. + +```c++ +if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); +} +``` + +이미지는 `vkCreateImage`로 만들어지고 특별히 언급할만한 매개변수는 없습니다. `VK_FORMAT_R8G8B8A8_SRGB` 포맷을 하드웨어가 지원하지 않는 경우가 있을 수 있습니다. 가능한 대안들의 목록을 가지고 있다가 지원되는 가장 괜찮은 것을 사용해야 합니다. 하지만 이 포맷은 널리 지원되므로 이러한 처리 과정을 지금은 넘어가겠습니다. 다른 포맷을 사용하려면 좀 귀찮은 변환 과정을 수행해야 합니다. 깊이 버퍼 챕터에서 이러한 시스템을 구현하면서 다시 살펴볼 것입니다. + +```c++ +VkMemoryRequirements memRequirements; +vkGetImageMemoryRequirements(device, textureImage, &memRequirements); + +VkMemoryAllocateInfo allocInfo{}; +allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; +allocInfo.allocationSize = memRequirements.size; +allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT); + +if (vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate image memory!"); +} + +vkBindImageMemory(device, textureImage, textureImageMemory, 0); +``` + +이미지를 위한 메모리 할당은 버퍼 메모리 할당과 완전히 동일합니다. `vkGetBufferMemoryRequirements` 대신에 `vkGetImageMemoryRequirements`를 사용하고, `vkBindBufferMemory` 대신에 `vkBindImageMemory`를 사용합니다. + +함수가 꽤 커졌고, 나중 챕터에서는 이미지를 더 만들어야 하기 때문에 이미지 생성은 버퍼에서처럼 `createImage` 함수로 추상화해야 합니다. 함수를 만들고 이미지 객체의 생성과 메모리 할당을 이 곳으로 옮깁니다: + +```c++ +void createImage(uint32_t width, uint32_t height, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { + VkImageCreateInfo imageInfo{}; + imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + imageInfo.imageType = VK_IMAGE_TYPE_2D; + imageInfo.extent.width = width; + imageInfo.extent.height = height; + imageInfo.extent.depth = 1; + imageInfo.mipLevels = 1; + imageInfo.arrayLayers = 1; + imageInfo.format = format; + imageInfo.tiling = tiling; + imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + imageInfo.usage = usage; + imageInfo.samples = VK_SAMPLE_COUNT_1_BIT; + imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + if (vkCreateImage(device, &imageInfo, nullptr, &image) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); + } + + VkMemoryRequirements memRequirements; + vkGetImageMemoryRequirements(device, image, &memRequirements); + + VkMemoryAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; + allocInfo.allocationSize = memRequirements.size; + allocInfo.memoryTypeIndex = findMemoryType(memRequirements.memoryTypeBits, properties); + + if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) { + throw std::runtime_error("failed to allocate image memory!"); + } + + vkBindImageMemory(device, image, imageMemory, 0); +} +``` + +너비, 높이, 포맷, 타일링 모드, 사용법, 메모리 속성을 매개변수로 만들었는데 이것들은 앞으로 튜토리얼에서 만들 이미지마다 다르기 때문입니다. + +`createTextureImage` 함수는 이제 아래와 같이 간략화됩니다: + +```c++ +void createTextureImage() { + int texWidth, texHeight, texChannels; + stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); + VkDeviceSize imageSize = texWidth * texHeight * 4; + + if (!pixels) { + throw std::runtime_error("failed to load texture image!"); + } + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data); + memcpy(data, pixels, static_cast(imageSize)); + vkUnmapMemory(device, stagingBufferMemory); + + stbi_image_free(pixels); + + createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory); +} +``` + +## 레이아웃 전환(transitions) + +이제 작성할 함수는 또한번 명령 버퍼를 기록하고 실행하는 부분이며 이에 따라 이러한 로직은 한두개의 헬퍼 함수로 옮기는 것이 좋겠습니다: + +```c++ +VkCommandBuffer beginSingleTimeCommands() { + VkCommandBufferAllocateInfo allocInfo{}; + allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; + allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; + allocInfo.commandPool = commandPool; + allocInfo.commandBufferCount = 1; + + VkCommandBuffer commandBuffer; + vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer); + + VkCommandBufferBeginInfo beginInfo{}; + beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; + + vkBeginCommandBuffer(commandBuffer, &beginInfo); + + return commandBuffer; +} + +void endSingleTimeCommands(VkCommandBuffer commandBuffer) { + vkEndCommandBuffer(commandBuffer); + + VkSubmitInfo submitInfo{}; + submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + submitInfo.commandBufferCount = 1; + submitInfo.pCommandBuffers = &commandBuffer; + + vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE); + vkQueueWaitIdle(graphicsQueue); + + vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer); +} +``` + +이 코드는 기존의 `copyBuffer`에 있던 코드에 기반해 만들어졌습니다. 이제 해당 함수는 아래와 같아집니다: + +```c++ +void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + VkBufferCopy copyRegion{}; + copyRegion.size = size; + vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, ©Region); + + endSingleTimeCommands(commandBuffer); +} +``` + +아직 버퍼를 사용 중이라면 `vkCmdCopyBufferToImage`를 기록하고 실행하는 함수를 만들어 작업을 완료할 수도 있습니다. 하지만 이 명령은 먼저 이미지가 올바른 레이아웃에 있는 것을 요구합니다. 레이아웃 전환을 위한 함수를 새로 만듭니다: + +```c++ +void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + endSingleTimeCommands(commandBuffer); +} +``` + +레이아웃 전환을 위한 가장 일반적인 방법은 *이미지 메모리 배리어*를 사용하는 것입니다. 이와 같은 파이프라인 배리어는 리소스에 대한 접근을 동기화하기 위해 사용되는데 예를 들자면 버퍼에 값을 쓰는 것이 읽기 전에 끝나야 하는 것을 보장하기 위해서와 같은 것입니다. 하지만 또한 이미지 레이아웃을 전환하고 `VK_SHARING_MODE_EXCLUSIVE`가 사용될 때 큐 패밀리 소유권을 이전하는 데에도 사용됩니다. 버퍼에 대해서는 대응되는 *버퍼 메모리 배리어*라는 것이 존재합니다. + +```c++ +VkImageMemoryBarrier barrier{}; +barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; +barrier.oldLayout = oldLayout; +barrier.newLayout = newLayout; +``` + +첫 두 필드는 레이아웃 전환을 명시합니다. 이미지에 존재하는 내용을 상관하지 않는다면 `oldLayout`에는 `VK_IMAGE_LAYOUT_UNDEFINED`를 사용할 수도 있습니다. + +```c++ +barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; +``` + +큐 패밀리 소유권 이전을 위해 배리어를 사용한다면, 이 두 필드는 큐 패밀리의 인덱스여야 합니다. 그렇지 않은 경우에는 `VK_QUEUE_FAMILY_IGNORED`로 설정해야 합니다 (이 값이 기본값이 아닙니다!). + +```c++ +barrier.image = image; +barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +barrier.subresourceRange.baseMipLevel = 0; +barrier.subresourceRange.levelCount = 1; +barrier.subresourceRange.baseArrayLayer = 0; +barrier.subresourceRange.layerCount = 1; +``` + +`image`와 `subresourceRange`는 영향을 받는 이미지와 이미지의 특정 영역을 명시합니다. 우리 이미지는 배열도 아니고 밉맵 레벨도 없으므로 1 레벨과 하나의 레이어로 명시합니다. + +```c++ +barrier.srcAccessMask = 0; // TODO +barrier.dstAccessMask = 0; // TODO +``` + +배리어의 주 목적은 동기화이므로 리소스와 관련한 어떤 종류의 연산이 배리어 앞에 오고 어떤 연산이 배리어를 대기해야 하는시를 명시해야 합니다. 이미 `vkQueueWaitIdle`를 사용해 매뉴얼하게 동기화 하고 있지만 그래도 해 주어야 합니다. 올바른 값은 old와 new 레이아웃에 달려 있으며 어떤 전환할 수행할 것인지를 알게 된 후에 다시 돌아오겠습니다. + +```c++ +vkCmdPipelineBarrier( + commandBuffer, + 0 /* TODO */, 0 /* TODO */, + 0, + 0, nullptr, + 0, nullptr, + 1, &barrier +); +``` + +모든 파이프라인 배리어는 같은 함수로 제출됩니다. 명령 버퍼 다음으로 오는 첫 매개변수는 배리어 앞에 수행되어야 할 연산의 파이프라인 스테이지를 명시합니다. 두 번째 매개변수는 배리어를 대기할 파이프라인 스테이지를 명시합니다. 배리어 앞과 뒤에 명시할 수 있는 파이프라인의 스테이지는 배리어 전후에 리소스를 어떻게 사용할 것인지에 달려 있습니다. 가능한 값의 목록은 명세의 [이 표](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#synchronization-access-types-supported)에 있습니다. For example, if you're going to read from a uniform after +the barrier, you would specify a usage of `VK_ACCESS_UNIFORM_READ_BIT` and the +earliest shader that will read from the uniform as pipeline stage, for example +`VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT`. 셰이더가 아닌 파이프라인 스테이지에 이러한 사용법을 명시한다면 검증 레이어가 용법에 맞지 않는 파이프라인 스테이지를 명시했다고 경고를 낼 것입니다. + +세 번째 매개변수는 `0` 또는 `VK_DEPENDENCY_BY_REGION_BIT`입니다. 후자는 배리어를 영역별 조건으로 바꿉니다. 즉, 예를 들자면 구현이 현재까지 쓰기가 완료된 리소스의 일부분을 읽을 수 있게 됩니다. + +마지막 세 개의 매개변수는 세 종류의 타입에 대한 파이프라인 배리어의 배열에 대한 참조입니다. 세 종류 타입은 메모리 배리어, 버퍼 메모리 배리어, 이미지 메모리 배리어이고 현재는 마지막 것을 사용입니다. `VkFormat` 매개변수는 아직 사용하지 않는 것에 유의하세요. 이는 깊이 버퍼 챕터에서 특수한 전환을 위해 사용할 예정입니다. + +## Copying buffer to image + +`createTextureImage`로 다시 돌아가기 전에 추가적인 헬퍼 함수 `copyBufferToImage`를 작성하겠습니다: + +```c++ +void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width, uint32_t height) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + endSingleTimeCommands(commandBuffer); +} +``` + +버퍼의 복사와 마찬가지로 버퍼의 어떤 부분이 이미지의 어떤 부분으로 복사될 것인지를 명시해야 합니다. 이는 `VkBufferImageCopy` 구조체를 통해 명시됩니다: + +```c++ +VkBufferImageCopy region{}; +region.bufferOffset = 0; +region.bufferRowLength = 0; +region.bufferImageHeight = 0; + +region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +region.imageSubresource.mipLevel = 0; +region.imageSubresource.baseArrayLayer = 0; +region.imageSubresource.layerCount = 1; + +region.imageOffset = {0, 0, 0}; +region.imageExtent = { + width, + height, + 1 +}; +``` + +대부분의 필드는 직관적입니다. `bufferOffset`은 픽셀 값이 시작하는 버퍼의 바이트 단위 오프셋입니다. `bufferRowLength`와 `bufferImageHeight` 필드는 픽셀이 메모리에 어떻게 배치되어있는지를 명시합니다. 예를 들어 각 행에 패딩(padding) 바이트가 있을 수 있습니다. 둘 다 `0`으로 명시하였다는 의미는 패딩 없이 연속적으로 데이터가 존재한다는 뜻입니다. `imageSubresource`, `imageOffset`, `imageExtent`는 픽셀이 복사될 이미지의 영역을 명시합니다. + +버퍼에서 메모리로의 복사 연산은 `vkCmdCopyBufferToImage` 함수를 통해 큐에 등록됩니다: + +```c++ +vkCmdCopyBufferToImage( + commandBuffer, + buffer, + image, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, + ®ion +); +``` + +네 번째 매개변수는 현재 이미지가 사용하고 있는 레이아웃을 명시합니다. 여기서 저는 이미지가 이미 픽셀을 복사하기에 최적화된 레이아웃으로 전환되었다고 가정하고 있습니다. 지금은 픽셀 값 덩어리를 전체 이미지에 복사하고 있지만 `VkBufferImageCopy`의 배열을 명시해서 버퍼로부터의 서로 다른 복사 연산들을 한 번에 수행할 수도 있습니다. + +## 텍스처 이미지 준비 + +이제 텍스처 이미지를 사용하기 위해 필요한 모든 도구가 준비되었으니 `createTextureImage` 함수로 다시 돌아갑시다. 여기서 마지막에 했던 것은 텍스처 이미지를 만든 것이었습니다. 그 다음 단계로 스테이징 버퍼를 텍스처 이미지로 복사해야 합니다. 여기에는 두 단계가 필요합니다: + +* 텍스처 이미지를 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`로 전환 +* 버퍼에서 이미지로의 복사 연산 실행 + +방금 만든 함수들을 사용하면 쉽게 수행할 수 있습니다: + +```c++ +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL); +copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); +``` + +이미지는 `VK_IMAGE_LAYOUT_UNDEFINED` 레이아웃으로 생성되었으므로 `textureImage`로 전환될 떄 기존(old) 레이아웃으로 명시되어야 합니다. 이것이 가능한 이유는 복사 연산을 수행하기 전, 기존에 쓰여있던 내용을 신경쓰지 않기 떄문에 가능한 것이라는 것을 기억하십시오. + +셰이더에서 텍스처 이미지를 샘플링하려면 마지막으로 셰이더에서 접근이 가능하도록 한번 더 전환이 필요합니다: + +```c++ +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); +``` + +## 전환 배리어 마스크(mask) + +지금 시점에 검증 레이어가 켜진 상태에서 프로그램을 실행하면 `transitionImageLayout`의 접근 마스크와 파이프라인 스테이지가 유효하지 않다는 오류를 보실 수 있습니다. 전환의 레이아웃에서 이들을 설정해 줘야 합니다. + +처리해야 할 전환이 두 가지 있습니다: + +* Undefined → transfer destination: 전송은 무언가를 기다릴 필요 없이 쓰기를 수행 +* Transfer destination → shader reading: 셰이더 읽기는 전송의 쓰기를 기다려야 하며, 정확히는 프래그먼트 셰이더의 읽기 연산임. 왜냐하면 이 시점이 텍스터를 사용하는 시점이므로 + +이러한 규칙들은 다음와 같은 접근 마스크와 파이프라인 스테이지로 명시됩니다: + +```c++ +VkPipelineStageFlags sourceStage; +VkPipelineStageFlags destinationStage; + +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 { + throw std::invalid_argument("unsupported layout transition!"); +} + +vkCmdPipelineBarrier( + commandBuffer, + sourceStage, destinationStage, + 0, + 0, nullptr, + 0, nullptr, + 1, &barrier +); +``` + +이전 표에서처럼 전송 쓰기는 파이프라인 전환 단계에서 수행되어야 합니다. 쓰기 연산이 무언가를 기다릴 필요는 없으므로 빈 접근 마스크를 명시하고 파이프라인의 가장 첫 단계인 `VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT`를 배리어 전 연산으로 지정해야 합니다. 중요한 것은 `VK_PIPELINE_STAGE_TRANSFER_BIT`은 *실제* 그래픽스나 컴퓨트 파이프라인의 스테이지가 아니라는 점입니다. 전송이 일어나는 의사(pseudo)-스테이지에 가깝습니다. 의사 스테이지의 예시들에 대해서는 [이 문서](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap7.html#VkPipelineStageFlagBits)를 살펴보세요. + +이미지는 같은 파이프라인 스테이지에서 쓰여지고 프래그먼트 셰이더에서 읽게 되므로 프래그먼트 셰이더 파이프라인 스테이지에 셰이더 읽기 접근을 명시하였습니다. + +나중에 추가적인 전환을 수행해야 한다면 그 때 함수를 확장할 것입니다. 이제 프로그램은 올바로 동작하고, 보이는 장면은 이전과 동일합니다. + +하나 언급하고 싶은 것은 초반부의 암시적 `VK_ACCESS_HOST_WRITE_BIT` 동기화에서의 명령 버퍼 제출 입니다. `transitionImageLayout` 함수는 명령이 하나만 존재하는 명령 버터를 실행하므로, 암시적 동기화를 수행하고 `srcAccessMask`를 `0`으로 설정할 수 있습니다. 이는 레이아웃 전환에서 `VK_ACCESS_HOST_WRITE_BIT` 의존성이 필요한 경우에 입니다. 이를 명시적으로 할지 아닐지는 여러분들에게 달려 있지만 저는 개인적으로 이러한 OpenGL 스타일의 "숨겨진" 연산이 있는 것을 좋아하지는 않습니다. + +사실 모든 연산을 지원하는 특별한 이미지 레이아웃인 `VK_IMAGE_LAYOUT_GENERAL`가 있습니다. 이것의 문제는 당연하지만 어떤 연산에 대해서 최선의 성능을 보장하지 않는다는 것입니다. 이것은 이미지를 입력과 출력에 동시에 사용하거나 사전 초기화가 끝난 이후에 이미지를 읽어온다거나 하는 등의 특정한 케이스에서는 필요할 수 있습니다. + +지금까지 명령을 제출한 헬퍼 함수들은 큐가 아이들 상태가 될때까지 대기하여 명령을 동기적으로 수행하도록 설정되었습니다. 실제 응용 프로그램에서는 이러한 연산들을 하나의 명령 버퍼에 통합하여 비동기적으로 실행하여 높은 쓰루풋(throughput)을 달성하는 것이 권장됩니다. 특히 `createTextureImage` 함수의 전환과 복사 연산에 대해서는요. 헬퍼 함수가 명령을 입력할 `setupCommandBuffer`를 만들고 `flushSetupCommands`를 추가하여 지금까지 기록된 명령을 실행하게 해보세요. 텍스처 맵핑 이후에 이러한 작업을 시도하여 텍스처 리소스가 문제 없이 설정되는지 확인해 보시는 것이 가장 좋을 것 같습니다. + +## 정리 + +스테이징 버퍼와 그 메모리를 마지막에 정리하는 것으로 `createTextureImage` 함수를 마무리 합시다: + +```c++ + transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + + vkDestroyBuffer(device, stagingBuffer, nullptr); + vkFreeMemory(device, stagingBufferMemory, nullptr); +} +``` + +메인 텍스터 이미지는 프로그램 종료 시까지 사용됩니다: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyImage(device, textureImage, nullptr); + vkFreeMemory(device, textureImageMemory, nullptr); + + ... +} +``` + +이미지는 이제 텍스처를 포함하지만 그래픽스 파이프라인에서 접근이 가능하게 할 방법이 필요합니다. 다음 챕터에서 진행해 보겠습니다. + +[C++ code](/code/24_texture_image.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/kr/06_Texture_mapping/01_Image_view_and_sampler.md b/kr/06_Texture_mapping/01_Image_view_and_sampler.md new file mode 100644 index 00000000..db6d4c47 --- /dev/null +++ b/kr/06_Texture_mapping/01_Image_view_and_sampler.md @@ -0,0 +1,292 @@ +이 챕터에서는 그래픽스 파이프라인에서 이미지를 샘플링하기 위해 필요한 리소스 두 개를 더 만들어 보겠습니다. 첫 번째 리소스는 스왑 체인 이미지를 다루면서 이미 살펴본 것이지만 두 번째는 새로운 것입니다. 셰이더에서 이미지로브터 텍셀을 어떻게 읽는 방법에 관한 리소스입니다. + +## 텍스처 이미지 뷰 + +전에 본 스왑 체인 이미지와 프레임버퍼에서, 이미지는 직접 접근되는 것이 아니라 이미지 뷰를 통해 접근하였습니다. 텍스처 이미지에 관해서도 이러한 이미지 뷰가 필요합니다. + +텍스터 이미지의 `VkImageView`를 위한 클래스 멤버를 추가하고 이를 생성할 `createTextureImageView` 함수를 새로 추가합니다: + +```c++ +VkImageView textureImageView; + +... + +void initVulkan() { + ... + createTextureImage(); + createTextureImageView(); + createVertexBuffer(); + ... +} + +... + +void createTextureImageView() { + +} +``` + +이 함수의 코드는 `createImageViews`에 기반합니다. 두 가지 변경해야 할 것은 `format`과 `image` 입니다: + +```c++ +VkImageViewCreateInfo viewInfo{}; +viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; +viewInfo.image = textureImage; +viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; +viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB; +viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +viewInfo.subresourceRange.baseMipLevel = 0; +viewInfo.subresourceRange.levelCount = 1; +viewInfo.subresourceRange.baseArrayLayer = 0; +viewInfo.subresourceRange.layerCount = 1; +``` + +`viewInfo.components`에 관한 명시적 초기화는 제외하였는데 `VK_COMPONENT_SWIZZLE_IDENTITY`는 어차피 `0`으로 정의되어 있기 떄문입니다. `vkCreateImageView`를 호출함으로써 이미지 뷰 생성을 마칩니다: + +```c++ +if (vkCreateImageView(device, &viewInfo, nullptr, &textureImageView) != VK_SUCCESS) { + throw std::runtime_error("failed to create texture image view!"); +} +``` + +`createImageViews`와 대부분의 로직이 동일하기 때문에 `createImageView` 함수를 새로 추상화 하는것이 좋겠습니다: + +```c++ +VkImageView createImageView(VkImage image, VkFormat format) { + VkImageViewCreateInfo viewInfo{}; + viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; + viewInfo.image = image; + viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; + viewInfo.format = format; + viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + viewInfo.subresourceRange.baseMipLevel = 0; + viewInfo.subresourceRange.levelCount = 1; + viewInfo.subresourceRange.baseArrayLayer = 0; + viewInfo.subresourceRange.layerCount = 1; + + VkImageView imageView; + if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) { + throw std::runtime_error("failed to create image view!"); + } + + return imageView; +} +``` + +`createTextureImageView` 함수는 이제 아래와 같이 간단해집니다: + +```c++ +void createTextureImageView() { + textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB); +} +``` + +그리고 `createImageViews`는 아래와 같이 간단해집니다: + +```c++ +void createImageViews() { + swapChainImageViews.resize(swapChainImages.size()); + + for (uint32_t i = 0; i < swapChainImages.size(); i++) { + swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat); + } +} +``` + +프로그램 종료 시점에 이미지 뷰를 소멸하는 것을 잊지 마십시오. 이미지 자체를 소멸하기 직전에 이러한 작업을 수행합니다. + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroyImageView(device, textureImageView, nullptr); + + vkDestroyImage(device, textureImage, nullptr); + vkFreeMemory(device, textureImageMemory, nullptr); +``` + +## 샘플러(Samplers) + +셰이더가 이미지로부터 텍셀을 직접 읽는 것도 가능하지만 텍스처에 대해서 이렇게 하는 것은 일반적이지 않습니다. 텍스처는 대개 샘플러를 통해 접근되는데, 이는 추출할 최종 색상을 계산하기 위해 필터링과 변환을 수행합니다. + +이 필터들은 오버샘플링(oversampling) 같은 문제를 다루는 데 유용합니다. 텍셀보다 프래그먼트가 많은 물체게 대해 텍스처 맵핑이 수행된다고 생각해 보십시오. 각 프래그먼트의 텍스처 좌표에 대해 단순히 가장 가까운 텍셀을 사용하면 첫 번째 이미지와 같은 결과를 얻게 될겁니다: + +![](/images/texture_filtering.png) + +네 개의 가장 가까운 텍셀을 선형 보간(linear interpolation)하면 오른쪽과 같이 좀 더 부드러운 결과를 얻을 수 있습니다. 물론 여러분 응용 프로그램의 아트 스타일이 왼쪽의 경우와 더 잘 어울릴 수도 있습니다(마인크래프트 같은 경우). 하지만 일반적인 그래픽스 응용 프로그램에서는 오른쪽의 경우가 더 선호됩니다. 샘플러 객체는 텍스처로부터 색상을 읽을 때 이러한 필터링을 자동으로 수행해 줍니다. + +언더샘플링(undersampling)은 반대의 경우로, 프래그먼트보다 텍셀이 더 많은 경우입니다. 이러한 경우 체커보드(checkerboard) 텍스처와 같은 고주파(high frequency) 패턴을 비스듬히 바라볼 때 문제가 생깁니다: + +![](/images/anisotropic_filtering.png) + +왼쪽 이미지에서 볼 수 있듯이 먼 곳의 텍스처는 흐릿하게 뭉개집니다. 이러한 문제의 해결 방안은 [비등방성(anisotropic) 필터링](https://en.wikipedia.org/wiki/Anisotropic_filtering)으로, 역시나 샘플러를 통해 자동적으로 적용될 수 있습니다. + +이러한 필터 이외에도 샘플러는 변환도 처리해 줍니다. 여러분의 이미지 범위 밖의 텍셀을 읽을 떄 어떻게 처리할지도 *어드레싱 모드(addressing mode)*를 기반으로 결정합니다. 아래 이미지는 몇 가지 가능성을 보여줍니다: + +![](/images/texture_addressing.png) + +이제 `createTextureSampler` 함수를 만들어 이러한 샘플러 객체를 설정해 봅시다. 나중에 셰이더에서 이러한 샘플러를 활용해 텍스처로부터 색상을 읽어올 것입니다. + +```c++ +void initVulkan() { + ... + createTextureImage(); + createTextureImageView(); + createTextureSampler(); + ... +} + +... + +void createTextureSampler() { + +} +``` + +샘플러는 `VkSamplerCreateInfo` 구조체를 통해 설정되는데, 적용되어야 할 필터와 변환들을 명시합니다. + +```c++ +VkSamplerCreateInfo samplerInfo{}; +samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO; +samplerInfo.magFilter = VK_FILTER_LINEAR; +samplerInfo.minFilter = VK_FILTER_LINEAR; +``` + +`magFilter`와 `minFilter` 필드는 확대되거나 축소되는 텍스처를 어떻게 보간할 것인지를 명시합니다. 확대(magnification)는 위에서 설명한 오버샘플링 문제를 처리하는 방법이고 축소(minification)는 언더샘플링에 대한 방법입니다. 우리의 선택은 `VK_FILTER_NEAREST`와 `VK_FILTER_NEAREST`인데, 위 이미지에서 예시로 보여드린 방법에 대응되는 옵션입니다. + +```c++ +samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT; +samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT; +samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT; +``` + +어드레싱 모드는 축(axis)별로 `addressMode` 필터를 통해 명시됩니다. 가능한 값들은 아래와 같습니다. 위 이미지에서 거의 모든 경우의 예시를 보여드렸습니다. 축들은 X,Y,Z가 아닌 U,V,W로 명시된다는 점을 주의하십시오. 이것이 텍스처 공간 좌표를 표현하는 일반적인 표기법입니다. + +* `VK_SAMPLER_ADDRESS_MODE_REPEAT`: 이미지 범위 밖을 벗어날 경우 반복 +* `VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT`: 반복과 유사하지만 범위 밖을 벗어날 경우 좌표를 뒤집어 이미지가 거울상(mirror)이 되도록 함 +* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE`: 범위 밖을 벗어날 경우 가장 가까운 축의 모서리(edge) 색상을 사용함 +* `VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE`: 위 경우와 같지만 가장 가까운 모서리의 반대쪽 모서리를 사용 +* `VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER`: 범위 밖을 샘플링할 경우 단색(solid color)값을 반환함 + +여기서는 어떤 어드레싱 모드를 사용하건 관계 없습니다. 이 튜토리얼에서는 이미지 범위 밖에서는 샘플링을 하는 경우가 없기 떄문입니다. 하지만 반복(repeat) 모드가 가장 일반적인데 이 모드가 벽이나 바닥 같은 타일 텍스처에 가장 적합하기 때문입니다. + +```c++ +samplerInfo.anisotropyEnable = VK_TRUE; +samplerInfo.maxAnisotropy = ???; +``` + +이 두 필드는 비등방성 필터링을 사용할 것인지를 명시합니다. 성능에 문제가 없다면 이 기능을 사용하지 않은 이유가 없습니다. `maxAnisotropy` 필드는 최종 색성을 계산할 때 사용되는 텍셀 샘플의 수에 대한 제한값입니다. 값이 작으면 성능이 높지만 품질이 떨어집니다. 어떤 값을 사용할지를 알아내기 위해 물리적 장치의 속성을 얻어와야 합니다: + +```c++ +VkPhysicalDeviceProperties properties{}; +vkGetPhysicalDeviceProperties(physicalDevice, &properties); +``` + +`VkPhysicalDeviceProperties` 구조체의 문서를 보시면 `limit`라고 이름지어진 `VkPhysicalDeviceLimits` 멤버를 보실 수 있습니다. 이 구조체는 `maxSamplerAnisotropy` 멤버를 가지고 있고 이것이 우리가 `maxAnisotropy`에 사용할 수 있는 최대값입니다. 가장 좋은 품질을 원한다면 그 값을 바로 사용하면 됩니다: + +```c++ +samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy; +``` + +프로그램의 시작 시점에 이 속성을 질의하고 값이 필요한 곳에 넘겨줄 수도 있습니다. 아니면 `createTextureSampler` 함수 내에서 질의하는 방법도 있습니다. + +```c++ +samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK; +``` + +`borderColor` 필드는 clamp to border 어드레싱 모드일 때, 범위 밖을 샘플링하는 경우 반환할 색상 값을 명시합니다. 검은색, 흰색, 또는 투명색을 float이나 int 포맷으로 반환할 수 있습니다. 임의의 색상을 명시하는 것은 불가능합니다. + +```c++ +samplerInfo.unnormalizedCoordinates = VK_FALSE; +``` + +`unnormalizedCoordinates` 필드는 이미지의 텍셀에 접근할 떄 어떤 좌표계를 사용할 지 명시합니다. `VK_TRUE`인 경우 `[0, texWidth)`와 `[0, texHeight)` 범위의 좌표를 사용하면 됩니다. `VK_FALSE`인 경우엔 텍셀은 모든 축에 대해 `[0,1)`로 접근합니다. 실제 응용 프로그램에서는 거의 항상 정규화된(normalized) 좌표계를 사용하는데, 이렇게 하면 다양한 해상도의 텍스처에 대해서도 동일한 좌표를 사용할 수 있기 때문입니다. + +```c++ +samplerInfo.compareEnable = VK_FALSE; +samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS; +``` + +비교(comparison) 함수가 활성화되면 텍셀은 먼저 값과 비교된 뒤에 그 비교 결과가 필터링 연산에 사용됩니다. 이는 주로 그림자 맵핑에서 [percentage-closer filtering](https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch11.html)에 사용됩니다. 이에 대해서는 나중 챕터에서 살펴보겠습니다. + +```c++ +samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; +samplerInfo.mipLodBias = 0.0f; +samplerInfo.minLod = 0.0f; +samplerInfo.maxLod = 0.0f; +``` + +이 필드들은 모두 밉맵핑에 적용됩니다. 밉맵핑에 대해서는 [나중 챕터](/Generating_Mipmaps)에서 살펴볼 것이고, 적용될 수 있는 또 다른 종류의 필터입니다. + +이제 샘플러를 위한 기능이 모두 정의되었습니다. 샘플러 객체의 핸들을 저장할 클래스 멤버를 추가하고 `vkCreateSampler`를 사용해 샘플러를 생성합니다: + +```c++ +VkImageView textureImageView; +VkSampler textureSampler; + +... + +void createTextureSampler() { + ... + + if (vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) { + throw std::runtime_error("failed to create texture sampler!"); + } +} +``` + +샘플러가 `VkImage`를 참조하지 않는다는 것을 주목하십시오. 샘플러는 텍스처에서 색상을 추출하는 인터페이스를 제공하는 별도의 객체입니다. 이는 1D, 2D, 3D 등 원하는 어떤 이미지에도 적용될 수 있습니다. 이것이 다른 오래된 API들과는 다른 점인데, 그것들의 경우 텍스처 이미지와 필터링을 하나의 상태로 결합합니다. + +프로그램의 종료 시점, 더 이상 이미지에 접근할 필요가 없어지는 시점에 샘플러를 소멸시킵니다: + +```c++ +void cleanup() { + cleanupSwapChain(); + + vkDestroySampler(device, textureSampler, nullptr); + vkDestroyImageView(device, textureImageView, nullptr); + + ... +} +``` + +## 비등방성 장치 기능 + +지금 시점에서 프로그램을 실행하면 아래와 같은 검증 레이어 메시지를 보게 됩니다: + +![](/images/validation_layer_anisotropy.png) + +사실 비등방성 필터링은 장치의 선택적인 기능입니다. 따라서 그 기능을 요청하기 위해서는 `createLogicalDevice` 함수를 수정해야 합니다: + +```c++ +VkPhysicalDeviceFeatures deviceFeatures{}; +deviceFeatures.samplerAnisotropy = VK_TRUE; +``` + +최근의 그래픽 카드가 이를 지원하지 않은 가능성은 매우 낮지만, 그래도 `isDeviceSuitable`에서 이를 확인하도록 합니다: + +```c++ +bool isDeviceSuitable(VkPhysicalDevice device) { + ... + + VkPhysicalDeviceFeatures supportedFeatures; + vkGetPhysicalDeviceFeatures(device, &supportedFeatures); + + return indices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy; +} +``` + +불리언 값으로 기능을 요청하는 대신, `vkGetPhysicalDeviceFeatures`를 사용해 `VkPhysicalDeviceFeatures` 구조체를 변경함으로써 어떤 기능이 지원되는지를 표시하도록 할 수 있습니다. + +비등방성 필터링의 가용성을 강제하는 대신, 아래와 같이 그 기능을 사용하지 않도록 할 수도 있습니다: + +```c++ +samplerInfo.anisotropyEnable = VK_FALSE; +samplerInfo.maxAnisotropy = 1.0f; +``` + +다음 챕터에서는 이미지와 샘플러 객체를 셰이더에 노출하여 사각형에 텍스처를 입혀 보도록 하겠습니다. + +[C++ code](/code/25_sampler.cpp) / +[Vertex shader](/code/22_shader_ubo.vert) / +[Fragment shader](/code/22_shader_ubo.frag) diff --git a/kr/06_Texture_mapping/02_Combined_image_sampler.md b/kr/06_Texture_mapping/02_Combined_image_sampler.md new file mode 100644 index 00000000..82ec8282 --- /dev/null +++ b/kr/06_Texture_mapping/02_Combined_image_sampler.md @@ -0,0 +1,231 @@ +## 서론 + +유니폼 버퍼 튜토리얼에서 처음으로 기술자에 대해 알아봤었습니다. 이 챕터에서는 새로운 종류의 기술자인 *결합된 이미지 샘플러(combined image sampler)* 에 대해 알아보겠습니다. 이 기술자를 사용해서 셰이더로부터 우리가 만든 샘플러 객체를 거쳐 이미지 리소스에 접글할 수 있게 됩니다. + +먼저 기술자 집합 레이아웃, 기술자 풀, 기술자 집합이 결합된 이미지 샘플러와 같은 것을 포함할 수 있도록 수정할 것입니다. 그 이후에 `Vertex`에 텍스처 좌표를 추가하고 프래그먼트 셰이더를 수정하여 정점 색상을 보간하는 것이 아니라 텍스처로부터 색상값을 읽어오도록 할 것입니다. + +## 기술자 수정 + +`createDescriptorSetLayout` 함수로 가서 결합된 이미지 샘플러 기술자를 위해 `VkDescriptorSetLayoutBinding`를 추가합니다. 유니폼 버퍼 이후에 바인딩에 추가 합니다: + +```c++ +VkDescriptorSetLayoutBinding samplerLayoutBinding{}; +samplerLayoutBinding.binding = 1; +samplerLayoutBinding.descriptorCount = 1; +samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +samplerLayoutBinding.pImmutableSamplers = nullptr; +samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT; + +std::array bindings = {uboLayoutBinding, samplerLayoutBinding}; +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = static_cast(bindings.size()); +layoutInfo.pBindings = bindings.data(); +``` + +`stageFlags`를 사용해 결합된 이미지 샘플러 기술자가 프래그먼트 셰이더에서 사용될 것이라는 것을 꼭 명시하십시오. 프래그먼트의 색상이 결정되는 것은 그 시점이기 때문입니다. 정점 셰이더에서 텍스처 샘플링을 하는 것도 가능한데, 예를 들어 정점들을 [하이트맵(heightmap)](https://en.wikipedia.org/wiki/Heightmap)을 기반으로 동적으로 변경하고 하려고 하는 경우에 사용할 수 있습니다. + +또한 결합된 이미지 샘플러를 위해 기술자 풀을 넉넉하게 만들어야 합니다. `VkDescriptorPoolCreateInfo`에 `VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER` 타입의 `VkPoolSize`를 추가할 것입니다. `createDescriptorPool` 함수로 가서 이 기술자를 위한 `VkDescriptorPoolSize`를 포함하도록 수정합니다: + +```c++ +std::array poolSizes{}; +poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +poolSizes[0].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); +poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +poolSizes[1].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT); + +VkDescriptorPoolCreateInfo poolInfo{}; +poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; +poolInfo.poolSizeCount = static_cast(poolSizes.size()); +poolInfo.pPoolSizes = poolSizes.data(); +poolInfo.maxSets = static_cast(MAX_FRAMES_IN_FLIGHT); +``` + +부적합한 기술자 풀은 검증 레이어가 문제를 탐지하지 못하는 대표적인 예입니다 (Vulkan 1.1 기준). 풀이 충분히 크지 않다면 `vkAllocateDescriptorSets`은 `VK_ERROR_POOL_OUT_OF_MEMORY` 오류 코드와 함께 실패하지만 드라이버 내부적으로 문제를 해결하려 시도합니다. 즉 어떤 경우에는 (하드웨어 및 풀 크기와 할당 크기에 따라) 기술자 풀의 크기 제한을 넘는 경우에도 드라이버가 우리의 할당 문제를 회피할 수 있게 해줄수도 있습니다. 그렇지 못한 경우에는 `vkAllocateDescriptorSets`가 실패하고 `VK_ERROR_POOL_OUT_OF_MEMORY`를 반환합니다. 어떤 환경에서는 할당에 성공하고 어떤 환경에서는 실패하기 때문에 까다로운 문제입니다. + +Vulkan은 할당과 관련한 역할을 드라이버에 맡기기 때문에, 특정한 타입 (`VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER` 등)을의 기술자를 `descriptorCount` 멤버에 명시된 숫자대로 생성하는 것은 더 이상 엄격한 요구사항이 되지 못합니다. 하지만 그것을 지키는 것이 좋은 구현 방법이고 추후에는 [검증 모범사례](https://vulkan.lunarg.com/doc/view/1.2.189.0/linux/best_practices.html)를 활성화하는 경우 `VK_LAYER_KHRONOS_validation`가 이러한 종류의 문제에 대한 경고를 하게 될 것입니다. + +마지막 단계는 실제 이미지와 샘플러 리소스를 기술자 집합의 기술자들에 바인딩하는 것입니다. `createDescriptorSets` 함수로 가 봅시다. + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + VkDescriptorBufferInfo bufferInfo{}; + bufferInfo.buffer = uniformBuffers[i]; + bufferInfo.offset = 0; + bufferInfo.range = sizeof(UniformBufferObject); + + VkDescriptorImageInfo imageInfo{}; + imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + imageInfo.imageView = textureImageView; + imageInfo.sampler = textureSampler; + + ... +} +``` + +결합된 이미지 샘플러 구조체의 리소스는 `VkDescriptorImageInfo` 구조체에 명시되어야 하고, 이는 유니폼 버퍼 기술자의 버퍼 리소스가 `VkDescriptorBufferInfo` 구조체에 명시되었던 것과 같습니다. 이제 이전 챕터에서의 구조체들이 함께 활용됩니다. + +```c++ +std::array descriptorWrites{}; + +descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrites[0].dstSet = descriptorSets[i]; +descriptorWrites[0].dstBinding = 0; +descriptorWrites[0].dstArrayElement = 0; +descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +descriptorWrites[0].descriptorCount = 1; +descriptorWrites[0].pBufferInfo = &bufferInfo; + +descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; +descriptorWrites[1].dstSet = descriptorSets[i]; +descriptorWrites[1].dstBinding = 1; +descriptorWrites[1].dstArrayElement = 0; +descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; +descriptorWrites[1].descriptorCount = 1; +descriptorWrites[1].pImageInfo = &imageInfo; + +vkUpdateDescriptorSets(device, static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr); +``` + +기술자는 버퍼와 동일하게 이미지 정보와 함께 갱신되어야 합니다. 이번에는 `pBufferInfo`가 아닌 `pImageInfo`가 사용됩니다. 이제 셰이더에서 기술자를 활용항 준비가 되었습니다! + +## 텍스처 좌표 + +텍스처 맵핑을 위한 중요한 요소가 아직 빠져 있고, 이는 각 정점의 텍스처 좌표입니다. 텍스처 좌표는 이미지가 어떻게 대상에 맵핑될 것인지를 결정합니다. + +```c++ +struct Vertex { + glm::vec2 pos; + glm::vec3 color; + glm::vec2 texCoord; + + static VkVertexInputBindingDescription getBindingDescription() { + VkVertexInputBindingDescription bindingDescription{}; + bindingDescription.binding = 0; + bindingDescription.stride = sizeof(Vertex); + bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX; + + return bindingDescription; + } + + static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].location = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[0].offset = offsetof(Vertex, pos); + + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].location = 1; + attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT; + attributeDescriptions[1].offset = offsetof(Vertex, color); + + attributeDescriptions[2].binding = 0; + attributeDescriptions[2].location = 2; + attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[2].offset = offsetof(Vertex, texCoord); + + return attributeDescriptions; + } +}; +``` + +`Vertex` 구조체를 텍스처 좌표인 `vec2`를 포함하도록 수정합니다. `VkVertexInputAttributeDescription`도 추가해서 정점 셰이더의 입력으로 텍스처 좌표를 사용하도록 합니다. 이렇게 해야 이 값을 프래그먼트 셰이더로 넘길때 사각형의 표면에 걸쳐 보간이 이루어집니다. + +```c++ +const std::vector vertices = { + {{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}}, + {{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}}, + {{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}}, + {{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}} +}; +``` + +이 튜토리얼에서 저는 왼쪽 위 모서리에 `0, 0`을, 오른쪽 아래 모서리에 `1, 1`을 사용해서 텍스처가 사각형을 채우도록 하였습니다. 다른 좌표값으로 테스트 해 보세요. `0` 이하의 값이나 `1` 이상의 값으로 어드레싱 모드가 어떻게 동작하는지 살펴보세요! + +## 셰이더 + +마지막 단계는 셰이더를 수정해 텍스처로부터 색상을 샘플링하도록 하는 것입니다. 먼저 정점 셰이더를 수정해서 프래그먼트 셰이더로 텍스처 좌표를 넘기도록 합니다: + +```glsl +layout(location = 0) in vec2 inPosition; +layout(location = 1) in vec3 inColor; +layout(location = 2) in vec2 inTexCoord; + +layout(location = 0) out vec3 fragColor; +layout(location = 1) out vec2 fragTexCoord; + +void main() { + gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0); + fragColor = inColor; + fragTexCoord = inTexCoord; +} +``` + +정점별 색상과 동일하게 `fragTexCoord`값도 래스터라이저에 의해 사각형 전체에 걸쳐 부드럽게 보간됩니다. 텍스처 좌표를 프래그먼트 세이더의 출력 색상으로 하여 이를 눈으로 확인할 수 있습니다: + +```glsl +#version 450 + +layout(location = 0) in vec3 fragColor; +layout(location = 1) in vec2 fragTexCoord; + +layout(location = 0) out vec4 outColor; + +void main() { + outColor = vec4(fragTexCoord, 0.0, 1.0); +} +``` + +아래와 같은 이미지가 보일 겁니다. 셰이더를 다시 컴파일하는 것을 잊지 마세요! + +![](/images/texcoord_visualization.png) + +초록색 채널이 수평축 좌표, 빨간색 채널이 수직축 좌표입니다. 검은색과 노란색 모서리를 통해 텍스처 좌표가 `0, 0`에서 `1, 1` 사이로 보간되었다는 것을 확인 가능합니다. 색상값으로 데이터를 가시화 하는 것이 셰이더 프로그래밍에서는 `printf`와 같은 겁니다. 더 나은 대안이 없기 때문이죠! + +결합된 이미지 샘플러 기술자는 GLSL에서 샘플러 유니폼으로 표현됩니다. 프래그먼트 셰이더에서 이에 대한 참조를 추가합니다: + +```glsl +layout(binding = 1) uniform sampler2D texSampler; +``` + +다른 타입의 이미지를 위한 `sampler1D`와 `sampler3D` 타입도 있습니다. 올바른 바인딩을 해야 하는 것에 주의 하세요. + +```glsl +void main() { + outColor = texture(texSampler, fragTexCoord); +} +``` + +텍스처는 `texture` 내장함수에 의해 샘플링됩니다. 이 함수는 `sampler`와 텍스처 좌표를 인자로 받습니다. 샘플러는 필터링과 변환을 자동적으로 수행해줍니다. 이제 프로그램을 실행하면 사각형 위에 텍스처가 보일 겁니다: + +![](/images/texture_on_square.png) + +텍스처 좌표를 `1`보다 큰 값으로 해서 어드레싱 모드를 살펴 보세요. 예를들어 다음 프래그먼트 셰이더는 `VK_SAMPLER_ADDRESS_MODE_REPEAT`인 경우 아래와 같은 이미지를 나타냅니다: + +```glsl +void main() { + outColor = texture(texSampler, fragTexCoord * 2.0); +} +``` + +![](/images/texture_on_square_repeated.png) + +텍스처 색상을 정점 색상을 활용해 변경하는 것도 가능합니다: + +```glsl +void main() { + outColor = vec4(fragColor * texture(texSampler, fragTexCoord).rgb, 1.0); +} +``` + +여기서는 RGB와 알파 채널을 분리해서 알파 채널값은 영향을 받지 않도록 하였습니다. + +![](/images/texture_on_square_colorized.png) + +이제 셰이더에서 이미지에 접근하는 법을 알았습니다! 프레임버퍼에 쓰여진 이미지와 결합하게 되면 아주 강력한 기술이 됩니다. 그러한 이미지를 입력으로 사용해 멋진 후처리(post-processing) 효과나 3D 공간상에서 카메라를 표현하는 등의 작업을 할 수 있습니다. + +[C++ code](/code/26_texture_mapping.cpp) / +[Vertex shader](/code/26_shader_textures.vert) / +[Fragment shader](/code/26_shader_textures.frag) diff --git a/kr/07_Depth_buffering.md b/kr/07_Depth_buffering.md new file mode 100644 index 00000000..389b1bf7 --- /dev/null +++ b/kr/07_Depth_buffering.md @@ -0,0 +1,493 @@ +## 서론 + +지금까지 작업한 물체는 3차원으로 투영되긴 했지만 여전히 평평한 물체입니다. 이 챕터에서는 3D 메쉬를 위해 Z 좌표를 추가할 예정입니다. 새로 추가된 세 번째 좌표를 가지고 사각형을 지금 있는 사각형 위에 그려 봄으로써 물체들이 깊이 값에 따른 정렬(sort)이 되지 않으면 발생하는 문제를 살펴볼 것입니다. + +## 3D 형상(geometry) + +`Vertex` 구조체를 수정하여 위치값으로 3차원 벡터를 사용하도록 하고 이와 대를되는 `VkVertexInputAttributeDescription`의 `format`도 갱신합니다: + +```c++ +struct Vertex { + glm::vec3 pos; + glm::vec3 color; + glm::vec2 texCoord; + + ... + + static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].location = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT; + attributeDescriptions[0].offset = offsetof(Vertex, pos); + + ... + } +}; +``` + +다음으로 정점 셰이더가 3차원 좌표를 입력으로 받아서 변환을 수행하도록 바꿉니다. 수정 후에 다시 컴파일하는 것을 잊지 마세요! + +```glsl +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 좌표를 포함하도록 수정합니다: + +```c++ +const std::vector 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}} +}; +``` + +지금 프로그램을 실행하전 전과 동일한 결과가 나타납니다. 좀 더 재미있는 결과를 보기 위해 형상들을 추가해서 이 챕터에서 다루고자하는 문제를 보여드리겠습니다. 정점들을 복사해서 현재 사각형 아래에 새로운 사각형이 아래 그림과 같이 위치하도록 정의합니다: + +![](/images/extra_square.svg) + +Z 좌표로 `-0.5f`를 사용하고 추가된 사각형에 대한 인덱스들도 추가합니다: + +```c++ +const std::vector 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 indices = { + 0, 1, 2, 2, 3, 0, + 4, 5, 6, 6, 7, 4 +}; +``` + +이제 프로그램을 실행하면 마치 에셔의 그림같은 결과를 볼 수 있습니다: + +![](/images/depth_issues.png) + +문제는 아래에 위치한 사각형이 위쪽 사각형을 구성하는 프래그먼트 위에 그려진다는 것인데, 이는 아래 위치한 사각형의 인덱스가 인덱스 배열의 뒤쪽에 있기 때문입니다. 이 문제를 해결하는 방법은 두 가지가 있습니다: + +* 모든 드로우콜을 뒤쪽에서 앞쪽 깊이값 순서로 정렬 +* 깊이 버퍼를 사용해 깊이 테스트를 수행 + +첫 번째 접근법은 투명한 물체를 그릴 때 일반적으로 사용되는 방법인데, 순서와 무관하게 투명한 물체를 그리는 것은 상당히 어려운 문제이기 때문입니다. 프래그먼트를 깊이 순서대로 정렬하는 문제는 *깊이 버퍼*를 사용해서 해결하는 것이 일반적입니다. 깊이 버퍼는 모든 위치값에 대해 깊이를 저장하기 위해 사용하는 추가적인 어태치먼트입니다. 색상 어태치먼트가 색상 값을 저장하는 것과 다를 것이 없습니다. 래스터라이저가 프래그먼트를 만들어 낼 때마가 깊이 테스트를 통해 새로운 프래그먼트가 이미 쓰여저 있는 프래그먼트보다 더 가까이 있는 것인지를 테스트합니다. 그렇지 않은 경우에는 프래그먼트가 버려집니다(discarded). 깊이 테스트를 통과하면 그 프래그먼트의 깊이 값이 깊이 버퍼에 쓰여집니다. 프래그먼트 셰이더에서 색상 출력값을 조정하는 것처럼 깊이 값을 조정하는 것도 가능합니다. + + +```c++ +#define GLM_FORCE_RADIANS +#define GLM_FORCE_DEPTH_ZERO_TO_ONE +#include +#include +``` + +GLM에서 만든 원근 투영 행렬은 기본적으로 OpenGL에서 사용하는 깊이 범위인 `-1.0`에서 `1.0` 사이로 깊이값을 도출하게 되어 있습니다. `GLM_FORCE_DEPTH_ZERO_TO_ONE`를 정의하여 Vulkan에서 사용하는 `0.0`에서 `1.0` 범위가 되도록 설정해야 합니다. + +## 깊이 이미지와 뷰 + +색상 어태치먼트처럼 깊이 어태치먼트도 이미지를 기반으로 정의됩니다. 차이점은 스왑 체인이 깊이 이미지를 자동으로 만들어 주지 않는다는 점입니다. 한 번에 하나의 그리기 연산만 수행되는 상태이기 때문에 현재는 깊이 이미지는 하나만 있으면 됩니다. 깊이 이미지 또한 세 가지 리소스인 이미지, 메모리, 이미지 뷰 리소스를 필요로 합니다: + +```c++ +VkImage depthImage; +VkDeviceMemory depthImageMemory; +VkImageView depthImageView; +``` + +이러한 리소스들을 준비하기 위해 `createDepthResources` 함수를 새로 만듭니다: + +```c++ +void initVulkan() { + ... + createCommandPool(); + createDepthResources(); + createTextureImage(); + ... +} + +... + +void createDepthResources() { + +} +``` + +깊이 이미지를 만드는 것은 꽤 직관적입니다. 스왑 체인 범위를 통해 정의된 색상 어태치먼트와 동일한 해상도를 가져야 하며, 이미지의 사용법이 깊이 어태치먼트에 적합해야 하고, 타일링과 장치 로컬 메모리에 최적화되어야 합니다. 문제는, 깊이 이미지에 적합한 포맷이 무엇이냐 입니다. 깊이 이미지를 위한 포맷은 깊이 컴포넌트를 반드시 가져야 하고 이는 `VK_FORMAT_`에 `_D??_`로 표시되어 있습니다. + +텍스처 이미지와는 다르게 특정 포맷을 사용해야 하는 것은 없는데 프로그램에서 텍셀 값을 직접 접근하지는 않을 것이기 때문입니다. 그냥 적절한 정밀도를 가지면 되는데 실제 응용 프로그램에서는 최소 24비트 정도를 사용합니다. 이러한 요구조건에 맞는 포맷들이 몇 가지 있습니다: + +* `VK_FORMAT_D32_SFLOAT`: 32비트 부동소수점 깊이값 +* `VK_FORMAT_D32_SFLOAT_S8_UINT`: 부호 있는 32비트 부동소수점 깊이값과 8비트의 스텐실 요소 +* `VK_FORMAT_D24_UNORM_S8_UINT`: 24비트 부동소수점 깊이값과 8비트 스텐실 요소 + +스텐실 요소는 [스텐실 테스트](https://en.wikipedia.org/wiki/Stencil_buffer)에 사용되는데 깊이 테스트와 함께 사용되는 추가적인 테스트 입니다. 이에 대해서는 나중 챕터에서 살펴보도록 하겠습니다. + +지금은 간단하게 `VK_FORMAT_D32_SFLOAT` 포맷을 사용할 것인데 이 포맷은 거의 대부분 하드웨어에서 지원되기 때문입니다(하드웨어 데이터베이스를 살펴보세요). 그래도 약간의 유연성을 가지도록 하는 것도 좋을 것 같습니다. `findSupportedFormat` 함수를 통해 선호도 순으로 정렬된 포맷들의 후보를 받고, 그 중 지원되는 가장 첫 번째 포맷을 확인합니다: + +```c++ +VkFormat findSupportedFormat(const std::vector& candidates, VkImageTiling tiling, VkFormatFeatureFlags features) { + +} +``` + +포맷의 지원 여부는 타일링 모드와 사용법에 따라 다르기 때문에 이러한 요소들도 매개변수로 받아야 합니다. 포맷의 지원 여부는 `vkGetPhysicalDeviceFormatProperties` 함수를 통해 질의할 수 있습니다: + +```c++ +for (VkFormat format : candidates) { + VkFormatProperties props; + vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &props); +} +``` + +`VkFormatProperties` 구조체는 세 개의 필드를 갖습니다: + +* `linearTilingFeatures`: 선형 타일링이 지원되는 사용법 +* `optimalTilingFeatures`: 최적 타일링지 지원되는 사용법 +* `bufferFeatures`: 버퍼 용으로 지원되는 사용법 + +여기서는 첫 두 경우만 신경쓰면 되고, 확인은 함수에서 `tiling` 매개변수로 받은 값으로 수행합니다: + +```c++ +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; +} +``` + +후보의 어떤 포맷도 지원되지 않는 경우 특수한 값을 반환하거나 예외를 던지도록 처리할 수 있습니다: + +```c++ +VkFormat findSupportedFormat(const std::vector& 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!"); +} +``` + +이제 이 함수를 사용하는 `findDepthFormat` 헬퍼 함수를 만들어 깊이 요소를 가지면서 깊이 어태치먼트 사용법을 지원하는 포맷을 선택하도록 합니다: + +```c++ +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_IMAGE_USAGE_` 대신 `VK_FORMAT_FEATURE_` 플래그를 사용해야 합니다. 후보 포맷들은 모두 깊이 요소를 가지고 있지만 뒤의 두 경우는 스텐실 요소도 포함합니다. 아직은 사용하지 않을 것이지만 이러한 포맷들에 대해 레이아웃 전환을 수행할 때에는 스텐실 요소도 고려해야 합니다. 간단한 헬퍼 함수를 하나 더 추가해서 선택된 깊이 포맷이 스텐실 요소를 포함하는지 체크합니다: + +```c++ +bool hasStencilComponent(VkFormat format) { + return format == VK_FORMAT_D32_SFLOAT_S8_UINT || format == VK_FORMAT_D24_UNORM_S8_UINT; +} +``` + +`createDepthResources`에서 함수를 호출해 깊이 포맷을 찾습니다: + +```c++ +VkFormat depthFormat = findDepthFormat(); +``` + +이제 `createImage`와 `createImageView` 헬퍼 함수를 호출하기 위한 모든 필요한 정보가 준비되었습니다: + +```c++ +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` 함수는 현재 서브리소스가 `VK_IMAGE_ASPECT_COLOR_BIT`인 것으로 가정하고 있기 때문에 이를 매개변수로 바꿔야 합니다: + +```c++ +VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags) { + ... + viewInfo.subresourceRange.aspectMask = aspectFlags; + ... +} +``` + +이 함수를 호출하는 모든 부분에서 적합한 aspect를 사용하도록 수정합니다: + +```c++ +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); +``` + +여기까지가 깊이 이미지 생성 부분입니다. 맵핑이나 다른 이미지를 복사할 필요는 없습니다. 색상 어태치먼트처럼 렌더 패스의 시작 지점에서 지울 것이기 때문입니다. + +### 깊이 이미지의 명시적 전환 + +이미지의 레이아웃을 깊이 어태치먼트로 명시적으로 전환할 필요는 없는데, 이를 렌더 패스에서 처리할 예정이기 때문입니다. 하지만 완전성을 위해 이 섹션에서 설명은 진행 하도록 하겠습니다. 필요하다면 그냥 넘어가셔도 됩니다. + +`createDepthResources`의 마지막에 `transitionImageLayout`를 호출합니다: + +```c++ +transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL); +``` + +깊이 이미지에 이미 쓰여있는 정보는 상관이 없기 때문에 정의되지 않은 레이아웃을 초기 레이아웃으로 사용해도 됩니다. `transitionImageLayout`에서 적정한 서브리소스 aspect를 사용하도록 몇 가지 로직을 수정합니다: + +```c++ +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; +} +``` + +스텐실 요소는 사용하지 않을 것이지만 깊이 이미지의 레이아웃 전환에는 포함 시켜야 합니다. + +마지막으로 적절한 접근 마스크와 파이프라인 스테이지를 추가합니다: + +```c++ +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!"); +} +``` + +깊이 테스트 진행 과정에서 프래그먼트가 보이는지를 테스트 하기 위해 깊이 버퍼값을 읽어야 하고, 새로운 프래그먼트가 그려지면 값이 쓰여져야 합니다. 값을 읽는 것은 `VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT` 스테이지에서, 쓰는 것은 `VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT` 스테이지에서 이루어집니다. 명시된 연산과 일치하는 가장 빠른 단계의 파이프라인 스테이지를 선택해서 필요한 시점에 깊이 어태치먼트로 사용될 수 있게끔 해야 합니다. + +## 렌더 패스 + +이제 `createRenderPass`를 수정해서 깊이 어태치먼트를 포함하도록 해야 합니다. 먼저 `VkAttachmentDescription`를 명시합니다: + +```c++ +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`)은 신경쓰지 않습니다. 이렇게 하면 하드웨어가 추가적인 최적화를 진행할 수 있게 됩니다. 색상 버퍼처럼 이전 깊이 값은 신경쓰지 않으므로 `VK_IMAGE_LAYOUT_UNDEFINED`를 `initialLayout`로 사용합니다. + +```c++ +VkAttachmentReference depthAttachmentRef{}; +depthAttachmentRef.attachment = 1; +depthAttachmentRef.layout = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL; +``` + +첫 서브패스의 어태치먼트에 참조를 추가합니다: + +```c++ +VkSubpassDescription subpass{}; +subpass.pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS; +subpass.colorAttachmentCount = 1; +subpass.pColorAttachments = &colorAttachmentRef; +subpass.pDepthStencilAttachment = &depthAttachmentRef; +``` + +색상 어태치먼트와는 달리 서브패스는 하나의 깊이(+스텐실) 어태치먼트만 사용할 수 있습니다. 여러 버퍼에 대해 깊이 테스트를 수행하는 것은 말이 안됩니다. + +```c++ +std::array attachments = {colorAttachment, depthAttachment}; +VkRenderPassCreateInfo renderPassInfo{}; +renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO; +renderPassInfo.attachmentCount = static_cast(attachments.size()); +renderPassInfo.pAttachments = attachments.data(); +renderPassInfo.subpassCount = 1; +renderPassInfo.pSubpasses = &subpass; +renderPassInfo.dependencyCount = 1; +renderPassInfo.pDependencies = &dependency; +``` + +다음으로 `VkSubpassDependency` 구조체를 갱신해서 두 어태치먼트를 참조하도록 합니다. + +```c++ +dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT; +dependency.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_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; +``` + +마지막으로 서브패스의 의존성을 확장하여 로드(load) 연산의 한 과정으로 깊이 이미지의 전환과 지우기간에 충돌이 없도록 해 줍니다. 깊이 이미지는 먼저 초기 프래그먼트 테스트 파이프라인 스테이지에서 접근되고 *지우기* 로드 연산을 사용하기 때문에 접근 마스크를 쓰기로 명시해 주어야 합니다. + +## 프레임버퍼 + +다음 단계는 프레임버퍼 생성 부분을 수정해 깊이 이미지를 깊이 어태치먼트에 바인딩하는 것입니다. `createFramebuffers`로 가서 깊이 이미지 뷰를 두 번째 어태치먼트로 명시합니다: + +```c++ +std::array attachments = { + swapChainImageViews[i], + depthImageView +}; + +VkFramebufferCreateInfo framebufferInfo{}; +framebufferInfo.sType = VK_STRUCTURE_TYPE_FRAMEBUFFER_CREATE_INFO; +framebufferInfo.renderPass = renderPass; +framebufferInfo.attachmentCount = static_cast(attachments.size()); +framebufferInfo.pAttachments = attachments.data(); +framebufferInfo.width = swapChainExtent.width; +framebufferInfo.height = swapChainExtent.height; +framebufferInfo.layers = 1; +``` + +각 스왑 체인 이미지마다 색상 어태치먼트가 다르지만 깊이 이미지는 하나로 모두에 대해 사용할 수 있는데 우리 세마포어로 인해 한 번에 하나의 서브패스만 실행되기 때문입니다. + +`createFramebuffers`의 호출을 옮겨서 깊이 이미지 뷰가 실제로 생성된 다음에 호출되도록 합니다: + +```c++ +void initVulkan() { + ... + createDepthResources(); + createFramebuffers(); + ... +} +``` + +## 지우기 값 + +`VK_ATTACHMENT_LOAD_OP_CLEAR`에 여러 개의 어태치먼트가 존재하기 때문에 지우기 값도 여러 개를 명시해 주어야 합니다. `recordCommandBuffer`로 가서 `VkClearValue`의 배열을 만듭니다: + +```c++ +std::array clearValues{}; +clearValues[0].color = {{0.0f, 0.0f, 0.0f, 1.0f}}; +clearValues[1].depthStencil = {1.0f, 0}; + +renderPassInfo.clearValueCount = static_cast(clearValues.size()); +renderPassInfo.pClearValues = clearValues.data(); +``` + +Vulkan에서 깊이 버퍼의 깊이값은 `0.0`과 `1.0` 사이이고, `1.0`이 원면, `0.0`이 근면입니다. 각 점의 초기값은 가장 먼 깊이여야 하므로 `1.0`으로 설정합니다. + +`clearValues`의 순서가 어태치먼트의 순서와 대응된다는 것에 유의하세요. + +## 깊이와 스텐실 상태 + +이제 깊이 어태치먼트를 사용할 준비가 되었고, 그래픽스 파이프라인에서 실제로 깊이 테스트를 수행하도록 설정해야 합니다. 이는 `VkPipelineDepthStencilStateCreateInfo` 구조체를 통해 설정됩니다: + +```c++ +VkPipelineDepthStencilStateCreateInfo depthStencil{}; +depthStencil.sType = VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO; +depthStencil.depthTestEnable = VK_TRUE; +depthStencil.depthWriteEnable = VK_TRUE; +``` + +`depthTestEnable` 필드는 새로운 프래그먼트의 깊이값에 대해 깊이 버퍼에 기존에 쓰여진 값과의 비교를 통해 버리기를 수행할지 여부를 명시합니다. `depthWriteEnable` 필드는 테스트를 통과한 새로운 프래그먼트의 깊이 값이 깊이 버퍼에 쓰여길 것인지를 명시합니다. + +```c++ +depthStencil.depthCompareOp = VK_COMPARE_OP_LESS; +``` + +`depthCompareOp` 필드는 프래그먼트의 버리기나 유지를 결정하기 위한 비교 연산을 명시합니다. 일반적으로 사용되는 적은 깊이값(=더 가까운 프래그먼트)을 사용할 것이고, 이는 즉 새로운 프래그먼트의 깊이값이 더 *작아야 한다*는 의미입니다. + +```c++ +depthStencil.depthBoundsTestEnable = VK_FALSE; +depthStencil.minDepthBounds = 0.0f; // Optional +depthStencil.maxDepthBounds = 1.0f; // Optional +``` + +`depthBoundsTestEnable`, `minDepthBounds`, `maxDepthBounds` 필드는 깊이 바운드(bound) 테스트에 대한 선택적인 값입니다. 이는 특정 범위 내의 프래그먼트만 유지할 수 있도록 해 줍니다. 우리는 사용하지 않을 것입니다. + +```c++ +depthStencil.stencilTestEnable = VK_FALSE; +depthStencil.front = {}; // Optional +depthStencil.back = {}; // Optional +``` + +마지막 세 필드는 스텐실 버퍼 연산의 설정이고, 이 튜토리얼에서는 사용하지 않습니다. 이 연산을 사용하려면 깊이/스텐실 이미지의 포맷이 스텐실 요소를 가지고 있어야만 합니다. + +```c++ +pipelineInfo.pDepthStencilState = &depthStencil; +``` + +`VkGraphicsPipelineCreateInfo` 구조체를 수정하여 방금 설정한 깇이 스텐실 상태를 참조하도록 합니다. 깊이 스텐실 상태는 렌더 패스가 깊이 스텐실 어태치먼트를 가지는 경우, 항상 명시 되어야만 합니다. + +이 상태에서 프로그램을 실행하면 각 물체의 프래그먼트가 올바른 순서로 표시되는 것을 보실 수 있습니다: + +![](/images/depth_correct.png) + +## 윈도우 리사이징 처리 + +윈도우가 리사이징되면 새로운 색상 어태치먼트의 해상도에 맞게 깊이 버퍼 해상도 또한 변해야 합니다. `recreateSwapChain` 함수를 확장하여 이러한 경우 깊이 리소스를 재생성하도록 합니다: + +```c++ +void recreateSwapChain() { + int width = 0, height = 0; + while (width == 0 || height == 0) { + glfwGetFramebufferSize(window, &width, &height); + glfwWaitEvents(); + } + + vkDeviceWaitIdle(device); + + cleanupSwapChain(); + + createSwapChain(); + createImageViews(); + createDepthResources(); + createFramebuffers(); +} +``` + +정리는 스왑 체인 정리 함수에서 수행되어야 합니다: + +```c++ +void cleanupSwapChain() { + vkDestroyImageView(device, depthImageView, nullptr); + vkDestroyImage(device, depthImage, nullptr); + vkFreeMemory(device, depthImageMemory, nullptr); + + ... +} +``` + +축하합니다. 이제 프로그램이 어떤 3D 형상이라도 제대로 그릴 수 있는 준비가 되었습니다. 다음 챕터에서 텍스처가 입혀진 모델을 그려서 이를 시도해 보겠습니다! + +[C++ code](/code/27_depth_buffering.cpp) / +[Vertex shader](/code/27_shader_depth.vert) / +[Fragment shader](/code/27_shader_depth.frag) diff --git a/kr/08_Loading_models.md b/kr/08_Loading_models.md new file mode 100644 index 00000000..a7077953 --- /dev/null +++ b/kr/08_Loading_models.md @@ -0,0 +1,248 @@ +## 서론 + +이제 텍스처가 입혀진 3D 메쉬를 렌더링할 준비가 되었지만 지금의 `vertices`와 `indices` 배열에 정의된 형상은 좀 재미가 없습니다. 이 챕터에서는 프로그램을 확장해서 실제 모델 파일로부터 정점과 인덱스를 로드하여 그래픽 카드가 좀 더 실질적인 작업을 하도록 만들어 보겠습니다. + +많은 그래픽스 API 튜토리얼에서는 이러한 챕터에서 직접 OBJ 로더를 작성합니다. 문제는 실제 3D 응용 프로그램을 만들다면 이 포맷에서 지원하지 않는, 예를 들자면 스켈레톤 애니메이션 같은 기능을 얼마 지나지 않아 필요로 하게 된다는 것입니다. 이 챕터에서 우리도 역시 OBJ 모델로부터 메쉬 데이터를 로딩할 것이지만, 파일에서 이러한 데이터를 어떻게 로딩하는지보다는 실제 프로그램에서 어떻게 메쉬 데이터를 사용하도록 해야 하는지에 대해서 집중하도록 하겠습니다. + +## 라이브러리 + +우리는 [tinyobjloader](https://github.com/syoyo/tinyobjloader) 라이브러리를 사용해 OBJ 파일로부터 정점과 표면(face) 정보를 로드할 것입니다. stb_image처럼 하나의 파일로 된 라이브러리기 때문에 빠르고, 프로그램에 통합하기도 쉽습니다. 위 링크의 레포지토리로 가서 `tiny_obj_loader.h` 파일을 여러분의 라이브러리 리렉토리에 다운로드 하십시오. + +**비주얼 스튜디오** + +`tiny_obj_loader.h`가 있는 디렉토리를 `추가 포함 디렉토리` 경로에 추가하세요. + +![](/images/include_dirs_tinyobjloader.png) + +**Makefile** + +`tiny_obj_loader.h`가 있는 디렉토리는 GCC의 include 리렉토리로 추가하세요: + +```text +VULKAN_SDK_PATH = /home/user/VulkanSDK/x.x.x.x/x86_64 +STB_INCLUDE_PATH = /home/user/libraries/stb +TINYOBJ_INCLUDE_PATH = /home/user/libraries/tinyobjloader + +... + +CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_INCLUDE_PATH) -I$(TINYOBJ_INCLUDE_PATH) +``` + +## 예제 메쉬 + +이 챕터에서 아직 라이팅(lighting)을 적용하진 않을 것이기 때문에 텍스처에 라이팅이 베이크 되어 있는 예제 모델을 사용하는 것이 좋을 것 같습니다. 쉬운 방법 중 하나는 [Sketchfab](https://sketchfab.com/)에서 3D 스캐닝 모델을 찾는 것입니다. 이 사이트의 많은 모델들이 OBJ 포맷을 허용적 라이센스(permissive license)로 제공합니다. + +이 튜토리얼에서는 [nigelgoh](https://sketchfab.com/nigelgoh)이 모델링한 [Viking room](https://sketchfab.com/3d-models/viking-room-a49f1b8e4f5c4ecf9e1fe7d81915ad38)을 사용하기로 했습니다 ([CC BY 4.0](https://web.archive.org/web/20200428202538/https://sketchfab.com/3d-models/viking-room-a49f1b8e4f5c4ecf9e1fe7d81915ad38)). 크기와 자세를 바꾸어서 현재 형상을 바로 대체할 수 있도록 했습니다: + +* [viking_room.obj](/resources/viking_room.obj) +* [viking_room.png](/resources/viking_room.png) + +다른 모델을 사용해도 되지만 하나의 머티리얼(material)로만 구성되고, 크기가 대략 1.5 x 1.5 x 1.5 여야 합니다. 이보다 큰 경우에는 뷰 행렬을 수정해야 합니다. `shaders`와 `textures` 디렉토리와 같은 위치에 `models` 디렉토리를 만들고 모델 파일을 넣으십시오. 그리고 텍스처 이미지는 `texture` 디렉토리에 넣으십시오. + +모델과 텍스처 경로에 대한 변수를 프로그램에 추가합니다: + +```c++ +const uint32_t WIDTH = 800; +const uint32_t HEIGHT = 600; + +const std::string MODEL_PATH = "models/viking_room.obj"; +const std::string TEXTURE_PATH = "textures/viking_room.png"; +``` + +그리고 `createTextureImage`에서 이 경로를 사용하도록 수정합니다: + +```c++ +stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); +``` + +## 정점과 인덱스 로딩 + +이제 모델 파일로부터 정점과 인덱스를 로딩할 것입니다. 따라서 전역 선언된 `vertices`와 `indices` 배열은 제거해야 합니다. 이들을 상수가 아닌 컨테이너로 클래스 멤버에 추가합니다: + +```c++ +std::vector vertices; +std::vector indices; +VkBuffer vertexBuffer; +VkDeviceMemory vertexBufferMemory; +``` + +인덱스의 타입을 `uint16_t`에서 `uint32_t`로 수정해야 하는데 65535보다 훨씬 많은 정점이 존재하기 때문입니다. `vkCmdBindIndexBuffer` 매개변수도 바꾸는 것을 잊지 마세요: + +```c++ +vkCmdBindIndexBuffer(commandBuffer, indexBuffer, 0, VK_INDEX_TYPE_UINT32); +``` + +tinyobjloader 라이브러리는 STB 라이브러리와 같은 방식으로 include됩니다. `tiny_obj_loader.h` 파일을 include하고 소스 파일 중 하나에 `TINYOBJLOADER_IMPLEMENTATION`를 선언하여 함수 본문에 대한 링크 오류가 발생하지 않도록 합니다: + +```c++ +#define TINYOBJLOADER_IMPLEMENTATION +#include +``` + +이제 이 라이브러리를 사용해 메쉬의 정점 데이터를 `vertices`와 `indices` 컨테이너에 담는 `loadModel` 함수를 만들 것입니다. 이 함수는 정점과 인덱스 버퍼가 생성되기 이전에 호출되어야 합니다: + +```c++ +void initVulkan() { + ... + loadModel(); + createVertexBuffer(); + createIndexBuffer(); + ... +} + +... + +void loadModel() { + +} +``` + +`tinyobj::LoadObj` 함수를 호출하면 모델이 라이브러리의 데이터 구조에 담겨 로딩됩니다: + +```c++ +void loadModel() { + tinyobj::attrib_t attrib; + std::vector shapes; + std::vector materials; + std::string warn, err; + + if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH.c_str())) { + throw std::runtime_error(warn + err); + } +} +``` + +OBj 파일은 위치, 법선, 텍스처 좌표와 표면(face)으로 구성되어 있습니다 표면은 임의 개의 정점으로 정의되는데 각 정점은 위치, 법선과 텍스처 좌표의 인덱스를 참조합니다. 이러한 방식을 통해 정점 전체를 재사용하는 것 뿐만 아니라 개별 속성을 재사용하는 것도 가능합니다. + +`attrib` 컨테이너는 전체 위치, 법선, 텍스처 좌표를 `attrib.vertices`, `attrib.normals`, `attrib.texcoords` 벡터에 저장하고 있습니다. `shapes` 컨테이너는 모든 개별 물체와 물체를 구성하는 표면을 가지고 있습니다. 각 표면은 정점의 배열로 이루어지며 각 정점은 위치, 법선, 텍스처 좌표 속성의 인덱스를 가지고 있습니다. OBJ 모델은 각 표면별 텍스처와 머티리얼을 정의할 수 있게 되어 있는데 지금은 이를 무시할 것입니다. + +`err` 문자열은 파일을 로딩하는 과정에서 발생한 오류를, `warn` 문자열은 경고를 가지고 있습니다. 예를 들자면 머티리얼 정의가 없는 경우 등이 있을 것입니다. `LoadObj` 함수가 `false`를 반환하는 경우가 실제 로딩이 실패한 경우입니다. 위에서 이야기한 것처럼 OBJ 파일에서 표면은 임의의 정점을 가질 수 있지만, 우리 프로그램에서는 삼각형만 그릴 것입니다. 다행히 `LoadObj` 함수는 표면을 자동으로 삼각화(triangulate) 해 주는 기능을 사용하기 위한 매개변수가 있고, 이는 사용하는 것이 기본값으로 되어 있습니다. + +파일에 들어있는 모든 표면을 하나의 모델로 만들 것이기 때문에 shape을 반복(iterate)합니다: + +```c++ +for (const auto& shape : shapes) { + +} +``` + +삼각화 기능을 통해 각 표면별로 세 개의 정점을 가지는 것이 보장되어 있기 때문에 정점을 반복하며 `vertices` 벡터에 집어 넣습니다: + +```c++ +for (const auto& shape : shapes) { + for (const auto& index : shape.mesh.indices) { + Vertex vertex{}; + + vertices.push_back(vertex); + indices.push_back(indices.size()); + } +} +``` + +For simplicity, we will assume that every vertex is unique for now, hence the +simple auto-increment indices.. `index` 변수는 `tinyobj::index_t` 타입인데, 이는 `vertex_index`, `normal_index`, `texcoord_index` 멤버를 가집니다. 이 인덱스를 사용해서 `attrib` 배열로부터 실제 정점 어트리뷰트를 가져옵니다: + +```c++ +vertex.pos = { + attrib.vertices[3 * index.vertex_index + 0], + attrib.vertices[3 * index.vertex_index + 1], + attrib.vertices[3 * index.vertex_index + 2] +}; + +vertex.texCoord = { + attrib.texcoords[2 * index.texcoord_index + 0], + attrib.texcoords[2 * index.texcoord_index + 1] +}; + +vertex.color = {1.0f, 1.0f, 1.0f}; +``` + +안타깝게도 `attrib.vertices` 배열은 `glm::vec3`와 같은 것이 아니고 `float`의 배열입니다. 따라서 익덱스에 `3`을 곱해 주어야 합니다. 또한 텍스처 좌표 요소는 두 개씩 들어 있습니다. X, Y, Z 요소 또는 텍스처 좌표의 경우 U, V 요소에 대해서 `0`, `1`, `2`의 오프셋을 가집니다. + +최적화를 수행한 상태(예를들어 비주얼 스튜디오에서는 `Release` 모드, GCC에서는 `-O3` 컴파일 플래스를 사용)에서 프로그램을 실행해 보세요. 이렇게 하지 않으면 모델을 로딩하는 것이 매우 느릴 겁니다. 그 결과로 아래와 같은 모습이 보일 겁니다: + +![](/images/inverted_texture_coordinates.png) + +좋습니다. 형상은 맞는 것 같네요. 하지만 텍스처에 무슨 문제가 생긴 걸까요? OBJ 포맷은 수직 좌표에서 `0`이 이미지의 하단이라고 가정하는데 우리는 Vulkan에 이미지를 업로드 할 때 위쪽이 `0`을 의미하도록 정의하였습니다. 텍스처 좌표의 수직 요소를 뒤집어서 이 문제를 해결합니다: + +```c++ +vertex.texCoord = { + attrib.texcoords[2 * index.texcoord_index + 0], + 1.0f - attrib.texcoords[2 * index.texcoord_index + 1] +}; +``` + +다시 프로그램을 실행하면, 올바른 결과를 볼 수 있습니다: + +![](/images/drawing_model.png) + + +지금까지의 모든 노력이 이제 이 같은 데모를 통해 결실을 얻게 되었습니다! + +>모델이 회전되면서 뒤쪽(벽면의 뒷면)이 이상하게 보이는 것을 눈치 채실 수 있을 겁니다. 이는 모델 자체가 뒤쪽에서 보는 것을 고려하지 많고 만들어졌기 때문이고, 정상적인 현상입니다. + +## 중복 정점 제거 + +아쉽게도 지금은 인덱스 버퍼를 통한 이점을 얻지 못하고 있습니다. `vertices` 벡터는 중복된 정점 데이터가 많은데 많은 정점들이 여러 삼각형에 중복되어 포함되기 때문입니다. 고유한 정점만을 남기고 인덱스 버퍼를 사용해 재사용해야 합니다. 이를 구현하는 간단한 방법은 `map`이나 `unordered_map`을 사용해 고유한 정점과 그에 상응하는 인덱스를 추적하는 것입니다: + +```c++ +#include + +... + +std::unordered_map uniqueVertices{}; + +for (const auto& shape : shapes) { + for (const auto& index : shape.mesh.indices) { + Vertex vertex{}; + + ... + + if (uniqueVertices.count(vertex) == 0) { + uniqueVertices[vertex] = static_cast(vertices.size()); + vertices.push_back(vertex); + } + + indices.push_back(uniqueVertices[vertex]); + } +} +``` + +OBJ 파일에서 정점을 읽어올 때마다 동일한 위치와 텍스처 좌표를 갖는 정점이 이미 존재하는지를 살펴봅니다. 없다면, `vertices`에 추가하고 그 인덱스를 `uniqueVertices` 컨테이너에 추가합니다. 그리고 그 정점의 인덱스를 `indices`에 추가합니다. 이미 존재하는 정점이라면 `uniqueVertices`에서 인덱스를 찾아서 그 인덱스를 `indices`에 저장합니다. + +지금 컴파일하면 컴파일이 실패하게 되는데 `Vertex`와 같은 유저가 정의한 타입을 해시 테이블의 키로 사용하기 위해서는 두 가지 기능을 구현해야 하기 때문입니다. 동일성 테스트와 해시 계산 기능을 구현해야 합니다. 전자는 `==`연산자를 `Vertex` 구조체에 오버라이딩 하면 됩니다: + +```c++ +bool operator==(const Vertex& other) const { + return pos == other.pos && color == other.color && texCoord == other.texCoord; +} +``` + +`Vertex`의 해시 함수는 `std::hash`에 대한 템플릿 특수화를 명시하여 구현할 수 있습니다. 해시 함수는 복잡한 주제이지만 [cppreference.com의 추천](http://en.cppreference.com/w/cpp/utility/hash)에 따라서 아래와 같이 구조체의 필드를 결합해서 괜찮은 해시 함수를 정의할 수 있습니다: + +```c++ +namespace std { + template<> struct hash { + size_t operator()(Vertex const& vertex) const { + return ((hash()(vertex.pos) ^ + (hash()(vertex.color) << 1)) >> 1) ^ + (hash()(vertex.texCoord) << 1); + } + }; +} +``` + +이 코드는 `Vertex` 구조체 밖에 정의되어야 합니다. GLM 타입에 대한 해시 함수는 아래와 같은 헤더를 include해야 사용할 수 있습니다: + +```c++ +#define GLM_ENABLE_EXPERIMENTAL +#include +``` + +해시 함수는 `gtx` 폴더에 정의되어 있는데 이는 이 기능이 사실은 GLM의 실험적 확장 기능이라는 의미입니다. 따라서 사용하기 위해서는 `GLM_ENABLE_EXPERIMENTAL`가 정의되어 있어야 합니다. 또한 이 기능은 나중에 GLM 버전이 바뀌면 API가 바뀔 수 있다는 뜻이기도 하지만 실제로는 API는 상당히 안정적입니다.(*역주: 바뀔 가능성이 적다는 의미*) + +이제 컴파일 오류 없이 프로그램을 실행할 수 있습니다. `vertices`의 크기를 확인해 보면 1,500,000에서 265,645로 줄어든 것을 확인할 수 있습니다! 즉 각 정점이 평균적으로 대략 여섯 개의 삼각형에서 재사용되고 있다는 뜻입니다. 이를 통해 GPU 메모리가 상당히 절약되었을 것입니다. + +[C++ code](/code/28_model_loading.cpp) / +[Vertex shader](/code/27_shader_depth.vert) / +[Fragment shader](/code/27_shader_depth.frag) diff --git a/kr/09_Generating_Mipmaps.md b/kr/09_Generating_Mipmaps.md new file mode 100644 index 00000000..894b9d4a --- /dev/null +++ b/kr/09_Generating_Mipmaps.md @@ -0,0 +1,354 @@ +## 서론 + +이제 우리 프로그램에서 3D 모델을 로딩하고 렌더링할 수 있게 되었습니다. 이 챕터에서는 기능을 하나 더 추가할 것인데, 밉맵 생성입니다. 밉맵은 게임이나 렌더링 소프트웨어에서 널리 사용되는 방법이고, Vulkan에서는 밉맵이 어떻게 생성될 것인지에 대한 완전한 제어 권한을 제공해 줍니다. + +밉맵은 미리 계산된, 축소된 이미지입니다. 각 이미지는 전 단계의 이미지보다 가로와 세로가 절반 크기로 줄어든 이미지 입니다. 밉맵은 *디테일 레벨(Level of Detail)*, 다시말해 *LOD*으로써 사용됩니다. 카메라에서 멀리 떨어진 물체는 텍스처를 샘플링 할 때 더 작은 밉 이미지로부터 샘플링합니다. 더 작은 이미지를 사용하면 렌더링 속도가 빨라지며 [Moiré 패턴](https://en.wikipedia.org/wiki/Moir%C3%A9_pattern)과 같은 문제점을 해결할 수 있습니다. 밉맵의 예시는 아래와 같습니다: + +![](/images/mipmaps_example.jpg) + +## 이미지 생성 + +Vulkan에서 각 밉 이미지는 `VkImage`의 서로 다른 *밉 레벨*에 저장됩니다. 밉 레벨 0은 원본 이미지이고 0 레벨 이후의 밉 레벨들은 일반적으로 *밉 체인(chain)*이라고 부릅니다. + +밉 레벨의 개수는 `VkImage`이 생성될 때 명시됩니다. 지금까지는 항상 이 값을 1로 설정했었습니다. 이제는 이미지의 크기로부터 밉 레벨의 개수를 계산해야 합니다. 먼저 이 숫자를 저장하기 위한 클래스 멤버를 추가합니다: + +```c++ +... +uint32_t mipLevels; +VkImage textureImage; +... +``` + +`mipLevels` 값은 `createTextureImage`에서 텍스처를 로딩한 뒤 저장됩니다: + +```c++ +int texWidth, texHeight, texChannels; +stbi_uc* pixels = stbi_load(TEXTURE_PATH.c_str(), &texWidth, &texHeight, &texChannels, STBI_rgb_alpha); +... +mipLevels = static_cast(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1; + +``` + +위와 같이 밉 체인의 레벨 개수를 계산합니다. `max` 함수는 가로 세로 중 큰 값을 찾습니다. `log2` 함수를 통해 그 값이 2로 몇번 나눌 수 있는지를 계산합니다. `floor` 함수를 통해 값이 2의 배수가 아닐 경우를 처리합니다. 원본 이미지가 밉 레벨 하나를 차지하기 때문에 `1`을 더합니다. + +이 값을 사용하기 위해서는 `createImage`, `createImageView`, `transitionImageLayout` 함수를 수정해서 밉 레벨을 명시해야 합니다. 해당 함수들에 `mipLevels` 매개변수를 추가합니다: + +```c++ +void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { + ... + imageInfo.mipLevels = mipLevels; + ... +} +``` +```c++ +VkImageView createImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels) { + ... + viewInfo.subresourceRange.levelCount = mipLevels; + ... +``` +```c++ +void transitionImageLayout(VkImage image, VkFormat format, VkImageLayout oldLayout, VkImageLayout newLayout, uint32_t mipLevels) { + ... + barrier.subresourceRange.levelCount = mipLevels; + ... +``` + +이 함수들을 호출하는 곳에서는 올바른 값을 넘겨주도록 수정합니다: + +```c++ +createImage(swapChainExtent.width, swapChainExtent.height, 1, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory); +... +createImage(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory); +``` +```c++ +swapChainImageViews[i] = createImageView(swapChainImages[i], swapChainImageFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1); +... +depthImageView = createImageView(depthImage, depthFormat, VK_IMAGE_ASPECT_DEPTH_BIT, 1); +... +textureImageView = createImageView(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_ASPECT_COLOR_BIT, mipLevels); +``` +```c++ +transitionImageLayout(depthImage, depthFormat, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, 1); +... +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels); +``` + + + +## 밉맵 생성 + +이제 텍스처 이미지가 여러 밉 레벨을 가지고 있지만 스테이징 버퍼는 밉 레벨 0을 채우는 데에만 사용되고 있습니다. 다른 레벨들에 대해서는 아직 정의되지 않았습니다. 이러한 레벨들을 채우기 위해서는 하나의 레벨으로부터 데이터들을 생성해야만 합니다. 이를 위해 `vkCmdBlitImage` 명령을 사용할 것입니다. 이 명령은 복사, 크기 변환과 필터링 연산을 수행합니다. 이를 여러 번 호출해서 데이터를 텍스처 이미지의 여러 레벨으로 *blit* 하도록 할 것입니다. + +`vkCmdBlitImage`은 전송 연산으로 취급되기 때문에, Vulkan에게 텍스처 이미지가 전송의 소스와 목적지로 사용된 것임을 알려줘야 합니다. `createTextureImage`에서 텍스처 이미지의 사용법 플래그에 `VK_IMAGE_USAGE_TRANSFER_SRC_BIT`를 추가합니다: + +```c++ +... +createImage(texWidth, texHeight, mipLevels, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory); +... +``` + +다른 이미지 연산들처럼 `vkCmdBlitImage`도 연산이 수행되는 이미지의 레이아웃에 의존적입니다. 전체 이미지를 `VK_IMAGE_LAYOUT_GENERAL`로 전환할 수도 있지만 이렇게 하면 느려질 겁니다. 최적 성능을 위해서는 소스 이미지는 `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`에, 목적 이미지는 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`에 있어야 합니다. Vulkan에서는 이미지의 각 레벨을 독립적으로 전환할 수 있도록 되어 있습니다. 각 blit마다 두 개의 밉 레벨을 처리하므로 blit 명령간에 최적 레이아웃으로 전환해 사용하면 됩니다. + +`transitionImageLayout`는 전체 이미지에 대한 레이아웃 전환만 수행하므로 몇 가지 파이프라인 배리어 연산을 작성해야 합니다. `createTextureImage`에 기존에 있던 `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`로의 전환을 삭제합니다: + +```c++ +... +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels); + copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); +//transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating mipmaps +... +``` + +이렇게 하면 텍스처 이미지의 각 레벨이 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`가 됩니다. 각 레벨은 blit 명령이 끝난 뒤에 `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`로 전환될 겁니다. + +이제 밉맵 생성을 위한 코드를 작성할 것입니다: + +```c++ +void generateMipmaps(VkImage image, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) { + VkCommandBuffer commandBuffer = beginSingleTimeCommands(); + + VkImageMemoryBarrier barrier{}; + barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; + barrier.image = image; + barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; + barrier.subresourceRange.baseArrayLayer = 0; + barrier.subresourceRange.layerCount = 1; + barrier.subresourceRange.levelCount = 1; + + endSingleTimeCommands(commandBuffer); +} +``` + +몇 번의 전환을 수행할 것이므로 `VkImageMemoryBarrier`를 재사용할 것입니다. 위에서 설정한 필드는 모든 배리어에 대해 동일하게 사용됩니다. `subresourceRange.miplevel`, `oldLayout`, `newLayout`, `srcAccessMask`, `dstAccessMask`은 각 전환마다 바뀔 예정입니다. + +```c++ +int32_t mipWidth = texWidth; +int32_t mipHeight = texHeight; + +for (uint32_t i = 1; i < mipLevels; i++) { + +} +``` + +위 반복문에서 각각의 `VkCmdBlitImage` 명령을 기록할 것입니다. 반복문의 변수가 0이 아닌 1부터 시작한다는 것에 유의하세요. + +```c++ +barrier.subresourceRange.baseMipLevel = i - 1; +barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; +barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; +barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; +barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; + +vkCmdPipelineBarrier(commandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &barrier); +``` + +먼저 `i - 1` 레벨을 `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`로 전환합니다. 이 전환은 `i - 1` 레벨이 다 채워질 때까지 기다릴 것인데 이는 이전의 blit 명령 또는 `vkCmdCopyBufferToImage`에 의해 이루어집니다. 현재의 blit 명령은 이러한 전환을 대기하게 됩니다. + +```c++ +VkImageBlit blit{}; +blit.srcOffsets[0] = { 0, 0, 0 }; +blit.srcOffsets[1] = { mipWidth, mipHeight, 1 }; +blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +blit.srcSubresource.mipLevel = i - 1; +blit.srcSubresource.baseArrayLayer = 0; +blit.srcSubresource.layerCount = 1; +blit.dstOffsets[0] = { 0, 0, 0 }; +blit.dstOffsets[1] = { mipWidth > 1 ? mipWidth / 2 : 1, mipHeight > 1 ? mipHeight / 2 : 1, 1 }; +blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; +blit.dstSubresource.mipLevel = i; +blit.dstSubresource.baseArrayLayer = 0; +blit.dstSubresource.layerCount = 1; +``` + +다음으로 blit 연산에 사용될 영역을 명시합니다. 소스 밉 레벨은 `i - 1`이고 목적 밉 레벨은 `i` 입니다. `srcOffsets` 배열의 두 요소는 데이터가 blit될 소스의 3D 영역을 결정합니다. `dstOffsets`은 데이터가 blit될 목적 영역을 의미합니다. `dstOffsets[1]`의 X와 Y 크기는 2로 나누었는데 각 밉 레벨이 이전 레벨의 절반 크기이기 때문입니다. `srcOffsets[1]`와 `dstOffsets[1]`의 Z 크기는 1이어야 하는데, 2D 이미지는 깊이값이 1이기 때문입니다. + +```c++ +vkCmdBlitImage(commandBuffer, + image, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, + image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + 1, &blit, + VK_FILTER_LINEAR); +``` + +이제 blit 명령을 기록합니다. `srcImage`와 `dstImage` 매개변수에 모두 `textureImage` 가 사용된 것을 주목하십시오. 같은 이미지의 다른 레벨로 blit을 하고 있기 때문입니다. 소스 밉 레벨은 `VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL`로 전환되었고 목적 레벨은 `createTextureImage`에서 정의한대로 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`인 상태입니다. + +[정점 버퍼](!kr/Vertex_buffers/Staging_buffer)에서 제시한 직접 만든 전송 큐를 사용하는 경우 주의하셔야 합니다. `vkCmdBlitImage`는 그래픽스 기능의 큐에 제출되어야만 합니다. + +마지막 매개변수는 blit에 사용할 `VkFilter`를 명시합니다. 여기서는 `VkSampler`를 만들 때 사용한 것과 같은 필터링 옵션을 사용합니다. 보간을 수행하기 위해 `VK_FILTER_LINEAR`를 사용합니다. + +```c++ +barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; +barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; +barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; +barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + +vkCmdPipelineBarrier(commandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &barrier); +``` + +이 배리어가 밉 레벨 `i - 1`을 `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`로 전환합니다. 이 전환은 현재의 blit 명령이 끝날 때까지 대기합니다. 이 전환이 끝날때까지 모든 샘플링 연산이 대기상태가 될 겁니다. + +```c++ + ... + if (mipWidth > 1) mipWidth /= 2; + if (mipHeight > 1) mipHeight /= 2; +} +``` + +반복문의 끝 부분에서 현재의 밉 크기를 2로 나눕니다. 나누기 전에 크기가 0이 되지 않도록 확입니다. 이를 통해 이미지가 정사각형 크기가 아닐 때를 처리할 수 있는데 이 경우 밉의 가로 크기는 1이 되었는데 세로 크기는 그렇지 않은 상태일 수도 있기 때문입니다. 이러한 상황이 생기면 나머지 레벨이 처리될 때까지 가로 크기는 1로 고정됩니다. + +```c++ + barrier.subresourceRange.baseMipLevel = mipLevels - 1; + barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; + barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; + + vkCmdPipelineBarrier(commandBuffer, + VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, + 0, nullptr, + 0, nullptr, + 1, &barrier); + + endSingleTimeCommands(commandBuffer); +} +``` + +명령 버퍼를 끝내기 전에 파이프라인 배리어를 하나 더 추가했습니다. 이 배리어는 마지막 밉 레벨을 `VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL`에서 `VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL`로 바꿉니다. 이 경우는 반복문에서 처리될 수 없는데, 마지막 밉 레벨은 blit이 수행되지 않기 때문입니다. + +끝으로 `createTextureImage`에 `generateMipmaps` 호출을 추가합니다: + +```c++ +transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, mipLevels); + copyBufferToImage(stagingBuffer, textureImage, static_cast(texWidth), static_cast(texHeight)); +//transitioned to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL while generating mipmaps +... +generateMipmaps(textureImage, texWidth, texHeight, mipLevels); +``` + +이제 텍스처 이미지의 밉맵들이 모두 채워집니다. + +## 선형 필터링 지원 + +`vkCmdBlitImage`와 같은 내장 함수를 통해 모든 밉 수준을 생성하는 것이 편리하지만, 안타깝게도 모든 플랫폼에서 지원이 보장된 것은 아닙니다. 우리가 사용하는 텍스처 이미지 포맷이 선형 필터링을 지원해야만 하고, 이는 `vkGetPhysicalDeviceFormatProperties` 함수를 통해 확인할 수 있습니다. `generateMipmaps` 함수에 확인 과정을 추가할 것입니다. + +먼저 이미지 포맷을 명시하는 매개변수를 추가합니다: + +```c++ +void createTextureImage() { + ... + + generateMipmaps(textureImage, VK_FORMAT_R8G8B8A8_SRGB, texWidth, texHeight, mipLevels); +} + +void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) { + + ... +} +``` + +`generateMipmaps` 함수에서 `vkGetPhysicalDeviceFormatProperties`를 사용해 텍스처 이미지 포맷에 대한 속성을 요청합니다: + +```c++ +void generateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels) { + + // Check if image format supports linear blitting + VkFormatProperties formatProperties; + vkGetPhysicalDeviceFormatProperties(physicalDevice, imageFormat, &formatProperties); + + ... +``` + +`VkFormatProperties` 구조체는 `linearTilingFeatures`, `optimalTilingFeatures`, `bufferFeatures` 필드를 갖는데 이들은 사용 방식에 따라서 포맷이 어떻게 사용될 수 있는지를 기술합니다. 우리는 텍스처 이미지를 최적 타일링 포맷으로 만들었기 때문에 `optimalTilingFeatures`를 확인해야 합니다. 선형 필터링 기능에 대한 지원을 확인하는 것은 `VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT`로 할 수 있습니다: + +```c++ +if (!(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT)) { + throw std::runtime_error("texture image format does not support linear blitting!"); +} +``` + +처리 방안의 대안은 두 가지가 있습니다. 선형 blit을 *지원하는* 일반적인 이미지 포맷을 찾는 함수를 작성할 수도 있고, 아니면 [stb_image_resize](https://github.com/nothings/stb/blob/master/stb_image_resize.h)와 같은 라이브러리를 사용해 소프트웨어적으로 밉맵 생성을 구현할 수도 있습니다. 원본 이미지를 로딩한 것과 같은 방식으로 각 밉 레벨을 로딩할 수 있습니다. + +중요한 것은 어쨌든 실제로는 밉맵 레벨을 런타임에 생성하는 것은 일반적이지 않은 경우라는 것입니다. 로딩 속도 향상을 위해 일반적으로 이들은 미리 생성되어서 텍스처 파일의 기본 레벨 옆에 저장됩니다. 소프트웨어적으로 크기를 변환한 뒤 여러 레벨을 한 파일로 로딩하는 것은 독자들을 위한 연습 문제로 남겨두겠습니다. + +## 샘플러 + +`VkImage`가 밉맵 데이터를 가지고 있으므로 `VkSampler`는 렌더링시에 데이터를 어떻게 읽어올 것인지를 제어할 수 있습니다. Vulkan은 `minLod`, `maxLod`, `mipLodBias`, `mipmapMode`를 명시할 수 있게 되어 있습니다("Lod"가 "디테일 레벨"을 의미합니다). 텍스처가 샘플링될 때, 샘플러는 아래 의사코드와 같은 방식으로 밉 레벨을 선택합니다: + +```c++ +lod = getLodLevelFromScreenSize(); //smaller when the object is close, may be negative +lod = clamp(lod + mipLodBias, minLod, maxLod); + +level = clamp(floor(lod), 0, texture.mipLevels - 1); //clamped to the number of mip levels in the texture + +if (mipmapMode == VK_SAMPLER_MIPMAP_MODE_NEAREST) { + color = sample(level); +} else { + color = blend(sample(level), sample(level + 1)); +} +``` + +`samplerInfo.mipmapMode`가 `VK_SAMPLER_MIPMAP_MODE_NEAREST`면, `lod`가 샘플링할 밉 레벨을 결정합니다. 밉맵 모드가 `VK_SAMPLER_MIPMAP_MODE_LINEAR`면, `lod`는 샘플링할 두 개의 밉 레벨을 결정합니다. 두 레벨에서 모두 샘플링이 되고 이들을 선형적으로 혼합한 결과가 반환됩니다. + +샘플링 연산 또한 `lod`에 영향을 미칩니다: + +```c++ +if (lod <= 0) { + color = readTexture(uv, magFilter); +} else { + color = readTexture(uv, minFilter); +} +``` + +물체가 카메라에 가까이 있으면 `magFilter`가 필터로 사용됩니다. 물체가 카메라에서 멀리 있으면 `minFilter`가 사용됩니다. 일반적으로 `lod`는 양수이고 카메라에 가까이 있을 경우에는 0입니다. `mipLodBias`를 통해 Vulkan에 일반적으로 적용되는 것보다 더 낮은 `lod`와 `level`을 사용하도록 하게 할 수 있습니다. + +이 챕터의 결과를 보기 위해서 `textureSampler`를 위한 값을 선택해야 합니다. `minFilter`와 `magFilter`에 대해서는 `VK_FILTER_LINEAR`를 이미 설정해 두었습니다. `minLod`, `maxLod`, `mipLodBias`, `mipmapMode`만 선택하면 됩니다. + +```c++ +void createTextureSampler() { + ... + samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + samplerInfo.minLod = 0.0f; // Optional + samplerInfo.maxLod = VK_LOD_CLAMP_NONE; + samplerInfo.mipLodBias = 0.0f; // Optional + ... +} +``` + +전체 밉 레벨을 사용하기 위해서 `minLod`는 0.0f로, `maxLod`는 `VK_LOD_CLAMP_NONE`로 설정했습니다. 이 값은 `1000.0f`와 같은데 텍스처의 모든 가능한 밉맵 레벨이 샘플링 가능하다는 뜻입니다. `lod` 값을 바꿀 이유는 없으므로 `mipLodBias`는 0.0f로 설정합니다. + +이제 프로그램을 실행하면 아래와 같은 결과를 볼 수 있습니다: + +![](/images/mipmaps.png) + +큰 차이는 없는데, 우리 장면이 아주 간단하기 때문입니다. 자세히 들여다보면 몇 가지 세세한 차이점은 있습니다. + +![](/images/mipmaps_comparison.png) + +가장 큰 차이점은 종이에 쓰여진 것들입니다. 밉맵을 사용하면 좀 더 부드럽게 표시됩니다. 맵밉이 없을 땐 모서리가 두드러지고 Moiré 패턴으로 인한 간격이 보입니다. + +샘플러 설정을 바꾸어 밉맵핑에 어떤 영향을 주는지 살펴보세요. 예를 들어 `minLod`를 바꾸면 샘플러가 가장 낮은 밉 레벨을 사용하지 않도록 할 수 있습니다: + +```c++ +samplerInfo.minLod = static_cast(mipLevels / 2); +``` + +위와 같은 설정으로 인해 아래와 같은 결과가 도출됩니다: + +![](/images/highmipmaps.png) + +이것이 물체가 카메라에서부터 멀어졌을 때 높은 레벨의 밉이 적용된 모습니다. + + +[C++ code](/code/29_mipmapping.cpp) / +[Vertex shader](/code/27_shader_depth.vert) / +[Fragment shader](/code/27_shader_depth.frag) diff --git a/kr/10_Multisampling.md b/kr/10_Multisampling.md new file mode 100644 index 00000000..146757e5 --- /dev/null +++ b/kr/10_Multisampling.md @@ -0,0 +1,291 @@ +## 서론 + +우리 프로그램은 이제 텍스처의 다양한 디테일 레벨을 로딩할 수 있어서 관찰자로부터 멀리 떨어져 있는 물체를 렌더링 할 때 발생하는 문제를 해결할 수 있게 되었습니다. 이제 이미지가 훨씬 부드럽게 보이지만 자세히 살펴보면 물체의 모서리를 따라서 톱날과 같은 들쭉날쭉한 패턴을 볼 수 있을 겁니다. 우리가 초반에 만든 사각형을 렌더링하는 프로그램과 같은 경우에 더 두드러집니다: + +![](/images/texcoord_visualization.png) + +이러한 의도하지 않은 효과는 "앨리어싱(aliasing)" 이라고 하며 렌더링에 사용하는 픽셀 수의 한계로 인해 나타나는 현상입니다. 무한한 해상도를 가진 디스플레이는 존재하지 않으니, 어느 정도는 피할 수 없는 현상이긴 합니다. 이 문제를 해결하는 여러 가지 방법이 있는데 이 챕터에서는 유명한 방법 중 하나인 [멀티샘플 안티앨리어싱(anti-aliasing), MSAA](https://en.wikipedia.org/wiki/Multisample_anti-aliasing)에 집중해 보도록 하겠습니다. + +일반적인 렌더링 과정에서 픽셀 색상은 하나의 샘플링 포인트로부터 결정되고, 일반적으로 이 포인트는 스크린의 각 픽셀의 중심점입니다. 어떤 선의 일부분이 픽셀을 지나긴 하지만 샘플링 포인트를 지나지는 않는다면, 해당 픽셀은 빈 픽셀이 되고 이로 인해 들쭉날쭉한 "계단" 현상이 발생합니다. + +![](/images/aliasing.png) + +MSAA가 하는 것은 이름 그대로 하나의 픽셀에 대해 여러 샘플링 포인트를 사용해서 최종 색상을 결정하는 것입니다. 예상할 수 있듯이 더 많은 샘플을 사용하면 더 좋은 결과를 얻을 수 있지만 연산량이 증가하게 됩니다. + +![](/images/antialiasing.png) + +우리의 구현에서는 사용 가능한 최대 샘플링 개수에 집중할 것입니다. 응용 프로그램에 따라 이러한 접근법보다는 품질이 만족되는 선에서 더 적은 샘플을 사용하는 것이 성능 면에서 더 나은 선택일 수 있습니다. + +## 가용한 샘플 개수 획득 + +먼저 우리 하드웨어가 얼마나 많은 샘플을 사용할 수 있는지부터 결정해 보겠습니다. 대부분의 현대 GPU들은 최소 8개의 샘플을 지원하지만 이 숫자가 항상 보장되는 것은 아닙니다. 새 플래스 멤버를 추가해서 추적해 보도록 하겠습니다: + +```c++ +... +VkSampleCountFlagBits msaaSamples = VK_SAMPLE_COUNT_1_BIT; +... +``` + +기본적으로 픽셀당 하나의 샘플을 사용하는데 이는 멀티샘플링을 적용하지 않는 것과 마찬가지입니다. 정확한 최대 샘플 개수는 선택된 물리적 장치와 관련된 `VkPhysicalDeviceProperties`를 통해 얻을 수 있습니다. 깊이 버퍼를 사용하고 있기 때문에 색상과 깊이에 대한 샘플 개수를 모두 고려할 필요가 있습니다. 두 개가 모두 지원하는 최대 샘플 개수가 최종적으로 사용할 최대 샘플 개수입니다. 이러한 정보를 획득하기 위한 함수를 추가합니다: + +```c++ +VkSampleCountFlagBits getMaxUsableSampleCount() { + VkPhysicalDeviceProperties physicalDeviceProperties; + vkGetPhysicalDeviceProperties(physicalDevice, &physicalDeviceProperties); + + VkSampleCountFlags counts = physicalDeviceProperties.limits.framebufferColorSampleCounts & physicalDeviceProperties.limits.framebufferDepthSampleCounts; + if (counts & VK_SAMPLE_COUNT_64_BIT) { return VK_SAMPLE_COUNT_64_BIT; } + if (counts & VK_SAMPLE_COUNT_32_BIT) { return VK_SAMPLE_COUNT_32_BIT; } + if (counts & VK_SAMPLE_COUNT_16_BIT) { return VK_SAMPLE_COUNT_16_BIT; } + if (counts & VK_SAMPLE_COUNT_8_BIT) { return VK_SAMPLE_COUNT_8_BIT; } + if (counts & VK_SAMPLE_COUNT_4_BIT) { return VK_SAMPLE_COUNT_4_BIT; } + if (counts & VK_SAMPLE_COUNT_2_BIT) { return VK_SAMPLE_COUNT_2_BIT; } + + return VK_SAMPLE_COUNT_1_BIT; +} +``` + +이제 이 함수를 사용해서 물리적 장치 선택 과정에서 `msaaSamples` 변수의 값을 설정할 것입니다. `pickPhysicalDevice` 함수를 조금만 변경하면 됩니다: + +```c++ +void pickPhysicalDevice() { + ... + for (const auto& device : devices) { + if (isDeviceSuitable(device)) { + physicalDevice = device; + msaaSamples = getMaxUsableSampleCount(); + break; + } + } + ... +} +``` + +## 렌더 타겟 설정 + +MSAA에서 각 픽셀은 오프스크린 버퍼에서 샘플링되고, 그 이후에 화면에 렌더링됩니다. 이 새로 등장한 버퍼는 지금까지 렌더링을 수행한 일반적인 이미지와는 약간 다릅니다. 각 픽셀에 하나 이상의 샘플을 저장합니다. 멀티샘플 버퍼가 생성되고 난 이후에 기본 프레임버퍼 (픽셀당 하나의 샘플을 저장하는)에 적용(resolve)되어야 합니다. 따라서 추가적인 렌더 타겟을 생성하고 그리기 과정을 수정해야만 합니다. 깊이 버퍼처럼 한 번에 하나의 그리기 연산만 활성화되기 때문에 렌더 타겟은 하나만 있으면 됩니다. 아래 클래스 멤버들을 추가합니다: + +```c++ +... +VkImage colorImage; +VkDeviceMemory colorImageMemory; +VkImageView colorImageView; +... +``` + +이 새로운 이미지가 픽셀당 의도한 숫자만큼의 샘플을 저장할 것이므로 그 숫자를 이미지 생성 과정에서 `VkImageCreateInfo`에 넘겨줘야 합니다. `createImage` 함수에 `numSamples` 매개변수를 추가합니다: + +```c++ +void createImage(uint32_t width, uint32_t height, uint32_t mipLevels, VkSampleCountFlagBits numSamples, VkFormat format, VkImageTiling tiling, VkImageUsageFlags usage, VkMemoryPropertyFlags properties, VkImage& image, VkDeviceMemory& imageMemory) { + ... + imageInfo.samples = numSamples; + ... +``` + +지금은 이 함수를 호출하는 모든 부분에서 `VK_SAMPLE_COUNT_1_BIT`를 사용하도록 수정합니다. 구현을 진행하면서 이 부분들을 적절한 값으로 대체할 것입니다: + +```c++ +createImage(swapChainExtent.width, swapChainExtent.height, 1, VK_SAMPLE_COUNT_1_BIT, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory); +... +createImage(texWidth, texHeight, mipLevels, VK_SAMPLE_COUNT_1_BIT, VK_FORMAT_R8G8B8A8_SRGB, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory); +``` + +이제 멀티샘플 색상 버퍼를 생성할 차례입니다. `createColorResources` 함수를 추가할 것인데 여기서는 `createImage`의 매개변수로 `msaaSamples`를 사용한 것에 주의하십시오. 또한 밉 레벨은 하나만 사용할 것인데 Vulkan 명세에서 픽셀당 샘플이 하나 이상인 경우에는 반드시 이렇게 하도록 요구하고 있습니다. 또한 어차피 텍스처로 활용할 것이 아니기 때문에 밉맵이 필요하지 않습니다: + +```c++ +void createColorResources() { + VkFormat colorFormat = swapChainImageFormat; + + createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, colorFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, colorImage, colorImageMemory); + colorImageView = createImageView(colorImage, colorFormat, VK_IMAGE_ASPECT_COLOR_BIT, 1); +} +``` + +일관성을 위해 `createDepthResources` 바로 앞에서 이 함수를 호출합니다: + +```c++ +void initVulkan() { + ... + createColorResources(); + createDepthResources(); + ... +} +``` + +이제 멀티샘플 색상 버퍼가 준비되었으니 깊이 값을 처리할 차례입니다. `createDepthResources`를 수정하여 깊이 버퍼에 사용할 샘플 개수를 적용합니다: + +```c++ +void createDepthResources() { + ... + createImage(swapChainExtent.width, swapChainExtent.height, 1, msaaSamples, depthFormat, VK_IMAGE_TILING_OPTIMAL, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, depthImage, depthImageMemory); + ... +} +``` + +Vulkan 리소스를 추가적으로 만든 것이니 적절한 시점에 해제하는 것도 잊으면 안됩니다: + +```c++ +void cleanupSwapChain() { + vkDestroyImageView(device, colorImageView, nullptr); + vkDestroyImage(device, colorImage, nullptr); + vkFreeMemory(device, colorImageMemory, nullptr); + ... +} +``` + +`recreateSwapChain`를 수정하여 윈도우 크기가 변하면 적절한 해상도로 색상 이미지를 다시 생성하도록 합니다: + +```c++ +void recreateSwapChain() { + ... + createImageViews(); + createColorResources(); + createDepthResources(); + ... +} +``` + +이제 초기 MSAA 설정은 끝났고, 그래픽스 파이프라인, 프레임버퍼, 렌더패스에서 새로 만든 리소스를 사용하도록 하여 결과를 살펴보겠습니다! + +## 새로운 어태치먼트 추가 + +먼저 렌더 패스부터 작업합니다. `createRenderPass`의 색상과 깊이 어태치먼트 생성 정보 구조체를 수정합니다: + +```c++ +void createRenderPass() { + ... + colorAttachment.samples = msaaSamples; + colorAttachment.finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + ... + depthAttachment.samples = msaaSamples; + ... +``` + +`finalLayout`을 `VK_IMAGE_LAYOUT_PRESENT_SRC_KHR`에서 `VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL`로 수정한 것을 눈치채셨을 겁니다. 그 이유는 멀티샘플링된 이미지가 바로 표시될 수 없기 때문입니다. 이 이미지를 일반적인 이미지에 먼저 적용(resolve)해야만 합니다. 이러한 요구사항이 깊이 버퍼에 대해서는 적용되지 않는데, 어쨌든 화면에 표시되지 않는 버퍼이기 때문입니다. 따라서 색상에 대한 어태치먼트만 추가하면 되고, 이를 적용 어태치먼트라고 하겠습니다: + +```c++ + ... + VkAttachmentDescription colorAttachmentResolve{}; + colorAttachmentResolve.format = swapChainImageFormat; + colorAttachmentResolve.samples = VK_SAMPLE_COUNT_1_BIT; + colorAttachmentResolve.loadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + colorAttachmentResolve.storeOp = VK_ATTACHMENT_STORE_OP_STORE; + colorAttachmentResolve.stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE; + colorAttachmentResolve.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; + colorAttachmentResolve.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + colorAttachmentResolve.finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; + ... +``` + +이제 렌더 패스가 멀티샘플링된 색상 이미지를 일반적인 어태치먼트로 적용하도록 명시해야만 합니다. 적용의 대상이 되는 색상 버퍼를 참조인 어태치먼트를 새로 만듭니다: + +```c++ + ... + VkAttachmentReference colorAttachmentResolveRef{}; + colorAttachmentResolveRef.attachment = 2; + colorAttachmentResolveRef.layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL; + ... +``` + +`pResolveAttachments` 서브패스 구조체 멤버가 새로 만든 어태치먼트 참조를 가리키도록 설정합니다. 이렇게 하면 렌더 패스가 멀티샘플 적용 연산을 정의하여 이미지를 화면에 렌더링 할 수 있습니다: + +``` + ... + subpass.pResolveAttachments = &colorAttachmentResolveRef; + ... +``` + +멀티샘플링된 색상 이미지를 재사용하고 있으므로 `VkSubpassDependency`의 `srcAccessMask`를 수정해야 합니다. 이러한 수정을 통해 색상 어태치먼트로의 쓰기 연산이 이후의 연산이 시작되기 전에 완료될 수 있어서 쓰기 연산의 중복으로 인해 발생할 수 있는 불안정한 렌더링 문제를 해결할 수 있습니다: + +```c++ + ... + dependency.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; + ... +``` + +이제 렌더 패스 정보 구조체를 새로운 색상 어태치먼트로 갱신합니다: + +```c++ + ... + std::array attachments = {colorAttachment, depthAttachment, colorAttachmentResolve}; + ... +``` + +렌더 패스가 준비되면 `createFramebuffers`를 수정하여 새로운 이미지 뷰 들을 목록에 추가합니다: + +```c++ +void createFramebuffers() { + ... + std::array attachments = { + colorImageView, + depthImageView, + swapChainImageViews[i] + }; + ... +} +``` + +마지막으로 `createGraphicsPipeline`를 수정해 새로 만들어진 파이프라인에 샘플을 하나 이상 사용하도록 명시합니다: + +```c++ +void createGraphicsPipeline() { + ... + multisampling.rasterizationSamples = msaaSamples; + ... +} +``` + +이제 프로그램을 실행하면 아래와 같은 화면이 보입니다: + +![](/images/multisampling.png) + +밉맵핑처럼 변화가 확 눈에 들어오지는 않습니다. 자세히 살펴보면 모서리가 이제 더이상 들쭉날쭉하지 않고, 전체적으로 그 전의 이미지보다 부드러워 진 것을 확인할 수 있습니다. + +![](/images/multisampling_comparison.png) + +모서리 부분을 확대해 보면 차이점이 좀 더 눈에 띕니다: + +![](/images/multisampling_comparison2.png) + +## 품질 향상 + +현재의 MSAA 구현은 좀 더 복잡한 장면의 경우에 대해서는 품질에 문제가 발생할 수 있습니다. 에를 들어, 셰이더 앨리어싱으로 인해 발생될 수 있는 잠재적인 문제는 해결하고 있지 않습니다. 즉, MSAA는 모서리를 부드럽게만 할 뿐, 내부에 채워진 값에 대해서는 그렇지 못합니다. 이로 인해 예를 들어 폴리곤 자체는 부드럽게 표현되지만 적용된 색상에 대해서는 색상 대조가 큰 경우 앨리어싱이 발생하게 됩니다. 이 문제에 대한 접근법 중 하나로 [샘플 세이딩](https://www.khronos.org/registry/vulkan/specs/1.3-extensions/html/chap27.html#primsrast-sampleshading)을 활성화하는 것이 있는데, 이렇게 하면 이미지 품질을 더 높일 수 있지만 더 높은 계산 비용이 발생합니다: + +```c++ + +void createLogicalDevice() { + ... + deviceFeatures.sampleRateShading = VK_TRUE; // enable sample shading feature for the device + ... +} + +void createGraphicsPipeline() { + ... + multisampling.sampleShadingEnable = VK_TRUE; // enable sample shading in the pipeline + multisampling.minSampleShading = .2f; // min fraction for sample shading; closer to one is smoother + ... +} +``` + +이 예시에서는 샘플 세이딩을 비활성화 한 채로 놔둘 것이지만 경우에 따라 이러한 품질의 차이가 크게 눈에 뜨일수도 있습니다: + +![](/images/sample_shading.png) + +## 결론 + +여기에 오기까지 힘드셨을 것이지만, 이제 Vulkan 프로그램에 대한 기본 지식을 갖게 되셨을 겁니다. 여러분이 갖게 된 Vulkan의 기본 원리에 대한 지식은, 더 다양한 기능들을 살펴보기 위한 배경으로 충분할 것입니다: + +* Push constants +* Instanced rendering +* Dynamic uniforms +* Separate images and sampler descriptors +* Pipeline cache +* Multi-threaded command buffer generation +* Multiple subpasses +* Compute shaders + +현재 만들어진 프로그램은 다양한 방식으로 확장될 수 있는데, Blinn-Phong 라이팅을 추가한다거나, 후처리 효과를 더한다거나, 그림자 맵핑을 수행하는 등이 있을 겁니다. 다른 API의 튜토리얼을 통해 이러한 효과들이 작동하는 방식을 배우실 수 있을 겁니다. Vulkan은 명시성이라는 특징이 있긴 하지만 대부분의 컨셉은 비슷하기 때문입니다. + +[C++ code](/code/30_multisampling.cpp) / +[Vertex shader](/code/27_shader_depth.vert) / +[Fragment shader](/code/27_shader_depth.frag) diff --git a/kr/11_Compute_Shader.md b/kr/11_Compute_Shader.md new file mode 100644 index 00000000..aedd6459 --- /dev/null +++ b/kr/11_Compute_Shader.md @@ -0,0 +1,653 @@ +## 서론 + +이 추가 챕터에서는 컴퓨트 셰이더에 대해 알아보겠습니다. 이전 챕터에서는 Vulkan 파이프라인의 전통적인 그래픽 부분에 대해 다뤘습니다. 그러나 OpenGL과 같은 이전 API와 달리 Vulkan에서는 컴퓨트 셰이더 지원이 필수 사항이 되었습니다. 이는 고성능 데스크톱 GPU이든 저전력 임베디드 장치이든 상관없이 모든 Vulkan 구현에서 컴퓨트 셰이더를 사용할 수 있다는 것을 의미합니다. + +이로써 응용 프로그램이 실행되는 장치의 종류에 관계없이 일반 목적으로의 GPU 컴퓨팅(GPGPU)을 수행할 수 있게 되었습니다. GPU에서 일반 계산을 수행할 수 있다는 것은 과거에는 CPU의 영역이었던 많은 작업을 GPU에서 실시간으로 수행할 수 있다는 것을 의미합니다. GPU가 점점 더 강력하고 유연해지면서, CPU의 일반 목적 기능이 필요한 많은 작업이 GPU에서 실시간으로 수행될 수 있게 되었습니다. + +GPU의 컴퓨트 기능을 사용할 수 있는 몇 가지 예는 이미지 조작, 가시성 테스트, 후처리, 고급 조명 계산, 애니메이션, 물리(예를 들어 파티클 시스템) 등이 있습니다. 게다가 컴퓨트를 사용하여 그래픽 출력이 필요하지 않은 비시각적인 계산 작업, 예를 들어 숫자 계산이나 AI 관련 작업도 가능합니다. 이를 "헤드리스 컴퓨트"라고 합니다. + +## 장점 + +GPU에서 컴퓨팅 비용이 높은 연산을 하는 것에는 몇 가지 장점이 있습니다. 가장 명확한 것은 CPU에서의 연산 비용을 줄일 수 있다는 것입니다. 또 다른 장점은 CPU의 메인 메모리에서 GPU의 메모리로 데이터를 옮길 필요가 없다는 것입니다. 모든 데이터는 GPU 내에 상주할 수 있으므로 메인 메모리로부터의 느린 전송을 기다릴 필요가 없습니다. + +이외에도 GPU는 몇 만 개의 작은 연산 유닛을 가진 병렬화된 연산 장치입니다. 이로 인해 몇 개의 큰 연산 유닛을 가진 CPU보다 병렬화된 연산에 더 적합합니다. + +## Vulkan 파이프라인 + +파이프라인의 그래픽스 부분과 컴퓨트 부분이 완전히 분리되어 있다는 사실을 이해하고 있는 것이 중요합니다. 이는 Vulkan 공식 명세에서 가져온 아래 Vulkan 파이프라인에 대한 블록 다이어그램을 통해서도 확인할 수 있습니다: + +![](/images/vulkan_pipeline_block_diagram.png) + +이 다이어그램에서 파이프라인의 일반적인 그래픽스 부분은 왼쪽에 표시되어 있고, 이러한 그래픽스 파트가 아닌 부분은 오른쪽에 표시되어 있는데 컴퓨트 셰이더(스테이지)도 오른쪽에 표시되어 있습니다. 컴퓨트 셰이더 스테이지가 그래픽스 파이프라인과 분리되어 있으므로 언제든 필요할 때 사용할 수 있습니다. 이는 예를 들어 프래그먼트 셰이더가 항상 정점 셰이더의 변환된 출력값을 활용해야 하는 것과는 아주 다르다고 볼 수 있습니다. + +다이어그램의 중간에는 컴퓨트에도 사용되는 기술자 집합 등이 표시되어 있습니다. 따라서 우리가 배웠던 기술자 레이아웃, 기술자 집합 등이 여기에서도 활용될 것입니다. + +## 예시 + +이해하기 쉬운 예시로 이 챕터에서는 GPU 기반의 파티클(particle) 시스템을 구현해 볼 것입니다. 이러한 시스템은 여러 게임에서 활용되며 몇 천개의 파티클들을 실시간에 갱신해야 합니다. 이러한 시스템을 렌더링 하기 위해서는 두 가지 주요 구성요소가 필요합니다. 정점 버퍼에서 전달된 정점들과, 이들을 수식(equation)에 기반하여 갱신하는 방법입니다. + +"전통적인" CPU 기반의 파티클 시스템은 파티클 데이터를 시스템의 메인 메모리에 저장해 두고 CPU를 사용해 갱신하였습니다. 갱신이 끝나면 GPU의 메모리로 정점들이 다시 전달되어 다음 프레임에 갱신된 파티클의 위치가 표시될 수 있도록 해야만 했습니다. 가장 간단한 방법으로는 각 프레임마다 새로운 데이터로 정점 버퍼를 다시 만드는 방법이 있습니다. 물론 아주 높은 비용이 발생하죠. 구현에 따라 GPU 메모리를 맵핑하여 CPU로부터 값을 쓸 수 있게 한다거나 (데스크톱 시스템에서는 "resizable BAR", 내장 GPU에서는 통합 메모리라고 불립니다) 아니면 그냥 호스트의 지역 버퍼 (PCI-E 대역폭 문제로 아주 느립니다)를 사용하는 방법이 있습니다. 어떤 방법을 선택하든 CPU에서 갱신된 파티클이 "왕복(round-trip)"해야 한다는 요구사항이 생깁니다. + +GPU 기반의 파티클 시스템에서는 이러한 왕복이 필요하지 않습니다. 정점은 처음에 GPU로 업로드되기만 하고, 이후의 모든 갱신은 컴퓨트 셰이더를 사용해 GPU의 메모리에서 이루어집니다. 이 방법이 빠른 가장 주요한 이유는 GPU와 GPU의 지역 메모리 사이의 대역폭이 훨씬 크기 때문입니다. CPU 기반의 시나리오에서는 메인 메모리와 PCI-express 대역폭으로 인해 속도가 제한되는데 이는 GPU의 메모리 대역폭에 비해 훨씬 작습니다. + +이러한 작업이 GPU의 컴퓨트 큐에서 이루어진다면 그래픽스 파이프라인의 렌더링 부분과 파티클의 갱신을 병렬적으로 수행할 수 있습니다. 이를 "비동기 컴퓨트"라 하고, 이 튜토리얼에서는 다루지 않는 고급 주제입니다. + +아래는 이 챕터 코드의 실행 예시입니다. 이 파티클들은 GPU의 컴퓨트 셰이더에서 직접 갱신되며, CPU와의 상호작용은 없습니다: + +![](/images/compute_shader_particles.png) + +## 데이터 조작(manipulation) + +이 튜토리얼을 통해 이미 정점 버퍼, 인덱스 버퍼를 통해 프리미티브 데이터를 전달하는 방법과 유니폼 버퍼를 통해 셰이더에 데이터를 전달하는 법 등을 배웠습니다. 또 이미지를 사용해 텍스처 맵핑을 하는 법도요. 하지만 지금까지 우리는 항상 CPU에서 데이터를 쓰고, GPU에서 그 데이터를 읽기만 했습니다. + +컴퓨트 셰이더에서 소개하는 중요한 개념은 버터의 데이터를 읽고 **쓰는** 기능입니다. 이를 위해 Vulkan은 두 종류의 스토리지를 제공합니다. + +### 셰이더 스토리지 버퍼 객체(Shader storage buffer objects, SSBO) + +셰이더 스토리지 버퍼(SSBO)를 통해 셰이더가 버퍼의 데이터를 읽고 쓸 수 있습니다. 사용 방법은 유니폼 버퍼 객체와 비슷합니다. 가장 큰 차이는 다른 버퍼 타입을 SSBO로 사용할 수 있어서 임의의 크기로 사용할 수 있다는 점입니다. + +GPU 기반의 파티클 시스템으로 돌아가서, 정점의 갱신(쓰기)를 어떻게 컴퓨트 셰이더로 수행하고 읽기(그리기)는 정점 셰이더로 수행하는지 의아하실겁니다. 왜냐하면 두 사용법이 서로 다른 버퍼 타입을 요구하는 것 같아 보이기 때문입니다. + +하지만 그렇지 않습니다. Vulkan에서는 버퍼와 이미지에 여러 사용법을 명시할 수 있습니다. 따라서 파티클 정점 버퍼를 (그래픽스 패스에서) 정점 버퍼로 활용하고 (컴퓨트 패스에서) 스토리지 버퍼로도 사용할 수 있습니다. 단지 버퍼를 만들 때 두 개의 사용법 플래그를 명시해주면 됩니다: + +```c++ +VkBufferCreateInfo bufferInfo{}; +... +bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT; +... + +if (vkCreateBuffer(device, &bufferInfo, nullptr, &shaderStorageBuffers[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create vertex buffer!"); +} +``` + +`bufferInfo.usage`에 명시한 `VK_BUFFER_USAGE_VERTEX_BUFFER_BIT`와 `VK_BUFFER_USAGE_STORAGE_BUFFER_BIT` 두 개의 플래그는 이 버퍼를 두 개의 시나리오에서 사용할 것이라는 뜻입니다. 정점 셰이더에서는 정점 버퍼로, 그리고 그리고 스토리지 버퍼로 말이죠. `VK_BUFFER_USAGE_TRANSFER_DST_BIT` 플래그 또한 추가해서 호스트에서 GPU로 데이터를 전송할 수 있도록도 한 것에 주의하세요. 셰이더 스토리지 버퍼가 GPU 메모리에만 상주해 있길 원하므로(`VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT`), 호스트에서 이 버퍼로 데이터를 전송해야만 합니다. + +`createBuffer` 헬퍼 함수를 통한 구현은 아래와 같습니다: + +```c++ +createBuffer(bufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, shaderStorageBuffers[i], shaderStorageBuffersMemory[i]); +``` + +이러한 버퍼에 접근하기 위한 GLSL 셰이더에서의 선언은 아래와 같습니다: + +```glsl +struct Particle { + vec2 position; + vec2 velocity; + vec4 color; +}; + +layout(std140, binding = 1) readonly buffer ParticleSSBOIn { + Particle particlesIn[ ]; +}; + +layout(std140, binding = 2) buffer ParticleSSBOOut { + Particle particlesOut[ ]; +}; +``` + +이 예제에서는 타입이 명시된 SSBO를 정의했는데 각 파티클은 위치와 속도(`Particle` 구조체 참고)를 가지고 있습니다. SSBO는 `[]`를 통해 명시되지 않은 개수의 파티클을 가지도록 했습니다. SSBO에 원소의 개수를 명시하지 않아도 되는 것도 예를 들자면 유니폼 버퍼와 비교했을 때의 장점입니다. `std140`는 메모리 레이아웃 한정자로 셰이더 스토리지 버퍼의 원소들이 어떻게 메모리에 정렬되어있는지를 결정합니다. 이를 통해 호스트와 GPU 사이의 버퍼를 맵핑하는 데 필요한 요구사항이 만족되었다고 보장합니다. + +스토리지 버퍼 객체에 컴퓨트 셰이더를 통해 쓰기를 수행하는 것은 어렵지 않은데, C++ 쪽에서 버퍼에 쓰기를 수행하는 것과 비슷합니다: + +```glsl +particlesOut[index].position = particlesIn[index].position + particlesIn[index].velocity.xy * ubo.deltaTime; +``` + +### 스토리지 이미지 + +*이 챕터에서 이미지 조작을 수행하지는 않을 것입니다. 이 문단은 컴퓨트 셰이더를 통해 이미지 조작도 가능하다는 것을 독자들에게 알려주기 위함입니다.* + +스토리지 이미지는 이미지에 읽고 쓰기를 가능하게 해줍니다. 일반적인 사용법으로는 텍스처에 이미지 이펙트를 적용한다거나, 후처리를 수행한다거나 (둘 다 비슷합니다), 밉맵을 생성하는 것입니다. + +이미지에 대해서도 비슷합니다: + +```c++ +VkImageCreateInfo imageInfo {}; +... +imageInfo.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT; +... + +if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) { + throw std::runtime_error("failed to create image!"); +} +``` + +`imageInfo.usage`에 설정된 `VK_IMAGE_USAGE_SAMPLED_BIT`과 `VK_IMAGE_USAGE_STORAGE_BIT`는 이 이미지를 서로 다른 두 시나리오에 사용할 것을 명시합니다. 프래그먼트에서 샘플링될 이미지와 컴퓨트 셰이더에서의 스토리지 이미지 입니다. + +GLSL 셰이더에서의 스토리지 이미지 선언은 프래그먼트 셰이더에서의 샘플링된 이미지의 사용과 비슷합니다: + +```glsl +layout (binding = 0, rgba8) uniform readonly image2D inputImage; +layout (binding = 1, rgba8) uniform writeonly image2D outputImage; +``` + +몇 가지 차이점은 이미지 포맷을 명시하기 위한 `rgba8`과 같은 어트리뷰트와 `readonly`와 `writeonly` 한정자를 통해서 입력 이미지는 읽기만, 출력 이미지는 쓰기만 수행할 것이라는 것을 명시한 점입니다. 또한 스토리지 이미지 선언을 위해 `image2D` 타입을 명시하였습니다. + +컴퓨트 셰이더에서 스토리지 이미지를 읽고 쓰는 것은 `imageLoad`와 `imageStore`를 통해 수행됩니다: + +```glsl +vec3 pixel = imageLoad(inputImage, ivec2(gl_GlobalInvocationID.xy)).rgb; +imageStore(outputImage, ivec2(gl_GlobalInvocationID.xy), pixel); +``` + +## 컴퓨트 큐 패밀리 + +[물리적 장치와 큐 패밀리 챕터](03_Drawing_a_triangle/00_Setup/03_Physical_devices_and_queue_families.md#page_Queue-families)에서 큐 패밀리가 무엇인지와 그래픽스 큐 패밀리는 선택하는 방법을 배웠습니다. 컴퓨트의 경우 `VK_QUEUE_COMPUTE_BIT` 플래그의 큐 패밀리 속성을 사용합니다. 따라서 컴퓨트 작업을 하려면 컴퓨트를 지원하는 큐 패밀리로부터 큐를 얻어와야 합니다. + +Vulkan은 그래픽스와 컴퓨트 연산을 모두 지원하는 큐 패밀리를 적어도 하나 갖는 그래픽스 연산을 지원하는 구현이 필요합니다. 하지만 구현이 전용 컴퓨트 큐를 제공해도 됩니다. 이러한 전용 컴퓨트 큐(그래픽스를 포함하지 않는)는 비동기 컴퓨트 큐임을 암시합니다. 이 튜토리얼에서는 좀 더 쉬운 안내를 위해 그래픽스와 컴퓨트 연산을 모두 지원하는 큐를 사용할 것입니다. 이렇게 하면 추가적인 비동기 메커니즘 또한 필요하지 않습니다. + +우리 예제에서는 장치 생성 코드를 일부 수정해야 합니다: + +```c++ +uint32_t queueFamilyCount = 0; +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, nullptr); + +std::vector queueFamilies(queueFamilyCount); +vkGetPhysicalDeviceQueueFamilyProperties(device, &queueFamilyCount, queueFamilies.data()); + +int i = 0; +for (const auto& queueFamily : queueFamilies) { + if ((queueFamily.queueFlags & VK_QUEUE_GRAPHICS_BIT) && (queueFamily.queueFlags & VK_QUEUE_COMPUTE_BIT)) { + indices.graphicsAndComputeFamily = i; + } + + i++; +} +``` + +수정된 큐 패밀리 인덱스 선택 코드는 이제 그래픽스와 컴퓨트를 모두 지원하는 큐 패밀리를 찾게 됩니다. + +`createLogicalDevice`에서는 이 큐 패밀리로부터 컴퓨트 큐를 얻습니다: + +```c++ +vkGetDeviceQueue(device, indices.graphicsAndComputeFamily.value(), 0, &computeQueue); +``` + +## 컴퓨트 셰이더 스테이지 + +그래픽스 예제에서 셰이더를 로드하는 부분과 기술자에 접근하는 별도의 파이프라인 스테이지가 있었습니다. 컴퓨트 셰이더 또한 비슷한 방법으로 `VK_SHADER_STAGE_COMPUTE_BIT` 파이프라인을 통해 접근됩니다. 따라서 컴퓨트 셰이더를 로드하는 것 또한 정점 셰이더를 로드하는 것과 동일하지만, 셰이더 스테이지가 다를 뿐입니다. 다음 부분에서 이 내용에 대해 자세히 알아볼 것입니다. 컴퓨트는 또한 기술자와 파이프라인에 `VK_PIPELINE_BIND_POINT_COMPUTE`라는 새로운 바인딩 포인트를 필요로 하고, 이를 사용할 예정입니다. + +## 컴퓨트 셰이더 로딩 + +우리 프로그램에서 컴퓨트 셰이더를 로딩하는 것은 다른 셰이더 로딩과 다를 바 없습니다. 차이점은 위에서 이야기한대로 `VK_SHADER_STAGE_COMPUTE_BIT`를 사용해야 한다는 것 뿐입니다. + +```c++ +auto computeShaderCode = readFile("shaders/compute.spv"); + +VkShaderModule computeShaderModule = createShaderModule(computeShaderCode); + +VkPipelineShaderStageCreateInfo computeShaderStageInfo{}; +computeShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO; +computeShaderStageInfo.stage = VK_SHADER_STAGE_COMPUTE_BIT; +computeShaderStageInfo.module = computeShaderModule; +computeShaderStageInfo.pName = "main"; +... +``` + +## 셰이더 스토리지 버퍼 준비 + +이전에 임의의 데이터를 컴퓨트 셰이더에 넘기기 위해 셰이더 스토리지 버퍼를 사용해야 한다는 것을 배웠습니다. 이 예제에서 파티클의 배열을 GPU로 넘겨서 GPU의 메모리에서 직접 이 데이터들을 조작할 수 있도록 할 것입니다. + +[여러 프레임의 사용](03_Drawing_a_triangle/03_Drawing/03_Frames_in_flight.md) 챕터에서 프레임별 리소스를 복제하는 방법을 통해 CPU와 GPU 연산을 동시에 수행할 수 있도록 하였습니다. 먼저 버퍼 객체를 위한 벡터와 이를 지원하는 장치 메모리를 선언합니다: + +```c++ +std::vector shaderStorageBuffers; +std::vector shaderStorageBuffersMemory; +``` + +`createShaderStorageBuffers`에서 이 벡터의 크기를 최대값에 맞게 설정합니다. 사용하는 프레임의 개수입니다: + +```c++ +shaderStorageBuffers.resize(MAX_FRAMES_IN_FLIGHT); +shaderStorageBuffersMemory.resize(MAX_FRAMES_IN_FLIGHT); +``` + +이제 초기 파티클 정보를 GPU로 넘겨줄 수 있습니다. 먼저 호스트 쪽에서 파티클의 벡터를 초기화 합니다: + +```c++ + // Initialize particles + std::default_random_engine rndEngine((unsigned)time(nullptr)); + std::uniform_real_distribution rndDist(0.0f, 1.0f); + + // Initial particle positions on a circle + std::vector particles(PARTICLE_COUNT); + for (auto& particle : particles) { + float r = 0.25f * sqrt(rndDist(rndEngine)); + float theta = rndDist(rndEngine) * 2 * 3.14159265358979323846; + float x = r * cos(theta) * HEIGHT / WIDTH; + float y = r * sin(theta); + particle.position = glm::vec2(x, y); + particle.velocity = glm::normalize(glm::vec2(x,y)) * 0.00025f; + particle.color = glm::vec4(rndDist(rndEngine), rndDist(rndEngine), rndDist(rndEngine), 1.0f); + } + +``` + +그리고 [스테이징 버퍼](04_Vertex_buffers/02_Staging_buffer.md)를 호스트의 메모리에 만들어 초기 파티클 속성을 저장합니다: + +```c++ + VkDeviceSize bufferSize = sizeof(Particle) * PARTICLE_COUNT; + + VkBuffer stagingBuffer; + VkDeviceMemory stagingBufferMemory; + createBuffer(bufferSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory); + + void* data; + vkMapMemory(device, stagingBufferMemory, 0, bufferSize, 0, &data); + memcpy(data, particles.data(), (size_t)bufferSize); + vkUnmapMemory(device, stagingBufferMemory); +``` + +이 스테이징 버퍼를 소스로 해서 프레임별 셰이더 스토리지 버퍼를 만들고 파티클 속성을 각각의 스테이징 버퍼에 복사합니다: + +```c++ + for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + createBuffer(bufferSize, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, shaderStorageBuffers[i], shaderStorageBuffersMemory[i]); + // Copy data from the staging buffer (host) to the shader storage buffer (GPU) + copyBuffer(stagingBuffer, shaderStorageBuffers[i], bufferSize); + } +} +``` + +## 기술자 + +컴퓨트를 위한 기술자를 설정하는 것은 그래픽스에서와 거의 동일합니다. 유일한 차이점은 기술자가 `VK_SHADER_STAGE_COMPUTE_BIT`가 설정되어 있어서 컴퓨트 스테이지에서 접근 가능해야 한다는 것입니다: + +```c++ +std::array layoutBindings{}; +layoutBindings[0].binding = 0; +layoutBindings[0].descriptorCount = 1; +layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +layoutBindings[0].pImmutableSamplers = nullptr; +layoutBindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; +... +``` + +셰이더 스테이지를 여기에서 결합할 수 있기 때문에 정점과 컴퓨트 스테이지에서 기술자를 접근 가능하게 하고 싶다면 (예를 들자면 유니폼 버퍼가 양 셰이더에서 매개변수를 공유하게 하고 싶다면) 두 스테이지를 모두 설정하면 됩니다: + +```c++ +layoutBindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_COMPUTE_BIT; +``` + +우리의 예제에 대한 기술자 설정은 이와 같습니다. 레이아웃은 아래와 같습니다: + +```c++ +std::array layoutBindings{}; +layoutBindings[0].binding = 0; +layoutBindings[0].descriptorCount = 1; +layoutBindings[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; +layoutBindings[0].pImmutableSamplers = nullptr; +layoutBindings[0].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + +layoutBindings[1].binding = 1; +layoutBindings[1].descriptorCount = 1; +layoutBindings[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; +layoutBindings[1].pImmutableSamplers = nullptr; +layoutBindings[1].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + +layoutBindings[2].binding = 2; +layoutBindings[2].descriptorCount = 1; +layoutBindings[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; +layoutBindings[2].pImmutableSamplers = nullptr; +layoutBindings[2].stageFlags = VK_SHADER_STAGE_COMPUTE_BIT; + +VkDescriptorSetLayoutCreateInfo layoutInfo{}; +layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; +layoutInfo.bindingCount = 3; +layoutInfo.pBindings = layoutBindings.data(); + +if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &computeDescriptorSetLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute descriptor set layout!"); +} +``` + +이 설정을 보면 셰이더 스토리지 버퍼 객체에 대해 왜 두 레이아웃 바인딩이 있는지 의문이 드실겁니다. 파티클 시스템은 하나만 렌더링 하는데도요. 그 이유는 파티클의 위치가 프레임별 시간에 따라 갱신될 것이기 때문입니다. 즉 각 프레임에서 이전 프레임에서의 파티클 위치를 알아야 하고, 그 위치들을 프레임간 소요 시간(delta time)을 기반으로 갱신한 뒤 자신 SSBO에 쓰게 됩니다: + +![](/images/compute_ssbo_read_write.svg) + +이렇게 하려면 컴퓨트 셰이더가 이전과 현재 프레임의 SSBO에 접근 가능해야 합니다. 기술자 설정 과정에서 셰이더에 둘 다 넘겨주면 됩니다. `storageBufferInfoLastFrame`과 `storageBufferInfoCurrentFrame`를 살펴 봅시다: + +```c++ +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + VkDescriptorBufferInfo uniformBufferInfo{}; + uniformBufferInfo.buffer = uniformBuffers[i]; + uniformBufferInfo.offset = 0; + uniformBufferInfo.range = sizeof(UniformBufferObject); + + std::array descriptorWrites{}; + ... + + VkDescriptorBufferInfo storageBufferInfoLastFrame{}; + storageBufferInfoLastFrame.buffer = shaderStorageBuffers[(i - 1) % MAX_FRAMES_IN_FLIGHT]; + storageBufferInfoLastFrame.offset = 0; + storageBufferInfoLastFrame.range = sizeof(Particle) * PARTICLE_COUNT; + + descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[1].dstSet = computeDescriptorSets[i]; + descriptorWrites[1].dstBinding = 1; + descriptorWrites[1].dstArrayElement = 0; + descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[1].descriptorCount = 1; + descriptorWrites[1].pBufferInfo = &storageBufferInfoLastFrame; + + VkDescriptorBufferInfo storageBufferInfoCurrentFrame{}; + storageBufferInfoCurrentFrame.buffer = shaderStorageBuffers[i]; + storageBufferInfoCurrentFrame.offset = 0; + storageBufferInfoCurrentFrame.range = sizeof(Particle) * PARTICLE_COUNT; + + descriptorWrites[2].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; + descriptorWrites[2].dstSet = computeDescriptorSets[i]; + descriptorWrites[2].dstBinding = 2; + descriptorWrites[2].dstArrayElement = 0; + descriptorWrites[2].descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; + descriptorWrites[2].descriptorCount = 1; + descriptorWrites[2].pBufferInfo = &storageBufferInfoCurrentFrame; + + vkUpdateDescriptorSets(device, 3, descriptorWrites.data(), 0, nullptr); +} +``` + +기술자 풀에서 SSBO의 기술자 타입을 요청해야 하는 것을 잊지 마세요: + +```c++ +std::array poolSizes{}; +... + +poolSizes[1].type = VK_DESCRIPTOR_TYPE_STORAGE_BUFFER; +poolSizes[1].descriptorCount = static_cast(MAX_FRAMES_IN_FLIGHT) * 2; +``` + +풀에서 요청한 `VK_DESCRIPTOR_TYPE_STORAGE_BUFFER` 타입의 숫자를 두 배 해서 이전과 현재 프레임의 SSBO의 참조할 수 있도록 합니다. + +## 컴퓨트 파이프라인 + +컴퓨트는 그래픽스 파이프라인에 속하지 않으므로 `vkCreateGraphicsPipelines`를 사용할 수 없습니다. 대신 `vkCreateComputePipelines`를 사용해 적절한 파이프라인을 만들어 컴퓨트 명령을 수행해야 합니다. 컴퓨트 파이프라인은 래스터화 상태와는 상관이 없으므로 그래픽스 파이프라인보다는 상태가 훨씬 적습니다: + +```c++ +VkComputePipelineCreateInfo pipelineInfo{}; +pipelineInfo.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO; +pipelineInfo.layout = computePipelineLayout; +pipelineInfo.stage = computeShaderStageInfo; + +if (vkCreateComputePipelines(device, VK_NULL_HANDLE, 1, &pipelineInfo, nullptr, &computePipeline) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute pipeline!"); +} +``` + +설정은 훨씬 간단한데, 하나의 셰이더 스테이지와 파이프라인 레이아웃만 있으면 되기 때문입니다. 파이프라인 레이아웃 작업은 그래픽스 파이프라인과 동일합니다: + +```c++ +VkPipelineLayoutCreateInfo pipelineLayoutInfo{}; +pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; +pipelineLayoutInfo.setLayoutCount = 1; +pipelineLayoutInfo.pSetLayouts = &computeDescriptorSetLayout; + +if (vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &computePipelineLayout) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute pipeline layout!"); +} +``` + +## 컴퓨트 공간(space) + +컴퓨트 셰이더가 어떻게 동작하고, 어떻게 GPU에 컴퓨트 작업을 제출하는지 알아보기 전에, 두 가지 중요한 개념에 대해 먼저 이야기 해보겠습니다. 이는 **작업 그룹(work groups)**과 **호출(invocations)** 입니다. 이들은 컴퓨트 작업이 GPU의 컴퓨트 하드웨어에서 어떻게 처리되는지를 3차원(x,y,z)으로 추상화한 실행 모델을 정의합니다. + +**작업 그룹**은 컴퓨트 작업이 어떻게 구성되고 GPU의 컴퓨트 하드웨어에서 처리되는지를 정의합니다. 이를 GPU가 작업해야 하는 작업 아이템이라고 생각할 수 있습니다. 작업 그룹의 차원은 응용 프로그램의 명령 버퍼 시점에서 디스패치(dispatch) 명령에 의해 설정됩니다. + +각 작업 그룹은 동일한 컴퓨트 셰이더를 실행하는 **호출**들의 집합입니다. 호출은 잠재적으로 병렬 실행되며 그 차원은 컴퓨트 셰이더에 설정합니다. 동일한 작업 그룹에 속한 호출들은 공유 메모리에 접근할 수 있습니다. + +아래 그림은 3차원으로 이 두 가지 개념의 관계를 보여줍니다: + +![](/images/compute_space.svg) + +(`vkCmdDispatch`에 의해 정의되는) 작업 그룹의 차원과 (컴퓨트 셰이더에서 로컬 크기로 정의되는) 호출의 차원은 입력 데이터가 어떻게 구성되어 있는지에 의존적입니다. 예를 들어 여러분이 1차원 배열에 대해 작업을 수행한다면 (이 챕터의 예제처럼), 두 가지 모두 x 차원만 명시하면 됩니다. + +예를 들어: 작업 그룹 [64, 1, 1]개를 컴퓨트 셰이더의 로컬 크기 [32, 32, 1]로 디스패치 하면 컴퓨트 셰이더는 총 64 x 32 x 32 = 65,536 번 호출됩니다. + +작업 그룹과 로컬 크기는 구현마다 다르므로 `VkPhysicalDeviceLimits`에 정의된 `maxComputeWorkGroupCount`, `maxComputeWorkGroupInvocations`, `maxComputeWorkGroupSize`를 항상 확인해야 합니다. + +## 컴퓨트 셰이더 + +이제 컴퓨트 셰이더 파이프라인과 관련한 설정에 대해 모두 배웠으니 이제 컴퓨트 셰이더 자체를 살펴 봅시다. 지금까지 GLSL 셰이더에 대해서 배웠던, 예를 들면 정점 셰이더와 프래그먼트 셰이더에 관한 것들이 컴퓨트 셰이더에도 적용됩니다. 문법 또한 동일하고 응용 프로그램에서 세이더로 데이터를 넘기는 방법도 같습니다. 하지만 중요한 차이도 몇 가지 있습니다. + +일차원 배열로 저장된 파티클을 갱신하는 간단한 컴퓨트 셰이더는 아래와 같습니다: + +```glsl +#version 450 + +layout (binding = 0) uniform ParameterUBO { + float deltaTime; +} ubo; + +struct Particle { + vec2 position; + vec2 velocity; + vec4 color; +}; + +layout(std140, binding = 1) readonly buffer ParticleSSBOIn { + Particle particlesIn[ ]; +}; + +layout(std140, binding = 2) buffer ParticleSSBOOut { + Particle particlesOut[ ]; +}; + +layout (local_size_x = 256, local_size_y = 1, local_size_z = 1) in; + +void main() +{ + uint index = gl_GlobalInvocationID.x; + + Particle particleIn = particlesIn[index]; + + particlesOut[index].position = particleIn.position + particleIn.velocity.xy * ubo.deltaTime; + particlesOut[index].velocity = particleIn.velocity; + ... +} +``` + +최상단에는 셰이더의 입력을 정의하는 부분이 있습니다. 첫 번째로 0에 바인딩된 유니폼 버퍼 객체가 있는데, 이미 배웠던 내용입니다. 그 아래는 파티클 구조체에 대한 선언이 있고 이는 C++쪽의 선언과 매칭됩니다. 아래 1에 바인딩한 것은 셰이더 스토리지 버퍼 객체로 이전 프레임의 파티클 데이터이고(기술자 설정 부분 참고), 2는 현재 프레임의 SSBO의 바인딩 포인트이며 셰이더에서 갱신할 부분입니다. + +흥미로운 부분은 컴퓨트 공간과 관련된, 컴퓨트에서만 활용되는 선언입니다: + +```glsl +layout (local_size_x = 256, local_size_y = 1, local_size_z = 1) in; +``` + +위 코드가 이 컴퓨트 셰이더가 작업 그룹 안에서 호출될 개수를 정의합니다. 전에 이야기한 것처럼 이 부분은 컴퓨트 공간의 로컬 부분입니다. 따라서 `local_` 접두어가 붙습니다. 우리는 파티클에 대한 1차원 배열을 사용하기 때문에 x 차원인 `local_size_x`에만 숫자를 명시해 줍니다. + +`main` 함수에서는 이전 프레임의 SSBO를 읽어와 현재 프레임의 SSBO에 파티클의 위치를 갱신해 줍니다. 다른 셰이더 타입과 비슷하게 컴퓨트 셰이더도 내장 입력 변수를 가지고 있습니다. 내장 변수는 항상 `gl_` 접두어를 가지고 있습니다. 내장 변수 중 하나로 `gl_GlobalInvocationID`가 있는데 현재 디스패치에 대한 현재 컴퓨트 셰이더 호출의 ID를 가지고 있는 변수입니다. 우리는 이 값을 파티클 배열의 인덱스로 사용합니다. + +## 컴퓨트 명령 실행 + +### 디스패치 + +이제 실제로 GPU가 컴퓨트 작업을 하도록 할 차례입니다. 이는 명령 버퍼에서 `vkCmdDispatch`를 호출하여 수행합니다. 완전히 같진 않지만, 그래픽스에서 드로우를 수행하기 위해 `vkCmdDraw`를 호출하는 것이 컴퓨트에서는 디스패치와 대응됩니다. 이를 통해 (최대) 3차원으로 주어진 개수만큼 컴퓨트 작업 아이템을 디스패치합니다. + +```c++ +VkCommandBufferBeginInfo beginInfo{}; +beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; + +if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) { + throw std::runtime_error("failed to begin recording command buffer!"); +} + +... + +vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipeline); +vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_COMPUTE, computePipelineLayout, 0, 1, &computeDescriptorSets[i], 0, 0); + +vkCmdDispatch(computeCommandBuffer, PARTICLE_COUNT / 256, 1, 1); + +... + +if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) { + throw std::runtime_error("failed to record command buffer!"); +} +``` + +`vkCmdDispatch`는 x 차원에 대해 `PARTICLE_COUNT / 256`개의 로컬 작업 그룹을 디스패치합니다. 우리의 파티클 배열은 1차원이므로 나머지 두 차원은 1로 두었고, 이는 1차원 디스패치를 의미합니다. 그런데 왜 (배열 내의) 파티클 개수를 256으로 나누는 것일까요? 이는 이전 장에서 우리가 작업 그룹 내의 각 컴퓨트 셰이더가 256번씩 호출되도록 정의했기 때문입니다. 따라서 4096개의 파티클이 있다면, 16개의 작업 그룹을 디스패치 하는데 각 작업 그룹 내에서는 256번의 컴퓨트 셰이더 호출이 수행되는 것입니다. 이러한 숫자들을 올바로 정의하는 것은 필요한 작업량과 실행되는 하드웨어에 따라 때때로 수정과 확인이 필요합니다. 만일 파티클 개수가 동적으로 변해서 항상 256으로 나누어 떨어지지 않는다면 컴퓨트 셰이더 시작 지점에서 `gl_GlobalInvocationID`를 사용해 파티클 개수보다 큰 글로벌 호출 인덱스를 갖는 경우 바로 반환하도록 할 수 있습니다. + +컴퓨트 파이프라인에서와 마찬가지고 컴퓨트 명령 버퍼도 그래픽스 명령 버퍼보다 훨씬 적인 상태만 가지고 있습니다. 렌더 패스를 시작하거나, 뷰포트를 설정하는 등의 작업은 필요 없습니다. + +### 작업 제출 + +우리 예제는 컴퓨트와 그래픽스 연산을 모두 가지고 있으므로 매 프레임마다 그래픽스와 컴퓨트 큐에 모두 제출을 수행해야 합니다(`drawFrame` 함수 참고): + +```c++ +... +if (vkQueueSubmit(computeQueue, 1, &submitInfo, nullptr) != VK_SUCCESS) { + throw std::runtime_error("failed to submit compute command buffer!"); +}; +... +if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { + throw std::runtime_error("failed to submit draw command buffer!"); +} +``` + +첫 번째의 컴퓨트 큐로의 제출은 컴퓨트 셰이더를 통해 파티클의 위치를 갱신하고, 두 번째의 제출은 갱신된 데이터로 파티클 시스템을 그립니다. + +### 그래픽스와 컴퓨트의 동기화 + +동기화는 Vulkan에서 중요한 부분 중 하나로, 컴퓨트와 그래픽스를 동시에 사용할 때는 더 중요해집니다. 동기화가 없거나 잘못된 경우 컴퓨트 셰이더가 갱신을 끝내기(=쓰기) 전에 정점 스테이지가 그리기(=읽기)가 시작되거나 정점에서 사용되고 있는 부분의 파티클을 컴퓨트 셰이더가 갱신하기 시작한다거나 하는 문제가 발생할 수 있습니다. + +따라서 이러한 경우가 발생하지 않도록 적절히 그래픽스와 컴퓨트 간에 동기화를 수행해야만 합니다. 수행하는 방법은 컴퓨트 작업을 어떻게 제출했느냐에 따라 여러 방법이 있을 수 있는데 우리의 경우 두 개의 제출이 분리되어 있으므로 [세마포어](03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md#page_Semaphores)와 [펜스](03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md#page_Fences) 를 사용해 컴퓨트 셰이더가 갱신을 끝내기 전에는 정점 셰이더가 데이터 읽기를 수행하지 않도록 할 것입니다. + +두 제출 과정에 선후 관계가 있어도 이러한 동기화가 필요한데, GPU에서 이렇게 제출된 순서대로 실행된다는 보장이 없기 때문입니다. 대기 및 시그널 상태 세마포어를 사용하면 실행 순서를 보장할 수 있습니다. + +먼저 `createSyncObjects`에서 컴퓨트 작업을 위한 동기화 요소들을 추가할 것입니다. 그래픽스 펜스와 마찬가지로 컴퓨트 펜스는 시그널 상태로 생성될 것인데, 그렇지 않으면 첫 번째의 그리기 호출이 펜스가 시그널 상태가 될때까지 계속 기다리다 타임아웃이 되기 때문입니다. 자세한 내용은 [여기](03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md#page_Waiting-for-the-previous-frame)를 참고하세요: + +```c++ +std::vector computeInFlightFences; +std::vector computeFinishedSemaphores; +... +computeInFlightFences.resize(MAX_FRAMES_IN_FLIGHT); +computeFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT); + +VkSemaphoreCreateInfo semaphoreInfo{}; +semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO; + +VkFenceCreateInfo fenceInfo{}; +fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO; +fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT; + +for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) { + ... + if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &computeFinishedSemaphores[i]) != VK_SUCCESS || + vkCreateFence(device, &fenceInfo, nullptr, &computeInFlightFences[i]) != VK_SUCCESS) { + throw std::runtime_error("failed to create compute synchronization objects for a frame!"); + } +} +``` + +그러고 나서 컴퓨트 버퍼의 제출과 그래픽스 제출을 동기화 하는데 이들을 사용합니다: + +```c++ +// Compute submission +vkWaitForFences(device, 1, &computeInFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + +updateUniformBuffer(currentFrame); + +vkResetFences(device, 1, &computeInFlightFences[currentFrame]); + +vkResetCommandBuffer(computeCommandBuffers[currentFrame], /*VkCommandBufferResetFlagBits*/ 0); +recordComputeCommandBuffer(computeCommandBuffers[currentFrame]); + +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &computeCommandBuffers[currentFrame]; +submitInfo.signalSemaphoreCount = 1; +submitInfo.pSignalSemaphores = &computeFinishedSemaphores[currentFrame]; + +if (vkQueueSubmit(computeQueue, 1, &submitInfo, computeInFlightFences[currentFrame]) != VK_SUCCESS) { + throw std::runtime_error("failed to submit compute command buffer!"); +}; + +// Graphics submission +vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX); + +... + +vkResetFences(device, 1, &inFlightFences[currentFrame]); + +vkResetCommandBuffer(commandBuffers[currentFrame], /*VkCommandBufferResetFlagBits*/ 0); +recordCommandBuffer(commandBuffers[currentFrame], imageIndex); + +VkSemaphore waitSemaphores[] = { computeFinishedSemaphores[currentFrame], imageAvailableSemaphores[currentFrame] }; +VkPipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT }; +submitInfo = {}; +submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; + +submitInfo.waitSemaphoreCount = 2; +submitInfo.pWaitSemaphores = waitSemaphores; +submitInfo.pWaitDstStageMask = waitStages; +submitInfo.commandBufferCount = 1; +submitInfo.pCommandBuffers = &commandBuffers[currentFrame]; +submitInfo.signalSemaphoreCount = 1; +submitInfo.pSignalSemaphores = &renderFinishedSemaphores[currentFrame]; + +if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { + throw std::runtime_error("failed to submit draw command buffer!"); +} +``` + +[세마포어 챕터](03_Drawing_a_triangle/03_Drawing/02_Rendering_and_presentation.md#page_Semaphores)에서의 예제와 비슷하게 이렇게 설정하면 대기 세마포어가 없기 때문에 컴퓨트 셰이더가 곧바로 실행됩니다. 문제는 없는것이 `vkWaitForFences` 커맨드를 통한 컴퓨트의 제출 이전에 현재 프레임의 컴퓨트 커맨드 버퍼의 실행이 끝나는 것을 기다리고 있기 때문입니다. + +그래픽스 관련 제출은 컴퓨트 작업이 끝나기를 기다려야 하므로 컴퓨트 버퍼가 갱신하는 도중에는 정점을 가져오지 말아야 합니다. 따라서 그래픽스의 제출은 현재 프레임의 `computeFinishedSemaphores`를 기다리면서 정점이 사용되는 `VK_PIPELINE_STAGE_VERTEX_INPUT_BIT` 스테이지에서 대기하도록 해야 합니다. + +또한 표시도 기다려야 하는데 이미지가 표시되기 전에 프래그먼트 셰이더가 생상 출력을 내지 않도록 하기 위함입니다. 따라서 현재 프레임의 `imageAvailableSemaphores`를 `VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT` 스테이지에서 기다려야 합니다. + +## 파티클 시스템 그리기 + +이전 내용에서 Vulkan에서 버퍼는 여러 사용법을 가질 수 있다는 것을 배웠고, 셰이더 스토리지 버퍼를 만들고 여기에 저장된 파티클들이 셰이더 스토리지 버퍼와 정점 버퍼로 활용될 수 있도록 설정했습니다. 다시말해 셰이더 스토리지 버퍼를 전에 사용했던 "순수한" 정점 버퍼처럼 그리기를 위해 사용할 수 있다는 뜻입니다. + +먼저 정점 입력 상태를 파티클 구조체와 매칭기켜줍니다: + +```c++ +struct Particle { + ... + + static std::array getAttributeDescriptions() { + std::array attributeDescriptions{}; + + attributeDescriptions[0].binding = 0; + attributeDescriptions[0].location = 0; + attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT; + attributeDescriptions[0].offset = offsetof(Particle, position); + + attributeDescriptions[1].binding = 0; + attributeDescriptions[1].location = 1; + attributeDescriptions[1].format = VK_FORMAT_R32G32B32A32_SFLOAT; + attributeDescriptions[1].offset = offsetof(Particle, color); + + return attributeDescriptions; + } +}; +``` + +정점 입력 어트리뷰트에 `velocity`는 추가하지 않은 것에 유의하세요. 이 값은 컴퓨트 셰이더에서만 사용하기 때문입니다. + +그리고 일반적인 정점 버퍼처럼 바딩인들 하고 그리기를 수행합니다: + +```c++ +vkCmdBindVertexBuffers(commandBuffer, 0, 1, &shaderStorageBuffer[currentFrame], offsets); + +vkCmdDraw(commandBuffer, PARTICLE_COUNT, 1, 0, 0); +``` + +## 결론 + +이 장에서, 우리는 컴퓨트 셰이더를 사용해 CPU의 작업을 GPU로 이전하는 방법을 배웠습니다. 컴퓨트 셰이더가 없었다면 현대 게임 및 응용 프로그램들의 몇몇 효과는 불가능했거나 훨씬 느렸을 것입니다. 그래픽스 용도 이외에도 컴퓨트는 다양한 사용 방법이 존재합니다. 이 챕터는 그 가능성 중 아주 일부분을 보여드렸을 뿐입니다. 이제 컴퓨트 셰이더를 사용하는 방법을 알게 되셨으니 컴퓨트 관련한 고급 토픽들을 알아보고 싶으실겁니다: + +- Shared memory +- [Asynchronous compute](https://github.com/KhronosGroup/Vulkan-Samples/tree/master/samples/performance/async_compute) +- Atomic operations +- [Subgroups](https://www.khronos.org/blog/vulkan-subgroup-tutorial) + +고급 컴퓨트 기능에 대한 예제는 [공식 Khronos Vulkan Samples 레포지토리](https://github.com/KhronosGroup/Vulkan-Samples/tree/master/samples/api)에서 찾아보실 수 있습니다. + +[C++ code](/code/31_compute_shader.cpp) / +[Vertex shader](/code/31_shader_compute.vert) / +[Fragment shader](/code/31_shader_compute.frag) / +[Compute shader](/code/31_shader_compute.comp) diff --git a/kr/90_FAQ.md b/kr/90_FAQ.md new file mode 100644 index 00000000..e8bad5c4 --- /dev/null +++ b/kr/90_FAQ.md @@ -0,0 +1,49 @@ +이 페이지에서는 Vulkan 응용 프로그램 개발 도중 마주치게 되는 흔한 문제들에 대한 해결법을 알려 드립니다. + +## core 검증 레이어에서 access violation error가 발생해요 + +MSI Afterburner / RivaTuner Statistics Server가 실행되고 있진 않은지 확인하세요. Vulkan과의 호환성 문제가 있습니다. + +## 검증 레이어에서 아무런 메시지가 보이지 않아요 / 검증 레이어를 사용할 수 없어요 + +프로그램 종료시에 터미널이 열려있게 해서 검증 레이어가 오류를 출력할 수 있도록 하세요. 비주얼 스튜디오에서는 F5 대신 Ctrl-F5로 실행하면 되고, 리눅스에서는 터미널 윈도우에서 프로그램을 실행하면 됩니다. 여전히 아무 메시지가 나오지 않으면 검증 레이어가 활성화 되었는지 확인하시고, Vulkan SDK가 제대로 설치되었는지를 [이 페이지](https://vulkan.lunarg.com/doc/view/1.2.135.0/windows/getting_started.html)의 "설치 확인" 안내에 따라 확인해 보세요. 또한 SDK의 버전이 1.1.106.0 이상이어야 `VK_LAYER_KHRONOS_validation` 레이어가 지원됩니다. + +## SteamOverlayVulkanLayer64.dll에서 vkCreateSwapchainKHR 오류가 발생해요 + +Steam 클라이언트 베타에 호환성 문제가 있습니다. 해결 방법은 몇 가지가 있습니다: + * Steam 베타 프로그램 탈되하기 + * `DISABLE_VK_LAYER_VALVE_steam_overlay_1` 환경 변수를 `1`로 설정하기 + * `HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers` 아래 레지스트리에서 Steam 오버레이 Vulkan 레이어를 삭제하기 + +예시: + +![](/images/steam_layers_env.png) + +## VK_ERROR_INCOMPATIBLE_DRIVER가 나오며 vkCreateInstance가 실패해요 + +MacOS에서 최신 MoltenSDK를 사용 중이시라면 `vkCreateInstance`가 `VK_ERROR_INCOMPATIBLE_DRIVER` 오류를 반환할 수 있습니다. 이는 [Vulkan SDK 버전 1.3.216 이상](https://vulkan.lunarg.com/doc/sdk/1.3.216.0/mac/getting_started.html)에서는 MoltenSDK를 사용하려면 `VK_KHR_PORTABILITY_subset` 확장을 활성화해야 하기 때문인데, 현재는 적합성이 완전히 검토되지 않았기 때문입니다. + +`VkInstanceCreateInfo`에 `VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR` 플래그를 추가해야 하고 `VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME`를 인스턴스 확장 리스트에 추가해야 합니다. + +코드 예시: + +```c++ +... + +std::vector requiredExtensions; + +for(uint32_t i = 0; i < glfwExtensionCount; i++) { + requiredExtensions.emplace_back(glfwExtensions[i]); +} + +requiredExtensions.emplace_back(VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME); + +createInfo.flags |= VK_INSTANCE_CREATE_ENUMERATE_PORTABILITY_BIT_KHR; + +createInfo.enabledExtensionCount = (uint32_t) requiredExtensions.size(); +createInfo.ppEnabledExtensionNames = requiredExtensions.data(); + +if (vkCreateInstance(&createInfo, nullptr, &instance) != VK_SUCCESS) { + throw std::runtime_error("failed to create instance!"); +} +``` diff --git a/kr/95_Privacy_policy.md b/kr/95_Privacy_policy.md new file mode 100644 index 00000000..a4b3d4e4 --- /dev/null +++ b/kr/95_Privacy_policy.md @@ -0,0 +1,21 @@ +## General + +This privacy policy applies to the information that is collected when you use vulkan-tutorial.com or any of its subdomains. It describes how the owner of this website, Alexander Overvoorde, collects, uses and shares information about you. + +## Analytics + +This website collects analytics about visitors using a self-hosted instance of Matomo ([https://matomo.org/](https://matomo.org/)), formerly known as Piwik. It records which pages you visit, what type of device and browser you use, how long you view a given page and where you came from. This information is anonymized by only recording the first two bytes of your IP address (e.g. `123.123.xxx.xxx`). These anonymized logs are stored for an indefinite amount of time. + +These analytics are used for the purpose of tracking how content on the website is consumed, how many people visit the website in general, and which other websites link here. This makes it easier to engage with the community and determine which areas of the website should be improved, for example if extra time should be spent on facilitating mobile reading. + +This data is not shared with third parties. + +## Advertisement + +This website uses a third-party advertisement server that may use cookies to track activities on the website to measure engagement with advertisements. + +## Comments + +Each chapter includes a comment section at the end that is provided by the third-party Disqus service. This service collects identity data to facilitate the reading and submission of comments, and aggregate usage information to improve their service. + +The full privacy policy of this third-party service can be found at [https://help.disqus.com/terms-and-policies/disqus-privacy-policy](https://help.disqus.com/terms-and-policies/disqus-privacy-policy). \ No newline at end of file diff --git a/kr/kr_glossary.md b/kr/kr_glossary.md new file mode 100644 index 00000000..879bd1f1 --- /dev/null +++ b/kr/kr_glossary.md @@ -0,0 +1,115 @@ +# 한글 번역 용어집 + +- 정점: Vertex +- 프로그램가능한: Programmable +- 명령: Command +- 제출: Submit +- 확장: Extension +- 기술/기술자: Describe/Descriptor +- 장치: Device +- 큐 패밀리: Queue Family +- 윈도우 표면: Window Surface +- 스왑 체인: Swap Chain +- 렌더 타겟: Render Target +- 프레임: Frame +- 표시: Present/Presentation +- 그리기: Draw +- 드로우 콜: Draw Call +- 래핑: Wrap(ping) +- 스텐실: Stensil +- 렌더 패스: Render Pass +- 슬롯: Slot +- 바인딩: Bind/Bound/Binding +- 지움/지우기: Clear +- 혼합/블렌딩: Blend/Blending +- 풀: Pool +- 유니폼: Uniform +- 버퍼: Buffer +- 소멸: Destroy +- 할당/할당자: Allocate/Allocator +- 검증 레이어: Validation Layer +- 윈도우즈(OS): Windows +- 오프스크린: Off-screen +- 질의: Query +- 프레임버퍼: Framebuffer +- 주사율: Refresh Rate +- 동기/동기화: Synchronize/Synchronization +- 집합: Set +- 깊이: Depth +- 부호 있는/없는: Signed/Unsigned +- 테어링: Tearing +- 수직 동기화: Vertical Sync +- 수직 공백: Vertical Blank +- 지연시간: Latency +- 범위: Extent +- 어태치먼트: Attachment +- 후처리: Post-processing +- 동시성: Concurrent/Concurrency +- 독점: Exclusive +- 변환: Transform(ation) +- 뒤집기: Flip +- 클리핑: Clipping +- 컨테이너: Container +- 이미지 뷰: Image View +- 밉맵: Mipmap +- 텍스처: Texture +- 큐브 맵: Cube Map +- 기본: Default +- 메쉬: Mesh +- 입력 조립기: Input Assembler +- 셰이더: Shader +- 불변: Immutable +- 뷰포트: Viewport +- 행렬: Matrix +- 외적: Cross Product +- 프래그먼트: Fragment +- 클립 좌표: Clip Coordinate +- 정규화된 장치 좌표: Normalized Device Coordinate +- 동차 좌표: Homogeneous Coordinate +- 정렬: Align(ment) +- 시저: Scissor +- 래스터화: Rasterization +- 편향: Bias +- Push 상수: Push Constant +- 서브패스: Subpass +- 세마포어: Semaphore +- 시그널 상태: Signaled +- 시그널이 아닌 상태: Unsignaled +- 블러킹: Blocking +- 펜스: Fence +- 종속성/종속/의존: Dependency +- 아이들(링): Idel(ing) +- 데드락: Deadlock +- 스테이징 버퍼: Staging Buffer +- 어트리뷰트: Attribute +- 오프셋: Offset +- 맵핑: Map(ping) +- 플러싱: Flush(ing) +- 전송: Transfer +- 사용법: Usage +- 소스: Source +- 목적(지): Destination +- 앨리어싱: Aliasing +- 투영: Projection +- 컬링: Culling +- 베이크: Bake(d) +- 샘플러: Sampler +- 배리어: Barrier +- 소유권: Ownership +- 텍셀: Texel +- 복셀: Voxel +- 타일링: Tiling +- 전환: Transition +- 희박한: Sparse +- 패딩: Padding +- 쓰루풋: Throughput +- 보간: Interpolation +- 비등방성: Anisotropic +- 어드레싱 모드: Addressing mode +- 하이트맵: Heightmap +- 표면: Face +- 라이팅: Lighting +- 머티리얼: Material +- 상세도: Level of Detail +- 헤드리스: Headless +- 파티클: Particle \ No newline at end of file