수업
🧭 주제
- 이 강의의 주제는 8비트 HeightMap 이미지를 기반으로 지형의 고도를 표현하는 3D Mesh 생성 기법입니다.
- 이미지의 픽셀 데이터를 CPU에서 직접 파싱하고, 해당 값을 정점의 Y좌표로 보정하여 울퉁불퉁한 지형을 구성하는 실습입니다.
- 이후에는 이 기법을 기반으로 Tessellation Shader, Collision, Tool 연동 등 다양한 시스템으로 확장할 수 있습니다.
📘 개념
| 개념 | 설명 |
|---|
| HeightMap | 흑백 이미지의 픽셀 밝기값(R)으로 지형의 높이를 표현하는 방식 |
| 픽셀 파싱 | 이미지 파일을 메모리에 로드한 후, 픽셀 데이터를 접근해 정점 Y좌표로 변환 |
| Grid Geometry | XZ 평면 상의 정점 배열을 기반으로 한 격자형 Mesh |
| CPU 기반 보정 | 높이맵 데이터를 기반으로 정점 위치를 CPU에서 수정한 후 버퍼에 반영 |
| Shader 연동 | 정점 데이터를 HLSL 셰이더로 넘겨 World/View/Projection 변환과 텍스처 샘플링 수행 |
📑 용어정리
| 용어 | 의미 |
|---|
ScratchImage | DirectXTex에서 사용하는 이미지 컨테이너. 픽셀 데이터 접근 가능 |
uint8* pixelBuffer | 픽셀 데이터가 담긴 버퍼 포인터 (한 픽셀 = 1바이트) |
CreateGrid() | 주어진 너비와 높이에 맞춰 정점과 인덱스 배열 생성 |
VertexTextureData | position + uv 정보를 가진 정점 구조체 |
DrawIndexed() | 인덱스 기반으로 GPU에서 정점 데이터를 그리는 함수 |
Wrap | 텍스처 좌표가 범위를 넘을 때 반복 적용하는 방식 (UV 처리) |
🔍 코드 분석
✅ HeightMapDemo::Init()
1. HeightMap 이미지 로드
_heightMap = RESOURCES->Load<Texture>(L"Height", L"..\\Resources\\Textures\\Terrain\\height.png");
_texture = RESOURCES->Load<Texture>(L"Grass", L"..\\Resources\\Textures\\Terrain\\grass.jpg");
Load() 함수로 리소스를 메모리에 로딩
- HeightMap은 8비트 grayscale로 저장된 height.png
2. 이미지 해상도 및 픽셀 데이터 획득
const int32 width = _heightMap->GetSize().x;
const int32 height = _heightMap->GetSize().y;
const DirectX::ScratchImage& info = _heightMap->GetInfo();
uint8* pixelBuffer = info.GetPixels();
- 이미지 해상도는 이후 격자(Grid) 생성 기준
GetPixels()는 픽셀 배열 반환 (R 값만 있음)
3. Grid 생성 및 정점 높이 수정
_geometry = make_shared<Geometry<VertexTextureData>>();
GeometryHelper::CreateGrid(_geometry, width, height);
vector<VertexTextureData>& v = const_cast<vector<VertexTextureData>&>(_geometry->GetVertices());
for (int32 z = 0; z < height; z++) {
for (int32 x = 0; x < width; x++) {
int32 idx = width * z + x;
float h = pixelBuffer[idx] / 255.f * 25.f;
v[idx].position.y = h;
}
}
- 픽셀 값을 정규화 후(0~1) 25를 곱해 고도로 변환
- 1픽셀 = 1정점에 매핑하여 Y좌표 보정
4. 정점/인덱스 버퍼 생성
_vertexBuffer = make_shared<VertexBuffer>();
_vertexBuffer->Create(_geometry->GetVertices());
_indexBuffer = make_shared<IndexBuffer>();
_indexBuffer->Create(_geometry->GetIndices());
- 보정된 정점과 인덱스를 GPU에 전달하기 위해 버퍼 생성
5. 카메라 위치 및 회전 설정
_camera = make_shared<GameObject>();
_camera->GetOrAddTransform();
_camera->AddComponent(make_shared<Camera>());
_camera->AddComponent(make_shared<CameraScript>());
_camera->GetTransform()->SetPosition(Vec3(0.f, 5.f, 0.f));
_camera->GetTransform()->SetRotation(Vec3(25.f, 0.f, 0.f));
- 카메라를 위로 올려 지형을 내려다볼 수 있게 설정
- 회전 각도는 X축으로 25도
✅ HeightMapDemo::Render()
_shader->GetMatrix("World")->SetMatrix((float*)&_world);
_shader->GetMatrix("View")->SetMatrix((float*)&Camera::S_MatView);
_shader->GetMatrix("Projection")->SetMatrix((float*)&Camera::S_MatProjection);
_shader->GetSRV("Texture0")->SetResource(_texture->GetComPtr().Get());
- 셰이더에 월드/뷰/프로젝션 행렬과 텍스처 설정
DC->IASetVertexBuffers(0, 1, _vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset);
DC->IASetIndexBuffer(_indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0);
_shader->DrawIndexed(0, 0, _indexBuffer->GetCount(), 0, 0);
✅ Terrain.fx 셰이더
VertexOutput VS(VertexInput input)
{
VertexOutput output;
output.position = mul(input.position, World);
output.position = mul(output.position, View);
output.position = mul(output.position, Projection);
output.uv = input.uv;
return output;
}
- 정점 → 월드 → 뷰 → 프로젝션 순으로 위치 변환
float4 PS(VertexOutput input) : SV_TARGET
{
return Texture0.Sample(Sampler0, input.uv);
}
RasterizerState FillModeWireFrame {
FillMode = Wireframe;
};
✅ 핵심
- HeightMap은 픽셀 밝기값을 높이값으로 사용하는 이미지 기반 지형 표현 방식이다.
- 픽셀 값은 0~255 정수형이며,
uint8* 배열로 접근해 정규화한 후 Y값으로 사용한다.
- Grid는 이미지의 해상도만큼 생성되며, 각 정점은 1:1로 픽셀에 대응된다.
- Y좌표 보정은 CPU에서 최초 1회만 수행되며, 이후 버퍼에 저장되어 GPU가 렌더링을 수행한다.
- 셰이더에서는 position 변환과 텍스처 샘플링만 수행하며, 필요한 경우 와이어프레임 시각화가 가능하다.
- 이 시스템은 후속적으로 GPU Tessellation Shader, 충돌 처리, LOD 시스템, 툴 연동 기반 Terrain 편집기로 확장할 수 있다.