DirectX 12를 사용한 지오메트리 렌더링: 상수 버퍼 최적화와 메시 로딩

1. Geosphere 생성을 통한 곡면 메시 구현

"Shapes" 예제를 확장하여 GeometryGenerator::CreateSphere 대신 GeometryGenerator::CreateGeosphere를 사용하도록 수정한다. 이 메서드는 구 형태의 메시를 더 자연스럽게 표현하기 위해 다각형 기반의 세분화(subdivision) 방식을 사용한다. 다음은 각 세분화 수준(level)에 따른 출력 결과의 특징이다:
  • 레벨 0: 초기 이십면체를 기반으로 하며, 매우 단순한 구형 외관을 가짐.
  • 레벨 1: 정점 수가 증가하며 표면이 다소 부드러워짐.
  • 레벨 2: 일반적인 게임 자산에서 허용 가능한 수준의 폴리곤 밀도를 제공.
  • 레벨 3: 시각적으로 거의 매끄러운 구체를 형성하며, 성능 오버헤드가 고려되어야 함.
렌더링 파이프라인에서의 적용은 다음과 같다:

std::unique_ptr<MeshGeometry> geo = std::make_unique<MeshGeometry>();
GeometryGenerator geoGen;
GeometryGenerator::MeshData sphere = geoGen.CreateGeosphere(1.5f, numSubdivisions); // 0~3 사용
여기서 numSubdivisions 값을 조정해 다양한 해상도를 실험할 수 있다.

2. 루트 서명 최적화: 루트 상수를 이용한 월드 행렬 전달

기존에는 데스크립터 테이블을 통해 개별 객체의 상수 버퍼(CBV)를 GPU에 전달했으나, 작은 데이터 크기의 경우 루트 시그니처 내에 직접 상수를 삽입하는 것이 더 효율적이다. 특히 월드 변환 행렬(16개의 float 값)은 루트 상수로 전달하기 적합하다.

2.1 루트 시그니처 구성 변경

루트 파라미터 배열에서 첫 번째 항목을 데스크립터 테이블 대신 32비트 루트 상수로 설정한다:

CD3DX12_ROOT_PARAMETER slotParam[2];

// 월드 행렬을 루트 상수로 전달 (16개 float = 64바이트)
slotParam[0].InitAsConstants(16, 0, D3D12_SHADER_VISIBILITY_VERTEX);

// Pass 상수는 여전히 CBV 테이블로 유지
CD3DX12_DESCRIPTOR_RANGE passCbvRange(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 1);
slotParam[1].InitAsDescriptorTable(1, &passCbvRange, D3D12_SHADER_VISIBILITY_ALL);

// 루트 시그니처 생성
CD3DX12_ROOT_SIGNATURE_DESC rootDesc(2, slotParam, 0, nullptr, D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

2.2 상수 버퍼 뷰 힙 재구성

객체별 CBV 생성 루프를 제거하고, 프레임당 PassConstant만 힙에 배치:

void ShapesApp::BuildConstantBufferViews()
{
    UINT passSize = d3dUtil::CalcConstantBufferByteSize(sizeof(PassConstants));

    for(int i = 0; i < gNumFrameResources; ++i)
    {
        auto resource = mFrameResources[i]->PassCB->Resource();
        D3D12_GPU_VIRTUAL_ADDRESS addr = resource->GetGPUVirtualAddress();

        D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
        cbvDesc.BufferLocation = addr;
        cbvDesc.SizeInBytes = passSize;

        CD3DX12_CPU_DESCRIPTOR_HANDLE handle(
            mCbvHeap->GetCPUDescriptorHandleForHeapStart(),
            i,
            mCbvSrvUavDescriptorSize
        );

        md3dDevice->CreateConstantBufferView(&cbvDesc, handle);
    }
}

2.3 렌더링 호출에서 루트 상수 설정

DrawRenderItems 함수 내에서 각 렌더 아이템의 월드 행렬을 루트 상수로 직접 전달:

for(auto ri : ritems)
{
    cmdList->IASetVertexBuffers(0, 1, &ri->Geo->VertexBufferView());
    cmdList->IASetIndexBuffer(&ri->Geo->IndexBufferView());
    cmdList->IASetPrimitiveTopology(ri->PrimitiveType);

    XMMATRIX world = XMLoadFloat4x4(&ri->World);
    XMVECTOR* vecPtr = reinterpret_cast<XMVECTOR*>(&world);

    // 16개의 32비트 상수 전달 (4x4 행렬)
    cmdList->SetGraphicsRoot32BitConstants(0, 16, vecPtr, 0);

    // Pass CBV는 계속해서 데스크립터 테이블 사용
    int passIdx = mCurrFrameResourceIndex;
    CD3DX12_GPU_DESCRIPTOR_HANDLE passHandle(
        mCbvHeap->GetGPUDescriptorHandleForHeapStart(),
        passIdx,
        mCbvSrvUavDescriptorSize
    );
    cmdList->SetGraphicsRootDescriptorTable(1, passHandle);

    cmdList->DrawIndexedInstanced(ri->IndexCount, 1, ri->StartIndexLocation, ri->BaseVertexLocation, 0);
}

2.4 불필요한 CBV 업데이트 제거

UpdateObjectCBs 함수 내에서 객체 상수 버퍼의 CPU 측 복사 로직을 제거한다. 이제 월드 데이터는 매 프레임 즉석에서 루트 상수로 전달되므로, 별도의 상수 버퍼 업데이트가 필요 없다.

// ObjectCB->CopyData 호출 제거됨
// 모든 월드 행렬은 DrawRenderItems에서 실시간 전달

2.5 성능 이점

  • 데스크립터 힙 접근 감소 → 바인딩 오버헤드 감소
  • 작은 데이터에 대한 빠른 업로드 가능
  • 최대 16 DWORD(64바이트)까지 지원하므로 행렬 하나 전달에 적합

3. 외부 메시 데이터 로딩: Skull.txt 기반 그리기

주어진 Models/Skull.txt 파일은 정점과 인덱스 리스트를 포함하고 있으며, 이는 정적 ASCII 형식으로 저장된 메시 데이터이다. 이를 로딩하여 렌더링 파이프라인에 통합한다. 예시 데이터 구조:

// 파일 형식 예시
VertexCount: 248
IndexCount:  720
Vertices:
-0.500000, 0.000000, 0.500000, ...
Indices:
0, 1, 2, ...
로딩 코드 예:

std::vector<Vertex> vertices;
std::vector<uint32_t> indices;

std::ifstream fin("Models/Skull.txt");
std::string line;

std::getline(fin, line); // VertexCount
int vertCount = std::stoi(line.substr(line.find(':')+1));
vertices.resize(vertCount);

// 정점 데이터 읽기
for(int i = 0; i < vertCount; ++i)
{
    float x,y,z,nx,ny,nz;
    fin >> x >> y >> z >> nx >> ny >> nz;
    vertices[i] = { XMFLOAT3(x,y,z), XMFLOAT3(nx,ny,nz) };
}

// 인덱스 읽기
std::getline(fin, line); // IndexCount
int indexCount = std::stoi(line.substr(line.find(':')+1));
indices.resize(indexCount);

for(int i = 0; i < indexCount; ++i)
{
    fin >> indices[i];
}
생성된 메시는 UploadToDefaultBuffer를 통해 GPU 메모리에 업로드되고, 기존 렌더 아이템에 추가된다:

SubmeshGeometry skullSubmesh;
skullSubmesh.IndexCount = (UINT)indices.size();
skullSubmesh.StartIndexLocation = 0;
skullSubmesh.BaseVertexLocation = 0;

std::unique_ptr<MeshGeometry> skullGeo = std::make_unique<MeshGeometry>();
skullGeo->Name = "skull";
skullGeo->LoadToDefaultBuffer(md3dDevice.Get(), mCommandList.Get(), vertices, indices);
skullGeo->DrawArgs["skull"] = skullSubmesh;

mGeometries["skull"] = std::move(skullGeo);
마지막으로, 해당 메시를 렌더 아이템으로 등록하고 그리기 명령을 추가하면 화면에 뼛속이 드러난 해골 모델이 출력된다.

태그: DirectX12 GPU 렌더링 루트 시그니처 루트 상수 메시 로딩

5월 27일 23:03에 게시됨