Урок 34. Построение красивых ландшафтов с помощью карты высот.

  Добро пожаловать в очередной потрясающий урок. Код этого урока был написан Беном Хамфри (Ben Humphrey) и он основан на коде первого урока. К этому моменту вы должны быть уже гуру в OpenGL (усмешка) и перенос кода из этого урока в ваш базовый код должно быть проще простого!

Этот урок научит вас, как сделать круто выглядящий ландшафт из карты высот. Для тех из вас, кто не представляет что такое карта высот, я попытаюсь объяснить. Карта высот это просто… смещение от поверхности. Для тех, кто до сих пор ломает голову вопросом «о чем, черт побери, этот парень толкует!?!»… отмечу, что по-английски, карта высот представляет низкие и высокие точки для нашего ландшафта. Исключительно от вас зависит, какие значения элементов карты высот будут представлять низкие точки, а какие высокие. Важно заметить, что карты высот не обязательно могут быть картинками… Вы можете создать карту высот из любого типа данных. Например, вы можете использовать аудио-поток для визуального представления карты высот. Если еще ничего не прояснилось… продолжайте читать… все будет проясняться по мере изучения урока :)

 

#include <windows.h> // Заголовочный файл для Windows

#include <stdio.h> // Заголовочный файл для стандартного ввода-вывода (НОВОЕ)

#include <gl\gl.h> // Заголовочный файл для OpenGL32 библиотеки

#include <gl\glu.h> // Заголовочный файл для GLu32 библиотеки

#include <gl\glaux.h> // Заголовочный файл для GLaux библиотеки

 

#pragma comment(lib, "opengl32.lib") // Ссылка на OpenGL32.lib

#pragma comment(lib, "glu32.lib") // Ссылка на Glu32.lib

  Мы начнем с объявления нескольких важных переменных. MAP_SIZE  — это размер нашей карты. В этом уроке размер будет 1024х1024. STEP_SIZE — это размер каждого квадрата, используемого для построения ландшафта. Уменьшая значение этой переменной, мы увеличиваем гладкость ландшафта. Важно заметить, что чем меньше STEP_SIZE, тем больше времени потребуется для выполнения программы, особенно когда используются большие карты высот. HEIGHT_RATIO используется для масштабирования ландшафта по оси y. Если это значение невелико, горы будут более пологими, иначе  — более отвесными.

В дальнейшем в исходном коде вы заметите переменную bRender. Если bRender истинно (по умолчанию), то рисуем заполненные полигоны, иначе — проволочные.

 

#define MAP_SIZE 1024 // Размер карты вершин (НОВОЕ)

#define STEP_SIZE 16 // Ширина и высота каждого квадрата (НОВОЕ)

// Коэффициент масштабирования по оси Y в соответствии с осями X и Z (НОВОЕ)

#define HEIGHT_RATIO 1.5f

 

HDC hDC=NULL; // Приватный контекст устройства GDI

HGLRC hRC=NULL; // Постоянный контекст рендеринга

HWND hWnd=NULL; // Указатель на наше окно

HINSTANCE hInstance; // Указывает на дескриптор текущего приложения

 

bool keys[256]; // Массив для процедуры обработки клавиатуры

bool active=TRUE; // Флаг активности окна, по умолчанию=TRUE

bool fullscreen=TRUE; // Флаг полноэкранного режима, по умолчанию=TRUE

bool bRender = TRUE; // Флаг режима отображения полигонов,

// по умолчанию=TRUE (НОВОЕ)

  Здесь мы создаем массив (g_HeightMap[ ]) байтов для хранения нашей карты вершин. Мы будем считывать массив из .RAW файла, который содержит значения от 0 до 255. 255 будет значением, соответствующим самой высокой точке, а 0 — самой низкой. Мы также создаем переменную scaleValue для масштабирования сцены. Это дает возможность пользователю увеличивать и уменьшать сцену.

 

BYTE g_HeightMap[MAP_SIZE*MAP_SIZE]; // Содержит карту вершин (НОВОЕ)

 

float scaleValue = 0.15f; // Величина масштабирования поверхности (НОВОЕ)

 

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Объявление для WndProc

  Код в процедуре ReSizeGLScene() остался таким же, как и в первом уроке, за исключением дальней плоскости отсечения. Она изменилась со 100.0f до 500.0f

 

GLvoid ReSizeGLScene(GLsizei width, GLsizei height) // Масштабирование и инициализация окна OpenGL

{

… вырезано …

}

  Следующие строчки кода загружают данные из .RAW файла. Все достаточно просто! Мы открываем файл в режиме бинарного чтения.(Read/Binary) Затем делаем проверку на существование и открытие файла. Если не удалось открыть файл, то возникнет сообщение об ошибке.

 

// Чтение и сохранение .RAW файла в pHeightMap

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;

}

  Если мы дошли до этого места, значит, никаких проблем с открытием файла не возникло. Теперь можно считывать данные. Делаем это с помощью функции fread(). pHeightMap это место для хранения данных (указатель на массив g_Heightmap). Цифра 1 говорит о том, что мы будем считывать по байту за раз, nSize — это сколько байт нужно считать (размер карты в байтах — ширина карты * высоту карты). Наконец, pFile — это указатель на структуру файла.

После чтения данных мы проверяем, возникли ли какие-либо ошибки. Сохраняем результат в result и потом проверяем его. Если произошла ошибка — выводим предупреждение.

И последнее что мы сделаем, это закроем файл с помощью fclose(pFile).

 

// Загружаем .RAW файл в массив pHeightMap

// Каждый раз читаем по одному байту, размер = ширина * высота

fread( pHeightMap, 1, nSize, pFile );

 

// Проверяем на наличие ошибки

int result = ferror( pFile );

 

// Если произошла ошибка

if (result)

{

MessageBox(NULL, "Failed To Get Data!", "Error", MB_OK);

}

 

// Закрываем файл

fclose(pFile);

}

  Код инициализации довольно простой. Мы устанавливаем цвет, которым будет очищен экран, в черный, создаем буфер глубины, включаем сглаживание полигонов и т.д. После всего этого загружаем наш .RAW файл. Для этого передаем в качестве параметров имя файла ("Data/Terrain.raw"), размер .RAW файла (MAP_SIZE * MAP_SIZE) и, наконец, массив HeightMap (g_HeightMap) в функцию LoadRawFile(). Файл будет загружен, и наши данные сохранятся в массиве g_HeightMap.

 

int InitGL(GLvoid) // Инициализация OpenGL

{

glShadeModel(GL_SMOOTH); // Включить сглаживание

glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Очистка экрана черным цветом

glClearDepth(1.0f); // Установка буфера глубины

glEnable(GL_DEPTH_TEST); // Включить буфер глубины

glDepthFunc(GL_LEQUAL); // Тип теста глубины

glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Улучшенные вычисления перспективы

 

// Читаем данные из файла и сохраняем их в массиве g_HeightMap array.

// Также передаем размер файла (1024).

 

LoadRawFile("Data/Terrain.raw", MAP_SIZE * MAP_SIZE, g_HeightMap); // ( НОВОЕ )

 

return TRUE; // Инициализация прошла успешно

}

  Когда имеем дело с массивами, мы должны быть уверены, что не выходим за их пределы. Чтобы быть уверенным, что это не произойдет, будем использовать оператор %, который, в нашем случае, будет запрещать превышение переменными x и y величины MAX_SIZE — 1.

Убеждаемся, что pHeightMap указывает на верные данные, иначе возвращаем 0.

Если все прошло успешно, мы возвратим величину, хранящуюся в переменных x и y в нашей карте вершин. К этому моменту вы должны знать, что мы умножаем y на ширину карты MAP_SIZE, чтобы перемещаться по данным, хранящимся в карте вершин.

 

int Height(BYTE *pHeightMap, int X, int Y) // Возвращает высоту из карты вершин (?)

{

int x = X % MAP_SIZE; // Проверка переменной х

int y = Y % MAP_SIZE; // Проверка переменной y

 

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

  Так как имеем двумерный массив, то можем использовать уравнение: номер = (x + (y * ширинаМассива) ), т.е. получаем pHeightMap[x][y], в противном случае: (y + (x * ширинаМассива) ).

Теперь, когда имеем верный номер, возвратим значение высоты этого номера (x, y в нашем массиве)

 

return pHeightMap[x + (y * MAP_SIZE)]; // Возвращаем значение высоты

}

  Здесь будем выбирать цвет вершины, основываясь на значении номера высоты. Чтобы сделать потемнее, начнем с -0.15f. Будем получать коэффициент цвета от 0.0f до 1.0f путем деление на 256.0f. Если нет никаких входных данных, функция не возвратит никакого значения. Если все прошло успешно, будем устанавливать оттенок синего цвета, используя glColor3f(0.0f, fColor, 0.0f). Попробуйте поизменять fColor в красный и зеленый, чтобы изменить цвет ландшафта.

 

// Эта функция устанавливает значение цвета для конкретного номера, зависящего от номера высоты

void SetVertexColor(BYTE *pHeightMap, int x, int y)

{

if(!pHeightMap) return; // Данные корректны?

 

float fColor = -0.15f + (Height(pHeightMap, x, y ) / 256.0f);

 

// Присвоить оттенок синего цвета для конкретной точки

glColor3f(0.0f, 0.0f, fColor );

}

  Далее мы вырисовываем наш ландшафт. Переменные X и Y будут использоваться для перемещения по массиву карты высоты. Переменные x, y и z будут использоваться для визуализации квадратов, составляющих ландшафт

Как обычно, проверяем, содержит ли pHeightMap нужные нам данные. Если нет — ничего не делаем.

 

void RenderHeightMap(BYTE pHeightMap[]) // Визуализация карты высоты с помощью квадратов

{

int X = 0, Y = 0; // Создаем пару переменных для перемещения по массиву

int x, y, z; // И еще три для удобства чтения

 

if(!pHeightMap) return; // Данные корректны?

  Здесь мы сможем изменять режим отображения ( проволочный или сплошной). Если bRender = True, то рендерим полигоны, иначе — линии.

 

if(bRender) // Что хотим визуализировать?

glBegin( GL_QUADS ); // Полигоны

else

glBegin( GL_LINES ); // Линии

  Далее рисуем поверхность из карты вершин. Для этого пройдемся по массиву высот и, доставая значения вершин, будем рисовать наши точки. Если бы мы могли видеть, как это происходит, то вначале нарисовались бы столбцы (Y), а затем строки. Заметьте, что мы используем STEP_SIZE. Чем больше STEP_SIZE, тем менее гладко выглядит поверхность, и наоборот. Если принять STEP_SIZE = 1, то вершина будет создаваться для каждого пикселя из карты высот. Я выбрал STEP_SIZE = 16, как достаточно скромный размер. Намного меньшее значение было бы безрассудством, да и потребовалось бы гораздо больше процессорного времени. Естественно вы можете увеличить значение, когда включите источник света. Освещение спрячет шероховатость формы. Вместо освещения мы ассоциируем цвет с каждой точкой карты вершин, дабы облегчить урок. Чем выше полигон — тем ярче цвет.

 

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); // Визуализация ее

 

// Получаем (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, 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); // Визуализация ее

 

// Получаем (X, Y, Z) координаты нижней правой вершины

x = X + STEP_SIZE;

y = Height(pHeightMap, X + STEP_SIZE, Y );

z = Y;

 

// Устанавливаем цвет конкретной точки

SetVertexColor(pHeightMap, x, z);

 

glVertex3i(x, y, z); // Визуализация ее

}

glEnd();

 

Как только все закончили, восстанавливаем цвет до ярко-белого с альфа-значением 1.0f. Если на экране будут присутствовать другие объекты, мы не хотим видеть их СИНИМИ :)

 

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

}

  Для тех, кто еще не использовал gluLookAt() поясню, что эта функция позиционирует камеру, вид и головной вектор. Мы отодвигаем камеру, чтобы получить хороший вид на ландшафт. Чтобы избежать больших значений, мы будем делить вершины ландшафта на константу масштабирования, как мы делаем это ниже.

Входные параметры функции gluLookAt() следующие: первые три значения указывают на положение камеры, т.е. 212, 60, 94 — это смещение по осям х, y и z соответственно от начала координат. Следующие 3 значения указывают точку визирования (направление камеры). В этом уроке во время просмотра демонстрационного примера вы заметите, что камера направлена немного влево. Также мы направляем камеру навстречу ландшафту. 186 меньше 212, что позволяет нам смотреть влево, 55 ниже, чем 60, что позволяет нам находиться выше ландшафта и смотреть на него с легким наклоном. 171 указывает как далеко от объектов находится камера. Последние три значения указывают OpenGL направление головного вектора. Наши горы растут вверх по оси y, поэтому мы ставим значение y в 1, а остальные в 0.

На первый взгляд gluLookAt кажется очень запутанной. После грубого объяснения этой функции вы возможно запутались. Мой лучший совет — поиграть со значениями. Изменить позицию камеры. Если вы измените y-позицию камеры, скажем в 120, вы увидите больше вершин ландшафта.

  Я не уверен, поможет ли это, но я попытаюсь объяснить на пальцах :). Допустим, ваш рост равен шести футам и немного выше. Также предположим, что ваши глаза находятся на высоте шести футов (глаза представляют камеру - 6 футов это 6 делений по оси y). Если вы встанете напротив двух футовой стены (2 деления по оси y), вы будете смотреть ВНИЗ на стену и будете способны видеть ее верх. Если бы стена была высотой 8 футов, вы бы смотрели ВВЕРХ на стену и НЕ видели бы ее верх. Направление взгляда изменяется, когда вы смотрите вверх или вниз (если вы находитесь выше или ниже объекта, на который смотрите). Надеюсь, я немного прояснил ситуацию.

 

int DrawGLScene(GLvoid) // Здесь содержится код рисования

{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана и буфера глубины

glLoadIdentity(); // Сброс просмотра

// Положение Вид Вектор вертикали

gluLookAt(212, 60, 194, 186, 55, 171, 0, 1, 0); // Определяет вид и положение камеры

  Следующая строчка кода позволит масштабировать поверхность. Мы можем изменять scaleValue нажатием клавиш ВВЕРХ и ВНИЗ. Заметьте, что мы умножаем Y scaleValue на HEIGHT_RATIO. Это сделано для того, чтобы можно было бы изменять высоту ландшафта.

 

glScalef(scaleValue, scaleValue * HEIGHT_RATIO, scaleValue);

  Если мы передадим g_HeightMap в качестве входного параметра в функцию RenderHeightMap(), эта функция визуализирует поверхность в квадратах. Если вы планируете использовать эту функцию в дальнейшем, было бы неплохо добавить в нее переменные (X, Y), чтобы позиционировать ландшафт именно там, где это нужно.

 

RenderHeightMap(g_HeightMap); // Визализация карты высот

 

return TRUE; // Идем дальше

}

  Код функции The KillGLWindow() такой же, как в первом уроке.

 

GLvoid KillGLWindow(GLvoid) // Уничтожение окна

{

}

  Код функции CreateGLWindow() также не изменился с первого урока.

 

BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)

{

}

  Единственное изменение в WndProc() — это добавление обработчика события M_LBUTTONDOWN. Он проверяет, нажата ли левая кнопка мыши. Если да, то меняется режим отображения поверхности с проволочной на полигональную и наоборот.

 

LRESULT CALLBACK WndProc(

HWND hWnd, // Указатель на окно

UINT uMsg, // Сообщение для этого окна

WPARAM wParam, // Параметры сообщения

LPARAM lParam) // Параметры сообщения

{

switch (uMsg) // Проверим сообщения окна

{

case WM_ACTIVATE: // Наблюдаем за сообщением об активизации окна

{

if (!HIWORD(wParam)) // Проверим состояние минимизации

{

active=TRUE; // Программа активна

}

else

{

active=FALSE; // Программа больше не активна

}

 

return 0; // Вернуться к циклу сообщений

}

 

case WM_SYSCOMMAND: // Перехватаем системную команду

{

switch (wParam) // Проверка выбора системы

{

case SC_SCREENSAVE: // Пытается включиться скринсэйвер?

case SC_MONITORPOWER: // Монитор пытается переключиться в режим сохранения энергии?

return 0; // Не давать этому случиться

}

break; // Выход

}

 

case WM_CLOSE: // Мы получили сообщение о закрытии программы?

{

PostQuitMessage(0);// Послать сообщение о выходе

return 0; // Возврат обратно

}

 

case WM_LBUTTONDOWN: // Нажата левая клавиша мыши?

{

bRender = !bRender;// Изменить тип визуализации

return 0; // Вернуться

}

 

case WM_KEYDOWN: // Клавиша была нажата?

{

keys[wParam] = TRUE; // Если так — пометим это TRUE

return 0; // Вернуться

}

 

case WM_KEYUP: // Клавиша была отпущена?

{

keys[wParam] = FALSE; // Если так — пометим это FALSE

return 0; // Вернуться

}

 

case WM_SIZE: // Изменились окна OpenGL

{

ReSizeGLScene(LOWORD(lParam),HIWORD(lParam)); // LoWord=ширина, HiWord=высота

return 0; // Вернуться

}

}

// Пересылаем все прочие сообщения в DefWindowProc

return DefWindowProc(hWnd,uMsg,wParam,lParam);

}

  Никаких принципиальных изменений в нижеследующем коде. Единственное, что претерпело изменение это заголовок окна. До проверки нажатий клавиш все осталось без изменений.

 

int WINAPI WinMain(

HINSTANCE hInstance, // Экземпляр

HINSTANCE hPrevInstance,// Предыдущий экземпляр

LPSTR lpCmdLine, // Параметры командной строки

int nCmdShow) // Показать состояние окна

{

MSG msg; // Структура сообщения окна

BOOL done=FALSE; // Булевская переменная выхода из цикла

 

// Запросим пользователя, какой режим отображения он предпочитает

if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?",

"Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO)

{

fullscreen=FALSE; // Оконный режим

}

 

// Создадим наше окно OpenGL

if (!CreateGLWindow("NeHe & Ben Humphrey's Height Map Tutorial",

640, 480, 16, fullscreen))

{

return 0; // Выходим если окно не было создано

}

 

while(!done) // Цикл, который продолжается пока done=FALSE

{

if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Есть ожидаемое сообщение?

{

if (msg.message==WM_QUIT)// Мы получили сообщение о выходе?

{

done=TRUE; // Если так done=TRUE

}

else // Если нет, продолжаем работать с сообщениями окна

{

TranslateMessage(&msg);// Переводим сообщение

DispatchMessage(&msg); // Отсылаем сообщение

}

}

else // Если сообщений нет

{

// Рисуем сцену. Ожидаем нажатия кнопки ESC и сообщения о выходе от DrawGLScene()

// Активно? Было получено сообщение о выходе?

if ((active && !DrawGLScene()) || keys[VK_ESCAPE])

{

done=TRUE; // ESC или DrawGLScene просигналили "выход"

}

else if (active) // Не время выходить, обновляем экран

{

SwapBuffers(hDC); // Переключаем буферы (Двойная буферизация)

}

 

if (keys[VK_F1]) // Была нажата кнопка F1?

{

keys[VK_F1]=FALSE; // Если так - установим значение FALSE

KillGLWindow(); // Закроем текущее окно OpenGL

fullscreen=!fullscreen;// Переключим режим "Полный экран"/"Оконный"

// Заново создадим наше окно OpenGL

if (!CreateGLWindow("NeHe & Ben Humphrey's Height Map Tutorial",

640, 480, 16, fullscreen))

{

return 0; // Выйти, если окно не было создано

}

}

  Следующий код позволяет вам увеличивать и уменьшать scaleValue. Нажав клавишу «вверх» scaleValue, и ландшафт вместе с ней, увеличатся. Нажав клавишу «вниз» - уменьшится.

 

if (keys[VK_UP]) // Нажата клавиша ВВЕРХ?

scaleValue += 0.001f; // Увеличить переменную масштабирования

 

if (keys[VK_DOWN]) // Нажата клавиша ВНИЗ?

scaleValue -= 0.001f; // Уменьшить переменную масштабирования

}

}

 

// Shutdown

KillGLWindow(); // Закроем окно

return (msg.wParam); // Выйдем из программы

}

  Это все, что нужно для создания красивых ландшафтов. Я надеюсь вы оценили работу Бена! Как обычно, если вы найдете ошибки в уроке, пожалуйста, напишите мне, и я попытаюсь исправить проблему / пересмотреть урок.

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

Надеюсь вы остались довольны уроком. Посетите сайт Бена: http://www.GameTutorials.com.

© Ben Humphrey (DigiBen)
© Jeff Molofee (NeHe)

 21 марта 2003 (c)  Евгений Каратаев