일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- C++
- 노멀 맵핑
- effective C++
- light
- 네트워크 게임 프로그래밍
- gitlab
- 게임 프로그래밍
- gitscm
- 디퍼드 렌더링
- 입방체 매핑
- FrameResource
- 직교 투영
- TCP/IP
- 절두체 컬링
- DirectX
- DirectX12
- 게임 클래스
- Direct3D12
- 조명 처리
- 큐브 매핑
- Frustum Culling
- 게임 디자인 패턴
- direct3d
- InputManager
- 네트워크
- 동적 색인화
- Deferred Rendering
- 장치 초기화
- Render Target
- Dynamic Indexing
- Today
- Total
코승호딩의 메모장
[CubeMap] 본문
이번 글에서는 큐브 매핑을 통해 텍스처를 하늘에 입히도록 한다. 큐브 매핑을 하는 방법은 다양하다. 우선 기존의 TEXTURE2D SRV를 사용하여 일반적인 텍스처를 구에 입히는 방법이 있다. 그러나 이번 구현에서는 보다 정확한 구현을 위해 TEXTURECUBE SRV를 사용하여 큐브 매핑을 하려 한다.
Direct3D에서는 2차원 텍스처를 적용할 때와는 달리, 큐브 맵의 한 텍셀을 2차원 텍스처 좌표로 지칭할 수 없다. 따라서 큐브 맵의 한 텍셀을 식별하기 위해서는 3차원 텍스처 좌표가 필요하다. 이를 조회 벡터 v라고 부른다.
위 그림은 3차원을 간단하게 2차원으로 표현한 모습이다. 3차원에서의 텍셀은 원점에서 v의 방향으로 나아가는 반직선의 조회 벡터가 큐브 맵의 한 면과 교차하는 지점에 있는 것에 해당한다. HLSL에서는 큐브 맵을 TextureCube라는 형식으로 나타낸다. 또한 당연하게도 조회 벡터는 큐브 맵과 같은 공간에 있어야 한다. 우리의 목표는 오브젝트 구를 생성하여 원점에서 뻗어 나가는 조회 벡터를 이용하여 큐브 맵을 샘플링 하는 것이다. 큐브 맵은 입방체로 되어 있기 때문에 조회 벡터와 입방체의 어떠한 면이 만나는 부분을 구에다 샘플링하게 되면 자연스럽게 큐브 매핑이 되는 것이다.
TextureCube gCubeMap : register(t3);
float4 PS_Main(VS_OUT pin) : SV_Target
{
return gCubeMap.Sample(gsamLinearWrap, pin.posL);
}
다음과 같이 HLSL에서 큐브 맵의 표본을 추출하는 방법이다.
Texassemble
큐브 맵을 구현하기에 앞서 우선 큐브 맵을 dds 파일로 생성하는 방법을 알아보자. 큐브 맵을 생성하기 위해서는 우선 6장의 이미지가 필요하다. 그리고 이 이미지들을 texassemble.exe 도구를 통해서 cube.dds로 변경해야 한다.
texassemble 도구를 다운받아서 cmd창을 켜고 texassemble이 위치한 곳에 6장의 이미지를 놓아주고 다음과 같이 명령어를 입력한다. 큐브 맵의 형식으로 만들 것이기 때문에 맨 앞에 cube가 붙고 뒤에는 파일 크기이다. 그리고 다음에 하나의 dds 출력 파일을 써주며 마지막으로 6장의 이미지를 입력하는데 차례대로 +X/-X/+Y/-Y/+Z/-Z 대로 넣어주면 파일이 만들어진다. 참고로 texassemble이나 texconv와 같은 도구들은 파이썬을 이용하여 편리하게 사용하도록 만들어 주는 것이 좋다.
다음과 같이 파이썬으로 도구들을 관리하면 Input에 이미지를 넣어주고 파이썬을 실행하게 되면 Output에 자동으로 이미지들이 dds 혹은 assemble 되어 출력된다. 그리고 원본은 Converted에 돌아간다.
CubeMap
이제 본격적으로 큐브 맵을 구현해보자. 딱히 많이 바뀔 내용은 없다.
enum class TEXTURE_TYPE
{
TEXTURE2D,
TEXTURECUBE,
};
void Texture::Init(const wstring& path, TEXTURE_TYPE textureType)
{
CreateTexture(path);
CreateView(textureType);
}
void Texture::CreateView(TEXTURE_TYPE textureType)
{
//...
switch (textureType)
{
case TEXTURE_TYPE::TEXTURE2D:
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
break;
case TEXTURE_TYPE::TEXTURECUBE:
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
break;
default:
break;
}
DEVICE->CreateShaderResourceView(_resource.Get(), &srvDesc, _srvHandle);
}
쉐이더 코드에서 TextureCube 형식의 텍스처를 사용하기 위해서는 SRV를 생성할 때 ViewDimension을 TEXTURECUBE로 바꿔줘야 한다. 따라서 텍스처 클래스에서 enum class를 통해 텍스처 형식을 더 추가해 주고 기본은 TEXTURE2D를 갖도록 추가한다. 그리고 텍스처의 타입에 따라서 switch 문을 통해 TEXTURECUBE로 변경할 수 있도록 한다.
enum class RASTERIGER_TYPE
{
CULL_NONE,
CULL_FRONT,
CULL_BACK,
WIREFRAME,
};
enum class DEPTH_STENCIL_TYPE
{
LESS,
LESS_EQUAL,
GREATER,
GREATER_EQUAL,
};
struct ShaderInfo
{
RASTERIGER_TYPE rasterizerType = RASTERIGER_TYPE::CULL_BACK;
DEPTH_STENCIL_TYPE dpethStencilType = DEPTH_STENCIL_TYPE::LESS;
};
또한 레스터라이저와 깊이 스텐실 버퍼도 살짝 변경해줘야 하는데, 이유는 게임에서 큐브 맵은 가장 바깥의 있는 닿을 수 없이 먼 부분이기 때문에 우리는 큐브 맵의 안쪽에 있는 것이고 이에 따라 은면 제거를 꺼줘야 한다. 그리고 큐브 맵을 가장 멀리 있게 만들기 위해서 나중에 쉐이더 코드에서 해당 큐브 맵 오브젝트의 깊이를 1로 만들어주는 작업을 하기 때문에 1이면 깊이 판정에서 걸려 그려지지 않는다. 따라서 1도 그리기 위해서는 LESS_EQUAL로 깊이 스텐실 버퍼를 변경해야 한다.
//...
shared_ptr<MeshRenderer> meshRenderer = make_shared<MeshRenderer>();
{
shared_ptr<Mesh> mesh = GET_SINGLE(Resources)->LoadSphereMesh();
meshRenderer->SetMesh(mesh);
}
{
//...
shader->Init(L"..\\Resources\\Shader\\Sky.hlsl", { RASTERIGER_TYPE::CULL_NONE, DEPTH_STENCIL_TYPE::LESS_EQUAL });
texture->Init(L"..\\Resources\\Texture\\Sky.dds", TEXTURE_TYPE::TEXTURECUBE);
shared_ptr<Material> material = make_shared<Material>();
material->SetShader(shader);
material->SetTexture(3, texture);
meshRenderer->SetMaterial(material);
}
//...
이제 게임 오브젝트를 생성할 때, 구 하나를 생성하고 컬링을 하지 않으며 1도 포함하는 쉐이더와 TEXTURECUBE의 텍스처를 생성하여 MeshRenderer에 붙이고 추가한다.
VS_OUT VS_Main(VS_IN vin)
{
VS_OUT vout = (VS_OUT)0.f;
vout.posL = vin.posL;
float4 posW = mul(float4(vout.posL, 1.f), gObjConstants.world);
posW.xyz += gPassConstants.eyePosW;
vout.posH = mul(posW, gPassConstants.viewProj).xyww;
return vout;
}
float4 PS_Main(VS_OUT pin) : SV_Target
{
return gCubeMap.Sample(gsamLinearWrap, pin.posL);
}
쉐이더 코드에서는 이게 끝이다. 현재 구현하는 세상은 카메라 기준이 아닌 월드 기준이다. 또한 큐브 맵에 해당하는 오브젝트의 중심은 항상 카메라의 중심이어야 한다. 즉, 카메라가 움직이면 큐브맵 오브젝트도 따라서 움직여야 한다는 것이다. 때문에 세계 행렬로 변환한다. 큐브 맵의 세계 행렬에서는 딱히 해준 것이 없기 때문에 로컬 좌표계와 동일할 것이다. 이 상태에서 카메라의 위치 값을 더하여 카메라와 같은 위치를 갖게 한다. 동차 좌표계를 생성할 때는 원근 투영 나누기 부분에서 원근 투영을 위해서 레스터라이저에서 z를 w로 나누게 된다. 또한 z 값을 가장 먼 거리인 1로 변경하기 위해서 z값을 w로 변경하면 원근 투영 나누기에서 z 값이 1이 될 것이다. 마지막의 픽셀 쉐이더에서는 그저 TextureCube로 생성한 텍스처를 샘플링할 뿐이다. 다만 이 때, uv값을 넣어주는 것이 아니라 posL 즉 조회 벡터를 넣어주는 것이다.
이제 실행하면 다음과 같이 자연스럽게 큐브맵이 생성되어 뉴진스가 외국 어딘가에 나가 있는 모습을 볼 수 있다. texassemble에서 옵션을 넣어줄 때 고화질로 2048x2048로 dds 파일을 만들어 줬기 때문에 화질이 아주 깔끔하고 보기 좋다. 참고로 프레임은 녹화 영상을 찍을때만 올라가기 때문에 별로 문제 없다.
큐브 맵을 구현함으로써 게임 세상이 한층 더 현실감 있게 바뀌었다. 그러나 아직 부족한 점이 많다. 바로 큐브 맵의 텍스처가 오브젝트들에 반사가 되지 않는다는 것이다. 예를 들어 거울이나 반짝이는 면과 같이 주변 환경을 반사할 수 있는 기능을 추가한다면 더욱 더 현실감 있게 바뀔 것이다. 생각보다 어렵지 않게 구현할 수 있었기에 더욱 재미있는 구현이었다.
'DirectX12 > DirectX12 응용' 카테고리의 다른 글
[Orthographic Projection] (1) | 2023.10.22 |
---|---|
[Dynamic Indexing] (1) | 2023.10.21 |
[Frustum Culling] (0) | 2023.10.21 |
[Normal Mapping] (0) | 2023.10.12 |
[Light-2] (1) | 2023.10.11 |
[Light-1] (1) | 2023.10.10 |
[Resources] (1) | 2023.10.10 |