일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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
- 장치 초기화
- effective C++
- Dynamic Indexing
- 직교 투영
- 디퍼드 렌더링
- 노멀 맵핑
- 조명 처리
- FrameResource
- C++
- 게임 디자인 패턴
- 큐브 매핑
- DirectX
- 절두체 컬링
- 네트워크 게임 프로그래밍
- light
- Render Target
- 게임 클래스
- 네트워크
- DirectX12
- 동적 색인화
- direct3d
- InputManager
- Frustum Culling
- Direct3D12
- 게임 프로그래밍
- Deferred Rendering
- gitlab
- gitscm
- Today
- Total
코승호딩의 메모장
[Dynamic Indexing] 본문
이번 글에서는 Dynamic Indexing(동적 색인화)를 구현하기로 한다. 동적 색인화란 쉐이더 프로그램 안에서 자원 배열을 동적으로 색인화하는 것이다. 여기서 말하는 자원 배열은 텍스처 배열이다. 이 배열의 색인화에 사용할 수 있는 색인들은 다양한데 우선 상수 버퍼의 요소를 색인으로 지정할 수 있다. 그리고 시스템 밸류 값인 SV_PrimitiveID, SV_VertexID, SV_DispatchID, SV_InstanceID 등을 색인으로 사용할 수 있다. 또한 셰이더 안에서 계산한 결과를 색인으로 사용할 수 있으며 텍스처에서 추출한 값을 색인으로 사용할 수 있고 정점 구조체의 한 성분을 색인으로 사용할 수도 있다. 이렇게 자원 배열을 색인화한다면 다양하게 구현이 가능하다.
동적 색인화 개요
cbuffer cbPerDrawIndex : register(b0)
{
int gDiffuseIndex;
};
Texture2D gDiffuseMap[4] : register(t0);
float diffuseMap = gDiffuseMap[gDiffuseIndex].Sample(gSamLinearWrap, pin.uv);
이런 방식으로 텍스처 배열을 선언하고 텍스처의 표본을 추출할 때 상수 버퍼로 받은 색인 값을 색인으로 사용할 수 있다. 이렇게 구현한다면 모든 텍스처를 프레임 당 한 번만 게임에서 사용할 모든 텍스처를 바인딩해도 될 것이다. 이번 구현 목표는 이 텍스처 색인 값을 머티리얼 정보에 추가하고 오브젝트 당 상수 버퍼에 머티리얼 또한 색인화하기 위해 머티리얼 색인 값을 넣어 주는 것이다.
이처럼 텍스처와 머티리얼을 동적으로 색인화한다면 장점이 몇 가지 있다.
- 서술자 개수를 최소화할 수 있어 루트 서명이 작아지며 그리기당 추가 부담이 최소화된다.
- 인스턴싱 기법에서 셰이더 안에서 자원 배열에 동적으로 색인을 통해 접근할 수 있어 유용하다.
- 오직 한 번의 프레임 당 셋을 하기 때문에 배치 처리에 좋다.
- 쉐이더 안에서 복수 개의 텍스처를 쉽고 간편하게 사용할 수 있다.
그렇다면 동적 색인화를 구현하기 위한 구체적인 방법을 알아보자.
- 구조적 버퍼에 모든 재질 자료를 저장한다. 즉, 상수 버퍼가 아닌 구조적 버퍼에 저장한다. 구조적 버퍼는 쉐이더 프로그램 안에서 색인화할 수 있기 때문이다. 이를 매 프레임 렌더링 파이프라인에 묶으면 셰이더에서 모든 재질 자료에 접근이 가능하다.
- 오브젝트 당 상수 버퍼에 해당 오브젝트에 해당하는 머티리얼 색인을 추가한다. 이를 이용해서 재질 구조적 버퍼를 색인화한다.
- 프레임마다 씬에 쓰이는 모든 텍스처 SRV 서술자를 렌더링 파이프라인에 묶는다.
- 머티리얼과 연관된 텍스처 맵을 식별할 수 있는 텍스처 인덱스를 머티리얼 정보에 추가한다. 이를 이용해서 텍스처 배열을 색인화한다.
동적 색인화 구현 - 머티리얼 구조적 버퍼
우선 기존에 머티리얼 당 상수 버퍼를 구조적 버퍼로 바꾸는 것부터 차근차근 시작해 보자.
template<typename T>
class UploadBuffer
{
public:
UploadBuffer(ComPtr<ID3D12Device> device, UINT elementCount, bool isConstantBuffer)
{
mElementByteSize = sizeof(T);
if (isConstantBuffer)
mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(T));
ThrowIfFailed(device->CreateCommittedResource(//...);
ThrowIfFailed(mUploadBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData)));
}
//...
};
참고로 ConstantBuffer를 상수 버퍼뿐만 아니라 구조적 버퍼로도 사용할 수 있게 하기 위해서 UploadBuffer로 변경한 후 생성자에서 상수 버퍼인지 여부를 인자로 받아서 상수 버퍼라면 버퍼의 크기를 256 배수의 사이즈로 만들어주고 만약 아니라면 즉, 업로드 힙으로만 사용할 버퍼라면 그냥 데이터 크기에 따라서 생성하도록 한다.
struct ObjectConstants
{
row_major matrix world;
uint materialIndex;
float3 padding;
};
struct MaterialData
{
//...
};
StructuredBuffer<MaterialData> gMaterialData : register(t0, space1);
기존에 MaterialConstants를 색인화를 위하여 이제는 상수 버퍼가 아닌 구조적 버퍼로 사용할 것이기 때문에 MaterialData로 변경하고 재질 구조적 버퍼를 선언한다. space1의 t0 레지스터를 사용할 것이기 때문에 space0의 t0 레지스터를 사용하는 텍스처와 겹치지 않는다. 그리고 재질 구조적 버퍼에 색인화할 수 있는 인덱스를 오브젝트 당 상수 버퍼에 추가한다.
// before
float4 uv = mul(float4(vin.uv, 0.f, 1.f), gMaterialConstants.matTransform);
// after
MaterialData matData = gMaterialData[gObjConstants.materialIndex];
float4 uv = mul(float4(vin.uv, 0.f, 1.f), matData.matTransform);
이제 쉐이더 코드 안에서 머티리얼의 상수 버퍼 정보를 바로 가져오는 것이 아닌 오브젝트 당 상수 버퍼에 있는 머티리얼 색인을 통해서 머티리얼 구조적 버퍼에 색인으로 접근하여 정보를 가져온다.
struct ObjectConstants
{
Matrix matWorld;
uint32 materialIndex;
Vec3 padding;
};
오브젝트 당 상수 버퍼의 내용이 바뀌었으니 당연히 CPU 쪽에서도 내용을 변경한 후에 정보를 memcpy 해야 할 것이다. 그전에 오브젝트 당 상수 버퍼의 머티리얼 인덱스를 설정해 보자.
class GameObject : public Object, public enable_shared_from_this<GameObject>
{
//...
void SetMatIndex(uint32 index) { _matIndex = index; }
uint32 GetMatIndex() const { return _matIndex; }
private:
//...
uint32 _matIndex = -1;
};
이제 게임 오브젝트 당 가지고 있어야 하는 머티리얼의 인덱스를 Set, Get 함수를 통해 설정해 주자.
void Transform::PushData()
{
ObjectConstants objectConstants = {};
objectConstants.matWorld = _matWorld;
objectConstants.materialIndex = GetGameObject()->GetMatIndex();
uint32 objCBIndex = GetGameObject()->GetObjCBIndex();
OBJECT_CB->CopyData(objCBIndex, objectConstants);
UINT objCBByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
D3D12_GPU_VIRTUAL_ADDRESS objCBAddress = OBJECT_CB->Resource()->GetGPUVirtualAddress() + objCBIndex * objCBByteSize;
CMD_LIST->SetGraphicsRootConstantBufferView(1, objCBAddress);
}
Transform 컴포넌트를 통해서 오브젝트 당 상수 버퍼의 내용을 GPU로 복사할 때 자신을 들고 있는 게임 오브젝트의 머티리얼 인덱스를 가져와서 오브젝트 당 상수 버퍼를 업데이트하여 상수 버퍼 뷰를 셋 한다.
CD3DX12_DESCRIPTOR_RANGE texTable;
texTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 1, 0);
CD3DX12_ROOT_PARAMETER slotRootParameter[4];
slotRootParameter[0].InitAsConstantBufferView(0);
slotRootParameter[1].InitAsConstantBufferView(1);
slotRootParameter[2].InitAsShaderResourceView(0, 1);
slotRootParameter[3].InitAsDescriptorTable(1, &texTable, D3D12_SHADER_VISIBILITY_PIXEL);
이제 머티리얼 정보를 상수 버퍼가 아닌 구조적 버퍼로 구현해야 하기 때문에 루트 서명에서 space1의 t0 레지스터를 사용할 수 있도록 루트 인자를 구현한다. 또한 루트 인자 마지막에는 텍스처를 사용할 것이기 때문에 서술자 테이블을 셋 한다. 참고로 루트 인자를 생성할 때, 자주 쓰이는 것은 가장 앞 배열에 두는 것이 좋다. 따라서 PassCB와 ObjectCB를 맨 앞에 둔다.
void CommandQueue::RenderBegin(const D3D12_VIEWPORT* vp, const D3D12_RECT* rect)
{
//...
auto passCB = CURR_FRAMERESOURCE->PassCB->Resource();
_cmdList->SetGraphicsRootConstantBufferView(0, passCB->GetGPUVirtualAddress());
auto matData = MATERIAL_CB->Resource();
_cmdList->SetGraphicsRootShaderResourceView(2, matData->GetGPUVirtualAddress());
}
마지막으로 렌더를 하기 전 매 프레임 당 한 번씩 머티리얼 SRV를 셋 한다. 참고로 현재 머티리얼 정보들은 MeshRenderer의 Render 함수에서 계속적으로 업데이트되는 상태인데 이때, 머티리얼은 계속 CopyData 함수를 통해 memcpy를 진행한다. 따라서 추후에 numDirty라는 변수를 프레임 리소스의 개수만큼 두고 업데이트가 꼭 필요한 머티리얼들 예를 들어 런타임에 머티리얼 변환이 바뀐다거나 하는 정보들만 업데이트를 할 수 있도록 한다. 만약 numDirty를 통해 런타임에 업데이트가 필요하지 않은 머티리얼이라는 것이 확인되었다면 한 번만 memcpy를 진행하면 계속 정보가 남아 있기 때문에 계속 업데이트할 필요가 없다.
동적 색인화 구현 - 텍스처 자원 배열
이제 텍스처를 자원 배열에 담을 수 있도록 엔진을 다시 설계해 보자.
struct MaterialData
{
float4 diffuseAlbedo;
float3 fresnelR0;
float roughness;
row_major float4x4 matTransform;
uint textureMapIndex;
uint normalMapIndex;
uint roughnessMapIndex;
uint padding;
};
TextureCube gCubeMap : register(t0);
Texture2D gTextureMaps[TEXTURE2D_COUNT] : register(t1);
StructuredBuffer<MaterialData> gMaterialData : register(t0, space1);
텍스처 배열을 색인화하기 위해 머티리얼 자료에 textureMapIndex 필드를 추가한다. 앞서 말했듯이 머티리얼과 텍스처들이 같은 레지스터를 사용하기는 하지만 space가 다르기 때문에 겹치지 않는다. 그러나 주의할 점은 Texture2D와 TextureCube 등 텍스처 형식이 다르다면 같은 배열에 담을 수 없기 때문에 이때는 레지스터를 나눠야 한다는 점이다. 현재 상태는 TextureCube는 Space0의 t0 레지스터만 사용하고 Texture2D는 Space0의 t1~TEXTURE2D_COUNT 까지 사용한다.
float4 PS_Main(VS_OUT pin) : SV_Target
{
MaterialData matData = gMaterialData[gObjConstants.materialIndex];
float4 diffuseAlbedo = matData.diffuseAlbedo;
float3 fresnelR0 = matData.fresnelR0;
float roughness = matData.roughness;
uint diffuseMapIndex = matData.textureMapIndex;
uint normalMapIndex = matData.normalMapIndex;
uint roughnessMapIndex = matData.roughnessMapIndex;
float4 normalMap = gTextureMaps[normalMapIndex].Sample(gsamAnisotropicWrap, pin.uv);
roughness *= gTextureMaps[roughnessMapIndex].Sample(gsamAnisotropicWrap, pin.uv).x;
diffuseAlbedo = gTextureMaps[diffuseMapIndex].Sample(gsamAnisotropicWrap, pin.uv) * diffuseAlbedo;
//...
}
이제 쉐이더 프로그램 안에서 머티리얼과 텍스처를 동적으로 색인화할 수 있게 되었다. 또한 구조적 버퍼를 사용하기 때문에 MaterialData가 구조체이다. 따라서 위와 같이 쉐이더 내에서 구조체를 선언한 다음 사용하기 쉽게 접근하도록 구현이 가능하다. 이제 모든 텍스처가 담겨 있는 gTextureMaps에 각 텍스처의 색인 값을 통해 접근하면 된다.
struct MaterialConstants
{
Vec4 DiffuseAlbedo = { 1.0f, 1.0f, 1.0f, 1.0f };
Vec3 FresnelR0 = { 0.01f, 0.01f, 0.01f };
float Roughness = 0.25f;
Matrix MatTransform = MathHelper::Identity4x4();
uint32 TextureMapIndex = -1;
uint32 NormalMapIndex = -1;
uint32 RoughnessMapIndex = -1;
uint32 padding;
};
머티리얼 정보가 바뀌었으니 당연히 CPU 쪽에서도 구조체의 정보가 달라져야 하고 이에 따라 Set, Get 함수를 정의한다.
void TableDescriptorHeap::CreateSRV(sptr<Texture> texture, TEXTURE_TYPE type)
{
auto tex = texture->Resource();
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = tex->GetDesc().Format;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = -1;
switch (type)
{
case TEXTURE_TYPE::TEXTURE2D:
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
break;
case TEXTURE_TYPE::TEXTURECUBE:
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
srvDesc.TextureCube.MipLevels = tex->GetDesc().MipLevels;
srvDesc.TextureCube.ResourceMinLODClamp = 0.f;
_skyTexHeapIndex = static_cast<uint8>(TEXTURECUBE_INDEX::SKYBOX);
break;
}
DEVICE->CreateShaderResourceView(tex.Get(), &srvDesc, hDescriptor);
hDescriptor.Offset(1, _cbvSrvDescriptorSize);
}
서술자 테이블의 서술자 힙에 모든 텍스처를 한 번에 생성하기 위해서 만든 함수이다. 텍스처와 타입을 입력받게 되면 타입에 따라 디스크립션을 생성하고 SRV를 생성하는데 이때 CPU 핸들을 Offset 하여 하나의 힙에 모든 서술자가 들어갈 수 있도록 한다. 참고로 TEXTURECUBE는 다른 레지스터 즉 다른 서술자 테이블을 사용할 것이기 때문에 따로 서술자 힙 내의 위치를 저장해 둔다.
CD3DX12_DESCRIPTOR_RANGE texCubeTable;
texCubeTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, TEXTURECUBE_COUNT, 0);
CD3DX12_DESCRIPTOR_RANGE tex2DTable;
tex2DTable.Init(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, TEXTURE2D_COUNT, 1);
CD3DX12_ROOT_PARAMETER slotRootParameter[5];
slotRootParameter[0].InitAsConstantBufferView(0);
slotRootParameter[1].InitAsConstantBufferView(1);
slotRootParameter[2].InitAsShaderResourceView(0, 1);
slotRootParameter[3].InitAsDescriptorTable(1, &texCubeTable, D3D12_SHADER_VISIBILITY_PIXEL);
slotRootParameter[4].InitAsDescriptorTable(1, &tex2DTable, D3D12_SHADER_VISIBILITY_PIXEL);
이제 루트 서명 또한 바뀌어야 한다. space0, t0 레지스터를 사용하는 TextureCube와 space0, t1~TEXTURE2D_COUNT 레지스터를 사용하는 Texture2D와 space1, t0 레지스터를 사용하는 머티리얼 정보가 있다.
void Scene::LoadTestTextures()
{
vector<string> texNames = {
"newjeans",
//...
};
vector<wstring> texFileNames = {
L"..\\Resources\\Texture\\newjeans.dds",
//...
};
// texture2D
for (int i = 0; i < TEXTURE2D_COUNT; ++i) {
auto texMap = std::make_shared<Texture>();
texMap->Init(texFileNames[i]);
DESCHEAP->CreateSRV(texMap);
_textures[texNames[i]] = move(texMap);
}
// textureCube
auto skyMap = std::make_shared<Texture>();
skyMap->Init(L"..\\Resources\\Texture\\Sky.dds");
DESCHEAP->CreateSRV(skyMap, TEXTURE_TYPE::TEXTURECUBE);
_textures["skybox"] = move(skyMap);
}
이제 모든 텍스처와 머티리얼을 미리 만들어서 맵에 있는 정보를 가져오는 방식이기 때문에 씬에서 먼저 만들어준다.
void Scene::BuildMaterials()
{
auto newjeans = make_shared<Material>();
newjeans->SetMatCBIndex(0);
newjeans->SetDiffuseSrvHeapIndex(TEXTURE2D_INDEX::B_NEWJEANS);
newjeans->SetFresnel(Vec3(0.9f, 0.9f, 0.9f));
newjeans->SetRoughness(0.125f);
shared_ptr<Shader> shader = make_shared<Shader>();
shader->Init(L"..\\Resources\\Shader\\Default.hlsl");
newjeans->SetShader(shader);
_materials["newjeans"] = move(newjeans);
//...
}
머티리얼 정보 또한 미리 씬에서 모두 만들어 주고 씬이 가지고 있는 머티리얼들의 정보에 저장해 둔다.
class Scene
{
public:
//...
unordered_map<string, sptr<Texture>>& GetTextures() { return _textures; }
unordered_map<string, sptr<Material>>& GetMaterials() { return _materials; }
void LoadTestTextures();
void BuildMaterials();
private:
//...
unordered_map<string, sptr<Texture>> _textures;
unordered_map<string, sptr<Material>> _materials;
};
shared_ptr<Scene> SceneManager::LoadTestScene()
{
scene->LoadTestTextures();
scene->BuildMaterials();
auto& textureMap = scene->GetTextures();
auto& materialMap = scene->GetMaterials();
//...
}
이제 씬의 머티리얼과 텍스처를 함수를 통해 생성하게 되면 씬이 가지고 있는 _textures와 _materials에 모든 텍스처와 머티리얼 정보가 저장될 것이다. 그리고 MeshRenderer는 SetMaterial을 할 때, 그저 맵을 통해 이름만 넣어서 가져올 수 있다.
CD3DX12_GPU_DESCRIPTOR_HANDLE skyTexDescriptor(DESCHEAP->GetDescriptorHeap()->GetGPUDescriptorHandleForHeapStart());
skyTexDescriptor.Offset(DESCHEAP->GetSkyTexHeapIndex(), DESCHEAP->GetCbvSrvDescriptorSize());
_cmdList->SetGraphicsRootDescriptorTable(3, skyTexDescriptor);
_cmdList->SetGraphicsRootDescriptorTable(4, DESCHEAP->GetDescriptorHeap()->GetGPUDescriptorHandleForHeapStart());
마지막으로 모든 텍스처를 한 프레임 당 한 번씩만 렌더링 파이프라인에 묶을 수 있도록 다음과 같이 커맨드 큐의 RenderBegin 함수에 작성해 준다. 우선 큐브 맵을 먼저 셋 하는데 이때, CreateSRV를 할 때 큐브 맵 텍스처의 SRV 위치를 저장해 뒀기 때문에 이 인덱스를 통해 서술자 힙을 오프셋 하여 테이블을 묶고 다음에 모든 텍스처들을 묶으면 된다.
enum class TEXTURE2D_INDEX : uint8
{
B_NEWJEANS,
B_NEWJEANS3,
B_LEATHER,
N_LEATHER,
R_LEATHER,
B_WALL,
N_WALL,
R_WALL,
END,
};
enum class TEXTURECUBE_INDEX : uint8
{
SKYBOX = static_cast<uint8>(TEXTURE2D_INDEX::END),
END,
};
enum
{
TEXTURE_COUNT = static_cast<uint8>(TEXTURECUBE_INDEX::END),
TEXTURE2D_COUNT = static_cast<uint8>(TEXTURE2D_INDEX::END),
TEXTURECUBE_COUNT = TEXTURE_COUNT - static_cast<uint8>(TEXTURE2D_INDEX::END),
};
추가적으로 다음과 같이 pch 파일 내에 이런 식으로 enum class를 통해서 모든 텍스처의 이름을 선언해 준다면 어디서든 텍스처 형식에 따라 개수를 나눠서 접근할 수 있기 때문에 편리하다.
이제 실행해 보면 이 전과 다른 점은 없지만 쉐이더 프로그램 안에서 굉장히 깔끔하고 편리하게 텍스처의 자원 배열에 접근할 수 있게 되었다. 속도도 더 빨라진 것 같은 것은 기분 탓일까. 참고로 위 코드는 간결하게 핵심만 보여준 것이고 나머지 머티리얼 개수 및 오브젝트 개수와 같은 변수를 받아 프레임 리소스를 생성할 때 생성자로 넣어주는 등의 구현은 따로 하도록 한다.
동적 색인화는 내가 가장 관심 있게 본 책의 주제 중 하나이다. (하나는 프레임 리소스일 것이다...) 이유는 구현도 어렵지 않은데 굉장히 쉽게 쉐이더 프로그램에서 색인화를 사용할 수 있기 때문이다. 편의뿐만 아니라 배치 처리를 통하여 텍스처로 보낼 SRV들을 매번 오브젝트를 그릴 때마다 렌더링 파이프라인에 묶지 않고 한 번에 다 묶어 버리니깐 굉장히 효율적이다. 따라서 무조건 프로젝트를 만들면 구현해야지 마음을 먹었지만 아무래도 인터넷 강의와 책의 내용 및 예제를 보면서 함께 구현하다 보니 둘 사이에 다른 점이 많았다. 예를 들어 테이블 서술자와 상수 버퍼에서도 강의에서는 서술자 복사를 통한 동적인 바인딩을 사용하였는데 이게 좋아 보여서 이 방법을 사용하여 구현하였지만 결국 이렇게 구현하면 동적 색인화가 굉장히 복잡해지고 어쩌면 구현을 할 수 없다고도 생각하게 되었다. 따라서 동적 바인딩을 위해 코드를 재설계하며 다시 뜯어고치고 나니 둘의 차이점을 명확히 알게 되었고 어떠한 것이 더 좋다가 아니라 이러한 상황에서는 다른 방법을 저러한 상황에서는 이런 방법을 사용하는 것이 맞다는 것을 알게 되었다. 역시 코딩에는 답이 정해져 있는 것이 아니다는 것을 깨달았다. 그저 답에 가까워질 뿐이다.
'DirectX12 > DirectX12 응용' 카테고리의 다른 글
[Deferred Rendering] (0) | 2023.10.30 |
---|---|
[Render Target] (0) | 2023.10.22 |
[Orthographic Projection] (1) | 2023.10.22 |
[Frustum Culling] (0) | 2023.10.21 |
[CubeMap] (1) | 2023.10.16 |
[Normal Mapping] (0) | 2023.10.12 |
[Light-2] (1) | 2023.10.11 |