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

여기서 주목할 설정값:

  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 배열로 메모리에 로드됩니다.

스무딩

  • 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. 모든 유효 픽셀의 높이 합을 픽셀 수로 나누어 평균값 반환

패치 구성

  • 패치 구조 초기화

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)까지 매핑

바운딩 정보 계산

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

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 계산에 사용할 수 있습니다.

하이트맵 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)
};
  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 단계에서 테셀레이션 인자 계산과 컬링에 사용됨

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

  1. 평면과 점의 거리:

    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)이면 박스는 평면 "뒤쪽"에 완전히 위치

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;
}
  1. 테셀레이션 설정: 속성 지정자로 테셀레이터 단계 구성

    1. [domain("quad")]: 사각형 패치 사용

    2. [partitioning("fractional_even")]: 부드러운 LOD 전환을 위한 균일 분할

    3. [maxtessfactor(64.0f)]: 최대 64×64 테셀레이션 가능

  2. 패스스루: 입력 제어점의 데이터를 그대로 출력에 전달

테셀레이터 단계 (하드웨어 고정 기능)

[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로 전달

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;
}
  1. 바이리니어 보간: 테셀레이터가 생성한 UV 좌표에 대해 4개 제어점 사이를 보간하여 3D 위치 계산

  2. 디스플레이스먼트 매핑: 하이트맵에서 정확한 높이를 샘플링하여 Y좌표 설정

  3. 좌표 변환: 최종 월드 공간 위치를 클립 공간과 라이트 공간으로 변환

  4. 텍스처 좌표 계산: 기본 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);

법선 계산 원리:

  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) 혼합

Last updated