일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- light
- DirectX
- Render Target
- gitscm
- 게임 클래스
- Frustum Culling
- direct3d
- Deferred Rendering
- gitlab
- 조명 처리
- 네트워크
- 게임 프로그래밍
- 동적 색인화
- DirectX12
- Direct3D12
- 노멀 맵핑
- TCP/IP
- 절두체 컬링
- 입방체 매핑
- 게임 디자인 패턴
- C++
- Dynamic Indexing
- 디퍼드 렌더링
- 장치 초기화
- 네트워크 게임 프로그래밍
- FrameResource
- 큐브 매핑
- effective C++
- 직교 투영
- InputManager
- Today
- Total
코승호딩의 메모장
[Texture Mapping] 본문
이번 글에서는 텍스쳐 맵핑을 사용하여 간단한 사각형에 텍스쳐를 입히도록 한다. 이전에 만들었던 서술자 테이블에 CBV이든 SRV이든 그냥 CopyDescriptors를 이용하여 서술자를 복사하면 되기 때문에 쉽게 구현할 수 있을 것이다.
서술자 테이블에 SRV 추가
enum class CBV_REGISTER : uint8
{
b0,
b1,
b2,
b3,
b4,
END
};
enum class SRV_REGISTER : uint8
{
t0 = static_cast<uint8>(CBV_REGISTER::END),
t1,
t2,
t3,
t4,
END
};
다음과 같이 CBV, SRV의 레지스터를 enum class를 사용하여 묶어줬기 때문에 서술자 테이블에서 GetCpuHandle을 할 때, 해당하는 레지스터 값만 넣어주면 자동으로 계산하여 CpuHandle을 반환하기 때문에 해당 서술자 힙에 복사할 수 있다. 예를 들어 SRV의 t2 레지스터에 텍스쳐를 넣고 싶다면 t2 레지스터는 총레지스터의 7번째 즉 7U에 해당하므로 GetCPUHandle을 통해서 Offset을 해줄 때, handle.ptr += 7U * _handleSize가 되므로 해당하는 CpuHandle을 편리하게 가져올 수 있다.
현재 레지스터의 상황은 딱 위 그림과 같다. 우리의 목표는 t0 레지스터에 텍스쳐를 넘겨주는 것이다.
void TableDescriptorHeap::SetSRV(D3D12_CPU_DESCRIPTOR_HANDLE srcHandle, SRV_REGISTER reg)
{
D3D12_CPU_DESCRIPTOR_HANDLE destHandle = GetCPUHandle(reg);
uint32 destRange = 1;
uint32 srcRange = 1;
DEVICE->CopyDescriptors(1, &destHandle, &destRange, 1, &srcHandle, &srcRange, D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
}
SetCBV와 마찬가지로 SetSRV도 reg 번호를 인자로 받아 해당 부분의 CpuHandle에 넘겨준 서술자를 복사해 준다.
이제 서술자 테이블에 대한 세팅이 완료되었으므로 Texture 클래스를 생성해 보도록 하자.
Texture Class
class Texture
{
public:
void Init(const wstring& path);
D3D12_CPU_DESCRIPTOR_HANDLE GetCpuHandle() { return _srvHandle; }
public:
void CreateTexture(const wstring& path);
void CreateView();
private:
ComPtr<ID3D12Resource> _resource;
ComPtr<ID3D12Resource> _uploadHeap;
ComPtr<ID3D12DescriptorHeap> _srvHeap;
D3D12_CPU_DESCRIPTOR_HANDLE _srvHandle;
};
텍스쳐 클래스는 Init 함수에서 텍스쳐가 있는 경로를 받아와서 CreateTexture에 넘겨줘서 텍스쳐를 생성하는 방식이다. 그리고 텍스쳐는 루트 서술자를 사용할 수 없기 때문에 무조건 서술자 테이블로만 넘겨줄 수 있다. 따라서 서술자 테이블에 복사를 하기 위한 srvHeap을 내부적으로 가지고 있으며 접근하기 쉽게 GetCpuHandle을 통해 srvHandle을 반환하도록 한다.
void Texture::CreateTexture(const wstring& path)
{
wstring ext = fs::path(path).extension();
if (ext == L".dds" || ext == L".DDS")
ThrowIfFailed(DirectX::CreateDDSTextureFromFile12(DEVICE.Get(), CMD_LIST.Get(), path.c_str(), _resource, _uploadHeap));
}
우선 텍스쳐를 생성하는 방법이다. dds 파일은 압축 파일로 밉맵이 가능하고 용량이 적기 때문에 앞으로 사용되는 텍스쳐는 무조건 dds 파일을 사용하도록 한다. 다른 형식의 파일은 texconv를 활용하면 쉽게 변환이 가능할 것이다.
DDSTextureLoader
The DirectX Tool Kit (aka DirectXTK12) is a collection of helper classes for writing DirectX 12 code in C++ - microsoft/DirectXTK12
github.com
위 깃허브 홈페이지에 가면 DDSTextureLoader라는 파일을 다운로드할 수 있는데 .h 파일과 .cpp파일을 그냥 자신의 프로젝트에 넣어주고 Include만 하면 dds 파일을 불러오는 CreateDDSTextureFromFile12 함수를 사용할 수 있다.
void Texture::CreateView()
{
D3D12_DESCRIPTOR_HEAP_DESC srvHeapDesc = {};
srvHeapDesc.NumDescriptors = 1;
srvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
srvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
DEVICE->CreateDescriptorHeap(&srvHeapDesc, IID_PPV_ARGS(&_srvHeap));
_srvHandle = _srvHeap->GetCPUDescriptorHandleForHeapStart();
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.Format = _resource->GetDesc().Format;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = -1;
DEVICE->CreateShaderResourceView(_resource.Get(), &srvDesc, _srvHandle);
}
텍스쳐에 대한 리소스가 완성이 되었다면 이 리소스에 대한 쉐이더 리소스 뷰를 만들어야 한다. 모두 완성이 되었다면 텍스쳐를 입히고 싶은 Mesh를 렌더링 할 때, 서술자 테이블에 SRV를 복사해 주면 끝이다.
class Mesh
{
public:
void SetTexture(shared_ptr<Texture> tex) { _tex = tex; }
private:
shared_ptr<Texture> _tex = {};
};
void Mesh::Render()
{
//...
{
D3D12_CPU_DESCRIPTOR_HANDLE handle = GEngine->GetCB()->PushData(0, &_transform, sizeof(_transform));
GEngine->GetTableDescHeap()->SetCBV(handle, CBV_REGISTER::b0);
GEngine->GetTableDescHeap()->SetSRV(_tex->GetCpuHandle(), SRV_REGISTER::t0);
}
GEngine->GetTableDescHeap()->CommitTable();
CMD_LIST->DrawIndexedInstanced(_indexCount, 1, 0, 0, 0);
}
Mesh에 Texture를 추가하고 렌더링 할 때, SetCBV처럼 SetSRV를 호출하여 인자에 해당 텍스쳐의 CpuHandle값을 넘겨주면 된다. 텍스쳐는 t(n) 레지스터를 사용하기 때문에 SRV_REGISTER::t0을 넣어주면 된다.
그렇다면 이제 텍스쳐를 Game에서 생성하여 Mesh에 전달만 해주면 끝일까? 아니다. 텍스쳐를 맵핑하기 위한 정점 버퍼의 uv값이 필요하다. uv 값은 0~1로 정규화되어 있으므로 버텍스 정보에 uv를 추가하도록 하자.
struct Vertex
{
Vec3 pos;
Vec4 color;
Vec2 uv;
};
vector<Vertex> vec(4);
vec[0].pos = Vec3(-0.5f, 0.5f, 0.5f);
vec[0].color = Vec4(1.f, 0.f, 0.f, 1.f);
vec[0].uv = Vec2(0.f, 0.f);
vec[1].pos = Vec3(0.5f, 0.5f, 0.5f);
vec[1].color = Vec4(0.f, 1.f, 0.f, 1.f);
vec[1].uv = Vec2(1.f, 0.f);
vec[2].pos = Vec3(0.5f, -0.5f, 0.5f);
vec[2].color = Vec4(0.f, 0.f, 1.f, 1.f);
vec[2].uv = Vec2(1.f, 1.f);
vec[3].pos = Vec3(-0.5f, -0.5f, 0.5f);
vec[3].color = Vec4(0.f, 1.f, 0.f, 1.f);
vec[3].uv = Vec2(0.f, 1.f);
D3D12_INPUT_ELEMENT_DESC desc[] =
{
{ "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 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 28, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
};
vec[0]은 왼쪽 상단, vec[1]은 오른쪽 상단, vec[2]는 오른쪽 하단. vec[3]는 왼쪽 하단이기 때문에 다음과 같이 uv를 정한다. 당연히 정점 버퍼의 내용이 바뀌었으므로 입력 레이아웃의 정보도 바뀌어야 한다.
struct ObjectConstants
{
float4 offset0;
float4 offset1;
};
ConstantBuffer<ObjectConstants> gObjConstants : register(b0);
Texture2D gDiffuseMap : register(t0);
SamplerState gsamPointWrap : register(s0);
SamplerState gsamPointClamp : register(s1);
SamplerState gsamLinearWrap : register(s2);
SamplerState gsamLinearClamp : register(s3);
SamplerState gsamAnisotropicWrap : register(s4);
SamplerState gsamAnisotropicClamp : register(s5);
struct VS_IN
{
float3 pos : POSITION;
float4 color : COLOR;
float2 uv : TEXCOORD;
};
struct VS_OUT
{
float4 pos : SV_Position;
float4 color : COLOR;
float2 uv : TEXCOORD;
};
VS_OUT VS_Main(VS_IN input)
{
VS_OUT output = (VS_OUT)0;
output.pos = float4(input.pos, 1.f);
output.pos += gObjConstants.offset0;
output.color = input.color;
output.color += gObjConstants.offset1;
output.uv = input.uv;
return output;
}
float4 PS_Main(VS_OUT input) : SV_Target
{
float4 color = gDiffuseMap.Sample(gsamAnisotropicWrap, input.uv);
return color;
}
이제 쉐이더 코드에서 t0 레지스터를 추가하고 이를 샘플링하면 된다. 정적 샘플링을 사용하였으며 정적 샘플링에 대한 자세한 정보는 [DirectX 입문]에서 하도록 한다. 모든 준비가 완료되었다면 원하는 텍스쳐를 생성하고 Mesh에 설정하자.
shared_ptr<Mesh> mesh = make_shared<Mesh>();
shared_ptr<Shader> shader = make_shared<Shader>();
shared_ptr<Texture> texture = make_shared<Texture>();
shader->Init(L"..\\Resources\\Shader\\Default.hlsl");
texture->Init(L"..\\Resources\\Texture\\newjeans.dds");
mesh->SetTexture(0, texture);
다음과 같이 mesh에 텍스쳐를 설정하고 프로그램을 시작하면 뉴진스 사진이 나오게 된다.
이제까지 배운 책에서는 서술자 테이블은 텍스쳐를 위한 용도로만 사용하고 나머지 상수 버퍼들은 루트 서술자를 사용하여 설정하였다. 그러나 이번 프로젝트에서는 모든 SRV, UAV, CBV를 하나의 서술자 테이블에서 관리하도록 하였으며 이 과정에서 위 예제와 책의 예제가 쉐이더에 리소스를 넘겨주는 방법이 다르다는 것을 파악하고 사실 많은 공부를 하였다. 어떻게 다른지 어느 부분이 장점이 있는지를 파악할 수 있었고 결국 조금의 성능은 떨어지지만 학부생 수준에서는 전혀 문제가 없는 방식 즉, 구현하기 쉬우면서 코드의 커플링을 줄일 수 있는 방식을 택하게 되었다. 특히 상수 버퍼, 루트 시그니처 쪽은 Direct3D를 공부하며 나에게는 엄청난 고비였기 때문에 확실히 잡고 가야겠다는 생각이 들었다. 따라서 이해할 때까지 그림을 그려가며 코드를 분석한 것이 굉장히 많은 도움이 되었다.
'DirectX12 > DirectX12 응용' 카테고리의 다른 글
[SceneManager] (1) | 2023.10.08 |
---|---|
[Component] (0) | 2023.10.07 |
[InputManager와 GameTimer] (1) | 2023.10.07 |
[Material] (0) | 2023.10.07 |
[Mesh] (1) | 2023.10.07 |
[ConstantBuffer와 FrameResource] (0) | 2023.10.07 |
[장치 초기화] (1) | 2023.10.07 |