Статья взята с сайта GameDev.ru

НАЗАД

Ландшафт шаг за шагом.


Автор: Серба Андрей

Введение.

Этот туториал охватывает азы техники создания ландшафтов. Я рассмотрю карту высот, простейший алгоритм визуализации, раскраску (текстурирование) ландшафта, а так же оптимизацию на уровне OpenGL. Что касается примеров, я считаю, что если размер примера (не считая инициализации OpenGL и т.п.) больше 8-10 Кб то это что угодно, но только не пример. Поэтому я старался следовать этому принципу.

Представление ландшафта, карта высот.

Для представления ландшафта мы будем использовать карту высот.

Карта высот - это двумерный массив значений высот ландшафта, взятых с определенным интервалом. Т.е. мы каждому дискретному значению Х и Y в горизонтальной плоскости сопоставляем высоту.

массив

Графическое представление массива

Создать карту высот можно с помощью графического редактора (прекрасно подойдет Photoshop) или с помощью программ специально для этого предназначенных, например TerraGen. Для создания карты высот в Photoshop'е необходимо выполнить фильтр Clouds, а затем несколько раз фильтр Different clouds. И сохранить полученную картинку как Grayscale файл в формате RAW (в принципе можно использовать другой формат, но об этом поговорим позже). Размер карты может быть произвольный, но удобней использовать квадратную, с размером стороны кратным числу степени двойки: 128х128, 256х256 и т.д.

Будем считать, что на представленных примерах светлые области представляют собой возвышения, соответственно темные - низменность.

Photoshop

TerraGen

Рендеринг ландшафта.

Итак, начнем. Как известно, любая поверхность может быть достаточно точно представлена треугольниками. Поэтому любые четыре расположенные рядом точки карты высот задают прямоугольник, который мы разбиваем на два треугольника и рисуем.

В примере ландшафт реализован в виде класса Terrain. Приведенный ниже кусочек кода - часть функции Terrain::RenderLandscape() демонстрирует простейшую реализацию процедуры рисования.

//...
const unsigned MapSize = 128; // Объявление из Terrain.H 
//...
for (i= 0;i<MapSize-1;i++) // Часть кода из Terrain.Cpp
{
  for (j=0;j<MapSize-1;j++)
  { 
    x=i*Zoom;
    y=j*Zoom;

    glBegin(GL_TRIANGLE_STRIP); 

      glVertex3f(x, y, HeightMap[i][j]);
      glVertex3f(x+Zoom, y, HeightMap[i+1][j]);
      glVertex3f(x, y+Zoom, HeightMap[i][j+1]);
      glVertex3f(x+Zoom, y+Zoom, HeightMap[i+1][j+1]);

    glEnd(); 
  }
}

//...

Пример (40 кБ)

Раскраска (текстурирование) ландшафта.

Я расскажу о двух способах закраски ландшафта (и во втором случае текстурирования). Это очень краткое объяснение, не затрагивающее множество аспектов, таких как мультитекстурирование, блендинг нескольких текстур, и т.п. Обо всем этом я напишу отдельную статью.

Способ первый - интерполяция цветов.

Первый способ раскраски ландшафта состоит в использовании интерполяции цветов. Что такое интерполяция цветов, наверное, знает каждый, но если кто-то не знает, краткий пример. Например, если, рисуя треугольник, каждой вершине задать разный цвет, получим картину изображенную ниже. При этом OpenGL сам выполнит интерполяцию, от нас только требуется включить интерполяцию, что делается просто:

glShadeModel(GL_SMOOTH);

ниже приведен код демонстрирующий рисование треугольника с интерполяцией цветов:

//...
glBegin(GL_TRIANGLE);
  glColor3f(1,0,0); glVertex3f(0,0,0);
  glColor3f(0,1,0); glVertex3f(0,1,0);
  glColor3f(0,0,1); glVertex3f(1,1,0);
glEnd();
//...

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

Грубо говоря, любая RGB картинка сохраненная, как Raw-файл, может быть использована в качестве карты цветов. Важно заметить, что ширина и высота этих файлов должны быть равными размеру карты высот!

Как известно в RGB режиме одна точка изображения описывается тремя байтами - значение Red, Green, Blue компоненты цвета. Таким же образом точки картинки хранятся и в Raw-файле.

Хранить карту цветов (как и карту высот) мы будем в массиве, только в карте высот одному элементу массива соответствует одно значение высоты, а в карте цветов три значения - интенсивность красной, зеленой и синей составляющей цвета. Поэтому введем следующую структуру, которая и будет элементом массива цветов:

struct RGB
{ 
  GLubyte Red, Green, Blue; // Интенсивности составляющих цвета
}; 

const unsigned MapSize = 128;

//...
RGB Colors[MapSize][ MapSize]; // Массив цветов

При этом процедура рисования (я убрал рисование "проволочной" модели) претерпела минимальные изменения:

for (i=0;i<MapSize-1;i++)
{
  for (j=0;j<MapSize-1;j++)
  { 
    x=i*Zoom;
    y=j*Zoom;

    glBegin(GL_TRIANGLE_STRIP); 
      glColor3ub(ColorsMap[i][j].Red, 
                 ColorsMap[i][j].Green, ColorsMap[i][j].Blue);
      glVertex3f(x, y, Land[i][j]);
      glColor3ub(ColorsMap[i+1][j].Red, 
                 ColorsMap[i+1][j].Green, ColorsMap[i+1][j].Blue);
      glVertex3f(x+Zoom, y, Land[i+1][j]); 
      glColor3ub(ColorsMap[i][j+1].Red, 
                 ColorsMap[i][j+1].Green, ColorsMap[i][j+1].Blue);
      glVertex3f(x, y+Zoom, Land[i][j+1]); 
      glColor3ub(ColorsMap[i+1][j+1].Red, 
                 ColorsMap[i+1][j+1].Green, ColorsMap[i+1][j+1].Blue);
      glVertex3f(x+Zoom, y+Zoom, Land[i+1][j+1]);
    glEnd(); 
  }
}

Способ второй - текстурирование.

Суть второго способа состоит в наложении на ландшафт текстуры. Для этого нам понадобится текстура, точно такая же как файл для карты цветов, только сохранить его надо как TGA файл (понятно, что выбор формата - это дело вкуса, но в приведенных мною примерах используются TGA-файлы). Я предполагаю что вы знакомы с текстурирование, и не буду описывать процесс загрузки и создания текстуры в OpenGL.

Размер текстуры, может и не соответствовать размеру карты высот. Например для карты высот 128x128 может быть выбрана текстура размером 64x64 или 512x512 точек. В последнем случае качество буде лучше. На практике достаточно выбрать размер текстуры равный размеру карты высот.

Суть метода состоит в текстурирование одной текстурой всех треугольников нашего ландшафта, рассчитывая каждому треугольнику соответствующие координаты текстуры.

Итак, прежде чем приступить к текстурированию мы должны определит шаг изменения координат текстуры. Необходимо вычислить значение 1.0/MapSize (MapSize - размер карты высот). И тогда умножая координаты x и y треугольника на этот шаг, мы получим координаты текстуры соответствующие этому треугольнику. Ниже приведен рисунок иллюстрирующий текстурирование, и пример процедуры рисования.

Если вы хотите, чтобы текстура не "растягивалась" на весь ландшафт, а соответствовала определенной его части, необходимо при вычислении шага, вместо 1 подставить нужное вам число, и текстура повториться заданное количество раз.

Шаг изменения координат текстуры объявлен как константа в Terrain.H.

const GLfloat TextureBit = 1.0f/(float)MapSize;

а измененная процедура рисования выглядит следующим образом:

for (i=0;i<MapSize-1;i++)
{
  for (j=0;j<MapSize-1;j++)
  { 
    x=i*Zoom;
    y=j*Zoom; 
    glBegin(GL_TRIANGLE_STRIP); 
      glTexCoord2f(i*TextureBit, j*TextureBit);
      glVertex3f(x, y, HeightMap[i][j]);
      glTexCoord2f((i + 1)*TextureBit, j*TextureBit);
      glVertex3f(x+Zoom, y, HeightMap[i+1][j]); 
      glTexCoord2f(i*TextureBit,(j + 1)*TextureBit);
      glVertex3f(x, y+Zoom, HeightMap[i][j+1]); 
      glTexCoord2f((i + 1)*TextureBit,(j + 1)*TextureBit);
      glVertex3f(x+Zoom, y+Zoom, HeightMap[i+1][j+1]);
    glEnd(); 
  }
}

Следует сказать пару слов о фильтрации текстуры. В приведенном примере используется линейная фильтрация, что намой взгляд, является оптимальным вариантом. Фильтрация ближайший сосед (GL_NEAREST) существенно ухудшает качество, в то время как мипмаппинг практически его не улучшает. Использование мипмаппинга будет иметь смысл, если размер текстуры превышает размер ландшафта хотя бы в два раза, или если текстура повторяется.

Оптимизация на уровне OpenGL

Сразу необходимо заметить, что только оптимизация на уровне OpenGL не может решить проблему производительности в целом. Безусловно, прирост производительности будет но не всегда достаточный. OpenGL оптимизацию необходимо использовать вместе с оптимальным алгоритмом отображения ландшафта. Хотя, если размер карты не превышает 128x128, можно обойтись оптимизацией только на уровне OpenGL.

Первое что можно сделать на уровне OpenGL, это уменьшить число вершин предаваемых на конвейеру текстурирования, для этого необходимо заменить рисование с использованием GL_TRIANGLE_STRIP на эквивалентный GL_TRIANGLE_FAN, как показано на рисунке:

GL_TRIANGLE_STRIP

GL_TRIANGLE_FAN

Правда в примере, я использовал GL_TRINGLE_STRIP.

Второе - использовать для отображения функцию glDrawElements. Это функция позволяет рендерить множество примитивов вызовом одной функции, тем самым уменьшая число вызовов OpenGL функций. Для того, что бы выполнить рисование с помощью функции glDrawElements необходимо выполнит три шага:

1. Активировать массивы с данными, в нашем случае массив вершин и координат текстур (аналогично можно поступить с цветами, нормалями и некоторыми другими атрибутами). Для этого необходимо воспользоваться функцией void glEnableClientState(GLenum Array), где в качестве параметра указать GL_VERTEX_ARRAY, а затем GL_TEXTURE_COORD_ARRAY.

glEnableClientState(GL_VERTEX_ARRAY); 
glEnableClientState(GL_TEXTURE_COORD_ARRAY);

2. Задать параметры данных и массивы для их хранения. Для задания массива и типа вершин служит функция

void glVertexPointer( GLint Size, GLenum Type, GLsizei Stride, void *Ptr )

которая определяет способ хранения и координаты вершин. При этом Size определяет число координат вершины (может быть равен 2, 3, 4), Type определяет тип данных (может быть равен GL_SHORT, GL_INT, GL_FLOAT, GL_DOUBLE). Иногда удобно хранить в одном массиве другие атрибуты вершины, и тогда параметр Stride задает смещение от координат одной вершины до координат следующей. Если Stride равен нулю, это значит, что координаты расположены последовательно. В параметре Ptr указывается адрес, где находятся данные.

Аналогично можно определить массив координат текстуры, используя команду

void glTexCoordPointer(GLint Size, GLenum Type, GLsizei Stride);
// Определяем массив вершин
GLfloat VertexMap[MapSize][3];
// Массив координат текстур
GLfloat TextureMap[MapSize][2];
// Указываем что конкретно хранит массив
glVertexPointer (3, GL_FLOAT, 0, VertexMap);
glTexCoordPointer(2, GL_FLOAT, 0, TextureMap);

3. Заполнить массивы данными и выполнить отрисовку. Как уже было сказано, рисование выполняется с помощью функции

void glDrawElements(GLenum Mode, GLsizei Count, 
                      GLenum Type, const GLvoid *Indices);

Mode - указывает какой примитив будет рисоваться, в нашем случае GL_TRIANGLE_STRIP (в принципе может быть любой другой). Count - количество примитивов. Type - тип значений в Indices, может быть GL_UNSIGNED_BYTE, GL_UNSIGNED_SHORT или GL_UNSIGNED_INT. В массиве Indices хранятся индексы, в соответствии с которыми данные будут извлекаться из массива вершин и координат текстур.

Пример (200 кБ)

Автор: Серба Андрей
Сайт Андрея: http://www.immerse.dp.ua/

НАЗАД