Basic3D Tutorials Основы 3D
Как создавать ландшафты.

Введение

 

Когда пишется графическая система трехмерной графики реального времени вначале вы должны решить, какие функции должна выполнять эта система, особенно, если эта система для игры. Далее вы должны подумать - "как избежать этого? ". Когда я познакомился с оригинальным алгоритмом вывода ландшафтов, я тут же отметил, что камера не требует сильных наклонов. Характер ландшафта, очевидно, позволяет ввести много упрощений в управление сценой, которые невозможны с более универсальной системой управления сцены. При визуализации ландшафта мы всегда знаем, что мы имеем дело с непрерывной поверхностью, которая простирается за экран во всех направлениях. В этой статье не содержит все, что нужно для создания полноценной трехмерной графической системы, всегда есть специфические случаи, с которыми приходится сталкиваться. В этой статье сосредоточимся только на ландшафтах.

 

Ограничения

 

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

 

-     Нет крутых склонов, так как данные для создания сцены включают в себя сетку  из равномерно расположенных точек называемых картой высот, это делает управление сценой невероятно простым.

-     Нет необходимости наклонять камеру.

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

 

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

 

Прежде, чем я начну описывать, как фактически рисуется ландшафт, возможно, лучше всего, если я опишу основы линейного наложения текстур. Треугольник состоит из 3 вершин и 3 граней. Для каждой из вершин необходимо задать U и V координаты на текстуре. Пары координат U и V по существу описывают треугольную область на карте текстуры, которая будет отображена на треугольник, который затем отображается на экран. Мы отображаем текстуру по шагам вниз с левого до правого края треугольника, вычисляя соответствующее значение для U и V в каждой точке. Затем, следуя вдоль горизонтальной строки, используем возникающие в результате значения U и V, чтобы получить цвет из карты текстуры, который мы должны записать в буфер изображения. Освещение или другие эффекты обычно применяются перед записью в буфер изображения.

 

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

 

Мое решение

 

Алгоритм, который представлен здесь, я выдумал летом 95 после окончания университета и началом работы в SCI. В то время я работал над игровой системой с моим хорошим приятелем Ричардом Маттиасом. Игровая система должна была быть переделанной версией системы, которую я описал как часть моей диссертации.

 

Вначале надо определить, какая ось на карте высот больше всего совпадает с горизонтальной осью экрана. Это можно сделать очень просто, проверив компоненты вектора направления камеры, т.е. какая из компонент X и Z имеет наибольшее значении (в левосторонней системе координат, где положительная ось Y направлена вверх). Следующий отрывок кода иллюстрирует это:

 

if ((abs(cam_look_x)>abs(cam_look_z)) {

   // компонента X камеры больше

   // тогда Y лучший выбор

} else {

   // Y ось доминирует

   // тогда X лучше

}         

   

Каждую точку на карте высот надо преобразовать в 3-х мерную позицию во всемирных координатах. Мы делаем это при помощи умножения U и V координат точки карты на коэффициент масштабирования заданный для X и Z компонентов, затем мы используем соответствующую позицию поля высоты как Y компоненту.

 

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

 

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

 

 #define screenwidth 320 // ширина экрана (в пикселях)

 #define screenheight 200 // высота экрана (в пикселях)

 

 typedef struct {

  int disp_height;

  int last_height;

  char *disp_pos;

  int U,V;

 } ytab_struct;

 

 ytab_struct ytab[screenwidth];

 

 void initialise_ytab()

 {

  ytab_struct *ytptr;

  int ind;

 

  ytptr = &ytab;

  for (ind=0;ind<screenwidth;ind++) {

   ytptr->disp_height=screenheight;

   ytptr->last_height=screenheight;

   ytptr->U=ytptr->V=0;

   ytptr->disp_pos=адрес_на_память_экрана + ind + (screenwidth*screenheight);

   ytptr++;

  }

 }

 

Для каждой пары элементов в массиве точек мы должны просканировать пиксель за пикселем слева направо. Мы должны рассматривать по одной точке на вертикальную линию. Между преобразованными точками сетки, мы должны интерполировать вертикальную позицию на экран. Также необходимо интерполировать значения U и V для точек сетки. Этот процесс очень похож на сканирование краев треугольника или многоугольника, хотя мы должны отделить 'край' для каждой точки сетки, который находится внутри экрана для соответствующей линии на карте высот. Если мы нарисуем график вертикальной позиции каждой точки, после того как мы выполнили интерполяцию, то мы должны получить в результате что-то навроде следующего изображения. Изображение имеет невидимые линии, удаленные для ясности.

Отрисовка ландшафта сканированием

Теперь мы можем рассмотреть наложение текстуры. Чтобы текстурировать ландшафт, мы просто берем новые интерполируемые значения для Y, U и V, и используем их, чтобы рисовать вертикальную линию между уровнем соответствующего значения для disp_height и новой интерполируемой позиции Y. Если новое значение Y - меньше чем соответствующие значение disp_height, тогда мы знаем, что линейный сегмент закрыт частью ландшафта перед ним (или началом экрана). После того, как мы нарисовали вертикальную линию таким же способом как сканирование внутренней строки треугольника, мы сохраняем новую позицию Y, U и V обратно в таблицу, мы также модифицируем значение для disp_pos. Фактически весь процесс интерполяции вдоль и вверх экрана может быть выполнен в одной ассемблерной функции для улучшения эффективности. Для демонстрации приведу следующий Си псевдо код, который описывает функцию, в которую передается вертикальная позиция линии (как 'ind'), новая интерполируемая позиция Y (как 'new_y') и U и V координаты.

 

void DrawVerticleStrip(int ind, int new_y, int U, int V)

{

int pos;

 

if (ytab[ind]->disp_height>=new_y) {

// линия невидна

ytab[ind].last_height=new_y;

ytab[ind].U=U;

ytab[ind].V=V;

return;

}

if (ytab[ind]->last_height<ytab[ind].disp_height) {

// часть линии видна

// Правильные U&V в

// ytab[ind]

}

 

// Нарисовать линию.

 

pos=disp_height;

while (pos!=new_y) {

ytab[ind].disp_pos-=screen_pitch // сдвинуть точку на один пиксель.

*(ytab[ind].disp_pos)=значение текстуры в U,V

 

Что мы имеем?

 

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

 

Теперь мы имеем ландшафт, на который наложена одна большая текстура, и это, очевидно, имеет много ограничений. Мы могли бы пробовать увеличить размер этой одной массивной текстуры, но даже карта текстуры размером 4 мегабайта (2048 x 2048) была бы неадекватна, если карта высот имеет реально необходимый размер (см. Возможные Добавления). Из-за способа наложения текстуры на ландшафт, мы не можем использовать стандартную клеточную систему наложения текстур, чтобы улучшить детализацию, так как алгоритм запрещает нам определения границ клеток. Решение, которое я использую, состоит в том, чтобы сделать то, что я называю 3-х мерным наложением текстуры.

 

Примечания переводчика: полностью не согласен с тем что надо использовать только большую текстуру, так как можно эту текстуру можно получить как набор блоков из более маленьких текстур. В этом случае вычислении U&V координат сведется вначале к получению номера блока в большой текстуре, а потом уже вычислению U&V координат в текстуре блока.

 

3-х мерное наложение текстуры.

 

При помощи множественных слоев (но связанных между собой) состоящих из текстурных карт, мы можем создать то, что я называю трехмерной текстурной картой. Вместо того чтобы использовать прямоугольный растр, который мы адресуем U и V координатами, мы будем иметь куб текстур, который мы адресуем U, V и W координатами (W - адрес уровня). Я первоначально хотел использовать эти текстурные карты для моделирования повреждений в игровой системе типа DOOM / QUAKE, где каждый из уровней содержит постепенное продвижение, скажем из неповрежденной стенки до разваленной стенки, я мог бы, затем показывать повреждение на стенке, регулируя W координату текстуры, где происходит повреждение. Применяя этот метод для ландшафта, видится простой способ применения этой трехмерной текстуры. Мы можем задать в этой текстуре на нижнем уровне текстуру воды, далее песок (или грязь), затем траву, камень и возможно в конце снег. Изображение ниже, показывает потенциальную 3D текстуру для ландшафта.

Трехмерная текстурная карта размером 64x64x32.

 

Как получить 'W'?

 

Теперь мы имеем текстурную карту с дополнительной координатой, и мы должны знать, как получить это значение 'W'. Это просто, однако, нам надо добавить третью координату текстуры к ранее рассмотренному алгоритму. Очевидный способ определения того, что является водой, травой, камнем и т.д., состоит в том, чтобы использовать высоту из карты высот, это, однако, может привести к ограничению задания деталей ландшафта. Я предпочитаю задавать дополнительную 'w' карту подобную карте высот (примечания переводчика: фактически это номер клетки). Мы можем создавать эту 'w'-карту, начиная с карты высот и редактируя ее, чтобы создать дополнительные детали, которые мы хотим. Мы могли бы, например, добавлять озера рядом с горами, Вы могли бы также добавить немного произвольного шума в плоские области ландшафта так, чтобы внести флуктуации подобных текстур.

 

Изображение внизу показывает готовый ландшафт, полученный наложением на карту 3D текстуры. Код, который выполнил это, включает и освещение.

Окончательный ландшафт с освещением.

То же самое изображение с наложением горизонтальных сканирующих линий.

 

Ограничения

 

Очень трудно добавить детали, не включенные в 3D текстуру, если, например, Вы хотите добавить дорогу, то это можно сделать или сверху полигонами, содержащими детали дороги или использовать наложение одной из существующих текстур со смешиванием ее с окружающей текстурой.

 

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

 

Ландшафт получается быстро и эффективно, но при этом не используются треугольники или полигоны. Это, однако, вызывает проблему, если Вы желаете также использовать аппаратные акселераторы. Я надеюсь рассказать методы визуализации ландшафта, используя аппаратные акселераторы в дальнейшем.

 

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

 

Добавления

 

Один метод внесения дополнительных деталей (и возможно небольшое повышение быстродействия) состоит в том, чтобы возвратиться к использованию одной большой карты текстуры. Вместо того чтобы использовать текстуру для всего ландшафта, мы используем текстуру возле текущего положения камеры. Оперируя большой текстурой (возможно 1024x1024) в подвижном окне, поскольку мы двигаемся по ландшафту, мы может добавить любые дополнительные детали, которые нам требуются. Поскольку камера перемещается, мы изменили бы данные на краях подвижного окна, возможно используя трехмерные карты текстур, как основа, и затем добавляя дороги и освещение сверху. Любые дополнительные данные могли бы быть добавлены в это время с небольшими затратами. В игровой среде можно добавить повреждения, ожоги и следы на лету к временной карте текстуры. Затем мы отбросили их, если мы сдвинулись достаточно далеко из области, а затем возвратились, но кому это надо в игре?

 

James Sharman

PMG  7 октября 2004 (c)  Сергей Анисимов