1. 3D Direct Application
1.1 Terrian Rendering
1.1.1 Terrian Rendering System
1.1.1.1 Height Map
- 지형의 산과 계곡에 대한 정보를 높이맵에서 가져옴
- 높이맵은 일종의 2차원 배열으로 원소는 지형 격자의 해당 정점의 높이를 의미
- 격자의 각 정점마다 높이맵의 원소 하나가 대응
- 높이맵은 각 원소를 하나의 바이트로 저장, 하나의 높이는 최소 0, 최대 255
- 지형의 높이들 사이의 전이를 보존하기에 충분하나 응용 프로그램 안에서는 이 구간의 높이들을 응용 프로그램이 렌더링할 3차원 세계의 축척에 맞게 비례시킬 필요가 잇음
- 255미터가 넘는 산을 표현하지 못하므로 응용 프로그램으로 적재할 때 float로 설정
1.1.1.2 Smoothing
- 8비트 높이맵을 지형 렌더링에 사용하는 것의 문제점은 표현 가능한 서로 다른 높이 단계들이 256개
- 높이 값들으 그대로 사용하지 못하고 소수부가 절단된 값들을 사용, 따라서 더 거친 지형이 만들어짐
- 이를 해결하기 위해 적절히 평활화한다면 소수부가 있는 모습을 얻는 것도 가능
- 부동소수점 높이맵에 평활화 필터를 적용하여 인접한 원소들 사이의 극단적인 높이차를 줄임
- 높이맵의 각 픽셀마다 그 픽셀과 이웃 픽셀 8개의 평균을 구해 새로운 높이로 사용
float Terrain::Average(int32 i, int32 j);
- 부동소수점 높이맵인 2차원 배열의 원소를 전달받아 픽셀의 평균 높이를 계산하는 함수 생성
- 위에서 말했듯이 ij 픽셀 및 그 이웃의 여덟 픽셀의 평균을 구함
- 가장자리 픽셀들의 경우 만일 해당 방향의 이웃 픽셀이 없으면 그냥 평균에 포함시키지 않고 넘김
float avg = 0.0f; float num = 0.0f;
for (int32 m = i - 1; m <= i + 1; ++m)
{
for (int32 n = j - 1; n <= j + 1; ++n)
if (InBounds(m, n)) { avg += _heightmap[m * _info.heightmapWidth + n]; num += 1.0f; }
}
return avg / num;
- 주어진 Index들이 높이맵의 유효한 원소에 해당하면 true 그렇지 않으면 false를 반환
- 가장자리 픽셀에서 높이맵의 일부가 아닌, 즉 존재하지 않는 이웃 항목을 추출하는 경우 포함하지 않음
bool Terrain::InBounds(int32 i, int32 j)
{ return i >= 0 && i < (int32)_info.heightmapHeight && j >= 0 && j < (int32)_info.heightmapWidth; }
- 높이맵의 모든 원소에 위의 함수를 통해 적용하여 높이맵 전체를 평활화하는 함수 사용
std::vector<float> dest(_heightmap.size());
for (uint32 i = 0; i < _info.heightmapHeight; ++i)
{
for (uint32 j = 0; j < _info.heightmapWidth; ++j)
dest[i * _info.heightmapWidth + j] = Average(i, j);
}
_heightmap = dest;
- 높이맵에 대한 셰이더 자원 뷰를 생성, 내용은 생략
void Terrain::BuildHeightmapSRV(ComPtr<ID3D11Device> device);
1.1.2 Tesselation
- 지형을 구축하는 데에 많은 수의 삼각형이 필요
- 그런데 지형 중에 카메라에서 멀리 떨어져 있는 부분은 어차피 세부사항이 잘 보이지 않음
- 이러한 사항들을 적절히 처리해주는 세부 수준 시스템을 지형 렌더링에 적용
- 사각형 패치들로 이루어진 격자를 배치, 그 패치들을 카메라와의 거리에 기초해서 테셀레이션 실행
- 높이맵에 대한 셰이더 자원 뷰를 파이프라인에 묶어 두고 영역 셰이더에서 높이맵에서 추출한 높이 값들로 변위 매핑을 수행해서 새 정점들의 높이를 적절히 조정
1.1.2.1 Cell Spacing
- 지형을 여러 패치들의 격자로 나누는데 이 패치 격자의 한 패치는 65 ✕ 65 영역을 포괄
- 최대 테셀레이션 계수가 64개, 하나의 패치를 최대로 테셀레이션하면 64 ✕ 64 낱칸, 정점으로 치면 65 ✕ 65
- 한 패치가 최대로 테셀레이션되면 생성된 모든 정점에 대해 높이맵의 높이 정보가 부여
- 패치의 테셀레이션 계수가 1이면 그 패치는 전혀 세분되지 않고 그냥 두 개의 삼각형으로 렌더링
static const int CellsPerPatch = 64;
_numPatchVertRows = ((_info.heightmapHeight - 1) / CellsPerPatch) + 1;
_numPatchVertCols = ((_info.heightmapWidth - 1) / CellsPerPatch) + 1;
_numPatchVertices = _numPatchVertRows * _numPatchVertCols;
_numPatchQuadFaces = (_numPatchVertRows - 1) * (_numPatchVertCols - 1);
1.1.2.1.1 Vertex Buffer Create
void Terrain::BuildQuadPatchVB(ComPtr<ID3D11Device> device);
- 절반의 가로 & 세로, 패치당 가로 & 세로 개수, 사각형 한개당 가로 & 세로 크기 설정
float halfWidth = 0.5f * GetWidth(); float halfDepth = 0.5f * GetDepth();
float patchWidth = GetWidth() / (_numPatchVertCols - 1); float patchDepth = GetDepth() / (_numPatchVertRows - 1);
float du = 1.0f / (_numPatchVertCols - 1); float dv = 1.0f / (_numPatchVertRows - 1);
- 화면 좌표상 좌상단 부터 시작하므로 x, z 값을 설정
- Terrian에 각 정점에 따라 높이 맵을 설정하고 Texture 적용을 위해 설정
for (uint32 i = 0; i < _numPatchVertRows; ++i)
{
float z = halfDepth - i * patchDepth;
for (uint32 j = 0; j < _numPatchVertCols; ++j)
{
float x = -halfWidth + j * patchWidth;
patchVertices[i * _numPatchVertCols + j].Pos = XMFLOAT3(x, 0.0f, z);
patchVertices[i * _numPatchVertCols + j].Tex.x = j * du;
patchVertices[i * _numPatchVertCols + j].Tex.y = i * dv;
}
}
- 축 정렬 경계상자의 Y 경계들을 왼쪽 상당 모퉁이 패치에 저장
for (uint32 i = 0; i < _numPatchVertRows - 1; ++i)
{
for (uint32 j = 0; j < _numPatchVertCols - 1; ++j)
{
uint32 patchID = i * (_numPatchVertCols - 1) + j;
patchVertices[i * _numPatchVertCols + j].BoundsY = _patchBoundsY[patchID];
}
}
- 마지막으로 정점 버퍼를 생성
D3D11_BUFFER_DESC vbd; D3D11_SUBRESOURCE_DATA vinitData; /* 생략 */
HR(device->CreateBuffer(&vbd, &vinitData, _quadPatchVB.GetAddressOf()));
1.1.2.1.2 Index Buffer Create
void Terrain::BuildQuadPatchIB(ComPtr<ID3D11Device> device);
- 각 사각형마다 Index 계산
- 사각형 패치의 위쪽 정점 2개, 아래쪽 정점 2개를 사용하고 다음 사각형으로 넘김
int32 k = 0;
for (uint32 i = 0; i < _numPatchVertRows - 1; ++i)
for (uint32 j = 0; j < _numPatchVertCols - 1; ++j)
{
indices[k] = i * _numPatchVertCols + j;
indices[k + 1] = i * _numPatchVertCols + j + 1;
indices[k + 2] = (i + 1) * _numPatchVertCols + j;
indices[k + 3] = (i + 1) * _numPatchVertCols + j + 1;
k += 4;
}
- 마지막으로 색인 버퍼 생성
D3D11_BUFFER_DESC ibd; D3D11_SUBRESOURCE_DATA iinitData; /* 생략 */
HR(device->CreateBuffer(&ibd, &iinitData, _quadPatchIB.GetAddressOf()));
1.1.2.2 Vertex Shader
- 테셀레이션을 사용하므로 정점 셰이더는 정점이 아닌 제어점을 처리하는 역할
- 높이맵 값을 읽어 패치 제어점에 대한 변위 매핑을 수행한다는 점만 제외하면 그대로 통과 셰이더에 가까움
- 변위 매핑은 제어점의 y 성분을 적절한 높이로 설정하는 역할
- 덮개 셰이더에서 각 패치와 시점 사이의 거리를 계산, 패치의 모퉁이 정점들이 xz평면이 아닌 적절한 높이에 있으면 그러한 계산이 좀 더 정확해지기 때문
VertexOut vout;
vout.PosW = vin.PosL;
vout.PosW.y = gHeightMap.SampleLevel(samHeightmap, vin.Tex, 0).r;
vout.Tex = vin.Tex; vout.BoundsY = vin.BoundsY;
return vout;
1.1.2.3 Hull Shader
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID);
- 각 패치마다 그 패치를 얼마나 세분할것인지 결정하는 테셀레이션 계수들을 계산 및 출력
- 이 상수 덮개 셰이더를 GPU에서 절두체 선별을 수행하는 기회로 사용 (다음 글을 통해 설명)
- 시점과 패치 각 변 중점 사이의 거리에 기초해 변 테셀레이션 계수를 계산
float CalcTessFactor(float3 p)
{
float d = distance(p, gEyePosW);
float s = saturate((d - gMinDist) / (gMaxDist - gMinDist));
return pow(2, (lerp(gMaxTess, gMinTess, s)));
}
- 세부수준이 하나 증가함에 따라 세분 정도는 두배가 됨, 제곱수를 사용
- 2의 거듭제곱 함수를 사용하면 세부수준들이 거리에 따라 좀 더 잘 분포
- 패치의 중정과 패치의 각 변의 중점에 이 테셀레이션 계수 계산 함수를 적용하여 결정
- 각 변의 중점과 패치 자체의 중점을 계산
PatchTess pt;
/* 절두체 선별 생략 */
else
{
float3 e0 = 0.5f * (patch[0].PosW + patch[2].PosW);
float3 e1 = 0.5f * (patch[0].PosW + patch[1].PosW);
float3 e2 = 0.5f * (patch[1].PosW + patch[3].PosW);
float3 e3 = 0.5f * (patch[2].PosW + patch[3].PosW);
float3 c = 0.25f * (patch[0].PosW + patch[1].PosW + patch[2].PosW + patch[3].PosW);
pt.EdgeTess[0] = CalcTessFactor(e0);
pt.EdgeTess[1] = CalcTessFactor(e1);
pt.EdgeTess[2] = CalcTessFactor(e2);
pt.EdgeTess[3] = CalcTessFactor(e3);
pt.InsideTess[0] = CalcTessFactor(c);
pt.InsideTess[1] = pt.InsideTess[0];
return pt;
}
1.1.2.4 Displacement Mapping & Domain Shader
[domain("quad")]
DomainOut DS(PatchTess patchTess, float2 uv : SV_DomainLocation, const OutputPatch<HullOut, 4> quad);
- 테셀레이션된 정점 위치의 매개변수 좌표를 이용하여 제어점 자료를 보간해 실제의 정점위치와 Texture 좌표를 유도
- 높이맵의 높이를 추출해서 변위 매핑 수행
- 겹선형 보간을 사용 & 지형에 Texture 계승들을 타일링하고 변위 매핑 후 동차 절단공간으로 투영
DomainOut dout;
dout.PosW = lerp(lerp(quad[0].PosW, quad[1].PosW, uv.x), lerp(quad[2].PosW, quad[3].PosW, uv.x), uv.y);
dout.Tex = lerp(lerp(quad[0].Tex, quad[1].Tex, uv.x), lerp(quad[2].Tex, quad[3].Tex, uv.x), uv.y);
dout.TiledTex = dout.Tex * gTexScale;
dout.PosW.y = gHeightMap.SampleLevel(samHeightmap, dout.Tex, 0).r;
dout.PosH = mul(float4(dout.PosW, 1.0f), gViewProj);
return dout;
1.1.2.5 Pixel Shader
float4 PS(DomainOut pin, uniform int gLightCount, uniform bool gFogEnabled) : SV_Target
- 높이맵에서 얻은 높이들에 중심차분법을 적용하여 즉석에서 추정
float2 leftTex = pin.Tex + float2(-gTexelCellSpaceU, 0.0f);
float2 rightTex = pin.Tex + float2(gTexelCellSpaceU, 0.0f);
float2 bottomTex = pin.Tex + float2(0.0f, gTexelCellSpaceV);
float2 topTex = pin.Tex + float2(0.0f, -gTexelCellSpaceV);
float leftY = gHeightMap.SampleLevel(samHeightmap, leftTex, 0).r;
float rightY = gHeightMap.SampleLevel(samHeightmap, rightTex, 0).r;
float bottomY = gHeightMap.SampleLevel(samHeightmap, bottomTex, 0).r;
float topY = gHeightMap.SampleLevel(samHeightmap, topTex, 0).r;
float3 tangent = normalize(float3(2.0f * gWorldCellSpace, rightY - leftY, 0.0f));
float3 bitan = normalize(float3(0.0f, bottomY - topY, -2.0f * gWorldCellSpace));
float3 normalW = cross(tangent, bitan);
'C++ Algorithm & Study > Game Math & DirectX 11' 카테고리의 다른 글
[Direct11] 41. 3D Direct Application - Particle System #1 (0) | 2023.07.03 |
---|---|
[Direct11] 40. 3D Direct Application - Terrian Rendering #2 (0) | 2023.06.30 |
[Direct11] 38. 3D Direct Application - Displacement Mapping (0) | 2023.06.30 |
[Direct11] 37. 3D Direct Application - Normal Mapping (0) | 2023.06.30 |
[Direct11] 36. 3D Direct Application - Dynamic Cube Mapping System (0) | 2023.06.27 |