기록공간

[DirectX 12] Direct3D 그리기 연산(정육면체) - 3 본문

DirectX/기초

[DirectX 12] Direct3D 그리기 연산(정육면체) - 3

입코딩 2020. 5. 1. 20:44
반응형

상수 버퍼

상수 버퍼의 생성

상수 버퍼는 쉐이더 프로그램에서 참조하는 자료를 담는 GPU 자원(ID3D12Resource)의 예이다. 앞에서 말했듯이 텍스처나 기타 버퍼 자원 역시 쉐이더 프로그램에서 참조할 수 있다. 이전에 봤던 정점 쉐이더 코드에 이런 코드가 있었다.

cbuffer cbPerObject : register(b0)
{
    float4x4 gWorldViewProj;
};

이 코드는 cbPerObject라는 cbuffer 객체(상수 버퍼)를 참조한다. 이 예에서 상수 버퍼는 gWorldViewProj라는 4 x 4 행렬 하나만 저장한다. 이 행렬은 한 점을 Local space에서 동차 절단 공간으로 변환하기 위해, World 행렬, View 행렬, Projection 행렬을 결합한 것이다. HLSL에서 4 x 4 행렬은 내장 형식 float4x4로 대표된다. 그 외에도 여러 행렬 형식이 있다. (float2x3, 3x3, 등)

 

상수 버퍼는 CPU가 프레임당 한 번 갱신하는 것이 일반적이다. 예를 들어 카메라가 매 프레임마다 움직인다면, 프레임마다 상수 버퍼를 새 view 행렬로 갱신해야 할 것이다. 그렇기 때문에 상수 버퍼는 기본 힙이 아닌 업로드 힙에 만들어야 한다. 그래야 CPU가 버퍼의 내용을 갱신할 수 있다.

 

또한, 상수 버퍼는 특별한 하드웨어 요구조건이 있다. 크기가 반드시 최소 하드웨어 할당 크기(256바이트)의 배수여야 한다는 것이다.

 

같은 종류의 상수 버퍼를 여러 개 사용해야 하는 경우가 많다. 예를 들어 위의 상수 버퍼 cbPerObject는 물체마다 달라지는 상수들을 담으므로, 만일 장면의 물체가 n개 이면 상수 버퍼가 n개 필요하다. 다음 코드는 NumElements 개의 상수 버퍼 객체를 담는 하나의 버퍼를 생성하는 방법을 보여준다.

struct ObjectConstants
{
    // 상수 버퍼로 쓸 동차절단 공간 변환 행렬을 단위 행렬로 초기화 한다.
    DirectX::XMFLOAT4X4 WorldVIewProj = MathHelpter::Identity4X4();
};

UINT elementByteSize = d3dUtil::CalcContantBufferByteSize(sizeof(ObjectConstants));

ComPtr<ID3D12Resource> mUploadCBuffer;
device->CreateCommittedResource(
    &CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
    D3D12_HEAP_FLAG_NONE,
    &CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize * NumElements),
    D3D12_RESOURCE_STATE_GENERIC_READ,
    nullptr,
    IID_PPV_ARGS(&mUploadCBuffer));

mUploadCBuffer를 ObjectConstants 형식의 상수 버퍼들의 배열을 담는 버퍼라고 간주할 수 있다. 어떤 물체를 그릴 때가 되면, 이 버퍼에서 해당 물체를 위한 상수를 서술하는 상수 버퍼 뷰를 파이프라인에 묶는다. 상수 버퍼들의 배열을 담은 mUploadCBuffer 자체를 그냥 상수 버퍼라고 부르는 경우도 많다.

 

사용자 정의 함수 d3dUtil::CalcConstantBufferByteSize는 버퍼의 크기를 최소 하드웨어 할당 크기의 배수가 되게 하는 계산을 수행한다.

UINT d3dUtil::CalcConstantBufferByteSize(UINT byteSize)
{
    // 주어진 크기에 가장 가까운 256의 배수를 구해서 돌려준다.
    // 예를 들어 300이 byteSize로 들어 왔다고 했을때
    // 255를 더하고 비트 마스크를 이용해서 하위 2바이트(8비트), 
    // 즉 256보다 작은 비트들을 모두 0으로 만든다.
    // (300 + 255) & ~255
    // 555 & ~255   (255 = ~(0000 1111 1111) -> 1111 0000 0000)
    // 0010 0010 1011 & 1111 0000 0000
    // 0010 0000 0000
    // 512
    return (byteSize + 255) & ~255
}

상수 자료를 256의 배수 크기로 할당하지만 HLSL 구조체에서 해당 상수 자료에 여분의 바이트들을 명시적으로 채울 필요가 없다. 채우는 작업은 암묵적으로 일어난다.

// 256바이트 경계에 맞게 바이트들이 암묵적으로 채워진다
cbuffer cbPerObject : register(b0)
{
    float4x4 gWorldViewProj;
};

// 256바이트 경계에 맞게 바이트들이 명시적으로 채운다.
cbuffer cbPerObject : register(b0)
{
    float4x4 gWorldViewProj;
    float4x4 dummy0;
    float4x4 dummy1;
    float4x4 dummy2;
};

HLSL ver 5.1에서는 상수 버퍼를 정의하는 또 다른 문법을 지원한다.

struct ObjectConstants
{
    float4x4 gWorldViewProj;
    uint matIndex;
};
ConstantBuffer<ObjectConstants> gObjConstants : register(b0);

상수 버퍼에 담을 자료의 형식을 개별적인 구조체로 정의하고, 그 구조체를 이용해서 상수 버퍼를 정의한다. 

 

상수 버퍼의 갱신

앞에서 상수 버퍼를 업로드 힙에 생성했으므로, CPU에서 상수 버퍼 자원에 자료를 올릴 수 있다. 자료를 올리려면 먼저 자원 자료를 가리키는 포인터를 얻어야 하는데, 그러려면 다음과 같이 Map 메서드를 호출해야 한다. 

ComPtr<ID3D12Resource> mUploadBuffer;
BYTE* mMappedData = nullptr;
mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));

Map 메서드의 첫 매개변수는 CPU 메모리에 매핑 시키려는 부분 자원의 Index이다. 버퍼의 경우는 버퍼 자체가 부분 자원이기 때문에 0을 지정하면 된다. 둘째 매개변수는 대응시킬 메모리의 범위를 서술하는 D3D12_RANGE의 구조체 포인터인데, 자원 전체를 대응시키려면 지금처럼 널 포인터를 지정하면 된다. 세 번째 매개변수는 출력 매개변수로 대응된 자료를 가리키는 포인터가 설정된다. 시스템 메모리에 있는 자료를 상수 버퍼에 복사하려면 memcpy를 사용하면 된다.

memcpy(mMappedData, &data, dataSizeInBytes);

Map을 호출하여 데이터를 복사했다면, 반드시 Unmap을 호출해줘야 한다.

if(mUploadBuffer != nullptr)
    mUploadBuffer->Unmap(0, nullptr);
    
mMappedData = nullptr;

Unmap의 첫째 둘째 매개변수는 Map의 매개변수와 같은 형식이다.

 

상수 버퍼 서술자

앞에서도 말했지만 자원을 렌더링 파이프라인에 묶으려면 서술자 객체가 필요하다. 상수 버퍼를 파이프라인에 묶을 때에도 역시 서술자가 필요하다. 상수 버퍼 서술자는 D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV 형식의 서술자가 힙에 담긴다. 이 힙은 상수 버퍼, 셰이더 리소스 뷰(SRV), 순서 없는 접근 뷰(UAV) 서술자들을 섞어서 담을 수 있다. 서술자 힙 생성은 다음과 같다.

D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
cbvHeapDesc.NumDescriptors = 1;
cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
cbvHeapDesc.NodeMask = 0;

ComPtr<ID3D12DescriptorHeap> mCbvHeap = nullptr;
md3dDevice->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&mCbvHeap));

이 코드는 렌더타겟이나 깊이 스텐실 버퍼 서술자 힙을 생성하는 코드와 비슷하다. 한 가지 중요한 차이는 셰이더 프로그램에서 이 서술자들에 접근할 것임을 뜻하는 D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE 플래그를 지정했다는 점이다. 

 

상수 버퍼 뷰를 생성하려면 D3D12_CONSTANT_BUFFER_VIEW_DESC 인스턴스를 채운 후 ID3D12Device::CreateConstantBufferView를 호출해야 한다.

struct ObjectConstants
{
    XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};

// 물체 n개의 상수 자료를 담을 상수 버퍼
// UploadBuffer는 업로드 버퍼를 자동으로 할당해주는 사용자 정의 클래스이다.
// 예제 자료 코드를 참고.
std::unique_ptr<UploadBuffer<ObjectConstants>> mObjectCB = nullptr;
mObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(
    md3dDevice.Get(), n, true);
    
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));

// 버퍼 자체의 시작 주소
D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();

// 버퍼에 담긴 i번째 상수 버퍼의 오프셋
int boxCBufIndex = i;
cbAddress += boxCBufIndex * objCBByteSize;

D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = cbAddress;
cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));

md3dDevice->CreateConstantBufferView(
    &cbvDesc,
    mCbvHeap->GetCPUDescriptorHandleForHeapStart());

 

루트 시그니처(서명)와 서술자 테이블

쉐이더 프로그램들은 특정 종류의 자원들이 렌더링 파이프라인에 묶인 상태에서 그리기 호출이 실행되었다고 기대한다. 자원들은 특정 레지스터 슬롯에 묶이며, 쉐이더 프로그램들은 그 슬롯들을 통해서 자원들에 접근하게 된다. 예를 들어 이전의 정점 쉐이더와 픽셀 쉐이더는 상수 버퍼 하나가 레지스터 b0에 묶여 있다고 기대한다. 다음은 파이프라인에 묶인 다양한 자원들의 예이다.

// 텍스처 레지스터 슬롯 0에 묶인 텍스처 자원
Texture2D gDiffuseMap = register(t0);

// 표본 추출기 레지스터 슬롯 0~5에 묶인 표본추출기 자원들
SamplerState gsamPointWrap          : register(s0);
SamplerState gsamPointClamp         : register(s1);
SamplerState gsamLinearWrap         : register(s2);
SamplerState gsamLinearClamp        : register(s3);
SamplerState gsamAnisotropicWrap    : register(s4);
SamplerState gsamAnisotropicClamp   : register(s5);

// 상수 버퍼 레지스터 슬롯 0-2에 묶인 cbuffer 자원
cbuffer cbPerObject : register(b0)
{
    float4x4 gWorld;
    float4x4 gTexTransform;
};

// 재질마다 달라지는 상수 자료
cbuffer cbPass : register(b1)
{
    float4x4 gView;
    float4x4 gProj;
    // ...
};

cbuffer cbMaterial : register(b2)
{
    // ...
};

루트 시그니처(root signature)는 그리기 호출 전에 응용 프로그램이 반드시 렌더링 파이프라인에 묶어야 하는 자원들이 무엇이고 그 자원들이 쉐이더 입력 레지스터들에 어떻게 대응되는지를 정의한다. 루트 시그니처는 반드시 그리기 호출에 쓰이는 쉐이더들과 호환되어야 한다. (즉, 쉐이더들이 기대하는 렌더링 파이프라인에 묶인 모든 자원을 제공해야 한다) 루트 시그니처의 유효성은 파이프라인 상태 객체를 생성할때 검증된다. 그리기 호출마다 서로 다른 쉐이더 프로그램들을 사용할 수 있으며, 그런 경우에는 루트 시그니처도 달라야 한다.

 

Direct3D에서 루트 시그니처를 대표하는 인터페이스는 ID3D12RootSignature이다. 루트 시그니처는 그리기 호출에서 쉐이더들이 기대하는 자원들을 서술하는 루트 매개변수들의 배열로 정의된다. 루트 매개변수는 루트 상수루트 서술자 일 수도 있고 서술자 테이블 일 수도 있다. 여기서는 서술자 테이블만을 사용한다. 서술자 테이블은 서술자 힙 안에 있는 연속된 서술자들의 구간을 지정한다.

 

다음 코드는 루트 매개변수 하나로 된 루트 시그니처를 생성한다. 그 루트 매개변수는 상수 버퍼 뷰(CBV) 하나를 담기에 충분한 크기의 서술자 테이블이다.

// 루트 매개변수는 루트 서술자 혹은 루트 상수나 테이블일 수도 있다.
CD3DX12_ROOT_PARAMETER slogRootParameter[1];

CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
    D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
    1,  // 테이블 서술자 개수
    0); // 이 루트 매개변수에 묶일 쉐이더 인수들의 기준 레지스터 번호
    
slotRootParameter[0].InitAsDescriptorTable(
    1,         // 구간 개수
    &cbvTable); // 구간들의 배열을 가리키는 포인터
    
// 루트 시그니처는 루트 매개변수들의 배열이다.
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr,
    D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);
    
// 상수 버퍼 하나로 구성된 서술자 구간을 가리키는
// 슬롯 하나로 이루어진 루트 시그니처를 생성한다.
ComPtr<ID3DBlob> serializedRootSig = nullptr;
ComPtr<ID3DBlob> errorBlob = nullptr;
HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, 
    D3D_ROOT_SIGNATURE_VERSION_1,
    serializedRootSig.GetAddressOf(),
    errorBlob.GetAddressOf());
    
ThrowIfFailed(md3dDevice->CreateRootSignature(
    0,
    serializedRootSig->GetBufferPointer(),
    serializedRootSig->GetBufferSize(),
    IID_PPV_ARGS(&mRootSignature)));

일단 지금은 다음 코드가 CBV 하나 (HLSL 코드에서 register(b0)에 대응되는)를 담은 서술자 테이블을 기대하는 루트 매개변수를 생성하는 점만 이해하고 넘어가자.

CD3DX12_ROOT_PARAMETER slogRootParameter[1];

CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(
    D3D12_DESCRIPTOR_RANGE_TYPE_CBV,
    1,  // 테이블 서술자 개수
    0); // 이 루트 매개변수에 묶일 쉐이더 인수들의 기준 레지스터 번호
    
slotRootParameter[0].InitAsDescriptorTable(
    1,         // 구간 개수
    &cbvTable); // 구간들의 배열을 가리키는 포인터

이 루트 시그니처 예제는 매우 단순한 편이다. 이후에는 더더욱 복잡한 루트 시그니처를 보게 될 것이다.

 

루트 시그니처는 응용 프로그램이 렌더링 파이프라인에 묶을 자원들을 정의하기만 한다. 루트 시그니처가 실제로 자원들을 묶지는 않는다. ID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable을 호출해서 서술자 테이블을 파이프라인에 묶는다.

void ID3D12GraphicsCommanList::SetGraphicsRootDescriptorTable(
    UINT RootParameterIndex, // 설정하고자 하는 루트 시그니처 인덱스
    D3D12_GPU_DESCRIPTOR_HANDLE BaseDescriptor); // 설정하고자 하는 서술자 테이블의 
                                                 // 첫 서술자에 해당하는 서술자의 핸들

다음 코드는 루트 시그니처와 CBV 힙을 명령 목록에 설정하고, 파이프라인에 묶을 자원들을 지정하는 서술자 테이블을 설정한다.

mCommandList->SetGraphicsRootSignature(mRootSignature.Get());
ID3D12DescriptorHeap* descriptorHeaps[] = {mCbvHeap.Get()};
mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);

// 그리기 호출에 사용할 CBV의 오프셋
CD3DX12_GPU_DESCRIPTOR_HANDLE cbv(mCbvHeap->GetGPUDescriptorHandleForHeapStart());
cbv.Offset(cbvIndex, mCbvSrvUavDescriptorSize);

mCommandList->SetGraphicsRootDescriptorTable(0, cbv);

성능을 위해서는 루트 시그니처를 최대한 작게 만들고, 루트 시그니처의 변경을 최소화해야 한다. 또한 루트 시그니처를 변경하면 기존의 모든 바인딩이 사라진다. 따라서 새 루트 시그니처가 기대하는 모든 자원을 파이프라인에 다시 묶어야 한다.

 

래스터화기 상태

렌더링 파이프라인의 많은 부분이 프로그래밍 가능하지만, 일부는 설정만 가능하다. 렌더링 파이프라인의 래스터화 단계가 그런 예이다. 래스터화 단계는 래스터화기 상태(raterizer state)를 통해서 구성한다. 이 상태를 대표하는 것은 D3D12_RASTERIZER DESC 구조체이다.

typedef struct D3D12_RASTERIZER_DESC
{
    D3D12_FILL_MODE FillMode;     // 기본 : D3D12_FILL_SOLID
    D3D12_CULL_MODE CullMode;     // 기본 : D3D12_CULL_BACK
    BOOL FrontCounterClockwise;   // 기본 : false
    INT DepthBias;                // 기본 : 0
    FLOAT DepthBiasClamp;         // 기본 : 0.f
    FLOAT SlopeScaledDepthBias;   // 기본 : 0.f
    BOOL DepthClipEnable;         // 기본 : true
    BOOL ScissorEnable;           // 기본 : false
    BOOL MultisampleEnable;       // 기본 : false
    BOOL AntialiasedLineEnable;   // 기본 : false
    UINT ForcedSampleCount        // 기본 : 0
}

1. FillMode : 어떻게 그릴 지를 결정한다. D3D12_FILL_WIREFRAME을 설정하면 와이어 프레임으로 물체를 그린다. D3D12_FILL_SOLID가 기본 속성으로 면의 속을 채운 상태로 그린다.

 

2. CullMode : 선별 작업을 결정한다. 선별을 끄려면 D3D12_CULL_NONE을, 후면을 선별하려면 D3D12_CULL_BACK, 전면을 선별하려면 D3D12_CULL_FRONT를 지정한다. 기본은 후면 선별이다.

 

3. FrontCounterClockwise : 정점들이 시계방향으로 감긴 삼각형을 전면 삼각형으로 취급하고 반시계 방향으로 감긴 삼각형을 후면 삼각형으로 취급하려면 false를 지정한다. 반대의 경우로 하려면 true를 지정한다.

 

4. ScissorEnable : 가위 판정의 활성화 여부이다. 활성화는 true, 비활성화는 false를 설정한다. 가위 판정은 뷰포트 화면을 마치 가위로 자른 것과 같은 효과를 주는 것으로 다음과 같은 작업을 가능하게 해 준다.

 

 

파이프라인 상태 객체

지금까지 여러 가지 렌더링 준비 과정을 살펴보았다. 그런데 그런 객체들을 실제로 사용하기 위해서 렌더링 파이프라인에 묶는 방법은 아직 이야기하지 않았다. 렌더링 파이프라인의 상태를 제어하는 대부분의 객체는 파이프라인 상태 객체(Pipeline State Object, PSO)라 부르는 집합체를 통해서 지정된다. 

 

Direct3D에서 PSO를 대표하는 인터페이스는 ID3D12PipelineState이다. PSO를 생성하려면 우선 파이프라인 상태를 서술하는 D3D12_GRAPHICS_PIPELINE_STATE_DESC 구조체의 인스턴스를 채워야 한다.

typedef struct D3D12_GRAPHICS_PIPELINE_STATE_DESC
{
    ID3D12RootSignature *pRootSignature;  // 묶을 루트 시그니처 포인터 
    D3D12_SHADER_BYTECODE VS;             // 묶을 정점 셰이더 서술 구조체
    D3D12_SHADER_BYTECODE PS;             // 묶을 픽셀 셰이더 서술 구조체
    D3D12_SHADER_BYTECODE DS;             // 묶을 도메인 셰이더 서술 구조체
    D3D12_SHADER_BYTECODE HS;             // 묶을 헐 셰이더 서술 구조체
    D3D12_SHADER_BYTECODE GS;             // 묶을 기하 셰이더 서술 구조체
    D3D12_STREAM_OUTPUT_DESC StreamOutput; // 스트림 출력에 쓰인다. 지금은 0
    D3D12_BLEND_DESC BlendState;           // 혼합 방식을 서술하는 혼합 상태(블랜딩), 기본값
    UINT SampleMask;                       // 다중표본화 설정
    D3D12_RASTERIZER_DESC RasterizerState; // 래스터화기 상태를 설정
    D3D12_DEPTH_STENCIL_DESC DepthStencilState; // 깊이 스텐실 상태를 설정
    D3D12_INPUT_LAYOUT_DESC InputLayout;    // 입력 배치를 서술하는 구조체를 지정
    D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType; // 기본도형 위상구조 설정
    UINT NumRenderTargets;                  // 동시에 사용할 렌더 대상 개수
    DXGI_FORMAT RTVFormats[8];              // 렌더 대상 형식들의 배열 (동시에 그릴수 있게)
    DXGI_FORMAT DSVFormat;                  // 깊이 스텐실 버퍼의 형식
    DXGI_SAMPLE_DESC SampleDesc;            // 다중표본화의 표본 개수와 품질 수준 서술
} D3D12_GRAPHICS_PIPELINE_STATE_DESC;

인스턴스를 다 채운 후에는 ID3D12Device::CreateGraphicsPipelineState 메서드를 이용해 ID3D12PipelineState 객체를 생성한다.

D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;

// 파이프라인 상태 구조체를 채운다...

ComPtr<ID3D12PipelineState> mPSO;
md3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO)));

하나의 집합적 ID3D12PipelineState 객체에 상당히 많은 상태가 들어 있다. 이 모든 객체를 하나의 집합체로서 렌더링 파이프라인에 지정하는 이유는 성능 때문이다. 이 덕분에 Direct3D는 모든 상태가 호환되는지를 미리 검증할 수 있으며, 드라이버는 하드웨어 상태의 프로그래밍을 위한 모든 코드를 미리 생성할 수 있다. 

 

뷰포트나 직사각형 같은 상태는 독립적으로 지정해도 비효율적이지 않기 때문에 PSO에 포함되지 않는다. 

Direct3D는 기본적으로 상태 기계이다. 명시적으로 변경되지 않는 한 그대로 남아있는 것들이 많다. 한 장면에서 여러 부류의 물체들을 각자 다른 PSO를 사용하여 그린다면, 코드의 구조를 다음과 같이 짜야한다.

// 초기 PSO을 지정한다.
mCommandList->Reset(mDirectCmdListAlloc.Get(), mPSO1.Get());
// PSO 1을 이용하여 물체들을 그린다.

// PSO를 변경한다.
mCommandList->SetPipelineState(mPSO2.Get());
// PSO 2을 이용하여 물체들을 그린다.

// PSO를 변경한다.
mCommandList->SetPipelineState(mPSO3.Get());
// PSO 3을 이용하여 물체들을 그린다.

다른 명령 목록에 묶었다면, 다른 PSO가 묶이기 전까지는 그 PSO가 계속 적용된다. 성능을 위해서는 PSO의 변경을 최소화해야 한다. 같은 PSO를 사용할 수 있는 물체들은 서로 함께 그리는 것이 좋다. (그리기 호출마다 PSO의 변경을 하는 것은 매우 바람직하지 않다)

 

상자 예제 

지금까지 살펴봤던 모든 내용을 바탕으로 3d 세계에 정육면체의 상자를 띄워본다. 코드는 다음과 같으며 이 코드는 다음 사이트에서 받을 수 있다. Box예제 https://github.com/d3dcoder/d3d12book

 

d3dcoder/d3d12book

Sample code for the book "Introduction to 3D Game Programming with DirectX 12" - d3dcoder/d3d12book

github.com

#include "../../Common/d3dApp.h"	// Direct3D 장치를 위한 사용자 정의 클래스
#include "../../Common/MathHelper.h"	// 수학 연산을 위한 사용자 정의 클래스
#include "../../Common/UploadBuffer.h"  // 손쉬운 업로드 버퍼 사용을 위한 사용자 정의 클래스

using Microsoft::WRL::ComPtr;
using namespace DirectX;
using namespace DirectX::PackedVector;

// 정점 구조체
struct Vertex
{
    XMFLOAT3 Pos;
    XMFLOAT4 Color;
};

// 상수버퍼로 쓰일 정보를 담는 구조체
struct ObjectConstants
{
	// 동차절단 공간 변환에 쓰일 행렬
    XMFLOAT4X4 WorldViewProj = MathHelper::Identity4x4();
};

class BoxApp : public D3DApp
{
public:
	BoxApp(HINSTANCE hInstance);
    BoxApp(const BoxApp& rhs) = delete;
    BoxApp& operator=(const BoxApp& rhs) = delete;
	~BoxApp();

	virtual bool Initialize()override;

private:
    virtual void OnResize()override;
    virtual void Update(const GameTimer& gt)override;
    virtual void Draw(const GameTimer& gt)override;

    virtual void OnMouseDown(WPARAM btnState, int x, int y)override;
    virtual void OnMouseUp(WPARAM btnState, int x, int y)override;
    virtual void OnMouseMove(WPARAM btnState, int x, int y)override;

    void BuildDescriptorHeaps();
	void BuildConstantBuffers();
    void BuildRootSignature();
    void BuildShadersAndInputLayout();
    void BuildBoxGeometry();
    void BuildPSO();

private:
    ComPtr<ID3D12RootSignature> mRootSignature = nullptr;
    ComPtr<ID3D12DescriptorHeap> mCbvHeap = nullptr;

    std::unique_ptr<UploadBuffer<ObjectConstants>> mObjectCB = nullptr;

	std::unique_ptr<MeshGeometry> mBoxGeo = nullptr;

    ComPtr<ID3DBlob> mvsByteCode = nullptr;
    ComPtr<ID3DBlob> mpsByteCode = nullptr;

    std::vector<D3D12_INPUT_ELEMENT_DESC> mInputLayout;

    ComPtr<ID3D12PipelineState> mPSO = nullptr;

    XMFLOAT4X4 mWorld = MathHelper::Identity4x4();
    XMFLOAT4X4 mView = MathHelper::Identity4x4();
    XMFLOAT4X4 mProj = MathHelper::Identity4x4();

    float mTheta = 1.5f*XM_PI;
    float mPhi = XM_PIDIV4;
    float mRadius = 5.0f;

    POINT mLastMousePos;
};

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance,
				   PSTR cmdLine, int showCmd)
{
	// 디버그 빌드에서는 실행시점 메모리 점검 기능을 켠다.
#if defined(DEBUG) | defined(_DEBUG)
	_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );
#endif

    try
    {
		// Box를 그리는 프로그램 객체를 만들어 초기화한다.
        BoxApp theApp(hInstance);
        if(!theApp.Initialize())
            return 0;

		// 객체를 실행시킨다.
        return theApp.Run();
    }
    catch(DxException& e)
    {
		// 초기화에 실패한경우 오는 곳
        MessageBox(nullptr, e.ToString().c_str(), L"HR Failed", MB_OK);
        return 0;
    }
}

BoxApp::BoxApp(HINSTANCE hInstance)
: D3DApp(hInstance) 
{
}

BoxApp::~BoxApp()
{
}

bool BoxApp::Initialize()
{
	// Direct3D를 초기화한다.
    if(!D3DApp::Initialize())
		return false;
		
    // 초기화 명령들을 준비하기 위해 명령 목록을 재설정한다.
    ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), nullptr));
 
	// 초기화 명령들
    BuildDescriptorHeaps();
	BuildConstantBuffers();
    BuildRootSignature();
    BuildShadersAndInputLayout();
    BuildBoxGeometry();
    BuildPSO();

    // 초기화 명령들을 실행한다.
    ThrowIfFailed(mCommandList->Close());
	ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
	mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);

    // 초기화가 완료될 때까지 기다린다.
    FlushCommandQueue();

	return true;
}

void BoxApp::OnResize()
{
	D3DApp::OnResize();

    // 창의 크기가 바뀌었으므로 종횡비를 갱신하고 투영 행렬을 다시 계산한다.
    XMMATRIX P = XMMatrixPerspectiveFovLH(0.25f*MathHelper::Pi, AspectRatio(), 1.0f, 1000.0f);
    XMStoreFloat4x4(&mProj, P);
}

void BoxApp::Update(const GameTimer& gt)
{
    // 구면 좌표를 직교 좌표로 변환한다.
    float x = mRadius*sinf(mPhi)*cosf(mTheta);
    float z = mRadius*sinf(mPhi)*sinf(mTheta);
    float y = mRadius*cosf(mPhi);

    // 시야 행렬을 구축한다.
    XMVECTOR pos = XMVectorSet(x, y, z, 1.0f);
    XMVECTOR target = XMVectorZero();
    XMVECTOR up = XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f);

    XMMATRIX view = XMMatrixLookAtLH(pos, target, up);
    XMStoreFloat4x4(&mView, view);

    XMMATRIX world = XMLoadFloat4x4(&mWorld);
    XMMATRIX proj = XMLoadFloat4x4(&mProj);
    XMMATRIX worldViewProj = world*view*proj;

	// 상수 버퍼를 갱신한다.
	ObjectConstants objConstants;
    XMStoreFloat4x4(&objConstants.WorldViewProj, XMMatrixTranspose(worldViewProj));
    mObjectCB->CopyData(0, objConstants);
}

void BoxApp::Draw(const GameTimer& gt)
{
    // 명령 기록에 관련된 메모리의 재활용을 위해 명령 할당자를 재설정한다.
    // 재설정은 GPU가 관련 명령 목록들을 모두 처리한 후에 일어난다.
	ThrowIfFailed(mDirectCmdListAlloc->Reset());

	// 명령 목록을 ExecuteCommandList를 통해서 명령 대기열에
	// 추가했다면 명령 목록을 재설정할 수 있다. 명령 목록을
	// 재설정하면 메모리가 재활용된다.
    ThrowIfFailed(mCommandList->Reset(mDirectCmdListAlloc.Get(), mPSO.Get()));

    mCommandList->RSSetViewports(1, &mScreenViewport);
    mCommandList->RSSetScissorRects(1, &mScissorRect);

    // 자원 용도에 관련된 상태 전이를 Direct3D에 통지한다.
	mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
		D3D12_RESOURCE_STATE_PRESENT, D3D12_RESOURCE_STATE_RENDER_TARGET));

    // 새로 그리기 위해 벡버퍼와 깊이 버퍼를 지운다.
    mCommandList->ClearRenderTargetView(CurrentBackBufferView(), Colors::LightSteelBlue, 0, nullptr);
    mCommandList->ClearDepthStencilView(DepthStencilView(), D3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCIL, 1.0f, 0, 0, nullptr);
	
    // 렌더링 결과가 기록될 렌더 대상 버퍼들을 지정한다.
	mCommandList->OMSetRenderTargets(1, &CurrentBackBufferView(), true, &DepthStencilView());

	// 루트 시그니처와 서술자 힙을 설정한다.
	ID3D12DescriptorHeap* descriptorHeaps[] = { mCbvHeap.Get() };
	mCommandList->SetDescriptorHeaps(_countof(descriptorHeaps), descriptorHeaps);
	mCommandList->SetGraphicsRootSignature(mRootSignature.Get());

	// 박스의 정점 정보를 묶어 입력 조립기에 세팅한다.
	mCommandList->IASetVertexBuffers(0, 1, &mBoxGeo->VertexBufferView());
	mCommandList->IASetIndexBuffer(&mBoxGeo->IndexBufferView());
    mCommandList->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
    
	// 상수버퍼와 같은 파이프라인에 묶을 자원들을 지정하는 서술자 테이블을 세팅한다.
    mCommandList->SetGraphicsRootDescriptorTable(0, mCbvHeap->GetGPUDescriptorHandleForHeapStart());

	// 인덱스 정보에 맞게 상자를 그린다.
    mCommandList->DrawIndexedInstanced(
		mBoxGeo->DrawArgs["box"].IndexCount, 
		1, 0, 0, 0);
	
    // 자원 용도에 관련된 상태 전이를 Direct3D에 통보한다.
	mCommandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(CurrentBackBuffer(),
		D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));

    // 명령들의 기록을 마친다.
	ThrowIfFailed(mCommandList->Close());
 
    // 명령 실행을 위해 명령 목록을 명령 대기열에 추가한다.
	ID3D12CommandList* cmdsLists[] = { mCommandList.Get() };
	mCommandQueue->ExecuteCommandLists(_countof(cmdsLists), cmdsLists);
	
	// 후면 버퍼와 전면 버퍼를 교환한다.
	ThrowIfFailed(mSwapChain->Present(0, 0));
	mCurrBackBuffer = (mCurrBackBuffer + 1) % SwapChainBufferCount;

	// 이 프레임의 명령들이 모두 처리되길 기다린다. 이러한 대기는
	// 비효율적이다. 예제의 간단함을 위해 사용하지만, 이후의 예제에서는
	// 프레임마다 대기할 필요가 없게 만들것이다.
	FlushCommandQueue();
}

void BoxApp::OnMouseDown(WPARAM btnState, int x, int y)
{
    mLastMousePos.x = x;
    mLastMousePos.y = y;

    SetCapture(mhMainWnd);
}

void BoxApp::OnMouseUp(WPARAM btnState, int x, int y)
{
    ReleaseCapture();
}

void BoxApp::OnMouseMove(WPARAM btnState, int x, int y)
{
    if((btnState & MK_LBUTTON) != 0)
    {
        // 마우스 한 픽셀 이동을 4분의 1도에 대응시킨다.
        float dx = XMConvertToRadians(0.25f*static_cast<float>(x - mLastMousePos.x));
        float dy = XMConvertToRadians(0.25f*static_cast<float>(y - mLastMousePos.y));

        // 마우스 입력에 기초해 각도를 갱신한다. 카메라가 상자를 중심으로 공전한다.
        mTheta += dx;
        mPhi += dy;

        // mPhi 각도를 제한한다.
        mPhi = MathHelper::Clamp(mPhi, 0.1f, MathHelper::Pi - 0.1f);
    }
    else if((btnState & MK_RBUTTON) != 0)
    {
        // 마우스 한 픽셀 이동을 장면의 0.005단위에 대응시킨다.
        float dx = 0.005f*static_cast<float>(x - mLastMousePos.x);
        float dy = 0.005f*static_cast<float>(y - mLastMousePos.y);

        // 마우스 입력에 기초해서 카메라 반지름을 갱신한다.
        mRadius += dx - dy;

        // 반지름을 제한한다.
        mRadius = MathHelper::Clamp(mRadius, 3.0f, 15.0f);
    }

    mLastMousePos.x = x;
    mLastMousePos.y = y;
}

void BoxApp::BuildDescriptorHeaps()
{
	// 루트 매개변수에 쓰기 위한 서술자 힙을 만든다.
    D3D12_DESCRIPTOR_HEAP_DESC cbvHeapDesc;
    cbvHeapDesc.NumDescriptors = 1;
    cbvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
    cbvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
	cbvHeapDesc.NodeMask = 0;
    ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc,
        IID_PPV_ARGS(&mCbvHeap)));
}

void BoxApp::BuildConstantBuffers()
{
	// 상수버퍼를 만든다.
	mObjectCB = std::make_unique<UploadBuffer<ObjectConstants>>(md3dDevice.Get(), 1, true);

	UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));

	D3D12_GPU_VIRTUAL_ADDRESS cbAddress = mObjectCB->Resource()->GetGPUVirtualAddress();
    
	// 버퍼에서 i번째 물체의 상수 버퍼의 오프셋을 얻는다.
	// 지금은 0
    int boxCBufIndex = 0;
	cbAddress += boxCBufIndex * objCBByteSize;

	D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
	cbvDesc.BufferLocation = cbAddress;
	cbvDesc.SizeInBytes = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));

	md3dDevice->CreateConstantBufferView(
		&cbvDesc,
		mCbvHeap->GetCPUDescriptorHandleForHeapStart());
}

void BoxApp::BuildRootSignature()
{
	// 일반적으로 쉐이더 프로그램은 특정 자원들이 입력된다고 기대한다.
	// 루트 시그니처는 쉐이더 프로그램이 기대하는 자원들을 정의한다.
	// 쉐이더 프로그램은 본질적으로 하나의 함수이고 쉐이더에 입력되는 자원들은
	// 함수의 매개변수들에 해당하므로, 루트 시그니처는 곧 함수 시그니처를 정의하는 수단이라 할 수 있다.

	// 루트 매개변수는 서술자 테이블이거나 루트 서술자 또는 루트 상수.
	CD3DX12_ROOT_PARAMETER slotRootParameter[1];

	// CBV 하나를 담는 서술자 테이블 생성.
	CD3DX12_DESCRIPTOR_RANGE cbvTable;
	cbvTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
	slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable);

	// 루트 시그니처는 루트 매개변수들의 배열.
	CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParameter, 0, nullptr, 
		D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT);

	// 상수 버퍼 하나로 구성된 서술자 구간을 가리키는 
	// 슬롯 하나로 이루어진 루트 시그니처를 생성한다.
	ComPtr<ID3DBlob> serializedRootSig = nullptr;
	ComPtr<ID3DBlob> errorBlob = nullptr;
	HRESULT hr = D3D12SerializeRootSignature(&rootSigDesc, D3D_ROOT_SIGNATURE_VERSION_1,
		serializedRootSig.GetAddressOf(), errorBlob.GetAddressOf());

	if(errorBlob != nullptr)
	{
		::OutputDebugStringA((char*)errorBlob->GetBufferPointer());
	}
	ThrowIfFailed(hr);

	ThrowIfFailed(md3dDevice->CreateRootSignature(
		0,
		serializedRootSig->GetBufferPointer(),
		serializedRootSig->GetBufferSize(),
		IID_PPV_ARGS(&mRootSignature)));
}

void BoxApp::BuildShadersAndInputLayout()
{
	// 쉐이더 프로그램을 빌드하고, 입력 레이아웃에 대한 세부 요소들이 무엇인지 설정한다.
    HRESULT hr = S_OK;
    
	mvsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "VS", "vs_5_0");
	mpsByteCode = d3dUtil::CompileShader(L"Shaders\\color.hlsl", nullptr, "PS", "ps_5_0");

    mInputLayout =
    {
        { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
        { "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
    };
}

void BoxApp::BuildBoxGeometry()
{
	// 박스 정점 정보 설정
    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) })
    };

	std::array<std::uint16_t, 36> indices =
	{
		// front face
		0, 1, 2,
		0, 2, 3,

		// back face
		4, 6, 5,
		4, 7, 6,

		// left face
		4, 5, 1,
		4, 1, 0,

		// right face
		3, 2, 6,
		3, 6, 7,

		// top face
		1, 5, 6,
		1, 6, 2,

		// bottom face
		4, 0, 3,
		4, 3, 7
	};

    const UINT vbByteSize = (UINT)vertices.size() * sizeof(Vertex);
	const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t);

	mBoxGeo = std::make_unique<MeshGeometry>();
	mBoxGeo->Name = "boxGeo";

	ThrowIfFailed(D3DCreateBlob(vbByteSize, &mBoxGeo->VertexBufferCPU));
	CopyMemory(mBoxGeo->VertexBufferCPU->GetBufferPointer(), vertices.data(), vbByteSize);

	ThrowIfFailed(D3DCreateBlob(ibByteSize, &mBoxGeo->IndexBufferCPU));
	CopyMemory(mBoxGeo->IndexBufferCPU->GetBufferPointer(), indices.data(), ibByteSize);

	mBoxGeo->VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
		mCommandList.Get(), vertices.data(), vbByteSize, mBoxGeo->VertexBufferUploader);

	mBoxGeo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
		mCommandList.Get(), indices.data(), ibByteSize, mBoxGeo->IndexBufferUploader);

	mBoxGeo->VertexByteStride = sizeof(Vertex);
	mBoxGeo->VertexBufferByteSize = vbByteSize;
	mBoxGeo->IndexFormat = DXGI_FORMAT_R16_UINT;
	mBoxGeo->IndexBufferByteSize = ibByteSize;

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

	mBoxGeo->DrawArgs["box"] = submesh;
}

void BoxApp::BuildPSO()
{
	// 파이프라인 상태를 생성한다.
    D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc;
    ZeroMemory(&psoDesc, sizeof(D3D12_GRAPHICS_PIPELINE_STATE_DESC));
    psoDesc.InputLayout = { mInputLayout.data(), (UINT)mInputLayout.size() };
    psoDesc.pRootSignature = mRootSignature.Get();
    psoDesc.VS = 
	{ 
		reinterpret_cast<BYTE*>(mvsByteCode->GetBufferPointer()), 
		mvsByteCode->GetBufferSize() 
	};
    psoDesc.PS = 
	{ 
		reinterpret_cast<BYTE*>(mpsByteCode->GetBufferPointer()), 
		mpsByteCode->GetBufferSize() 
	};
    psoDesc.RasterizerState = CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT);
    psoDesc.BlendState = CD3DX12_BLEND_DESC(D3D12_DEFAULT);
    psoDesc.DepthStencilState = CD3DX12_DEPTH_STENCIL_DESC(D3D12_DEFAULT);
    psoDesc.SampleMask = UINT_MAX;
    psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
    psoDesc.NumRenderTargets = 1;
    psoDesc.RTVFormats[0] = mBackBufferFormat;
    psoDesc.SampleDesc.Count = m4xMsaaState ? 4 : 1;
    psoDesc.SampleDesc.Quality = m4xMsaaState ? (m4xMsaaQuality - 1) : 0;
    psoDesc.DSVFormat = mDepthStencilFormat;
    ThrowIfFailed(md3dDevice->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&mPSO)));
}

 

출력 결과

 

반응형
Comments