Terrain
Terrain 생성 과정 요약
데이터 준비 단계
하이트맵 로드: RAW파일로 되어있는 HeightMap에 저장된 높이값(0~255)을 샘플링하여 실제 높이값(0~heightScale)으로 변환하여 메모리에 로드
하이트맵 스무딩: 각 높이값을 주변 8개 픽셀과 평균하여 부드럽게 변환 (갑작스러운 높이 변화 방지)
패치 구성: HeightMap(2049x2049)을 64셀 단위로 나누어 32x32개의 패치로 분할하고, 이를 제어하기 위한 33x33개의 패치 제어점을 생성하여 버텍스 버퍼로 만듦
패치 바운딩 정보 계산: 32x32개의 패치 각각에 포함된 모든 높이값의 최소/최대값을 계산하여 저장 (프러스텀 컬링과 LOD 계산에 사용)
하이트맵 SRV 생성: 하이트맵 데이터를 16비트 float 형식의 텍스처로 변환하여 GPU에서 샘플링할 수 있도록 준비
렌더링 파이프라인 단계
VS(버텍스 셰이더) 단계:
패치 제어점(33x33개)을 월드 좌표계로 변환
하이트맵에서 각 제어점의 정확한 높이 샘플링하여 적용
각 제어점의 UV 좌표와 해당 패치의 높이 범위 정보를 다음 단계로 전달
HS(헐 셰이더) 단계:
패치 바운딩 박스(AABB)가 시야(프러스텀) 밖이면 테셀레이션 계수를 0으로 설정하여 컬링
카메라와의 거리에 따라 테셀레이션 수준 결정 (가까운 패치는 높은 수준, 먼 패치는 낮은 수준)
이웃 패치 간 균열 방지를 위해 공유 엣지의 테셀레이션 수준 일관되게 유지
테셀레이터 단계 (하드웨어 고정 기능):
HS에서 계산된 테셀레이션 인자에 따라 패치를 매개변수 공간(uv)에서 더 작은 삼각형으로 분할
새로 생성된 각 정점의 uv 좌표만 계산하여 DS로 전달
DS(도메인 셰이더) 단계:
테셀레이터가 생성한 각 정점의 uv 좌표를 사용하여 바이리니어 보간으로 3D 위치 계산
계산된 위치의 UV 좌표로 하이트맵을 샘플링하여 정확한 높이(y값) 적용
타일링된 텍스처 좌표 계산 및 최종 정점 위치를 클립 공간으로 변환
PS(픽셀 셰이더) 단계:
하이트맵의 인접한 높이값 차이를 사용해 각 픽셀의 법선 벡터 계산
5개 레이어 텍스처(잔디, 흙, 돌 등)를 블렌드맵에 따라 혼합
계산된 법선과 라이트 정보를 바탕으로 조명 및 그림자 효과 적용
추가 핵심 요소
하이트맵 해상도: 2049x2049 크기로, 모든 지형 높이 정보를 상세히 저장
패치 제어점: 33x33=1089개로, 지형 전체의 "뼈대" 역할
테셀레이션 수준: 최대 64x64까지 가능하여, 하나의 패치가 최대 4096개의 삼각형으로 분할될 수 있음
블렌드맵: 다양한 지형 텍스처 레이어를 자연스럽게 혼합하는 데 사용되는 RGBA 텍스처
LOD 시스템: 거리에 따른 자동 LOD와 프러스텀 컬링으로 성능 최적화
메모리 최적화: 하이트맵을 16비트 float으로 저장하여 메모리 사용량 절반으로 감소
하이트맵 로드
RAW 파일 열기 및 데이터 읽기
void Mesh::LoadHeightmap()
{
// 각 정점의 높이(
std::vector<unsigned char> in(_info.heightmapWidth * _info.heightmapHeight);
// 파일 열기
std::ifstream inFile;
inFile.open(_info.heightMapFilename.c_str(), std::ios_base::binary);
if (inFile)
{
// Read the RAW bytes.
inFile.read((char*)&in[0], (std::streamsize)in.size());
// Done with file.
inFile.close();
}
// 배열 데이터를 float 배열로 복사하고 크기를 조정
_heightmap.resize(_info.heightmapHeight * _info.heightmapWidth, 0);
for (uint32 i = 0; i < _info.heightmapHeight * _info.heightmapWidth; ++i)
{
_heightmap[i] = (in[i] / 255.0f) * _info.heightScale;
}
}
HeightMap을 바이너리 모드로 열고, 모든 바이트 데이터를 in 배열에 한 번에 읽어옵니다. RAW 파일은 각 픽셀마다 1바이트(0-255)의 높이 정보를 담고 있습니다.
여기서 주목할 설정값:
heightMapFilename: 하이트맵 RAW 파일 경로
heightScale: 50.0f - 이 값에 의해 RAW 파일의 0-255 값이 0-50 단위의 실제 높이로 변환됨
heightmapWidth/Height: 2049x2049 - 하이트맵의 해상도
cellSpacing: 0.5f - 하이트맵의 각 셀 간격(월드 단위)
결과적으로, 이 과정을 통해 2049x2049 크기의 RAW 하이트맵 데이터가 0~50 범위의 실제 높이값을 가진 float 배열로 메모리에 로드됩니다.
스무딩
Smooth()
void Mesh::Smooth()
{
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;
}
Smooth() 함수는 높이맵과 같은 크기의 새 배열(dest)을 생성합니다.
모든 높이맵 좌표(i,j)에 대해:
Average(i,j) 함수를 호출하여 해당 위치와 주변 픽셀의 평균 계산
계산된 평균값을 dest 배열의 같은 위치에 저장
모든 처리가 끝나면 원본 높이맵을 새로 계산된 값으로 완전히 교체합니다
Average() / InBound()
float Mesh::Average(int32 i, int32 j)
{
// 자신과 8개 이웃 픽셀의 평균 높이 계산
float avg = 0.0f;
float num = 0.0f;
// 3x3 범위 순회 (중앙 픽셀 포함)
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;
}
bool Mesh::InBounds(int32 i, int32 j)
{
// 인덱스가 유효한지 확인
return
i >= 0 && i < (int32)_info.heightmapHeight &&
j >= 0 && j < (int32)_info.heightmapWidth;
}
Average() 함수는 중심 픽셀(i,j)과 그 주변 8개 픽셀을 포함한 3x3 영역을 순회합니다.
각 픽셀이 유효한 범위인지 InBounds()로 검사하고, 유효한 경우에만(HeightMap을 벗어나지 않도록):
픽셀의 높이값을 avg에 누적
카운터 num을 1 증가
모든 유효 픽셀의 높이 합을 픽셀 수로 나누어 평균값 반환
패치 구성
패치 구조 초기화
void Mesh::CreateTerrain()
{
TerrainInfo tmpInfo;
// ... 설정 코드 ...
tmpInfo.heightmapWidth = 2049;
tmpInfo.heightmapHeight = 2049;
tmpInfo.cellSpacing = 0.5f;
_info = tmpInfo;
// Divide heightmap into patches such that each patch has CellsPerPatch.
// heightmapHeight = 2049, CellsPerPatch = 64
// 1패치에 64개의 셀을 포함하겠다 뜻인데.
_numPatchVertRows = ((_info.heightmapHeight - 1) / CellsPerPatch) + 1; // _numPatchVertRows = 33
_numPatchVertCols = ((_info.heightmapWidth - 1) / CellsPerPatch) + 1; // _numPatchVertCols = 33 => 33 x 33개의 패치를 생성하고 각 패치는 64개의 셀을 포함
_numPatchVertices = _numPatchVertRows * _numPatchVertCols; // 33 * 33 = 1089
_numPatchQuadFaces = (_numPatchVertRows - 1) * (_numPatchVertCols - 1); // 32 * 32 = 1024
// ... 다른 함수 호출 ...
BuildQuadPatchVB(); // 패치 정점 버퍼 생성
// ... 이후 코드 ...
}
패치 구성 계산:
CellsPerPatch = 64: 각 패치는 64x64 크기의 셀을 담당
(heightmapHeight-1) / CellsPerPatch + 1: 높이맵 치수를 패치 크기로 나누고 1을 더함
(2049 - 1) / 64 + 1 = 32 + 1 = 33
따라서 33 x 33 = 1089개의 패치 정점이 생성됨
실제 패치 면은 (33 - 1) x (33 - 1) = 3 2x 32 = 1024개
패치 정점 버퍼 생성
void Mesh::BuildQuadPatchVB()
{
// 33x33=1089개의 패치 정점 생성
vector<VertexTerrain> patchVertices(_numPatchVertRows * _numPatchVertCols);
_sizeofGridX = _numPatchVertRows;
_sizeofGridZ = _numPatchVertCols;
float halfWidth = 0.5f * GetWidth(); // 512
float halfDepth = 0.5f * GetDepth(); // 512
float patchWidth = GetWidth() / (_numPatchVertCols - 1); // 32
float patchDepth = GetDepth() / (_numPatchVertRows - 1); // 32
float du = 1.0f / (_numPatchVertCols - 1); // 0.03125
float dv = 1.0f / (_numPatchVertRows - 1); // 0.03125
// 각 패치 정점의 위치와 텍스처 좌표 설정
for (uint32 i = 0; i < _numPatchVertRows; ++i)
{
float z = halfDepth - i * patchDepth;
for (uint32 j = 0; j < _numPatchVertCols; ++j)
{
float x = -halfWidth + j * patchWidth;
// 정점 위치 설정 (y는 나중에 디스플레이스먼트됨)
patchVertices[i * _numPatchVertCols + j].position = XMFLOAT3(x, 0.0f, z);
// 텍스처 좌표 설정
patchVertices[i * _numPatchVertCols + j].uv.x = j * du;
patchVertices[i * _numPatchVertCols + j].uv.y = i * dv;
}
}
// 각 패치의 바운딩 박스 정보 저장
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];
}
}
// 인덱스 버퍼 생성
vector<uint32> idx = BuildQuadPatchIB();
// 버텍스 및 인덱스 버퍼 생성
_geometryForTerrain = make_shared<Geometry<VertexTerrain>>();
_geometryForTerrain->SetVertices(patchVertices);
_geometryForTerrain->SetIndices(idx);
_buffer = make_shared<Buffer>();
_buffer->CreateBuffer(BufferType::VERTEX_BUFFER, _geometryForTerrain->GetVertices());
_buffer->CreateBuffer(BufferType::INDEX_BUFFER, _geometryForTerrain->GetIndices());
}
33 x 33개의 패치 정점을 위한 배열 생성
각 정점의 XZ 위치를 계산 ( (-512, -512)부터 (512, 512)까지 )
각 정점의 UV 좌표 계산 ( (0, 0)부터 (1, 1)까지 )
각 패치의 바운딩 정보(높이 범위)를 패치의 왼쪽 상단 정점에 저장
인덱스 버퍼를 생성하여 정점들이 패치를 형성하도록 구성
패치 인덱스 버퍼 생성
vector<uint32> Mesh::BuildQuadPatchIB()
{
// 각 패치마다 4개의 인덱스 필요 (사각형 패치)
vector<uint32> indices(_numPatchQuadFaces * 4); // 32*32*4 = 4096
// 각 패치의 인덱스 계산
int32 k = 0;
for (uint32 i = 0; i < _numPatchVertRows - 1; ++i) // _numPatchVertRows = 33
{
for (uint32 j = 0; j < _numPatchVertCols - 1; ++j) // _numPatchVertCols = 33
{
// 2x2 쿼드 패치의 상단 행
indices[k] = i * _numPatchVertCols + j; // 좌상단
indices[k + 1] = i * _numPatchVertCols + j + 1; // 우상단
// 2x2 쿼드 패치의 하단 행
indices[k + 2] = (i + 1) * _numPatchVertCols + j; // 좌하단
indices[k + 3] = (i + 1) * _numPatchVertCols + j + 1; // 우하단
k += 4; // 다음 쿼드
}
}
return indices;
}
32x32=1024개의 사각형 패치를 생성하기 위한, 총 4096개의 인덱스를 담은 배열 생성
각 패치는 정확히 4개의 인덱스로 구성 (좌상단, 우상단, 좌하단, 우하단)
이 패치들이 테셀레이션의 기본 단위가 됨
패치 구성 시각화
패치 제어점(33x33개) ↓ 패치(32x32개) ↓ 테셀레이션
● ● ● ● ● ┌─┬─┬─┬─┐ 세밀한 메시로 변환
● ● ● ● ● ├─┼─┼─┼─┤
● ● ● ● ● → ├─┼─┼─┼─┤ → /\/\/\/\/\/\
● ● ● ● ● ├─┼─┼─┼─┤ /\/\/\/\/\/\
● ● ● ● ● └─┴─┴─┴─┘ /\/\/\/\/\/\
패치 정점 월드 좌표 범위
[0]------[1] (-512,512)----(512,512)
| | | |
| 패치 | | |
| | | |
[2]------[3] (-512,-512)----(512,-512)
분할 방식: 2049x2049 하이트맵을 64셀 단위로 나눔
실제 계산: (2049-1) / 64 + 1 = 33
33x33개의 패치 제어점과 32x32개의 실제 패치 생성
공간 매핑:
월드 공간: (-512,-512)부터 (512,512)까지 1024x1024 크기
패치 크기: 약 32x32 월드 단위
UV 공간: 전체 지형이 (0,0)부터 (1,1)까지 매핑
바운딩 정보 계산
모든 패치의 바운딩 정보 계산
void Mesh::CalcAllPatchBoundsY()
{
// 32x32=1024개 패치에 대한 바운딩 정보 배열 준비
_patchBoundsY.resize(_numPatchQuadFaces); // _numPatchQuadFaces = 32 * 32 = 1024
// 모든 패치에 대해 반복
for (uint32 i = 0; i < _numPatchVertRows - 1; ++i) // _numPatchVertRows = 33
{
for (uint32 j = 0; j < _numPatchVertCols - 1; ++j) // _numPatchVertCols = 33
{
// 각 패치의 높이 범위 계산
CalcPatchBoundsY(i, j);
}
}
}
32x32=1024개의 패치에 대한 바운딩 정보를 저장할 벡터 준비
모든 패치에 대해 반복 루프를 실행하며 각 패치의 바운딩 정보 계산
각 패치의 높이 범위 계산
void Mesh::CalcPatchBoundsY(uint32 i, uint32 j)
{
// 패치가 하이트맵에서 차지하는 영역 계산
uint32 x0 = j * CellsPerPatch; // CellsPerPatch = 64
uint32 x1 = (j + 1) * CellsPerPatch;
uint32 y0 = i * CellsPerPatch;
uint32 y1 = (i + 1) * CellsPerPatch;
// 초기 최소/최대값을 극단적인 값으로 설정
float minY = +3.402823466e+38F; // 최대 float 값에 가까운 값
float maxY = -3.402823466e+38F; // 최소 float 값에 가까운 값
// 패치 영역 내의 모든 하이트맵 값을 검사
for (uint32 y = y0; y <= y1; ++y)
{
for (uint32 x = x0; x <= x1; ++x)
{
// 하이트맵에서 해당 위치의 높이 가져오기
uint32 k = y * _info.heightmapWidth + x;
// 최소값 갱신
minY = min(minY, _heightmap[k]);
// 최대값 갱신
maxY = max(maxY, _heightmap[k]);
}
}
// 계산된 높이 범위를 패치 ID에 맞게 저장
uint32 patchID = i * (_numPatchVertCols - 1) + j;
_patchBoundsY[patchID] = Vec2(minY, maxY); // Vec2의 x에 최소값, y에 최대값 저장
}
패치의 인덱스(i,j)를 하이트맵 좌표 범위로 변환 (CellsPerPatch=64를 곱함)
이 범위 내의 모든 하이트맵 높이값을 검사하여 최소/최대값 찾기
찾은 최소/최대 높이를 Vec2 형태로 해당 패치의 ID에 저장
패치 정점에 바운딩 정보 저장
void Mesh::BuildQuadPatchVB()
{
// ... 앞부분 생략 ...
// 각 패치의 좌상단 정점에 바운딩 정보 저장
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];
}
}
// ... 나머지 코드 생략 ...
}
이 코드는 각 패치의 바운딩 정보를 해당 패치의 좌상단 정점에 저장합니다. 이렇게 하면 셰이더에서 이 정보를 전달하여 프러스텀 컬링과 LOD 계산에 사용할 수 있습니다.
하이트맵 SRV
void Mesh::BuildHeightmapSRV()
{
// 1. 텍스처 설명자(descriptor) 설정
D3D11_TEXTURE2D_DESC texDesc;
texDesc.Width = _info.heightmapWidth; // 2049
texDesc.Height = _info.heightmapHeight; // 2049
texDesc.MipLevels = 1; // 미입맵 사용 안함
texDesc.ArraySize = 1; // 단일 텍스처
texDesc.Format = DXGI_FORMAT_R16_FLOAT; // 16비트 float 형식(중요!)
texDesc.SampleDesc.Count = 1; // MSAA 사용 안함
texDesc.SampleDesc.Quality = 0;
texDesc.Usage = D3D11_USAGE_DEFAULT; // GPU 읽기 최적화
texDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE; // 셰이더에서 읽기 가능
texDesc.CPUAccessFlags = 0; // CPU에서 접근 불가
texDesc.MiscFlags = 0;
// 2. 32비트 float -> 16비트 half-float 변환 (메모리 최적화)
vector<uint16> hmap(_heightmap.size());
std::transform(_heightmap.begin(), _heightmap.end(), hmap.begin(),
MathHelper::ConvertFloatToHalf);
// 3. 텍스처 초기 데이터 설정
D3D11_SUBRESOURCE_DATA data;
data.pSysMem = &hmap[0]; // 데이터 포인터
data.SysMemPitch = _info.heightmapWidth * sizeof(uint16); // 한 행의 바이트 크기
data.SysMemSlicePitch = 0; // 3D 텍스처에만 사용
// 4. 실제 텍스처 생성
ID3D11Texture2D* hmapTex = 0;
HRESULT hr = DEVICE->CreateTexture2D(&texDesc, &data, &hmapTex);
CHECK(hr);
// 5. 셰이더 리소스 뷰(SRV) 설명자 설정
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc;
srvDesc.Format = texDesc.Format; // 텍스처와 동일한 형식
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D; // 2D 텍스처
srvDesc.Texture2D.MostDetailedMip = 0;
srvDesc.Texture2D.MipLevels = -1; // 모든 미입맵
// 6. 셰이더 리소스 뷰 생성
hr = DEVICE->CreateShaderResourceView(hmapTex, &srvDesc, _heightMapSRV.GetAddressOf());
CHECK(hr);
}
텍스처 설정
DXGI_FORMAT_R16_FLOAT 형식을 사용
일반 32비트 float(4바이트)보다 절반 크기인 16비트(2바이트) 사용
하이트맵은 단일 채널(높이값)만 필요하므로 R 채널만 사용
높이값은 정확도가 매우 중요하므로 정수가 아닌 부동소수점 형식 사용
32비트 float에서 16비트 half-float로 변환
vector<uint16> hmap(_heightmap.size()); std::transform(_heightmap.begin(), _heightmap.end(), hmap.begin(), MathHelper::ConvertFloatToHalf);
원본 하이트맵(_heightmap)은 32비트 float 배열(약 16MB)
이를 16비트 half-float(hmap)로 변환하여 메모리 크기를 절반(약 8MB)으로 줄임
MathHelper::ConvertFloatToHalf 함수는 IEEE-754 표준에 따라 float를 half로 변환
VS 단계
버텍스 셰이더 입력/출력 구조체
// 버텍스 셰이더 입력 구조체
struct VertexIn
{
float3 PosL : POSITION; // 로컬 좌표계 위치
float2 Tex : TEXCOORD; // 텍스처 좌표
float2 BoundsY : BOUNDY; // 패치의 높이 범위(min, max)
};
// 버텍스 셰이더 출력 구조체
struct VertexOut
{
float3 PosW : POSITION; // 월드 좌표계 위치
float2 Tex : TEXCOORD0; // 텍스처 좌표
float2 BoundsY : TEXCOORD1; // 패치의 높이 범위(min, max)
};
VertexIn: BuildQuadPatchVB에서 생성된 정점 버퍼의 데이터 형식
VertexOut: VS가 처리한 결과를 헐 셰이더로 전달하는 데이터 형식
상수 버퍼 (필요한 변환 행렬과 데이터)
// 월드 변환 행렬
cbuffer TransformBuffer : register(b6)
{
row_major matrix worldMatrix;
row_major matrix worldInvTranspose;
}
// 하이트맵과 샘플러 선언
SamplerState samHeightmap : register(s2);
Texture2D gHeightMap : register(t2);
worldMatrix: 로컬 좌표를 월드 좌표로 변환하는 행렬
gHeightMap: BuildHeightmapSRV에서 생성된 하이트맵 텍스처
samHeightmap: 하이트맵 샘플링 방식 정의
Vertex 셰이더 함수
VertexOut VS(VertexIn vin)
{
VertexOut vout;
// 1. 로컬 좌표를 월드 좌표로 변환
vout.PosW = mul(float4(vin.PosL, 1.0f), worldMatrix).xyz;
// 2. 하이트맵에서 샘플링하여 실제 높이 적용
// (제어점의 정확한 Y좌표 설정)
vout.PosW.y = gHeightMap.SampleLevel(samHeightmap, vin.Tex, 0).r;
// 3. 텍스처 좌표와 패치 높이 범위 정보 전달
vout.Tex = vin.Tex;
vout.BoundsY = vin.BoundsY;
return vout;
}
정점의 로컬 좌표(vin.PosL)에 월드 변환 행렬을 곱해 월드 좌표로 변환
이 시점에서는 Y좌표(높이)가 아직 정확하지 않음
정점의 텍스처 좌표(vin.Tex)를 사용해 하이트맵의 해당 위치 샘플링
.r은 R 채널만 사용(하이트맵은 단일 채널 높이값만 저장)
이 단계에서 제어점의 정확한 높이가 설정됨
입력 텍스처 좌표를 그대로 출력 구조체로 전달
패치의 높이 범위 정보(BoundsY)도 그대로 전달
이 정보들은 이후 HS 단계에서 테셀레이션 인자 계산과 컬링에 사용됨
HS 단계
패치 상수 함수 (ConstantHS)
// 테셀레이션 인자를 담는 구조체
struct PatchTess
{
float EdgeTess[4] : SV_TessFactor; // 4개 에지의 테셀레이션 인자
float InsideTess[2] : SV_InsideTessFactor; // 내부 테셀레이션 인자
};
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID)
{
PatchTess pt;
// 1. 프러스텀 컬링 - 시야 밖 패치 제거
// 첫 번째 제어점에 저장된 패치의 높이 범위
float minY = patch[0].BoundsY.x;
float maxY = patch[0].BoundsY.y;
// 패치의 3D 바운딩 박스(AABB) 생성
float3 vMin = float3(patch[2].PosW.x, minY, patch[2].PosW.z);
float3 vMax = float3(patch[1].PosW.x, maxY, patch[1].PosW.z);
float3 boxCenter = 0.5f * (vMin + vMax);
float3 boxExtents = 0.5f * (vMax - vMin);
// 박스가 시야(프러스텀) 밖이면 테셀레이션 안함
if (AabbOutsideFrustumTest(boxCenter, boxExtents, gWorldFrustumPlanes))
{
pt.EdgeTess[0] = 0.0f;
pt.EdgeTess[1] = 0.0f;
pt.EdgeTess[2] = 0.0f;
pt.EdgeTess[3] = 0.0f;
pt.InsideTess[0] = 0.0f;
pt.InsideTess[1] = 0.0f;
return pt;
}
// 2. 거리 기반 테셀레이션 수준 결정
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;
}
}
// 거리 기반 테셀레이션 인자 계산 함수
float CalcTessFactor(float3 p)
{
float d = distance(p, cameraPosition);
float s = saturate((d - gMinDist) / (gMaxDist - gMinDist));
return pow(2, (lerp(gMaxTess, gMinTess, s))); // 지수 함수로 부드러운 감소
}
// 박스가 프러스텀 완전히 밖에 있는지 검사
bool AabbOutsideFrustumTest(float3 center, float3 extents, float4 frustumPlanes[6])
{
for (int i = 0; i < 6; ++i)
{
// 6개 프러스텀 평면 중 하나라도 완전히 뒤에 있으면
// 박스는 프러스텀 밖에 있는 것
if (AabbBehindPlaneTest(center, extents, frustumPlanes[i]))
{
return true;
}
}
return false;
}
// 박스가 평면의 뒤쪽(음의 반공간)에 완전히 있는지 검사
bool AabbBehindPlaneTest(float3 center, float3 extents, float4 plane)
{
// 평면 법선의 절대값 (방향만 중요)
float3 n = abs(plane.xyz);
// 박스의 "반지름" 계산 (법선 방향으로의 투영 길이)
float r = dot(extents, n);
// 박스 중심에서 평면까지의 부호있는 거리
float s = dot(float4(center, 1.0f), plane);
// 박스의 가장 가까운 점이 평면 뒤에 있으면 완전히 밖에 있는 것
return (s + r) < 0.0f;
}
프러스텀 컬링: 패치의 AABB가 시야 밖이면 테셀레이션 인자를 0으로 설정
거리 기반 LOD: 카메라와의 거리로 테셀레이션 수준 결정
에지별 테셀레이션: 각 에지마다 별도 인자 계산 (이웃 패치와 공유 에지의 일관성 유지)
CalcTessFactor 함수 - 거리 기반 LOD
p: 테셀레이션 수준을 결정할 점 (패치의 에지 중점이나 중심)
cameraPosition: 카메라의 현재 위치
gMinDist: 최대 테셀레이션이 적용될 최소 거리 (이보다 가까우면 항상 최대)
gMaxDist: 최소 테셀레이션이 적용될 최대 거리 (이보다 멀면 항상 최소)
gMinTess: 로그 스케일의 최소 테셀레이션 인자 (예: 2.0 → 2^2 = 4)
gMaxTess: 로그 스케일의 최대 테셀레이션 인자 (예: 6.0 → 2^6 = 64)
카메라와의 거리 d를 [0,1] 범위로 정규화
saturate(): 값을 0~1 사이로 클램핑
s = 0: 카메라가 gMinDist 이내에 있음 (최대 테셀레이션)
s = 1: 카메라가 gMaxDist 이상 떨어짐 (최소 테셀레이션)
평면과 점의 거리:
평면 방정식 ax + by + cz + d = 0에서, 점(x,y,z)에서 평면까지의 부호있는 거리는 (ax + by + cz + d) / || (a,b,c) ||
박스의 투영 반경: 평면 법선 방향으로 바운딩 박스가 차지하는 최대 반경을 계산.
포함/배제 테스트:
박스 중심에서 평면까지의 거리 = s
박스의 평면 방향 투영 반경 = r
만약 (s + r < 0)이면 박스는 평면 "뒤쪽"에 완전히 위치
Hull Shader
struct HullOut
{
float3 PosW : POSITION;
float2 Tex : TEXCOORD0;
};
[domain("quad")] // 사각형 패치 사용
[partitioning("fractional_even")] // 균일한 분할 방식 (균열 방지)
[outputtopology("triangle_cw")] // 시계방향 삼각형 생성
[outputcontrolpoints(4)] // 4개 제어점 출력 (사각형)
[patchconstantfunc("ConstantHS")] // 패치 상수 함수 지정
[maxtessfactor(64.0f)] // 최대 테셀레이션 인자
HullOut HS(InputPatch<VertexOut, 4> p,
uint i : SV_OutputControlPointID,
uint patchId : SV_PrimitiveID)
{
HullOut hout;
// Pass through shader - 값 그대로 전달
hout.PosW = p[i].PosW;
hout.Tex = p[i].Tex;
return hout;
}
테셀레이션 설정: 속성 지정자로 테셀레이터 단계 구성
[domain("quad")]: 사각형 패치 사용
[partitioning("fractional_even")]: 부드러운 LOD 전환을 위한 균일 분할
[maxtessfactor(64.0f)]: 최대 64×64 테셀레이션 가능
패스스루: 입력 제어점의 데이터를 그대로 출력에 전달
테셀레이터 단계 (하드웨어 고정 기능)
[domain("quad")] // 패치 타입: 사각형
[partitioning("fractional_even")] // 분할 방식: 균일 분할
[outputtopology("triangle_cw")] // 출력 위상: 시계방향 삼각형
[maxtessfactor(64.0f)] // 최대 분할 수준: 64
패치를 ConstantHS 함수에서 정한 인자에 따라 UV 공간에서 분할
예를 들어 Edge[0]=Edge[1]=Inside[0]=Inside[1]=16이면 16×16 그리드 생성
각 새로운 정점에 대해 SV_DomainLocation(UV 좌표)만 계산하고 DS로 전달
DS(도메인 셰이더) 단계
struct DomainOut
{
float4 PosH : SV_POSITION; // 클립 공간 위치
float3 PosW : POSITION; // 월드 공간 위치
float4 LPosH : LIGHT_POSITION; // 라이트 공간 위치
float2 Tex : TEXCOORD0; // 텍스처 좌표
float2 TiledTex : TEXCOORD1; // 타일링된 텍스처 좌표
};
// 테셀레이터가 생성한 모든 새 정점에 대해 호출되는 도메인 셰이더
[domain("quad")]
DomainOut DS(PatchTess patchTess,
float2 uv : SV_DomainLocation, // 테셀레이터가 생성한 UV 좌표
const OutputPatch<HullOut, 4> quad) // 패치의 4개 제어점
{
DomainOut dout;
// 1. 바이리니어 보간으로 월드 위치 계산
dout.PosW = lerp(
lerp(quad[0].PosW, quad[1].PosW, uv.x), // 상단 에지 보간
lerp(quad[2].PosW, quad[3].PosW, uv.x), // 하단 에지 보간
uv.y); // 상하 보간 결과 사이 보간
// 2. 텍스처 좌표 보간
dout.Tex = lerp(
lerp(quad[0].Tex, quad[1].Tex, uv.x),
lerp(quad[2].Tex, quad[3].Tex, uv.x),
uv.y);
// 3. 타일링된 텍스처 좌표 계산 (마테리얼 확대)
dout.TiledTex = dout.Tex * float2(gTexScale, gTexScale);
// 4. 하이트맵에서 샘플링하여 정확한 높이 설정 - 핵심!
dout.PosW.y = gHeightMap.SampleLevel(samHeightmap, dout.Tex, 0).r;
// 5. 월드 위치를 클립 공간으로 변환
dout.PosH = mul(mul(float4(dout.PosW, 1.0f), viewMatrix), projectionMatrix);
// 6. 그림자 계산을 위한 라이트 공간 위치 계산
dout.LPosH = mul(mul(float4(dout.PosW, 1.0f), light_viewMatrix), light_projectionMatrix);
return dout;
}
바이리니어 보간: 테셀레이터가 생성한 UV 좌표에 대해 4개 제어점 사이를 보간하여 3D 위치 계산
디스플레이스먼트 매핑: 하이트맵에서 정확한 높이를 샘플링하여 Y좌표 설정
좌표 변환: 최종 월드 공간 위치를 클립 공간과 라이트 공간으로 변환
텍스처 좌표 계산: 기본 UV와 타일링된 UV 좌표 계산
PS 단계
법선 벡터 계산 (하이트맵 기반)
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);
법선 계산 원리:
중앙 차분법(Central Difference): 현재 픽셀의 좌/우/위/아래 지점의 높이를 샘플링하여 높이 변화 계산
접선 벡터(Tangent): X축 방향의 높이 변화로 계산 (2*간격, 오른쪽높이-왼쪽높이, 0)
종법선 벡터(Bitangent): Z축 방향의 높이 변화로 계산 (0, 아래높이-위높이, -2*간격)
법선 벡터: 접선과 종법선의 외적으로 계산 (표면에 수직인 벡터)
다중 텍스처 레이어 블렌딩
// 레이어 텍스처 배열에서 샘플링 (5개 레이어)
float4 c0 = gLayerMapArray.Sample(sampler0, float3(pin.TiledTex, 0.0f));
float4 c1 = gLayerMapArray.Sample(sampler0, float3(pin.TiledTex, 1.0f));
float4 c2 = gLayerMapArray.Sample(sampler0, float3(pin.TiledTex, 2.0f));
float4 c3 = gLayerMapArray.Sample(sampler0, float3(pin.TiledTex, 3.0f));
float4 c4 = gLayerMapArray.Sample(sampler0, float3(pin.TiledTex, 4.0f));
// 블렌드맵 샘플링
float4 t = gBlendMap.Sample(sampler0, pin.Tex);
// 레이어 텍스처 혼합
float4 texColor = c0;
texColor = lerp(texColor, c1, t.r);
texColor = lerp(texColor, c2, t.g);
texColor = lerp(texColor, c3, t.b);
texColor = lerp(texColor, c4, t.a);
텍스처 레이어 블렌딩 원리:
레이어 텍스처 배열: 5개의 서로 다른 지형 텍스처(잔디, 흙, 돌 등)를 2D 텍스처 배열로 관리
타일링된 텍스처 좌표: pin.TiledTex는 텍스처 반복을 위해 기본 UV에 배율(gTexScale)을 적용한 좌표
블렌드맵: RGBA 4채널 텍스처로, 각 채널이 특정 레이어의 혼합 비율을 결정
선형 보간(lerp): 블렌드맵의 값에 따라 텍스처를 순차적으로 혼합
첫 번째 텍스처(c0)를 기본으로 시작
레드 채널(t.r)로 두 번째 텍스처(c1) 혼합
그린 채널(t.g)로 세 번째 텍스처(c2) 혼합
블루 채널(t.b)로 네 번째 텍스처(c3) 혼합
알파 채널(t.a)로 다섯 번째 텍스처(c4) 혼합
Last updated