# Terrain

{% embed url="<https://youtu.be/6aocVCtYH0Q>" %}

## <mark style="background-color:yellow;">Terrain 생성 과정 요약</mark>

* **데이터 준비 단계**

  * 하이트맵 로드: 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으로 저장하여 메모리 사용량 절반으로 감소

## <mark style="background-color:yellow;">하이트맵 로드</mark>

* **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)의 높이 정보를 담고 있습니다.

여기서 주목할 설정값:

1. heightMapFilename: 하이트맵 RAW 파일 경로
2. heightScale: 50.0f - 이 값에 의해 RAW 파일의 0-255 값이 0-50 단위의 실제 높이로 변환됨
3. heightmapWidth/Height: 2049x2049 - 하이트맵의 해상도
4. cellSpacing: 0.5f - 하이트맵의 각 셀 간격(월드 단위)

결과적으로, 이 과정을 통해 2049x2049 크기의 RAW 하이트맵 데이터가 0\~50 범위의 실제 높이값을 가진 float 배열로 메모리에 로드됩니다.

## <mark style="background-color:yellow;">스무딩</mark>

* **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;
}
```

1. Smooth() 함수는 높이맵과 같은 크기의 새 배열(dest)을 생성합니다.
   1. 모든 높이맵 좌표(i,j)에 대해:
      1. Average(i,j) 함수를 호출하여 해당 위치와 주변 픽셀의 평균 계산
         1. 계산된 평균값을 dest 배열의 같은 위치에 저장
         2. 모든 처리가 끝나면 원본 높이맵을 새로 계산된 값으로 완전히 교체합니다

* **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 영역을 순회합니다.

1. 각 픽셀이 유효한 범위인지 InBounds()로 검사하고, 유효한 경우에만(HeightMap을 벗어나지 않도록):
   1. 픽셀의 높이값을 avg에 누적
   2. 카운터 num을 1 증가
2. 모든 유효 픽셀의 높이 합을 픽셀 수로 나누어 평균값 반환

## <mark style="background-color:yellow;">패치 구성</mark>

* **패치 구조 초기화**

```
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();  // 패치 정점 버퍼 생성
    
    // ... 이후 코드 ...
}
```

패치 구성 계산:

1. CellsPerPatch = 64: 각 패치는 64x64 크기의 셀을 담당
2. (heightmapHeight-1) / CellsPerPatch + 1: 높이맵 치수를 패치 크기로 나누고 1을 더함
   1. (2049 - 1) / 64 + 1 = 32 + 1 = 33
3. 따라서 33 x 33 = 1089개의 패치 정점이 생성됨
4. 실제 패치 면은 (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());
}
```

1. 33 x 33개의 패치 정점을 위한 배열 생성
2. 각 정점의 XZ 위치를 계산 ( (-512, -512)부터 (512, 512)까지 )
3. 각 정점의 UV 좌표 계산 ( (0, 0)부터 (1, 1)까지 )
4. 각 패치의 바운딩 정보(높이 범위)를 패치의 왼쪽 상단 정점에 저장
5. 인덱스 버퍼를 생성하여 정점들이 패치를 형성하도록 구성

* **패치 인덱스 버퍼 생성**

```
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;
}
```

1. 32x32=1024개의 사각형 패치를 생성하기 위한, 총 4096개의 인덱스를 담은 배열 생성
2. 각 패치는 정확히 4개의 인덱스로 구성 (좌상단, 우상단, 좌하단, 우하단)
3. 이 패치들이 테셀레이션의 기본 단위가 됨

* **패치 구성 시각화**

```
   패치 제어점(33x33개)     ↓     패치(32x32개)     ↓     테셀레이션
     ● ● ● ● ●               ┌─┬─┬─┬─┐             세밀한 메시로 변환
     ● ● ● ● ●               ├─┼─┼─┼─┤
     ● ● ● ● ●       →       ├─┼─┼─┼─┤     →       /\/\/\/\/\/\
     ● ● ● ● ●               ├─┼─┼─┼─┤             /\/\/\/\/\/\
     ● ● ● ● ●               └─┴─┴─┴─┘             /\/\/\/\/\/\
     
         패치 정점            월드 좌표 범위
     [0]------[1]       (-512,512)----(512,512)
      |        |            |            |
      |  패치   |            |            |
      |        |            |            |
     [2]------[3]       (-512,-512)----(512,-512)
```

1. 분할 방식: 2049x2049 하이트맵을 64셀 단위로 나눔
   1. 실제 계산: (2049-1) / 64 + 1 = 33
   2. 33x33개의 패치 제어점과 32x32개의 실제 패치 생성
2. 공간 매핑:
   1. 월드 공간: (-512,-512)부터 (512,512)까지 1024x1024 크기
   2. 패치 크기: 약 32x32 월드 단위
   3. UV 공간: 전체 지형이 (0,0)부터 (1,1)까지 매핑

## <mark style="background-color:yellow;">바운딩 정보 계산</mark>

* 모든 패치의 바운딩 정보 계산

```
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);
        }
    }
}
```

1. 32x32=1024개의 패치에 대한 바운딩 정보를 저장할 벡터 준비
2. 모든 패치에 대해 반복 루프를 실행하며 각 패치의 바운딩 정보 계산

* **각 패치의 높이 범위 계산**

```
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에 최대값 저장
}
```

1. 패치의 인덱스(i,j)를 하이트맵 좌표 범위로 변환 (CellsPerPatch=64를 곱함)
2. 이 범위 내의 모든 하이트맵 높이값을 검사하여 최소/최대값 찾기
3. 찾은 최소/최대 높이를 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 계산에 사용할 수 있습니다.

## <mark style="background-color:yellow;">하이트맵 SRV</mark>

```
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로 변환<br>

    ```
    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로 변환

## <mark style="background-color:yellow;">VS 단계</mark>

* **버텍스 셰이더 입력/출력 구조체**

```
// 버텍스 셰이더 입력 구조체
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)
};
```

1. VertexIn: BuildQuadPatchVB에서 생성된 정점 버퍼의 데이터 형식
2. VertexOut: VS가 처리한 결과를 헐 셰이더로 전달하는 데이터 형식

* **상수 버퍼 (필요한 변환 행렬과 데이터)**

```
// 월드 변환 행렬
cbuffer TransformBuffer : register(b6)
{
    row_major matrix worldMatrix;
    row_major matrix worldInvTranspose;
}

// 하이트맵과 샘플러 선언
SamplerState samHeightmap : register(s2);
Texture2D gHeightMap : register(t2);
```

1. worldMatrix: 로컬 좌표를 월드 좌표로 변환하는 행렬
2. gHeightMap: BuildHeightmapSRV에서 생성된 하이트맵 텍스처
3. 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;
}
```

1. 정점의 로컬 좌표(vin.PosL)에 월드 변환 행렬을 곱해 월드 좌표로 변환
2. 이 시점에서는 Y좌표(높이)가 아직 정확하지 않음
3. 정점의 텍스처 좌표(vin.Tex)를 사용해 하이트맵의 해당 위치 샘플링
4. .r은 R 채널만 사용(하이트맵은 단일 채널 높이값만 저장)
5. 이 단계에서 제어점의 정확한 높이가 설정됨
6. 입력 텍스처 좌표를 그대로 출력 구조체로 전달
7. 패치의 높이 범위 정보(BoundsY)도 그대로 전달
8. 이 정보들은 이후 HS 단계에서 테셀레이션 인자 계산과 컬링에 사용됨

## <mark style="background-color:yellow;">HS 단계</mark>

### <mark style="background-color:blue;">**패치 상수 함수 (ConstantHS)**</mark>

```
// 테셀레이션 인자를 담는 구조체
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;
}
```

1. 프러스텀 컬링: 패치의 AABB가 시야 밖이면 테셀레이션 인자를 0으로 설정
2. 거리 기반 LOD: 카메라와의 거리로 테셀레이션 수준 결정
3. 에지별 테셀레이션: 각 에지마다 별도 인자 계산 (이웃 패치와 공유 에지의 일관성 유지)

* **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 이상 떨어짐 (최소 테셀레이션)

* ### AabbBehindPlaneTest&#x20;

1. 평면과 점의 거리:&#x20;
   1. 평면 방정식 ax + by + cz + d = 0에서, 점(x,y,z)에서 평면까지의 부호있는 거리는 \
      (ax + by + cz + d) / || (a,b,c) ||
2. 박스의 투영 반경: 평면 법선 방향으로 바운딩 박스가 차지하는 최대 반경을 계산.
3. 포함/배제 테스트:
   1. 박스 중심에서 평면까지의 거리 = s
   2. 박스의 평면 방향 투영 반경 = r
   3. 만약 (s + r < 0)이면 박스는 평면 "뒤쪽"에 완전히 위치

### <mark style="background-color:blue;">Hull Shader</mark>

```
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;
}
```

1. 테셀레이션 설정: 속성 지정자로 테셀레이터 단계 구성
   1. \[domain("quad")]: 사각형 패치 사용
   2. \[partitioning("fractional\_even")]: 부드러운 LOD 전환을 위한 균일 분할
   3. \[maxtessfactor(64.0f)]: 최대 64×64 테셀레이션 가능
2. 패스스루: 입력 제어점의 데이터를 그대로 출력에 전달

## <mark style="background-color:yellow;">테셀레이터 단계 (하드웨어 고정 기능)</mark>

```
[domain("quad")]               // 패치 타입: 사각형
[partitioning("fractional_even")] // 분할 방식: 균일 분할
[outputtopology("triangle_cw")] // 출력 위상: 시계방향 삼각형
[maxtessfactor(64.0f)]         // 최대 분할 수준: 64
```

1. 패치를 ConstantHS 함수에서 정한 인자에 따라 UV 공간에서 분할
2. 예를 들어 Edge\[0]=Edge\[1]=Inside\[0]=Inside\[1]=16이면 16×16 그리드 생성
3. 각 새로운 정점에 대해 SV\_DomainLocation(UV 좌표)만 계산하고 DS로 전달

## <mark style="background-color:yellow;">DS(도메인 셰이더) 단계</mark>

```
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;
}
```

1. 바이리니어 보간: 테셀레이터가 생성한 UV 좌표에 대해 4개 제어점 사이를 보간하여 3D 위치 계산
2. 디스플레이스먼트 매핑: 하이트맵에서 정확한 높이를 샘플링하여 Y좌표 설정
3. 좌표 변환: 최종 월드 공간 위치를 클립 공간과 라이트 공간으로 변환
4. 텍스처 좌표 계산: 기본 UV와 타일링된 UV 좌표 계산

## <mark style="background-color:yellow;">PS 단계</mark>

* **법선 벡터 계산 (하이트맵 기반)**

```
    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);
```

#### 법선 계산 원리:

1. 중앙 차분법(Central Difference): 현재 픽셀의 좌/우/위/아래 지점의 높이를 샘플링하여 높이 변화 계산
2. 접선 벡터(Tangent): X축 방향의 높이 변화로 계산 (2\*간격, 오른쪽높이-왼쪽높이, 0)
3. 종법선 벡터(Bitangent): Z축 방향의 높이 변화로 계산 (0, 아래높이-위높이, -2\*간격)
4. 법선 벡터: 접선과 종법선의 외적으로 계산 (표면에 수직인 벡터)

* **다중 텍스처 레이어 블렌딩**

```
    // 레이어 텍스처 배열에서 샘플링 (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);
```

#### 텍스처 레이어 블렌딩 원리:

1. 레이어 텍스처 배열: 5개의 서로 다른 지형 텍스처(잔디, 흙, 돌 등)를 2D 텍스처 배열로 관리
2. 타일링된 텍스처 좌표: pin.TiledTex는 텍스처 반복을 위해 기본 UV에 배율(gTexScale)을 적용한 좌표
3. 블렌드맵: RGBA 4채널 텍스처로, 각 채널이 특정 레이어의 혼합 비율을 결정
4. 선형 보간(lerp): 블렌드맵의 값에 따라 텍스처를 순차적으로 혼합
   1. 첫 번째 텍스처(c0)를 기본으로 시작
   2. 레드 채널(t.r)로 두 번째 텍스처(c1) 혼합
   3. 그린 채널(t.g)로 세 번째 텍스처(c2) 혼합
   4. 블루 채널(t.b)로 네 번째 텍스처(c3) 혼합
   5. 알파 채널(t.a)로 다섯 번째 텍스처(c4) 혼합


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://jihoon-jungs-organization.gitbook.io/jihoon_engine/feature-and-description/terrain.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
