В этом уроке мы научимся создавать поверхность — карту высот. Основа этого урока взята из урока «Камера: стрейф», к этой основе мы добавим нужный функционал.
Файлик Terrain.raw, наша карта высот.

Сначала мы создадим функционал для обработки и рисования поверхности.
Сначала создадим хидер; файл будет содержать константы и функции для обработки и отрисовки поверхности.

Файл terrain.h:

#ifndef _TERRAIN_H
#define _TERRAIN_H#define MAP_SIZE    1024    // Размер нашей .raw карты высот
#define STEP_SIZE   16  // Ширина и высота каждого составляющего карту квадрата
#define HEIGHT_RATIO    1.5f    // Отношение, с которым Y изменяется относительно X и Z// Возвращает высоту (0 — 255) из карты высот для переданных X и Y
int Height(BYTE *pHeightMap, int X, int Y);

// Загружает .raw файл в переменную с передаваемой длинной
void LoadRawFile(LPSTR strName, int nSize, BYTE *pHeightMap);

// Конвертирует данные карты высот в примитивы и отрисовывает их на экран
void RenderHeightMap(BYTE *pHeightMap);

#endif

 

Теперь опишем реализацию нужного функционала. Замечание: на экране поверхность может сейчас показаться маленькой, но не переживайте, она может быть и большой, и занимать весь ваш мир, если вы захотите =)

Итак, файл terrain.cpp:

#include «main.h»

///////////////////////////////// HEIGHT \\\\\\\\\\\\\\\\*
/////
/////   Возвращает высоту в карту высот
/////
///////////////////////////////// HEIGHT \\\\\\\\\\\\\\\\*

int Height(BYTE *pHeightMap, int X, int Y)
{

// Ф-я используется для индексации нашей карты высот.
// При работе с массивами всегда нужно убеждатся, что мы не выходим
// за пределы массива, так что убедимся, что этого не случилось, с a%.
// Таким образом X и Y будут в пределах (MAX_SIZE — 1).

int x = X % MAP_SIZE;   // Проверка для X
int y = Y % MAP_SIZE;   // Проверка для Y

if(!pHeightMap) return 0;   // Убедимся, что работаем с правильными данными

// Ниже нам нужно вернуть единичный — 2д-массив.
// Мы можем использовать формулу: index = (x + (y * arrayWidth) ).
// Это значит, что мы используем 2д-массив[x][y],
// иначе он будет противоположным. Теперь, имея верный индекс,
// вернём высоту для этого индекса.

return pHeightMap[x + (y * MAP_SIZE)];  // вернём высоту данного индекса
}

///////////////////////////////// SET VERTEX COLOR \\\\\\\\\\\\\\\\*
/////
/////   Устанавливает цветовое значение вершины, в зависимости от высоты и ширины
/////
///////////////////////////////// SET VERTEX COLOR \\\\\\\\\\\\\\\\*
void SetVertexColor(BYTE *pHeightMap, int x, int y)
{
if(!pHeightMap) return;     // Убедимся, что данные верны

// Здесь мы устанавливаем цвет вершины, основываясь на индексе её высоты.
// Чтобы сделать цвет темнее, я начал с -0.15f. Также мы получим
// значение от 0 до 1.0 делением высоту на 256.0f.
float fColor = 0.15f + (Height(pHeightMap, x, y ) / 256.0f);

// Применим нужный оттенок зеленого на текущую вершину
glColor3f(0, fColor, 0 );
}

///////////////////////////////// RENDER HEIGHT MAP \\\\\\\\\\\\\\\\*
/////
/////   Рендерит карту высот как множество квадратов.
/////
///////////////////////////////// RENDER HEIGHT MAP \\\\\\\\\\\\\\\\*

void RenderHeightMap(BYTE pHeightMap[])
{
int X = 0, Y = 0;       // Переменные для прохда по массиву
int x, y, z;            // Переменные для чтения
float fColor = 0.0f;        // Переменная для цвета полигона

if(!pHeightMap) return;     // Убедимся, что данные верны

glBegin( GL_QUADS );        // Начинаем рендерить квадраты

// Теперь нам нужно собственно нарисовать поверхность из карты высот.
// Чтобы это сделать, просто проходим через массив данных высот и
// применяем высоты к нашим точкам. Сначала будут отрисованы колонки
// (column), затем ряды (row). Заметьте, что мы применяем STEP_SIZE.
// Оно указывает размер квадратных полигонов на карте. Чем выше этот
// параметр, тем более резкой и «блочной» будет вытлядеть поверхность,
// и чем ниже — тем более сглаженной. Если установим STEP_SIZE в один,
// будет создаватся вершина для каждого пиксела карты высот. Не увлекайтесь,
// иначе рендер может происходить слишком медленно. Конечно, можно и увеличить
// переменную + включить свет. Вершинное освещение прикроет структурированность
// поверхности. В этом уроке, чтобы упростить его, иы просот передадим
// значение света для каждой вершины. Чем выше полигон, тем ярче его цвет.

for ( X = 0; X < MAP_SIZE; X += STEP_SIZE )
for ( Y = 0; Y < MAP_SIZE; Y += STEP_SIZE )
{
// Получаем X,Y,Z для нижней левой вершины
x = X;
y = Height(pHeightMap, X, Y );
z = Y;

// Устанавливаем цвет для текущей вершины
SetVertexColor(pHeightMap, x, z);

glVertex3i(x, y, z);    // Посылаем эту вершину для рендера в OpenGL
// (int работают быстрее)

// Получаем (X, Y, Z) для верхней левой вершины
x = X;
y = Height(pHeightMap, X, Y + STEP_SIZE );
z = Y + STEP_SIZE ;

// Устанавливаем её цвет
SetVertexColor(pHeightMap, x, z);

glVertex3i(x, y, z);    // Посылаем для отрисовки

// Получаем координаты для правой верхней вершины
x = X + STEP_SIZE;
y = Height(pHeightMap, X + STEP_SIZE, Y + STEP_SIZE );
z = Y + STEP_SIZE ;

// Устанавливаем для неё цвет
SetVertexColor(pHeightMap, x, z);

glVertex3i(x, y, z);    // Рисуем

// Get the (X, Y, Z) value for the bottom right vertex
// Получаем координаты для нижней правой вершины
x = X + STEP_SIZE;
y = Height(pHeightMap, X + STEP_SIZE, Y );
z = Y;

// Устанавливаем цвет
SetVertexColor(pHeightMap, x, z);

glVertex3i(x, y, z);    // Отрисовываем
}
glEnd();

// Сбрасываем цвет
glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
}

///////////////////////////////// LOAD RAW FILE \\\\\\\\\\\\\\\\*
/////
/////   Загружает .raw файл в массив байт. Каждое значение — значение высоты.
/////
///////////////////////////////// LOAD RAW FILE \\\\\\\\\\\\\\\\*

void LoadRawFile(LPSTR strName, int nSize, BYTE *pHeightMap)
{
FILE *pFile = NULL;

// Откроем файл для бинарного чтения
pFile = fopen( strName, «rb» );

// Проверяем, корректно ли открылся файл
if ( pFile == NULL )
{
// Выводим ошибку и останавливаем функцию
MessageBox(NULL, «Can’t find the height map!», «Error», MB_OK);
return;
}

// Здесь мы загружаем файлы из .raw файла в массив pHeightMap.
// Мы читаем в ‘1’, и размер = (width * height)
fread( pHeightMap, 1, nSize, pFile );

// После чтения данных неплохо бы проверить, все ли прочиталось верно
int result = ferror( pFile );

// Проверим, получили ли мы ошибку
if (result)
{
MessageBox(NULL, «Can’t get data!», «Error», MB_OK);
}

// Закроем файл
fclose(pFile);
}

 

Это самый простой способ отрисовывать поверхность из карты высот. На следующем этапе мы научимся делать quad-дерево, и будем отрисовывать только те вершины, которые нам видны в конусе взгляда камеры (frustum). Это позволит нам создавать очень большую поверхность, но не обрабатывать каждый кадр все вершины.

Пройдем по шагам всё, что мы сделали:

1) Сначала читаем карту высот из файла .raw. Это просто, так как в .raw файлах нет никаких заголовков, просто биты изображения.

2) После прочтения карты высот, нам нужно вывести её на экран. Это тоже не слишком сложно, мы просто рендерим квадраты с установленным размером (STEP_SIZE). Я выбрал размер 16х16, но вы можете увеличить или уменьшить его, как вам покажется лучше.
Основываясь на карте высот, рисуем эти квадраты в цикле. Вместо задания освещения окрашиваем вершины в разные оттенки зеленого цвета.

Теперь немного изменим camera.cpp:

//////////////////////////////////////////////////////////////////////////////////
//
// В этом файле мы просто увеличим скорость перемещения + добавим возможность
// пользователю двигатся по вертикали, немного изменив функцию MoveCamera():
//// Увеличим скорость перемещения:
#define kSpeed 5.0fvoid CCamera::MoveCamera(float speed)
{
CVector3 vVector = m_vView m_vPosition;
vVector = Normalize(vVector);

m_vPosition.x += vVector.x * speed;
m_vPosition.y += vVector.y * speed;
m_vPosition.z += vVector.z * speed;
m_vView.x += vVector.x * speed;
m_vView.y += vVector.y * speed;
m_vView.z += vVector.z * speed;
}

 

В файле main.h включим новый хидер:

#include «terrain.h»

 

В файле init.cpp увеличим дистанцию отображения до 4000:

void SizeOpenGLScreen(int width, int height)
{
…….
…….
…….gluPerspective(45.0f,(GLfloat)width/(GLfloat)height, 4 , 4000.0f);
}

 

Переходим к самой отрисовке.

Файл main.cpp:

//////////////////////////////////////////////////////////////////////////////
//
// В начале файла добавим новые переменные:
//// Данные карты высот
BYTE g_HeightMap[MAP_SIZE*MAP_SIZE];// Переменная для хранения типа рендера
bool  g_bRenderMode = true;

// Класс камеры
CCamera g_Camera;

//////////////////////////////////////////////////////////////////////////////
//
// В функции Init() загрузим карту высот:
//

// Читаем файл «Terrain.raw» и загружаем данные в массив:
LoadRawFile(«Terrain.raw», MAP_SIZE * MAP_SIZE, g_HeightMap);

// Устанавливаем камеру
g_Camera.PositionCamera(1200, 300, 1150,  1199, 300, 1150,  0, 1, 0);

//////////////////////////////////////////////////////////////////////////////
//
// В функции WinProc() добавим обработку мыши:
//

case WM_LBUTTONDOWN:        // Если нажата левая кнопка мыши

// Включаем/выключаем отрисовку полигонов или сетки
g_bRenderMode = !g_bRenderMode;

// Изменяем режим отрисовка
if(g_bRenderMode)
{
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
}
else
{
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
}
break;

//////////////////////////////////////////////////////////////////////////////
//
// Функция RenderScene():
//

void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();

// Передаём OpenGL позицию камеры
g_Camera.Look();

// Передаём данные высот для отрисовки
RenderHeightMap(g_HeightMap);                       // Render the height map

SwapBuffers(g_hDC);
}

Исходные коды к уроку