-
[27] - Bindable/Drawable SystemGraphics 2021. 8. 1. 22:36
(26) C++ 3D DirectX Tutorial [Bindable / Drawable System Part 3] - YouTube
planetchili (chili) (github.com)
planetchili - Overview
planetchili has 36 repositories available. Follow their code on GitHub.
github.com
hw3d/hw3d at T21.1-End · planetchili/hw3d (github.com)
개념에 대한 좋은 강의를 제공하는 채널입니다.
이번엔 어떤 개념을 배우기 보다는 Architecture에 대한 것을 배웁니다. DrawTestTriangle이라는 함수에 너무 많은 것들을 넣었으니 이 부분을 개선하려는 겁니다. 매 프레임 마다 이 모든 과정을 반복하고 있고 별로 좋지 않습니다. 수 백, 수 천 개의 Mesh를 만든다고 생각하면 심각한 문제입니다. Chili의 아이디어를 따라가도록 하겠습니다.
지금까지 배운 것은 크게 두 가지의 요소로 나눌 수 있습니다. 파이프 라인[Pipeline]에 Bind할 수 있는 것과 그럴 필요가 없는 것. 그래서 Bind가 가능한 요소들-예를 들면, Input Layout, Buffer, Shader-을 어떤 통합된 인터페이스[Interface]로 관리하려 합니다. 그래서 Binable라는 Abstract class에 Graphics Object를 받는 순수 가상 함수를 만들고 이 함수를 상속을 받는 Class들은 어떤 요소를 나름대로 파이프 라인에 Bind할 함수의 정의가 반드시 있어야 할 것 입니다.
그리고 Drawable이라는 Entity를 만들게 됩니다. 이 Drawble은 복수의 Bind가 가능한 무언가로 이루어질 것입니다. 그래서 모든 Drawable은 Pixel shader, Vertext Shader, Input layout 등등이 필요할 것입니다. 즉, 기본적으로 Bindable의 집합체입니다. 그래서 Draw를 호출하면 Bind가 호출되어 Draw에 필요한 모든 Resources를 Bind할 것입니다. 이 구조의 아름다움은 모든 Bindable이 하나의 Interface에 속해 있기 때문에 Homogeneous한 Container를 사용할 수 있게 된다는 점입니다. 예를 들어봅시다. 어떤 class template T와 class template V로 만든 두 Instances는 하나의 Vertor Containter에 함께 존재할 수 없습니다. 하지만 Base class B로부터 상속 받은 T와 V는 하나의 Container에 존재할 수가 있습니다.
이는 곧 std::vector에 Objects들을 넣고 Loop를 돌 수 있는 장점을 갖게 됩니다. 게다가 Bindable을 Dynamic하고 Polymorphic하게 만들면 각 Drawble의 Bindable을 자유롭게 구성할 수 있습니다. 이것은 무슨 말인가 하면 어떤 Drawable Object를 만들 때 서로 다른 요소들을 Bind하게 만들 수 있다는 뜻입니다. 즉, Drawble A에서 a,b,c의 요소를 Bind했다고 해서 다른 Drawable object B에서 a,b,c를 반드시 Bind해야 할 필요가 없다는 뜻입니다.
마지막으로 모든 Objects들이 분리된 Class와 파일에 존재하기 때문에 다시 사용하거나 유지,보수에도 효과적입니다.
이러한 목적을 달성하기 위해 가장 먼저, Graphics Throw Macros라는 Header file에 기존 Graphics.h가 갖고 있던 각종 Macro들을 분리했습니다. 그 이유는 Bindable한 Object 역시 이런 Exception을 사용하게 되고 이 Macro에 대한 접근이 필요하기 때문입니다.


이렇게 Solution에 Filter를 추가하여 보기 좋게 관리합니다. Bindable부터 살펴보도록 합니다. 여기까지 오면서 어떤 요소들이 파이프 라인에 Bind되어야 하는지 개략적으로 배웠기 때문에 이해하는데 큰 도움이 될 것 같습니다. 돌이켜 보면 아래의 요소들은 전부 파이프 라인에 Bind되는 요소들이었습니다.

Header File 내에 새롭게 필터를 만들어 구분하기 좋게 만듭니다 
일단 Bindable은 기본적인 Interface Class입니다. 상속을 받은 Children Class에서 정의될 Bind함수를 위한 Stage를 설정합니다. 당연히 Virtual Desctructor도 필요하게 될 것입니다. 그 이유는 상속된 Class의 Object를 Virtual Destructor가 아닌 일반 Destructor로 제거하면 정의되지 않는 행동을 유발하기 때문입니다. 참조(Virtual Destructor - GeeksforGeeks)
Protected로 선언된 함수들은 Effective C++에서 볼 수 있는 약간의 Trick입니다.

ConstantBuffers.h의 일부입니다 보이는 것처럼 Bindable의 많은 요소(혹은 전부)들이 Bindable으로부터 상속을 받고 Graphics의 내용에 대해 접근해야 합니다. 접근해야 하는 이유는 Graphics에 필요한 Device, SwapChain 등등이 있기 때문입니다.

이렇게 Private:에 선언이 되어 있었습니다 이 문제를 해결할 수 있는 다양한 방법이 있는데 그 중 하나는 Bindable의 모든 요소들을 Graphics의 Friend로 만드는 것입니다. 그렇게 하면 모든 요소들이 Graphics의 Internal Contents에 대한 접근 권한이 생깁니다. 그러나 Bindable 요소들에게 Graphics에 대한 모든 권한을 주고 싶지 않다면 Bindable(Base Class) Graphics의 Friend로 만들면 됩니다. 일반적으로 생각하기를 만약 어떤 base class가 다른 class의 friend라면 base class의 derived class도 다른 class의 friend가 될 것처럼 여겨지지만 실상은 그렇지 않습니다. Derived class는 base class의 friend 관계를 받아오지 못합니다.
그래서 Base class에 friend의 특정 요소를 강제로 호출할 수 있는 함수를 만들고 해당 class의 children들이 쓸 수 있도록 만들어 준 것입니다. Bindable의 Protected 내의 함수들이 이 역할을 수행합니다. Bindable class가 children에게 제공해야 할 요소는 pContext, pDevice, DxgiInfoManager 이렇게 세 가지입니다. 그러면 Topolgy의 예시처럼 사용할 수 있습니다.

Topology가 Setup이 된 요소를 파이프 라인에 Bind하기 위해서 Base Class의 GetContext(gfx)를 호출하는 모습입니다. 다른 Bindable의 요소들은 기존에 Test 함수 내에 하드 코딩했던 것들을 Encapsulate한 것 뿐입니다. 덕분에 저 같은 사람들은 이제 사용할 때 각 Bindable이 무슨 짓을 하는 지 몰라도 그냥 호출만 하면 파이프 라인에 Bind되는 모습을 볼 수 있습니다.
"When they call bind, Shit gets bound."
강의에서는 다른 것은 몰라도 INFOMAN(gfx)에 대해서 강조를 합니다. IndexBuffer를 예로 들겠습니다.

INFOMAN은 Macro 중 하나 입니다. GFX_THROW_INFO 등의 Macro는 현재 Scope에 InfoManager가 사용 가능해야 한다고 여깁니다. InfoManager는 Graphics.h 내에 Member로 있기 때문에 Graphics에서 GFX_THROW_INFO 는 유효합니다. 하지만 IndexBuffer class 내의 어떤 Scope에서도 InfoManager를 찾을 수 없기 때문에 실은 유효하지 않습니다. 그래서 InfoManager를 현재 Scope에 Import하는 과정이 필요합니다.

만약 Debug 모드가 아니라면 단순히 HRESULT hr만 Declare하고 끝입니다. 하지만 Debug 모드라면 HRESULT를 Declare하고 InfoManager의 Reference를 현재의 Scope에 선언합니다. 그래서 Bindable Base clas에는 GetInfoManager가 있습니다. 결론적으로 INFOMAN(gfx) Macro가 현재 Scope에 InfoManager에 대한 Reference를 갖고 오기 때문에 GFX_THROW_INFO가 정상적으로 동작할 수 있는 근거가 됩니다. 그 외의 것들은 대체로 Bindable에서 상속 받은 평범한 것들입니다. 이 중에서 Vertex Buffer는 유심히 살펴볼 필요가 있습니다.

Vertex Buffer는 사용하는 입장인 우리가 정의하는 타입의 Vertex Buffer입니다. 그래서 이 Vertex의 생성에 대해서 완전한 유연성을 주기 위해서는 class template로 만드는 방법을 사용할 것입니다. 대신, 생성자[Constructor]에 한해서 Templating을 시도합니다. 따라서, Vertex Buffer는 어떤 vertor container를 생성자의 Parameter로 받는데 해당 vector의 내용이 'V'인 벡터를 받습니다. 받은 V라는 Template information를 Vertex Buffer를 생성할 때 크기에 대한 정보로 사용합니다. (bd.ByteWidth라든가 StructureByStride가 예입니다.) 당연히 사용자인 우리가 정의한다는 점에서 일맥상통하는 Constant Buffer에서도 같은 방식을 차용합니다. 차이가 있다면 Constant Buffer는 class 전체가 class template가 됩니다. 또 다른 Constant Buffer에 대한 흥미로운 점은 이 버퍼는 두 종류가 존재한다는 사실입니다. 하나는 Vertex Constant Buffer이고 다른 하나는 Pixel Constant Buffer입니다.

이렇게 두 종류가 존재합니다 Constant Buffer는 매 프레임 마다 갱신 된다는 특징이 있었고 이 성질은 Constant Buffer를 사용하는 목적이기도 했습니다. 그래서 모든 Constant Buffer에는 다음과 같은 Update 함수가 있어야 합니다.

이 함수의 등장은 마침내 실질적인 그래픽 엔진의 구현에 한 발 가까워졌다는 사실을 암시합니다. 여태 Test 함수 내에서 모든 Object, Buffer 등등을 매 프레임마다 만들었다가 부수고 다시 만드는 과정을 반복했기 때문에 이런 함수가 필요하지 않았습니다.
Device Context에는 Map이라는 함수가 존재합니다. 어떤 Resource를 이 Map이용하여 Map하면 해당 Resource를 Lock하고 해당 Resource의 Memory에 대한 Pointer를 얻게 됩니다. 그러면 우리는 이 Memory에 대해 독점적으로 Write할 수 있고 작업이 끝나면 unmap을 호출합니다. 이것으로 Bindable에 대한 설명이 끝난 것은 아니지만 Drawable에 대해 조금 알아보도록 합니다.
Drawable base class는 기본적으로 Bindable을 위한 저장 공간을 비롯한 Draw를 위한 다양한 논리를 제공합니다. 그래서 Draw를 위한 Drawable Class를 보면 다음과 같은 내용이 있습니다.

여기서 눈여겨 보아야 할 함수는 Draw, Addbind, AddIndexBuffer입니다.
Draw를 보면 상당히 직관적으로 동작합니다.

Draw는 벡터 컨테이너인 Binds를 돌면서 Bind할 수 있는 모든 것들을 Bind합니다. 그리고 graphics에서 DrawIndexed 호출하는데 index buffer에 있는 indices의 개수를 넘겨줍니다. Bindable 요소 중 어떤 것은 분명 Index의 개수를 갖고 있을 것이고 해당 버퍼를 잡아주는 포인터가 바로 pIndexBuffer 입니다. 그래서 파이프 라인에 Bind할 때, 혹시라도 index buffer를 Bind해야하는데 다른 것을 Bind할 수도 있으니 assert를 통해 미연에 방지합니다.
다음은 실질적으로 Drawable class를 상속받는 어떤 Concrete Object입니다.

이 박스가 하는 일은 상자를 만들어주는 겁니다. 생성자에 대한 것은 나중으로 미루고 Drawable의 속성인 Update를 강제했다는 점을 확인합니다.

Ctor에서 상자의 움직임과 위치를 위한 정보를 만듭니다. 이후엔 다음과 같이 Vertex 등의 Bindable을 위한 요소들을 생성합니다.

AddBind하는 것을 확인할 수 있습니다. 그런데 GetByteCode() 함수가 눈에 띕니다. Vertex Shader의 Layout을 만들기 위해서는 Vertex Shader의 ByteCode가 필요합니다. 설명에 따르면 그렇습니다. 그래서 Vertex Shader의 Interface에는 해당 ByteCode를 반환하는 함수가 있습니다. 우리는 Vertex Shader를 만들고(pvs) 해당 shader의 bytecode를 받고(pvsbc) 그것을 나중에 Input Layout에서 사용합니다. 다음 사진에서 Input Layout을 Bind하는 모습을 확인할 수 있고 Parameter로 pvsbc(Pointer to Vertex Shader ByteCode)가 사용된 모습을 확인할 수 있습니다.

이 과정이 여태 DrawTestTriangle함수가 수행하던 일을 세분화 하여 Encapsulate한 것과 같습니다. 이 상자의 위치나 회전 등의 정보는 Update에서 delta값만 더해주면 됩니다.

Transform을 위해선 몇 개의 행렬을 곱하면 됩니다.

이 박스 object에 한정된 이야기입니다 이제 Constant Buffer, 특히 Transform과 관련된 버퍼에 대한 이야기를 합니다. 현재 Vertex Shader는 하나의 Constant Buffer를 갖고 이 버퍼에는 각 Vertex에 적용될 Transform Matrix를 갖고 있습니다. Box를 통해서 예를 들면 이 Transform matrix는 Box의 위치를 바탕으로 결정되어야 합니다.

UML Diagram으로 표현한 구조입니다 그러나 Drawable입장에서는 자기 자신은 그저 Bindable의 집합체에 불과하고 어떤 Bindable Object가 Transform Constant Buffer인지 알 방법이 없습니다. 그래서 Transformation Constant Buffer가 Bindable에서 직접 상속을 받고 Vertex Constant Buffer를 Transform C Buffer 내에 직접 Include합니다. 그리고 TrasnformCBuffer는 Drawable 즉 부모 class에 대한 Reference를 직접 갖고 있게 합니다. 이렇게 만들면 매 프레임마다 Transform matrix를 Update해야하면 Parent class에서 직접 받아와서 파이프 라인에 Bind합니다.

그래서 TransformCBuf Class에서는 Parent class인 Drawable에 대한 Reference가 존재합니다. Bind를 보면 확실하게 이해할 수 있습니다.

Drawable(parent로 Declare되어 있습니다.)로부터 직접 Transformation Matrix를 받아와서 업데이트를 해주는 모습을 볼 수 있습니다.
내용이 너무 방대하고 어렵습니다. 쓰면서도 무슨 소리인가 싶은 것들이 너무 많습니다.
'Graphics' 카테고리의 다른 글
[29] - (C++) For each, Move Semantics, Lambdas, Uniform Initialization - 1 (0) 2021.08.03 [28] - Bindable/Drawable System - 2 (0) 2021.08.02 [26] - Z-buffer (0) 2021.07.31 [25] - DirectXMath (0) 2021.07.31 [24] - Constant Buffer (0) 2021.07.30