ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [20] - My First Triangle - 1
    Graphics 2021. 7. 22. 17:43

    마침내, 뭔가 그려볼 준비가 끝난 것 같습니다. 

    Graphics.h에 작은 함수를 만들고 여기에 모든 것을 기입하도록 하겠습니다. 구조에 관해서는 당장에 중요한 문제가 아니기 때문에 고려하지 않도록 하겠습니다. 감이라도 잡으면 좋을 것 같습니다. 뭘 어떻게 접근해야 삼각형을 그려볼 수 있을까요? 

    이전에 모든 Resources를 분배하는 역할은 Device가 한다고 했습니다. Context는 파이프 라인을 Configure하거나 Rendering Command를 Issue하는 역할을 했습니다. 그렇다면 Context의 Object인 pContext가 뭔가 답을 알고 있을 것 같습니다.

    실제로 있습니다
    이제 MSDN으로 가서 각 Parameter가 뭘 의미하는 지 파악하면 됩니다

    그런데 구태여 MSDN까지 달려가지 않더라도 감은 잡을 수 있습니다. 무작정 값을 집어 넣는다고 그릴 수 있을 것 같지는 않기 때문입니다. 분명 여기에 박을 Vertex를 Configure 하는 방법이 존재할 것입니다. 일단 그래픽 파이프 라인의 전반적인 흐름을 보면 좋습니다. Graphics Pipeline - Win32 apps | Microsoft Docs/

    위부터 시작입니다. Input-Assembler Stage는 그림엔 나오지 않았으나 Index/Vertex Buffer를 Bind하는 단계입니다. 그럼 pContext의 함수를 열심히 뒤져보면 이 단계에 해당하는 부분이 분명 나올 것입니다.

    벌써 의심스러운 용의자들의 목록을 뽑았습니다. IA가 느낌 상 InputAssembler 일 것 같습니다. 실제로 그렇습니다. IA Stage가 Vertex나 Index를 Bind하는 단계라고 했으니 SetVertexBuffer Method를 살펴보는 편이 좋겠습니다.

    직관적입니다

    1. StartSlot : 첫 번째로 Bind할 슬롯입니다. 각 슬롯 마다 하나의 Vertex Buffer를 받습니다. 그런데 하나의 Vertex buffer만 Bind할 것이니 0으로 둡니다.

    2. NumBuffers : 배열의 Vertex Buffer의 개수를 넣습니다. 여기서는 1입니다. 하나만 넣을 테니까요.

    3. *const* ppVertexBuffers : 이 함수의 핵심입니다. 잘 보면 pointer의 pointer를 받습니다. 즉, 포인터로 이루어진 배열의 포인터를 받는 겁니다. 그래서 함수의 이름도 Buffers로 복수형 입니다. 한 번의 호출로 다수의 Buffers를 받기 때문입니다. 단순히 vertex buffers의 배열의 포인터를 받습니다. 

    4. pStrides : Vertex 사이의 간격을 의미합니다. 정확히 무슨 뜻인지는 차차 나옵니다.

    5. pOffsets : Vertex Buffer Array 내의 Vertex Buffer 간의 간격을 의미합니다. 

    해당 MSDN 문서에서 갖추어야 할 지식은 IAStage를 위해서 Vertex Buffer라는 것을 사용해야 한다는 것입니다. 그리고 그것이 COM Object인 ID3D11Buffer라는 점도 염두에 두어야 합니다. 그럼 이걸 만들어야 하는데 과거에 Device와 Context의 차이를 고려해보면 Device가 만들어 줄 수 있을 것이라는 확신이 이제 듭니다.

    진짜 있습니다

    이제 각 요소들이 뭔지 MSDN을 또 가야 합니다. 

    보아하니 주로 세 가지의 버퍼를 만들어 주는 것 같습니다. 그 세 가지 중엔 지금 만들고 싶은 Vertex Buffer도 포함되어 있습니다.

    1. pDesc : 이건 익숙한 요소입니다. Description으로 Buffer에 대한 Description입니다. 이걸 우리는 과거에 CreateDeviceAndSwapChain을 호출 할 때, SwapChain의 Description에서 봤습니다.

    2. pInitalData : 초기화 데이터를 담고 있는 D3D11_SUBRESOURCE_DATA 구조체에 대한 포인터입니다. 이건 Flag가 D3D11_USAGE_IMMUTABLE이면 NULL값을 줄 수 없습니다. 여기에 아무것도 넘겨주지 않아도 되지만 그런 경우엔 Buffer에 특별한 방법을 사용해서 무언갈 채워 넣어야 합니다.

    3. ppBuffer : 이게 정말 채워 놓고 싶은 버퍼입니다.

    그러면 먼저 버퍼를 만들어야 할 것 같습니다. ComPtr을 사용하기로 했으니 그렇게 합니다. 그리고 Description이 필요하다고 했으니 함께 만들어 줍니다. 무엇이 필요한 지 살펴 봅시다.

    ByteWidth는 버퍼의 크기를 Bytes로 나타난 것입니다. Usage는 무엇이 있는지 살펴봅니다.

    가장 무난한 것이 Default라고 합니다. GPU가 리소스를 읽고 쓸 수 있도록 해줍니다. 그리고 Flag는 버퍼가 파이프 라인에 어떤 식으로 Bind 될 것인지 정의하고 많은 Flags가 있습니다. 이것 역시 Logical OR을 사용해서 복수의 옵션을 줄 수 있습니다.

    또 CPU에게 이 버퍼에 대한 접근 권한을 줄 수도 있습니다. Misc는 miscellaneous 의 약자고 다양한 Flags가 있는데 확인을 해 보면 될 것 같습니다. 당장은 필요하지 않습니다. 마지막에 StructureByteStride는 기본적으로 구조 내의 크기이기 때문에 Vertex의 크기라고 보면 됩니다. 

    이제 bd가 버퍼를 어떤 식으로 만들 것인지 묘사(Description을 직역)한다는 사실을 알았습니다. 그런데 다시 살펴보면 이 Description엔 어떤 데이터를 넣을 것인지 설명되어 있지 않습니다. 우리는 Vertices를 넣고 싶었던 것이고 그 정보를 두 번째 Parameter Subresrouce에 넣는 겁니다. 

    이 서브 리소스 데이터는 CPU 상의 배열인데 GPU로 전송되고 새롭게 만든 버퍼에 탑재 됩니다. pSysMem이 실제 데이터에 대한 포인터이며 Pitch와 관련된 정보가 필요합니다. 이 구조체는 기본적으로 Texture와 관련이 깊습니다. 일단 Vertex를 넣어 주어야 하니까 이것부터 만들도록 합니다.

    Vertex의 구조체를 만들고 내용은 x,y 좌표만 넣어줍니다. z도 필요한 게 맞지만 일단 이렇게 합니다.

    만들었으면 넘겨줄 실제 데이터를 만듭니다. 

    그리고 나서 Description을 채워줍니다.

    ByteWidth가 배열의 크기라고 했고 StructureByteStride가 Vertex의 크기라고 했으니 Vertex의 크기를 넣어 줍니다. 그리고 SubResourceData도 만들어 주면 됩니다. 그리고 pDevice가 무언가 만들 때 결과를 HRESULT로 준다고 했으니 해당 Scope 내에 HRESULT hr을 Declare해 주고 GFX_THROW_INFO로 에러를 탐지하도록 합니다.

     이제 이걸 Pipeline에 Bind 해주면 됩니다. IAStage이니까 IASetVertexBuffers를 찾아서 알맞게 넣어줍니다. 그런데 Parameter 중에서 Offset과 Stride의 포인터를 달라고 하는 부분이 있는데 다른 방법이 있겠으나 일단은 Vertex 구조체의 크기와 0u의 값을 가지는 상수를 만들어 해당 포인터를 넘겨주도록 합니다.

    그리고 그리면 되겠죠?

    How could possibly go wrong?
    ?

    안 됩니다. 그러나 Error Handling을 구현했기 때문에 기쁜 마음으로 디버그가 가능합니다. 그런데 왜 드라이버가 Crash 해버렸는지 설명은 안 해주니 슬픕니다. 그럴 수 밖에 없는 것이 HRESULT가 아닌 void를 반환하는 함수에서 에러가 발생하면 무기력하게 당할 수밖에 없습니다!

    OUtput 창을 보면 이렇게나 많은 경고와 오류가 있긴 있었습니다.

    그래서 할 일은 새로운 Exception을 만드는 일입니다. Macro를 정의할 것인데 하는 일은 어떤 함수 호출 이전에 어떤 메시지가 있는지 확인하는 것입니다. 그리고 함수 호출 이후에 새로운 디버그 메시지가 있는지 확인하고 만약 그러하다면 Exception을 Throw합니다. 

    Graphics.h 안에 새로운 Inner class를 추가합니다

    기존의 Exception과 차이가 있습니다. 이 Class는 더 이상 HRESULT를 저장하지 않습니다. 

    새롭게 Macro도 만들고 Draw 함수를 이 Macro로 감싸서 다시 디버그를 진행하면 다음과 같은 에러를 얻습니다
    Shader가 없다고 합니다

    Chili의 다른 강의에 3D Fundametal에 대한 영상이 있는데 사실 거기서 Pipeline을 구축할 때 기본적으로 반드시 Vertex와 Pixel Shader가 필요하다고 했습니다. 하나씩 해결하면 됩니다. Shader는 High Level Shader Language[HLSL]로 쓰여있습니다. 사실 이걸 배워야하는데 다행히 C++과 닮은 Syntax를 갖추고 있어서 배우기 쉽습니다.

    프로젝트에 Add를 하면 저렇게 좌측에 HLSL이 떡하니 있습니다.

    이제 Shader Object를 만들어 주면 됩니다. 

    VertexShader를 위한 Com Object를 만들어주고 Shader Bytecode를 Load해주는 함수가 D3DReadFileToBlob입니다. Blob은 그저 Binary data를 가리키는 말이고 이 함수가 Blob에 어떤 데이터를 저장하게 만듭니다. *.cso File은 Vertex Shader가 컴파일 되고 나면 생기는 파일입니다. 이를 Bind해주는 것이 현재 목표입니다. 

    여기서 잠깐 VertexShader.cso file에 대해 이야기합니다. VertexShader.hlsl 파일이 Compile되고 나면 그 생성물을 어디에 둘 것인지 결정해야 하는데 기본적으로 Out Directory에 생성합니다. 그러나 우리는 Project directory에 이 파일이 있을 것으로 기대하고 있기 때문에 경로를 수정해주든가 아니면 생성이 project directory에 되도록 정해주어야 합니다. HLSL 파일의 Propery에서 Output files로 가면 다음과 같은 설정을 할 수 있습니다.

    보시는 바와 같이 OutDir로 경로가 설정되어 있습니다.
    이렇게 바꿔줍니다

    이러면 Working Directory에 cso가 생성 되기 때문에 경로 때문에 골머리를 앓을 일은 없습니다. 추가로 General에 가서 이 Shader가 특히 Vertex Shader임을 명시하는 편이 좋습니다. 빌드 해보면 됩니다. 그러나 디버그 돌리면 또 에러가 나옵니다.

    Shader를 Bind 했다고 생각했는데 아닌가 봅니다. 그리고 Primitive Topology에도 문제가 있는 것 같습니다. 한 가지 드는 의문은 왜 Pixel Shader가 없다고 에러가 나지 않는가? 입니다. 사실 Pipeline에서 Geomety shader에서 Rasterizer 단계로 넘어가지 않고 Vertex만 가지고도 플레이가 되긴 합니다. 해결은 다른 내용과 함께 해버리는 편이 좋겠습니다.

    'Graphics' 카테고리의 다른 글

    [22] - Viewport and clip space  (0) 2021.07.23
    [21] - My First Triangle - 2  (0) 2021.07.22
    [19] - ComPtr and Smart Pointer  (0) 2021.07.21
    [18] - Debug Layer Diagnostics  (0) 2021.07.21
    [17] - Device Init  (0) 2021.07.20
Designed by Tistory.