Thinking Different




Terrain 02 - 높이 맵



원문 : http://www.rastertek.com/tertut02.html



 이 튜토리얼에서는 DirectX 11 및 C ++를 사용하여 3D로 지형을 표현하기 위한 높이 맵을 구현하는 방법에 대해 설명합니다. 이 듀토리얼의 코드는 이전 지형 듀토리얼을 기반으로 합니다.


높이 맵은 파일에 저장된 높이 포인트 매핑입니다. 높이 맵을 저장하는 가장 일반적인 방법은 비트맵, 원시, 텍스트 또는 이진 파일을 사용하는데 그 원리는 지형의 가장 낮은 높이가 0이고 최대 높이가 255 인 0-255의 값을 사용하여 지형의 높이를 저장하는 것입니다. 그레이 스케일 비트맵과 .raw 파일은 회색 색상의 밝기를 사용하여 높이를 나타낼 수 있기 때문에 높이 맵에 가장 적합합니다. 이것은 그리기 프로그램이나 수학적 알고리즘으로 편집하고 조작하기가 매우 쉽습니다.


높이 맵을 생성하는 가장 좋은 방법은 Perlin Noise 알고리즘을 사용하고 그 결과를 비트맵 파일에 저장하는 것입니다. 자신의 Perlin Noise 생성기를 작성하거나 이미 존재하는 Perlin Noise 생성기를 사용할 수 있습니다.


Perlin Noise generator를 사용하여 간단한 높이 맵을 다음과 같이 생성했습니다.




그 후 보통 다음과 같이 평평한 표면 / 언덕 스타일 지형을 생성하기 위해 Perlin Noise 이미지에 지수 알고리즘을 실행합니다.




마지막으로 다음과 같이 지형의 블록화 되지 않은 버전을 만들기 위해 이웃 픽셀을 평균화하는 평활화 프로세스를 실행합니다.




그런 다음 해당 이미지를 비트맵 형식으로 저장하고 이를 내 지형 높이 맵에 사용합니다. 비트맵은 지형 엔진에 로드되고 지도의 각 지점을 사용하여 삼각형으로 표시되는 지형 메쉬를 만듭니다.


이 튜토리얼에서는 이전 튜토리얼의 격자 코드를 새로운 높이 맵 코드로 바꿀 것입니다. 코드는 높이 맵에서 읽은 다음 그리드의 각 점을 높이 맵 높이와 일치하도록 높이기 위해 변경됩니다. 이렇게 하기 위해서는 간단히 TerrainClass 코드만 변경하면 됩니다.



Terrainclass.h


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#pragma once
 
class TerrainClass
{
private:
    struct VertexType
    {
        XMFLOAT3 position;
        XMFLOAT4 color;
    };
 
    struct HeightMapType 
    { 
        float x, y, z;
    };
 
public:
    TerrainClass();
    TerrainClass(const TerrainClass&);
    ~TerrainClass();
 
    bool Initialize(ID3D11Device*const char*);
    void Shutdown();
    void Render(ID3D11DeviceContext*);
 
    int GetIndexCount();
 
private:
    bool LoadHeightMap(const char*);
    void NormalizeHeightMap();
    void ShutdownHeightMap();
 
    bool InitializeBuffers(ID3D11Device*);
    void ShutdownBuffers();
    void RenderBuffers(ID3D11DeviceContext*);
    
private:
    int m_terrainWidth = 0;
    int m_terrainHeight = 0;
    int m_vertexCount = 0;
    int m_indexCount = 0;
    ID3D11Buffer* m_vertexBuffer = nullptr;
    ID3D11Buffer* m_indexBuffer = nullptr;
    HeightMapType* m_heightMap = nullptr;
};
cs



Terrainclass.cpp


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
#include "stdafx.h"
#include "terrainclass.h"
#include <stdio.h>
 
 
TerrainClass::TerrainClass()
{
}
 
 
TerrainClass::TerrainClass(const TerrainClass& other)
{
}
 
 
TerrainClass::~TerrainClass()
{
}
 
 
bool TerrainClass::Initialize(ID3D11Device* device, const char* heightMapFilename)
{
    // 지형의 높이 맵을 로드합니다.
    if(!LoadHeightMap(heightMapFilename))
    {
        return false;
    }
 
    // 높이 맵의 높이를 표준화합니다.
    NormalizeHeightMap();
 
    // 지형에 대한 지오 메트릭을 포함하는 정점 및 인덱스 버퍼를 초기화합니다.
    return InitializeBuffers(device);
}
 
 
void TerrainClass::Shutdown()
{
    // 버텍스와 인덱스 버퍼를 해제합니다.
    ShutdownBuffers();
 
    // 높이맵 데이터를 해제합니다.
    ShutdownHeightMap();
}
 
 
void TerrainClass::Render(ID3D11DeviceContext* deviceContext)
{
    // 그리기를 준비하기 위해 그래픽 파이프 라인에 꼭지점과 인덱스 버퍼를 놓습니다.
    RenderBuffers(deviceContext);
}
 
 
int TerrainClass::GetIndexCount()
{
    return m_indexCount;
}
 
 
bool TerrainClass::LoadHeightMap(const char* filename)
{
    // 바이너리 모드로 높이맵 파일을 엽니다.
    FILE* filePtr = nullptr;
    if(fopen_s(&filePtr, filename, "rb"!= 0)
    {
        return false;
    }
 
    // 파일 헤더를 읽습니다.
    BITMAPFILEHEADER bitmapFileHeader;
    if(fread(&bitmapFileHeader, sizeof(BITMAPFILEHEADER), 1, filePtr) != 1)
    {
        return false;
    }
 
    // 비트맵 정보 헤더를 읽습니다.
    BITMAPINFOHEADER bitmapInfoHeader;
    if(fread(&bitmapInfoHeader, sizeof(BITMAPINFOHEADER), 1, filePtr) != 1)
    {
        return false;
    }
 
    // 지형의 크기를 저장합니다.
    m_terrainWidth = bitmapInfoHeader.biWidth;
    m_terrainHeight = bitmapInfoHeader.biHeight;
 
    // 비트맵 이미지 데이터의 크기를 계산합니다.
    int imageSize = m_terrainWidth * m_terrainHeight * 3;
 
    // 비트맵 이미지 데이터에 메모리를 할당합니다.
    unsigned char* bitmapImage = new unsigned char[imageSize];
    if(!bitmapImage)
    {
        return false;
    }
 
    // 비트맵 데이터의 시작 부분으로 이동합니다.
    fseek(filePtr, bitmapFileHeader.bfOffBits, SEEK_SET);
 
    // 비트맵 이미지 데이터를 읽습니다.
    if(fread(bitmapImage, 1, imageSize, filePtr) != imageSize)
    {
        return false;
    }
 
    // 파일을 닫습니다.
    if(fclose(filePtr) != 0)
    {
        return false;
    }
 
    // 높이 맵 데이터를 저장할 구조체를 만듭니다.
    m_heightMap = new HeightMapType[m_terrainWidth * m_terrainHeight];
    if(!m_heightMap)
    {
        return false;
    }
 
    // 이미지 데이터 버퍼의 위치를 ​​초기화합니다.
    int k = 0;
 
    // 이미지 데이터를 높이 맵으로 읽어들입니다.
    for(int j=0; j<m_terrainHeight; j++)
    {
        for(int i=0; i<m_terrainWidth; i++)
        {
            unsigned char height = bitmapImage[k];
            
            int index = (m_terrainHeight * j) + i;
 
            m_heightMap[index].x = (float)i;
            m_heightMap[index].y = (float)height;
            m_heightMap[index].z = (float)j;
 
            k+=3;
        }
    }
 
    // 비트맵 이미지 데이터를 해제합니다.
    delete [] bitmapImage;
    bitmapImage = 0;
 
    return true;
}
 
 
void TerrainClass::NormalizeHeightMap()
{
    for(int j=0; j<m_terrainHeight; j++)
    {
        for(int i=0; i<m_terrainWidth; i++)
        {
            m_heightMap[(m_terrainHeight * j) + i].y /= 15.0f;
        }
    }
}
 
 
void TerrainClass::ShutdownHeightMap()
{
    if(m_heightMap)
    {
        delete [] m_heightMap;
        m_heightMap = 0;
    }
}
 
 
bool TerrainClass::InitializeBuffers(ID3D11Device* device)
{
    // 지형 메쉬의 정점 수를 계산합니다.
    m_vertexCount = (m_terrainWidth - 1* (m_terrainHeight - 1* 12;
 
    // 인덱스 수를 꼭지점 수와 같게 설정합니다.
    m_indexCount = m_vertexCount;
 
    // 정점 배열을 만듭니다.
    VertexType* vertices = new VertexType[m_vertexCount];
    if(!vertices)
    {
        return false;
    }
 
    // 인덱스 배열을 만듭니다.
    unsigned long* indices = new unsigned long[m_indexCount];
    if(!indices)
    {
        return false;
    }
 
    // 정점 배열에 대한 인덱스를 초기화합니다.
    int index = 0;
 
    // 지형 데이터로 정점 및 인덱스 배열을 로드합니다.
    for(int j=0; j<(m_terrainHeight-1); j++)
    {
        for(int i=0; i<(m_terrainWidth-1); i++)
        {
            int index1 = (m_terrainHeight * j) + i;          // 왼쪽 아래.
            int index2 = (m_terrainHeight * j) + (i+1);      // 오른쪽 아래.
            int index3 = (m_terrainHeight * (j+1)) + i;      // 왼쪽 위.
            int index4 = (m_terrainHeight * (j+1)) + (i+1);  // 오른쪽 위.
 
            // 왼쪽 위.
            vertices[index].position = XMFLOAT3(m_heightMap[index3].x, m_heightMap[index3].y, m_heightMap[index3].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 오른쪽 위.
            vertices[index].position = XMFLOAT3(m_heightMap[index4].x, m_heightMap[index4].y, m_heightMap[index4].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 오른쪽 위.
            vertices[index].position = XMFLOAT3(m_heightMap[index4].x, m_heightMap[index4].y, m_heightMap[index4].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 왼쪽 아래.
            vertices[index].position = XMFLOAT3(m_heightMap[index1].x, m_heightMap[index1].y, m_heightMap[index1].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 왼쪽 아래.
            vertices[index].position = XMFLOAT3(m_heightMap[index1].x, m_heightMap[index1].y, m_heightMap[index1].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 왼쪽 위.
            vertices[index].position = XMFLOAT3(m_heightMap[index3].x, m_heightMap[index3].y, m_heightMap[index3].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 왼쪽 아래.
            vertices[index].position = XMFLOAT3(m_heightMap[index1].x, m_heightMap[index1].y, m_heightMap[index1].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 오른쪽 위.
            vertices[index].position = XMFLOAT3(m_heightMap[index4].x, m_heightMap[index4].y, m_heightMap[index4].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 오른쪽 위.
            vertices[index].position = XMFLOAT3(m_heightMap[index4].x, m_heightMap[index4].y, m_heightMap[index4].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 오른쪽 아래.
            vertices[index].position = XMFLOAT3(m_heightMap[index2].x, m_heightMap[index2].y, m_heightMap[index2].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 오른쪽 아래.
            vertices[index].position = XMFLOAT3(m_heightMap[index2].x, m_heightMap[index2].y, m_heightMap[index2].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;
 
            // 왼쪽 아래.
            vertices[index].position = XMFLOAT3(m_heightMap[index1].x, m_heightMap[index1].y, m_heightMap[index1].z);
            vertices[index].color = XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f);
            indices[index] = index;
            index++;            
        }
    }
 
    // 정적 정점 버퍼의 구조체를 설정한다.
    D3D11_BUFFER_DESC vertexBufferDesc;
    vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    vertexBufferDesc.ByteWidth = sizeof(VertexType) * m_vertexCount;
    vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
    vertexBufferDesc.CPUAccessFlags = 0;
    vertexBufferDesc.MiscFlags = 0;
    vertexBufferDesc.StructureByteStride = 0;
 
    // subresource 구조에 정점 데이터에 대한 포인터를 제공합니다.
    D3D11_SUBRESOURCE_DATA vertexData;
    vertexData.pSysMem = vertices;
    vertexData.SysMemPitch = 0;
    vertexData.SysMemSlicePitch = 0;
 
    // 이제 정점 버퍼를 만듭니다.
    if(FAILED(device->CreateBuffer(&vertexBufferDesc, &vertexData, &m_vertexBuffer)))
    {
        return false;
    }
 
    // 정적 인덱스 버퍼의 구조체를 설정합니다.
    D3D11_BUFFER_DESC indexBufferDesc;
    indexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
    indexBufferDesc.ByteWidth = sizeof(unsigned long* m_indexCount;
    indexBufferDesc.BindFlags = D3D11_BIND_INDEX_BUFFER;
    indexBufferDesc.CPUAccessFlags = 0;
    indexBufferDesc.MiscFlags = 0;
    indexBufferDesc.StructureByteStride = 0;
 
    // 하위 리소스 구조에 인덱스 데이터에 대한 포인터를 제공합니다.
    D3D11_SUBRESOURCE_DATA indexData;
    indexData.pSysMem = indices;
    indexData.SysMemPitch = 0;
    indexData.SysMemSlicePitch = 0;
 
    // 인덱스 버퍼를 만듭니다.
    if(FAILED(device->CreateBuffer(&indexBufferDesc, &indexData, &m_indexBuffer)))
    {
        return false;
    }
 
    // 이제 버퍼가 생성되고 로드된 배열을 해제하십시오.
    delete [] vertices;
    vertices = 0;
 
    delete [] indices;
    indices = 0;
 
    return true;
}
 
 
void TerrainClass::ShutdownBuffers()
{
    // 인덱스 버퍼를 해제합니다.
    if(m_indexBuffer)
    {
        m_indexBuffer->Release();
        m_indexBuffer = 0;
    }
 
    // 버텍스 버퍼를 해제합니다.
    if(m_vertexBuffer)
    {
        m_vertexBuffer->Release();
        m_vertexBuffer = 0;
    }
}
 
 
void TerrainClass::RenderBuffers(ID3D11DeviceContext* deviceContext)
{
    // 정점 버퍼 보폭 및 오프셋을 설정합니다.
    unsigned int stride = sizeof(VertexType); 
    unsigned int offset = 0;
    
    // 렌더링 할 수 있도록 입력 어셈블러에서 정점 버퍼를 활성으로 설정합니다.
    deviceContext->IASetVertexBuffers(01&m_vertexBuffer, &stride, &offset);
 
    // 렌더링 할 수 있도록 입력 어셈블러에서 인덱스 버퍼를 활성으로 설정합니다.
    deviceContext->IASetIndexBuffer(m_indexBuffer, DXGI_FORMAT_R32_UINT, 0);
 
    // 이 버텍스 버퍼에서 렌더링되어야하는 프리미티브의 타입을 설정한다.이 경우 라인리스트이다.
    deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
}
cs





이 듀토리얼에서는 ApplicationClass의 헤더가 변경되지 않았습니다. ApplicationClass.cpp 파일의 유일한 변경 사항은 지형 초기화 함수에 지형 높이 맵을 나타내는데 사용되는 비트맵의 파일 이름이 필요하다는 것입니다.


Applicationclass.cpp


1
2
3
4
5
6
7
8
9
bool ApplicationClass::Initialize(HINSTANCE hinstance, HWND hwnd, int screenWidth, int screenHeight)
{
    // ...
 
    // 지형 객체를 초기화 합니다.
    result = m_Terrain->Initialize(m_Direct3D->GetDevice(), "../Dx11Terrain_02/data/heightmap01.bmp");
 
    // ...
}
cs




출력 화면




마치면서


우리는 이제 높이 맵을 로드하고 높이 맵 기반의 지형을 3D로 움직일 수 있습니다.



연습문제


1. 코드를 다시 컴파일하고 입력 키를 사용하여 지형을 따라 이동하십시오.


2. 자신만의 그레이 스케일 비트맵을 만들어 지형 엔진에 로드하여 해당 높이 맵을 3D로 봅니다.


3. .raw 형식으로 로드하도록 LoadHeightMap 함수를 변경하십시오.



소스코드


소스코드 : Dx11Terrain_02.zip