비타Cpp

정점 버퍼(Vertex Buffer) 본문

DirectX12/튜토리얼

정점 버퍼(Vertex Buffer)

멍C 2021. 12. 12. 20:40

GPU가 정점 배열에 접근하려면, 그 정점들을 버퍼라는 GPU 자원(ID3D12Resource)에 넣어 두어야 한다. 정점들을 저장하는 버퍼를 정점 버퍼(Vertex Buffer)라고 부른다. 버퍼는 텍스처보다 단순한 자원이다. 버퍼는 다차원이 아니며, 밉맵이나 필터, 다중 표본화 기능이 없다. 응용 프로그램에서 정점 같은 자료 원소들의 배열을 GPU에 제공해야 할 때에는 항상 버퍼를 사용한다.

정점 버퍼를 생성하려면 D3D12_RESOURCE_DESC를 채우고 ID3D12Device::CreateCommittedResource 메서드를 호출해서 ID3D12Resource객체를 생성한다. Direct3D 12는 D3D12_RESOURCE_DESC를 상속해서 편의용 생성자들과 메서들을 추가한 C++ 래퍼 클래스 CD3DX12_RESOURCE_DESC를 제공한다.

 

static inline CD3DX12_RESOURCE_DESC Buffer( 
        UINT64 width,
        D3D12_RESOURCE_FLAGS flags = D3D12_RESOURCE_FLAG_NONE,
        UINT64 alignment = 0 )
    {
        return CD3DX12_RESOURCE_DESC( 
        	D3D12_RESOURCE_DIMENSION_BUFFER, 
        	alignment, width, 1, 1, 1, 
        	DXGI_FORMAT_UNKNOWN, 1, 0, 
        	D3D12_TEXTURE_LAYOUT_ROW_MAJOR, flags );
    }

범용 GPU자원으로서의 버퍼에서 너비(Width)는 가로길이가 아니라 버퍼의 바이트 개수를 뜻한다. 예를 들어 float 64개를 담는 버퍼의 너비는 64 * sizeof(float)이다.

 

정적 기하구조(즉, 프레임마다 변하지 않는 기하구조)를 그릴 때에는 최적의 성능을 위해 정점 버퍼들을 기본 힙(D3D12_HEAP_TYPE_DEFAULT)에 넣는다. 정적 기하구조의 경우, 정점 버퍼를 초기화한 후에는 GPU만 버퍼의 정점들을 읽으므로(기하구조를 그리기 위해), 기본 힙에 넣는 것이 합당하다. CPU는 기본 힙에 있는 정점 버퍼를 수정하지 못한다. 그렇다면, 애초에 응용프로그램이 어떻게 정점 버퍼를 초기화하는 것일까?

응용프로그램은 D3D12_HEAP_TYPE_UPLOAD 형식의 힙에 임시 업로드용 버퍼 자원을 생성한다. 그리고 CPU에서 GPU로 데이터를 복사하기 위해 업로드 힙에 자원을 맡겨야 한다. 즉 CPU -> 업로드 버퍼 ->  GPU로 데이터 복제가 이루어지면서 정점 데이터가 초기화된다.

기본 버퍼(D3D12_HEAP_TYPE_DEFAULT)의 자료를 초기화하려면 항상 임시 업로드 버퍼가 필요하므로, 이과정을 하나의 함수로 정의해놓으면 기본 버퍼가 필요할 때마다 같은 코드를 반복하지 않아도 된다.(해당 예제 코드에는 d3dUtil.h/.cpp에 정의돼있다.)

Microsoft::WRL::ComPtr<ID3D12Resource> d3dUtil::CreateDefaultBuffer(
    ID3D12Device* device,
    ID3D12GraphicsCommandList* cmdList,
    const void* initData,
    UINT64 byteSize,
    Microsoft::WRL::ComPtr<ID3D12Resource>& uploadBuffer)
{
    ComPtr<ID3D12Resource> defaultBuffer;

    // 실제 기본 버퍼 자원을 생성한다.
    ThrowIfFailed(device->CreateCommittedResource(
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT),
        D3D12_HEAP_FLAG_NONE,
        &CD3DX12_RESOURCE_DESC::Buffer(byteSize),
		D3D12_RESOURCE_STATE_COMMON,
        nullptr,
        IID_PPV_ARGS(defaultBuffer.GetAddressOf())));

    // CPU 메모리의 자료를 기본 버퍼에 복사히기 위해
    // 임시 업로드 힙을 만든다.
    ThrowIfFailed(device->CreateCommittedResource(
        &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
		D3D12_HEAP_FLAG_NONE,
        &CD3DX12_RESOURCE_DESC::Buffer(byteSize),
		D3D12_RESOURCE_STATE_GENERIC_READ,
        nullptr,
        IID_PPV_ARGS(uploadBuffer.GetAddressOf())));


    // 기본 버퍼에 복사할 자료를 서술한다.
    D3D12_SUBRESOURCE_DATA subResourceData = {};
    subResourceData.pData = initData;
    subResourceData.RowPitch = byteSize;
    subResourceData.SlicePitch = subResourceData.RowPitch;

    // 기본 버퍼 자원으로의 자료 복사를 요청한다.
    // 대략적으로 말하자면, 보조함수 UpdateSubresources는 CPU 메모리를 임시 업로드 힙에 복사하고,
    // ID3D12CommandList::CopySubresourceRegion을 이용해서 임시 업로드 힙의 자료를 mBuffer에 복사한다.
	cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(), 
		D3D12_RESOURCE_STATE_COMMON, D3D12_RESOURCE_STATE_COPY_DEST));
    UpdateSubresources<1>(cmdList, defaultBuffer.Get(), uploadBuffer.Get(), 0, 0, 1, &subResourceData);
	cmdList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(defaultBuffer.Get(),
		D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_GENERIC_READ));

    // 주의 : 위의 함수 호출 이후에도 uploadBuffer를 계속 유지해야 한다.
    // 실제로 복사를 수행하는 명령 목록이 아직 실행되지 않았기 때문이다.
    // 복사가 완료 되었음이 확실해진 후에 호출자가 uploadBuffer를 해재하면된다.

    return defaultBuffer;
}

 

D3D12_SUBRESOURCE_DATA는 다음과 같이 정의되어 있다.

typedef struct D3D12_SUBRESOURCE_DATA {
  const void *pData;
  LONG_PTR   RowPitch;
  LONG_PTR   SlicePitch;
} D3D12_SUBRESOURCE_DATA;

1. pData: 버퍼 초기화용 자료를 담은 시스템 메모리 배열을 가리키는 포인터. 버퍼에 n개의 정점을 담을 수 있다고 할 때, 버퍼 전체를 초기화하려면 해당 시스템 메모리 배열에 적어도 n개의 정점이 있어야 한다.

2. RowPitch: 버퍼의 경우, 복사할 자료의 크기(바이트 개수).

3. SlicePitch: 버퍼의 경우, 복사할 자료의 크기(바이트 개수).

 

다음 코드는 입방체의 정점 여덟 개를 저장하는 기본 버퍼를 이 클래스를 이용해서 생성하는 방법이다.(각 정점의 Color를 다르게 부여했다.)

 std::array<Vertex, 8> vertices =
    {
        Vertex({ XMFLOAT3(-1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::White) }),
		Vertex({ XMFLOAT3(-1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Black) }),
		Vertex({ XMFLOAT3(+1.0f, +1.0f, -1.0f), XMFLOAT4(Colors::Red) }),
		Vertex({ XMFLOAT3(+1.0f, -1.0f, -1.0f), XMFLOAT4(Colors::Green) }),
		Vertex({ XMFLOAT3(-1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Blue) }),
		Vertex({ XMFLOAT3(-1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Yellow) }),
		Vertex({ XMFLOAT3(+1.0f, +1.0f, +1.0f), XMFLOAT4(Colors::Cyan) }),
		Vertex({ XMFLOAT3(+1.0f, -1.0f, +1.0f), XMFLOAT4(Colors::Magenta) })
    };
    
    const UINT vbByteSize = (UINT)vertices.size() * sizeof(Vertex);
    
    Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
    Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
    
    VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
		mCommandList.Get(), vertices.data(), vbByteSize, mBoxGeo->VertexBufferUploader);

이 코드에서 쓰인 Vertex구조체의 형식은 다음과 같이 정의된다.

struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};

정점 버퍼를 파이프라인에 묶으려면 정점 버퍼 자원을 서술하는 정점 버퍼 뷰를 만들어야 한다. RTV와는 달리, 정점 버퍼 뷰에는 서술자 힙이 필요하지 않다. 정점 버퍼 뷰를 대표하는 형식은 D3D12_VERTEX_BUFFER_VIEW 구조체이다.

typedef struct D3D12_VERTEX_BUFFER_VIEW {
  D3D12_GPU_VIRTUAL_ADDRESS BufferLocation;
  UINT                      SizeInBytes;
  UINT                      StrideInBytes;
} D3D12_VERTEX_BUFFER_VIEW;

1. BufferLocation: 생성할 뷰의 대상이 되는 정점 버퍼 자원의 가상 주소. 이 주소는 ID3D12Resource::GetGPUVirtualAddress 메서드로 얻을 수 있다.

2. SizeInBytes: BufferLocation에서 시작하는 정점 버퍼의 크기(바이트 개수).

3. StrideInBytes: 버퍼에 담긴 한 정점 원소의 크기(바이트 개수).

 

정점 버퍼를 생성하고 그에 대한 뷰까지 생성했다면, 이제 정점 버퍼를 파이프라인의 한입력 슬롯에 묶을 수 있다. 그러면 정점들이 파이프라인의 입력 조립기 단계로 공급된다. 다음은 정점 버퍼를 파이프라인에 묶는 메서드이다.

 

void IASetVertexBuffers(
  [in]           UINT                           StartSlot,
  [in]           UINT                           NumViews,
  [in, optional] const D3D12_VERTEX_BUFFER_VIEW *pViews
);

1. StartSlot: 시작 슬롯, 즉 첫째 정점 버퍼를 묶을 입력 슬롯의 색인. 입력 슬롯은 총 16개이다.(0~15)

2. NumBuffers: 입력 슬롯들에 묶을 정점 버퍼 개수. 시작 슬롯의 색인이 k이고 묶을 버퍼가 n개이면, 버퍼들은 입력 슬롯 Ik, Ik+1, lk+2 ...... Ik+n-1에 묶이게 된다.

3. pViews: 정점 버퍼 뷰 배열의 첫 원소를 가리키는 포인터.

 

다음은 이 메서드의 호출 예이다.

D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = sizeof(Vertex);
vbv.SizeInBytes = 8 * sizeof(Vertex);

D3D12_VERTEX_BUFFER_VIEW vertexBuffers[1] = { vbv };
mCommandList->IASetVertexBuffers(0, 1, vertexBuffers);

 

일단 입력 슬롯에 묶은 정점 버퍼는 다시 변경하지 않는 한 계속 그 입력 슬롯에 묶여있다. 따라서, 정점 버퍼를 여러 개 사용하는 경우 코드의 전반적인 구조를 다음과 같이 짜면 될 것이다.

 

ID3D12Resource* mVB1;
ID3D12Resource* mVB2;

D3D12_VERTEX_BUFFER_VIEW mVBView1;
D3D12_VERTEX_BUFFER_VIEW mVBView2;

/* ...정점 버퍼들과 뷰들을 생성한다...*/

mCommandList->IASetVertexBuffers(0, 1, &mVBView1);
/* ...정점 버퍼1을 이용해서 물체들을 그린다...*/

mCommandList->IASetVertexBuffers(0, 1, &mVBView2);
/* ...정점 버퍼2을 이용해서 물체들을 그린다...*/

 

정점 버퍼를 입력 슬롯에 설정한다고 해서 버퍼의 정점들이 바로 그려지는 것은 아니다. 단지 그 정점들을 파이프라인에 공급할 준비가 된 것일 뿐이다. 실제로 그리기 위해서는 ID3D12GraphicsCommandList::DrawInstanced 메서드를 호출해야 한다.

void DrawInstanced(
   UINT VertexCountPerInstance,
   UINT InstanceCount,
   UINT StartVertexLocation,
   UINT StartInstanceLocation
);

1. VertexCountPerInstance: 그릴 정점들의 개수(인스턴스당).
2. InstanceCount: 그릴 인스턴스 개수. 인스턴싱이라고 부르는 고급 기법에서는 여러 개의 인스턴스를 그리지만, 보통의 경우에는 인스턴스를 하나만 그리므로 1로 설정한다.
3. StartVertexLocation: 정점 버퍼에서 이 그리기 호출로 그릴 일련의 정점들 중 첫 정점의 색인(0 기반).

4. StartInstanceLocation: 고급 기법인 인스턴싱에 쓰이며, 지금은 그냥 0으로 설정한다.

 

그런데 DrawInstanced 메서드를 보면 주어진 정점들로 그릴 PrimitveTopology가 어떤 종류인지에 관한 매개변수가 없다. 따라서 ID3D12GraphicsCommandList::IASetPrimitiveTopology메서드로 기본 도형 위상 구조 상태를 결정한다. 다음은 이 메서드의 호출 예이다.

mCommandList->IASetPrimitiveTopology(D3D12_PRIMITIVE_TOPOLOGY::D3D1_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

 

Comments