ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [47] - Bindable/Drawable System - 3
    Graphics 2021. 8. 12. 21:44

    절차지향적으로 만들었던 코드를 객체지향적으로 재구성하면서 어떻게 코드를 이해하면 좋을지 감을 잡기가 어려워졌습니다. 따라서 변화 이전의 절차 지향적 코드와 비교를 해보면서 이 엔진을 어떤 식으로 응용하면 좋을지 고민해봅니다.

    Window

    나름대로 분석해본 결과, 이 ChiliFramework는 Window 하나를 화면에 띄우면서 시작합니다. 따라서 가장 먼저 모든 절차에서 분리할 수 있는 요소는 Window를 초기화하는 과정이라고 할 수 있습니다. 그렇기 때문에 App class가 만들어지는 순간 Window의 인스턴스를 만드는 과정에 따로 Encapsulate된 상태로 존재하는 것입니다. 

    App class의 일부
    생성자를 통해서 Window를 만들며 시작됩니다

    동시에 Window class 안에는 키보드, 마우스, Graphics를 비롯한 필수적인 요소들이 존재합니다. 이들은 Graphics Pipeline과 별개로 초기화 해도 상관 없기 때문에 분리가 가능했습니다. 서로가 상호배타적인 요소라는 뜻은 아닙니다. 그래픽 파이프 라인과 Window가 상호 배타적이면 사용자는 아무것도 할 수가 없을 것입니다. 이는 프로그램을 구현하는 입장에서 그럴 듯한 말입니다. 

    Graphics Pipeline

    Window를 성공적으로 띄웠으면 이젠 Grapics Pipeline을 구축할 차례가 됩니다. 여태껏 계속 확인했던 것처럼 Graphics Pipeline 자체는 절차가 존재합니다. 그러나 해당 절차를 분석해보면 Bind와 Draw로 절차를 분리할 가능성이 있었습니다. 

    철저히 Bind - Draw 과정을 분리하고 OOP의 개념을 도입하며 Class Inheritance까지 도입됩니다. 구조가 크게 변화하고 솔직히 이때부터 정신을 차릴 수가 없었습니다. 그러나 유심히 분석해보면 기존의 DrawTestTriangle이라는 함수에 절차 지향적으로 작성했던 코드를 객체 지향적으로 바꾸었다는 사실을 알 수 있습니다.(hw3d/Graphics.cpp at T20-End · planetchili/hw3d (github.com)) -> 해당 버전의 풀 리소스 입니다.

     즉, 이 코드를 통해서 ChiliFramework가 어떤 식으로 재편성 되었는지 분석하고 체득하면 고급 기술자의 능력을 훔쳐올 수 있는 기회가 될 것입니다. 해당 코드는 구조를 재편성 하기 직전의 코드입니다. Window에 시동을 걸어주는 과정은 변화가 없을 것입니다. 그렇다면 가장 주목해야 할 변화는 어떤 Mesh를 만들 때, 그래픽 파이프 라인에 어떤 식으로 묶어주는가 입니다.

    Comparison

    figure 1a

    기존의 코드에서는 가장 먼저 Vertex의 정보를 위해서 구조체를 만들었습니다. 그리고 그리려는 삼각형의 Vertex buffer를 생성했습니다. 하지만 이런 식으로는 수 천, 수 만 개의 Mesh를 그린다는 것이 불가능합니다. 따라서 해당 과정이 다음과 같은 식으로 재편되었습니다. 

    figure 1b

    여기서 case는 무시해도 좋습니다. 이는 단순히 어떤 Factory함수의 일부입니다. 저번에 배웠던 Random Number Generator 을 통해서 0부터 x까지의 수를 만들어서 어떤 Mesh를 생성하는 과정일 뿐입니다.

    핵심은 unique pointer를 만드는 과정에 있습니다. Pyramid, Box, Melon 등등의 Drawable 객체들을 볼 수 있습니다. 이들의 내부를 살펴보면 다음과 같은 구성입니다.

    가장 간편하면서 핵심을 모두 담고 있는 Box로 분석을 해보겠습니다.

    figure 1c

    Figure a의 첫 번째 Vertex Struct의 선언이 Box.cpp에 있습니다. Box의 인스턴스를 만들면( 여기서는 make_unique를 통해서 만들었으며 gfx를 비롯한 arguments들이 있습니다.) Box의 Vertex Buffer를 만들기 위한 준비에 착수합니다. 중요한 것은 Cube의 존재입니다.

    figure d-1

    Geomerty라는 filter로 따로 관리되는 Cube를 비롯한 여러 클래스에는 Vertex와 Index를 Bind하기 위한 Setup이 되어 있습니다. 즉, App에서 Box mesh를 만들면 App->Box->Cube로 이어지게 됩니다. 그리고 Cube::Make<Vertex>();의 반환은 IndexedTriangleList<V>로 되어 있는 것을 확인할 수 있습니다. 먼저 Make함수가 어떤 식으로 IndexedTraiangleList를 반환하는지 파악할 필요가 있습니다.

    figure 1e

    Figure 1c를 참조하면 Template argument로 Vertex라는 구조체를 주었다는 사실을 알 수 있습니다. 이 함수는 단순히 벡터 두 개를 반환하는 것이 아니라 IndexedTriangleList라는 클래스의 인스턴스를 만들며 반환하고 있습니다. 다시 말해서 이렇게 반환된 두 개의 벡터는 IndexedTraiangleList의 Paramter가 되어 생성자를 Invoke합니다. 그리고 이 생성자는 다음과 같이 구성되어 있습니다. 

    또한, figure 1e에는 figure a의 vertex를 만들고 index를 찍는 과정이 모두 포함되어 있습니다. 아직 bind를 한 것은 아닙니다. Vertex라는 벡터에 8개의 Vertex( 육면체는 총 여섯 면을 가지고 있기 때문입니다.)를 넣고 반환은 이 Vertex를 가지고 삼각형 12개를 만드는 Index들을 격납한 벡터를 반환합니다.

    figure 1f

    std::move는 이전에 했습니다. 복사 없이 새로운 IndexedTriangleList의 인스턴스가 생성 되는 모습입니다. figure c의 const auto model의 정체는 바로 이 Instance였습니다. 여기까지가 figure 1a에 나타난 과정입니다.

    이전에 배웠던 것을 잘 떠올려 보면 Description이라는 것이 존재했습니다. 이 Desc는 Vertex Buffer를 파이프 라인에 묶는 Class에서 함께 생성됩니다. 다음과 같은 코드가 기존의 구현이었습니다.

    figure 2a

    기존 코드에서는 Vertex를 만든 후 바로 오는 것이었습니다.

    figure 2b

    저 Syntax 자체가 gfx, model.vertices를 Arguments로 VertexBuffer Type을 Bind하겠다. 라는 선언과 같습니다. 그 이유는 VertexBuffer의 구현을 보면 이해가 갑니다.

    figure 2c

    figure 2a의 내용이 여기에 있습니다. 즉, Vertex, Index를 만들고 Description을 만들어 Bind하기 까지 과정을 Encapsulate한 것입니다. 이런 내용은 후에 나만의 Mesh를 만들때 반드시 지켜야 할 약속과 같습니다.

    figure 2d

    그 Bind의 내용은 여기에 있습니다. 이 VertexBuffer가 Bindable의 자녀 클래스이기 때문에 Bindable의 GetContext를 통해서 Graphics 인스턴스에 접근할 수 있습니다. 그러면 다음 과정이 예측이 되어야 합니다. Vertex Buffer를 Bind했으면 Index Buffer를 Bind할 차례입니다. 그런데, figure 1f를 참조하면 이미 index buffer가 생성이 되어 있음을 알 수 있습니다. 따라서 별도의 Index를 생성하는 추가적인 과정은 필요하지 않습니다.

    figure 2e
    figure 2f

    vertex buffer와 완전히 같은 논리가 적용됩니다. 그러면 상수 버퍼[Constant Buffer]의 경우도 마찬가지라고 할 수 있을까요? 그렇습니다. 다만, 이 과정은 Shader를 요구하기 때문에 Vertex/Pixel Shader를 먼저 생성합니다.

    figure 2g

    이제 왜 이론이 탄탄하지 않으면 성장할 수 없는지 깨닫게 되었습니다. 얼마나 D3D를 이해하고 있는지도 중요하지만 그래픽 파이프 라인에 대한 전반적인 이해가 없으면 구조가 변화했을 때 아무것도 이해할 수 없습니다. 기존의 절차 지향적인 코드를 객체 지향적으로 변화시킬 수 있었던 이유는 이렇게 각 파이프 라인의 단계를 분리할 수 있었기 때문입니다.

     이렇게 Box 하나를 만들면 다시 App으로 나오게 될 것입니다.

    figure 3a

    figure 1b에서 해당 Loop를 얼마나 돌게 되는지가 관건이지만 저번에 배웠던 std::generate_n을 보면 만들어진 Mesh들이 drawables에 차곡차곡 격납될 것이라는 생각이 듭니다. 

    그렇게 격납된 Mesh들은 DoFrame함수를 통해 화면에 출력이 됩니다.

    figure 3b

    이 엔진에 대한 이해가 조금 서기 시작합니다. 일단 게임을 시작하면 필요한 Mesh나 Object들을 벡터에 저장해놓고 필요할 때 출력할 수도 있겠다는 감이 잡힙니다. 그건 실장[実装]하기 나름입니다.

    figure 3c

    WinMain에서 App을 만들며 Go를 호출합니다. 저 DoFrame이전에 Setup을 살펴보았습니다. 아마 다음엔 Texture에 대한 공부를 하게 될 것 같습니다.

    'Graphics' 카테고리의 다른 글

    [49] - Texture - 1  (0) 2021.08.17
    [48] - Rasterization  (0) 2021.08.13
    [46] - std::optional  (0) 2021.08.12
    [45] - Multithreading Execution Policy  (0) 2021.08.11
    [44] - Container Adaptors, Priority Queue  (0) 2021.08.11
Designed by Tistory.