NeHe Tutorials Народный учебник по OpenGL
Урок 19. OpenGL

Particle Engine Using Triangle Strips

Добро пожаловать на урок 19. Вы многое узнали, и теперь слегка развлечься. Я познакомлю Вас только с одной новой командой в этом уроке... Полоски из треугольников (triangle strip). Это очень удобно, и поможет ускорить ваши программы, когда надо рисовать множество треугольников.

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

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

Вы можете мне не поверить, если я Вам скажу, что этот урок был написан на 100% с нуля. Я не заимствовал других идей, и я не имел никакой дополнительной технической информации. Я начал думать о частицах, и внезапно моя голова, наполнилась идеями (мозг включился?). Вместо того чтобы думать о каждой частице, как о пикселе, который был должен следовать от точки 'A' до точки 'B', и делать это или то, я решил, что будет лучше думать о каждой частице как об индивидуальном объекте, реагирующему на окружающую среду вокруг ее. Я дал каждой частице жизнь, случайное старение, цвет, скорость, гравитационное влияние и другое.

Вскоре я имел готовый проект. Я взглянул на часы, видимо инопланетяне снова забирали меня. Прошло 4 часа! Я помню, что время от времени пил кофе и закрывал глаза, но 4 часа...?

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

Этот урок использует код урока 1. Есть, однако, много нового кода, поэтому я будут переписывать любой раздел кода, который содержит изменения (это будет проще для понимания).

Используя код урока 1, мы добавим 5 новых строк кода в начало нашей программы. Первая строка (stdio.h) позволит нам читать данные из файлов. Такую же строку, мы добавили и к другим урокам, которые использовали текстуры. Во второй строке задается, сколько мы будем создавать частиц, и отображать на экране. MAX_PARTICLES будет равно любому значению, которое мы зададим. В нашем случае 1000. В третьей строке будет переключаться 'режим радуги' (включен или выключен). Мы установим по умолчанию включенным этот режим. sp и rp - переменные, которые мы будем использовать, чтобы предотвратить автогенерацию повторений нажатия клавиш пробел или ввод (enter), когда они нажаты.

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

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

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

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

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

#define MAX_PARTICLES 1000 // Число частиц для создания ( НОВОЕ )

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

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

HWND       hWnd=NULL; // Сохраняет дескриптор окна

HINSTANCE  hInstance; // Сохраняет экземпляр приложения

 

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

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

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


bool rainbow=true; // Режим радуги? ( НОВОЕ )
bool sp; // Пробел нажат? ( НОВОЕ )
bool rp; // Ввод нажат? ( НОВОЕ)

В следующих 4 строках - разнообразные переменные. Переменная slowdown (торможение) контролирует, как быстро перемещаются частицы. Чем больше ее значение, тем медленнее они двигаются. Чем меньше ее значение, тем быстрее они двигаются. Если значение задано маленькое, частицы будут двигаться слишком быстро! Скорость, с которой частиц перемещаются, будет задавать их траекторию движения по экрану. Более медленные частицы не будут улетать далеко. Запомните это.

Переменные xspeed и yspeed позволяют нам контролировать направлением хвоста потока частиц. xspeed будет добавляться к текущей скорости частицы по оси X. Если у xspeed - положительное значение, то наша частица будет смещаться направо. Если у xspeed - отрицательное значение, то наша частица будет смещаться налево. Чем выше значение, тем больше это смещение в соответствующем направлении. yspeed работает также, но по оси Y. Причина, по которой я говорю 'БОЛЬШЕ' в заданном направлении означает, что есть и другие коэффициенты, воздействующие на направление траектории частицы. xspeed и yspeed позволяют перемещать частицу в том направлении, в котором мы хотим.

Последняя переменная zoom. Мы используем эту переменную для панорамирования внутрь и вне нашей сцены. В машине моделирования частиц, это позволяет увеличить размер просмотра, или резко его сократить.

float slowdown=2.0f; // Торможение частиц
float xspeed; // Основная скорость по X (с клавиатуры изменяется направление хвоста)
float yspeed; // Основная скорость по Y (с клавиатуры изменяется направление хвоста)
float zoom=-40.0f; // Масштаб пучка частиц

Теперь мы задаем еще одну переменную названную loop. Мы будем использовать ее для задания частиц и вывода частиц на экран. col будет использоваться для сохранения цвета, с каким созданы частицы. delay будет использоваться, чтобы циклически повторять цвета в режиме радуги.

Наконец, мы резервируем память для одной текстуры (текстура частицы). Я решил использовать текстуру вместо точек по нескольким причинам. Наиболее важная причина, что точки замедляют быстродействие и выглядят очень плохо. Во-вторых, текстуры - более крутой способ :). Вы можете использовать квадратную частицу, крошечное изображение вашего лица, изображение звезды, и т.д.  Больше возможностей!

GLuint loop;       // Переменная цикла
GLuint col;        // Текущий выбранный цвет
GLuint delay;      // Задержка для эффекта радуги
GLuint texture[1]; // Память для нашей текстуры

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

Мы начинаем с булевой переменной active. Если эта переменная ИСТИННА, то наша частица жива и летит. Если она равно ЛОЖЬ, то наша частица мертва, или мы выключили ее! В этой программе я не использую active, но ее удобно иметь на всякий случай (прим. переводчика: никогда неизвестно что будет потом, главное следовать определенным принципам).

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

typedef struct // Структура частицы
{
  bool active; // Активность (Да/нет)
  float life;  // Жизнь
  float fade;  // Скорость угасания

Переменные r, g и b задают красную, зеленую и синюю яркости нашей частицы. Чем ближе r к 1.0f, тем более красной будет частица. Если все 3 переменных равны 1.0f, то это создаст белую частицу.

  float r; // Красное значение
  float g; // Зеленное значение
  float b; // Синие значение

Переменные x, y и z задают, где частица будет отображена на экране. x задает положение нашей частицы по оси X. y задает положение нашей частицы по оси Y, и, наконец, z задает положение нашей частицы по оси Z.

  float x; // X позиция
  float y; // Y позиция
  float z; // Z позиция

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

  float xi; // X направление
  float yi; // Y направление
  float zi; // Z направление

Наконец, последние 3 переменные! О каждой из этих переменных можно думать как о гравитации. Если xg имеет положительное значение, то нашу частицу будет притягивать вправо. Если отрицательное, то нашу частицу будет притягивать влево. Поэтому, если наша частица перемещает влево (отрицательно) и мы применяем положительную гравитацию, то скорость в итоге замедлится настолько, что наша частица начнет перемещать в противоположном направлении. yg притягивает вверх или вниз, и zg притягивает вперед или назад от зрителя.

  float xg; // X гравитация
  float yg; // Y гравитация
  float zg; // Z гравитация

particles - название нашей структуры.

}
particles; // Структура Частиц

Затем мы создаем массив называемый particle. Этот массив имеет размер MAX_PARTICLES. В переводе на русский язык: мы создаем память для хранения 1000 (MAX_PARTICLES) частиц. Это зарезервированная память будет хранить информацию о каждой индивидуальной частице.

particles particle[MAX_PARTICLES]; // Массив частиц (Место для информации о частицах)

Мы сокращаем код программы, при помощи запоминания наших 12 разных цветов в массиве цвета. Для каждого цвета от 0 до 11 мы запоминаем красную, зеленую, и, наконец, синею яркость. В таблице цветов ниже запомнено 12 различных цветов, которые постепенно изменяются от красного до фиолетового цвета.

static GLfloat colors[12][3]= // Цветовая радуга

{
  {1.0f,0.5f,0.5f},{1.0f,0.75f,0.5f},{1.0f,1.0f,0.5f},{0.75f,1.0f,0.5f},
  {0.5f,1.0f,0.5f},{0.5f,1.0f,0.75f},{0.5f,1.0f,1.0f},{0.5f,0.75f,1.0f},
  {0.5f,0.5f,1.0f},{0.75f,0.5f,1.0f},{1.0f,0.5f,1.0f},{1.0f,0.5f,0.75f}
};

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

Код загрузки картинки не изменился.

AUX_RGBImageRec *LoadBMP(char *Filename)     // Загрузка картинки

{

 FILE *File=NULL;          // Индекс файла

 

 if (!Filename)            // Проверка имени файла

 {

  return NULL;             // Если нет вернем NULL

 }

 

 File=fopen(Filename,"r"); // Проверим существует ли файл

 

 if (File)                 // Файл существует?

 {

  fclose(File);            // Закрыть файл

  return auxDIBImageLoad(Filename); // Загрузка картинки и вернем на нее указатель

 }

 return NULL;              // Если загрузка не удалась вернем NULL

}

 

В этом разделе кода загружается картинка (вызов кода выше) и конвертирует его в текстуру. Status используется за тем, чтобы отследить, действительно ли текстура была загружена и создана.

int LoadGLTextures()                      // Загрузка картинки и конвертирование в текстуру

{

 int Status=FALSE;                        // Индикатор состояния

 

 AUX_RGBImageRec *TextureImage[1];        // Создать место для текстуры

 

 memset(TextureImage,0,sizeof(void *)*1); // Установить указатель в NULL

 

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

if (TextureImage[0]=LoadBMP("Data/Particle.bmp")) // Загрузка текстуры частицы

{

  Status=TRUE; // Задать статус в TRUE
  glGenTextures(1, &texture[0]); // Создать одну текстуру

  glBindTexture(GL_TEXTURE_2D, texture[0]);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
  glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY,

    0, GL_RGB,   GL_UNSIGNED_BYTE, TextureImage[0]->data);
}

if (TextureImage[0])            // Если текстура существует

{

 if (TextureImage[0]->data)     // Если изображение текстуры существует

 {

   free(TextureImage[0]->data); // Освобождение памяти изображения текстуры

 }

 free(TextureImage[0]);         // Освобождение памяти под структуру

}

 

  return Status;        // Возвращаем статус

}

Единственное изменение, которое я сделал в коде изменения размера, было увеличение области просмотра. Вместо 100.0f, мы можем теперь рассматривать частицы на 200.0f единиц в глубине экрана.

// Изменение размеров и инициализация окна GL

GLvoid ReSizeGLScene(GLsizei width, GLsizei height)
{
  if (height==0) // Предотвращение деления на ноль, если окно слишком мало
  {
    height=1; // Сделать высоту равной единице
  }


  //Сброс текущей области вывода и перспективных преобразований
  glViewport(0, 0, width, height);

  glMatrixMode(GL_PROJECTION); // Выбор матрицы проекций
  glLoadIdentity(); // Сброс матрицы проекции

  // Вычисление соотношения геометрических размеров для окна
  gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,200.0f); // ( МОДИФИЦИРОВАНО )

  glMatrixMode(GL_MODELVIEW); // Выбор матрицы просмотра модели
  glLoadIdentity(); // Сброс матрицы просмотра модели
}

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

int InitGL(GLvoid)       // Все начальные настройки OpenGL здесь
{
  if (!LoadGLTextures()) // Переход на процедуру загрузки текстуры
  {
    return FALSE;        // Если текстура не загружена возвращаем FALSE
  }

Мы разрешаем плавное затенение, очищаем фон черным цветом, запрещаем тест глубины, разрешаем смешивание и наложение текстуры. После разрешения наложения текстуры мы выбираем нашу текстуру частицы.

  glShadeModel(GL_SMOOTH);    // Разрешить плавное затенение

  glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Черный фон

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

  glDisable(GL_DEPTH_TEST);   // Запрещение теста глубины

  glEnable(GL_BLEND);         // Разрешаем смешивание
  glBlendFunc(GL_SRC_ALPHA,GL_ONE); // Тип смешивания

  // Улучшенные вычисления перспективы
  glHint(GL_PERSPECTIVE_CORRECTION_HINT,GL_NICEST);
  glHint(GL_POINT_SMOOTH_HINT,GL_NICEST);  // Улучшенные точечное смешение
  glEnable(GL_TEXTURE_2D);                 // Разрешение наложения текстуры
  glBindTexture(GL_TEXTURE_2D,texture[0]); // Выбор нашей текстуры

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

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

for (loop=0;loop<MAX_PARTICLES;loop++) // Инициализация всех частиц
{
  particle[loop].active=true; // Сделать все частицы активными
  particle[loop].life=1.0f;   // Сделать все частицы с полной жизнью

Мы задаем, как быстро частица угасает, при помощи присвоения fade случайного значения. Переменная life будет уменьшаться на значение fade, каждый раз, после того как частица будет отображена. Случайное значение будет от 0 до 99. Его мы делим его на 1000, поэтому мы получим очень маленькое значение с плавающей запятой. В завершении мы добавим 0.003 к конечному результату так, чтобы скорость угасания никогда не равнялось 0.

  //Случайная скорость угасания
  particle[loop].fade=float(rand()%100)/1000.0f+0.003f;

 

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

Вот пример: 900 * (12/900) =12. 1000 * (12/1000) =12, и т.д.

  particle[loop].r=colors[loop*(12/MAX_PARTICLES)][0]; // Выбор красного цвета радуги
  particle[loop].g=colors[loop*(12/MAX_PARTICLES)][1]; // Выбор зеленного цвета радуги
  particle[loop].b=colors[loop*(12/MAX_PARTICLES)][2]; // Выбор синего цвета радуги

Теперь мы зададим направление, в котором каждая частица двигается, наряду со скоростью. Мы умножаем результат на 10.0f, чтобы создать впечатление взрыва, когда программа запускается.

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

  particle[loop].xi=float((rand()%50)-26.0f)*10.0f; // Случайная скорость по оси X
  particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Y
  particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Z

Наконец, мы задаем величину гравитации, которая воздействует на каждую частицу. В отличие от реальной гравитации, под действием которой все предметы падают вниз, наша гравитация сможет смещать частицы вниз, влево, вправо, вперед или назад (прим. переводчика: скорее всего это электромагнитное поле, а не гравитация). Вначале мы зададим гравитацию в полсилы, которая притягивает вниз. Чтобы сделать это, мы устанавливаем xg в 0.0f. Т.е. нет перемещения влево или вправо по плоскости X. Мы устанавливаем yg в -0.8f. Это создает притяжение вниз в полсилы. Если значение положительное, то притяжение вверх. Мы не хотим, чтобы частицы притягивались к нам или от нас, поэтому мы установим zg в 0.0f.

  particle[loop].xg=0.0f;  // Зададим горизонтальное притяжение в ноль
  particle[loop].yg=-0.8f; // Зададим вертикальное притяжение вниз
  particle[loop].zg=0.0f;  // зададим притяжение по оси Z в ноль
}
return TRUE; // Инициализация завершена OK
}

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

Мы сбрасываем матрицу просмотра модели только однажды. Мы позиционируем частицы, используя команду glVertex3f() вместо использования перемещения их, при таком способе вывода частиц мы не изменяем матрицу просмотра модели при выводе наших частиц.

int DrawGLScene(GLvoid) // Здесь мы все рисуем
{
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистка экрана и буфера глубины

  glLoadIdentity(); // Сброс матрицы просмотра модели

Мы начинаем наш вывод с цикла. Этот цикл обновит каждую из наших частиц.

  for (loop=0;loop<MAX_PARTICLES;loop++) // Цикл по всем частицам
  {

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

    if (particle[loop].active) // Если частицы не активны
    {

Следующие три переменные x, y и z - временные переменные, которые мы будем использовать, чтобы запомнить позицию частицы по x, y и z. Отмечу, что мы добавляем zoom к позиции по z, так как наша сцена смещена в экран на значение zoom. particle[loop].x - это наша позиция по x для любой частицы, которую мы выводим в цикле. particle[loop].y - это наша позиция по y для нашей частицы, и particle[loop].z - это наша позиция по z.

      float x=particle[loop].x; // Захватим позицию X нашей частицы
      float y=particle[loop].y; // Захватим позицию Н нашей частицы
      float z=particle[loop].z+zoom; // Позиция частицы по Z + Zoom

Теперь, когда мы имеем позицию частицы, мы можем закрасить частицу. particle[loop].r - это красная яркость частицы, particle[loop].g – это зеленая яркость, и particle[loop].b – это синяя яркость. Напомню, что я использую жизнь частицы (life) для альфа значения. По мере того, как частица умирает, она становится все более и более прозрачной, пока она, в конечном счете, не исчезнет. Именно поэтому, жизнь частиц никогда не должна быть больше чем 1.0f. Если Вы хотите, чтобы частицы горели более долго, пробуйте уменьшить скорость угасания так, чтобы частица не так быстро исчезла.

      // Вывод частицы, используя наши RGB значения, угасание частицы согласно её жизни
      glColor4f(particle[loop].r,particle[loop].g,particle[loop].b,particle[loop].life);

Мы задали позицию частицы и цвет. Все, что мы должны теперь сделать - вывести нашу частицу. Вместо использования текстурированного четырехугольника, я решил использовать текстурированную полоску из треугольников, чтобы немного ускорить программу. Некоторые 3D платы могут выводить треугольники намного быстрее чем, они могут выводить четырехугольники. Некоторые 3D платы конвертируют четырехугольник в два треугольника за Вас, но некоторые платы этого не делают. Поэтому мы сделаем эту работу сами. Мы начинаемся с того, что сообщаем OpenGL, что мы хотим вывести полоску из треугольников.

      glBegin(GL_TRIANGLE_STRIP); // Построение четырехугольника из треугольной полоски

Цитата непосредственно из красной книги (OpenGL Red Book): полоска из треугольников рисуется как ряд треугольников (трехсторонних полигонов) используя вершины V0, V1, V2, затем V2, V1, V3 (обратите внимание на порядок), затем V2, V3, V4, и так далее. Порядок должен гарантировать, что все треугольники будут выведены с той же самой ориентацией так, чтобы полоска могла правильно формировать часть поверхности. Соблюдение ориентации важно для некоторых операций, типа отсечения. Должны быть, по крайней мере, 3 точки, чтобы было что-то выведено.


Рисунок 1. Полоска из двух треугольников

 

Поэтому первый треугольник выведен, используя вершины 0, 1 и 2. Если Вы посмотрите на рисунок 1, Вы увидите, что точки вершин 0, 1 и 2 действительно составляют первый треугольник (верхняя правая, верхняя левая, нижняя правая). Второй треугольник выведен, используя вершины 2, 1 и 3. Снова, если Вы посмотрите на рисунок 1, вершины 2, 1 и 3 создают второй треугольник (нижняя правая, верхняя левая, нижняя правая). Заметьте, что оба треугольника выведены с тем же самым порядком обхода (против часовой стрелки). Я видел несколько сайтов, на которых заявлялось, что каждый второй треугольник должен быть в противоположном направлении. Это не так. OpenGL будет менять вершины, чтобы гарантировать, что все треугольники выведены тем же самым способом!

Есть две хорошие причины, для того чтобы использовать полоски из треугольников. Во-первых, после определения первых трех вершин начального треугольника, Вы должны только определять одну единственную точку для каждого другого дополнительного треугольника. Эта точка будет объединена с 2 предыдущим вершинами для создания треугольника. Во-вторых, сокращая количество данных, необходимых для создания треугольников ваша программа будет работать быстрее, и количество кода или данных, требуемых для вывода объекта резко сократиться.

Примечание: число треугольников, которые Вы видите на экране, будет равно числу вершин, которые Вы зададите минус 2. В коде ниже мы имеем 4 вершины, и мы видим два треугольника.

      glTexCoord2d(1,1); glVertex3f(x+0.5f,y+0.5f,z); // Верхняя правая
      glTexCoord2d(0,1); glVertex3f(x-0.5f,y+0.5f,z); // Верхняя левая
      glTexCoord2d(1,0); glVertex3f(x+0.5f,y-0.5f,z); // Нижняя правая
      glTexCoord2d(0,0); glVertex3f(x-0.5f,y-0.5f,z); // Нижняя левая

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

      glEnd(); // Завершение построения полоски треугольников

Теперь мы можем переместить частицу. Математически это может выглядеть несколько странно, но довольно просто. Сначала мы берем текущую позицию x частицы. Затем мы добавляем значение смещения частицы по x, деленной на slowdown/1000. Поэтому, если наша частица была в центре экрана на оси X (0), наша переменная смещения (xi) для оси X равна +10 (смещение вправо от нас) и slowdown было равно 1, мы сместимся направо на 10/(1*1000), или на 0.01f. Если мы увеличим slowdown на 2, мы сместимся только на 0.005f. Буду надеяться, что это поможет Вам понять, как работает замедление (slowdown).

Это также объясняет, почему умножение начальных значений на 10.0f заставляет пиксели перемещаться намного быстрее, создавая эффект взрыва.

Мы используем ту же самую формулу для осей y и z, для того чтобы переместить частицу по экрану.

      // Передвижение по оси X на скорость по X

      particle[loop].x+=particle[loop].xi/(slowdown*1000);
      // Передвижение по оси Y на скорость по Y

      particle[loop].y+=particle[loop].yi/(slowdown*1000);
      // Передвижение по оси Z на скорость по Z

      particle[loop].z+=particle[loop].zi/(slowdown*1000);

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

Предположим, что скорость перемещения равна 10, а сопротивление равно 1. Первый раз, когда частица выводиться на экран, сопротивление воздействует на нее. Во второй раз, когда она выводится, сопротивление будет действовать, и скорость перемещения понизится от 10 до 9. Это заставит частицу немного замедлится. В третий раз, когда частица выводиться, сопротивление действует снова, и скорость перемещения понизится до 8. Если бы частица горела больше чем 10 перерисовок, то она будет в итоге перемещаться в противоположном направлении, потому что скорость перемещения станет отрицательным значением.

Сопротивление применяется к скорости перемещения по y и z, так же, как и по x.

      particle[loop].xi+=particle[loop].xg; // Притяжение по X для этой записи
      particle[loop].yi+=particle[loop].yg; // Притяжение по Y для этой записи
      particle[loop].zi+=particle[loop].zg; // Притяжение по Z для этой записи

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

      particle[loop].life-=particle[loop].fade; // Уменьшить жизнь частицы на ‘угасание’

Теперь мы проверим, жива ли частица, после того как мы изменили ее жизнь.

      if (particle[loop].life<0.0f) // Если частица погасла
      {

Если частица мертва (сгорела), мы оживим ее. Мы сделаем это, задав ей полную жизнь и новую скорость угасания.

        particle[loop].life=1.0f; // Дать новую жизнь
        // Случайное значение угасания
        particle[loop].fade=float(rand()%100)/1000.0f+0.003f;

Мы также сделаем сброс позиций частицы в центр экрана. Мы делаем это, при помощи сброса позиций x, y и z частицы в ноль.

        particle[loop].x=0.0f; // На центр оси X
        particle[loop].y=0.0f; // На центр оси Y
        particle[loop].z=0.0f; // На центр оси Z

После того, как частица была сброшена в центр экрана, мы задаем ей новую скорость перемещения / направления. Отмечу, что я увеличил максимальную и минимальную скорость, с которой частица может двигаться со случайного значения в диапазоне 50 до диапазона 60, но на этот раз, мы не собирается умножать скорость перемещения на 10. Мы не хотим взрыва на этот раз, мы хотим иметь более медленно перемещающиеся частицы.

Также заметьте, что я добавил xspeed к скорости перемещения по оси X, и yspeed к скорости перемещения по оси Y. Это позволит нам позже контролировать, в каком направлении двигаются частицы.

        particle[loop].xi=xspeed+float((rand()%60)-32.0f);//Скорость и направление по оси X
        particle[loop].yi=yspeed+float((rand()%60)-30.0f);//Скорость и направление по оси Y
        particle[loop].zi=float((rand()%60)-30.0f);       //Скорость и направление по оси Z

Наконец мы назначаем частице новый цвет. В переменной col содержится число от 0 до 11 (12 цветов). Мы используем эту переменную для извлечения красной, зеленой и синей яркостей из нашей таблицы цветов, которую мы сделали в начале программы. В первой строке ниже задается красная яркость (r) согласно значению красного, сохраненного в colors[col][0]. Поэтому, если бы col равен 0, красная яркость равна 1.0f. Зеленые и синие значения получаются таким же способом.

Если Вы не поняли, как я получил значение 1.0f для красной яркости, если col - 0, я объясню это немного более подробно. Смотрите в начало программы. Найдите строку: static GLfloat colors[12][3]. Запомните, что есть 12 групп по 3 числа. Первые три числа - красная яркость. Второе значение - зеленая яркость, и третье значение - синяя яркость. [0], [1] и [2] ниже являются 1-ым, 2-ым и 3-ьим значениями, которые я только что упомянул. Если col равен 0, то мы хотим взглянуть на первую группу. 11 – последняя группа (12-ый цвет).

        particle[loop].r=colors[col][0]; // Выбор красного из таблицы цветов
        particle[loop].g=colors[col][1]; // Выбор зеленого из таблицы цветов
        particle[loop].b=colors[col][2]; // Выбор синего из таблицы цветов
      }

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

      // Если клавиша 8 на цифровой клавиатуре нажата и гравитация меньше чем 1.5

      // тогда увеличим притяжение вверх
      if (keys[VK_NUMPAD8] && (particle[loop].yg<1.5f)) particle[loop].yg+=0.01f;

Эта строка создает точно противоположный эффект. При помощи нажатия 2 на цифровой клавиатуре мы уменьшаем yg, создавая более сильное притяжение вниз.

      // Если клавиша 2 на цифровой клавиатуре нажата и гравитация больше чем -1.5

      // тогда увеличим притяжение вниз
      if (keys[VK_NUMPAD2] && (particle[loop].yg>-1.5f)) particle[loop].yg-=0.01f;

Теперь мы модифицируем притяжение вправо. Если клавиша 6 на цифровой клавиатуре нажата, то мы увеличиваем притяжение вправо.

      // Если клавиша 6 на цифровой клавиатуре нажата и гравитация меньше чем 1.5

      // тогда увеличим притяжение вправо
      if (keys[VK_NUMPAD6] && (particle[loop].xg<1.5f)) particle[loop].xg+=0.01f;

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

      // Если клавиша 4 на цифровой клавиатуре нажата и гравитация больше чем -1.5

      // тогда увеличим притяжение влево
      if (keys[VK_NUMPAD4] && (particle[loop].xg>-1.5f)) particle[loop].xg-=0.01f;

Я добавил этот небольшой код только для развлечения. Мой брат думает, что взрыв интересный эффект :). При помощи нажатия клавиши табуляции все частицы будут отброшены назад к центру экрана. Скорость перемещения частиц будет еще раз умножена на 10, создавая большой взрыв частиц. После того, как частицы взрыва постепенно исчезнут, появиться предыдущий столб частиц.

      if (keys[VK_TAB]) // Клавиша табуляции вызывает взрыв
      {
        particle[loop].x=0.0f; // Центр по оси X
        particle[loop].y=0.0f; // Центр по оси Y
        particle[loop].z=0.0f; // Центр по оси Z
        particle[loop].xi=float((rand()%50)-26.0f)*10.0f; // Случайная скорость по оси X
        particle[loop].yi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Y
        particle[loop].zi=float((rand()%50)-25.0f)*10.0f; // Случайная скорость по оси Z
      }
    }
  }
  return TRUE; // Все OK
}

Код в KillGLWindow(), CreateGLWindow() и WndProc() не изменился, поэтому мы перейдем к WinMain(). Я повторю весь этот раздел кода, чтобы сделать просмотр кода проще.

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's Particle Tutorial",640,480,16,fullscreen))

  {

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

  }

 

Далее наше первое изменение в WinMain(). Я добавил код, который проверяет, в каком режиме пользователь решил запустить программу - в полноэкранном режиме или в окне. Если  используется полноэкранный режим, я изменяю переменную slowdown на 1.0f вместо 2.0f. Вы можете опустить этот небольшой код, если Вы хотите. Я добавил этот код, чтобы ускорить полноэкранный режим на моем 3dfx (потому что при этом выполнение программы намного медленнее, чем в режиме окна по некоторым причинам).

  if (fullscreen)  // Полноэкранный режим ( ДОБАВЛЕНО )
  {
    slowdown=1.0f; // Скорость частиц (для 3dfx) ( ДОБАВЛЕНО )
  }

  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 // Не время выходить, обновляем экран

      {

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

 

Я немного попотел со следующим куском кода. Обычно я не включаю все в одну строку, и это делает просмотр кода немного яснее :).

В строке ниже проверяется, нажата ли клавиша ‘+’ на цифровой клавиатуре. Если она нажата, и slowdown больше чем 1.0f, то мы уменьшаем slowdown на 0.01f. Это заставит частицы двигаться быстрее. Вспомните, что я говорил выше о торможении и как оно воздействует на скорость, с которой частица перемещается.

        if (keys[VK_ADD] && (slowdown>1.0f)) slowdown-=0.01f;//Скорость частицы увеличилась

В этой строке проверяется, нажата ли клавиша ‘-‘ на цифровой клавиатуре. Если она нажата, и slowdown меньше чем 4.0f, то мы увеличиваем slowdown. Это заставляет частицы двигаться медленнее. Я выставил предел в 4.0f, потому что я не хочу, чтобы они двигались очень медленно. Вы можете изменить минимальные и максимальные скорости, на какие Вы хотите :).

        if (keys[VK_SUBTRACT] && (slowdown<4.0f)) slowdown+=0.01f; // Торможение частиц

В строке ниже проверяется, нажата ли клавиша PAGE UP. Если она нажата, то переменная zoom увеличивается. Это заставит частицы двигаться ближе к нам.

        if (keys[VK_PRIOR]) zoom+=0.1f; // Крупный план

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

        if (keys[VK_NEXT]) zoom-=0.1f; // Мелкий план

В следующей секции кода происходит проверка, была ли нажата клавиша Enter. Если она нажата в первый раз, и она не удерживается уже некоторое время, мы позволим компьютеру узнать, что она нажата, устанавливая rp в true. Тем самым мы переключим режим радуги. Если радуга была true, она станет false. Если она была false, то станет true. В последней строке проверяется, была ли клавиша Enter отпущена. Если это так, то rp устанавливается в false, сообщая компьютеру, что клавиша больше не нажата.

        if (keys[VK_RETURN] && !rp) // нажата клавиша Enter
        {
          rp=true; // Установка флага, что клавиша нажата
          rainbow=!rainbow; // Переключение режима радуги в Вкл/Выкл
        }
        if (!keys[VK_RETURN]) rp=false; // Если клавиша Enter не нажата – сбросить флаг

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

Если клавиша пробел была нажата, или радуга включена, и задержка больше чем 25, цвет будет изменен!

        if ((keys[' '] && !sp) || (rainbow && (delay>25))) // Пробел или режим радуги
        {

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

          if (keys[' ']) rainbow=false; // Если пробел нажат запрет режима радуги

Если клавиша пробел была нажата, или режим радуги включен, и задержка больше чем 25, мы позволим компьютеру узнать, что пробел было нажат, делая sp равной true. Затем мы зададим задержку равной 0, чтобы снова начать считать до 25. Наконец мы увеличим переменную col, чтобы цвет изменился на следующий цвет в таблице цветов.

          sp=true; // Установка флага нам скажет, что пробел нажат
          delay=0; // Сброс задержки циклической смены цветов радуги
          col++;   // Изменить цвет частицы

Если цвет больше чем 11, мы сбрасываем его обратно в ноль. Если бы мы не сбрасывали col в ноль, наша программа попробовала бы найти 13-ый цвет. А мы имеем только 12 цветов! Попытка получить информацию о цвете, который не существует, привела бы к краху нашей программы.

          if (col>11) col=0; // Если цвет выше, то сбросить его
        }

Наконец, если клавиша пробел больше не нажата, мы позволяем компьютеру узнать это, устанавливая переменную sp в false.

        if (!keys[' ']) sp=false; // Если клавиша пробел не нажата, то сбросим флаг

Теперь внесем немного управления нашими частицами. Помните, что мы создали 2 переменные в начале нашей программы? Одна называлась xspeed, и вторая называлась yspeed. Также Вы помните, что после того как частица сгорит, мы давали ей новую скорость перемещения и добавляли новую скорость или к xspeed или к yspeed. Делая это, мы можем повлиять, в каком направлении частицы будут двигаться, когда они впервые созданы.

Например. Пусть частица имеет скорость перемещения 5 по оси X и 0 по оси Y. Если мы уменьшим xspeed до -10, то скорость перемещения будет равна -10 (xspeed) +5 (начальная скорость). Поэтому вместо перемещения с темпом 10 вправо, частица будет перемещаться с темпом -5 влево Понятно?

Так или иначе. В строке ниже проверяем, нажата ли стрелка "вверх". Если это так, то yspeed будет увеличено. Это заставит частицы двигаться вверх. Частицы будут двигаться вверх с максимальной скоростью не больше чем 200. Если бы они двигались быстрее этого значения, то это выглядело бы не очень хорошо.

        //Если нажата клавиша вверх и скорость по Y меньше чем 200, то увеличим скорость
        if (keys[VK_UP] && (yspeed<200)) yspeed+=1.0f;

В этой строке проверяем, нажата ли клавиша стрелка "вниз". Если это так, то yspeed будет уменьшено. Это заставит частицу двигаться вниз. И снова, задан максимум скорости вниз не больше чем 200.

        // Если стрелка вниз и скорость по Y больше чем –200, то увеличим скорость падения
        if (keys[VK_DOWN] && (yspeed>-200)) yspeed-=1.0f;

Теперь мы проверим, нажата ли клавиша стрелка вправо. Если это так, то xspeed будет увеличено. Это заставит частицы двигаться вправо. Задан максимум скорости не больше чем 200.

        // Если стрелка вправо и X скорость меньше чем 200, то увеличить скорость вправо
        if (keys[VK_RIGHT] && (xspeed<200)) xspeed+=1.0f;

Наконец мы проверим, нажата ли клавиша стрелка влево. Если это так... то Вы уже поняли что... xspeed уменьшено, и частицы двигаются влево. Задан максимум скорости не больше чем 200.

        // Если стрелка влево и X скорость больше чем –200, то увеличить скорость влево
        if (keys[VK_LEFT] && (xspeed>-200)) xspeed-=1.0f;

И последнее, что мы должны сделать - увеличить переменную delay. Я уже говорил, что delay управляет скоростью смены цветов, когда Вы используете режим радуги.

        delay++; // Увеличить счетчик задержки циклической смены цветов в режиме радуги

Так же как и во всех предыдущих уроках, проверьте, что заголовок сверху окна правильный.

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

        {

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

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

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

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

          if (!CreateGLWindow("NeHe's Particle Tutorial",640,480,16,fullscreen))

          {

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

 

          }

        }

      }

    }

  }

 

  // Сброс

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

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

}

 

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

Благодарю Richard Nutman за предложение о том, что частицы можно позиционировать с помощью glVertex3f() вместо сброса матрицы модели просмотра и перепозиционирования каждой частицы с помощью glTranslatef(). Оба метода эффективны, но его метод уменьшил количество вычислений для вывода каждой частицы, что вызвало увеличение быстродействия программы.

Благодарю Antoine Valentim за предложение использовать полоски из треугольников для ускорения программы и введения новой команды в этом уроке. Замечания к этому уроку были великолепными, и я признателен за это!

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

© Jeff Molofee (NeHe)

PMG  9 августа 2002 (c)  Сергей Анисимов