Урок 11. Эффект "флага" на OpenGL

Всем привет. Для тех, кто хочет узнать, чем же мы тут занимаемся: эту вещь можно увидеть в конце моего демо/хака "Worthless!". Меня зовут Боско (Bosco), и я сделаю все, что в моих силах, чтобы научить вас, парни, делать анимированную картинку с синусоидальной волной по ней. Этот урок основан на уроке №6 от NeHe, и вы должны, по крайней мере, знать и уметь делать то, что в нем описано. Вы должны скачать архив с исходным кодом, распаковать его куда-нибудь, взять картинку из папки data и поместить в подпапку data той папки, где находится ваш исходник :). Ну, или использовать свою текстуру, если она подходящего для OpenGL размера.

  Перво-наперво откройте урок №6 для Visual C++ и добавьте этот #include сразу за другими. Данный #include позволит нам использовать различные библиотечные математические функции, как, например, синус и косинус.

 

#include <math.h>                                  // для функции Sin()

  Мы будем использовать массив точек (points) для хранения отдельных x, y и z - координат нашей сетки. Размер сетки 45x45, и она в свою очередь образует 44x44 квадрата. wiggle_count будет использоваться для определения того, насколько быстро "развевается" текстура. Каждые три кадра выглядят достаточно хорошо, и переменная hold будет содержать число с плавающей запятой для сглаживания волн. Эти строки можно добавить в начале программы, где-нибудь под последней строчкой с #include и перед строкой GLuint texture[1]:

 

float points[ 45 ][ 45 ][3]; // массив точек сетки нашей "волны"

int wiggle_count = 0;        // счетчик для контроля быстроты развевания флага

GLfloat hold;                // временно содержит число с плавающей запятой

  Перейдем вниз, к функции LoadGLTextures(). Мы хотим использовать текстуру с именем Tim.bmp. Найдите LoadBMP("Data/NeHe.bmp") и поменяйте на LoadBMP("Data/Tim.bmp").

 

if (TextureImage[0]=LoadBMP("Data/Tim.bmp"))       // загружаем изображение

  Теперь добавьте приведенный ниже код в конец функции InitGL() перед return TRUE:

 

glPolygonMode( GL_BACK, GL_FILL );       // нижняя (задняя) сторона заполнена

glPolygonMode( GL_FRONT, GL_LINE );      // верхняя (передняя) сторона прорисована линиями

 

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

Это мои личные предпочтения. Причина – ориентация полигона или направления граней. Загляните в Красную Книгу (по OpenGL), если хотите узнать больше. Пользуясь случаем, хочу сказать, что эта книга как раз из числа того, что помогает мне в изучении OpenGL, не считая сайта NeHe :). Спасибо NeHe. Купите книгу The Programmer's Guide to OpenGL издательства Addison-Wesley. Она – неисчерпаемый ресурс по части программирования на OpenGL. Ладно, вернемся к уроку.

  Прямо под предыдущим кодом и выше return TRUE добавьте следующие строки:

 

// пройдемся по плоскости X

      for(int x=0; x<45; x++)

      {

            // пройдемся по плоскости Y

            for(int y=0; y<45; y++)

            {

                  // применим волну к нашей сетке

                  points[x][y][0]=float((x/5.0f)-4.5f);

                  points[x][y][1]=float((y/5.0f)-4.5f);

                  points[x][y][2]=float(sin((((x/5.0f)*40.0f)/360.0f)*3.141592654*2.0f));

            }

      }

  Благодарю Грэма Гиббонса (Graham Gibbons) за замечание об целочисленном цикле для того чтобы избежать пиков на волне.

  Два цикла вверху инициализируют точки на нашей сетке. Я инициализирую переменные в моем цикле, чтобы помнить, что они просто переменные цикла. Не уверен, что это хорошо. Мы используем целочисленные циклы, дабы предупредить глюки, которые появляются при вычислениях с плавающей запятой. Мы делим переменные x и y на 5 (т.е. 45 / 9 = 5) и отнимаем 4.5 от каждой из них, чтобы отцентрировать "волну". Того же эффекта можно достигнуть переносом (translate), но этот способ мне больше нравится.

  Окончательное значение points[x][y][2] - это наше значение синуса. Функция sin() принимает параметр в радианах. Мы берем наши градусы, которые получились умножением float_x (x/5.0f) на 40.0f, и, чтобы конвертировать их в радианы делением, мы берем градусы, делим на 360.0f, умножаем на число пи (или на аппроксимацию) и на 2.0f.

  Я заново перепишу функцию DrawGLScene, так что удалите ее текст и замените его текстом, приведенным ниже.

 

int DrawGLScene(GLvoid)                          // рисуем нашу сцену

{

      int x, y;                                  // переменные циклов

      // для разбиения флага на маленькие квадраты

      float float_x, float_y, float_xb, float_yb;

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

 

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

glLoadIdentity();                                   // сброс текущей матрицы

 

glTranslatef(0.0f,0.0f,-12.0f);                     // перенести 17 единиц в глубь экрана

 

glRotatef(xrot,1.0f,0.0f,0.0f);                     // вращение по оси X

glRotatef(yrot,0.0f,1.0f,0.0f);                     // вращение по оси Y 

glRotatef(zrot,0.0f,0.0f,1.0f);                     // вращение по оси Z

 

glBindTexture(GL_TEXTURE_2D, texture[0]);           // выбрать нашу текстуру

  Вы могли видеть это все раньше. Все тут так же, как в уроке №6, кроме того, что я просто отвожу сцену от камеры чуть дальше.

 

glBegin(GL_QUADS);              // начинаем рисовать квадраты

      for( x = 0; x < 44; x++ ) // пройдемся по плоскости X 0-44 (45 точек)

      {

            for( y = 0; y < 44; y++ ) // пройдемся по плоскости Y 0-44 (45 точек)

            {

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

 

float_x = float(x)/44.0f;         // создать значение X как float

float_y = float(y)/44.0f;         // создать значение Y как float

float_xb = float(x+1)/44.0f;      // создать значение X как float плюс 0.0227f

float_yb = float(y+1)/44.0f;      // создать значение Y как float плюс 0.0227f

 

Мы используем четыре переменных выше для координат текстуры. Каждый наш полигон (квадрат в сетке) имеет секцию размером 1/44 x 1/44 с отображенной на нее текстурой. В начале циклов задается нижняя левая вершина секции, и потом мы просто добавляем к этой вершине соответственно 0 или 1 для получения трех остальных вершин (т.е. x+1 или y+1 будет правая верхняя вершина).

 

// первая координата текстуры (нижняя левая)

         glTexCoord2f( float_x, float_y);  

         glVertex3f( points[x][y][0], points[x][y][1], points[x][y][2] );

                 

      // вторая координата текстуры (верхняя левая)

         glTexCoord2f( float_x, float_yb );

         glVertex3f( points[x][y+1][0], points[x][y+1][1], points[x][y+1][2]);

 

// третья координата текстуры (верхняя правая)                

         glTexCoord2f( float_xb, float_yb );

glVertex3f( points[x+1][y+1][0], points[x+1][y+1][1], points[x+1][y+1][2]);

                 

            // четвертая координата текстуры (нижняя правая)

         glTexCoord2f( float_xb, float_y );

         glVertex3f( points[x+1][y][0], points[x+1][y][1], points[x+1][y][2]);

      }

}

glEnd();                                  // закончили с квадратами

  Строки выше просто делают OpenGL-вызовы для передачи всех данных, о которых мы говорили. Четыре отдельных вызова каждой функции glTexCoord2f() и glVertex3f(). Продолжим. Заметьте – квадраты рисуются по часовой стрелке. Это означает, что сторона, которую вы видите, вначале будет задней. Задняя заполнена. Передняя состоит из линий.

  Если бы вы рисовали против часовой стрелки, то сторона, которую вы бы видели вначале, была бы передней. Значит, вы увидели бы текстуру, выглядящую как сетка, (ты написал: типа сетки) вместо заполненной текстурой поверхности.

 

if( wiggle_count == 2 )      // для замедления волны (только каждый второй кадр)

      {

  Теперь мы хотим повторять значения синуса, тем самым создавая "движение".

 

      for( y = 0; y < 45; y++ )           // пройдемся по плоскости Y

      {

// сохраним текущее значение одной точки левой стороны волны

            hold=points[0][y][2];

            for( x = 0; x < 44; x++)      // пройдемся по плоскости X

            {

                 // текущее значение волны равно значению справа

                 points[x][y][2] = points[x+1][y][2];

            }

      // последнее значение берется из дальнего левого сохраненного значения

            points[44][y][2]=hold;

      }

      wiggle_count = 0;                    // снова сбросить счетчик

}

wiggle_count++;                            // увеличить счетчик

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

  Код вверху был изменен NeHe (в феврале 2000) для исправления недостатка волны. Волна теперь гладкая.

 

xrot+=0.3f;             // увеличить значение переменной вращения по X

yrot+=0.2f;             // увеличить значение переменной вращения по Y

zrot+=0.4f;             // увеличить значение переменной вращения по Z

 

return TRUE;            // возврат из функции

}

  Обычные значения поворота у NeHe. :) Вот и все. Скомпилируйте, и у вас должна появиться миленькая вращающаяся "волна". Я не знаю, что еще сказать, фу-у-ух… Это было так ДОЛГО! Но я надеюсь, вы, парни, сможете последовать этому, либо приобрести что-то полезное для себя. Если у вас есть вопросы, если вы хотите, чтобы я что-нибудь подправил или сказать мне, как, все-таки, жутко я пишу программы :), то пришлите мне письмецо.

  Это был экспромт, но он очень сэкономил время и силы. Он заставил меня ценить теперь взгляды NeHe гораздо больше. Спасибо всем.

 

© Bosco (bosco4@home.com)

 25 января 2002 (c)  Евгений Борисов