일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- TCP/IP
- DirectX
- C++
- 조명 처리
- DirectX12
- FrameResource
- 게임 프로그래밍
- 노멀 맵핑
- 입방체 매핑
- 네트워크
- Render Target
- gitlab
- Dynamic Indexing
- Direct3D12
- effective C++
- 게임 클래스
- 장치 초기화
- 디퍼드 렌더링
- Frustum Culling
- Deferred Rendering
- InputManager
- 큐브 매핑
- gitscm
- light
- 게임 디자인 패턴
- 직교 투영
- 네트워크 게임 프로그래밍
- 동적 색인화
- direct3d
- 절두체 컬링
- Today
- Total
코승호딩의 메모장
[Direct3D 렌더링] 본문
01 Vertex와 InputLayout
Direct3D에서 정점은 공간 정보 이외의 추가 정보를 부여할 수 있다. 원하는 정보를 가진 정점을 만들기 위해서는 자료를 담을 구조체를 정의해야 한다. 정점 구조체를 정의 하였다면 정점의 각 성분들이 무엇을 해야 하는지 Direct3D에 알려주어야 하는데 이 수단이 바로 InputLayout이다.
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 },
};
다음와 같이 각 정점이 정점 셰이더의 어떤 변수와 대응되는지, 포맷 형식이 무엇인지, 어느 슬롯을 사용할지(총 16개의 슬롯 사용 가능), 오프셋, 인스턴싱을 사용할지를 정할 수 있다. InputLayout을 결정 했으면 PSO에 설정해주면 된다.
이제 응용 프로그램에서 작성한 정점 정보를 GPU가 읽어야 하는데 GPU가 정점들의 배열에 접근하기 위해서는 정점들을 GPU 자원인 정점 버퍼에 넣어야 한다. 이와 같이 응용 프로그램이 GPU에 정점과 같은 정보를 제공하기 위해서는 항상 버퍼를 사용해야 한다.
정점 버퍼 생성하기
정점 버퍼를 생성하기 위해서는 버퍼 자원을 서술하는 D3D12_RESOURCE_DESC을 채우고 CreateCommittedResource 메서드를 호출해 ID3D12Resource 객체를 생성해야 한다. D3D12_RESOURCE_DESC 구조체 인스턴스는 CD3DX12_RESOURCE_DESC 래퍼 클래스를 사용하면 쉽게 생성할 수 있는데 인자로 들어가는 width는 길이가 아니라 버퍼의 바이트 개수이다. 만약 float 64개를 버퍼에 담는다면 64 * sizeof(float)을 넣으면 된다.
정점 정보와 같이 정적인 기하구조 즉, 프레임마다 변하지 않는 기하구조들은 최적의 성능을 위해 기본 힙에 담아야 한다. 그러나 CPU는 기본 힙에 접근하지 못하므로 임시 업로드용 버퍼를 생성하여 시스템 메모리의 정점 정보를 업로드 버퍼에 복사하여 업로드 버퍼를 실제 정점 버퍼로 복사해야 한다.
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())));
// 임시 업로드용 버퍼
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;
// 기본 버퍼 자원으로 자료 복사
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));
return defaultBuffer;
}
한 가지 주의할 점은 업로드 버퍼를 계속 유지해야 한다는 것이다. 실제 복사를 수행하는 명령 목록을 GPU가 아직 실행하지 않았기 때문이다. 복사가 완료되었음이 확실해지고 해제해야 한다. 위 코드에서 UpdateSubresources 함수는 CPU 메모리를 임시 업로드 힙에 복사하여 정점 버퍼에 복사한다.
정점 버퍼를 파이프라인에 묶기 위해서는 정점 버퍼 뷰를 만들어야 하지만, RTV와 다르게 서술자 힙이 필요하지는 않다. 정점 버퍼 뷰는 D3D12_VERTEX_BUFFER_VIEW 구조체이다. 매개 변수로 첫 인자에 정점 버퍼의 가상 주소가 필요한데 정점 버퍼는 ID3D12Resource로 되어 있으므로 GetGpuVirtualAddress 함수를 사용하면 된다.
정점 버퍼 뷰까지 생성하였다면 IASetVertexBuffers 함수를 사용하면 PSO에 묶인다. 이 함수에서 중요한 점은 마지막 세 번째 인자인데 정점 버퍼 뷰 배열의 첫 원소를 가리키는 포인터이다. 즉 D3D12_VERTEX_BUFFER_VIEW 형식을 전달하면 되는데 이 함수가 중요한 이유는 여러 도형의 정점을 하나의 정점 버퍼로 합칠 수 있기 때문이다. 그렇다면 적절히 정점 버퍼 뷰 배열의 첫 원소를 가져와 설정 해주면 된다.
void DrawInstanced(
UINT VertexCountPerInstance, // 그릴 정점들의 개수
UINT InstanceCount, // 그릴 인스턴스의 개수(인스턴싱을 하지 않는다면 1)
UINT StartVertexLocation, // 첫 정점의 색인
UINT StartInstanceLocation // 인스턴싱을 하지 않는다면 0
);
마지막으로 정점 버퍼가 모두 셋 되었기 때문에 드로우를 하면 되는데 DrawInstanced 함수를 사용한다.
색인 버퍼 생성하기
정점들과 마찬가지로, 색인들 또한 GPU 자원에 넣어야 한다. 색인 버퍼 또한 CreateDefaultBuffer 함수를 통해 생성할 수 있으며 색인 버퍼 뷰를 만들어야 한다. 색인 버퍼 뷰는 D3D12_INDEX_BUFFER_VIEW 구조체이다. 마찬가지로 버퍼의 주소를 얻고 버퍼의 크기를 넣은 후 포맷 형식을 넣어야 한다. 포맷 형식은 메모리와 대역폭을 절약하기 위해선 16바이트를 사용해야 한다. 아닌 경우만 32비트를 사용한다. 이후 IASetIndexBuffer 함수를 사용하여 PSO에 묶는다. 이 함수는 색인 버퍼의 가상 주소 한 개만 인자로 받는다.
void DrawIndexedInstanced(
UINT IndexCountPerInstance, // 색인의 개수
UINT InstanceCount, // 인스턴스 개수
UINT StartIndexLocation, // 첫 색인의 색인
INT BaseVertexLocation, // 색인들에 더할 정수 값
UINT StartInstanceLocation // 인스턴싱 안하면 0
);
마지막으로 색인 버퍼가 모두 셋 되었기 때문에 드로우를 하면 되는데 DrawIndexedInstanced 함수를 사용한다. 다음 정점 버퍼 합치기에서 색인 버퍼의 인자 설정하는 방법을 자세히 보도록 한다.
정점 버퍼 합치기
만약 장면에 구, 상자, 원기둥이 있다고 하자. 세 물체는 각자 개별적인 정점 버퍼와 색인이 있다. 이 물체들의 정점 버퍼와 색인 버퍼를 하나로 합치게 되면 버퍼 전환에 따른 추가 API 비용을 피할 수 있다. 만약 작은 정점 버퍼들과 색인 버퍼들이 많다면 버퍼들을 합치는 것이 성능에 도움이 될 것이다. 버퍼들을 합치게 되면 당연히 색인들이 잘못된 정점을 가리키게 될 것이다. 따라서 정점 버퍼에 맞게 색인들을 재설정 해야 한다.
위 그림과 같이 상자의 정점들은 구 정점 뒤에 추가가 되었지만 색인의 값들은 그대로 0~EndBoxIndex를 가리킬 것이다. 따라서 상자 색인들의 첫 색인은 앞의 구의 색인의 개수일 것이고 정점의 첫 위치는 앞의 구의 정점의 개수일 것이다.
DrawIndexedInstanced(numSphereIndices, 1, 0, 0, 0);
DrawIndexedInstanced(numBoxIndices, 1, firstBoxIndex, firstBoxVertexPos, 0);
DrawIndexedInstanced(numCylIndices, 1, firstCylIndex, firstCylVertexPos, 0);
02 정점 셰이더 예제
셰이더는 HLSL이라고 하는 언어로 작성한다. 본질적으로 정점 셰이더는 하나의 함수이다. 또한 HLSL에서는 참조나 포인터가 없으며 함수가 여러 개의 값을 돌려주려면 구조체를 사용하거나 out이 지정된 출력 매개변수를 사용해야 한다.
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
vout.PosH = mul(posW, gViewProj);
vout.Color = vin.Color;
return vout;
}
위 코드는 간단한 정점 셰이더 예제이다. VertexIn의 시멘틱 값 :POSITION과 :COLOR는 VS의 입력 매개변수와 정점 구조체의 멤버들을 대응시킨다. VertexOut에도 시멘틱 값 :SV_POSITION과 :COLOR가 있는데 SV_POSITION은 특별한 의미소이며 시스템 값 의미소를 뜻한다. 이 뜻은 출력 정점이 동차 절단 공간이라는 뜻을 담고 있다. 이를 지정하면 GPU에게 알려줌으로써 절단, 깊이 판정, 래스터화 등 다양한 특별 연산을 수행할 수 있다.
자세히 설명하자면 VS에서 출력되는 값은 테셀레이션 및 기하 쉐이더 등이 없다면 레스터라이저로 바로 넘어가게 되는데 레스터라이저에서는 절단이나 래스터화 등의 연산이 수행된다. 이를 수행하기 위해서 래스터라이저는 동차절단공간 즉, 월드 변환, 뷰 변환, 프로젝션 변환이 완료된 좌표의 값을 기다리고 있는 것이다. 따라서 VS의 출력을 SV_POSITION으로 설정한 것이다. 만약, 중간에 테셀레이션의 단계가 있다면 VS의 출력 값을 월드 변환만 하거나 로컬 좌표로 넘겨줄 수 있을 것이다.
주의할 점 한가지는 정점 자료와 InputLayout의 자료는 정확히 일치하지 않아도 된다. 또한 정점 셰이더가 사용하지 않는 추가적인 정점 정보를 제공하는 것은 오류가 아니다. 그러나 만약 정점 셰이더에서 사용하는 정보를 덜 주게 된다면 예를 들어 정점 셰이더에 VertexIn의 변수가 3개인데 InputLayout이 2개라면 위법은 아니지만 엄청난 경고 메시지를 보게 될 것이다.
03 픽셀 셰이더 예제
정점 셰이더에서 출력한 정점들은 래스터화 단계를 거쳐 삼각형의 픽셀들을 따라 보간되며 보간된 결과는 픽셀 셰이더의 입력으로 들어간다. 픽셀 셰이더 또한 하나의 함수이며 정점마다 출력되는 정점 셰이더와는 달리 픽셀 단편마다 출력된다.
struct VertexIn
{
float3 PosL : POSITION;
float4 Color : COLOR;
};
struct VertexOut
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
float4 posW = mul(float4(vin.PosL, 1.0f), gWorld);
vout.PosH = mul(posW, gViewProj);
vout.Color = vin.Color;
return vout;
}
float4 PS(VertexOut pin) : SV_Target
{
return pin.Color;
}
위 코드와 같이 정점 셰이더의 출력이 픽셀 셰이더의 입력과 정확히 동일하다. 또한 픽셀 셰이더의 출력 값은 float4 즉 색상 값이라는 것을 알 수 있다. 그리고 픽셀 셰이더에 붙은 SV_Target 의미소는 픽셀 셰이더의 반환 값이 렌더 대상(render target)의 형식과 일치해야 함을 뜻한다.
04 상수 버퍼
개인적으로 Direct3D에서 가장 중요하다고 생각되는 부분이다. 상수 버퍼는 셰이더에서 참조하는 자료를 담는 GPU 자원이다.
cbuffer cbPerObject : register(b0)
{
float4x4 gWorldViewProj;
}
위 코드는 cbPerObject라는 cbuffer 객체를 참조한다. 이 상수 버퍼는 월드, 뷰, 프로젝션 행렬을 하나로 결합한 것이다. 정점 버퍼와 색인 버퍼와는 달리 상수 버퍼는 프레임당 한 번 갱신하는 것이 일반적이다. 매 프레임 당 카메라가 움직인다면 상수 버퍼를 새 시야 행렬로 갱신해야 한다. 따라서 상수 버퍼는 기본 힙이 아닌 업로드 힙에 만들어야 한다. 또한 상수 버퍼는 256바이트의 배수이어야 한다. 만약 장면에 객체가 n개가 있다면, 상수 버퍼 또한 n개가 있어야 할 것이다.
struct ObjectConstants
{
DirectX::XMFLOAT4X4 World = MathHelper::Identity4x4();
};
mElementByteSize = d3dUtil::CalcConstantBufferByteSize(ObjectConstants);
ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*elementCount),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mUploadBuffer)));
위 코드는 n개의 상수 버퍼 객체를 담는 하나의 버퍼를 생성하는 코드이다. mUploadBuffer를 ObjectConstants 형식의 상수 버퍼들의 배열을 담는 버퍼라고 간주할 수 있다. 어떠한 물체를 그릴 때, 해당 물체를 위한 상수들이 있는 영역을 서술하는 상수 버퍼 뷰를 파이프라인에 묶어야 한다.
static UINT CalcConstantBufferByteSize(UINT byteSize)
{
return (byteSize + 255) & ~255;
}
위 코드를 통해 상수 버퍼의 크기를 256의 배수로 맞출 수 있는데 간단하게 계산기를 통해 확인할 수 있을 것이다. 하위 1바이트는 모두 밀고 다음 상위 바이트를 &연산을 하기 때문에 256의 배수가 된다.
struct ObjectConstants
{
float4x4 gWorld;
};
struct PassConstants
{
float4x4 gView;
float4x4 gInvView;
float4x4 gProj;
float4x4 gInvProj;
float4x4 gViewProj;
float4x4 gInvViewProj;
float3 gEyePosW;
float cbPerObjectPad1;
float2 gRenderTargetSize;
float2 gInvRenderTargetSize;
float gNearZ;
float gFarZ;
float gTotalTime;
float gDeltaTime;
};
ConstantBuffer<ObjectConstants> gObjectCostants : register(b0);
ConstantBuffer<PassConstants> gPassConstants : register(b1);
셰이더 모델 5.1에서는 상수 버퍼를 정의하는 또 다른 문법을 지원한다. 다음과 같이 상수 버퍼에 담을 자료를 개별적인 구조체로 정의하고 그 구조체를 이용해서 상수 버퍼를 정의할 수 있다.
BYTE* mMappedData = nullptr;
mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));
상수 버퍼를 업로드 힙에 생성하였기 때문에 CPU에서 상수 버퍼 자원에 자료를 올릴 수 있다. 자료를 올리기 위해서는 포인터를 얻어야 하는데 Map 함수를 통해 얻을 수 있다. 이후 응용 프로그램에서 mMappedData에 자료를 복사하면 상수 버퍼에 정보가 올라가게 된다. 매개 변수에는 자원 전체를 대응시키기 위해 nullptr을 사용하였다. mMappedData에 시스템 메모리에 있는 자료를 복사하기 위해서는 memcpy를 사용하면 된다. 상수 버퍼 복사가 끝났다면 해당 메모리를 해제하기 전 Unmap을 호출해야 한다. 마찬가지로 nullptr로 자원 전체에 대응시킨다.
class UploadBuffer
{
public:
UploadBuffer(ID3D12Device* device, UINT elementCount, bool isConstantBuffer) :
mIsConstantBuffer(isConstantBuffer)
{
mElementByteSize = sizeof(T);
if(isConstantBuffer)
mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T));
ThrowIfFailed(device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize*elementCount),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&mUploadBuffer)));
ThrowIfFailed(mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData)));
}
UploadBuffer(const UploadBuffer& rhs) = delete;
UploadBuffer& operator=(const UploadBuffer& rhs) = delete;
~UploadBuffer()
{
if(mUploadBuffer != nullptr)
mUploadBuffer->Unmap(0, nullptr);
mMappedData = nullptr;
}
ID3D12Resource* Resource()const
{
return mUploadBuffer.Get();
}
void CopyData(int elementIndex, const T& data)
{
memcpy(&mMappedData[elementIndex*mElementByteSize], &data, sizeof(T));
}
private:
Microsoft::WRL::ComPtr<ID3D12Resource> mUploadBuffer;
BYTE* mMappedData = nullptr;
UINT mElementByteSize = 0;
bool mIsConstantBuffer = false;
};
위 코드의 UploadBuffer 클래스는 업로드 버퍼를 손쉽게 다룰 수 있게 한다. 생성자에서 상수 자료의 사이즈로 256배수에 맞게 업로드 버퍼를 생성하며 GPU 자원의 포인터를 mMappedData 변수에 저장한다. 소멸자에서 자동으로 Unmap을 해주고 CopyData를 통해서 각 물체에 사용되는 상수를 개별적으로 GPU 자원에 올린다.
상수 버퍼 서술자
자원을 렌더링 파이프라인에 묶기 위해 서술자가 필요하다. 상수 버퍼 서술자는 DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV 형식의 서술자 힙에 담긴다. 이 힙은 CBV, 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;
ThrowIfFailed(md3dDevice->CreateDescriptorHeap(&cbvHeapDesc, IID_PPV_ARGS(&mSrvDescriptorHeap)));
서술자 힙을 생성했다면 상수 버퍼 뷰를 만들어야 한다. 상수 버퍼 뷰는 CreateConstantBufferView로 생성할 수 있으며 적절하게 핸들을 받아오고 DESC를 채워 넣으면 된다.
루트 서명과 서술자 테이블
자원들은 특정 레지스터 슬롯에 묶이며 셰이더는 이 슬롯들을 통해 자원에 접근한다.
ConstantBuffer<ObjectConstants> gObjectCostants : register(b0);
ConstantBuffer<PassConstants> gPassConstants : register(b1);
다음과 같이 gObjectConstants는 b0 슬롯에 묶였으며 gPassConstants는 b1 슬롯에 묶인 것이다. 여기서 루트 서명이란 그리기 호출 전, 응용 프로그램이 반드시 렌더링 파이프라인에 묶어야 하는 자원들이 무엇이고 레지스터 어디에 대응되는지를 정의한다. 루트 서명은 자원들을 서술하는 루트 매개변수들의 배열로 정의된다. 루트 매개변수는 루트 상수, 루트 서술자, 서술자 테이블이 될 수 있다. 다음 코드는 서술자 테이블 하나로 된 루트 서명을 생성한다.
CD3DX12_DESCRIPTOR_RANGE cbvTable;
cbvTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_CBV, 1, 0);
CD3DX12_ROOT_PARAMETER slotRootParam[1];
slotRootParameter[0].InitAsDescriptorTable(1, &cbvTable);
CD3DX12_ROOT_SIGNATURE_DESC rootSigDesc(1, slotRootParam, 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.GetAddressOf())));
위 코드는 b0 레지스터에 CBV 하나를 담은 서술자 테이블이 묶인다. 이후 파이프라인에 서술자 테이블을 묶어야 한다.
cmdList->SetGraphicsRootDescriptorTable(0, cbvHandle);
나머지 루트 서명과 서술자 힙 또한 Set을 해주면 된다.
한 가지 꿀팁은 CPU 핸들은 생성할 때, GPU 핸들은 바인딩할 때 주로 사용된다.
05 래스터라이저
래스터화기 상태는 직접 프로그래밍하지 않는 단계이다. 일부 구성만 설정하면 된다. D3D12_RASTERIZER_DESC 구조체를 채우면 된다.
typedef struct D3D12_RASTERIZER_DESC {
D3D12_FILL_MODE FillMode;
D3D12_CULL_MODE CullMode;
BOOL FrontCounterClockwise;
INT DepthBias;
FLOAT DepthBiasClamp;
FLOAT SlopeScaledDepthBias;
BOOL DepthClipEnable;
BOOL MultisampleEnable;
BOOL AntialiasedLineEnable;
UINT ForcedSampleCount;
D3D12_CONSERVATIVE_RASTERIZATION_MODE ConservativeRaster;
} D3D12_RASTERIZER_DESC;
다음과 같이 래스터라이저에서 와이어, 솔리드 프레임을 정할 수 있으며 컬링 모드를 지정할 수 있다. CD3DX12_RASTERIZER_DESC(D3D12_DEFAULT) 편의용 클래스를 사용하여 기본값으로 초기화할 수 있다.
06 파이프라인 상태 객체(PSO)
이제까지 PSO에 묶기 위한 객체들을 생성해줬으니 다음으로 묶는 방법을 알아보도록 한다. PSO는 ID3D12PipelineState 인터페이스에 해당하며 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;
D3D12_BLEND_DESC BlendState;
UINT SampleMask;
D3D12_RASTERIZER_DESC RasterizerState;
D3D12_DEPTH_STENCIL_DESC DepthStencilState;
D3D12_INPUT_LAYOUT_DESC InputLayout;
D3D12_INDEX_BUFFER_STRIP_CUT_VALUE IBStripCutValue;
D3D12_PRIMITIVE_TOPOLOGY_TYPE PrimitiveTopologyType;
UINT NumRenderTargets;
DXGI_FORMAT RTVFormats[8];
DXGI_FORMAT DSVFormat;
DXGI_SAMPLE_DESC SampleDesc;
UINT NodeMask;
D3D12_CACHED_PIPELINE_STATE CachedPSO;
D3D12_PIPELINE_STATE_FLAGS Flags;
} D3D12_GRAPHICS_PIPELINE_STATE_DESC;
위 코드를 보면 드디어 아래 그림이 무엇인지 알게 될 것이다. 이렇게 PSO에 모든 상태들을 Set을 해주는 것이다. 물론 PSO에 바로 Set해야 하는 것이 있는가 하면 NON_PSO 예를 들어 IASetPrimitiveTopology처럼 PSO에 Set을 바로 하지 않는 경우도 있다. 이 모든 객체를 하나의 집합체로 렌더링 파이프라인에 지정하여 성능을 높일 수 있다. 또한 모든 상태가 호환되는지 미리 검사할 수 있다. 아마 이 부분이 Direct3D 12의 가장 큰 차이점 아닐까 싶다. 생성을 완료했다면 SetPipelineState 함수를 통해 PSO를 사용할 수 있다.
중요한 점은 성능을 위해 PSO 상태 변경은 최소화 해야 한다는 점이다. 같은 PSO를 사용할 수 있는 물체들은 함께 그려야 한다. 그리기 호출마다 PSO를 변경하지 말아야 한다.
07 기하구조 보조 구조체
다음 코드는 정점 버퍼와 색인 버퍼를 합칠 수 있는 편리한 보조 구조체이다. 이 구조체는 실제 정점 자료와 색인 자료를 시스템 메모리에 유지해서 CPU가 그 자료를 언제라도 읽을 수 있게 한다. 픽킹이나 충돌 검출을 위해서는 CPU가 기하구조 자료에 접근해야 하기 때문이다.
struct SubmeshGeometry
{
UINT IndexCount = 0;
UINT StartIndexLocation = 0;
INT BaseVertexLocation = 0;
DirectX::BoundingBox Bounds;
};
이 구조체는 기하구조 그룹의 부분을 정의한다. 부분 메시들은 하나의 정점 색인 버퍼에 여러 개의 기하구조가 들어 있는 경우에 사용된다. 앞서 설명했듯이 작은 자료를 합쳐 사용할 때 유용하다.
struct MeshGeometry
{
// 메시를 이름으로 조회할 수 있도록 이름을 부여한다.
std::string Name;
// 시스템 메모리 복사본
Microsoft::WRL::ComPtr<ID3DBlob> VertexBufferCPU = nullptr;
Microsoft::WRL::ComPtr<ID3DBlob> IndexBufferCPU = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferGPU = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferGPU = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> VertexBufferUploader = nullptr;
Microsoft::WRL::ComPtr<ID3D12Resource> IndexBufferUploader = nullptr;
// 버퍼들에 관한 자료
UINT VertexByteStride = 0;
UINT VertexBufferByteSize = 0;
DXGI_FORMAT IndexFormat = DXGI_FORMAT_R16_UINT;
UINT IndexBufferByteSize = 0;
// 하나의 MeshGeometry에 여러 개의 기하구조를 담을 수 있다.
std::unordered_map<std::string, SubmeshGeometry> DrawArgs;
D3D12_VERTEX_BUFFER_VIEW VertexBufferView()const
{
D3D12_VERTEX_BUFFER_VIEW vbv;
vbv.BufferLocation = VertexBufferGPU->GetGPUVirtualAddress();
vbv.StrideInBytes = VertexByteStride;
vbv.SizeInBytes = VertexBufferByteSize;
return vbv;
}
D3D12_INDEX_BUFFER_VIEW IndexBufferView()const
{
D3D12_INDEX_BUFFER_VIEW ibv;
ibv.BufferLocation = IndexBufferGPU->GetGPUVirtualAddress();
ibv.Format = IndexFormat;
ibv.SizeInBytes = IndexBufferByteSize;
return ibv;
}
// GPU에 모든 자료를 올린 후 해제한다.
void DisposeUploaders()
{
VertexBufferUploader = nullptr;
IndexBufferUploader = nullptr;
}
};
추가적으로 하나의 물체를 그리는데 정점 버퍼 두 개(입력 슬롯 두 개)를 사용하여 파이프라인에 정점을 공급하는 방법이 있는데 예를 들어, 정점 정보로 Pos과 Color를 넘겨 준다고 할 때, 각각 다른 슬롯에 넘겨 주는 방법이다.
// hlsl
struct VertexIn
{
float3 PosL : SV_Position;
float4 Color : COLOR;
}
// inputLayout
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 },
};
위 코드는 기존 방법대로 정점 버퍼에 Pos과 Color를 함께 넣어 준 것이다. 이 코드를 각각 분리하여 정점 버퍼에 넘겨줄 수 있다.
// hlsl
struct VertexInPos
{
float3 PosL : SV_Position;
}
struct VertexInColor
{
float4 Color : COLOR;
}
// inputLayout
mInputLayout =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
이와 같이 각각 다른 슬롯에 정점의 자료를 분리하여 담아줄 수 있는데 이를 일종의 최적화로 사용할 수 있다. 예를 들어 그림자 매핑 알고리즘에서 프레임마다 장면을 두 번 그려야 한다. 한 번은 광원에서, 한 번은 카메라에서 그려야 하는데 광원의 관점에서 그릴 때는 정점의 위치와 텍스처 좌표만 있으면 된다. 따라서 정점 자료를 위치와 텍스처 좌표를 담는 입력 슬롯 하나와 그 밖의 정점 특성들을 담는 입력 슬롯으로 분할해 두면, 그림자 패스에서는 그 패스에 필요한 정점들만 사용하면 되므로 자료의 대역폭을 절약할 수 있다. 주 렌더링 패스에서는 모든 슬롯을 사용해서 정점 자료를 공급한다. 다만 성능을 위해서는 입력 슬롯 개수를 3개 이하로 최소화해야 한다.
'DirectX12 > DirectX12 입문' 카테고리의 다른 글
[Frame Resource와 Render Item] (0) | 2023.09.19 |
---|---|
[시간 측정] (0) | 2023.09.14 |
[전체 화면 모드 전환] (0) | 2023.09.11 |
[Direct3D 개요와 초기화] (0) | 2023.09.11 |