Кадр из создаваемой сценыЧто такое трехмерная графика и где она применяется, знает сейчас каждый ребенок. Самые популярные компьютерные игры, такие как DOOM, Quake, Counter Strike, наглядно демонстрируют модели трехмерных миров. Как их создают, какими они обладают возможностями и чем ограничены — вот что лежит в тени общеизвестного и представляет действительный интерес.
Существует несколько идеологически различных подходов к реализации 3D-графики. Одни из них предпочтительны для задач, требующих высокой скорости обработки, другие предоставляют потрясающе красивые и реалистичные результаты, третьи объединяют достоинства первых двух, но требуют самого современного оборудования. В этой статье мы внимательно рассмотрим один из подходов, а в дальнейшем раскроем секреты еще двух.
Классическим средством для решения задач трехмерного моделирования служит графическая библиотека OpenGL (Open Graphics Library). Эта библиотека представляет собой интерфейс к функциям графического процессора и тем самым обеспечивает достаточно эффективную (быструю) обработку запросов. Вопрос времени всегда очень остро стоит во всем, что связано с обработкой графики, тем более если речь идет об анимированных сюжетах, разработку которых мы и рассмотрим в этой статье. Вот почему весьма существенным как для создания, так и для просмотра этих проектов является наличие достаточно хорошей видеоплаты. Это не значит, что вам обязательно понадобится графический процессор GeForce последней модели, но мощности средств, встроенных по умолчанию в немолодой ноутбук, может не хватить.
Один из лучших способов чему-то научиться — просто сделать это. Давайте создадим сцену, представляющую собой пустыню, по которой катится перекати-поле, а затем начинается дождь.

Прежде чем начать
Библиотека OpenGL несовершенна. Убедиться в этом совсем несложно, так как на пути создания даже самого простого проекта вас ждут многочисленные тернии. Я хочу провести вас через них с минимальными потерями. Проект будет создаваться в MS Visual Studio 2003, но все то же самое можно повторить почти без изменений в любой другой среде разработки. Исключение составляют только Unix-подобные операционные системы и платформа Mac, библиотеки для которых по вполне понятным причинам выглядят иначе. Мы будем использовать язык разработки C++, но так как классовая структура вводиться не будет, можно считать, что это практически чистый Си.
Помимо базовой библиотеки, поставляющейся вместе с операционной системой, мы будем использовать очень удобное дополнение — библиотеку GLUT. Поэтому первым делом нужно позаботиться о правильном расположении всех необходимых файлов в системе (файлы можно взять на «Мир ПК-диске»).
Теперь создадим проект консольного приложения (VC++ Console Application). В созданный каталог, где лежат файлы проекта, добавим файл glut.h и присоединим его к проекту: File•Add Existing Item•glut.h add.

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

glColor3f(0.0,0.0,1.0);

изменит текущее значение цвета на синий. После выполнения этой команды все создаваемые объекты будут синего цвета.
Для обеспечения эффекта анимации, т.е. движения объектов и изменения их свойств, выполняется постоянное обновление картинки. Скорость перерисовки сцены (количество раз в секунду) зависит прежде всего от производительности графического процессора и, конечно, от возложенных на него задач. Соответственно меняется скорость воспроизведения. Чтобы обеспечить равномерное воспроизведение ролика, обновление можно искусственно замедлить, привязав его к системным часам.
Механизм обновления в цикле реализуется с использованием четырех функций:
void init(void); — это действия, выполняемые предварительно, до запуска основного цикла, например инициализация переменных, установка параметров;
void reshape(int,int); — это действия, связанные с масштабированием сцены, совершаемые при изменении размеров окна;
void display(void); — основная часть, прорисовка всех объектов, находящихся на сцене;
void idle(void); — функция вызывается на каждом витке цикла. Как правило, ее задача — это увеличение счетчика, являющегося эквивалентом времени, и вызов display.
Далее мы передаем библиотеке указатели на эти функции и запускаем основной цикл.
Файлы проекта и сам код (файл OpenGLSample.cpp) вы можете найти на прилагаемом к журналу «Мир ПК-диске».

Вывод основных форм
Вначале создадим небо и землю.
Для этого используем метод создания графических примитивов через наборы вершин. Координаты вершин задаются процедурой
glVertex3d(GLdouble x, GLdouble y, GLdouble z),
где GLdouble — тип, преобразующийся в double и обратно.
Вершины объединяются в объект с помощью конструкции:
glBegin(GLenum mode);
  glVertex3d(x1,y1,z1);
  ..
  glVertex3d(xn,yn,zn);
glEnd();

Здесь параметр mode определяет способ трактовки списка вершин. Вот некоторые из его значений:
GL_POINTS — создается набор точек;
GL_TRIANGLES — тройки вершин задают отдельные треугольники;
GL_QUADS — четверки вершин задают отдельные четырехугольники.

Замечание о координатах
Не углубляясь в теорию, можно сказать, что на самом деле нам безразличны абсолютные значения координат, а существенны лишь соотношения между ними. Проектировать сцену можно в любых наиболее удобных числах. Скорректировать положение камеры поможет процедура glViewPort().
Земля будет представлять собой прямоугольник, лежащий в плоскости y = 0, небо нам понадобится в виде двух прямоугольников — один в плоскости z = –length, другой горизонтально в плоскости z = height. Итак,

void DrawGround(){
  glColor3f(1.0,1.0,1.0);
  glBegin(GL_QUADS);
      glVertex3d (width, -1.0, length);
      glVertex3d (width, -1.0, -length);
      glVertex3d (-width, -1.0, -length);
      glVertex3d (-width, -1.0, length);

      glVertex3d (width, -1.0, -length);
      glColor3f(0.10,0.20,0.30);
      glVertex3d (width, height, -length);
      glVertex3d (-width, height, -length);
      glColor3f(0.35,0.65,0.99);
      glVertex3d (-width, -1.0, -length);

      glColor3f(0.10,0.20,0.30);
      glVertex3d(width, height, -length);
      glVertex3d (-width, height, -length);
      glVertex3d (-width, height, length);
      glVertex3d (width, height, length);
    glEnd ();
}
Здесь цвет задается в формате RGB в вещественных числах из отрезка [0,1]. При использовании прозрачности возможно задание цвета в формате RGBA с помощью процедуры glColor4d(r,g,b,alpha).
Собственно, на этом принципе — задание вершин и осмысление групп вершин как неких объектов — и основывается графическое моделирование. Для создания сложных объектов, таких как модели людей, облаков, травы, приходится задавать большое (иногда астрономическое) число вершин, причем вычисление координат делается вручную. Но не отчаивайтесь, разработки по автоматизации этого процесса уже ведутся.

Освещение
Под освещением понимается изменение цвета закрашивания объекта в зависимости от его расположения по отношению к источнику света. По умолчанию есть один источник рассеянного света, но можно добавлять и новые, со своим положением, углом рассеяния, цветом и другими параметрами. Освещение включается процедурой glEnable(GL_LIGHTING) и выключается с помощью glDisable(GL_LIGHTING).
Освещенность объекта, вид отбрасываемых им теней и его собственная затененность зависят от направления нормалей к вершинам, а также от материала объекта.
Нормали задаются процедурой glNormal3d(x,y,z), используемой перед glVertex3d().
Для присвоения объекту материала нужно перед прорисовкой вершин вызвать следующие процедуры:

   GLfloat emissive[4] = { 0.0f,0.0f,0.0f,0.0f};
   GLfloat diffuse[4] = { 0.98f,0.836f,0.35f,1.0f };
   GLfloat specular[4] = { 0.0f,0.0f,0.0f,0.0f };
   GLfloat ambient[4] = { 0.98f,0.836f,0.35f,1.0f };
   glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, emissive);
   glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, diffuse);
   glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular);
   glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, ambient);
   glMaterialf(GL_FRONT_AND_BACK,GL_SHININESS,10);
 glEnable(GL_COLOR_MATERIAL);

После прорисовки объекта нужно выключить параметр:

   glDisable(GL_COLOR_MATERIAL);

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

Текстурирование
Наложение текстуры делает объект реалистичным и красочным, а также позволяет добиться некоторых специальных эффектов. В качестве текстур можно использовать изображения в формате BMP. Мы будем применять функции загрузки текстур, реализация которых приведена в файле BmpLoad.cpp. Вот как проводится работа с текстурой:

  1. Создается массив textures[NTEXT] значений типа GLuint, куда с помощью процедуры glGenTextures(NTEXT, textures) записываются идентификаторы. По ним впоследствии будут вызываться «привязанные» к ним текстуры.
  2. В байтовый массив считывается содержимое файла, затем массив загружается во внутренний ресурс библиотеки (см. процедуру OpaqTexture), при этом устанавливается связь последнего с выбранным значением id идентификатора текстуры.
  3. При вызове glBindTexture (GL_TEXTURE_2D,id); в библиотеке текущим устанавливается именно этот текстурный объект.

Превратим землю в пустыню.
У нас есть BMP-файл с изображением песка. Загружаем его по приведенному выше алгоритму и «натягиваем» на прямоугольник земли. Для этого используем процедуру glTexCoord2d(i,j), где i и j — текстурные координаты, отображающие текстуру на квадрат [0,1]x[0,1]. Далее необходимо вызвать glVertex, и к этой вершине будет «приколота» указанная точка текстуры. Тогда она будет плавно «размазана» по большому прямоугольнику. Но нам нужно сделать так, чтобы текстура многократно размножилась по всей земле. Для этого надо рисовать землю не сплошным прямоугольником, а в виде матрицы, состоящей из маленьких прямоугольников, на каждый из которых «натянута» текстура. Будем описывать полосы прямоугольников, устанавливая GL_QUAD_ STRIP в glBegin. Это действие объединяет пару точек в отрезок, присоединяет их к предыдущей паре и трактует всю четверку как четырехугольник, затем присоединяет к нему следующую пару и т.д. В итоге получается полоса.
Описание земли выглядит следующим образом:

glNormal3d (0.0, 1.0, 0.0);
int k = (int)(width/length);
glBindTexture (GL_TEXTURE_2D,textures[0]);
for (int i = 0; i <= GRsl; i++){
  glBegin(GL_QUAD_STRIP);
  for (int j = 0; j <= GRsl*k; j++){
    glTexCoord2d(j%2,0);
    glVertex3d (-width + 2*width/((double)GRsl*k)*j, -1.0, length - 2*length/(double)GRsl*i);
    glTexCoord2d((j+1)%2,1);
    glVertex3d (-width + 2*width/((double)GRsl*k)*j, -1.0, length - 2*length/(double)GRsl*(i+1));
  }
  glEnd();
}

Здесь формируется GRsl полос прямоугольников.

Дождь — система частиц
Такие объекты, как огонь, дождь, облака, чаще всего моделируются с помощью большого числа точечных объектов — системы частиц.
Пусть в нашем дожде будет N капель. Первым делом произвольно раскидаем их по некоторому объему, находящемуся над небом. Чтобы дождь шел постепенно, а не одним пластом, капли должны иметь разную начальную высоту. Вычислим N произвольных координат внутри заданного объема и положим их в массив double3[N], где double3 = (double, double, double) — вектор, описанный в файле Vector.h. Пусть дождь начинается медленно, а не как из ведра, т.е. вначале упадет несколько первых капель, дальше больше. Будем опускать не все капли сразу, а только каждую десятую в списке. Количество вовлекаемых в процесс капель будем постепенно увеличивать, пока оно не достигнет значения N.
Сделаем дождь еще более эстетически приятным. Для этого будем рисовать капли не в виде точек, а уподобим их маленьким полупрозрачным сосудам. Будем рисовать их как прямоугольники, на которые наложена текстура с каналом прозрачности. Загрузка этой текстуры осуществляется процедурой AlphaTexture, принцип работы которой почти ничем не отличается от уже известной нам OpaqTexture. Различие состоит лишь в том, что цвета в массиве хранятся в формате RGBA. Параметр A (прозрачность) считывается из монохромной маски, которая также представляет собой файл BMP.
Перед выводом капель нужно включить обработку прозрачности:

   glBlendFunc(GL_ONE_MINUS_SRC_ALPHA,GL_SRC_ALPHA);
   glEnable(GL_BLEND);
      DrawRain();
glDisable(GL_BLEND);

Позаботимся еще об одной важной детали: так как капли полупрозрачные, то, чтобы добиться правдоподобного результата, будем выводить сначала самые дальние из них, а потом те, что ближе. Тогда они будут правильно просвечивать друг через друга. Нам нужно всего лишь отсортировать массив координат по z, т.е. по глубине. Можно воспользоваться стандартной процедурой сортировки qsort, для которой напишем простую функцию сравнения координат капель. Разумеется, сортировку нужно делать не в процедуре display, а в init, так как эта операция занимает ощутимое время, а произвести ее надо только один раз.

Перекати-поле и динамика
В нашей сцене не хватает истинно трехмерного объекта. Земля, небо, капли — это всего лишь прямоугольники. Создадим катящееся перекати-поле. Построим шар и наложим на него полупрозрачную текстуру сухих веточек. Нам повезло — можно не хвататься за учебник по линейной алгебре в поисках формулы для вычисления координаты точки на сфере, потому что сферу для нас уже реализовали. Это один из стандартных объектов GLUquadricObj, описанный в библиотеке glu, поэтому создать его и наложить на него текстуру очень просто:

ball = gluNewQuadric();
glBindTexture (GL_TEXTURE_2D,textures[1]);
gluQuadricTexture (ball, GL_TRUE);
glEnable(GL_CULL_FACE);
glCullFace(GL_FRONT);
gluSphere (ball,BLrad, BLsl, BLsl);
glCullFace(GL_BACK);
gluSphere (ball,BLrad, BLsl, BLsl);
glDisable(GL_CULL_FACE);

BLsl — число секторов, из которых строится сфера, оно характеризует качество ее округлости и, в обратной зависимости, скорость прорисовки.
Здесь, как и в случае с дождем, иллюстрируется очень важный момент — вывод на экран трехмерного объекта. В случае, если объект полупрозрачный или невыпуклый, мы должны выводить сначала его задние грани, а затем передние. Вопрос упорядочения граней на лицевые-нелицевые в общем случае довольно сложен, но, поскольку мы используем стандартный объект, для него упорядочение выполняется автоматически. Установка glCullFace(GL_ FRONT) включает вывод лицевых граней.
Зададимся еще одним важным вопросом: где будет нарисована эта сфера? Ведь ни одна из процедур не предоставила нам возможности указать координаты. Сфера появится в точке (0,0,0). Чтобы переместить ее в другое место, нужно узнать немного больше о работе OpenGL. На самом деле все, что мы рисуем в процедуре display, попадает не прямо на экран, а в специальный накопительный буфер. Их два, и в каждый момент времени один используется как накопитель, а содержимое другого выводится на экран. Процедура glutSwapBuffers() меняет буферы ролями, т.е. содержимое накопителя попадает на экран. Прежде чем точки окажутся в накопителе, все их координаты домножаются на матрицу GL_MODELVEW. Если она единичная, как задано по умолчанию, то с ними ничего не происходит. Но если домножить эту матрицу на матрицу поворота или переноса, тогда то же самое произойдет и с координатами точек. Именно так мы и реализуем движение и поворот перекати-поля вокруг своей оси.

Продолжим?
Мы создали целый мир — у него свои законы, уникальная природа. В него можно что-то добавить, населить его существами, наполнить прекрасными явлениями. И все это в ваших руках. Трехмерная графика — одна из самых увлекательных ветвей программирования, она, как ничто другое, подходит для реализации творческих идей, ваших фантазий. Программируйте с интересом.
Для получения более полной информации, реализации собственных проектов я рекомендую обратиться к книгам Е.В. Шикина и А.В. Борескова «Компьютерная графика. Полигональные модели» и Ю.М. Баяковского, А.В. Игнатенко, А.И. Фролова «Графическая библиотека OpenGL», откуда и были почерпнуты все изложенные здесь сведения. Документацию по библиотеке OpenGL и дополнениям к ней, а также примеры программ можно найти на сайте opengl.org.


Расположение необходимых файлов в системе
ФайлыРасположение
gl.h
glut.h
glu.h
[compiler]includegl
в нашем случае это C:Program Files
Microsoft Visual Studio .NET 2003Vc7PlatformSDKIncludegl
Opengl32.lib
glut32.lib
glu32.lib
[compiler]lib
в нашем случае это C:Program FilesMicrosoft Visual
Studio .NET 2003Vc7PlatformSDKLib
Opengl32.dll
glut32.dll
glu32.dll
C:WINDOWSsystem32
2596