В этом уроке мы научимся создавать поверхность — карту высот. Основа этого урока взята из урока «Камера: стрейф», к этой основе мы добавим нужный функционал.
Файлик Terrain.raw, наша карта высот.
Сначала мы создадим функционал для обработки и рисования поверхности.
Сначала создадим хидер; файл будет содержать константы и функции для обработки и отрисовки поверхности.
Файл 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 включим новый хидер:
В файле init.cpp увеличим дистанцию отображения до 4000:
{
…….
…….
…….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);
}