올해 1월에 글을 쓴 이후로 오랫동안 적지 않았었는데 정신없이 구현만 하다보니 7월이 되서야 다음 글을 적게 되었다.
터레인은 지금 기준으로는 구현한지 너무 오래된 옜날 작업물이다보니 당시 어떻게 만들었는지 긴가만가 하지만 최대한 기억을 짜네 적어볼 수 밖에...
아무튼 이전 글에서 유니티를 통하여 터레인 객체의 알파 텍스쳐 맵을 얻어내는 것까지는 성공했다.

이번 프로젝트에서 구성되는 터레인은 총 4개의 레이어를 활용할 계획이다. 위 텍스쳐에서 각각의 색깔이 각 레이어가 어느 부분에 칠해져야 하는지를 알려준다.
이제 내가 할 일은 기존 터레인 클래스를 수정하여 알파 블렌딩과 타일링을 할 수 있는 터레인 클래스를 만들면 끝이다.
class CSplatTerrain : public CHeightMapTerrain
{
public:
CSplatTerrain(ID3D12Device* pd3dDevice, ID3D12GraphicsCommandList* pd3dCommandList, ID3D12RootSignature* pd3dGraphicsRootSignature, LPCTSTR pFileName, int nWidth, int nLength, int nBlockWidth, int nBlockLength, XMFLOAT3 xmf3Scale, XMFLOAT4 xmf4Color, CShader* pTerrainShader);
virtual ~CSplatTerrain();
virtual void SetRigidStatic();
}
CSplatTerrain을 기존 CHeightMapTerrain를 상속받아 만들었다. 여기서 딱히 추가할 함수는 없다. 왜냐하면 정성적인 레이어 터레인을 구현하고자 한다면 이를 위해 각 터레인 레이어를 위한 자료형이 필요하며 이를 통해 터레인을 구성하도록 하는 구조를 만들어야할 필요가 있는데, 나의 목적은 그런 정교한 터레인 객체를 만드는 것이 아니라 유니티에서 구성한 씬은 가능한 유사하게 내 프로젝트에 올리는 것이다.
나는 이미 각 레이어가 어느 위치에 그려져야하는 지에 대한 정보를 알고 있으므로 이를 이용하여 셰이더 코드를 통한 간단한 블렌딩 만으로 레이어를 표현할 수 있다. 런타임에 텍스쳐 레이어의 정보를 수정할 것도 아니기 때문에 복잡한 구조를 만들 필요가 전혀 없는 것이다.
다만 SetRigidStatic()이라는 함수가 하나 있긴 한데, 이 함수는 추후 엔비디아의 physx 라이브러리를 이용해 물리 시뮬레이션을 하기 위해 터레인 강체를 생성하는 함수이므로 지금 중요한 것은 아니다.
그러면 이제 중요한 CSplatTerrain 생성자를 코딩해주면 되는데, 딱히 바꿀 것은 많이 없다. 중요한 부분은 아래와 같다.
std::shared_ptr<CTexture> pTerrainTextures = std::make_shared<CTexture>(13, RESOURCE_TEXTURE2D, 0, 1);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/Grass.dds", RESOURCE_TEXTURE2D, 0);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/Sand.dds", RESOURCE_TEXTURE2D, 1);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/Pebbles_Sand.dds", RESOURCE_TEXTURE2D, 2);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/Rockwall.dds", RESOURCE_TEXTURE2D, 3);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/BaseGrass_normals.dds", RESOURCE_TEXTURE2D, 4);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/BaseGrass_normals.dds", RESOURCE_TEXTURE2D, 5);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/Pebbles_normals.dds", RESOURCE_TEXTURE2D, 6);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/Rockwall_Normal.dds", RESOURCE_TEXTURE2D, 7);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/BaseGrass_Heights.dds", RESOURCE_TEXTURE2D, 8);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/BaseGrass_Heights.dds", RESOURCE_TEXTURE2D, 9);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/Pebbles_Heights.dds", RESOURCE_TEXTURE2D, 10);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/Rockwall_Heights.dds", RESOURCE_TEXTURE2D, 11);
pTerrainTextures->LoadTextureFromDDSFile(pd3dDevice, pd3dCommandList, L"Terrain/Alphatexture.dds", RESOURCE_TEXTURE2D, 12);
블렌딩에 사용할 텍스쳐를 로딩하는 부분이다. 총 13개의 텍스쳐를 로드하고 있는데, 위에 4개는 기본 색상, 그 다음 4개는 노말 텍스쳐, 이어지는 4개는 높이 텍스쳐, 마지막이 알파 텍스쳐다.
이 중에서 높이 텍스쳐의 경우 없어도 된다. 이 텍스쳐들을 로딩하는 이유는 터레인에 퍼럴랙스 오클루젼 매핑을 적용하는데 필요하기 때문인데, 지금은 쓰지 않는다. 터레인의 높이맵 정보가 그렇게 극적인 높이 차이를 가지고 있지도 않아 예상한 것에 비하여 비주얼 향상이 크게 두드러지지 않고, 별도의 컬링을 하고 있지 않다보니 너무 많은 시간이 들기에 현재는 쓰지 않는 텍스쳐들이다.
CSplatGridMesh* pHeightMapGridMesh = NULL;
for (int z = 0, zStart = 0; z < czBlocks; z++)
{
for (int x = 0, xStart = 0; x < cxBlocks; x++)
{
xStart = x * (nBlockWidth - 1);
zStart = z * (nBlockLength - 1);
pHeightMapGridMesh = new CSplatGridMesh(pd3dDevice, pd3dCommandList, xStart, zStart, nBlockWidth, nBlockLength, m_xmf3Scale, xmf4Color, m_pHeightMapImage);
SetMesh(std::static_pointer_cast<CMesh>(std::shared_ptr<CSplatGridMesh>(pHeightMapGridMesh)));
}
}
그리고 중요한 부분은 이부분이다. 기존에는 CHeightMapGridMesh라는 메쉬 클래스를 사용했으나 이를 CSplatGridMesh로 바꾸었다. 이 클래스의 경우에도 매커니즘이 크게 달라지지는 않으나 메쉬의 각 정점들이 두 개의 텍스쳐 좌표를 가지도록 변경하였고, 추가로 탄젠트 벡터를 계산하여 가지게 하였다.
클래스 구성은 여기까지만 보면 충분하고 다음은 실제 블렌딩이 이뤄지는 셰이더 코드를 보자.
VS_TERRAIN_OUTPUT VSParallaxTerrain(VS_TERRAIN_INPUT input)
{
VS_TERRAIN_OUTPUT output;
output.uv0 = input.uv0;
output.uv1 = input.uv1;
float4 positionW = mul(float4(input.position, 1.0f), gmtxGameObject);
float3 normalW = mul(float4(input.normal, 0.0f), gmtxGameObject).xyz;
output.normalW = normalW;
output.position = mul(mul(positionW, gmtxView), gmtxProjection);
output.positionW = positionW;
float3x3 mtxTangentToWorld;
mtxTangentToWorld[0] = normalize(mul(input.tangent, gmtxGameObject));
mtxTangentToWorld[2] = normalize(mul(input.normal, gmtxGameObject));
mtxTangentToWorld[1] = normalize( mul(cross(input.tangent, input.normal), gmtxGameObject));
output.tangent = mtxTangentToWorld[0];
output.normal = normalize(mul(mtxTangentToWorld, normalW));
return(output);
}
터레인의 정점 셰이더 코드다. 딱히 볼 건 없고, 픽셀 셰이더에서 노멀 매핑과 지금은 쓰지 않는 패럴랙스 매핑을 위하여 TBN 행렬을 계산하여 넘겨주도록 하였다.
PS_MULTIPLE_RENDER_TARGETS_OUTPUT PSParallaxTerrain(VS_TERRAIN_OUTPUT input) : SV_TARGET
{
PS_MULTIPLE_RENDER_TARGETS_OUTPUT output;
float4 cAlphaColor = gtxTerrainTextures[12].Sample(gSamplerState, input.uv1);
float4 cNormalColor[4];
float4 cHeightColor[4];
float4 FHeightColor;
float2 vFinalCoords;
vFinalCoords = input.uv0;
float4 vFinalNormal;
float4 vFinalColor;
float4 cBaseTexColor[4];
for (int i = 0; i < 4; ++i) {
cBaseTexColor[i] = gtxTerrainTextures[i].Sample(gSamplerState, vFinalCoords);
cNormalColor[i] = gtxTerrainTextures[4 + i].Sample(gSamplerState, vFinalCoords);
if (i == 0 && cAlphaColor.r > 0.0f) {
vFinalColor += cBaseTexColor[i] * cAlphaColor.r;
vFinalNormal += cNormalColor[i] * cAlphaColor.r;
}
else if (i == 1 && cAlphaColor.g > 0.0f) {
vFinalColor += cBaseTexColor[i] * cAlphaColor.g;
vFinalNormal += cNormalColor[i] * cAlphaColor.g;
}
else if (i == 2 && cAlphaColor.b > 0.0f) {
vFinalColor += cBaseTexColor[i] * cAlphaColor.b;
vFinalNormal += cNormalColor[i] * cAlphaColor.b;
}
else if (i == 3 && cAlphaColor.a > 0.0f) {
vFinalColor += cBaseTexColor[i] * cAlphaColor.a;
vFinalNormal += cNormalColor[i] * cAlphaColor.a;
}
}
float3x3 mtxTangentToWorld;
mtxTangentToWorld[0] = normalize(input.tangent);
mtxTangentToWorld[2] = normalize(input.normal);
mtxTangentToWorld[1] = normalize(cross(input.tangent, input.normal));
vFinalNormal = vFinalNormal * 2.0f - 1.0f;
float3 toLight = float3(0.0f, 1.0f, 0.0f);
float3 Normal = mul(vFinalNormal, mtxTangentToWorld);
vFinalColor.rgb = vFinalColor.rgb * 2.5;
float4 cIllumination = Lighting(input.positionW, Normal, vFinalColor, true, input.uvs);
output.f4Scene = cIllumination;
output.f4Color = vFinalColor;
float Depth = cIllumination.w < 0.001f ? 0.0f : input.position.z;
output.f4Normal = float4(Normal * 0.5f + 0.5f, 0.0f);
output.f4PositoinW = input.positionW;
return (output);
}
픽셀 셰이더에서 가장 중요한 부분은 알파 텍스쳐를 통해 블렌딩을 하는 부분이 되겠다. uv1에 있는 알파 텍스쳐 좌표를 이용하여 알파 텍스쳐로 부터 먼저 샘플링을 한 뒤, 이후 4번의 For 루프를 돌며 각 레이어의 블렌딩을 시도한다.
코드 자체는 단순하게 샘플링을 한 뒤 각 레이어의 알맞은 알파 텍스쳐의 값을 곱하여 최종 색상에 더해주는 연산을 반복한다. 한 픽셀에 대하여 알파 텍스쳐의 기록된 RGBA의 총합은 항상 1이기 때문에 이렇게 한다면 간단하게 알파 블렌딩을 할 수 있다.

그렇게 하면 위와 같은 결과물이 나온다. 당장 상태를 보면 노멀 매핑을 했음에도 불구하고 색감도 이상하고 조잡한 느낌이 드는데, 이는 그림자 연산이나 조명 연산이 제대로 구현되지 않은 상태라 그렇다. 노멀 매핑은 조명 계산이 이루어질 때 의미가 있으므로 당연한 결과다.

조명 계산과 그림자가 들어가면 위와 같은 모습이 된다. 그림자를 다소 과하게 넣고 있는 상태라 자연적인 경관이라 할 수는 없으나 위 사진과 비교하면 보다 입체적인 느낌이 잘 나타난다.
아무튼 여기까지만 해도 터레인을 어느정도 구성이 되었다고 봐도 무방하다. 하지만 게임에 터레인만 달랑 놓아두면 경관이 너무 밋밋해지기 때문에 다양한 오브젝트를 배치하거나 할 필요가 있다.
그러므로 다음에 해야할 일은

저기 빨간색 원 안에 보이는 풀숲을 넣어줄 필요가 있다.
이를 위해서는 먼저 저 풀숲 오브젝트들의 분포도를 알려줄 데이터가 필요한 데... 이건 다음에 이어서
'Rampage' 카테고리의 다른 글
Detail Object Map - 지형 위에 풀숲 (2) (0) | 2023.07.29 |
---|---|
Detail Object Map - 지형 위에 풀숲 (1) (0) | 2023.07.24 |
Terrain - 지형 (1) (0) | 2023.01.28 |
Rampage - 소개 및 개요 (2) (0) | 2023.01.21 |
Rampage - 소개 및 개요 (1) (0) | 2023.01.14 |